@ai-gui/react 0.1.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 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
- onCardAction={({ type, params, cardType }) => {/* app makes the real request */}}
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
@@ -26,26 +26,67 @@ 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,
70
+ options.debug,
71
+ options.onDebugEvent
41
72
  ]);
42
- const push = (0, react.useCallback)((chunk) => renderer.push(chunk), [renderer]);
43
- const feed = (0, react.useCallback)((source) => renderer.feed(source), [renderer]);
73
+ (0, react.useEffect)(() => {
74
+ setNodes([]);
75
+ return () => {
76
+ session.renderer.reset();
77
+ if (active.current === session.token) active.current = null;
78
+ };
79
+ }, [session]);
80
+ const push = (0, react.useCallback)((chunk) => {
81
+ if (active.current === session.token) session.renderer.push(chunk);
82
+ }, [session]);
83
+ const feed = (0, react.useCallback)((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
44
84
  const reset = (0, react.useCallback)(() => {
45
- renderer.reset();
85
+ session.renderer.reset();
46
86
  setNodes([]);
47
- }, [renderer]);
87
+ }, [session]);
48
88
  return {
89
+ renderer: session.renderer,
49
90
  nodes,
50
91
  push,
51
92
  feed,
@@ -53,16 +94,32 @@ function useAIRenderer(options = {}) {
53
94
  };
54
95
  }
55
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
+
56
113
  //#endregion
57
114
  //#region src/render-output.tsx
58
115
  /** Translate a framework-neutral RenderOutput into React nodes. */
59
- function renderOutput(out, key) {
116
+ function renderOutput(out, key, sanitize) {
60
117
  switch (out.kind) {
61
- case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: (0, __ai_gui_core.sanitizeHtml)(out.html) } }, key);
118
+ case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
62
119
  case "element": return (0, react.createElement)(out.tag, {
63
120
  key,
64
121
  ...out.props
65
- }, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
122
+ }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
66
123
  case "card": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
67
124
  "data-aigui-card-fallback": true,
68
125
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(out.data, null, 2) })
@@ -79,53 +136,85 @@ function MountHost({ mount }) {
79
136
  return () => {
80
137
  if (typeof cleanup === "function") cleanup();
81
138
  };
82
- }, []);
139
+ }, [mount]);
83
140
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
84
141
  ref,
85
142
  "data-aigui-mount": true
86
143
  });
87
144
  }
88
145
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
89
- function AsyncOutput({ promise }) {
146
+ function AsyncOutput({ promise, sanitize }) {
90
147
  const [resolved, setResolved] = (0, react.useState)(null);
148
+ const [failed, setFailed] = (0, react.useState)(false);
91
149
  (0, react.useEffect)(() => {
92
150
  let cancelled = false;
151
+ setResolved(null);
152
+ setFailed(false);
93
153
  promise.then((out) => {
94
154
  if (!cancelled) setResolved(out);
155
+ }, () => {
156
+ if (!cancelled) setFailed(true);
95
157
  });
96
158
  return () => {
97
159
  cancelled = true;
98
160
  };
99
161
  }, [promise]);
162
+ if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
100
163
  if (resolved === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-pending": true });
101
- return renderOutput(resolved);
164
+ try {
165
+ return renderOutput(resolved, void 0, sanitize);
166
+ } catch {
167
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
168
+ }
169
+ }
170
+ function sanitizeOutput(html, sanitize) {
171
+ if (sanitize === false) return html;
172
+ return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
102
173
  }
103
174
 
104
175
  //#endregion
105
176
  //#region src/render-node.tsx
106
177
  function renderNode(node, ctx) {
107
- const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
178
+ const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
108
179
  if (r) {
109
- 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);
180
+ if (node.complete === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
181
+ "data-aigui-block-loading": "",
182
+ "data-block-type": node.type
183
+ }, node.key);
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
+ }
112
194
  }
113
195
  switch (node.type) {
114
196
  case "heading": return (0, react.createElement)(node.tag ?? "h1", {
115
197
  key: node.key,
116
- dangerouslySetInnerHTML: { __html: node.html ?? "" }
198
+ dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
117
199
  });
118
- case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
200
+ case "paragraph": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
119
201
  case "code": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
120
202
  "data-lang": node.attrs?.lang,
121
203
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: node.content })
122
204
  }, node.key);
123
205
  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);
206
+ case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
125
207
  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);
208
+ default: return renderFallback(node, ctx);
127
209
  }
128
210
  }
211
+ function renderFallback(node, ctx) {
212
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
213
+ }
214
+ function renderHtml(html, ctx) {
215
+ if (ctx.sanitized || ctx.sanitize === false) return html;
216
+ return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
217
+ }
129
218
  function renderCard(node, ctx) {
130
219
  const card = node.card;
131
220
  if (!card) return null;
@@ -139,10 +228,15 @@ function renderCard(node, ctx) {
139
228
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
140
229
  }, node.key);
141
230
  const Comp = getCardComponent(ctx.registry, card.type);
142
- if (!Comp) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
143
- "data-aigui-card-fallback": true,
144
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(card.data, null, 2) })
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
145
238
  }, node.key);
239
+ if (!Comp) return renderCardFallback(node.key, card.data);
146
240
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Comp, {
147
241
  data: card.data,
148
242
  onAction: (a) => ctx.onCardAction?.({
@@ -151,6 +245,49 @@ function renderCard(node, ctx) {
151
245
  })
152
246
  }, node.key);
153
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
+ }
154
291
  function getCardComponent(registry, type) {
155
292
  if (!registry) return void 0;
156
293
  return registry.getRender(type);
@@ -158,27 +295,73 @@ function getCardComponent(registry, type) {
158
295
 
159
296
  //#endregion
160
297
  //#region src/ai-renderer.tsx
298
+ function createActionScope() {
299
+ return {
300
+ controller: new AbortController(),
301
+ owner: {}
302
+ };
303
+ }
161
304
  const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
162
- const { registry, sanitize, plugins, onCardAction, className } = props;
305
+ const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
163
306
  const opts = {
164
307
  registry,
165
308
  sanitize,
166
- plugins
309
+ plugins,
310
+ debug,
311
+ onDebugEvent
167
312
  };
168
- 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]);
169
344
  (0, react.useImperativeHandle)(ref, () => ({
345
+ debugSource: "renderer",
346
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
170
347
  push,
171
348
  feed,
172
349
  reset
173
350
  }), [
351
+ renderer,
174
352
  push,
175
353
  feed,
176
354
  reset
177
355
  ]);
356
+ const nodeRenderers = (0, react.useMemo)(() => (0, __ai_gui_core.collectNodeRenderers)(plugins, { debugTarget: renderer }), [plugins, renderer]);
178
357
  const ctx = {
179
358
  registry,
359
+ cardStore,
180
360
  plugins,
181
- onCardAction
361
+ nodeRenderers,
362
+ onCardAction: handleCardAction,
363
+ sanitize,
364
+ sanitized: true
182
365
  };
183
366
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
184
367
  className,
@@ -187,21 +370,10 @@ const AIRenderer = (0, react.forwardRef)(function AIRenderer$1(props, ref) {
187
370
  });
188
371
  });
189
372
 
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
373
  //#endregion
203
374
  exports.AIRenderer = AIRenderer
204
375
  exports.applyPatches = applyPatches
205
376
  exports.renderNode = renderNode
206
377
  exports.renderOutput = renderOutput
207
- exports.useAIRenderer = useAIRenderer
378
+ exports.useAIRenderer = useAIRenderer
379
+ exports.useActionState = useActionState
package/dist/index.d.cts CHANGED
@@ -1,54 +1,79 @@
1
- import { AIGuiPlugin, ASTNode, CardRegistry, 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
- feed: (source: AsyncIterable<string> | ReadableStream) => Promise<void>;
11
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
11
12
  reset: () => void;
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
- onCardAction?: (action: {
32
+ nodeRenderers?: Record<string, NodeRenderer>;
33
+ onCardAction?: (action: CardActionPayload) => void;
34
+ sanitize?: RendererOptions["sanitize"];
35
+ sanitized?: boolean;
36
+ }
37
+ declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
38
+ interface CardComponentProps {
39
+ data: unknown;
40
+ state?: CardAction;
41
+ onAction: (a: {
21
42
  type: string;
22
43
  params?: unknown;
23
- cardType: string;
24
44
  }) => void;
25
45
  }
26
- declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
27
46
 
28
47
  //#endregion
29
48
  //#region src/ai-renderer.d.ts
30
49
  interface AIRendererHandle {
50
+ readonly debugSource: "renderer";
51
+ subscribeDebug: (listener: DebugEventListener) => () => void;
31
52
  push: (chunk: string) => void;
32
- feed: (source: AsyncIterable<string> | ReadableStream) => Promise<void>;
53
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
33
54
  reset: () => void;
34
55
  }
35
56
  interface AIRendererProps {
36
57
  registry?: CardRegistry;
37
- sanitize?: boolean;
58
+ cardStore?: CardStore;
59
+ sanitize?: RendererOptions["sanitize"];
38
60
  plugins?: AIGuiPlugin[];
61
+ actionRuntime?: ActionRuntime;
39
62
  onCardAction?: RenderContext["onCardAction"];
40
63
  className?: string;
64
+ debug?: RendererOptions["debug"];
65
+ onDebugEvent?: RendererOptions["onDebugEvent"];
41
66
  }
42
67
  declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
43
68
 
44
69
  //#endregion
45
70
  //#region src/render-output.d.ts
46
71
  /** Translate a framework-neutral RenderOutput into React nodes. */
47
- declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
72
+ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
48
73
 
49
74
  //#endregion
50
75
  //#region src/apply-patches.d.ts
51
76
  declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
52
77
 
53
78
  //#endregion
54
- 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,54 +1,79 @@
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, 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
- feed: (source: AsyncIterable<string> | ReadableStream) => Promise<void>;
11
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
11
12
  reset: () => void;
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
- onCardAction?: (action: {
32
+ nodeRenderers?: Record<string, NodeRenderer>;
33
+ onCardAction?: (action: CardActionPayload) => void;
34
+ sanitize?: RendererOptions["sanitize"];
35
+ sanitized?: boolean;
36
+ }
37
+ declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
38
+ interface CardComponentProps {
39
+ data: unknown;
40
+ state?: CardAction;
41
+ onAction: (a: {
21
42
  type: string;
22
43
  params?: unknown;
23
- cardType: string;
24
44
  }) => void;
25
45
  }
26
- declare function renderNode(node: ASTNode, ctx: RenderContext): ReactNode;
27
46
 
28
47
  //#endregion
29
48
  //#region src/ai-renderer.d.ts
30
49
  interface AIRendererHandle {
50
+ readonly debugSource: "renderer";
51
+ subscribeDebug: (listener: DebugEventListener) => () => void;
31
52
  push: (chunk: string) => void;
32
- feed: (source: AsyncIterable<string> | ReadableStream) => Promise<void>;
53
+ feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
33
54
  reset: () => void;
34
55
  }
35
56
  interface AIRendererProps {
36
57
  registry?: CardRegistry;
37
- sanitize?: boolean;
58
+ cardStore?: CardStore;
59
+ sanitize?: RendererOptions["sanitize"];
38
60
  plugins?: AIGuiPlugin[];
61
+ actionRuntime?: ActionRuntime;
39
62
  onCardAction?: RenderContext["onCardAction"];
40
63
  className?: string;
64
+ debug?: RendererOptions["debug"];
65
+ onDebugEvent?: RendererOptions["onDebugEvent"];
41
66
  }
42
67
  declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & react0.RefAttributes<AIRendererHandle>>;
43
68
 
44
69
  //#endregion
45
70
  //#region src/render-output.d.ts
46
71
  /** Translate a framework-neutral RenderOutput into React nodes. */
47
- declare function renderOutput(out: RenderOutput, key?: string): ReactNode;
72
+ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
48
73
 
49
74
  //#endregion
50
75
  //#region src/apply-patches.d.ts
51
76
  declare function applyPatches(nodes: ASTNode[], patches: Patch[]): ASTNode[];
52
77
 
53
78
  //#endregion
54
- 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,27 +1,68 @@
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
+ //#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,
46
+ options.debug,
47
+ options.onDebugEvent
17
48
  ]);
18
- const push = useCallback((chunk) => renderer.push(chunk), [renderer]);
19
- const feed = useCallback((source) => renderer.feed(source), [renderer]);
49
+ useEffect(() => {
50
+ setNodes([]);
51
+ return () => {
52
+ session.renderer.reset();
53
+ if (active.current === session.token) active.current = null;
54
+ };
55
+ }, [session]);
56
+ const push = useCallback((chunk) => {
57
+ if (active.current === session.token) session.renderer.push(chunk);
58
+ }, [session]);
59
+ const feed = useCallback((source, feedOptions) => active.current === session.token ? session.renderer.feed(source, feedOptions) : Promise.resolve(), [session]);
20
60
  const reset = useCallback(() => {
21
- renderer.reset();
61
+ session.renderer.reset();
22
62
  setNodes([]);
23
- }, [renderer]);
63
+ }, [session]);
24
64
  return {
65
+ renderer: session.renderer,
25
66
  nodes,
26
67
  push,
27
68
  feed,
@@ -29,16 +70,32 @@ function useAIRenderer(options = {}) {
29
70
  };
30
71
  }
31
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
+
32
89
  //#endregion
33
90
  //#region src/render-output.tsx
34
91
  /** Translate a framework-neutral RenderOutput into React nodes. */
35
- function renderOutput(out, key) {
92
+ function renderOutput(out, key, sanitize) {
36
93
  switch (out.kind) {
37
- case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeHtml(out.html) } }, key);
94
+ case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
38
95
  case "element": return createElement(out.tag, {
39
96
  key,
40
97
  ...out.props
41
- }, (out.children ?? []).map((c, i) => renderOutput(c, String(i))));
98
+ }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
42
99
  case "card": return /* @__PURE__ */ jsx("pre", {
43
100
  "data-aigui-card-fallback": true,
44
101
  children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(out.data, null, 2) })
@@ -55,53 +112,85 @@ function MountHost({ mount }) {
55
112
  return () => {
56
113
  if (typeof cleanup === "function") cleanup();
57
114
  };
58
- }, []);
115
+ }, [mount]);
59
116
  return /* @__PURE__ */ jsx("div", {
60
117
  ref,
61
118
  "data-aigui-mount": true
62
119
  });
63
120
  }
64
121
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
65
- function AsyncOutput({ promise }) {
122
+ function AsyncOutput({ promise, sanitize }) {
66
123
  const [resolved, setResolved] = useState(null);
124
+ const [failed, setFailed] = useState(false);
67
125
  useEffect(() => {
68
126
  let cancelled = false;
127
+ setResolved(null);
128
+ setFailed(false);
69
129
  promise.then((out) => {
70
130
  if (!cancelled) setResolved(out);
131
+ }, () => {
132
+ if (!cancelled) setFailed(true);
71
133
  });
72
134
  return () => {
73
135
  cancelled = true;
74
136
  };
75
137
  }, [promise]);
138
+ if (failed) return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
76
139
  if (resolved === null) return /* @__PURE__ */ jsx("span", { "data-aigui-async-pending": true });
77
- return renderOutput(resolved);
140
+ try {
141
+ return renderOutput(resolved, void 0, sanitize);
142
+ } catch {
143
+ return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
144
+ }
145
+ }
146
+ function sanitizeOutput(html, sanitize) {
147
+ if (sanitize === false) return html;
148
+ return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
78
149
  }
79
150
 
80
151
  //#endregion
81
152
  //#region src/render-node.tsx
82
153
  function renderNode(node, ctx) {
83
- const r = collectNodeRenderers(ctx.plugins)[node.type];
154
+ const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
84
155
  if (r) {
85
- 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);
156
+ if (node.complete === false) return /* @__PURE__ */ jsx("div", {
157
+ "data-aigui-block-loading": "",
158
+ "data-block-type": node.type
159
+ }, node.key);
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
+ }
88
170
  }
89
171
  switch (node.type) {
90
172
  case "heading": return createElement(node.tag ?? "h1", {
91
173
  key: node.key,
92
- dangerouslySetInnerHTML: { __html: node.html ?? "" }
174
+ dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) }
93
175
  });
94
- case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: node.html ?? "" } }, node.key);
176
+ case "paragraph": return /* @__PURE__ */ jsx("p", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? "", ctx) } }, node.key);
95
177
  case "code": return /* @__PURE__ */ jsx("pre", {
96
178
  "data-lang": node.attrs?.lang,
97
179
  children: /* @__PURE__ */ jsx("code", { children: node.content })
98
180
  }, node.key);
99
181
  case "hr": return /* @__PURE__ */ jsx("hr", {}, node.key);
100
- case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: node.content ?? "" } }, node.key);
182
+ case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.content ?? "", ctx) } }, node.key);
101
183
  case "card": return renderCard(node, ctx);
102
- default: return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: node.html ?? sanitizeHtml(node.content ?? "") } }, node.key);
184
+ default: return renderFallback(node, ctx);
103
185
  }
104
186
  }
187
+ function renderFallback(node, ctx) {
188
+ return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
189
+ }
190
+ function renderHtml(html, ctx) {
191
+ if (ctx.sanitized || ctx.sanitize === false) return html;
192
+ return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
193
+ }
105
194
  function renderCard(node, ctx) {
106
195
  const card = node.card;
107
196
  if (!card) return null;
@@ -115,10 +204,15 @@ function renderCard(node, ctx) {
115
204
  children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
116
205
  }, node.key);
117
206
  const Comp = getCardComponent(ctx.registry, card.type);
118
- if (!Comp) return /* @__PURE__ */ jsx("pre", {
119
- "data-aigui-card-fallback": true,
120
- children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(card.data, null, 2) })
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
121
214
  }, node.key);
215
+ if (!Comp) return renderCardFallback(node.key, card.data);
122
216
  return /* @__PURE__ */ jsx(Comp, {
123
217
  data: card.data,
124
218
  onAction: (a) => ctx.onCardAction?.({
@@ -127,6 +221,49 @@ function renderCard(node, ctx) {
127
221
  })
128
222
  }, node.key);
129
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
+ }
130
267
  function getCardComponent(registry, type) {
131
268
  if (!registry) return void 0;
132
269
  return registry.getRender(type);
@@ -134,27 +271,73 @@ function getCardComponent(registry, type) {
134
271
 
135
272
  //#endregion
136
273
  //#region src/ai-renderer.tsx
274
+ function createActionScope() {
275
+ return {
276
+ controller: new AbortController(),
277
+ owner: {}
278
+ };
279
+ }
137
280
  const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
138
- const { registry, sanitize, plugins, onCardAction, className } = props;
281
+ const { registry, cardStore, sanitize, plugins, actionRuntime, onCardAction, className, debug, onDebugEvent } = props;
139
282
  const opts = {
140
283
  registry,
141
284
  sanitize,
142
- plugins
285
+ plugins,
286
+ debug,
287
+ onDebugEvent
143
288
  };
144
- 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]);
145
320
  useImperativeHandle(ref, () => ({
321
+ debugSource: "renderer",
322
+ subscribeDebug: (listener) => renderer.subscribeDebug(listener),
146
323
  push,
147
324
  feed,
148
325
  reset
149
326
  }), [
327
+ renderer,
150
328
  push,
151
329
  feed,
152
330
  reset
153
331
  ]);
332
+ const nodeRenderers = useMemo(() => collectNodeRenderers(plugins, { debugTarget: renderer }), [plugins, renderer]);
154
333
  const ctx = {
155
334
  registry,
335
+ cardStore,
156
336
  plugins,
157
- onCardAction
337
+ nodeRenderers,
338
+ onCardAction: handleCardAction,
339
+ sanitize,
340
+ sanitized: true
158
341
  };
159
342
  return /* @__PURE__ */ jsx("div", {
160
343
  className,
@@ -164,16 +347,4 @@ const AIRenderer = forwardRef(function AIRenderer$1(props, ref) {
164
347
  });
165
348
 
166
349
  //#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
- //#endregion
179
- 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.1.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",
@@ -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.3.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
  }