@ai-gui/react 0.4.2 → 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 +41 -5
- package/dist/index.d.cts +26 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +41 -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,12 +67,13 @@ 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
|
|
73
74
|
]);
|
|
74
75
|
(0, react.useEffect)(() => {
|
|
76
|
+
active.current = session.token;
|
|
75
77
|
setNodes([]);
|
|
76
78
|
return () => {
|
|
77
79
|
session.renderer.reset();
|
|
@@ -94,6 +96,14 @@ function useAIRenderer(options = {}) {
|
|
|
94
96
|
reset
|
|
95
97
|
};
|
|
96
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
|
+
}
|
|
97
107
|
|
|
98
108
|
//#endregion
|
|
99
109
|
//#region src/use-action-state.ts
|
|
@@ -255,11 +265,13 @@ function renderNode(node, ctx) {
|
|
|
255
265
|
function PluginOutputHost({ node, renderer, context }) {
|
|
256
266
|
const cache = (0, react.useRef)();
|
|
257
267
|
const signature = nodeSignature(node);
|
|
268
|
+
const theme = context.theme;
|
|
258
269
|
try {
|
|
259
|
-
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 = {
|
|
260
271
|
renderer,
|
|
261
272
|
signature,
|
|
262
|
-
|
|
273
|
+
theme,
|
|
274
|
+
output: renderer(node, { theme })
|
|
263
275
|
};
|
|
264
276
|
const output = cache.current.output;
|
|
265
277
|
if (output && typeof output.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
|
|
@@ -373,7 +385,7 @@ function createActionScope() {
|
|
|
373
385
|
};
|
|
374
386
|
}
|
|
375
387
|
const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
376
|
-
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;
|
|
377
389
|
const opts = {
|
|
378
390
|
registry,
|
|
379
391
|
sanitize,
|
|
@@ -383,6 +395,7 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
383
395
|
};
|
|
384
396
|
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
385
397
|
const actionScope = (0, react.useRef)(createActionScope());
|
|
398
|
+
const rendered = (0, react.useRef)("");
|
|
386
399
|
(0, react.useEffect)(() => () => {
|
|
387
400
|
actionScope.current.controller.abort();
|
|
388
401
|
actionScope.current = createActionScope();
|
|
@@ -395,8 +408,30 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
395
408
|
const reset = (0, react.useCallback)(() => {
|
|
396
409
|
actionScope.current.controller.abort();
|
|
397
410
|
actionScope.current = createActionScope();
|
|
411
|
+
rendered.current = "";
|
|
398
412
|
resetRenderer();
|
|
399
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]);
|
|
400
435
|
const handleCardAction = (0, react.useCallback)((action) => {
|
|
401
436
|
if (actionRuntime) {
|
|
402
437
|
const scope = actionScope.current;
|
|
@@ -432,7 +467,8 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
432
467
|
nodeRenderers,
|
|
433
468
|
onCardAction: handleCardAction,
|
|
434
469
|
sanitize,
|
|
435
|
-
sanitized: true
|
|
470
|
+
sanitized: true,
|
|
471
|
+
theme
|
|
436
472
|
};
|
|
437
473
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
438
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,12 +43,13 @@ 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
|
|
49
50
|
]);
|
|
50
51
|
useEffect(() => {
|
|
52
|
+
active.current = session.token;
|
|
51
53
|
setNodes([]);
|
|
52
54
|
return () => {
|
|
53
55
|
session.renderer.reset();
|
|
@@ -70,6 +72,14 @@ function useAIRenderer(options = {}) {
|
|
|
70
72
|
reset
|
|
71
73
|
};
|
|
72
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
|
+
}
|
|
73
83
|
|
|
74
84
|
//#endregion
|
|
75
85
|
//#region src/use-action-state.ts
|
|
@@ -231,11 +241,13 @@ function renderNode(node, ctx) {
|
|
|
231
241
|
function PluginOutputHost({ node, renderer, context }) {
|
|
232
242
|
const cache = useRef();
|
|
233
243
|
const signature = nodeSignature(node);
|
|
244
|
+
const theme = context.theme;
|
|
234
245
|
try {
|
|
235
|
-
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 = {
|
|
236
247
|
renderer,
|
|
237
248
|
signature,
|
|
238
|
-
|
|
249
|
+
theme,
|
|
250
|
+
output: renderer(node, { theme })
|
|
239
251
|
};
|
|
240
252
|
const output = cache.current.output;
|
|
241
253
|
if (output && typeof output.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
@@ -349,7 +361,7 @@ function createActionScope() {
|
|
|
349
361
|
};
|
|
350
362
|
}
|
|
351
363
|
const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
352
|
-
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;
|
|
353
365
|
const opts = {
|
|
354
366
|
registry,
|
|
355
367
|
sanitize,
|
|
@@ -359,6 +371,7 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
359
371
|
};
|
|
360
372
|
const { renderer, nodes, push, feed, reset: resetRenderer } = useAIRenderer(opts);
|
|
361
373
|
const actionScope = useRef(createActionScope());
|
|
374
|
+
const rendered = useRef("");
|
|
362
375
|
useEffect(() => () => {
|
|
363
376
|
actionScope.current.controller.abort();
|
|
364
377
|
actionScope.current = createActionScope();
|
|
@@ -371,8 +384,30 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
371
384
|
const reset = useCallback(() => {
|
|
372
385
|
actionScope.current.controller.abort();
|
|
373
386
|
actionScope.current = createActionScope();
|
|
387
|
+
rendered.current = "";
|
|
374
388
|
resetRenderer();
|
|
375
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]);
|
|
376
411
|
const handleCardAction = useCallback((action) => {
|
|
377
412
|
if (actionRuntime) {
|
|
378
413
|
const scope = actionScope.current;
|
|
@@ -408,7 +443,8 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
408
443
|
nodeRenderers,
|
|
409
444
|
onCardAction: handleCardAction,
|
|
410
445
|
sanitize,
|
|
411
|
-
sanitized: true
|
|
446
|
+
sanitized: true,
|
|
447
|
+
theme
|
|
412
448
|
};
|
|
413
449
|
return /* @__PURE__ */ jsx("div", {
|
|
414
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",
|