@ai-gui/react 0.2.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 +134 -18
- package/dist/index.d.cts +29 -7
- package/dist/index.d.ts +29 -7
- package/dist/index.js +135 -20
- package/package.json +2 -2
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
|
@@ -66,7 +66,9 @@ function useAIRenderer(options = {}) {
|
|
|
66
66
|
options.registry,
|
|
67
67
|
options.sanitize,
|
|
68
68
|
options.plugins,
|
|
69
|
-
options.scheduler
|
|
69
|
+
options.scheduler,
|
|
70
|
+
options.debug,
|
|
71
|
+
options.onDebugEvent
|
|
70
72
|
]);
|
|
71
73
|
(0, react.useEffect)(() => {
|
|
72
74
|
setNodes([]);
|
|
@@ -84,6 +86,7 @@ function useAIRenderer(options = {}) {
|
|
|
84
86
|
setNodes([]);
|
|
85
87
|
}, [session]);
|
|
86
88
|
return {
|
|
89
|
+
renderer: session.renderer,
|
|
87
90
|
nodes,
|
|
88
91
|
push,
|
|
89
92
|
feed,
|
|
@@ -91,6 +94,22 @@ function useAIRenderer(options = {}) {
|
|
|
91
94
|
};
|
|
92
95
|
}
|
|
93
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
|
+
|
|
94
113
|
//#endregion
|
|
95
114
|
//#region src/render-output.tsx
|
|
96
115
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
@@ -157,15 +176,21 @@ function sanitizeOutput(html, sanitize) {
|
|
|
157
176
|
//#region src/render-node.tsx
|
|
158
177
|
function renderNode(node, ctx) {
|
|
159
178
|
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
160
|
-
if (r)
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
sanitize: ctx.sanitize
|
|
179
|
+
if (r) {
|
|
180
|
+
if (node.complete === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
181
|
+
"data-aigui-block-loading": "",
|
|
182
|
+
"data-block-type": node.type
|
|
165
183
|
}, node.key);
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
+
}
|
|
169
194
|
}
|
|
170
195
|
switch (node.type) {
|
|
171
196
|
case "heading": return (0, react.createElement)(node.tag ?? "h1", {
|
|
@@ -203,10 +228,15 @@ function renderCard(node, ctx) {
|
|
|
203
228
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
|
|
204
229
|
}, node.key);
|
|
205
230
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
206
|
-
if (
|
|
207
|
-
|
|
208
|
-
|
|
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
|
|
209
238
|
}, node.key);
|
|
239
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
210
240
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
|
|
211
241
|
data: card.data,
|
|
212
242
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -215,6 +245,49 @@ function renderCard(node, ctx) {
|
|
|
215
245
|
})
|
|
216
246
|
}, node.key);
|
|
217
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
|
+
}
|
|
218
291
|
function getCardComponent(registry, type) {
|
|
219
292
|
if (!registry) return void 0;
|
|
220
293
|
return registry.getRender(type);
|
|
@@ -222,29 +295,71 @@ function getCardComponent(registry, type) {
|
|
|
222
295
|
|
|
223
296
|
//#endregion
|
|
224
297
|
//#region src/ai-renderer.tsx
|
|
298
|
+
function createActionScope() {
|
|
299
|
+
return {
|
|
300
|
+
controller: new AbortController(),
|
|
301
|
+
owner: {}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
225
304
|
const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
226
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
305
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
227
306
|
const opts = {
|
|
228
307
|
registry,
|
|
229
308
|
sanitize,
|
|
230
|
-
plugins
|
|
309
|
+
plugins,
|
|
310
|
+
debug,
|
|
311
|
+
onDebugEvent
|
|
231
312
|
};
|
|
232
|
-
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]);
|
|
233
344
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
345
|
+
debugSource: "renderer",
|
|
346
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
234
347
|
push,
|
|
235
348
|
feed,
|
|
236
349
|
reset
|
|
237
350
|
}), [
|
|
351
|
+
renderer,
|
|
238
352
|
push,
|
|
239
353
|
feed,
|
|
240
354
|
reset
|
|
241
355
|
]);
|
|
242
|
-
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins), [plugins]);
|
|
356
|
+
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
243
357
|
const ctx = {
|
|
244
358
|
registry,
|
|
359
|
+
cardStore,
|
|
245
360
|
plugins,
|
|
246
361
|
nodeRenderers,
|
|
247
|
-
onCardAction,
|
|
362
|
+
onCardAction: handleCardAction,
|
|
248
363
|
sanitize,
|
|
249
364
|
sanitized: true
|
|
250
365
|
};
|
|
@@ -260,4 +375,5 @@ exports.AIRenderer = AIRenderer
|
|
|
260
375
|
exports.applyPatches = applyPatches
|
|
261
376
|
exports.renderNode = renderNode
|
|
262
377
|
exports.renderOutput = renderOutput
|
|
263
|
-
exports.useAIRenderer = useAIRenderer
|
|
378
|
+
exports.useAIRenderer = useAIRenderer
|
|
379
|
+
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,35 +13,56 @@ 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
|
|
|
@@ -54,4 +76,4 @@ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: Render
|
|
|
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,35 +13,56 @@ 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
|
|
|
@@ -54,4 +76,4 @@ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: Render
|
|
|
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,5 +1,5 @@
|
|
|
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
5
|
//#region src/apply-patches.ts
|
|
@@ -42,7 +42,9 @@ function useAIRenderer(options = {}) {
|
|
|
42
42
|
options.registry,
|
|
43
43
|
options.sanitize,
|
|
44
44
|
options.plugins,
|
|
45
|
-
options.scheduler
|
|
45
|
+
options.scheduler,
|
|
46
|
+
options.debug,
|
|
47
|
+
options.onDebugEvent
|
|
46
48
|
]);
|
|
47
49
|
useEffect(() => {
|
|
48
50
|
setNodes([]);
|
|
@@ -60,6 +62,7 @@ function useAIRenderer(options = {}) {
|
|
|
60
62
|
setNodes([]);
|
|
61
63
|
}, [session]);
|
|
62
64
|
return {
|
|
65
|
+
renderer: session.renderer,
|
|
63
66
|
nodes,
|
|
64
67
|
push,
|
|
65
68
|
feed,
|
|
@@ -67,6 +70,22 @@ function useAIRenderer(options = {}) {
|
|
|
67
70
|
};
|
|
68
71
|
}
|
|
69
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
|
+
|
|
70
89
|
//#endregion
|
|
71
90
|
//#region src/render-output.tsx
|
|
72
91
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
@@ -133,15 +152,21 @@ function sanitizeOutput(html, sanitize) {
|
|
|
133
152
|
//#region src/render-node.tsx
|
|
134
153
|
function renderNode(node, ctx) {
|
|
135
154
|
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
136
|
-
if (r)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
sanitize: ctx.sanitize
|
|
155
|
+
if (r) {
|
|
156
|
+
if (node.complete === false) return /* @__PURE__ */ jsx("div", {
|
|
157
|
+
"data-aigui-block-loading": "",
|
|
158
|
+
"data-block-type": node.type
|
|
141
159
|
}, node.key);
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
}
|
|
145
170
|
}
|
|
146
171
|
switch (node.type) {
|
|
147
172
|
case "heading": return createElement(node.tag ?? "h1", {
|
|
@@ -179,10 +204,15 @@ function renderCard(node, ctx) {
|
|
|
179
204
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
|
|
180
205
|
}, node.key);
|
|
181
206
|
const Comp = getCardComponent(ctx.registry, card.type);
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
185
214
|
}, node.key);
|
|
215
|
+
if (!Comp) return renderCardFallback(node.key, card.data);
|
|
186
216
|
return /* @__PURE__ */ jsx(Comp, {
|
|
187
217
|
data: card.data,
|
|
188
218
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -191,6 +221,49 @@ function renderCard(node, ctx) {
|
|
|
191
221
|
})
|
|
192
222
|
}, node.key);
|
|
193
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
|
+
}
|
|
194
267
|
function getCardComponent(registry, type) {
|
|
195
268
|
if (!registry) return void 0;
|
|
196
269
|
return registry.getRender(type);
|
|
@@ -198,29 +271,71 @@ function getCardComponent(registry, type) {
|
|
|
198
271
|
|
|
199
272
|
//#endregion
|
|
200
273
|
//#region src/ai-renderer.tsx
|
|
274
|
+
function createActionScope() {
|
|
275
|
+
return {
|
|
276
|
+
controller: new AbortController(),
|
|
277
|
+
owner: {}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
201
280
|
const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
202
|
-
const { registry, sanitize, plugins, onCardAction, className } = props;
|
|
281
|
+
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
203
282
|
const opts = {
|
|
204
283
|
registry,
|
|
205
284
|
sanitize,
|
|
206
|
-
plugins
|
|
285
|
+
plugins,
|
|
286
|
+
debug,
|
|
287
|
+
onDebugEvent
|
|
207
288
|
};
|
|
208
|
-
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]);
|
|
209
320
|
useImperativeHandle(ref, () => ({
|
|
321
|
+
debugSource: "renderer",
|
|
322
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
210
323
|
push,
|
|
211
324
|
feed,
|
|
212
325
|
reset
|
|
213
326
|
}), [
|
|
327
|
+
renderer,
|
|
214
328
|
push,
|
|
215
329
|
feed,
|
|
216
330
|
reset
|
|
217
331
|
]);
|
|
218
|
-
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins), [plugins]);
|
|
332
|
+
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins, { debugTarget: renderer }), [plugins, renderer]);
|
|
219
333
|
const ctx = {
|
|
220
334
|
registry,
|
|
335
|
+
cardStore,
|
|
221
336
|
plugins,
|
|
222
337
|
nodeRenderers,
|
|
223
|
-
onCardAction,
|
|
338
|
+
onCardAction: handleCardAction,
|
|
224
339
|
sanitize,
|
|
225
340
|
sanitized: true
|
|
226
341
|
};
|
|
@@ -232,4 +347,4 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
232
347
|
});
|
|
233
348
|
|
|
234
349
|
//#endregion
|
|
235
|
-
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.3.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": ">=18"
|