@ai-gui/react 0.2.0 → 0.4.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 +194 -28
- package/dist/index.d.cts +30 -8
- package/dist/index.d.ts +30 -8
- package/dist/index.js +196 -31
- package/package.json +4 -3
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
|
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
//#endregion
|
|
25
25
|
const react = __toESM(require("react"));
|
|
26
26
|
const __ai_gui_core = __toESM(require("@ai-gui/core"));
|
|
27
|
+
const react_dom = __toESM(require("react-dom"));
|
|
27
28
|
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
|
|
28
29
|
|
|
29
30
|
//#region src/apply-patches.ts
|
|
@@ -66,7 +67,9 @@ function useAIRenderer(options = {}) {
|
|
|
66
67
|
options.registry,
|
|
67
68
|
options.sanitize,
|
|
68
69
|
options.plugins,
|
|
69
|
-
options.scheduler
|
|
70
|
+
options.scheduler,
|
|
71
|
+
options.debug,
|
|
72
|
+
options.onDebugEvent
|
|
70
73
|
]);
|
|
71
74
|
(0, react.useEffect)(() => {
|
|
72
75
|
setNodes([]);
|
|
@@ -84,6 +87,7 @@ function useAIRenderer(options = {}) {
|
|
|
84
87
|
setNodes([]);
|
|
85
88
|
}, [session]);
|
|
86
89
|
return {
|
|
90
|
+
renderer: session.renderer,
|
|
87
91
|
nodes,
|
|
88
92
|
push,
|
|
89
93
|
feed,
|
|
@@ -91,40 +95,104 @@ function useAIRenderer(options = {}) {
|
|
|
91
95
|
};
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/use-action-state.ts
|
|
100
|
+
function useActionState(runtime, key) {
|
|
101
|
+
const idle = (0, react.useMemo)(() => (0, __ai_gui_core.getIdleActionState)(key), [key]);
|
|
102
|
+
const subscribe = (0, react.useCallback)((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
|
|
103
|
+
const getSnapshot = (0, react.useCallback)(() => {
|
|
104
|
+
const state = runtime?.getState(key) ?? idle;
|
|
105
|
+
return state.status === "idle" ? idle : state;
|
|
106
|
+
}, [
|
|
107
|
+
idle,
|
|
108
|
+
key,
|
|
109
|
+
runtime
|
|
110
|
+
]);
|
|
111
|
+
return (0, react.useSyncExternalStore)(subscribe, getSnapshot, () => idle);
|
|
112
|
+
}
|
|
113
|
+
|
|
94
114
|
//#endregion
|
|
95
115
|
//#region src/render-output.tsx
|
|
96
116
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
97
|
-
function renderOutput(out, key, sanitize) {
|
|
117
|
+
function renderOutput(out, key, sanitize, context) {
|
|
98
118
|
switch (out.kind) {
|
|
99
119
|
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
100
120
|
case "element": return (0, react.createElement)(out.tag, {
|
|
101
121
|
key,
|
|
102
122
|
...out.props
|
|
103
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
123
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize, context)));
|
|
104
124
|
case "card": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
105
125
|
"data-aigui-card-fallback": true,
|
|
106
126
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(out.data, null, 2) })
|
|
107
127
|
}, key);
|
|
108
|
-
case "mount": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MountHost, {
|
|
128
|
+
case "mount": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MountHost, {
|
|
129
|
+
mount: out.mount,
|
|
130
|
+
context
|
|
131
|
+
}, key);
|
|
109
132
|
}
|
|
110
133
|
}
|
|
111
134
|
/** Host a framework-neutral imperative mount into a managed DOM element. */
|
|
112
|
-
function MountHost({ mount }) {
|
|
135
|
+
function MountHost({ mount, context }) {
|
|
113
136
|
const ref = (0, react.useRef)(null);
|
|
137
|
+
const [slots, setSlots] = (0, react.useState)([]);
|
|
114
138
|
(0, react.useEffect)(() => {
|
|
115
139
|
if (!ref.current) return;
|
|
116
|
-
const
|
|
140
|
+
const mountContext = createMountContext(context, setSlots);
|
|
141
|
+
const cleanup = mount(ref.current, mountContext);
|
|
117
142
|
return () => {
|
|
118
143
|
if (typeof cleanup === "function") cleanup();
|
|
119
144
|
};
|
|
120
|
-
}, [
|
|
121
|
-
|
|
145
|
+
}, [
|
|
146
|
+
mount,
|
|
147
|
+
context?.registry,
|
|
148
|
+
context?.onCardAction
|
|
149
|
+
]);
|
|
150
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
122
151
|
ref,
|
|
123
152
|
"data-aigui-mount": true
|
|
124
|
-
})
|
|
153
|
+
}), slots.map((slot) => (0, react_dom.createPortal)((0, react.createElement)(slot.Comp, {
|
|
154
|
+
data: slot.data,
|
|
155
|
+
onAction: (action) => context?.onCardAction?.({
|
|
156
|
+
...action,
|
|
157
|
+
cardType: slot.type
|
|
158
|
+
})
|
|
159
|
+
}), slot.host, slot.id))] });
|
|
160
|
+
}
|
|
161
|
+
let nextCardSlotId = 0;
|
|
162
|
+
function createMountContext(context, setSlots) {
|
|
163
|
+
return { mountCard(host, request) {
|
|
164
|
+
return mountCard(host, request, context, setSlots);
|
|
165
|
+
} };
|
|
166
|
+
}
|
|
167
|
+
function mountCard(host, request, context, setSlots) {
|
|
168
|
+
const Comp = context?.registry?.getRender(request.type);
|
|
169
|
+
if (!Comp) return void 0;
|
|
170
|
+
const id = String(nextCardSlotId++);
|
|
171
|
+
let destroyed = false;
|
|
172
|
+
setSlots((slots) => [...slots, {
|
|
173
|
+
id,
|
|
174
|
+
host,
|
|
175
|
+
type: request.type,
|
|
176
|
+
data: request.data,
|
|
177
|
+
Comp
|
|
178
|
+
}]);
|
|
179
|
+
return {
|
|
180
|
+
update(data) {
|
|
181
|
+
if (destroyed) return;
|
|
182
|
+
setSlots((slots) => slots.map((slot) => slot.id === id ? {
|
|
183
|
+
...slot,
|
|
184
|
+
data
|
|
185
|
+
} : slot));
|
|
186
|
+
},
|
|
187
|
+
destroy() {
|
|
188
|
+
if (destroyed) return;
|
|
189
|
+
destroyed = true;
|
|
190
|
+
setSlots((slots) => slots.filter((slot) => slot.id !== id));
|
|
191
|
+
}
|
|
192
|
+
};
|
|
125
193
|
}
|
|
126
194
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
127
|
-
function AsyncOutput({ promise, sanitize }) {
|
|
195
|
+
function AsyncOutput({ promise, sanitize, context }) {
|
|
128
196
|
const [resolved, setResolved] = (0, react.useState)(null);
|
|
129
197
|
const [failed, setFailed] = (0, react.useState)(false);
|
|
130
198
|
(0, react.useEffect)(() => {
|
|
@@ -143,7 +211,7 @@ function AsyncOutput({ promise, sanitize }) {
|
|
|
143
211
|
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
144
212
|
if (resolved === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-pending": true });
|
|
145
213
|
try {
|
|
146
|
-
return renderOutput(resolved, void 0, sanitize);
|
|
214
|
+
return renderOutput(resolved, void 0, sanitize, context);
|
|
147
215
|
} catch {
|
|
148
216
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
149
217
|
}
|
|
@@ -157,15 +225,22 @@ function sanitizeOutput(html, sanitize) {
|
|
|
157
225
|
//#region src/render-node.tsx
|
|
158
226
|
function renderNode(node, ctx) {
|
|
159
227
|
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
160
|
-
if (r)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
sanitize: ctx.sanitize
|
|
228
|
+
if (r) {
|
|
229
|
+
if (node.complete === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
230
|
+
"data-aigui-block-loading": "",
|
|
231
|
+
"data-block-type": node.type
|
|
165
232
|
}, node.key);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
233
|
+
try {
|
|
234
|
+
const out = r(node);
|
|
235
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
|
|
236
|
+
promise: out,
|
|
237
|
+
sanitize: ctx.sanitize,
|
|
238
|
+
context: ctx
|
|
239
|
+
}, node.key);
|
|
240
|
+
return renderOutput(out, node.key, ctx.sanitize, ctx);
|
|
241
|
+
} catch {
|
|
242
|
+
return renderFallback(node, ctx);
|
|
243
|
+
}
|
|
169
244
|
}
|
|
170
245
|
switch (node.type) {
|
|
171
246
|
case "heading": return (0, react.createElement)(node.tag ?? "h1", {
|
|
@@ -203,10 +278,15 @@ function renderCard(node, ctx) {
|
|
|
203
278
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
|
|
204
279
|
}, node.key);
|
|
205
280
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
281
|
+
if (card.id && ctx.cardStore) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatefulCardHost, {
|
|
282
|
+
cardStore: ctx.cardStore,
|
|
283
|
+
cardId: card.id,
|
|
284
|
+
cardType: card.type,
|
|
285
|
+
initialData: card.data,
|
|
286
|
+
Comp,
|
|
287
|
+
onCardAction: ctx.onCardAction
|
|
209
288
|
}, node.key);
|
|
289
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
210
290
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
211
291
|
data: card.data,
|
|
212
292
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -215,6 +295,49 @@ function renderCard(node, ctx) {
|
|
|
215
295
|
})
|
|
216
296
|
}, node.key);
|
|
217
297
|
}
|
|
298
|
+
const getServerCardSnapshot = () => void 0;
|
|
299
|
+
function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
|
|
300
|
+
const subscribe = (0, react.useCallback)((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
|
|
301
|
+
const getSnapshot = (0, react.useCallback)(() => cardStore.get(cardId), [cardStore, cardId]);
|
|
302
|
+
const record = (0, react.useSyncExternalStore)(subscribe, getSnapshot, getServerCardSnapshot);
|
|
303
|
+
(0, react.useEffect)(() => {
|
|
304
|
+
if (cardStore.get(cardId)) return;
|
|
305
|
+
try {
|
|
306
|
+
cardStore.register({
|
|
307
|
+
id: cardId,
|
|
308
|
+
type: cardType,
|
|
309
|
+
data: initialData
|
|
310
|
+
});
|
|
311
|
+
} catch {}
|
|
312
|
+
}, [
|
|
313
|
+
cardStore,
|
|
314
|
+
cardId,
|
|
315
|
+
cardType,
|
|
316
|
+
initialData
|
|
317
|
+
]);
|
|
318
|
+
if (!record) return renderCardFallback(void 0, initialData);
|
|
319
|
+
if (record.type !== cardType) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
320
|
+
"data-aigui-card-invalid": true,
|
|
321
|
+
"data-card-type": cardType,
|
|
322
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(initialData, null, 2) })
|
|
323
|
+
});
|
|
324
|
+
if (!Comp) return renderCardFallback(void 0, record.data);
|
|
325
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
326
|
+
data: record.data,
|
|
327
|
+
state: record.action,
|
|
328
|
+
onAction: (action) => onCardAction?.({
|
|
329
|
+
...action,
|
|
330
|
+
cardType,
|
|
331
|
+
cardId
|
|
332
|
+
})
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function renderCardFallback(key, data) {
|
|
336
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
337
|
+
"data-aigui-card-fallback": true,
|
|
338
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(data, null, 2) })
|
|
339
|
+
}, key);
|
|
340
|
+
}
|
|
218
341
|
function getCardComponent(registry, type) {
|
|
219
342
|
if (!registry) return void 0;
|
|
220
343
|
return registry.getRender(type);
|
|
@@ -222,29 +345,71 @@ function getCardComponent(registry, type) {
|
|
|
222
345
|
|
|
223
346
|
//#endregion
|
|
224
347
|
//#region src/ai-renderer.tsx
|
|
348
|
+
function createActionScope() {
|
|
349
|
+
return {
|
|
350
|
+
controller: new AbortController(),
|
|
351
|
+
owner: {}
|
|
352
|
+
};
|
|
353
|
+
}
|
|
225
354
|
const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
226
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
355
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
227
356
|
const opts = {
|
|
228
357
|
registry,
|
|
229
358
|
sanitize,
|
|
230
|
-
plugins
|
|
359
|
+
plugins,
|
|
360
|
+
debug,
|
|
361
|
+
onDebugEvent
|
|
231
362
|
};
|
|
232
|
-
const { nodes, push, feed, reset } = useAIRenderer(opts);
|
|
363
|
+
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
364
|
+
const actionScope = (0, react.useRef)(createActionScope());
|
|
365
|
+
(0, react.useEffect)(() => () => {
|
|
366
|
+
actionScope.current.controller.abort();
|
|
367
|
+
actionScope.current = createActionScope();
|
|
368
|
+
}, [
|
|
369
|
+
actionRuntime,
|
|
370
|
+
registry,
|
|
371
|
+
sanitize,
|
|
372
|
+
plugins
|
|
373
|
+
]);
|
|
374
|
+
const reset = (0, react.useCallback)(() => {
|
|
375
|
+
actionScope.current.controller.abort();
|
|
376
|
+
actionScope.current = createActionScope();
|
|
377
|
+
resetRenderer();
|
|
378
|
+
}, [resetRenderer]);
|
|
379
|
+
const handleCardAction = (0, react.useCallback)((action) => {
|
|
380
|
+
if (actionRuntime) {
|
|
381
|
+
const scope = actionScope.current;
|
|
382
|
+
actionRuntime.dispatch({
|
|
383
|
+
type: action.type,
|
|
384
|
+
params: action.params,
|
|
385
|
+
cardType: action.cardType,
|
|
386
|
+
cardId: action.cardId
|
|
387
|
+
}, {
|
|
388
|
+
signal: scope.controller.signal,
|
|
389
|
+
owner: scope.owner
|
|
390
|
+
}).catch(() => {});
|
|
391
|
+
}
|
|
392
|
+
onCardAction?.(action);
|
|
393
|
+
}, [actionRuntime, onCardAction]);
|
|
233
394
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
395
|
+
debugSource: "renderer",
|
|
396
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
234
397
|
push,
|
|
235
398
|
feed,
|
|
236
399
|
reset
|
|
237
400
|
}), [
|
|
401
|
+
renderer,
|
|
238
402
|
push,
|
|
239
403
|
feed,
|
|
240
404
|
reset
|
|
241
405
|
]);
|
|
242
|
-
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins), [plugins]);
|
|
406
|
+
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
243
407
|
const ctx = {
|
|
244
408
|
registry,
|
|
409
|
+
cardStore,
|
|
245
410
|
plugins,
|
|
246
411
|
nodeRenderers,
|
|
247
|
-
onCardAction,
|
|
412
|
+
onCardAction: handleCardAction,
|
|
248
413
|
sanitize,
|
|
249
414
|
sanitized: true
|
|
250
415
|
};
|
|
@@ -260,4 +425,5 @@ exports.AIRenderer = AIRenderer
|
|
|
260
425
|
exports.applyPatches = applyPatches
|
|
261
426
|
exports.renderNode = renderNode
|
|
262
427
|
exports.renderOutput = renderOutput
|
|
263
|
-
exports.useAIRenderer = useAIRenderer
|
|
428
|
+
exports.useAIRenderer = useAIRenderer
|
|
429
|
+
exports.useActionState = useActionState
|
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, 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
11
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
@@ -12,46 +13,67 @@ interface UseAIRendererResult {
|
|
|
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>;
|
|
21
|
-
onCardAction?: (action:
|
|
22
|
-
type: string;
|
|
23
|
-
params?: unknown;
|
|
24
|
-
cardType: string;
|
|
25
|
-
}) => void;
|
|
33
|
+
onCardAction?: (action: CardActionPayload) => void;
|
|
26
34
|
sanitize?: RendererOptions["sanitize"];
|
|
27
35
|
sanitized?: boolean;
|
|
28
36
|
}
|
|
29
37
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
|
+
interface CardComponentProps {
|
|
39
|
+
data: unknown;
|
|
40
|
+
state?: CardAction;
|
|
41
|
+
onAction: (a: {
|
|
42
|
+
type: string;
|
|
43
|
+
params?: unknown;
|
|
44
|
+
}) => void;
|
|
45
|
+
}
|
|
30
46
|
|
|
31
47
|
//#endregion
|
|
32
48
|
//#region src/ai-renderer.d.ts
|
|
33
49
|
interface AIRendererHandle {
|
|
50
|
+
readonly debugSource: "renderer";
|
|
51
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
34
52
|
push: (chunk: string) => void;
|
|
35
53
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
36
54
|
reset: () => void;
|
|
37
55
|
}
|
|
38
56
|
interface AIRendererProps {
|
|
39
57
|
registry?: CardRegistry;
|
|
58
|
+
cardStore?: CardStore;
|
|
40
59
|
sanitize?: RendererOptions["sanitize"];
|
|
41
60
|
plugins?: AIGuiPlugin[];
|
|
61
|
+
actionRuntime?: ActionRuntime;
|
|
42
62
|
onCardAction?: RenderContext["onCardAction"];
|
|
43
63
|
className?: string;
|
|
64
|
+
debug?: RendererOptions["debug"];
|
|
65
|
+
onDebugEvent?: RendererOptions["onDebugEvent"];
|
|
44
66
|
}
|
|
45
67
|
declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
|
|
46
68
|
|
|
47
69
|
//#endregion
|
|
48
70
|
//#region src/render-output.d.ts
|
|
49
71
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
50
|
-
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
72
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"], context?: RenderContext): ReactNode;
|
|
51
73
|
|
|
52
74
|
//#endregion
|
|
53
75
|
//#region src/apply-patches.d.ts
|
|
54
76
|
declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
|
|
55
77
|
|
|
56
78
|
//#endregion
|
|
57
|
-
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,10 +1,11 @@
|
|
|
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, FeedOptions, FeedSource, NodeRenderer, 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
11
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
@@ -12,46 +13,67 @@ interface UseAIRendererResult {
|
|
|
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>;
|
|
21
|
-
onCardAction?: (action:
|
|
22
|
-
type: string;
|
|
23
|
-
params?: unknown;
|
|
24
|
-
cardType: string;
|
|
25
|
-
}) => void;
|
|
33
|
+
onCardAction?: (action: CardActionPayload) => void;
|
|
26
34
|
sanitize?: RendererOptions["sanitize"];
|
|
27
35
|
sanitized?: boolean;
|
|
28
36
|
}
|
|
29
37
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
|
+
interface CardComponentProps {
|
|
39
|
+
data: unknown;
|
|
40
|
+
state?: CardAction;
|
|
41
|
+
onAction: (a: {
|
|
42
|
+
type: string;
|
|
43
|
+
params?: unknown;
|
|
44
|
+
}) => void;
|
|
45
|
+
}
|
|
30
46
|
|
|
31
47
|
//#endregion
|
|
32
48
|
//#region src/ai-renderer.d.ts
|
|
33
49
|
interface AIRendererHandle {
|
|
50
|
+
readonly debugSource: "renderer";
|
|
51
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
34
52
|
push: (chunk: string) => void;
|
|
35
53
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
36
54
|
reset: () => void;
|
|
37
55
|
}
|
|
38
56
|
interface AIRendererProps {
|
|
39
57
|
registry?: CardRegistry;
|
|
58
|
+
cardStore?: CardStore;
|
|
40
59
|
sanitize?: RendererOptions["sanitize"];
|
|
41
60
|
plugins?: AIGuiPlugin[];
|
|
61
|
+
actionRuntime?: ActionRuntime;
|
|
42
62
|
onCardAction?: RenderContext["onCardAction"];
|
|
43
63
|
className?: string;
|
|
64
|
+
debug?: RendererOptions["debug"];
|
|
65
|
+
onDebugEvent?: RendererOptions["onDebugEvent"];
|
|
44
66
|
}
|
|
45
67
|
declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
|
|
46
68
|
|
|
47
69
|
//#endregion
|
|
48
70
|
//#region src/render-output.d.ts
|
|
49
71
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
50
|
-
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
72
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"], context?: RenderContext): ReactNode;
|
|
51
73
|
|
|
52
74
|
//#endregion
|
|
53
75
|
//#region src/apply-patches.d.ts
|
|
54
76
|
declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
|
|
55
77
|
|
|
56
78
|
//#endregion
|
|
57
|
-
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,6 +1,7 @@
|
|
|
1
|
-
import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
2
|
-
import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
|
|
3
|
-
import {
|
|
1
|
+
import { Fragment, createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
+
import { Renderer, collectNodeRenderers, getIdleActionState, sanitizeHtml } from "@ai-gui/core";
|
|
3
|
+
import { createPortal } from "react-dom";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
|
|
5
6
|
//#region src/apply-patches.ts
|
|
6
7
|
function applyPatches(nodes, patches) {
|
|
@@ -42,7 +43,9 @@ function useAIRenderer(options = {}) {
|
|
|
42
43
|
options.registry,
|
|
43
44
|
options.sanitize,
|
|
44
45
|
options.plugins,
|
|
45
|
-
options.scheduler
|
|
46
|
+
options.scheduler,
|
|
47
|
+
options.debug,
|
|
48
|
+
options.onDebugEvent
|
|
46
49
|
]);
|
|
47
50
|
useEffect(() => {
|
|
48
51
|
setNodes([]);
|
|
@@ -60,6 +63,7 @@ function useAIRenderer(options = {}) {
|
|
|
60
63
|
setNodes([]);
|
|
61
64
|
}, [session]);
|
|
62
65
|
return {
|
|
66
|
+
renderer: session.renderer,
|
|
63
67
|
nodes,
|
|
64
68
|
push,
|
|
65
69
|
feed,
|
|
@@ -67,40 +71,104 @@ function useAIRenderer(options = {}) {
|
|
|
67
71
|
};
|
|
68
72
|
}
|
|
69
73
|
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/use-action-state.ts
|
|
76
|
+
function useActionState(runtime, key) {
|
|
77
|
+
const idle = useMemo(() => getIdleActionState(key), [key]);
|
|
78
|
+
const subscribe = useCallback((notify) => runtime?.subscribe(() => notify()) ?? (() => {}), [runtime]);
|
|
79
|
+
const getSnapshot = useCallback(() => {
|
|
80
|
+
const state = runtime?.getState(key) ?? idle;
|
|
81
|
+
return state.status === "idle" ? idle : state;
|
|
82
|
+
}, [
|
|
83
|
+
idle,
|
|
84
|
+
key,
|
|
85
|
+
runtime
|
|
86
|
+
]);
|
|
87
|
+
return useSyncExternalStore(subscribe, getSnapshot, () => idle);
|
|
88
|
+
}
|
|
89
|
+
|
|
70
90
|
//#endregion
|
|
71
91
|
//#region src/render-output.tsx
|
|
72
92
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
73
|
-
function renderOutput(out, key, sanitize) {
|
|
93
|
+
function renderOutput(out, key, sanitize, context) {
|
|
74
94
|
switch (out.kind) {
|
|
75
95
|
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
76
96
|
case "element": return createElement(out.tag, {
|
|
77
97
|
key,
|
|
78
98
|
...out.props
|
|
79
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
99
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize, context)));
|
|
80
100
|
case "card": return /* @__PURE__ */ jsx("pre", {
|
|
81
101
|
"data-aigui-card-fallback": true,
|
|
82
102
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(out.data, null, 2) })
|
|
83
103
|
}, key);
|
|
84
|
-
case "mount": return /* @__PURE__ */ jsx(MountHost, {
|
|
104
|
+
case "mount": return /* @__PURE__ */ jsx(MountHost, {
|
|
105
|
+
mount: out.mount,
|
|
106
|
+
context
|
|
107
|
+
}, key);
|
|
85
108
|
}
|
|
86
109
|
}
|
|
87
110
|
/** Host a framework-neutral imperative mount into a managed DOM element. */
|
|
88
|
-
function MountHost({ mount }) {
|
|
111
|
+
function MountHost({ mount, context }) {
|
|
89
112
|
const ref = useRef(null);
|
|
113
|
+
const [slots, setSlots] = useState([]);
|
|
90
114
|
useEffect(() => {
|
|
91
115
|
if (!ref.current) return;
|
|
92
|
-
const
|
|
116
|
+
const mountContext = createMountContext(context, setSlots);
|
|
117
|
+
const cleanup = mount(ref.current, mountContext);
|
|
93
118
|
return () => {
|
|
94
119
|
if (typeof cleanup === "function") cleanup();
|
|
95
120
|
};
|
|
96
|
-
}, [
|
|
97
|
-
|
|
121
|
+
}, [
|
|
122
|
+
mount,
|
|
123
|
+
context?.registry,
|
|
124
|
+
context?.onCardAction
|
|
125
|
+
]);
|
|
126
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
|
|
98
127
|
ref,
|
|
99
128
|
"data-aigui-mount": true
|
|
100
|
-
})
|
|
129
|
+
}), slots.map((slot) => createPortal(createElement(slot.Comp, {
|
|
130
|
+
data: slot.data,
|
|
131
|
+
onAction: (action) => context?.onCardAction?.({
|
|
132
|
+
...action,
|
|
133
|
+
cardType: slot.type
|
|
134
|
+
})
|
|
135
|
+
}), slot.host, slot.id))] });
|
|
136
|
+
}
|
|
137
|
+
let nextCardSlotId = 0;
|
|
138
|
+
function createMountContext(context, setSlots) {
|
|
139
|
+
return { mountCard(host, request) {
|
|
140
|
+
return mountCard(host, request, context, setSlots);
|
|
141
|
+
} };
|
|
142
|
+
}
|
|
143
|
+
function mountCard(host, request, context, setSlots) {
|
|
144
|
+
const Comp = context?.registry?.getRender(request.type);
|
|
145
|
+
if (!Comp) return void 0;
|
|
146
|
+
const id = String(nextCardSlotId++);
|
|
147
|
+
let destroyed = false;
|
|
148
|
+
setSlots((slots) => [...slots, {
|
|
149
|
+
id,
|
|
150
|
+
host,
|
|
151
|
+
type: request.type,
|
|
152
|
+
data: request.data,
|
|
153
|
+
Comp
|
|
154
|
+
}]);
|
|
155
|
+
return {
|
|
156
|
+
update(data) {
|
|
157
|
+
if (destroyed) return;
|
|
158
|
+
setSlots((slots) => slots.map((slot) => slot.id === id ? {
|
|
159
|
+
...slot,
|
|
160
|
+
data
|
|
161
|
+
} : slot));
|
|
162
|
+
},
|
|
163
|
+
destroy() {
|
|
164
|
+
if (destroyed) return;
|
|
165
|
+
destroyed = true;
|
|
166
|
+
setSlots((slots) => slots.filter((slot) => slot.id !== id));
|
|
167
|
+
}
|
|
168
|
+
};
|
|
101
169
|
}
|
|
102
170
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
103
|
-
function AsyncOutput({ promise, sanitize }) {
|
|
171
|
+
function AsyncOutput({ promise, sanitize, context }) {
|
|
104
172
|
const [resolved, setResolved] = useState(null);
|
|
105
173
|
const [failed, setFailed] = useState(false);
|
|
106
174
|
useEffect(() => {
|
|
@@ -119,7 +187,7 @@ function AsyncOutput({ promise, sanitize }) {
|
|
|
119
187
|
if (failed) return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
120
188
|
if (resolved === null) return /* @__PURE__ */ jsx("span", { "data-aigui-async-pending": true });
|
|
121
189
|
try {
|
|
122
|
-
return renderOutput(resolved, void 0, sanitize);
|
|
190
|
+
return renderOutput(resolved, void 0, sanitize, context);
|
|
123
191
|
} catch {
|
|
124
192
|
return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
125
193
|
}
|
|
@@ -133,15 +201,22 @@ function sanitizeOutput(html, sanitize) {
|
|
|
133
201
|
//#region src/render-node.tsx
|
|
134
202
|
function renderNode(node, ctx) {
|
|
135
203
|
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
136
|
-
if (r)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
sanitize: ctx.sanitize
|
|
204
|
+
if (r) {
|
|
205
|
+
if (node.complete === false) return /* @__PURE__ */ jsx("div", {
|
|
206
|
+
"data-aigui-block-loading": "",
|
|
207
|
+
"data-block-type": node.type
|
|
141
208
|
}, node.key);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
209
|
+
try {
|
|
210
|
+
const out = r(node);
|
|
211
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
212
|
+
promise: out,
|
|
213
|
+
sanitize: ctx.sanitize,
|
|
214
|
+
context: ctx
|
|
215
|
+
}, node.key);
|
|
216
|
+
return renderOutput(out, node.key, ctx.sanitize, ctx);
|
|
217
|
+
} catch {
|
|
218
|
+
return renderFallback(node, ctx);
|
|
219
|
+
}
|
|
145
220
|
}
|
|
146
221
|
switch (node.type) {
|
|
147
222
|
case "heading": return createElement(node.tag ?? "h1", {
|
|
@@ -179,10 +254,15 @@ function renderCard(node, ctx) {
|
|
|
179
254
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
|
|
180
255
|
}, node.key);
|
|
181
256
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
257
|
+
if (card.id && ctx.cardStore) return /* @__PURE__ */ jsx(StatefulCardHost, {
|
|
258
|
+
cardStore: ctx.cardStore,
|
|
259
|
+
cardId: card.id,
|
|
260
|
+
cardType: card.type,
|
|
261
|
+
initialData: card.data,
|
|
262
|
+
Comp,
|
|
263
|
+
onCardAction: ctx.onCardAction
|
|
185
264
|
}, node.key);
|
|
265
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
186
266
|
return /* @__PURE__ */ jsx(Comp, {
|
|
187
267
|
data: card.data,
|
|
188
268
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -191,6 +271,49 @@ function renderCard(node, ctx) {
|
|
|
191
271
|
})
|
|
192
272
|
}, node.key);
|
|
193
273
|
}
|
|
274
|
+
const getServerCardSnapshot = () => void 0;
|
|
275
|
+
function StatefulCardHost({ cardStore, cardId, cardType, initialData, Comp, onCardAction }) {
|
|
276
|
+
const subscribe = useCallback((notify) => cardStore.subscribe(cardId, notify), [cardStore, cardId]);
|
|
277
|
+
const getSnapshot = useCallback(() => cardStore.get(cardId), [cardStore, cardId]);
|
|
278
|
+
const record = useSyncExternalStore(subscribe, getSnapshot, getServerCardSnapshot);
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (cardStore.get(cardId)) return;
|
|
281
|
+
try {
|
|
282
|
+
cardStore.register({
|
|
283
|
+
id: cardId,
|
|
284
|
+
type: cardType,
|
|
285
|
+
data: initialData
|
|
286
|
+
});
|
|
287
|
+
} catch {}
|
|
288
|
+
}, [
|
|
289
|
+
cardStore,
|
|
290
|
+
cardId,
|
|
291
|
+
cardType,
|
|
292
|
+
initialData
|
|
293
|
+
]);
|
|
294
|
+
if (!record) return renderCardFallback(void 0, initialData);
|
|
295
|
+
if (record.type !== cardType) return /* @__PURE__ */ jsx("pre", {
|
|
296
|
+
"data-aigui-card-invalid": true,
|
|
297
|
+
"data-card-type": cardType,
|
|
298
|
+
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(initialData, null, 2) })
|
|
299
|
+
});
|
|
300
|
+
if (!Comp) return renderCardFallback(void 0, record.data);
|
|
301
|
+
return /* @__PURE__ */ jsx(Comp, {
|
|
302
|
+
data: record.data,
|
|
303
|
+
state: record.action,
|
|
304
|
+
onAction: (action) => onCardAction?.({
|
|
305
|
+
...action,
|
|
306
|
+
cardType,
|
|
307
|
+
cardId
|
|
308
|
+
})
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
function renderCardFallback(key, data) {
|
|
312
|
+
return /* @__PURE__ */ jsx("pre", {
|
|
313
|
+
"data-aigui-card-fallback": true,
|
|
314
|
+
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(data, null, 2) })
|
|
315
|
+
}, key);
|
|
316
|
+
}
|
|
194
317
|
function getCardComponent(registry, type) {
|
|
195
318
|
if (!registry) return void 0;
|
|
196
319
|
return registry.getRender(type);
|
|
@@ -198,29 +321,71 @@ function getCardComponent(registry, type) {
|
|
|
198
321
|
|
|
199
322
|
//#endregion
|
|
200
323
|
//#region src/ai-renderer.tsx
|
|
324
|
+
function createActionScope() {
|
|
325
|
+
return {
|
|
326
|
+
controller: new AbortController(),
|
|
327
|
+
owner: {}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
201
330
|
const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
202
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
331
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
203
332
|
const opts = {
|
|
204
333
|
registry,
|
|
205
334
|
sanitize,
|
|
206
|
-
plugins
|
|
335
|
+
plugins,
|
|
336
|
+
debug,
|
|
337
|
+
onDebugEvent
|
|
207
338
|
};
|
|
208
|
-
const { nodes, push, feed, reset } = useAIRenderer(opts);
|
|
339
|
+
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
340
|
+
const actionScope = useRef(createActionScope());
|
|
341
|
+
useEffect(() => () => {
|
|
342
|
+
actionScope.current.controller.abort();
|
|
343
|
+
actionScope.current = createActionScope();
|
|
344
|
+
}, [
|
|
345
|
+
actionRuntime,
|
|
346
|
+
registry,
|
|
347
|
+
sanitize,
|
|
348
|
+
plugins
|
|
349
|
+
]);
|
|
350
|
+
const reset = useCallback(() => {
|
|
351
|
+
actionScope.current.controller.abort();
|
|
352
|
+
actionScope.current = createActionScope();
|
|
353
|
+
resetRenderer();
|
|
354
|
+
}, [resetRenderer]);
|
|
355
|
+
const handleCardAction = useCallback((action) => {
|
|
356
|
+
if (actionRuntime) {
|
|
357
|
+
const scope = actionScope.current;
|
|
358
|
+
actionRuntime.dispatch({
|
|
359
|
+
type: action.type,
|
|
360
|
+
params: action.params,
|
|
361
|
+
cardType: action.cardType,
|
|
362
|
+
cardId: action.cardId
|
|
363
|
+
}, {
|
|
364
|
+
signal: scope.controller.signal,
|
|
365
|
+
owner: scope.owner
|
|
366
|
+
}).catch(() => {});
|
|
367
|
+
}
|
|
368
|
+
onCardAction?.(action);
|
|
369
|
+
}, [actionRuntime, onCardAction]);
|
|
209
370
|
useImperativeHandle(ref, () => ({
|
|
371
|
+
debugSource: "renderer",
|
|
372
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
210
373
|
push,
|
|
211
374
|
feed,
|
|
212
375
|
reset
|
|
213
376
|
}), [
|
|
377
|
+
renderer,
|
|
214
378
|
push,
|
|
215
379
|
feed,
|
|
216
380
|
reset
|
|
217
381
|
]);
|
|
218
|
-
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins), [plugins]);
|
|
382
|
+
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
219
383
|
const ctx = {
|
|
220
384
|
registry,
|
|
385
|
+
cardStore,
|
|
221
386
|
plugins,
|
|
222
387
|
nodeRenderers,
|
|
223
|
-
onCardAction,
|
|
388
|
+
onCardAction: handleCardAction,
|
|
224
389
|
sanitize,
|
|
225
390
|
sanitized: true
|
|
226
391
|
};
|
|
@@ -232,4 +397,4 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
232
397
|
});
|
|
233
398
|
|
|
234
399
|
//#endregion
|
|
235
|
-
export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer };
|
|
400
|
+
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.4.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",
|
|
@@ -50,10 +50,11 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.4.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"react": ">=18"
|
|
56
|
+
"react": ">=18",
|
|
57
|
+
"react-dom": ">=18"
|
|
57
58
|
},
|
|
58
59
|
"devDependencies": {
|
|
59
60
|
"react": "^18.3.1",
|