@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 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 renderer = (0, react.useMemo)(() => {
33
- return new __ai_gui_core.Renderer({
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: (_patches, nextNodes) => setNodes(nextNodes)
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
- const push = (0, react.useCallback)((chunk) => renderer.push(chunk), [renderer]);
43
- const feed = (0, react.useCallback)((source) => renderer.feed(source), [renderer]);
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
- }, [renderer]);
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: (0, __ai_gui_core.sanitizeHtml)(out.html) } }, key);
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
- return renderOutput(resolved);
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, { promise: out }, node.key);
111
- return renderOutput(out, node.key);
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 /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: node.html ?? (0, __ai_gui_core.sanitizeHtml)(node.content ?? "") } }, node.key);
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
- onCardAction
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: AsyncIterable<string> | ReadableStream) => Promise<void>;
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: AsyncIterable<string> | ReadableStream) => Promise<void>;
35
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
33
36
  reset: () => void;
34
37
  }
35
38
  interface AIRendererProps {
36
39
  registry?: CardRegistry;
37
- sanitize?: boolean;
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: AsyncIterable<string> | ReadableStream) => Promise<void>;
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: AsyncIterable<string> | ReadableStream) => Promise<void>;
35
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
33
36
  reset: () => void;
34
37
  }
35
38
  interface AIRendererProps {
36
39
  registry?: CardRegistry;
37
- sanitize?: boolean;
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 renderer = useMemo(() => {
9
- return new Renderer({
27
+ const active = useRef(null);
28
+ const session = useMemo(() => {
29
+ const token = {};
30
+ const renderer = new Renderer({
10
31
  ...options,
11
- onPatch: (_patches, nextNodes) => setNodes(nextNodes)
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
- const push = useCallback((chunk) => renderer.push(chunk), [renderer]);
19
- const feed = useCallback((source) => renderer.feed(source), [renderer]);
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
- }, [renderer]);
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: sanitizeHtml(out.html) } }, key);
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
- return renderOutput(resolved);
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, { promise: out }, node.key);
87
- return renderOutput(out, node.key);
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 /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: node.html ?? sanitizeHtml(node.content ?? "") } }, node.key);
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
- onCardAction
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.1.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
- "types": "./dist/index.d.ts",
31
- "import": "./dist/index.js",
32
- "require": "./dist/index.cjs"
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.1.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
  }