@ai-gui/react 0.1.0 → 0.2.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 +91 -35
- package/dist/index.d.cts +8 -5
- package/dist/index.d.ts +8 -5
- package/dist/index.js +91 -35
- package/package.json +15 -5
package/dist/index.cjs
CHANGED
|
@@ -26,25 +26,63 @@ const react = __toESM(require("react"));
|
|
|
26
26
|
const __ai_gui_core = __toESM(require("@ai-gui/core"));
|
|
27
27
|
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
|
|
28
28
|
|
|
29
|
+
//#region src/apply-patches.ts
|
|
30
|
+
function applyPatches(nodes, patches) {
|
|
31
|
+
let out = nodes.slice();
|
|
32
|
+
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
33
|
+
else if (p.op === "update") {
|
|
34
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
35
|
+
if (i >= 0) out[i] = p.node;
|
|
36
|
+
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
37
|
+
else if (p.op === "move") {
|
|
38
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
39
|
+
if (i >= 0) {
|
|
40
|
+
const [node] = out.splice(i, 1);
|
|
41
|
+
out.splice(p.index, 0, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
29
48
|
//#region src/use-ai-renderer.ts
|
|
30
49
|
function useAIRenderer(options = {}) {
|
|
31
50
|
const [nodes, setNodes] = (0, react.useState)([]);
|
|
32
|
-
const
|
|
33
|
-
|
|
51
|
+
const active = (0, react.useRef)(null);
|
|
52
|
+
const session = (0, react.useMemo)(() => {
|
|
53
|
+
const token = {};
|
|
54
|
+
const renderer = new __ai_gui_core.Renderer({
|
|
34
55
|
...options,
|
|
35
|
-
onPatch: (
|
|
56
|
+
onPatch: (patches) => {
|
|
57
|
+
if (active.current === token) setNodes((current) => applyPatches(current, patches));
|
|
58
|
+
}
|
|
36
59
|
});
|
|
60
|
+
active.current = token;
|
|
61
|
+
return {
|
|
62
|
+
renderer,
|
|
63
|
+
token
|
|
64
|
+
};
|
|
37
65
|
}, [
|
|
38
66
|
options.registry,
|
|
39
67
|
options.sanitize,
|
|
40
|
-
options.plugins
|
|
68
|
+
options.plugins,
|
|
69
|
+
options.scheduler
|
|
41
70
|
]);
|
|
42
|
-
|
|
43
|
-
|
|
71
|
+
(0, react.useEffect)(() => {
|
|
72
|
+
setNodes([]);
|
|
73
|
+
return () => {
|
|
74
|
+
session.renderer.reset();
|
|
75
|
+
if (active.current === session.token) active.current = null;
|
|
76
|
+
};
|
|
77
|
+
}, [session]);
|
|
78
|
+
const push = (0, react.useCallback)((chunk) => {
|
|
79
|
+
if (active.current === session.token) session.renderer.push(chunk);
|
|
80
|
+
}, [session]);
|
|
81
|
+
const feed = (0, react.useCallback)((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
|
|
44
82
|
const reset = (0, react.useCallback)(() => {
|
|
45
|
-
renderer.reset();
|
|
83
|
+
session.renderer.reset();
|
|
46
84
|
setNodes([]);
|
|
47
|
-
}, [
|
|
85
|
+
}, [session]);
|
|
48
86
|
return {
|
|
49
87
|
nodes,
|
|
50
88
|
push,
|
|
@@ -56,13 +94,13 @@ function useAIRenderer(options = {}) {
|
|
|
56
94
|
//#endregion
|
|
57
95
|
//#region src/render-output.tsx
|
|
58
96
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
59
|
-
function renderOutput(out, key) {
|
|
97
|
+
function renderOutput(out, key, sanitize) {
|
|
60
98
|
switch (out.kind) {
|
|
61
|
-
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: (
|
|
99
|
+
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
62
100
|
case "element": return (0, react.createElement)(out.tag, {
|
|
63
101
|
key,
|
|
64
102
|
...out.props
|
|
65
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
|
|
103
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
66
104
|
case "card": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
67
105
|
"data-aigui-card-fallback": true,
|
|
68
106
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(out.data, null, 2) })
|
|
@@ -79,53 +117,79 @@ function MountHost({ mount }) {
|
|
|
79
117
|
return () => {
|
|
80
118
|
if (typeof cleanup === "function") cleanup();
|
|
81
119
|
};
|
|
82
|
-
}, []);
|
|
120
|
+
}, [mount]);
|
|
83
121
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
84
122
|
ref,
|
|
85
123
|
"data-aigui-mount": true
|
|
86
124
|
});
|
|
87
125
|
}
|
|
88
126
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
89
|
-
function AsyncOutput({ promise }) {
|
|
127
|
+
function AsyncOutput({ promise, sanitize }) {
|
|
90
128
|
const [resolved, setResolved] = (0, react.useState)(null);
|
|
129
|
+
const [failed, setFailed] = (0, react.useState)(false);
|
|
91
130
|
(0, react.useEffect)(() => {
|
|
92
131
|
let cancelled = false;
|
|
132
|
+
setResolved(null);
|
|
133
|
+
setFailed(false);
|
|
93
134
|
promise.then((out) => {
|
|
94
135
|
if (!cancelled) setResolved(out);
|
|
136
|
+
}, () => {
|
|
137
|
+
if (!cancelled) setFailed(true);
|
|
95
138
|
});
|
|
96
139
|
return () => {
|
|
97
140
|
cancelled = true;
|
|
98
141
|
};
|
|
99
142
|
}, [promise]);
|
|
143
|
+
if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
100
144
|
if (resolved === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-pending": true });
|
|
101
|
-
|
|
145
|
+
try {
|
|
146
|
+
return renderOutput(resolved, void 0, sanitize);
|
|
147
|
+
} catch {
|
|
148
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function sanitizeOutput(html, sanitize) {
|
|
152
|
+
if (sanitize === false) return html;
|
|
153
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
102
154
|
}
|
|
103
155
|
|
|
104
156
|
//#endregion
|
|
105
157
|
//#region src/render-node.tsx
|
|
106
158
|
function renderNode(node, ctx) {
|
|
107
|
-
const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
|
|
108
|
-
if (r) {
|
|
159
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
160
|
+
if (r) try {
|
|
109
161
|
const out = r(node);
|
|
110
|
-
if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
|
|
111
|
-
|
|
162
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.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);
|
|
112
169
|
}
|
|
113
170
|
switch (node.type) {
|
|
114
171
|
case "heading": return (0, react.createElement)(node.tag ?? "h1", {
|
|
115
172
|
key: node.key,
|
|
116
|
-
dangerouslySetInnerHTML: { __html: node.html ?? "" }
|
|
173
|
+
dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
|
|
117
174
|
});
|
|
118
|
-
case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
|
|
175
|
+
case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
|
|
119
176
|
case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
120
177
|
"data-lang": node.attrs?.lang,
|
|
121
178
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: node.content })
|
|
122
179
|
}, node.key);
|
|
123
180
|
case "hr": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("hr", {}, node.key);
|
|
124
|
-
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: node.content ?? "" } }, node.key);
|
|
181
|
+
case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
|
|
125
182
|
case "card": return renderCard(node, ctx);
|
|
126
|
-
default: return
|
|
183
|
+
default: return renderFallback(node, ctx);
|
|
127
184
|
}
|
|
128
185
|
}
|
|
186
|
+
function renderFallback(node, ctx) {
|
|
187
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
|
|
188
|
+
}
|
|
189
|
+
function renderHtml(html, ctx) {
|
|
190
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
191
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
192
|
+
}
|
|
129
193
|
function renderCard(node, ctx) {
|
|
130
194
|
const card = node.card;
|
|
131
195
|
if (!card) return null;
|
|
@@ -175,10 +239,14 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
175
239
|
feed,
|
|
176
240
|
reset
|
|
177
241
|
]);
|
|
242
|
+
const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins), [plugins]);
|
|
178
243
|
const ctx = {
|
|
179
244
|
registry,
|
|
180
245
|
plugins,
|
|
181
|
-
|
|
246
|
+
nodeRenderers,
|
|
247
|
+
onCardAction,
|
|
248
|
+
sanitize,
|
|
249
|
+
sanitized: true
|
|
182
250
|
};
|
|
183
251
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
184
252
|
className,
|
|
@@ -187,18 +255,6 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
|
|
|
187
255
|
});
|
|
188
256
|
});
|
|
189
257
|
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/apply-patches.ts
|
|
192
|
-
function applyPatches(nodes, patches) {
|
|
193
|
-
let out = nodes.slice();
|
|
194
|
-
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
195
|
-
else if (p.op === "update") {
|
|
196
|
-
const i = out.findIndex((n) => n.key === p.key);
|
|
197
|
-
if (i >= 0) out[i] = p.node;
|
|
198
|
-
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
199
|
-
return out;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
258
|
//#endregion
|
|
203
259
|
exports.AIRenderer = AIRenderer
|
|
204
260
|
exports.applyPatches = applyPatches
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, 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";
|
|
@@ -7,7 +7,7 @@ import { ReactNode } from "react";
|
|
|
7
7
|
interface UseAIRendererResult {
|
|
8
8
|
nodes: ASTNode[];
|
|
9
9
|
push: (chunk: string) => void;
|
|
10
|
-
feed: (source:
|
|
10
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
11
11
|
reset: () => void;
|
|
12
12
|
}
|
|
13
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
@@ -17,11 +17,14 @@ declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseA
|
|
|
17
17
|
interface RenderContext {
|
|
18
18
|
registry?: CardRegistry;
|
|
19
19
|
plugins?: AIGuiPlugin[];
|
|
20
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
20
21
|
onCardAction?: (action: {
|
|
21
22
|
type: string;
|
|
22
23
|
params?: unknown;
|
|
23
24
|
cardType: string;
|
|
24
25
|
}) => void;
|
|
26
|
+
sanitize?: RendererOptions["sanitize"];
|
|
27
|
+
sanitized?: boolean;
|
|
25
28
|
}
|
|
26
29
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
27
30
|
|
|
@@ -29,12 +32,12 @@ declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
|
29
32
|
//#region src/ai-renderer.d.ts
|
|
30
33
|
interface AIRendererHandle {
|
|
31
34
|
push: (chunk: string) => void;
|
|
32
|
-
feed: (source:
|
|
35
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
33
36
|
reset: () => void;
|
|
34
37
|
}
|
|
35
38
|
interface AIRendererProps {
|
|
36
39
|
registry?: CardRegistry;
|
|
37
|
-
sanitize?:
|
|
40
|
+
sanitize?: RendererOptions["sanitize"];
|
|
38
41
|
plugins?: AIGuiPlugin[];
|
|
39
42
|
onCardAction?: RenderContext["onCardAction"];
|
|
40
43
|
className?: string;
|
|
@@ -44,7 +47,7 @@ declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & rea
|
|
|
44
47
|
//#endregion
|
|
45
48
|
//#region src/render-output.d.ts
|
|
46
49
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
47
|
-
declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
|
|
50
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
48
51
|
|
|
49
52
|
//#endregion
|
|
50
53
|
//#region src/apply-patches.d.ts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as react0 from "react";
|
|
2
2
|
import * as react1 from "react";
|
|
3
3
|
import { ReactNode } from "react";
|
|
4
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
4
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, Patch, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
5
5
|
|
|
6
6
|
//#region src/use-ai-renderer.d.ts
|
|
7
7
|
interface UseAIRendererResult {
|
|
8
8
|
nodes: ASTNode[];
|
|
9
9
|
push: (chunk: string) => void;
|
|
10
|
-
feed: (source:
|
|
10
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
11
11
|
reset: () => void;
|
|
12
12
|
}
|
|
13
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
@@ -17,11 +17,14 @@ declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseA
|
|
|
17
17
|
interface RenderContext {
|
|
18
18
|
registry?: CardRegistry;
|
|
19
19
|
plugins?: AIGuiPlugin[];
|
|
20
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
20
21
|
onCardAction?: (action: {
|
|
21
22
|
type: string;
|
|
22
23
|
params?: unknown;
|
|
23
24
|
cardType: string;
|
|
24
25
|
}) => void;
|
|
26
|
+
sanitize?: RendererOptions["sanitize"];
|
|
27
|
+
sanitized?: boolean;
|
|
25
28
|
}
|
|
26
29
|
declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
27
30
|
|
|
@@ -29,12 +32,12 @@ declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
|
|
|
29
32
|
//#region src/ai-renderer.d.ts
|
|
30
33
|
interface AIRendererHandle {
|
|
31
34
|
push: (chunk: string) => void;
|
|
32
|
-
feed: (source:
|
|
35
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
33
36
|
reset: () => void;
|
|
34
37
|
}
|
|
35
38
|
interface AIRendererProps {
|
|
36
39
|
registry?: CardRegistry;
|
|
37
|
-
sanitize?:
|
|
40
|
+
sanitize?: RendererOptions["sanitize"];
|
|
38
41
|
plugins?: AIGuiPlugin[];
|
|
39
42
|
onCardAction?: RenderContext["onCardAction"];
|
|
40
43
|
className?: string;
|
|
@@ -44,7 +47,7 @@ declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & rea
|
|
|
44
47
|
//#endregion
|
|
45
48
|
//#region src/render-output.d.ts
|
|
46
49
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
47
|
-
declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
|
|
50
|
+
declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
|
|
48
51
|
|
|
49
52
|
//#endregion
|
|
50
53
|
//#region src/apply-patches.d.ts
|
package/dist/index.js
CHANGED
|
@@ -2,25 +2,63 @@ import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle,
|
|
|
2
2
|
import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
|
|
3
3
|
import { jsx } from "react/jsx-runtime";
|
|
4
4
|
|
|
5
|
+
//#region src/apply-patches.ts
|
|
6
|
+
function applyPatches(nodes, patches) {
|
|
7
|
+
let out = nodes.slice();
|
|
8
|
+
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
9
|
+
else if (p.op === "update") {
|
|
10
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
11
|
+
if (i >= 0) out[i] = p.node;
|
|
12
|
+
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
13
|
+
else if (p.op === "move") {
|
|
14
|
+
const i = out.findIndex((n) => n.key === p.key);
|
|
15
|
+
if (i >= 0) {
|
|
16
|
+
const [node] = out.splice(i, 1);
|
|
17
|
+
out.splice(p.index, 0, node);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
5
24
|
//#region src/use-ai-renderer.ts
|
|
6
25
|
function useAIRenderer(options = {}) {
|
|
7
26
|
const [nodes, setNodes] = useState([]);
|
|
8
|
-
const
|
|
9
|
-
|
|
27
|
+
const active = useRef(null);
|
|
28
|
+
const session = useMemo(() => {
|
|
29
|
+
const token = {};
|
|
30
|
+
const renderer = new Renderer({
|
|
10
31
|
...options,
|
|
11
|
-
onPatch: (
|
|
32
|
+
onPatch: (patches) => {
|
|
33
|
+
if (active.current === token) setNodes((current) => applyPatches(current, patches));
|
|
34
|
+
}
|
|
12
35
|
});
|
|
36
|
+
active.current = token;
|
|
37
|
+
return {
|
|
38
|
+
renderer,
|
|
39
|
+
token
|
|
40
|
+
};
|
|
13
41
|
}, [
|
|
14
42
|
options.registry,
|
|
15
43
|
options.sanitize,
|
|
16
|
-
options.plugins
|
|
44
|
+
options.plugins,
|
|
45
|
+
options.scheduler
|
|
17
46
|
]);
|
|
18
|
-
|
|
19
|
-
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
setNodes([]);
|
|
49
|
+
return () => {
|
|
50
|
+
session.renderer.reset();
|
|
51
|
+
if (active.current === session.token) active.current = null;
|
|
52
|
+
};
|
|
53
|
+
}, [session]);
|
|
54
|
+
const push = useCallback((chunk) => {
|
|
55
|
+
if (active.current === session.token) session.renderer.push(chunk);
|
|
56
|
+
}, [session]);
|
|
57
|
+
const feed = useCallback((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
|
|
20
58
|
const reset = useCallback(() => {
|
|
21
|
-
renderer.reset();
|
|
59
|
+
session.renderer.reset();
|
|
22
60
|
setNodes([]);
|
|
23
|
-
}, [
|
|
61
|
+
}, [session]);
|
|
24
62
|
return {
|
|
25
63
|
nodes,
|
|
26
64
|
push,
|
|
@@ -32,13 +70,13 @@ function useAIRenderer(options = {}) {
|
|
|
32
70
|
//#endregion
|
|
33
71
|
//#region src/render-output.tsx
|
|
34
72
|
/** Translate a framework-neutral RenderOutput into React nodes. */
|
|
35
|
-
function renderOutput(out, key) {
|
|
73
|
+
function renderOutput(out, key, sanitize) {
|
|
36
74
|
switch (out.kind) {
|
|
37
|
-
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html:
|
|
75
|
+
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
|
|
38
76
|
case "element": return createElement(out.tag, {
|
|
39
77
|
key,
|
|
40
78
|
...out.props
|
|
41
|
-
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
|
|
79
|
+
}, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
|
|
42
80
|
case "card": return /* @__PURE__ */ jsx("pre", {
|
|
43
81
|
"data-aigui-card-fallback": true,
|
|
44
82
|
children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(out.data, null, 2) })
|
|
@@ -55,53 +93,79 @@ function MountHost({ mount }) {
|
|
|
55
93
|
return () => {
|
|
56
94
|
if (typeof cleanup === "function") cleanup();
|
|
57
95
|
};
|
|
58
|
-
}, []);
|
|
96
|
+
}, [mount]);
|
|
59
97
|
return /* @__PURE__ */ jsx("div", {
|
|
60
98
|
ref,
|
|
61
99
|
"data-aigui-mount": true
|
|
62
100
|
});
|
|
63
101
|
}
|
|
64
102
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
65
|
-
function AsyncOutput({ promise }) {
|
|
103
|
+
function AsyncOutput({ promise, sanitize }) {
|
|
66
104
|
const [resolved, setResolved] = useState(null);
|
|
105
|
+
const [failed, setFailed] = useState(false);
|
|
67
106
|
useEffect(() => {
|
|
68
107
|
let cancelled = false;
|
|
108
|
+
setResolved(null);
|
|
109
|
+
setFailed(false);
|
|
69
110
|
promise.then((out) => {
|
|
70
111
|
if (!cancelled) setResolved(out);
|
|
112
|
+
}, () => {
|
|
113
|
+
if (!cancelled) setFailed(true);
|
|
71
114
|
});
|
|
72
115
|
return () => {
|
|
73
116
|
cancelled = true;
|
|
74
117
|
};
|
|
75
118
|
}, [promise]);
|
|
119
|
+
if (failed) return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
76
120
|
if (resolved === null) return /* @__PURE__ */ jsx("span", { "data-aigui-async-pending": true });
|
|
77
|
-
|
|
121
|
+
try {
|
|
122
|
+
return renderOutput(resolved, void 0, sanitize);
|
|
123
|
+
} catch {
|
|
124
|
+
return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function sanitizeOutput(html, sanitize) {
|
|
128
|
+
if (sanitize === false) return html;
|
|
129
|
+
return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
78
130
|
}
|
|
79
131
|
|
|
80
132
|
//#endregion
|
|
81
133
|
//#region src/render-node.tsx
|
|
82
134
|
function renderNode(node, ctx) {
|
|
83
|
-
const r = collectNodeRenderers(ctx.plugins)[node.type];
|
|
84
|
-
if (r) {
|
|
135
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
136
|
+
if (r) try {
|
|
85
137
|
const out = r(node);
|
|
86
|
-
if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
87
|
-
|
|
138
|
+
if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
|
|
139
|
+
promise: out,
|
|
140
|
+
sanitize: ctx.sanitize
|
|
141
|
+
}, node.key);
|
|
142
|
+
return renderOutput(out, node.key, ctx.sanitize);
|
|
143
|
+
} catch {
|
|
144
|
+
return renderFallback(node, ctx);
|
|
88
145
|
}
|
|
89
146
|
switch (node.type) {
|
|
90
147
|
case "heading": return createElement(node.tag ?? "h1", {
|
|
91
148
|
key: node.key,
|
|
92
|
-
dangerouslySetInnerHTML: { __html: node.html ?? "" }
|
|
149
|
+
dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
|
|
93
150
|
});
|
|
94
|
-
case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
|
|
151
|
+
case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
|
|
95
152
|
case "code": return /* @__PURE__ */ jsx("pre", {
|
|
96
153
|
"data-lang": node.attrs?.lang,
|
|
97
154
|
children: /* @__PURE__ */ jsx("code", { children: node.content })
|
|
98
155
|
}, node.key);
|
|
99
156
|
case "hr": return /* @__PURE__ */ jsx("hr", {}, node.key);
|
|
100
|
-
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: node.content ?? "" } }, node.key);
|
|
157
|
+
case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
|
|
101
158
|
case "card": return renderCard(node, ctx);
|
|
102
|
-
default: return
|
|
159
|
+
default: return renderFallback(node, ctx);
|
|
103
160
|
}
|
|
104
161
|
}
|
|
162
|
+
function renderFallback(node, ctx) {
|
|
163
|
+
return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
|
|
164
|
+
}
|
|
165
|
+
function renderHtml(html, ctx) {
|
|
166
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
167
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
168
|
+
}
|
|
105
169
|
function renderCard(node, ctx) {
|
|
106
170
|
const card = node.card;
|
|
107
171
|
if (!card) return null;
|
|
@@ -151,10 +215,14 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
151
215
|
feed,
|
|
152
216
|
reset
|
|
153
217
|
]);
|
|
218
|
+
const nodeRenderers = useMemo(() => collectNodeRenderers(plugins), [plugins]);
|
|
154
219
|
const ctx = {
|
|
155
220
|
registry,
|
|
156
221
|
plugins,
|
|
157
|
-
|
|
222
|
+
nodeRenderers,
|
|
223
|
+
onCardAction,
|
|
224
|
+
sanitize,
|
|
225
|
+
sanitized: true
|
|
158
226
|
};
|
|
159
227
|
return /* @__PURE__ */ jsx("div", {
|
|
160
228
|
className,
|
|
@@ -163,17 +231,5 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
|
|
|
163
231
|
});
|
|
164
232
|
});
|
|
165
233
|
|
|
166
|
-
//#endregion
|
|
167
|
-
//#region src/apply-patches.ts
|
|
168
|
-
function applyPatches(nodes, patches) {
|
|
169
|
-
let out = nodes.slice();
|
|
170
|
-
for (const p of patches) if (p.op === "insert") out.splice(p.index, 0, p.node);
|
|
171
|
-
else if (p.op === "update") {
|
|
172
|
-
const i = out.findIndex((n) => n.key === p.key);
|
|
173
|
-
if (i >= 0) out[i] = p.node;
|
|
174
|
-
} else if (p.op === "remove") out = out.filter((n) => n.key !== p.key);
|
|
175
|
-
return out;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
234
|
//#endregion
|
|
179
235
|
export { AIRenderer, applyPatches, renderNode, renderOutput, useAIRenderer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "React adapter for AIGUI — stream LLM output into live React components with cards, plugins, and interactive widgets.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -22,14 +22,23 @@
|
|
|
22
22
|
"homepage": "https://github.com/liliang-cn/aigui#readme",
|
|
23
23
|
"bugs": "https://github.com/liliang-cn/aigui/issues",
|
|
24
24
|
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
25
29
|
"main": "./dist/index.cjs",
|
|
26
30
|
"module": "./dist/index.js",
|
|
27
31
|
"types": "./dist/index.d.ts",
|
|
28
32
|
"exports": {
|
|
29
33
|
".": {
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
},
|
|
35
44
|
"files": [
|
|
@@ -41,7 +50,7 @@
|
|
|
41
50
|
"access": "public"
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.2.0"
|
|
45
54
|
},
|
|
46
55
|
"peerDependencies": {
|
|
47
56
|
"react": ">=18"
|
|
@@ -56,6 +65,7 @@
|
|
|
56
65
|
},
|
|
57
66
|
"scripts": {
|
|
58
67
|
"build": "tsdown",
|
|
68
|
+
"test": "pnpm --dir ../.. exec vitest run --project react",
|
|
59
69
|
"typecheck": "tsc --noEmit"
|
|
60
70
|
}
|
|
61
71
|
}
|