@ai-gui/vanilla 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 +22 -6
- package/dist/index.cjs +259 -50
- package/dist/index.d.cts +27 -4
- package/dist/index.d.ts +27 -4
- package/dist/index.js +260 -51
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -11,24 +11,37 @@ pnpm add @ai-gui/core @ai-gui/vanilla
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
import { CardRegistry } from "@ai-gui/core"
|
|
14
|
+
import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
|
|
15
15
|
import { createRenderer } from "@ai-gui/vanilla"
|
|
16
16
|
|
|
17
17
|
const registry = new CardRegistry()
|
|
18
18
|
registry.register({
|
|
19
19
|
type: "weather",
|
|
20
20
|
description: "Weather summary",
|
|
21
|
-
//
|
|
22
|
-
render: (data: any, { onAction }: any) => {
|
|
21
|
+
// HTMLElement remains supported. Return a VanillaCardInstance for stateful cards.
|
|
22
|
+
render: (data: any, { state, onAction }: any) => {
|
|
23
23
|
const el = document.createElement("div")
|
|
24
24
|
el.textContent = `${data.city} — ${data.tempC}°C`
|
|
25
|
-
return
|
|
25
|
+
return {
|
|
26
|
+
element: el,
|
|
27
|
+
update(next: any, context: any) {
|
|
28
|
+
el.textContent = `${next.city} — ${next.tempC}°C (${context.state.status})`
|
|
29
|
+
},
|
|
30
|
+
destroy() {},
|
|
31
|
+
}
|
|
26
32
|
},
|
|
27
33
|
})
|
|
28
34
|
|
|
35
|
+
const actions = new ActionRegistry()
|
|
36
|
+
actions.register({ type: "refresh", run: async (params, { signal }) => fetch("/api/weather", { signal }) })
|
|
37
|
+
const cardStore = new CardStore({ registry })
|
|
38
|
+
const actionRuntime = createActionRuntime({ registry: actions, cardStore })
|
|
39
|
+
|
|
29
40
|
const r = createRenderer(document.getElementById("out")!, {
|
|
30
41
|
registry,
|
|
31
|
-
|
|
42
|
+
cardStore,
|
|
43
|
+
actionRuntime,
|
|
44
|
+
onCardAction: (action) => console.log("observed", action),
|
|
32
45
|
})
|
|
33
46
|
|
|
34
47
|
await r.feed(response.body!) // r.push(chunk) / r.reset() / r.destroy()
|
|
@@ -36,6 +49,9 @@ await r.feed(response.body!) // r.push(chunk) / r.reset() / r.destroy()
|
|
|
36
49
|
|
|
37
50
|
## Exports
|
|
38
51
|
|
|
39
|
-
- `createRenderer(el, { registry?, plugins?, sanitize?, onCardAction? })` → `{ push, feed, reset, destroy }`.
|
|
52
|
+
- `createRenderer(el, { registry?, cardStore?, plugins?, sanitize?, actionRuntime?, onCardAction? })` → `{ push, feed, reset, destroy }`.
|
|
53
|
+
- `VanillaCardInstance` is `{ element, update(data, { state, onAction }), destroy? }`. Its host and `element` stay mounted while AST data, store data, or action state changes.
|
|
54
|
+
- Legacy factories returning an `HTMLElement` remain supported. For cards with an `id` and a `cardStore`, the adapter may rebuild that child element after store updates so it displays current data.
|
|
55
|
+
- Cards with a valid top-level `id` register their initial data in the supplied external `CardStore`. The adapter subscribes but never clears that store; a shared store updates every renderer displaying the same card id.
|
|
40
56
|
|
|
41
57
|
`destroy()` tears down the host and cleans up any mounted interactive widgets. See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
|
package/dist/index.cjs
CHANGED
|
@@ -26,18 +26,18 @@ const __ai_gui_core = __toESM(require("@ai-gui/core"));
|
|
|
26
26
|
|
|
27
27
|
//#region src/render-output.ts
|
|
28
28
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
29
|
-
function renderOutputToElement(out) {
|
|
29
|
+
function renderOutputToElement(out, sanitize) {
|
|
30
30
|
switch (out.kind) {
|
|
31
31
|
case "html": {
|
|
32
32
|
const el = document.createElement("div");
|
|
33
|
-
el.innerHTML = (
|
|
33
|
+
el.innerHTML = sanitizeOutput(out.html, sanitize);
|
|
34
34
|
return el;
|
|
35
35
|
}
|
|
36
36
|
case "element": {
|
|
37
37
|
const el = document.createElement(out.tag);
|
|
38
38
|
for (const [key, value] of Object.entries(out.props ?? {})) if (key === "class" || key === "className") el.className = String(value);
|
|
39
39
|
else el.setAttribute(key, String(value));
|
|
40
|
-
for (const child of out.children ?? []) el.appendChild(renderOutputToElement(child));
|
|
40
|
+
for (const child of out.children ?? []) el.appendChild(renderOutputToElement(child, sanitize));
|
|
41
41
|
return el;
|
|
42
42
|
}
|
|
43
43
|
case "card": {
|
|
@@ -52,37 +52,61 @@ function renderOutputToElement(out) {
|
|
|
52
52
|
const el = document.createElement("div");
|
|
53
53
|
el.setAttribute("data-aigui-mount", "");
|
|
54
54
|
queueMicrotask(() => {
|
|
55
|
+
if (el.__aiguiDisposed) return;
|
|
55
56
|
const c = out.mount(el);
|
|
56
|
-
if (typeof c === "function") el.
|
|
57
|
+
if (typeof c === "function") if (el.__aiguiDisposed) c();
|
|
58
|
+
else el.__aiguiCleanup = c;
|
|
57
59
|
});
|
|
58
60
|
return el;
|
|
59
61
|
}
|
|
60
62
|
}
|
|
61
63
|
}
|
|
64
|
+
function sanitizeOutput(html, sanitize) {
|
|
65
|
+
if (sanitize === false) return html;
|
|
66
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
67
|
+
}
|
|
62
68
|
|
|
63
69
|
//#endregion
|
|
64
70
|
//#region src/render-node-dom.ts
|
|
65
71
|
function renderNodeToElement(node, ctx) {
|
|
66
|
-
const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
|
|
72
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
67
73
|
if (r) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
if (node.complete === false) {
|
|
75
|
+
const loading = document.createElement("div");
|
|
76
|
+
loading.setAttribute("data-aigui-block-loading", "");
|
|
77
|
+
loading.setAttribute("data-block-type", node.type);
|
|
78
|
+
return loading;
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const out = r(node);
|
|
82
|
+
if (typeof out?.then === "function") {
|
|
83
|
+
const host = document.createElement("div");
|
|
84
|
+
host.setAttribute("data-aigui-async-pending", "");
|
|
85
|
+
out.then((res) => {
|
|
86
|
+
if (host.__aiguiDisposed) return;
|
|
87
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
88
|
+
host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
|
|
89
|
+
}, () => {
|
|
90
|
+
if (host.__aiguiDisposed) return;
|
|
91
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
92
|
+
host.setAttribute("data-aigui-async-error", "");
|
|
93
|
+
});
|
|
94
|
+
return host;
|
|
95
|
+
}
|
|
96
|
+
return renderOutputToElement(out, ctx.sanitize);
|
|
97
|
+
} catch {
|
|
98
|
+
return renderFallback(node, ctx);
|
|
74
99
|
}
|
|
75
|
-
return renderOutputToElement(out);
|
|
76
100
|
}
|
|
77
101
|
switch (node.type) {
|
|
78
102
|
case "heading": {
|
|
79
103
|
const el = document.createElement(node.tag ?? "h1");
|
|
80
|
-
el.innerHTML = node.html ?? "";
|
|
104
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
81
105
|
return el;
|
|
82
106
|
}
|
|
83
107
|
case "paragraph": {
|
|
84
108
|
const el = document.createElement("p");
|
|
85
|
-
el.innerHTML = node.html ?? "";
|
|
109
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
86
110
|
return el;
|
|
87
111
|
}
|
|
88
112
|
case "code": {
|
|
@@ -96,17 +120,22 @@ function renderNodeToElement(node, ctx) {
|
|
|
96
120
|
case "hr": return document.createElement("hr");
|
|
97
121
|
case "html": {
|
|
98
122
|
const el = document.createElement("div");
|
|
99
|
-
el.innerHTML = node.content ?? "";
|
|
123
|
+
el.innerHTML = renderHtml(node.content ?? "", ctx);
|
|
100
124
|
return el;
|
|
101
125
|
}
|
|
102
126
|
case "card": return renderCardElement(node, ctx);
|
|
103
|
-
default:
|
|
104
|
-
const el = document.createElement("div");
|
|
105
|
-
el.innerHTML = node.html ?? (0, __ai_gui_core.sanitizeHtml)(node.content ?? "");
|
|
106
|
-
return el;
|
|
107
|
-
}
|
|
127
|
+
default: return renderFallback(node, ctx);
|
|
108
128
|
}
|
|
109
129
|
}
|
|
130
|
+
function renderFallback(node, ctx) {
|
|
131
|
+
const el = document.createElement("div");
|
|
132
|
+
el.innerHTML = renderHtml(node.html ?? node.content ?? "", ctx);
|
|
133
|
+
return el;
|
|
134
|
+
}
|
|
135
|
+
function renderHtml(html, ctx) {
|
|
136
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
137
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
138
|
+
}
|
|
110
139
|
function renderCardElement(node, ctx) {
|
|
111
140
|
const card = node.card;
|
|
112
141
|
if (!card) return document.createElement("div");
|
|
@@ -125,22 +154,153 @@ function renderCardElement(node, ctx) {
|
|
|
125
154
|
return pre;
|
|
126
155
|
}
|
|
127
156
|
const factory = ctx.registry?.getRender(card.type);
|
|
128
|
-
if (!factory)
|
|
129
|
-
const pre = document.createElement("pre");
|
|
130
|
-
pre.setAttribute("data-aigui-card-fallback", "");
|
|
131
|
-
const c = document.createElement("code");
|
|
132
|
-
c.textContent = JSON.stringify(card.data, null, 2);
|
|
133
|
-
pre.appendChild(c);
|
|
134
|
-
return pre;
|
|
135
|
-
}
|
|
157
|
+
if (!factory) return createCardFallback(card, ctx);
|
|
136
158
|
const host = document.createElement("div");
|
|
137
159
|
host.setAttribute("data-aigui-card", card.type);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
}) }));
|
|
160
|
+
const controller = createCardController(host, card, factory, ctx);
|
|
161
|
+
host.__aiguiCardController = controller;
|
|
162
|
+
host.__aiguiCleanup = () => controller.destroy();
|
|
142
163
|
return host;
|
|
143
164
|
}
|
|
165
|
+
function createCardFallback(card, ctx) {
|
|
166
|
+
const pre = document.createElement("pre");
|
|
167
|
+
pre.setAttribute("data-aigui-card-fallback", "");
|
|
168
|
+
const code = document.createElement("code");
|
|
169
|
+
pre.appendChild(code);
|
|
170
|
+
const render = (data) => {
|
|
171
|
+
pre.removeAttribute("data-aigui-card-missing");
|
|
172
|
+
code.textContent = JSON.stringify(data, null, 2);
|
|
173
|
+
};
|
|
174
|
+
const missing = () => {
|
|
175
|
+
pre.setAttribute("data-aigui-card-missing", "");
|
|
176
|
+
code.textContent = "Card data is unavailable";
|
|
177
|
+
};
|
|
178
|
+
let unsubscribe = () => {};
|
|
179
|
+
if (card.id !== void 0 && ctx.cardStore) {
|
|
180
|
+
try {
|
|
181
|
+
const record = ctx.cardStore.register({
|
|
182
|
+
id: card.id,
|
|
183
|
+
type: card.type,
|
|
184
|
+
data: card.data
|
|
185
|
+
});
|
|
186
|
+
render(record.data);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
pre.setAttribute("data-aigui-card-store-error", "");
|
|
189
|
+
code.textContent = error instanceof Error ? error.message : "Card registration failed";
|
|
190
|
+
}
|
|
191
|
+
unsubscribe = ctx.cardStore.subscribe(card.id, (next) => {
|
|
192
|
+
if (!next || next.type !== card.type) missing();
|
|
193
|
+
else {
|
|
194
|
+
pre.removeAttribute("data-aigui-card-store-error");
|
|
195
|
+
render(next.data);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
pre.__aiguiCleanup = unsubscribe;
|
|
199
|
+
} else render(card.data);
|
|
200
|
+
return pre;
|
|
201
|
+
}
|
|
202
|
+
function updateCardElement(el, node) {
|
|
203
|
+
return el.__aiguiCardController?.updateNode(node) ?? false;
|
|
204
|
+
}
|
|
205
|
+
function createCardController(host, initialCard, factory, ctx) {
|
|
206
|
+
let card = initialCard;
|
|
207
|
+
let destroyed = false;
|
|
208
|
+
let unsubscribe = () => {};
|
|
209
|
+
let instance;
|
|
210
|
+
let element;
|
|
211
|
+
let missing = false;
|
|
212
|
+
const id = card.id;
|
|
213
|
+
const onAction = (action) => ctx.onCardAction?.({
|
|
214
|
+
...action,
|
|
215
|
+
cardType: card.type,
|
|
216
|
+
...id === void 0 ? {} : { cardId: id }
|
|
217
|
+
});
|
|
218
|
+
let data = card.data;
|
|
219
|
+
let state = { status: "idle" };
|
|
220
|
+
let registrationError;
|
|
221
|
+
if (id !== void 0 && ctx.cardStore) try {
|
|
222
|
+
const record = ctx.cardStore.register({
|
|
223
|
+
id,
|
|
224
|
+
type: card.type,
|
|
225
|
+
data
|
|
226
|
+
});
|
|
227
|
+
data = record.data;
|
|
228
|
+
state = record.action;
|
|
229
|
+
} catch (error) {
|
|
230
|
+
registrationError = error;
|
|
231
|
+
}
|
|
232
|
+
const context = () => ({
|
|
233
|
+
state,
|
|
234
|
+
onAction
|
|
235
|
+
});
|
|
236
|
+
const mount = (nextData) => {
|
|
237
|
+
const rendered = factory(nextData, context());
|
|
238
|
+
if (isVanillaCardInstance(rendered)) {
|
|
239
|
+
instance = rendered;
|
|
240
|
+
element = rendered.element;
|
|
241
|
+
} else {
|
|
242
|
+
instance = void 0;
|
|
243
|
+
element = rendered;
|
|
244
|
+
}
|
|
245
|
+
host.replaceChildren(element);
|
|
246
|
+
};
|
|
247
|
+
const update = (nextData, nextState) => {
|
|
248
|
+
if (destroyed) return;
|
|
249
|
+
data = nextData;
|
|
250
|
+
state = nextState;
|
|
251
|
+
host.removeAttribute("data-aigui-card-store-error");
|
|
252
|
+
if (missing) {
|
|
253
|
+
missing = false;
|
|
254
|
+
host.removeAttribute("data-aigui-card-missing");
|
|
255
|
+
if (element) host.replaceChildren(element);
|
|
256
|
+
}
|
|
257
|
+
if (instance) instance.update(data, context());
|
|
258
|
+
else mount(data);
|
|
259
|
+
};
|
|
260
|
+
const showMissing = () => {
|
|
261
|
+
if (destroyed || missing) return;
|
|
262
|
+
missing = true;
|
|
263
|
+
host.removeAttribute("data-aigui-card-store-error");
|
|
264
|
+
host.setAttribute("data-aigui-card-missing", "");
|
|
265
|
+
const fallback = document.createElement("code");
|
|
266
|
+
fallback.textContent = "Card data is unavailable";
|
|
267
|
+
host.replaceChildren(fallback);
|
|
268
|
+
};
|
|
269
|
+
if (registrationError === void 0) mount(data);
|
|
270
|
+
else {
|
|
271
|
+
host.setAttribute("data-aigui-card-store-error", "");
|
|
272
|
+
const code = document.createElement("code");
|
|
273
|
+
code.textContent = registrationError instanceof Error ? registrationError.message : "Card registration failed";
|
|
274
|
+
host.replaceChildren(code);
|
|
275
|
+
}
|
|
276
|
+
if (id !== void 0 && ctx.cardStore) unsubscribe = ctx.cardStore.subscribe(id, (record) => {
|
|
277
|
+
if (!record || record.type !== card.type) {
|
|
278
|
+
showMissing();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
update(record.data, record.action);
|
|
282
|
+
});
|
|
283
|
+
return {
|
|
284
|
+
updateNode(node) {
|
|
285
|
+
const next = node.card;
|
|
286
|
+
if (!next?.complete || !next.valid || next.type !== card.type || next.id !== id) return false;
|
|
287
|
+
card = next;
|
|
288
|
+
if (id === void 0 || !ctx.cardStore) update(next.data, state);
|
|
289
|
+
return true;
|
|
290
|
+
},
|
|
291
|
+
destroy() {
|
|
292
|
+
if (destroyed) return;
|
|
293
|
+
destroyed = true;
|
|
294
|
+
unsubscribe();
|
|
295
|
+
unsubscribe = () => {};
|
|
296
|
+
instance?.destroy?.();
|
|
297
|
+
instance = void 0;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
function isVanillaCardInstance(value) {
|
|
302
|
+
return !(value instanceof HTMLElement) && value !== null && typeof value === "object" && value.element instanceof HTMLElement && typeof value.update === "function";
|
|
303
|
+
}
|
|
144
304
|
|
|
145
305
|
//#endregion
|
|
146
306
|
//#region src/reconcile.ts
|
|
@@ -149,7 +309,15 @@ function createReconcileState() {
|
|
|
149
309
|
}
|
|
150
310
|
/** Run a mounted widget's cleanup (if any) before the element leaves the DOM. */
|
|
151
311
|
function disposeEl(el) {
|
|
152
|
-
el.
|
|
312
|
+
for (const child of el.querySelectorAll("*")) disposeManagedEl(child);
|
|
313
|
+
disposeManagedEl(el);
|
|
314
|
+
}
|
|
315
|
+
function disposeManagedEl(el) {
|
|
316
|
+
const managed = el;
|
|
317
|
+
if (managed.__aiguiDisposed) return;
|
|
318
|
+
managed.__aiguiDisposed = true;
|
|
319
|
+
managed.__aiguiCleanup?.();
|
|
320
|
+
managed.__aiguiCleanup = void 0;
|
|
153
321
|
}
|
|
154
322
|
function reconcile(container, nodes, ctx, state) {
|
|
155
323
|
const nextKeys = new Set(nodes.map((n) => n.key));
|
|
@@ -184,6 +352,7 @@ function reconcile(container, nodes, ctx, state) {
|
|
|
184
352
|
}
|
|
185
353
|
/** Mutate an existing element to reflect the node without replacing it. Returns false when a fresh element is required. */
|
|
186
354
|
function updateElementInPlace(el, node, ctx) {
|
|
355
|
+
if (ctx.nodeRenderers?.[node.type]) return false;
|
|
187
356
|
switch (node.type) {
|
|
188
357
|
case "heading":
|
|
189
358
|
if (el.tagName !== (node.tag ?? "h1").toUpperCase()) return false;
|
|
@@ -209,10 +378,10 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
209
378
|
if (el.tagName !== "DIV") return false;
|
|
210
379
|
el.innerHTML = node.content ?? "";
|
|
211
380
|
return true;
|
|
212
|
-
case "card": return
|
|
381
|
+
case "card": return updateCardElement(el, node);
|
|
213
382
|
default:
|
|
214
383
|
if (el.tagName !== "DIV") return false;
|
|
215
|
-
el.innerHTML = node.html ??
|
|
384
|
+
el.innerHTML = node.html ?? node.content ?? "";
|
|
216
385
|
return true;
|
|
217
386
|
}
|
|
218
387
|
}
|
|
@@ -220,33 +389,73 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
220
389
|
//#endregion
|
|
221
390
|
//#region src/create-renderer.ts
|
|
222
391
|
function createRenderer(el, options = {}) {
|
|
223
|
-
const { onCardAction,...rendererOpts } = options;
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
392
|
+
const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
|
|
393
|
+
let actionScope = {
|
|
394
|
+
owner: {},
|
|
395
|
+
controller: new AbortController()
|
|
396
|
+
};
|
|
397
|
+
const handleCardAction = (action) => {
|
|
398
|
+
if (actionRuntime) actionRuntime.dispatch({
|
|
399
|
+
...action,
|
|
400
|
+
params: action.params
|
|
401
|
+
}, {
|
|
402
|
+
owner: actionScope.owner,
|
|
403
|
+
signal: actionScope.controller.signal
|
|
404
|
+
}).catch(() => {});
|
|
405
|
+
onCardAction?.(action);
|
|
228
406
|
};
|
|
229
407
|
const state = createReconcileState();
|
|
408
|
+
let destroyed = false;
|
|
230
409
|
const renderer = new __ai_gui_core.Renderer({
|
|
231
410
|
...rendererOpts,
|
|
232
|
-
onPatch: (_patches, nodes) =>
|
|
411
|
+
onPatch: (_patches, nodes) => {
|
|
412
|
+
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
413
|
+
}
|
|
233
414
|
});
|
|
415
|
+
const ctx = {
|
|
416
|
+
registry: options.registry,
|
|
417
|
+
cardStore,
|
|
418
|
+
onCardAction: handleCardAction,
|
|
419
|
+
plugins: options.plugins,
|
|
420
|
+
nodeRenderers: (0, __ai_gui_core.collectNodeRenderers)(options.plugins, { debugTarget: renderer }),
|
|
421
|
+
sanitize: options.sanitize,
|
|
422
|
+
sanitized: true
|
|
423
|
+
};
|
|
234
424
|
const disposeAll = () => {
|
|
235
425
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
236
426
|
};
|
|
427
|
+
const resetActionScope = () => {
|
|
428
|
+
actionScope.controller.abort();
|
|
429
|
+
actionScope = {
|
|
430
|
+
owner: {},
|
|
431
|
+
controller: new AbortController()
|
|
432
|
+
};
|
|
433
|
+
};
|
|
237
434
|
return {
|
|
238
|
-
|
|
239
|
-
|
|
435
|
+
debugSource: "renderer",
|
|
436
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
437
|
+
push: (c) => {
|
|
438
|
+
if (!destroyed) renderer.push(c);
|
|
439
|
+
},
|
|
440
|
+
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
240
441
|
reset: () => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
442
|
+
if (!destroyed) {
|
|
443
|
+
resetActionScope();
|
|
444
|
+
disposeAll();
|
|
445
|
+
renderer.reset();
|
|
446
|
+
state.els.clear();
|
|
447
|
+
el.replaceChildren();
|
|
448
|
+
}
|
|
245
449
|
},
|
|
246
450
|
destroy: () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
451
|
+
if (!destroyed) {
|
|
452
|
+
destroyed = true;
|
|
453
|
+
actionScope.controller.abort();
|
|
454
|
+
renderer.reset();
|
|
455
|
+
disposeAll();
|
|
456
|
+
state.els.clear();
|
|
457
|
+
el.replaceChildren();
|
|
458
|
+
}
|
|
250
459
|
}
|
|
251
460
|
};
|
|
252
461
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
2
|
|
|
3
3
|
//#region src/render-node-dom.d.ts
|
|
4
|
+
interface VanillaCardAction {
|
|
5
|
+
type: string;
|
|
6
|
+
params?: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface VanillaCardUpdateContext {
|
|
9
|
+
state: CardAction;
|
|
10
|
+
onAction: (action: VanillaCardAction) => void;
|
|
11
|
+
}
|
|
12
|
+
interface VanillaCardInstance {
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
update: (data: unknown, context: VanillaCardUpdateContext) => void;
|
|
15
|
+
destroy?: () => void;
|
|
16
|
+
}
|
|
17
|
+
type VanillaCardFactory = (data: unknown, context: VanillaCardUpdateContext) => HTMLElement | VanillaCardInstance;
|
|
4
18
|
interface DomRenderContext {
|
|
5
19
|
registry?: CardRegistry;
|
|
20
|
+
cardStore?: CardStore;
|
|
6
21
|
onCardAction?: (action: {
|
|
7
22
|
type: string;
|
|
8
23
|
params?: unknown;
|
|
9
24
|
cardType: string;
|
|
25
|
+
cardId?: string;
|
|
10
26
|
}) => void;
|
|
11
27
|
plugins?: AIGuiPlugin[];
|
|
28
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
29
|
+
sanitize?: RendererOptions["sanitize"];
|
|
30
|
+
sanitized?: boolean;
|
|
12
31
|
}
|
|
13
32
|
declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
|
|
14
33
|
|
|
15
34
|
//#endregion
|
|
16
35
|
//#region src/create-renderer.d.ts
|
|
17
36
|
interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
37
|
+
actionRuntime?: ActionRuntime;
|
|
38
|
+
cardStore?: CardStore;
|
|
18
39
|
onCardAction?: DomRenderContext["onCardAction"];
|
|
19
40
|
}
|
|
20
41
|
interface VanillaRenderer {
|
|
42
|
+
readonly debugSource: "renderer";
|
|
43
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
21
44
|
push: (chunk: string) => void;
|
|
22
|
-
feed: (source:
|
|
45
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
23
46
|
reset: () => void;
|
|
24
47
|
destroy: () => void;
|
|
25
48
|
}
|
|
@@ -28,7 +51,7 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
28
51
|
//#endregion
|
|
29
52
|
//#region src/render-output.d.ts
|
|
30
53
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
31
|
-
declare function renderOutputToElement(out: RenderOutput): HTMLElement;
|
|
54
|
+
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
32
55
|
|
|
33
56
|
//#endregion
|
|
34
|
-
export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
|
57
|
+
export { CreateRendererOptions, DomRenderContext, VanillaCardAction, VanillaCardFactory, VanillaCardInstance, VanillaCardUpdateContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,48 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, CardAction, CardRegistry, CardStore, DebugEventListener, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
2
|
|
|
3
3
|
//#region src/render-node-dom.d.ts
|
|
4
|
+
interface VanillaCardAction {
|
|
5
|
+
type: string;
|
|
6
|
+
params?: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface VanillaCardUpdateContext {
|
|
9
|
+
state: CardAction;
|
|
10
|
+
onAction: (action: VanillaCardAction) => void;
|
|
11
|
+
}
|
|
12
|
+
interface VanillaCardInstance {
|
|
13
|
+
element: HTMLElement;
|
|
14
|
+
update: (data: unknown, context: VanillaCardUpdateContext) => void;
|
|
15
|
+
destroy?: () => void;
|
|
16
|
+
}
|
|
17
|
+
type VanillaCardFactory = (data: unknown, context: VanillaCardUpdateContext) => HTMLElement | VanillaCardInstance;
|
|
4
18
|
interface DomRenderContext {
|
|
5
19
|
registry?: CardRegistry;
|
|
20
|
+
cardStore?: CardStore;
|
|
6
21
|
onCardAction?: (action: {
|
|
7
22
|
type: string;
|
|
8
23
|
params?: unknown;
|
|
9
24
|
cardType: string;
|
|
25
|
+
cardId?: string;
|
|
10
26
|
}) => void;
|
|
11
27
|
plugins?: AIGuiPlugin[];
|
|
28
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
29
|
+
sanitize?: RendererOptions["sanitize"];
|
|
30
|
+
sanitized?: boolean;
|
|
12
31
|
}
|
|
13
32
|
declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
|
|
14
33
|
|
|
15
34
|
//#endregion
|
|
16
35
|
//#region src/create-renderer.d.ts
|
|
17
36
|
interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
37
|
+
actionRuntime?: ActionRuntime;
|
|
38
|
+
cardStore?: CardStore;
|
|
18
39
|
onCardAction?: DomRenderContext["onCardAction"];
|
|
19
40
|
}
|
|
20
41
|
interface VanillaRenderer {
|
|
42
|
+
readonly debugSource: "renderer";
|
|
43
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
21
44
|
push: (chunk: string) => void;
|
|
22
|
-
feed: (source:
|
|
45
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
23
46
|
reset: () => void;
|
|
24
47
|
destroy: () => void;
|
|
25
48
|
}
|
|
@@ -28,7 +51,7 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
28
51
|
//#endregion
|
|
29
52
|
//#region src/render-output.d.ts
|
|
30
53
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
31
|
-
declare function renderOutputToElement(out: RenderOutput): HTMLElement;
|
|
54
|
+
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
32
55
|
|
|
33
56
|
//#endregion
|
|
34
|
-
export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
|
57
|
+
export { CreateRendererOptions, DomRenderContext, VanillaCardAction, VanillaCardFactory, VanillaCardInstance, VanillaCardUpdateContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
package/dist/index.js
CHANGED
|
@@ -2,18 +2,18 @@ import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
|
|
|
2
2
|
|
|
3
3
|
//#region src/render-output.ts
|
|
4
4
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
5
|
-
function renderOutputToElement(out) {
|
|
5
|
+
function renderOutputToElement(out, sanitize) {
|
|
6
6
|
switch (out.kind) {
|
|
7
7
|
case "html": {
|
|
8
8
|
const el = document.createElement("div");
|
|
9
|
-
el.innerHTML =
|
|
9
|
+
el.innerHTML = sanitizeOutput(out.html, sanitize);
|
|
10
10
|
return el;
|
|
11
11
|
}
|
|
12
12
|
case "element": {
|
|
13
13
|
const el = document.createElement(out.tag);
|
|
14
14
|
for (const [key, value] of Object.entries(out.props ?? {})) if (key === "class" || key === "className") el.className = String(value);
|
|
15
15
|
else el.setAttribute(key, String(value));
|
|
16
|
-
for (const child of out.children ?? []) el.appendChild(renderOutputToElement(child));
|
|
16
|
+
for (const child of out.children ?? []) el.appendChild(renderOutputToElement(child, sanitize));
|
|
17
17
|
return el;
|
|
18
18
|
}
|
|
19
19
|
case "card": {
|
|
@@ -28,37 +28,61 @@ function renderOutputToElement(out) {
|
|
|
28
28
|
const el = document.createElement("div");
|
|
29
29
|
el.setAttribute("data-aigui-mount", "");
|
|
30
30
|
queueMicrotask(() => {
|
|
31
|
+
if (el.__aiguiDisposed) return;
|
|
31
32
|
const c = out.mount(el);
|
|
32
|
-
if (typeof c === "function") el.
|
|
33
|
+
if (typeof c === "function") if (el.__aiguiDisposed) c();
|
|
34
|
+
else el.__aiguiCleanup = c;
|
|
33
35
|
});
|
|
34
36
|
return el;
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
39
|
}
|
|
40
|
+
function sanitizeOutput(html, sanitize) {
|
|
41
|
+
if (sanitize === false) return html;
|
|
42
|
+
return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
43
|
+
}
|
|
38
44
|
|
|
39
45
|
//#endregion
|
|
40
46
|
//#region src/render-node-dom.ts
|
|
41
47
|
function renderNodeToElement(node, ctx) {
|
|
42
|
-
const r = collectNodeRenderers(ctx.plugins)[node.type];
|
|
48
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
43
49
|
if (r) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
if (node.complete === false) {
|
|
51
|
+
const loading = document.createElement("div");
|
|
52
|
+
loading.setAttribute("data-aigui-block-loading", "");
|
|
53
|
+
loading.setAttribute("data-block-type", node.type);
|
|
54
|
+
return loading;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const out = r(node);
|
|
58
|
+
if (typeof out?.then === "function") {
|
|
59
|
+
const host = document.createElement("div");
|
|
60
|
+
host.setAttribute("data-aigui-async-pending", "");
|
|
61
|
+
out.then((res) => {
|
|
62
|
+
if (host.__aiguiDisposed) return;
|
|
63
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
64
|
+
host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
|
|
65
|
+
}, () => {
|
|
66
|
+
if (host.__aiguiDisposed) return;
|
|
67
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
68
|
+
host.setAttribute("data-aigui-async-error", "");
|
|
69
|
+
});
|
|
70
|
+
return host;
|
|
71
|
+
}
|
|
72
|
+
return renderOutputToElement(out, ctx.sanitize);
|
|
73
|
+
} catch {
|
|
74
|
+
return renderFallback(node, ctx);
|
|
75
|
+
}
|
|
52
76
|
}
|
|
53
77
|
switch (node.type) {
|
|
54
78
|
case "heading": {
|
|
55
79
|
const el = document.createElement(node.tag ?? "h1");
|
|
56
|
-
el.innerHTML = node.html ?? "";
|
|
80
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
57
81
|
return el;
|
|
58
82
|
}
|
|
59
83
|
case "paragraph": {
|
|
60
84
|
const el = document.createElement("p");
|
|
61
|
-
el.innerHTML = node.html ?? "";
|
|
85
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
62
86
|
return el;
|
|
63
87
|
}
|
|
64
88
|
case "code": {
|
|
@@ -72,17 +96,22 @@ function renderNodeToElement(node, ctx) {
|
|
|
72
96
|
case "hr": return document.createElement("hr");
|
|
73
97
|
case "html": {
|
|
74
98
|
const el = document.createElement("div");
|
|
75
|
-
el.innerHTML = node.content ?? "";
|
|
99
|
+
el.innerHTML = renderHtml(node.content ?? "", ctx);
|
|
76
100
|
return el;
|
|
77
101
|
}
|
|
78
102
|
case "card": return renderCardElement(node, ctx);
|
|
79
|
-
default:
|
|
80
|
-
const el = document.createElement("div");
|
|
81
|
-
el.innerHTML = node.html ?? sanitizeHtml(node.content ?? "");
|
|
82
|
-
return el;
|
|
83
|
-
}
|
|
103
|
+
default: return renderFallback(node, ctx);
|
|
84
104
|
}
|
|
85
105
|
}
|
|
106
|
+
function renderFallback(node, ctx) {
|
|
107
|
+
const el = document.createElement("div");
|
|
108
|
+
el.innerHTML = renderHtml(node.html ?? node.content ?? "", ctx);
|
|
109
|
+
return el;
|
|
110
|
+
}
|
|
111
|
+
function renderHtml(html, ctx) {
|
|
112
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
113
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
114
|
+
}
|
|
86
115
|
function renderCardElement(node, ctx) {
|
|
87
116
|
const card = node.card;
|
|
88
117
|
if (!card) return document.createElement("div");
|
|
@@ -101,22 +130,153 @@ function renderCardElement(node, ctx) {
|
|
|
101
130
|
return pre;
|
|
102
131
|
}
|
|
103
132
|
const factory = ctx.registry?.getRender(card.type);
|
|
104
|
-
if (!factory)
|
|
105
|
-
const pre = document.createElement("pre");
|
|
106
|
-
pre.setAttribute("data-aigui-card-fallback", "");
|
|
107
|
-
const c = document.createElement("code");
|
|
108
|
-
c.textContent = JSON.stringify(card.data, null, 2);
|
|
109
|
-
pre.appendChild(c);
|
|
110
|
-
return pre;
|
|
111
|
-
}
|
|
133
|
+
if (!factory) return createCardFallback(card, ctx);
|
|
112
134
|
const host = document.createElement("div");
|
|
113
135
|
host.setAttribute("data-aigui-card", card.type);
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}) }));
|
|
136
|
+
const controller = createCardController(host, card, factory, ctx);
|
|
137
|
+
host.__aiguiCardController = controller;
|
|
138
|
+
host.__aiguiCleanup = () => controller.destroy();
|
|
118
139
|
return host;
|
|
119
140
|
}
|
|
141
|
+
function createCardFallback(card, ctx) {
|
|
142
|
+
const pre = document.createElement("pre");
|
|
143
|
+
pre.setAttribute("data-aigui-card-fallback", "");
|
|
144
|
+
const code = document.createElement("code");
|
|
145
|
+
pre.appendChild(code);
|
|
146
|
+
const render = (data) => {
|
|
147
|
+
pre.removeAttribute("data-aigui-card-missing");
|
|
148
|
+
code.textContent = JSON.stringify(data, null, 2);
|
|
149
|
+
};
|
|
150
|
+
const missing = () => {
|
|
151
|
+
pre.setAttribute("data-aigui-card-missing", "");
|
|
152
|
+
code.textContent = "Card data is unavailable";
|
|
153
|
+
};
|
|
154
|
+
let unsubscribe = () => {};
|
|
155
|
+
if (card.id !== void 0 && ctx.cardStore) {
|
|
156
|
+
try {
|
|
157
|
+
const record = ctx.cardStore.register({
|
|
158
|
+
id: card.id,
|
|
159
|
+
type: card.type,
|
|
160
|
+
data: card.data
|
|
161
|
+
});
|
|
162
|
+
render(record.data);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
pre.setAttribute("data-aigui-card-store-error", "");
|
|
165
|
+
code.textContent = error instanceof Error ? error.message : "Card registration failed";
|
|
166
|
+
}
|
|
167
|
+
unsubscribe = ctx.cardStore.subscribe(card.id, (next) => {
|
|
168
|
+
if (!next || next.type !== card.type) missing();
|
|
169
|
+
else {
|
|
170
|
+
pre.removeAttribute("data-aigui-card-store-error");
|
|
171
|
+
render(next.data);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
pre.__aiguiCleanup = unsubscribe;
|
|
175
|
+
} else render(card.data);
|
|
176
|
+
return pre;
|
|
177
|
+
}
|
|
178
|
+
function updateCardElement(el, node) {
|
|
179
|
+
return el.__aiguiCardController?.updateNode(node) ?? false;
|
|
180
|
+
}
|
|
181
|
+
function createCardController(host, initialCard, factory, ctx) {
|
|
182
|
+
let card = initialCard;
|
|
183
|
+
let destroyed = false;
|
|
184
|
+
let unsubscribe = () => {};
|
|
185
|
+
let instance;
|
|
186
|
+
let element;
|
|
187
|
+
let missing = false;
|
|
188
|
+
const id = card.id;
|
|
189
|
+
const onAction = (action) => ctx.onCardAction?.({
|
|
190
|
+
...action,
|
|
191
|
+
cardType: card.type,
|
|
192
|
+
...id === void 0 ? {} : { cardId: id }
|
|
193
|
+
});
|
|
194
|
+
let data = card.data;
|
|
195
|
+
let state = { status: "idle" };
|
|
196
|
+
let registrationError;
|
|
197
|
+
if (id !== void 0 && ctx.cardStore) try {
|
|
198
|
+
const record = ctx.cardStore.register({
|
|
199
|
+
id,
|
|
200
|
+
type: card.type,
|
|
201
|
+
data
|
|
202
|
+
});
|
|
203
|
+
data = record.data;
|
|
204
|
+
state = record.action;
|
|
205
|
+
} catch (error) {
|
|
206
|
+
registrationError = error;
|
|
207
|
+
}
|
|
208
|
+
const context = () => ({
|
|
209
|
+
state,
|
|
210
|
+
onAction
|
|
211
|
+
});
|
|
212
|
+
const mount = (nextData) => {
|
|
213
|
+
const rendered = factory(nextData, context());
|
|
214
|
+
if (isVanillaCardInstance(rendered)) {
|
|
215
|
+
instance = rendered;
|
|
216
|
+
element = rendered.element;
|
|
217
|
+
} else {
|
|
218
|
+
instance = void 0;
|
|
219
|
+
element = rendered;
|
|
220
|
+
}
|
|
221
|
+
host.replaceChildren(element);
|
|
222
|
+
};
|
|
223
|
+
const update = (nextData, nextState) => {
|
|
224
|
+
if (destroyed) return;
|
|
225
|
+
data = nextData;
|
|
226
|
+
state = nextState;
|
|
227
|
+
host.removeAttribute("data-aigui-card-store-error");
|
|
228
|
+
if (missing) {
|
|
229
|
+
missing = false;
|
|
230
|
+
host.removeAttribute("data-aigui-card-missing");
|
|
231
|
+
if (element) host.replaceChildren(element);
|
|
232
|
+
}
|
|
233
|
+
if (instance) instance.update(data, context());
|
|
234
|
+
else mount(data);
|
|
235
|
+
};
|
|
236
|
+
const showMissing = () => {
|
|
237
|
+
if (destroyed || missing) return;
|
|
238
|
+
missing = true;
|
|
239
|
+
host.removeAttribute("data-aigui-card-store-error");
|
|
240
|
+
host.setAttribute("data-aigui-card-missing", "");
|
|
241
|
+
const fallback = document.createElement("code");
|
|
242
|
+
fallback.textContent = "Card data is unavailable";
|
|
243
|
+
host.replaceChildren(fallback);
|
|
244
|
+
};
|
|
245
|
+
if (registrationError === void 0) mount(data);
|
|
246
|
+
else {
|
|
247
|
+
host.setAttribute("data-aigui-card-store-error", "");
|
|
248
|
+
const code = document.createElement("code");
|
|
249
|
+
code.textContent = registrationError instanceof Error ? registrationError.message : "Card registration failed";
|
|
250
|
+
host.replaceChildren(code);
|
|
251
|
+
}
|
|
252
|
+
if (id !== void 0 && ctx.cardStore) unsubscribe = ctx.cardStore.subscribe(id, (record) => {
|
|
253
|
+
if (!record || record.type !== card.type) {
|
|
254
|
+
showMissing();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
update(record.data, record.action);
|
|
258
|
+
});
|
|
259
|
+
return {
|
|
260
|
+
updateNode(node) {
|
|
261
|
+
const next = node.card;
|
|
262
|
+
if (!next?.complete || !next.valid || next.type !== card.type || next.id !== id) return false;
|
|
263
|
+
card = next;
|
|
264
|
+
if (id === void 0 || !ctx.cardStore) update(next.data, state);
|
|
265
|
+
return true;
|
|
266
|
+
},
|
|
267
|
+
destroy() {
|
|
268
|
+
if (destroyed) return;
|
|
269
|
+
destroyed = true;
|
|
270
|
+
unsubscribe();
|
|
271
|
+
unsubscribe = () => {};
|
|
272
|
+
instance?.destroy?.();
|
|
273
|
+
instance = void 0;
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function isVanillaCardInstance(value) {
|
|
278
|
+
return !(value instanceof HTMLElement) && value !== null && typeof value === "object" && value.element instanceof HTMLElement && typeof value.update === "function";
|
|
279
|
+
}
|
|
120
280
|
|
|
121
281
|
//#endregion
|
|
122
282
|
//#region src/reconcile.ts
|
|
@@ -125,7 +285,15 @@ function createReconcileState() {
|
|
|
125
285
|
}
|
|
126
286
|
/** Run a mounted widget's cleanup (if any) before the element leaves the DOM. */
|
|
127
287
|
function disposeEl(el) {
|
|
128
|
-
el.
|
|
288
|
+
for (const child of el.querySelectorAll("*")) disposeManagedEl(child);
|
|
289
|
+
disposeManagedEl(el);
|
|
290
|
+
}
|
|
291
|
+
function disposeManagedEl(el) {
|
|
292
|
+
const managed = el;
|
|
293
|
+
if (managed.__aiguiDisposed) return;
|
|
294
|
+
managed.__aiguiDisposed = true;
|
|
295
|
+
managed.__aiguiCleanup?.();
|
|
296
|
+
managed.__aiguiCleanup = void 0;
|
|
129
297
|
}
|
|
130
298
|
function reconcile(container, nodes, ctx, state) {
|
|
131
299
|
const nextKeys = new Set(nodes.map((n) => n.key));
|
|
@@ -160,6 +328,7 @@ function reconcile(container, nodes, ctx, state) {
|
|
|
160
328
|
}
|
|
161
329
|
/** Mutate an existing element to reflect the node without replacing it. Returns false when a fresh element is required. */
|
|
162
330
|
function updateElementInPlace(el, node, ctx) {
|
|
331
|
+
if (ctx.nodeRenderers?.[node.type]) return false;
|
|
163
332
|
switch (node.type) {
|
|
164
333
|
case "heading":
|
|
165
334
|
if (el.tagName !== (node.tag ?? "h1").toUpperCase()) return false;
|
|
@@ -185,10 +354,10 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
185
354
|
if (el.tagName !== "DIV") return false;
|
|
186
355
|
el.innerHTML = node.content ?? "";
|
|
187
356
|
return true;
|
|
188
|
-
case "card": return
|
|
357
|
+
case "card": return updateCardElement(el, node);
|
|
189
358
|
default:
|
|
190
359
|
if (el.tagName !== "DIV") return false;
|
|
191
|
-
el.innerHTML = node.html ??
|
|
360
|
+
el.innerHTML = node.html ?? node.content ?? "";
|
|
192
361
|
return true;
|
|
193
362
|
}
|
|
194
363
|
}
|
|
@@ -196,33 +365,73 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
196
365
|
//#endregion
|
|
197
366
|
//#region src/create-renderer.ts
|
|
198
367
|
function createRenderer(el, options = {}) {
|
|
199
|
-
const { onCardAction,...rendererOpts } = options;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
368
|
+
const { actionRuntime, cardStore, onCardAction,...rendererOpts } = options;
|
|
369
|
+
let actionScope = {
|
|
370
|
+
owner: {},
|
|
371
|
+
controller: new AbortController()
|
|
372
|
+
};
|
|
373
|
+
const handleCardAction = (action) => {
|
|
374
|
+
if (actionRuntime) actionRuntime.dispatch({
|
|
375
|
+
...action,
|
|
376
|
+
params: action.params
|
|
377
|
+
}, {
|
|
378
|
+
owner: actionScope.owner,
|
|
379
|
+
signal: actionScope.controller.signal
|
|
380
|
+
}).catch(() => {});
|
|
381
|
+
onCardAction?.(action);
|
|
204
382
|
};
|
|
205
383
|
const state = createReconcileState();
|
|
384
|
+
let destroyed = false;
|
|
206
385
|
const renderer = new Renderer({
|
|
207
386
|
...rendererOpts,
|
|
208
|
-
onPatch: (_patches, nodes) =>
|
|
387
|
+
onPatch: (_patches, nodes) => {
|
|
388
|
+
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
389
|
+
}
|
|
209
390
|
});
|
|
391
|
+
const ctx = {
|
|
392
|
+
registry: options.registry,
|
|
393
|
+
cardStore,
|
|
394
|
+
onCardAction: handleCardAction,
|
|
395
|
+
plugins: options.plugins,
|
|
396
|
+
nodeRenderers: collectNodeRenderers(options.plugins, { debugTarget: renderer }),
|
|
397
|
+
sanitize: options.sanitize,
|
|
398
|
+
sanitized: true
|
|
399
|
+
};
|
|
210
400
|
const disposeAll = () => {
|
|
211
401
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
212
402
|
};
|
|
403
|
+
const resetActionScope = () => {
|
|
404
|
+
actionScope.controller.abort();
|
|
405
|
+
actionScope = {
|
|
406
|
+
owner: {},
|
|
407
|
+
controller: new AbortController()
|
|
408
|
+
};
|
|
409
|
+
};
|
|
213
410
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
411
|
+
debugSource: "renderer",
|
|
412
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
413
|
+
push: (c) => {
|
|
414
|
+
if (!destroyed) renderer.push(c);
|
|
415
|
+
},
|
|
416
|
+
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
216
417
|
reset: () => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
418
|
+
if (!destroyed) {
|
|
419
|
+
resetActionScope();
|
|
420
|
+
disposeAll();
|
|
421
|
+
renderer.reset();
|
|
422
|
+
state.els.clear();
|
|
423
|
+
el.replaceChildren();
|
|
424
|
+
}
|
|
221
425
|
},
|
|
222
426
|
destroy: () => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
427
|
+
if (!destroyed) {
|
|
428
|
+
destroyed = true;
|
|
429
|
+
actionScope.controller.abort();
|
|
430
|
+
renderer.reset();
|
|
431
|
+
disposeAll();
|
|
432
|
+
state.els.clear();
|
|
433
|
+
el.replaceChildren();
|
|
434
|
+
}
|
|
226
435
|
}
|
|
227
436
|
};
|
|
228
437
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/vanilla",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Framework-free DOM adapter for AIGUI — stream LLM output straight into the DOM.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -22,14 +22,23 @@
|
|
|
22
22
|
"homepage": "https://github.com/liliang-cn/aigui#readme",
|
|
23
23
|
"bugs": "https://github.com/liliang-cn/aigui/issues",
|
|
24
24
|
"type": "module",
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
25
29
|
"main": "./dist/index.cjs",
|
|
26
30
|
"module": "./dist/index.js",
|
|
27
31
|
"types": "./dist/index.d.ts",
|
|
28
32
|
"exports": {
|
|
29
33
|
".": {
|
|
30
|
-
"
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
"import": {
|
|
35
|
+
"types": "./dist/index.d.ts",
|
|
36
|
+
"default": "./dist/index.js"
|
|
37
|
+
},
|
|
38
|
+
"require": {
|
|
39
|
+
"types": "./dist/index.d.cts",
|
|
40
|
+
"default": "./dist/index.cjs"
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
43
|
},
|
|
35
44
|
"files": [
|
|
@@ -41,10 +50,11 @@
|
|
|
41
50
|
"access": "public"
|
|
42
51
|
},
|
|
43
52
|
"dependencies": {
|
|
44
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.3.0"
|
|
45
54
|
},
|
|
46
55
|
"scripts": {
|
|
47
56
|
"build": "tsdown",
|
|
57
|
+
"test": "pnpm --dir ../.. exec vitest run --project vanilla",
|
|
48
58
|
"typecheck": "tsc --noEmit"
|
|
49
59
|
}
|
|
50
60
|
}
|