@ai-gui/react 0.3.0 → 0.4.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
@@ -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
  }
@@ -185,9 +234,10 @@ function renderNode(node, ctx) {
185
234
  const out = r(node);
186
235
  if (out && typeof out.then === "function") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AsyncOutput, {
187
236
  promise: out,
188
- sanitize: ctx.sanitize
237
+ sanitize: ctx.sanitize,
238
+ context: ctx
189
239
  }, node.key);
190
- return renderOutput(out, node.key, ctx.sanitize);
240
+ return renderOutput(out, node.key, ctx.sanitize, ctx);
191
241
  } catch {
192
242
  return renderFallback(node, ctx);
193
243
  }
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
  }
@@ -161,9 +210,10 @@ function renderNode(node, ctx) {
161
210
  const out = r(node);
162
211
  if (out && typeof out.then === "function") return /* @__PURE__ */ jsx(AsyncOutput, {
163
212
  promise: out,
164
- sanitize: ctx.sanitize
213
+ sanitize: ctx.sanitize,
214
+ context: ctx
165
215
  }, node.key);
166
- return renderOutput(out, node.key, ctx.sanitize);
216
+ return renderOutput(out, node.key, ctx.sanitize, ctx);
167
217
  } catch {
168
218
  return renderFallback(node, ctx);
169
219
  }
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.0",
4
4
  "description": "React adapter for AIGUI — stream LLM output into live React components with cards, plugins, and interactive widgets.",
5
5
  "keywords": [
6
6
  "llm",
@@ -50,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.0"
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",