@ai-gui/react 0.3.0 → 0.4.1

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
@@ -24,6 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  //#endregion
25
25
  const react = __toESM(require("react"));
26
26
  const __ai_gui_core = __toESM(require("@ai-gui/core"));
27
+ const react_dom = __toESM(require("react-dom"));
27
28
  const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
28
29
 
29
30
  //#region src/apply-patches.ts
@@ -113,37 +114,85 @@ function useActionState(runtime, key) {
113
114
  //#endregion
114
115
  //#region src/render-output.tsx
115
116
  /** Translate a framework-neutral RenderOutput into React nodes. */
116
- function renderOutput(out, key, sanitize) {
117
+ function renderOutput(out, key, sanitize, context) {
117
118
  switch (out.kind) {
118
119
  case "html": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
119
120
  case "element": return (0, react.createElement)(out.tag, {
120
121
  key,
121
122
  ...out.props
122
- }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
123
+ }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize, context)));
123
124
  case "card": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
124
125
  "data-aigui-card-fallback": true,
125
126
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", { children: JSON.stringify(out.data, null, 2) })
126
127
  }, key);
127
- case "mount": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MountHost, { mount: out.mount }, key);
128
+ case "mount": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MountHost, {
129
+ mount: out.mount,
130
+ context
131
+ }, key);
128
132
  }
129
133
  }
130
134
  /** Host a framework-neutral imperative mount into a managed DOM element. */
131
- function MountHost({ mount }) {
135
+ function MountHost({ mount, context }) {
132
136
  const ref = (0, react.useRef)(null);
137
+ const [slots, setSlots] = (0, react.useState)([]);
133
138
  (0, react.useEffect)(() => {
134
139
  if (!ref.current) return;
135
- const cleanup = mount(ref.current);
140
+ const mountContext = createMountContext(context, setSlots);
141
+ const cleanup = mount(ref.current, mountContext);
136
142
  return () => {
137
143
  if (typeof cleanup === "function") cleanup();
138
144
  };
139
- }, [mount]);
140
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
145
+ }, [
146
+ mount,
147
+ context?.registry,
148
+ context?.onCardAction
149
+ ]);
150
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
141
151
  ref,
142
152
  "data-aigui-mount": true
143
- });
153
+ }), slots.map((slot) => (0, react_dom.createPortal)((0, react.createElement)(slot.Comp, {
154
+ data: slot.data,
155
+ onAction: (action) => context?.onCardAction?.({
156
+ ...action,
157
+ cardType: slot.type
158
+ })
159
+ }), slot.host, slot.id))] });
160
+ }
161
+ let nextCardSlotId = 0;
162
+ function createMountContext(context, setSlots) {
163
+ return { mountCard(host, request) {
164
+ return mountCard(host, request, context, setSlots);
165
+ } };
166
+ }
167
+ function mountCard(host, request, context, setSlots) {
168
+ const Comp = context?.registry?.getRender(request.type);
169
+ if (!Comp) return void 0;
170
+ const id = String(nextCardSlotId++);
171
+ let destroyed = false;
172
+ setSlots((slots) => [...slots, {
173
+ id,
174
+ host,
175
+ type: request.type,
176
+ data: request.data,
177
+ Comp
178
+ }]);
179
+ return {
180
+ update(data) {
181
+ if (destroyed) return;
182
+ setSlots((slots) => slots.map((slot) => slot.id === id ? {
183
+ ...slot,
184
+ data
185
+ } : slot));
186
+ },
187
+ destroy() {
188
+ if (destroyed) return;
189
+ destroyed = true;
190
+ setSlots((slots) => slots.filter((slot) => slot.id !== id));
191
+ }
192
+ };
144
193
  }
145
194
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
146
- function AsyncOutput({ promise, sanitize }) {
195
+ function AsyncOutput({ promise, sanitize, context }) {
147
196
  const [resolved, setResolved] = (0, react.useState)(null);
148
197
  const [failed, setFailed] = (0, react.useState)(false);
149
198
  (0, react.useEffect)(() => {
@@ -162,7 +211,7 @@ function AsyncOutput({ promise, sanitize }) {
162
211
  if (failed) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
163
212
  if (resolved === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-pending": true });
164
213
  try {
165
- return renderOutput(resolved, void 0, sanitize);
214
+ return renderOutput(resolved, void 0, sanitize, context);
166
215
  } catch {
167
216
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { "data-aigui-async-error": true });
168
217
  }
@@ -181,16 +230,11 @@ function renderNode(node, ctx) {
181
230
  "data-aigui-block-loading": "",
182
231
  "data-block-type": node.type
183
232
  }, 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
- }
233
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PluginOutputHost, {
234
+ node,
235
+ renderer: r,
236
+ context: ctx
237
+ }, node.key);
194
238
  }
195
239
  switch (node.type) {
196
240
  case "heading": return (0, react.createElement)(node.tag ?? "h1", {
@@ -208,6 +252,33 @@ function renderNode(node, ctx) {
208
252
  default: return renderFallback(node, ctx);
209
253
  }
210
254
  }
255
+ function PluginOutputHost({ node, renderer, context }) {
256
+ const cache = (0, react.useRef)();
257
+ const signature = nodeSignature(node);
258
+ try {
259
+ if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature) cache.current = {
260
+ renderer,
261
+ signature,
262
+ output: renderer(node)
263
+ };
264
+ const output = cache.current.output;
265
+ if (output && typeof output.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
266
+ promise: output,
267
+ sanitize: context.sanitize,
268
+ context
269
+ });
270
+ return renderOutput(output, void 0, context.sanitize, context);
271
+ } catch {
272
+ return renderFallback(node, context);
273
+ }
274
+ }
275
+ function nodeSignature(node) {
276
+ try {
277
+ return JSON.stringify(node);
278
+ } catch {
279
+ return node;
280
+ }
281
+ }
211
282
  function renderFallback(node, ctx) {
212
283
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
213
284
  }
package/dist/index.d.cts CHANGED
@@ -69,7 +69,7 @@ declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & rea
69
69
  //#endregion
70
70
  //#region src/render-output.d.ts
71
71
  /** Translate a framework-neutral RenderOutput into React nodes. */
72
- declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
72
+ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"], context?: RenderContext): ReactNode;
73
73
 
74
74
  //#endregion
75
75
  //#region src/apply-patches.d.ts
package/dist/index.d.ts CHANGED
@@ -69,7 +69,7 @@ declare const AIRenderer: react1.ForwardRefExoticComponent<AIRendererProps & rea
69
69
  //#endregion
70
70
  //#region src/render-output.d.ts
71
71
  /** Translate a framework-neutral RenderOutput into React nodes. */
72
- declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"]): ReactNode;
72
+ declare function renderOutput(out: RenderOutput, key?: string, sanitize?: RendererOptions["sanitize"], context?: RenderContext): ReactNode;
73
73
 
74
74
  //#endregion
75
75
  //#region src/apply-patches.d.ts
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
1
+ import { Fragment, createElement, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, useSyncExternalStore } from "react";
2
2
  import { Renderer, collectNodeRenderers, getIdleActionState, sanitizeHtml } from "@ai-gui/core";
3
- import { jsx } from "react/jsx-runtime";
3
+ import { createPortal } from "react-dom";
4
+ import { jsx, jsxs } from "react/jsx-runtime";
4
5
 
5
6
  //#region src/apply-patches.ts
6
7
  function applyPatches(nodes, patches) {
@@ -89,37 +90,85 @@ function useActionState(runtime, key) {
89
90
  //#endregion
90
91
  //#region src/render-output.tsx
91
92
  /** Translate a framework-neutral RenderOutput into React nodes. */
92
- function renderOutput(out, key, sanitize) {
93
+ function renderOutput(out, key, sanitize, context) {
93
94
  switch (out.kind) {
94
95
  case "html": return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: sanitizeOutput(out.html, sanitize) } }, key);
95
96
  case "element": return createElement(out.tag, {
96
97
  key,
97
98
  ...out.props
98
- }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize)));
99
+ }, (out.children ?? []).map((c, i) => renderOutput(c, String(i), sanitize, context)));
99
100
  case "card": return /* @__PURE__ */ jsx("pre", {
100
101
  "data-aigui-card-fallback": true,
101
102
  children: /* @__PURE__ */ jsx("code", { children: JSON.stringify(out.data, null, 2) })
102
103
  }, key);
103
- case "mount": return /* @__PURE__ */ jsx(MountHost, { mount: out.mount }, key);
104
+ case "mount": return /* @__PURE__ */ jsx(MountHost, {
105
+ mount: out.mount,
106
+ context
107
+ }, key);
104
108
  }
105
109
  }
106
110
  /** Host a framework-neutral imperative mount into a managed DOM element. */
107
- function MountHost({ mount }) {
111
+ function MountHost({ mount, context }) {
108
112
  const ref = useRef(null);
113
+ const [slots, setSlots] = useState([]);
109
114
  useEffect(() => {
110
115
  if (!ref.current) return;
111
- const cleanup = mount(ref.current);
116
+ const mountContext = createMountContext(context, setSlots);
117
+ const cleanup = mount(ref.current, mountContext);
112
118
  return () => {
113
119
  if (typeof cleanup === "function") cleanup();
114
120
  };
115
- }, [mount]);
116
- return /* @__PURE__ */ jsx("div", {
121
+ }, [
122
+ mount,
123
+ context?.registry,
124
+ context?.onCardAction
125
+ ]);
126
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
117
127
  ref,
118
128
  "data-aigui-mount": true
119
- });
129
+ }), slots.map((slot) => createPortal(createElement(slot.Comp, {
130
+ data: slot.data,
131
+ onAction: (action) => context?.onCardAction?.({
132
+ ...action,
133
+ cardType: slot.type
134
+ })
135
+ }), slot.host, slot.id))] });
136
+ }
137
+ let nextCardSlotId = 0;
138
+ function createMountContext(context, setSlots) {
139
+ return { mountCard(host, request) {
140
+ return mountCard(host, request, context, setSlots);
141
+ } };
142
+ }
143
+ function mountCard(host, request, context, setSlots) {
144
+ const Comp = context?.registry?.getRender(request.type);
145
+ if (!Comp) return void 0;
146
+ const id = String(nextCardSlotId++);
147
+ let destroyed = false;
148
+ setSlots((slots) => [...slots, {
149
+ id,
150
+ host,
151
+ type: request.type,
152
+ data: request.data,
153
+ Comp
154
+ }]);
155
+ return {
156
+ update(data) {
157
+ if (destroyed) return;
158
+ setSlots((slots) => slots.map((slot) => slot.id === id ? {
159
+ ...slot,
160
+ data
161
+ } : slot));
162
+ },
163
+ destroy() {
164
+ if (destroyed) return;
165
+ destroyed = true;
166
+ setSlots((slots) => slots.filter((slot) => slot.id !== id));
167
+ }
168
+ };
120
169
  }
121
170
  /** Await an async RenderOutput, rendering a placeholder until it resolves. */
122
- function AsyncOutput({ promise, sanitize }) {
171
+ function AsyncOutput({ promise, sanitize, context }) {
123
172
  const [resolved, setResolved] = useState(null);
124
173
  const [failed, setFailed] = useState(false);
125
174
  useEffect(() => {
@@ -138,7 +187,7 @@ function AsyncOutput({ promise, sanitize }) {
138
187
  if (failed) return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
139
188
  if (resolved === null) return /* @__PURE__ */ jsx("span", { "data-aigui-async-pending": true });
140
189
  try {
141
- return renderOutput(resolved, void 0, sanitize);
190
+ return renderOutput(resolved, void 0, sanitize, context);
142
191
  } catch {
143
192
  return /* @__PURE__ */ jsx("span", { "data-aigui-async-error": true });
144
193
  }
@@ -157,16 +206,11 @@ function renderNode(node, ctx) {
157
206
  "data-aigui-block-loading": "",
158
207
  "data-block-type": node.type
159
208
  }, 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
- }
209
+ return /* @__PURE__ */ jsx(PluginOutputHost, {
210
+ node,
211
+ renderer: r,
212
+ context: ctx
213
+ }, node.key);
170
214
  }
171
215
  switch (node.type) {
172
216
  case "heading": return createElement(node.tag ?? "h1", {
@@ -184,6 +228,33 @@ function renderNode(node, ctx) {
184
228
  default: return renderFallback(node, ctx);
185
229
  }
186
230
  }
231
+ function PluginOutputHost({ node, renderer, context }) {
232
+ const cache = useRef();
233
+ const signature = nodeSignature(node);
234
+ try {
235
+ if (!cache.current || cache.current.renderer !== renderer || cache.current.signature !== signature) cache.current = {
236
+ renderer,
237
+ signature,
238
+ output: renderer(node)
239
+ };
240
+ const output = cache.current.output;
241
+ if (output && typeof output.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
242
+ promise: output,
243
+ sanitize: context.sanitize,
244
+ context
245
+ });
246
+ return renderOutput(output, void 0, context.sanitize, context);
247
+ } catch {
248
+ return renderFallback(node, context);
249
+ }
250
+ }
251
+ function nodeSignature(node) {
252
+ try {
253
+ return JSON.stringify(node);
254
+ } catch {
255
+ return node;
256
+ }
257
+ }
187
258
  function renderFallback(node, ctx) {
188
259
  return /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: renderHtml(node.html ?? node.content ?? "", ctx) } }, node.key);
189
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-gui/react",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "React adapter for AIGUI — stream LLM output into live React components with cards, plugins, and interactive widgets.",
5
5
  "keywords": [
6
6
  "llm",
@@ -50,10 +50,11 @@
50
50
  "access": "public"
51
51
  },
52
52
  "dependencies": {
53
- "@ai-gui/core": "0.3.0"
53
+ "@ai-gui/core": "0.4.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "react": ">=18"
56
+ "react": ">=18",
57
+ "react-dom": ">=18"
57
58
  },
58
59
  "devDependencies": {
59
60
  "react": "^18.3.1",