@ai-gui/react 0.4.4 → 0.5.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/dist/index.cjs +40 -5
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +40 -5
- package/package.json +2 -2
package/dist/index.cjs
CHANGED
|
@@ -50,6 +50,7 @@ function applyPatches(nodes, patches) {
|
|
|
50
50
|
function useAIRenderer(options = {}) {
|
|
51
51
|
const [nodes, setNodes] = (0, react.useState)([]);
|
|
52
52
|
const active = (0, react.useRef)(null);
|
|
53
|
+
const pluginList = useStableList(options.plugins);
|
|
53
54
|
const session = (0, react.useMemo)(() => {
|
|
54
55
|
const token = {};
|
|
55
56
|
const renderer = new __ai_gui_core.Renderer({
|
|
@@ -66,7 +67,7 @@ function useAIRenderer(options = {}) {
|
|
|
66
67
|
}, [
|
|
67
68
|
options.registry,
|
|
68
69
|
options.sanitize,
|
|
69
|
-
|
|
70
|
+
pluginList,
|
|
70
71
|
options.scheduler,
|
|
71
72
|
options.debug,
|
|
72
73
|
options.onDebugEvent
|
|
@@ -95,6 +96,14 @@ function useAIRenderer(options = {}) {
|
|
|
95
96
|
reset
|
|
96
97
|
};
|
|
97
98
|
}
|
|
99
|
+
/** Keep the same array reference for as long as it holds the same members in the same order. */
|
|
100
|
+
function useStableList(list) {
|
|
101
|
+
const held = (0, react.useRef)(list);
|
|
102
|
+
const current = held.current;
|
|
103
|
+
const same = list === current || !!list && !!current && list.length === current.length && list.every((item, index) => item === current[index]);
|
|
104
|
+
if (!same) held.current = list;
|
|
105
|
+
return held.current;
|
|
106
|
+
}
|
|
98
107
|
|
|
99
108
|
//#endregion
|
|
100
109
|
//#region src/use-action-state.ts
|
|
@@ -256,11 +265,13 @@ function renderNode(node, ctx) {
|
|
|
256
265
|
function PluginOutputHost({ node, renderer, context }) {
|
|
257
266
|
const cache = (0, react.useRef)();
|
|
258
267
|
const signature = nodeSignature(node);
|
|
268
|
+
const theme = context.theme;
|
|
259
269
|
try {
|
|
260
|
-
if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature) cache.current = {
|
|
270
|
+
if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature || cache.current.theme !== theme) cache.current = {
|
|
261
271
|
renderer,
|
|
262
272
|
signature,
|
|
263
|
-
|
|
273
|
+
theme,
|
|
274
|
+
output: renderer(node, { theme })
|
|
264
275
|
};
|
|
265
276
|
const output = cache.current.output;
|
|
266
277
|
if (output && typeof output.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
|
|
@@ -374,7 +385,7 @@ function createActionScope() {
|
|
|
374
385
|
};
|
|
375
386
|
}
|
|
376
387
|
const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
377
|
-
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
388
|
+
const { text, registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, onRender, theme, className, debug, onDebugEvent } = props;
|
|
378
389
|
const opts = {
|
|
379
390
|
registry,
|
|
380
391
|
sanitize,
|
|
@@ -384,6 +395,7 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
384
395
|
};
|
|
385
396
|
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
386
397
|
const actionScope = (0, react.useRef)(createActionScope());
|
|
398
|
+
const rendered = (0, react.useRef)("");
|
|
387
399
|
(0, react.useEffect)(() => () => {
|
|
388
400
|
actionScope.current.controller.abort();
|
|
389
401
|
actionScope.current = createActionScope();
|
|
@@ -396,8 +408,30 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
396
408
|
const reset = (0, react.useCallback)(() => {
|
|
397
409
|
actionScope.current.controller.abort();
|
|
398
410
|
actionScope.current = createActionScope();
|
|
411
|
+
rendered.current = "";
|
|
399
412
|
resetRenderer();
|
|
400
413
|
}, [resetRenderer]);
|
|
414
|
+
(0, react.useEffect)(() => {
|
|
415
|
+
if (text === void 0 || text === rendered.current) return;
|
|
416
|
+
if (text.startsWith(rendered.current)) push(text.slice(rendered.current.length));
|
|
417
|
+
else {
|
|
418
|
+
reset();
|
|
419
|
+
push(text);
|
|
420
|
+
}
|
|
421
|
+
rendered.current = text;
|
|
422
|
+
}, [
|
|
423
|
+
text,
|
|
424
|
+
push,
|
|
425
|
+
reset
|
|
426
|
+
]);
|
|
427
|
+
const onRenderRef = (0, react.useRef)(onRender);
|
|
428
|
+
onRenderRef.current = onRender;
|
|
429
|
+
(0, react.useEffect)(() => {
|
|
430
|
+
onRenderRef.current?.(nodes);
|
|
431
|
+
}, [nodes]);
|
|
432
|
+
(0, react.useEffect)(() => () => {
|
|
433
|
+
rendered.current = "";
|
|
434
|
+
}, [renderer]);
|
|
401
435
|
const handleCardAction = (0, react.useCallback)((action) => {
|
|
402
436
|
if (actionRuntime) {
|
|
403
437
|
const scope = actionScope.current;
|
|
@@ -433,7 +467,8 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
433
467
|
nodeRenderers,
|
|
434
468
|
onCardAction: handleCardAction,
|
|
435
469
|
sanitize,
|
|
436
|
-
sanitized: true
|
|
470
|
+
sanitized: true,
|
|
471
|
+
theme
|
|
437
472
|
};
|
|
438
473
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
439
474
|
className,
|
package/dist/index.d.cts
CHANGED
|
@@ -33,6 +33,8 @@ interface RenderContext {
|
|
|
33
33
|
onCardAction?: (action: CardActionPayload) => void;
|
|
34
34
|
sanitize?: RendererOptions["sanitize"];
|
|
35
35
|
sanitized?: boolean;
|
|
36
|
+
/** The host's colour scheme, handed to every plugin that renders a node. */
|
|
37
|
+
theme?: string;
|
|
36
38
|
}
|
|
37
39
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
40
|
interface CardComponentProps {
|
|
@@ -54,12 +56,36 @@ interface AIRendererHandle {
|
|
|
54
56
|
reset: () => void;
|
|
55
57
|
}
|
|
56
58
|
interface AIRendererProps {
|
|
59
|
+
/**
|
|
60
|
+
* The whole text to render, as a controlled component.
|
|
61
|
+
*
|
|
62
|
+
* Streaming a model's answer means re-rendering with a longer string, and only the added part
|
|
63
|
+
* is new. Working that out is the renderer's job: pass the full text on every update and it
|
|
64
|
+
* pushes the delta, or starts over when the new text is not a continuation of what it holds.
|
|
65
|
+
* Leave it undefined to drive the renderer through the imperative handle instead.
|
|
66
|
+
*/
|
|
67
|
+
text?: string;
|
|
57
68
|
registry?: CardRegistry;
|
|
58
69
|
cardStore?: CardStore;
|
|
59
70
|
sanitize?: RendererOptions["sanitize"];
|
|
60
71
|
plugins?: AIGuiPlugin[];
|
|
61
72
|
actionRuntime?: ActionRuntime;
|
|
62
73
|
onCardAction?: RenderContext["onCardAction"];
|
|
74
|
+
/**
|
|
75
|
+
* Called with the nodes currently on screen, whenever they change.
|
|
76
|
+
*
|
|
77
|
+
* What a model produced is only knowable from the parsed nodes: a host that wants to offer
|
|
78
|
+
* "export this chart" or count the diagrams in an answer would otherwise have to watch the DOM
|
|
79
|
+
* for the elements a plugin happened to create.
|
|
80
|
+
*/
|
|
81
|
+
onRender?: (nodes: ASTNode[]) => void;
|
|
82
|
+
/**
|
|
83
|
+
* The host's colour scheme, "light" or "dark" by convention, handed to every plugin.
|
|
84
|
+
*
|
|
85
|
+
* Charts and diagrams choose their own colours and cannot see the page they sit on, so an
|
|
86
|
+
* answer rendered on a dark page comes back with white plot areas until the host says so.
|
|
87
|
+
*/
|
|
88
|
+
theme?: string;
|
|
63
89
|
className?: string;
|
|
64
90
|
debug?: RendererOptions["debug"];
|
|
65
91
|
onDebugEvent?: RendererOptions["onDebugEvent"];
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,8 @@ interface RenderContext {
|
|
|
33
33
|
onCardAction?: (action: CardActionPayload) => void;
|
|
34
34
|
sanitize?: RendererOptions["sanitize"];
|
|
35
35
|
sanitized?: boolean;
|
|
36
|
+
/** The host's colour scheme, handed to every plugin that renders a node. */
|
|
37
|
+
theme?: string;
|
|
36
38
|
}
|
|
37
39
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
38
40
|
interface CardComponentProps {
|
|
@@ -54,12 +56,36 @@ interface AIRendererHandle {
|
|
|
54
56
|
reset: () => void;
|
|
55
57
|
}
|
|
56
58
|
interface AIRendererProps {
|
|
59
|
+
/**
|
|
60
|
+
* The whole text to render, as a controlled component.
|
|
61
|
+
*
|
|
62
|
+
* Streaming a model's answer means re-rendering with a longer string, and only the added part
|
|
63
|
+
* is new. Working that out is the renderer's job: pass the full text on every update and it
|
|
64
|
+
* pushes the delta, or starts over when the new text is not a continuation of what it holds.
|
|
65
|
+
* Leave it undefined to drive the renderer through the imperative handle instead.
|
|
66
|
+
*/
|
|
67
|
+
text?: string;
|
|
57
68
|
registry?: CardRegistry;
|
|
58
69
|
cardStore?: CardStore;
|
|
59
70
|
sanitize?: RendererOptions["sanitize"];
|
|
60
71
|
plugins?: AIGuiPlugin[];
|
|
61
72
|
actionRuntime?: ActionRuntime;
|
|
62
73
|
onCardAction?: RenderContext["onCardAction"];
|
|
74
|
+
/**
|
|
75
|
+
* Called with the nodes currently on screen, whenever they change.
|
|
76
|
+
*
|
|
77
|
+
* What a model produced is only knowable from the parsed nodes: a host that wants to offer
|
|
78
|
+
* "export this chart" or count the diagrams in an answer would otherwise have to watch the DOM
|
|
79
|
+
* for the elements a plugin happened to create.
|
|
80
|
+
*/
|
|
81
|
+
onRender?: (nodes: ASTNode[]) => void;
|
|
82
|
+
/**
|
|
83
|
+
* The host's colour scheme, "light" or "dark" by convention, handed to every plugin.
|
|
84
|
+
*
|
|
85
|
+
* Charts and diagrams choose their own colours and cannot see the page they sit on, so an
|
|
86
|
+
* answer rendered on a dark page comes back with white plot areas until the host says so.
|
|
87
|
+
*/
|
|
88
|
+
theme?: string;
|
|
63
89
|
className?: string;
|
|
64
90
|
debug?: RendererOptions["debug"];
|
|
65
91
|
onDebugEvent?: RendererOptions["onDebugEvent"];
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,7 @@ function applyPatches(nodes, patches) {
|
|
|
26
26
|
function useAIRenderer(options = {}) {
|
|
27
27
|
const [nodes, setNodes] = useState([]);
|
|
28
28
|
const active = useRef(null);
|
|
29
|
+
const pluginList = useStableList(options.plugins);
|
|
29
30
|
const session = useMemo(() => {
|
|
30
31
|
const token = {};
|
|
31
32
|
const renderer = new Renderer({
|
|
@@ -42,7 +43,7 @@ function useAIRenderer(options = {}) {
|
|
|
42
43
|
}, [
|
|
43
44
|
options.registry,
|
|
44
45
|
options.sanitize,
|
|
45
|
-
|
|
46
|
+
pluginList,
|
|
46
47
|
options.scheduler,
|
|
47
48
|
options.debug,
|
|
48
49
|
options.onDebugEvent
|
|
@@ -71,6 +72,14 @@ function useAIRenderer(options = {}) {
|
|
|
71
72
|
reset
|
|
72
73
|
};
|
|
73
74
|
}
|
|
75
|
+
/** Keep the same array reference for as long as it holds the same members in the same order. */
|
|
76
|
+
function useStableList(list) {
|
|
77
|
+
const held = useRef(list);
|
|
78
|
+
const current = held.current;
|
|
79
|
+
const same = list === current || !!list && !!current && list.length === current.length && list.every((item, index) => item === current[index]);
|
|
80
|
+
if (!same) held.current = list;
|
|
81
|
+
return held.current;
|
|
82
|
+
}
|
|
74
83
|
|
|
75
84
|
//#endregion
|
|
76
85
|
//#region src/use-action-state.ts
|
|
@@ -232,11 +241,13 @@ function renderNode(node, ctx) {
|
|
|
232
241
|
function PluginOutputHost({ node, renderer, context }) {
|
|
233
242
|
const cache = useRef();
|
|
234
243
|
const signature = nodeSignature(node);
|
|
244
|
+
const theme = context.theme;
|
|
235
245
|
try {
|
|
236
|
-
if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature) cache.current = {
|
|
246
|
+
if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature || cache.current.theme !== theme) cache.current = {
|
|
237
247
|
renderer,
|
|
238
248
|
signature,
|
|
239
|
-
|
|
249
|
+
theme,
|
|
250
|
+
output: renderer(node, { theme })
|
|
240
251
|
};
|
|
241
252
|
const output = cache.current.output;
|
|
242
253
|
if (output && typeof output.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
@@ -350,7 +361,7 @@ function createActionScope() {
|
|
|
350
361
|
};
|
|
351
362
|
}
|
|
352
363
|
const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
353
|
-
const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
|
|
364
|
+
const { text, registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, onRender, theme, className, debug, onDebugEvent } = props;
|
|
354
365
|
const opts = {
|
|
355
366
|
registry,
|
|
356
367
|
sanitize,
|
|
@@ -360,6 +371,7 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
360
371
|
};
|
|
361
372
|
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
362
373
|
const actionScope = useRef(createActionScope());
|
|
374
|
+
const rendered = useRef("");
|
|
363
375
|
useEffect(() => () => {
|
|
364
376
|
actionScope.current.controller.abort();
|
|
365
377
|
actionScope.current = createActionScope();
|
|
@@ -372,8 +384,30 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
372
384
|
const reset = useCallback(() => {
|
|
373
385
|
actionScope.current.controller.abort();
|
|
374
386
|
actionScope.current = createActionScope();
|
|
387
|
+
rendered.current = "";
|
|
375
388
|
resetRenderer();
|
|
376
389
|
}, [resetRenderer]);
|
|
390
|
+
useEffect(() => {
|
|
391
|
+
if (text === void 0 || text === rendered.current) return;
|
|
392
|
+
if (text.startsWith(rendered.current)) push(text.slice(rendered.current.length));
|
|
393
|
+
else {
|
|
394
|
+
reset();
|
|
395
|
+
push(text);
|
|
396
|
+
}
|
|
397
|
+
rendered.current = text;
|
|
398
|
+
}, [
|
|
399
|
+
text,
|
|
400
|
+
push,
|
|
401
|
+
reset
|
|
402
|
+
]);
|
|
403
|
+
const onRenderRef = useRef(onRender);
|
|
404
|
+
onRenderRef.current = onRender;
|
|
405
|
+
useEffect(() => {
|
|
406
|
+
onRenderRef.current?.(nodes);
|
|
407
|
+
}, [nodes]);
|
|
408
|
+
useEffect(() => () => {
|
|
409
|
+
rendered.current = "";
|
|
410
|
+
}, [renderer]);
|
|
377
411
|
const handleCardAction = useCallback((action) => {
|
|
378
412
|
if (actionRuntime) {
|
|
379
413
|
const scope = actionScope.current;
|
|
@@ -409,7 +443,8 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
409
443
|
nodeRenderers,
|
|
410
444
|
onCardAction: handleCardAction,
|
|
411
445
|
sanitize,
|
|
412
|
-
sanitized: true
|
|
446
|
+
sanitized: true,
|
|
447
|
+
theme
|
|
413
448
|
};
|
|
414
449
|
return /* @__PURE__ */ jsx("div", {
|
|
415
450
|
className,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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.5.0"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"react": ">=18",
|