@ai-gui/react 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -4
- package/dist/index.cjs +214 -42
- package/dist/index.d.cts +34 -9
- package/dist/index.d.ts +34 -9
- package/dist/index.js +215 -44
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -11,8 +11,8 @@ pnpm add @ai-gui/core @ai-gui/react
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```tsx
|
|
14
|
-
import { CardRegistry } from "@ai-gui/core"
|
|
15
|
-
import { AIRenderer } from "@ai-gui/react"
|
|
14
|
+
import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
|
|
15
|
+
import { AIRenderer, useActionState } from "@ai-gui/react"
|
|
16
16
|
import { useRef } from "react"
|
|
17
17
|
|
|
18
18
|
const registry = new CardRegistry()
|
|
@@ -28,6 +28,14 @@ registry.register({
|
|
|
28
28
|
),
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
const actions = new ActionRegistry()
|
|
32
|
+
actions.register({
|
|
33
|
+
type: "refresh",
|
|
34
|
+
run: async (params, { signal }) => fetch("/api/weather", { signal }).then((r) => r.json()),
|
|
35
|
+
})
|
|
36
|
+
const cardStore = new CardStore({ registry })
|
|
37
|
+
const actionRuntime = createActionRuntime({ registry: actions, cardStore })
|
|
38
|
+
|
|
31
39
|
function Chat() {
|
|
32
40
|
const ref = useRef<React.ComponentRef<typeof AIRenderer>>(null)
|
|
33
41
|
// ref.current?.push(chunk) / feed(source) / reset()
|
|
@@ -35,7 +43,9 @@ function Chat() {
|
|
|
35
43
|
<AIRenderer
|
|
36
44
|
ref={ref}
|
|
37
45
|
registry={registry}
|
|
38
|
-
|
|
46
|
+
cardStore={cardStore}
|
|
47
|
+
actionRuntime={actionRuntime}
|
|
48
|
+
onCardAction={(action) => console.log("observed", action)}
|
|
39
49
|
/>
|
|
40
50
|
)
|
|
41
51
|
}
|
|
@@ -43,7 +53,10 @@ function Chat() {
|
|
|
43
53
|
|
|
44
54
|
## Exports
|
|
45
55
|
|
|
46
|
-
- `<AIRenderer ref registry plugins sanitize onCardAction />` — imperative `ref.current.push/feed/reset`.
|
|
56
|
+
- `<AIRenderer ref registry cardStore plugins sanitize actionRuntime onCardAction />` — imperative `ref.current.push/feed/reset`.
|
|
47
57
|
- `useAIRenderer(options)` → `{ nodes, push, feed, reset }` — the hook form when you want to render `nodes` yourself.
|
|
58
|
+
- `useActionState(runtime, key)` — subscribe to one action's `idle | pending | success | error | cancelled` state.
|
|
59
|
+
|
|
60
|
+
Cards with a top-level `id` use the supplied `CardStore`. Their React component receives `{ data, state, onAction }`; patches update props while preserving component and DOM state.
|
|
48
61
|
|
|
49
62
|
See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
|
package/dist/index.cjs
CHANGED
|
@@ -26,26 +26,67 @@ const react = __toESM(require("react"));
|
|
|
26
26
|
const __ai_gui_core = __toESM(require("@ai-gui/core"));
|
|
27
27
|
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
|
|
28
28
|
|
|
29
|
+
//#region src/apply-patches.ts
|
|
30
|
+
function applyPatches(nodes, patches) {
|
|
31
|
+
let out = nodes.slice();
|
|
32
|
+
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
33
|
+
else if (p.op === "update") {
|
|
34
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
35
|
+
if (i >= 0) out[i] = p.node;
|
|
36
|
+
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
37
|
+
else if (p.op === "move") {
|
|
38
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
39
|
+
if (i >= 0) {
|
|
40
|
+
const [node] = out.splice(i, 1);
|
|
41
|
+
out.splice(p.index, 0, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
29
48
|
//#region src/use-ai-renderer.ts
|
|
30
49
|
function useAIRenderer(options = {}) {
|
|
31
50
|
const [nodes, setNodes] = (0, react.useState)([]);
|
|
32
|
-
const
|
|
33
|
-
|
|
51
|
+
const active = (0, react.useRef)(null);
|
|
52
|
+
const session = (0, react.useMemo)(() => {
|
|
53
|
+
const token = {};
|
|
54
|
+
const renderer = new __ai_gui_core.Renderer({
|
|
34
55
|
...options,
|
|
35
|
-
onPatch: (
|
|
56
|
+
onPatch: (patches) => {
|
|
57
|
+
if (active.current === token) setNodes((current) => applyPatches(current, patches));
|
|
58
|
+
}
|
|
36
59
|
});
|
|
60
|
+
active.current = token;
|
|
61
|
+
return {
|
|
62
|
+
renderer,
|
|
63
|
+
token
|
|
64
|
+
};
|
|
37
65
|
}, [
|
|
38
66
|
options.registry,
|
|
39
67
|
options.sanitize,
|
|
40
|
-
options.plugins
|
|
68
|
+
options.plugins,
|
|
69
|
+
options.scheduler,
|
|
70
|
+
options.debug,
|
|
71
|
+
options.onDebugEvent
|
|
41
72
|
]);
|
|
42
|
-
|
|
43
|
-
|
|
73
|
+
(0, react.useEffect)(() => {
|
|
74
|
+
setNodes([]);
|
|
75
|
+
return () => {
|
|
76
|
+
session.renderer.reset();
|
|
77
|
+
if (active.current === session.token) active.current = null;
|
|
78
|
+
};
|
|
79
|
+
}, [session]);
|
|
80
|
+
const push = (0, react.useCallback)((chunk) => {
|
|
81
|
+
if (active.current === session.token) session.renderer.push(chunk);
|
|
82
|
+
}, [session]);
|
|
83
|
+
const feed = (0, react.useCallback)((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
|
|
44
84
|
const reset = (0, react.useCallback)(() => {
|
|
45
|
-
renderer.reset();
|
|
85
|
+
session.renderer.reset();
|
|
46
86
|
setNodes([]);
|
|
47
|
-
}, [
|
|
87
|
+
}, [session]);
|
|
48
88
|
return {
|
|
89
|
+
renderer: session.renderer,
|
|
49
90
|
nodes,
|
|
50
91
|
push,
|
|
51
92
|
feed,
|
|
@@ -53,16 +94,32 @@ function useAIRenderer(options = {}) {
|
|
|
53
94
|
};
|
|
54
95
|
}
|
|
55
96
|
|
|
97
|
+
//#endregion
|
|
98
|
+
//#region src/use-action-state.ts
|
|
99
|
+
function useActionState(runtime, key) {
|
|
100
|
+
const idle = (0, react.useMemo)(() => (0, __ai_gui_core.getIdleActionState)(key), [key]);
|
|
101
|
+
const subscribe = (0, react.useCallback)((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
|
|
102
|
+
const getSnapshot = (0, react.useCallback)(() => {
|
|
103
|
+
const state = runtime?.getState(key) ?? idle;
|
|
104
|
+
return state.status === "idle" ? idle : state;
|
|
105
|
+
}, [
|
|
106
|
+
idle,
|
|
107
|
+
key,
|
|
108
|
+
runtime
|
|
109
|
+
]);
|
|
110
|
+
return (0, react.useSyncExternalStore)(subscribe, getSnapshot, () => idle);
|
|
111
|
+
}
|
|
112
|
+
|
|
56
113
|
//#endregion
|
|
57
114
|
//#region src/render-output.tsx
|
|
58
115
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
59
|
-
function renderOutput(out, key) {
|
|
116
|
+
function renderOutput(out, key, sanitize) {
|
|
60
117
|
switch (out.kind) {
|
|
61
|
-
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: (
|
|
118
|
+
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
62
119
|
case "element": return (0, react.createElement)(out.tag, {
|
|
63
120
|
key,
|
|
64
121
|
...out.props
|
|
65
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
|
|
122
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
66
123
|
case "card": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
67
124
|
"data-aigui-card-fallback": true,
|
|
68
125
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(out.data, null, 2) })
|
|
@@ -79,53 +136,85 @@ function MountHost({ mount }) {
|
|
|
79
136
|
return () => {
|
|
80
137
|
if (typeof cleanup === "function") cleanup();
|
|
81
138
|
};
|
|
82
|
-
}, []);
|
|
139
|
+
}, [mount]);
|
|
83
140
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
84
141
|
ref,
|
|
85
142
|
"data-aigui-mount": true
|
|
86
143
|
});
|
|
87
144
|
}
|
|
88
145
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
89
|
-
function AsyncOutput({ promise }) {
|
|
146
|
+
function AsyncOutput({ promise, sanitize }) {
|
|
90
147
|
const [resolved, setResolved] = (0, react.useState)(null);
|
|
148
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
91
149
|
(0, react.useEffect)(() => {
|
|
92
150
|
let cancelled = false;
|
|
151
|
+
setResolved(null);
|
|
152
|
+
setFailed(false);
|
|
93
153
|
promise.then((out) => {
|
|
94
154
|
if (!cancelled) setResolved(out);
|
|
155
|
+
}, () => {
|
|
156
|
+
if (!cancelled) setFailed(true);
|
|
95
157
|
});
|
|
96
158
|
return () => {
|
|
97
159
|
cancelled = true;
|
|
98
160
|
};
|
|
99
161
|
}, [promise]);
|
|
162
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
100
163
|
if (resolved === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-pending": true });
|
|
101
|
-
|
|
164
|
+
try {
|
|
165
|
+
return renderOutput(resolved, void 0, sanitize);
|
|
166
|
+
} catch {
|
|
167
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function sanitizeOutput(html, sanitize) {
|
|
171
|
+
if (sanitize === false) return html;
|
|
172
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
102
173
|
}
|
|
103
174
|
|
|
104
175
|
//#endregion
|
|
105
176
|
//#region src/render-node.tsx
|
|
106
177
|
function renderNode(node, ctx) {
|
|
107
|
-
const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
|
|
178
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
108
179
|
if (r) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
180
|
+
if (node.complete === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
181
|
+
"data-aigui-block-loading": "",
|
|
182
|
+
"data-block-type": node.type
|
|
183
|
+
}, node.key);
|
|
184
|
+
try {
|
|
185
|
+
const out = r(node);
|
|
186
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
|
|
187
|
+
promise: out,
|
|
188
|
+
sanitize: ctx.sanitize
|
|
189
|
+
}, node.key);
|
|
190
|
+
return renderOutput(out, node.key, ctx.sanitize);
|
|
191
|
+
} catch {
|
|
192
|
+
return renderFallback(node, ctx);
|
|
193
|
+
}
|
|
112
194
|
}
|
|
113
195
|
switch (node.type) {
|
|
114
196
|
case "heading": return (0, react.createElement)(node.tag ?? "h1", {
|
|
115
197
|
key: node.key,
|
|
116
|
-
dangerouslySetInnerHTML: { __html: node.html ?? "" }
|
|
198
|
+
dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
|
|
117
199
|
});
|
|
118
|
-
case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
|
|
200
|
+
case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
|
|
119
201
|
case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
120
202
|
"data-lang": node.attrs?.lang,
|
|
121
203
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: node.content })
|
|
122
204
|
}, node.key);
|
|
123
205
|
case "hr": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", {}, node.key);
|
|
124
|
-
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: node.content ?? "" } }, node.key);
|
|
206
|
+
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
|
|
125
207
|
case "card": return renderCard(node, ctx);
|
|
126
|
-
default: return
|
|
208
|
+
default: return renderFallback(node, ctx);
|
|
127
209
|
}
|
|
128
210
|
}
|
|
211
|
+
function renderFallback(node, ctx) {
|
|
212
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
|
|
213
|
+
}
|
|
214
|
+
function renderHtml(html, ctx) {
|
|
215
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
216
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
217
|
+
}
|
|
129
218
|
function renderCard(node, ctx) {
|
|
130
219
|
const card = node.card;
|
|
131
220
|
if (!card) return null;
|
|
@@ -139,10 +228,15 @@ function renderCard(node, ctx) {
|
|
|
139
228
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
|
|
140
229
|
}, node.key);
|
|
141
230
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
231
|
+
if (card.id && ctx.cardStore) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatefulCardHost, {
|
|
232
|
+
cardStore: ctx.cardStore,
|
|
233
|
+
cardId: card.id,
|
|
234
|
+
cardType: card.type,
|
|
235
|
+
initialData: card.data,
|
|
236
|
+
Comp,
|
|
237
|
+
onCardAction: ctx.onCardAction
|
|
145
238
|
}, node.key);
|
|
239
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
146
240
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
147
241
|
data: card.data,
|
|
148
242
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -151,6 +245,49 @@ function renderCard(node, ctx) {
|
|
|
151
245
|
})
|
|
152
246
|
}, node.key);
|
|
153
247
|
}
|
|
248
|
+
const getServerCardSnapshot = () => void 0;
|
|
249
|
+
function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
|
|
250
|
+
const subscribe = (0, react.useCallback)((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
|
|
251
|
+
const getSnapshot = (0, react.useCallback)(() => cardStore.get(cardId), [cardStore, cardId]);
|
|
252
|
+
const record = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getServerCardSnapshot);
|
|
253
|
+
(0, react.useEffect)(() => {
|
|
254
|
+
if (cardStore.get(cardId)) return;
|
|
255
|
+
try {
|
|
256
|
+
cardStore.register({
|
|
257
|
+
id: cardId,
|
|
258
|
+
type: cardType,
|
|
259
|
+
data: initialData
|
|
260
|
+
});
|
|
261
|
+
} catch {}
|
|
262
|
+
}, [
|
|
263
|
+
cardStore,
|
|
264
|
+
cardId,
|
|
265
|
+
cardType,
|
|
266
|
+
initialData
|
|
267
|
+
]);
|
|
268
|
+
if (!record) return renderCardFallback(void 0, initialData);
|
|
269
|
+
if (record.type !== cardType) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
270
|
+
"data-aigui-card-invalid": true,
|
|
271
|
+
"data-card-type": cardType,
|
|
272
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(initialData, null, 2) })
|
|
273
|
+
});
|
|
274
|
+
if (!Comp) return renderCardFallback(void 0, record.data);
|
|
275
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
276
|
+
data: record.data,
|
|
277
|
+
state: record.action,
|
|
278
|
+
onAction: (action) => onCardAction?.({
|
|
279
|
+
...action,
|
|
280
|
+
cardType,
|
|
281
|
+
cardId
|
|
282
|
+
})
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
function renderCardFallback(key, data) {
|
|
286
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
287
|
+
"data-aigui-card-fallback": true,
|
|
288
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(data, null, 2) })
|
|
289
|
+
}, key);
|
|
290
|
+
}
|
|
154
291
|
function getCardComponent(registry, type) {
|
|
155
292
|
if (!registry) return void 0;
|
|
156
293
|
return registry.getRender(type);
|
|
@@ -158,27 +295,73 @@ function getCardComponent(registry, type) {
|
|
|
158
295
|
|
|
159
296
|
//#endregion
|
|
160
297
|
//#region src/ai-renderer.tsx
|
|
298
|
+
function createActionScope() {
|
|
299
|
+
return {
|
|
300
|
+
controller: new AbortController(),
|
|
301
|
+
owner: {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
161
304
|
const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
162
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
305
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
163
306
|
const opts = {
|
|
164
307
|
registry,
|
|
165
308
|
sanitize,
|
|
166
|
-
plugins
|
|
309
|
+
plugins,
|
|
310
|
+
debug,
|
|
311
|
+
onDebugEvent
|
|
167
312
|
};
|
|
168
|
-
const { nodes, push, feed, reset } = useAIRenderer(opts);
|
|
313
|
+
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
314
|
+
const actionScope = (0, react.useRef)(createActionScope());
|
|
315
|
+
(0, react.useEffect)(() => () => {
|
|
316
|
+
actionScope.current.controller.abort();
|
|
317
|
+
actionScope.current = createActionScope();
|
|
318
|
+
}, [
|
|
319
|
+
actionRuntime,
|
|
320
|
+
registry,
|
|
321
|
+
sanitize,
|
|
322
|
+
plugins
|
|
323
|
+
]);
|
|
324
|
+
const reset = (0, react.useCallback)(() => {
|
|
325
|
+
actionScope.current.controller.abort();
|
|
326
|
+
actionScope.current = createActionScope();
|
|
327
|
+
resetRenderer();
|
|
328
|
+
}, [resetRenderer]);
|
|
329
|
+
const handleCardAction = (0, react.useCallback)((action) => {
|
|
330
|
+
if (actionRuntime) {
|
|
331
|
+
const scope = actionScope.current;
|
|
332
|
+
actionRuntime.dispatch({
|
|
333
|
+
type: action.type,
|
|
334
|
+
params: action.params,
|
|
335
|
+
cardType: action.cardType,
|
|
336
|
+
cardId: action.cardId
|
|
337
|
+
}, {
|
|
338
|
+
signal: scope.controller.signal,
|
|
339
|
+
owner: scope.owner
|
|
340
|
+
}).catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
onCardAction?.(action);
|
|
343
|
+
}, [actionRuntime, onCardAction]);
|
|
169
344
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
345
|
+
debugSource: "renderer",
|
|
346
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
170
347
|
push,
|
|
171
348
|
feed,
|
|
172
349
|
reset
|
|
173
350
|
}), [
|
|
351
|
+
renderer,
|
|
174
352
|
push,
|
|
175
353
|
feed,
|
|
176
354
|
reset
|
|
177
355
|
]);
|
|
356
|
+
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
178
357
|
const ctx = {
|
|
179
358
|
registry,
|
|
359
|
+
cardStore,
|
|
180
360
|
plugins,
|
|
181
|
-
|
|
361
|
+
nodeRenderers,
|
|
362
|
+
onCardAction: handleCardAction,
|
|
363
|
+
sanitize,
|
|
364
|
+
sanitized: true
|
|
182
365
|
};
|
|
183
366
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
184
367
|
className,
|
|
@@ -187,21 +370,10 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
187
370
|
});
|
|
188
371
|
});
|
|
189
372
|
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/apply-patches.ts
|
|
192
|
-
function applyPatches(nodes, patches) {
|
|
193
|
-
let out = nodes.slice();
|
|
194
|
-
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
195
|
-
else if (p.op === "update") {
|
|
196
|
-
const i = out.findIndex((n) => n.key === p.key);
|
|
197
|
-
if (i >= 0) out[i] = p.node;
|
|
198
|
-
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
373
|
//#endregion
|
|
203
374
|
exports.AIRenderer = AIRenderer
|
|
204
375
|
exports.applyPatches = applyPatches
|
|
205
376
|
exports.renderNode = renderNode
|
|
206
377
|
exports.renderOutput = renderOutput
|
|
207
|
-
exports.useAIRenderer = useAIRenderer
|
|
378
|
+
exports.useAIRenderer = useAIRenderer
|
|
379
|
+
exports.useActionState = useActionState
|
package/dist/index.d.cts
CHANGED
|
@@ -1,54 +1,79 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
|
|
2
2
|
import * as react0 from "react";
|
|
3
3
|
import * as react1 from "react";
|
|
4
4
|
import { ReactNode } from "react";
|
|
5
5
|
|
|
6
6
|
//#region src/use-ai-renderer.d.ts
|
|
7
7
|
interface UseAIRendererResult {
|
|
8
|
+
renderer: Renderer;
|
|
8
9
|
nodes: ASTNode[];
|
|
9
10
|
push: (chunk: string) => void;
|
|
10
|
-
feed: (source:
|
|
11
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
11
12
|
reset: () => void;
|
|
12
13
|
}
|
|
13
14
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
14
15
|
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/use-action-state.d.ts
|
|
18
|
+
declare function useActionState(runtime: ActionRuntime | undefined, key: string): ActionState;
|
|
19
|
+
|
|
15
20
|
//#endregion
|
|
16
21
|
//#region src/render-node.d.ts
|
|
22
|
+
interface CardActionPayload {
|
|
23
|
+
type: string;
|
|
24
|
+
params?: unknown;
|
|
25
|
+
cardType: string;
|
|
26
|
+
cardId?: string;
|
|
27
|
+
}
|
|
17
28
|
interface RenderContext {
|
|
18
29
|
registry?: CardRegistry;
|
|
30
|
+
cardStore?: CardStore;
|
|
19
31
|
plugins?: AIGuiPlugin[];
|
|
20
|
-
|
|
32
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
33
|
+
onCardAction?: (action: CardActionPayload) => void;
|
|
34
|
+
sanitize?: RendererOptions["sanitize"];
|
|
35
|
+
sanitized?: boolean;
|
|
36
|
+
}
|
|
37
|
+
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
|
+
interface CardComponentProps {
|
|
39
|
+
data: unknown;
|
|
40
|
+
state?: CardAction;
|
|
41
|
+
onAction: (a: {
|
|
21
42
|
type: string;
|
|
22
43
|
params?: unknown;
|
|
23
|
-
cardType: string;
|
|
24
44
|
}) => void;
|
|
25
45
|
}
|
|
26
|
-
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
27
46
|
|
|
28
47
|
//#endregion
|
|
29
48
|
//#region src/ai-renderer.d.ts
|
|
30
49
|
interface AIRendererHandle {
|
|
50
|
+
readonly debugSource: "renderer";
|
|
51
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
31
52
|
push: (chunk: string) => void;
|
|
32
|
-
feed: (source:
|
|
53
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
33
54
|
reset: () => void;
|
|
34
55
|
}
|
|
35
56
|
interface AIRendererProps {
|
|
36
57
|
registry?: CardRegistry;
|
|
37
|
-
|
|
58
|
+
cardStore?: CardStore;
|
|
59
|
+
sanitize?: RendererOptions["sanitize"];
|
|
38
60
|
plugins?: AIGuiPlugin[];
|
|
61
|
+
actionRuntime?: ActionRuntime;
|
|
39
62
|
onCardAction?: RenderContext["onCardAction"];
|
|
40
63
|
className?: string;
|
|
64
|
+
debug?: RendererOptions["debug"];
|
|
65
|
+
onDebugEvent?: RendererOptions["onDebugEvent"];
|
|
41
66
|
}
|
|
42
67
|
declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
|
|
43
68
|
|
|
44
69
|
//#endregion
|
|
45
70
|
//#region src/render-output.d.ts
|
|
46
71
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
47
|
-
declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
|
|
72
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
48
73
|
|
|
49
74
|
//#endregion
|
|
50
75
|
//#region src/apply-patches.d.ts
|
|
51
76
|
declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
|
|
52
77
|
|
|
53
78
|
//#endregion
|
|
54
|
-
export { AIRenderer, AIRendererHandle, AIRendererProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer };
|
|
79
|
+
export { AIRenderer, AIRendererHandle, AIRendererProps, CardActionPayload, CardComponentProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,54 +1,79 @@
|
|
|
1
1
|
import * as react0 from "react";
|
|
2
2
|
import * as react1 from "react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
4
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
|
|
5
5
|
|
|
6
6
|
//#region src/use-ai-renderer.d.ts
|
|
7
7
|
interface UseAIRendererResult {
|
|
8
|
+
renderer: Renderer;
|
|
8
9
|
nodes: ASTNode[];
|
|
9
10
|
push: (chunk: string) => void;
|
|
10
|
-
feed: (source:
|
|
11
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
11
12
|
reset: () => void;
|
|
12
13
|
}
|
|
13
14
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
14
15
|
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/use-action-state.d.ts
|
|
18
|
+
declare function useActionState(runtime: ActionRuntime | undefined, key: string): ActionState;
|
|
19
|
+
|
|
15
20
|
//#endregion
|
|
16
21
|
//#region src/render-node.d.ts
|
|
22
|
+
interface CardActionPayload {
|
|
23
|
+
type: string;
|
|
24
|
+
params?: unknown;
|
|
25
|
+
cardType: string;
|
|
26
|
+
cardId?: string;
|
|
27
|
+
}
|
|
17
28
|
interface RenderContext {
|
|
18
29
|
registry?: CardRegistry;
|
|
30
|
+
cardStore?: CardStore;
|
|
19
31
|
plugins?: AIGuiPlugin[];
|
|
20
|
-
|
|
32
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
33
|
+
onCardAction?: (action: CardActionPayload) => void;
|
|
34
|
+
sanitize?: RendererOptions["sanitize"];
|
|
35
|
+
sanitized?: boolean;
|
|
36
|
+
}
|
|
37
|
+
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
|
+
interface CardComponentProps {
|
|
39
|
+
data: unknown;
|
|
40
|
+
state?: CardAction;
|
|
41
|
+
onAction: (a: {
|
|
21
42
|
type: string;
|
|
22
43
|
params?: unknown;
|
|
23
|
-
cardType: string;
|
|
24
44
|
}) => void;
|
|
25
45
|
}
|
|
26
|
-
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
27
46
|
|
|
28
47
|
//#endregion
|
|
29
48
|
//#region src/ai-renderer.d.ts
|
|
30
49
|
interface AIRendererHandle {
|
|
50
|
+
readonly debugSource: "renderer";
|
|
51
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
31
52
|
push: (chunk: string) => void;
|
|
32
|
-
feed: (source:
|
|
53
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
33
54
|
reset: () => void;
|
|
34
55
|
}
|
|
35
56
|
interface AIRendererProps {
|
|
36
57
|
registry?: CardRegistry;
|
|
37
|
-
|
|
58
|
+
cardStore?: CardStore;
|
|
59
|
+
sanitize?: RendererOptions["sanitize"];
|
|
38
60
|
plugins?: AIGuiPlugin[];
|
|
61
|
+
actionRuntime?: ActionRuntime;
|
|
39
62
|
onCardAction?: RenderContext["onCardAction"];
|
|
40
63
|
className?: string;
|
|
64
|
+
debug?: RendererOptions["debug"];
|
|
65
|
+
onDebugEvent?: RendererOptions["onDebugEvent"];
|
|
41
66
|
}
|
|
42
67
|
declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
|
|
43
68
|
|
|
44
69
|
//#endregion
|
|
45
70
|
//#region src/render-output.d.ts
|
|
46
71
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
47
|
-
declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
|
|
72
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
48
73
|
|
|
49
74
|
//#endregion
|
|
50
75
|
//#region src/apply-patches.d.ts
|
|
51
76
|
declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
|
|
52
77
|
|
|
53
78
|
//#endregion
|
|
54
|
-
export { AIRenderer, AIRendererHandle, AIRendererProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer };
|
|
79
|
+
export { AIRenderer, AIRendererHandle, AIRendererProps, CardActionPayload, CardComponentProps, RenderContext, UseAIRendererResult, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,68 @@
|
|
|
1
|
-
import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
|
|
1
|
+
import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { Renderer, collectNodeRenderers, getIdleActionState, sanitizeHtml } from "@ai-gui/core";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
|
+
//#region src/apply-patches.ts
|
|
6
|
+
function applyPatches(nodes, patches) {
|
|
7
|
+
let out = nodes.slice();
|
|
8
|
+
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
9
|
+
else if (p.op === "update") {
|
|
10
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
11
|
+
if (i >= 0) out[i] = p.node;
|
|
12
|
+
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
13
|
+
else if (p.op === "move") {
|
|
14
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
15
|
+
if (i >= 0) {
|
|
16
|
+
const [node] = out.splice(i, 1);
|
|
17
|
+
out.splice(p.index, 0, node);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
5
24
|
//#region src/use-ai-renderer.ts
|
|
6
25
|
function useAIRenderer(options = {}) {
|
|
7
26
|
const [nodes, setNodes] = useState([]);
|
|
8
|
-
const
|
|
9
|
-
|
|
27
|
+
const active = useRef(null);
|
|
28
|
+
const session = useMemo(() => {
|
|
29
|
+
const token = {};
|
|
30
|
+
const renderer = new Renderer({
|
|
10
31
|
...options,
|
|
11
|
-
onPatch: (
|
|
32
|
+
onPatch: (patches) => {
|
|
33
|
+
if (active.current === token) setNodes((current) => applyPatches(current, patches));
|
|
34
|
+
}
|
|
12
35
|
});
|
|
36
|
+
active.current = token;
|
|
37
|
+
return {
|
|
38
|
+
renderer,
|
|
39
|
+
token
|
|
40
|
+
};
|
|
13
41
|
}, [
|
|
14
42
|
options.registry,
|
|
15
43
|
options.sanitize,
|
|
16
|
-
options.plugins
|
|
44
|
+
options.plugins,
|
|
45
|
+
options.scheduler,
|
|
46
|
+
options.debug,
|
|
47
|
+
options.onDebugEvent
|
|
17
48
|
]);
|
|
18
|
-
|
|
19
|
-
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setNodes([]);
|
|
51
|
+
return () => {
|
|
52
|
+
session.renderer.reset();
|
|
53
|
+
if (active.current === session.token) active.current = null;
|
|
54
|
+
};
|
|
55
|
+
}, [session]);
|
|
56
|
+
const push = useCallback((chunk) => {
|
|
57
|
+
if (active.current === session.token) session.renderer.push(chunk);
|
|
58
|
+
}, [session]);
|
|
59
|
+
const feed = useCallback((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
|
|
20
60
|
const reset = useCallback(() => {
|
|
21
|
-
renderer.reset();
|
|
61
|
+
session.renderer.reset();
|
|
22
62
|
setNodes([]);
|
|
23
|
-
}, [
|
|
63
|
+
}, [session]);
|
|
24
64
|
return {
|
|
65
|
+
renderer: session.renderer,
|
|
25
66
|
nodes,
|
|
26
67
|
push,
|
|
27
68
|
feed,
|
|
@@ -29,16 +70,32 @@ function useAIRenderer(options = {}) {
|
|
|
29
70
|
};
|
|
30
71
|
}
|
|
31
72
|
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/use-action-state.ts
|
|
75
|
+
function useActionState(runtime, key) {
|
|
76
|
+
const idle = useMemo(() => getIdleActionState(key), [key]);
|
|
77
|
+
const subscribe = useCallback((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
|
|
78
|
+
const getSnapshot = useCallback(() => {
|
|
79
|
+
const state = runtime?.getState(key) ?? idle;
|
|
80
|
+
return state.status === "idle" ? idle : state;
|
|
81
|
+
}, [
|
|
82
|
+
idle,
|
|
83
|
+
key,
|
|
84
|
+
runtime
|
|
85
|
+
]);
|
|
86
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => idle);
|
|
87
|
+
}
|
|
88
|
+
|
|
32
89
|
//#endregion
|
|
33
90
|
//#region src/render-output.tsx
|
|
34
91
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
35
|
-
function renderOutput(out, key) {
|
|
92
|
+
function renderOutput(out, key, sanitize) {
|
|
36
93
|
switch (out.kind) {
|
|
37
|
-
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html:
|
|
94
|
+
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
38
95
|
case "element": return createElement(out.tag, {
|
|
39
96
|
key,
|
|
40
97
|
...out.props
|
|
41
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
|
|
98
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
42
99
|
case "card": return /* @__PURE__ */ jsx("pre", {
|
|
43
100
|
"data-aigui-card-fallback": true,
|
|
44
101
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(out.data, null, 2) })
|
|
@@ -55,53 +112,85 @@ function MountHost({ mount }) {
|
|
|
55
112
|
return () => {
|
|
56
113
|
if (typeof cleanup === "function") cleanup();
|
|
57
114
|
};
|
|
58
|
-
}, []);
|
|
115
|
+
}, [mount]);
|
|
59
116
|
return /* @__PURE__ */ jsx("div", {
|
|
60
117
|
ref,
|
|
61
118
|
"data-aigui-mount": true
|
|
62
119
|
});
|
|
63
120
|
}
|
|
64
121
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
65
|
-
function AsyncOutput({ promise }) {
|
|
122
|
+
function AsyncOutput({ promise, sanitize }) {
|
|
66
123
|
const [resolved, setResolved] = useState(null);
|
|
124
|
+
const [failed, setFailed] = useState(false);
|
|
67
125
|
useEffect(() => {
|
|
68
126
|
let cancelled = false;
|
|
127
|
+
setResolved(null);
|
|
128
|
+
setFailed(false);
|
|
69
129
|
promise.then((out) => {
|
|
70
130
|
if (!cancelled) setResolved(out);
|
|
131
|
+
}, () => {
|
|
132
|
+
if (!cancelled) setFailed(true);
|
|
71
133
|
});
|
|
72
134
|
return () => {
|
|
73
135
|
cancelled = true;
|
|
74
136
|
};
|
|
75
137
|
}, [promise]);
|
|
138
|
+
if (failed) return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
76
139
|
if (resolved === null) return /* @__PURE__ */ jsx("span", { "data-aigui-async-pending": true });
|
|
77
|
-
|
|
140
|
+
try {
|
|
141
|
+
return renderOutput(resolved, void 0, sanitize);
|
|
142
|
+
} catch {
|
|
143
|
+
return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function sanitizeOutput(html, sanitize) {
|
|
147
|
+
if (sanitize === false) return html;
|
|
148
|
+
return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
78
149
|
}
|
|
79
150
|
|
|
80
151
|
//#endregion
|
|
81
152
|
//#region src/render-node.tsx
|
|
82
153
|
function renderNode(node, ctx) {
|
|
83
|
-
const r = collectNodeRenderers(ctx.plugins)[node.type];
|
|
154
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
84
155
|
if (r) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
156
|
+
if (node.complete === false) return /* @__PURE__ */ jsx("div", {
|
|
157
|
+
"data-aigui-block-loading": "",
|
|
158
|
+
"data-block-type": node.type
|
|
159
|
+
}, node.key);
|
|
160
|
+
try {
|
|
161
|
+
const out = r(node);
|
|
162
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
163
|
+
promise: out,
|
|
164
|
+
sanitize: ctx.sanitize
|
|
165
|
+
}, node.key);
|
|
166
|
+
return renderOutput(out, node.key, ctx.sanitize);
|
|
167
|
+
} catch {
|
|
168
|
+
return renderFallback(node, ctx);
|
|
169
|
+
}
|
|
88
170
|
}
|
|
89
171
|
switch (node.type) {
|
|
90
172
|
case "heading": return createElement(node.tag ?? "h1", {
|
|
91
173
|
key: node.key,
|
|
92
|
-
dangerouslySetInnerHTML: { __html: node.html ?? "" }
|
|
174
|
+
dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
|
|
93
175
|
});
|
|
94
|
-
case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
|
|
176
|
+
case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
|
|
95
177
|
case "code": return /* @__PURE__ */ jsx("pre", {
|
|
96
178
|
"data-lang": node.attrs?.lang,
|
|
97
179
|
children: /* @__PURE__ */ jsx("code", { children: node.content })
|
|
98
180
|
}, node.key);
|
|
99
181
|
case "hr": return /* @__PURE__ */ jsx("hr", {}, node.key);
|
|
100
|
-
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: node.content ?? "" } }, node.key);
|
|
182
|
+
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
|
|
101
183
|
case "card": return renderCard(node, ctx);
|
|
102
|
-
default: return
|
|
184
|
+
default: return renderFallback(node, ctx);
|
|
103
185
|
}
|
|
104
186
|
}
|
|
187
|
+
function renderFallback(node, ctx) {
|
|
188
|
+
return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
|
|
189
|
+
}
|
|
190
|
+
function renderHtml(html, ctx) {
|
|
191
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
192
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
193
|
+
}
|
|
105
194
|
function renderCard(node, ctx) {
|
|
106
195
|
const card = node.card;
|
|
107
196
|
if (!card) return null;
|
|
@@ -115,10 +204,15 @@ function renderCard(node, ctx) {
|
|
|
115
204
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
|
|
116
205
|
}, node.key);
|
|
117
206
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
118
|
-
if (
|
|
119
|
-
|
|
120
|
-
|
|
207
|
+
if (card.id && ctx.cardStore) return /* @__PURE__ */ jsx(StatefulCardHost, {
|
|
208
|
+
cardStore: ctx.cardStore,
|
|
209
|
+
cardId: card.id,
|
|
210
|
+
cardType: card.type,
|
|
211
|
+
initialData: card.data,
|
|
212
|
+
Comp,
|
|
213
|
+
onCardAction: ctx.onCardAction
|
|
121
214
|
}, node.key);
|
|
215
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
122
216
|
return /* @__PURE__ */ jsx(Comp, {
|
|
123
217
|
data: card.data,
|
|
124
218
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -127,6 +221,49 @@ function renderCard(node, ctx) {
|
|
|
127
221
|
})
|
|
128
222
|
}, node.key);
|
|
129
223
|
}
|
|
224
|
+
const getServerCardSnapshot = () => void 0;
|
|
225
|
+
function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
|
|
226
|
+
const subscribe = useCallback((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
|
|
227
|
+
const getSnapshot = useCallback(() => cardStore.get(cardId), [cardStore, cardId]);
|
|
228
|
+
const record = useSyncExternalStore(subscribe, getSnapshot, getServerCardSnapshot);
|
|
229
|
+
useEffect(() => {
|
|
230
|
+
if (cardStore.get(cardId)) return;
|
|
231
|
+
try {
|
|
232
|
+
cardStore.register({
|
|
233
|
+
id: cardId,
|
|
234
|
+
type: cardType,
|
|
235
|
+
data: initialData
|
|
236
|
+
});
|
|
237
|
+
} catch {}
|
|
238
|
+
}, [
|
|
239
|
+
cardStore,
|
|
240
|
+
cardId,
|
|
241
|
+
cardType,
|
|
242
|
+
initialData
|
|
243
|
+
]);
|
|
244
|
+
if (!record) return renderCardFallback(void 0, initialData);
|
|
245
|
+
if (record.type !== cardType) return /* @__PURE__ */ jsx("pre", {
|
|
246
|
+
"data-aigui-card-invalid": true,
|
|
247
|
+
"data-card-type": cardType,
|
|
248
|
+
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(initialData, null, 2) })
|
|
249
|
+
});
|
|
250
|
+
if (!Comp) return renderCardFallback(void 0, record.data);
|
|
251
|
+
return /* @__PURE__ */ jsx(Comp, {
|
|
252
|
+
data: record.data,
|
|
253
|
+
state: record.action,
|
|
254
|
+
onAction: (action) => onCardAction?.({
|
|
255
|
+
...action,
|
|
256
|
+
cardType,
|
|
257
|
+
cardId
|
|
258
|
+
})
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function renderCardFallback(key, data) {
|
|
262
|
+
return /* @__PURE__ */ jsx("pre", {
|
|
263
|
+
"data-aigui-card-fallback": true,
|
|
264
|
+
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(data, null, 2) })
|
|
265
|
+
}, key);
|
|
266
|
+
}
|
|
130
267
|
function getCardComponent(registry, type) {
|
|
131
268
|
if (!registry) return void 0;
|
|
132
269
|
return registry.getRender(type);
|
|
@@ -134,27 +271,73 @@ function getCardComponent(registry, type) {
|
|
|
134
271
|
|
|
135
272
|
//#endregion
|
|
136
273
|
//#region src/ai-renderer.tsx
|
|
274
|
+
function createActionScope() {
|
|
275
|
+
return {
|
|
276
|
+
controller: new AbortController(),
|
|
277
|
+
owner: {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
137
280
|
const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
138
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
281
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
139
282
|
const opts = {
|
|
140
283
|
registry,
|
|
141
284
|
sanitize,
|
|
142
|
-
plugins
|
|
285
|
+
plugins,
|
|
286
|
+
debug,
|
|
287
|
+
onDebugEvent
|
|
143
288
|
};
|
|
144
|
-
const { nodes, push, feed, reset } = useAIRenderer(opts);
|
|
289
|
+
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
290
|
+
const actionScope = useRef(createActionScope());
|
|
291
|
+
useEffect(() => () => {
|
|
292
|
+
actionScope.current.controller.abort();
|
|
293
|
+
actionScope.current = createActionScope();
|
|
294
|
+
}, [
|
|
295
|
+
actionRuntime,
|
|
296
|
+
registry,
|
|
297
|
+
sanitize,
|
|
298
|
+
plugins
|
|
299
|
+
]);
|
|
300
|
+
const reset = useCallback(() => {
|
|
301
|
+
actionScope.current.controller.abort();
|
|
302
|
+
actionScope.current = createActionScope();
|
|
303
|
+
resetRenderer();
|
|
304
|
+
}, [resetRenderer]);
|
|
305
|
+
const handleCardAction = useCallback((action) => {
|
|
306
|
+
if (actionRuntime) {
|
|
307
|
+
const scope = actionScope.current;
|
|
308
|
+
actionRuntime.dispatch({
|
|
309
|
+
type: action.type,
|
|
310
|
+
params: action.params,
|
|
311
|
+
cardType: action.cardType,
|
|
312
|
+
cardId: action.cardId
|
|
313
|
+
}, {
|
|
314
|
+
signal: scope.controller.signal,
|
|
315
|
+
owner: scope.owner
|
|
316
|
+
}).catch(() => {});
|
|
317
|
+
}
|
|
318
|
+
onCardAction?.(action);
|
|
319
|
+
}, [actionRuntime, onCardAction]);
|
|
145
320
|
useImperativeHandle(ref, () => ({
|
|
321
|
+
debugSource: "renderer",
|
|
322
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
146
323
|
push,
|
|
147
324
|
feed,
|
|
148
325
|
reset
|
|
149
326
|
}), [
|
|
327
|
+
renderer,
|
|
150
328
|
push,
|
|
151
329
|
feed,
|
|
152
330
|
reset
|
|
153
331
|
]);
|
|
332
|
+
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
154
333
|
const ctx = {
|
|
155
334
|
registry,
|
|
335
|
+
cardStore,
|
|
156
336
|
plugins,
|
|
157
|
-
|
|
337
|
+
nodeRenderers,
|
|
338
|
+
onCardAction: handleCardAction,
|
|
339
|
+
sanitize,
|
|
340
|
+
sanitized: true
|
|
158
341
|
};
|
|
159
342
|
return /* @__PURE__ */ jsx("div", {
|
|
160
343
|
className,
|
|
@@ -164,16 +347,4 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
164
347
|
});
|
|
165
348
|
|
|
166
349
|
//#endregion
|
|
167
|
-
|
|
168
|
-
function applyPatches(nodes, patches) {
|
|
169
|
-
let out = nodes.slice();
|
|
170
|
-
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
171
|
-
else if (p.op === "update") {
|
|
172
|
-
const i = out.findIndex((n) => n.key === p.key);
|
|
173
|
-
if (i >= 0) out[i] = p.node;
|
|
174
|
-
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
//#endregion
|
|
179
|
-
export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer };
|
|
350
|
+
export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "React adapter for AIGUI — stream LLM output into live React components with cards, plugins, and interactive widgets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -22,14 +22,23 @@
|
|
|
22
22
|
"homepage": "https://github.com/liliang-cn/aigui#readme",
|
|
23
23
|
"bugs": "https://github.com/liliang-cn/aigui/issues",
|
|
24
24
|
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
25
29
|
"main": "./dist/index.cjs",
|
|
26
30
|
"module": "./dist/index.js",
|
|
27
31
|
"types": "./dist/index.d.ts",
|
|
28
32
|
"exports": {
|
|
29
33
|
".": {
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
},
|
|
35
44
|
"files": [
|
|
@@ -41,7 +50,7 @@
|
|
|
41
50
|
"access": "public"
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.3.0"
|
|
45
54
|
},
|
|
46
55
|
"peerDependencies": {
|
|
47
56
|
"react": ">=18"
|
|
@@ -56,6 +65,7 @@
|
|
|
56
65
|
},
|
|
57
66
|
"scripts": {
|
|
58
67
|
"build": "tsdown",
|
|
68
|
+
"test": "pnpm --dir ../.. exec vitest run --project react",
|
|
59
69
|
"typecheck": "tsc --noEmit"
|
|
60
70
|
}
|
|
61
71
|
}
|