forked from CopilotKit/CopilotKit
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathframework-provider.tsx
More file actions
201 lines (182 loc) · 6.95 KB
/
Copy pathframework-provider.tsx
File metadata and controls
201 lines (182 loc) · 6.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
"use client";
// FrameworkProvider — tracks the currently "active" agentic backend.
//
// Three fields, three different jobs:
//
// - `framework` — STRICTLY URL-derived. Non-null only on `/<framework>/...`
// routes. Use this when chrome legitimately needs to know "is the URL
// scoped to a framework?" (e.g. RouterPivot's redirect-target logic, or
// a banner that only shows when the URL is genuinely scoped).
//
// - `storedFramework` — last REMEMBERED choice from localStorage. Use to
// mark the user's last pick in a picker UI (e.g. ring around their card)
// without treating it as the active selection.
//
// - `effectiveFramework` — what the page renders as. Falls back through
// URL → stored → DEFAULT_FRAMEWORK so it's never null. This is the
// field every snippet renderer, sidebar link, and "Continue with X"
// pointer should read. Treating no-choice as Built-in Agent removes
// the dead-end where fresh visitors saw a forced picker before any
// working code.
//
// Whenever the URL asserts a framework, we persist it as the new
// storedFramework so the preference carries.
import React, {
createContext,
useContext,
useEffect,
useMemo,
useState,
} from "react";
import { usePathname } from "next/navigation";
export interface FrameworkContextValue {
/**
* URL-derived framework. Non-null only on `/<framework>/...` routes.
* Use only when chrome needs to know "is the URL scoped to a
* framework?" (e.g. redirect logic). For rendering content, prefer
* `effectiveFramework`.
*/
framework: string | null;
/**
* Last remembered framework from localStorage — advisory only. Use to
* mark the user's last pick in a picker UI without implying the
* current view is scoped to it.
*/
storedFramework: string | null;
/**
* Framework the page should render as — never null. Falls through
* URL → stored → DEFAULT_FRAMEWORK. Snippet renderers, sidebar links,
* and "Continue with X" affordances should read this.
*/
effectiveFramework: string;
/** All known framework slugs derived from the registry. */
knownFrameworks: string[];
/** Persist a new framework preference (does not navigate). */
setStoredFramework: (slug: string | null) => void;
}
const FrameworkContext = createContext<FrameworkContextValue | null>(null);
const STORAGE_KEY = "selectedFramework";
/**
* Built-in Agent is the default integration: zero config, runs in-process
* via the Next.js runtime, no external agent server. Treating fresh
* visitors as if they'd picked it removes the forced-picker dead end and
* gives them working code on first paint.
*/
export const DEFAULT_FRAMEWORK = "built-in-agent";
// Log each failure mode once per session so we don't spam the console
// on repeated reads/writes when localStorage is unavailable (SSR, private
// mode, storage disabled), but we still surface the failure to devs the
// first time it happens.
let readLogged = false;
let writeLogged = false;
function readStoredFramework(): string | null {
if (typeof window === "undefined") return null;
try {
return window.localStorage.getItem(STORAGE_KEY);
} catch (err) {
if (!readLogged) {
console.warn("[framework-provider] localStorage read failed", err);
readLogged = true;
}
return null;
}
}
function writeStoredFramework(slug: string | null) {
if (typeof window === "undefined") return;
try {
if (slug === null) {
window.localStorage.removeItem(STORAGE_KEY);
} else {
window.localStorage.setItem(STORAGE_KEY, slug);
}
} catch (err) {
// localStorage may be unavailable (SSR, private mode, etc.) — log
// once per session so the failure isn't completely silent.
if (!writeLogged) {
console.warn("[framework-provider] localStorage write failed", err);
writeLogged = true;
}
}
}
export function FrameworkProvider({
children,
knownFrameworks,
}: {
children: React.ReactNode;
knownFrameworks: string[];
}) {
const pathname = usePathname() ?? "";
const urlFramework = useMemo(() => {
const first = pathname.split("/").filter(Boolean)[0];
if (first && knownFrameworks.includes(first)) return first;
return null;
}, [pathname, knownFrameworks]);
const [stored, setStored] = useState<string | null>(null);
// Hydrate stored framework on client mount
useEffect(() => {
setStored(readStoredFramework());
}, []);
// Whenever the URL asserts a framework, persist it so the preference
// follows the user when they navigate back to /docs/*.
//
// `stored` is intentionally NOT a dep: including it causes this effect
// to re-run every time we update stored from within (infinite-ish
// ping-pong with the state setter below). We only care about URL
// changes as the trigger. The internal `!== stored` check short-circuits
// the no-op case using the latest closed-over value.
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (urlFramework && urlFramework !== stored) {
writeStoredFramework(urlFramework);
setStored(urlFramework);
}
}, [urlFramework]);
// ACTIVE framework is strictly URL-derived. localStorage NEVER promotes
// itself into `framework` — it lives in `storedFramework` where it can
// be shown as "your last pick" without implying the current view is
// scoped to it.
const framework = urlFramework;
// Effective framework falls through URL → stored → default so content
// always has a target to render against. Validate `stored` against the
// known registry before honouring it, so a stale entry from a renamed
// or removed integration doesn't poison the render.
const storedIsValid = stored !== null && knownFrameworks.includes(stored);
const effectiveFramework =
framework ?? (storedIsValid ? stored! : DEFAULT_FRAMEWORK);
const setStoredFramework = (slug: string | null) => {
// Validate the slug against the known registry. Callers passing a
// slug we don't recognise would poison the stored preference (e.g.
// leaking a route segment like "docs"). Drop + warn instead.
if (slug !== null && !knownFrameworks.includes(slug)) {
console.warn(
`[framework-provider] setStoredFramework called with unknown slug "${slug}" — ignoring`,
);
return;
}
writeStoredFramework(slug);
setStored(slug);
};
const value: FrameworkContextValue = {
framework,
storedFramework: stored,
effectiveFramework,
knownFrameworks,
setStoredFramework,
};
return (
<FrameworkContext.Provider value={value}>
{children}
</FrameworkContext.Provider>
);
}
export function useFramework(): FrameworkContextValue {
const ctx = useContext(FrameworkContext);
if (!ctx) {
// Fail loudly: a silent fallback masks wiring bugs (components
// rendered outside the provider tree would silently report "no
// framework" forever). Matches every other context pattern in the
// app.
throw new Error("useFramework must be used within FrameworkProvider");
}
return ctx;
}