@ai-gui/vanilla 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +76 -32
- package/dist/index.d.cts +6 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +76 -32
- package/package.json +15 -5
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,53 @@ 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];
|
|
67
|
-
if (r) {
|
|
72
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
73
|
+
if (r) try {
|
|
68
74
|
const out = r(node);
|
|
69
75
|
if (typeof out?.then === "function") {
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
out.then((res) =>
|
|
73
|
-
|
|
76
|
+
const host = document.createElement("div");
|
|
77
|
+
host.setAttribute("data-aigui-async-pending", "");
|
|
78
|
+
out.then((res) => {
|
|
79
|
+
if (host.__aiguiDisposed) return;
|
|
80
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
81
|
+
host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
|
|
82
|
+
}, () => {
|
|
83
|
+
if (host.__aiguiDisposed) return;
|
|
84
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
85
|
+
host.setAttribute("data-aigui-async-error", "");
|
|
86
|
+
});
|
|
87
|
+
return host;
|
|
74
88
|
}
|
|
75
|
-
return renderOutputToElement(out);
|
|
89
|
+
return renderOutputToElement(out, ctx.sanitize);
|
|
90
|
+
} catch {
|
|
91
|
+
return renderFallback(node, ctx);
|
|
76
92
|
}
|
|
77
93
|
switch (node.type) {
|
|
78
94
|
case "heading": {
|
|
79
95
|
const el = document.createElement(node.tag ?? "h1");
|
|
80
|
-
el.innerHTML = node.html ?? "";
|
|
96
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
81
97
|
return el;
|
|
82
98
|
}
|
|
83
99
|
case "paragraph": {
|
|
84
100
|
const el = document.createElement("p");
|
|
85
|
-
el.innerHTML = node.html ?? "";
|
|
101
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
86
102
|
return el;
|
|
87
103
|
}
|
|
88
104
|
case "code": {
|
|
@@ -96,17 +112,22 @@ function renderNodeToElement(node, ctx) {
|
|
|
96
112
|
case "hr": return document.createElement("hr");
|
|
97
113
|
case "html": {
|
|
98
114
|
const el = document.createElement("div");
|
|
99
|
-
el.innerHTML = node.content ?? "";
|
|
115
|
+
el.innerHTML = renderHtml(node.content ?? "", ctx);
|
|
100
116
|
return el;
|
|
101
117
|
}
|
|
102
118
|
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
|
-
}
|
|
119
|
+
default: return renderFallback(node, ctx);
|
|
108
120
|
}
|
|
109
121
|
}
|
|
122
|
+
function renderFallback(node, ctx) {
|
|
123
|
+
const el = document.createElement("div");
|
|
124
|
+
el.innerHTML = renderHtml(node.html ?? node.content ?? "", ctx);
|
|
125
|
+
return el;
|
|
126
|
+
}
|
|
127
|
+
function renderHtml(html, ctx) {
|
|
128
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
129
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
130
|
+
}
|
|
110
131
|
function renderCardElement(node, ctx) {
|
|
111
132
|
const card = node.card;
|
|
112
133
|
if (!card) return document.createElement("div");
|
|
@@ -149,7 +170,15 @@ function createReconcileState() {
|
|
|
149
170
|
}
|
|
150
171
|
/** Run a mounted widget's cleanup (if any) before the element leaves the DOM. */
|
|
151
172
|
function disposeEl(el) {
|
|
152
|
-
el.
|
|
173
|
+
for (const child of el.querySelectorAll("*")) disposeManagedEl(child);
|
|
174
|
+
disposeManagedEl(el);
|
|
175
|
+
}
|
|
176
|
+
function disposeManagedEl(el) {
|
|
177
|
+
const managed = el;
|
|
178
|
+
if (managed.__aiguiDisposed) return;
|
|
179
|
+
managed.__aiguiDisposed = true;
|
|
180
|
+
managed.__aiguiCleanup?.();
|
|
181
|
+
managed.__aiguiCleanup = void 0;
|
|
153
182
|
}
|
|
154
183
|
function reconcile(container, nodes, ctx, state) {
|
|
155
184
|
const nextKeys = new Set(nodes.map((n) => n.key));
|
|
@@ -184,6 +213,7 @@ function reconcile(container, nodes, ctx, state) {
|
|
|
184
213
|
}
|
|
185
214
|
/** Mutate an existing element to reflect the node without replacing it. Returns false when a fresh element is required. */
|
|
186
215
|
function updateElementInPlace(el, node, ctx) {
|
|
216
|
+
if (ctx.nodeRenderers?.[node.type]) return false;
|
|
187
217
|
switch (node.type) {
|
|
188
218
|
case "heading":
|
|
189
219
|
if (el.tagName !== (node.tag ?? "h1").toUpperCase()) return false;
|
|
@@ -212,7 +242,7 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
212
242
|
case "card": return false;
|
|
213
243
|
default:
|
|
214
244
|
if (el.tagName !== "DIV") return false;
|
|
215
|
-
el.innerHTML = node.html ??
|
|
245
|
+
el.innerHTML = node.html ?? node.content ?? "";
|
|
216
246
|
return true;
|
|
217
247
|
}
|
|
218
248
|
}
|
|
@@ -224,29 +254,43 @@ function createRenderer(el, options = {}) {
|
|
|
224
254
|
const ctx = {
|
|
225
255
|
registry: options.registry,
|
|
226
256
|
onCardAction,
|
|
227
|
-
plugins: options.plugins
|
|
257
|
+
plugins: options.plugins,
|
|
258
|
+
nodeRenderers: (0, __ai_gui_core.collectNodeRenderers)(options.plugins),
|
|
259
|
+
sanitize: options.sanitize,
|
|
260
|
+
sanitized: true
|
|
228
261
|
};
|
|
229
262
|
const state = createReconcileState();
|
|
263
|
+
let destroyed = false;
|
|
230
264
|
const renderer = new __ai_gui_core.Renderer({
|
|
231
265
|
...rendererOpts,
|
|
232
|
-
onPatch: (_patches, nodes) =>
|
|
266
|
+
onPatch: (_patches, nodes) => {
|
|
267
|
+
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
268
|
+
}
|
|
233
269
|
});
|
|
234
270
|
const disposeAll = () => {
|
|
235
271
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
236
272
|
};
|
|
237
273
|
return {
|
|
238
|
-
push: (c) =>
|
|
239
|
-
|
|
274
|
+
push: (c) => {
|
|
275
|
+
if (!destroyed) renderer.push(c);
|
|
276
|
+
},
|
|
277
|
+
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
240
278
|
reset: () => {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
279
|
+
if (!destroyed) {
|
|
280
|
+
disposeAll();
|
|
281
|
+
renderer.reset();
|
|
282
|
+
state.els.clear();
|
|
283
|
+
el.replaceChildren();
|
|
284
|
+
}
|
|
245
285
|
},
|
|
246
286
|
destroy: () => {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
287
|
+
if (!destroyed) {
|
|
288
|
+
destroyed = true;
|
|
289
|
+
renderer.reset();
|
|
290
|
+
disposeAll();
|
|
291
|
+
state.els.clear();
|
|
292
|
+
el.replaceChildren();
|
|
293
|
+
}
|
|
250
294
|
}
|
|
251
295
|
};
|
|
252
296
|
}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
2
|
|
|
3
3
|
//#region src/render-node-dom.d.ts
|
|
4
4
|
interface DomRenderContext {
|
|
@@ -9,6 +9,9 @@ interface DomRenderContext {
|
|
|
9
9
|
cardType: string;
|
|
10
10
|
}) => void;
|
|
11
11
|
plugins?: AIGuiPlugin[];
|
|
12
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
13
|
+
sanitize?: RendererOptions["sanitize"];
|
|
14
|
+
sanitized?: boolean;
|
|
12
15
|
}
|
|
13
16
|
declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
|
|
14
17
|
|
|
@@ -19,7 +22,7 @@ interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
|
19
22
|
}
|
|
20
23
|
interface VanillaRenderer {
|
|
21
24
|
push: (chunk: string) => void;
|
|
22
|
-
feed: (source:
|
|
25
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
23
26
|
reset: () => void;
|
|
24
27
|
destroy: () => void;
|
|
25
28
|
}
|
|
@@ -28,7 +31,7 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
28
31
|
//#endregion
|
|
29
32
|
//#region src/render-output.d.ts
|
|
30
33
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
31
|
-
declare function renderOutputToElement(out: RenderOutput): HTMLElement;
|
|
34
|
+
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
32
35
|
|
|
33
36
|
//#endregion
|
|
34
37
|
export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
1
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
2
|
|
|
3
3
|
//#region src/render-node-dom.d.ts
|
|
4
4
|
interface DomRenderContext {
|
|
@@ -9,6 +9,9 @@ interface DomRenderContext {
|
|
|
9
9
|
cardType: string;
|
|
10
10
|
}) => void;
|
|
11
11
|
plugins?: AIGuiPlugin[];
|
|
12
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
13
|
+
sanitize?: RendererOptions["sanitize"];
|
|
14
|
+
sanitized?: boolean;
|
|
12
15
|
}
|
|
13
16
|
declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTMLElement;
|
|
14
17
|
|
|
@@ -19,7 +22,7 @@ interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
|
19
22
|
}
|
|
20
23
|
interface VanillaRenderer {
|
|
21
24
|
push: (chunk: string) => void;
|
|
22
|
-
feed: (source:
|
|
25
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
23
26
|
reset: () => void;
|
|
24
27
|
destroy: () => void;
|
|
25
28
|
}
|
|
@@ -28,7 +31,7 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
28
31
|
//#endregion
|
|
29
32
|
//#region src/render-output.d.ts
|
|
30
33
|
/** Translate a framework-neutral RenderOutput (from a plugin node renderer) into a DOM element. */
|
|
31
|
-
declare function renderOutputToElement(out: RenderOutput): HTMLElement;
|
|
34
|
+
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
32
35
|
|
|
33
36
|
//#endregion
|
|
34
37
|
export { CreateRendererOptions, DomRenderContext, 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,53 @@ 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];
|
|
43
|
-
if (r) {
|
|
48
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
49
|
+
if (r) try {
|
|
44
50
|
const out = r(node);
|
|
45
51
|
if (typeof out?.then === "function") {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
out.then((res) =>
|
|
49
|
-
|
|
52
|
+
const host = document.createElement("div");
|
|
53
|
+
host.setAttribute("data-aigui-async-pending", "");
|
|
54
|
+
out.then((res) => {
|
|
55
|
+
if (host.__aiguiDisposed) return;
|
|
56
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
57
|
+
host.replaceChildren(renderOutputToElement(res, ctx.sanitize));
|
|
58
|
+
}, () => {
|
|
59
|
+
if (host.__aiguiDisposed) return;
|
|
60
|
+
host.removeAttribute("data-aigui-async-pending");
|
|
61
|
+
host.setAttribute("data-aigui-async-error", "");
|
|
62
|
+
});
|
|
63
|
+
return host;
|
|
50
64
|
}
|
|
51
|
-
return renderOutputToElement(out);
|
|
65
|
+
return renderOutputToElement(out, ctx.sanitize);
|
|
66
|
+
} catch {
|
|
67
|
+
return renderFallback(node, ctx);
|
|
52
68
|
}
|
|
53
69
|
switch (node.type) {
|
|
54
70
|
case "heading": {
|
|
55
71
|
const el = document.createElement(node.tag ?? "h1");
|
|
56
|
-
el.innerHTML = node.html ?? "";
|
|
72
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
57
73
|
return el;
|
|
58
74
|
}
|
|
59
75
|
case "paragraph": {
|
|
60
76
|
const el = document.createElement("p");
|
|
61
|
-
el.innerHTML = node.html ?? "";
|
|
77
|
+
el.innerHTML = renderHtml(node.html ?? "", ctx);
|
|
62
78
|
return el;
|
|
63
79
|
}
|
|
64
80
|
case "code": {
|
|
@@ -72,17 +88,22 @@ function renderNodeToElement(node, ctx) {
|
|
|
72
88
|
case "hr": return document.createElement("hr");
|
|
73
89
|
case "html": {
|
|
74
90
|
const el = document.createElement("div");
|
|
75
|
-
el.innerHTML = node.content ?? "";
|
|
91
|
+
el.innerHTML = renderHtml(node.content ?? "", ctx);
|
|
76
92
|
return el;
|
|
77
93
|
}
|
|
78
94
|
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
|
-
}
|
|
95
|
+
default: return renderFallback(node, ctx);
|
|
84
96
|
}
|
|
85
97
|
}
|
|
98
|
+
function renderFallback(node, ctx) {
|
|
99
|
+
const el = document.createElement("div");
|
|
100
|
+
el.innerHTML = renderHtml(node.html ?? node.content ?? "", ctx);
|
|
101
|
+
return el;
|
|
102
|
+
}
|
|
103
|
+
function renderHtml(html, ctx) {
|
|
104
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
105
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
106
|
+
}
|
|
86
107
|
function renderCardElement(node, ctx) {
|
|
87
108
|
const card = node.card;
|
|
88
109
|
if (!card) return document.createElement("div");
|
|
@@ -125,7 +146,15 @@ function createReconcileState() {
|
|
|
125
146
|
}
|
|
126
147
|
/** Run a mounted widget's cleanup (if any) before the element leaves the DOM. */
|
|
127
148
|
function disposeEl(el) {
|
|
128
|
-
el.
|
|
149
|
+
for (const child of el.querySelectorAll("*")) disposeManagedEl(child);
|
|
150
|
+
disposeManagedEl(el);
|
|
151
|
+
}
|
|
152
|
+
function disposeManagedEl(el) {
|
|
153
|
+
const managed = el;
|
|
154
|
+
if (managed.__aiguiDisposed) return;
|
|
155
|
+
managed.__aiguiDisposed = true;
|
|
156
|
+
managed.__aiguiCleanup?.();
|
|
157
|
+
managed.__aiguiCleanup = void 0;
|
|
129
158
|
}
|
|
130
159
|
function reconcile(container, nodes, ctx, state) {
|
|
131
160
|
const nextKeys = new Set(nodes.map((n) => n.key));
|
|
@@ -160,6 +189,7 @@ function reconcile(container, nodes, ctx, state) {
|
|
|
160
189
|
}
|
|
161
190
|
/** Mutate an existing element to reflect the node without replacing it. Returns false when a fresh element is required. */
|
|
162
191
|
function updateElementInPlace(el, node, ctx) {
|
|
192
|
+
if (ctx.nodeRenderers?.[node.type]) return false;
|
|
163
193
|
switch (node.type) {
|
|
164
194
|
case "heading":
|
|
165
195
|
if (el.tagName !== (node.tag ?? "h1").toUpperCase()) return false;
|
|
@@ -188,7 +218,7 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
188
218
|
case "card": return false;
|
|
189
219
|
default:
|
|
190
220
|
if (el.tagName !== "DIV") return false;
|
|
191
|
-
el.innerHTML = node.html ??
|
|
221
|
+
el.innerHTML = node.html ?? node.content ?? "";
|
|
192
222
|
return true;
|
|
193
223
|
}
|
|
194
224
|
}
|
|
@@ -200,29 +230,43 @@ function createRenderer(el, options = {}) {
|
|
|
200
230
|
const ctx = {
|
|
201
231
|
registry: options.registry,
|
|
202
232
|
onCardAction,
|
|
203
|
-
plugins: options.plugins
|
|
233
|
+
plugins: options.plugins,
|
|
234
|
+
nodeRenderers: collectNodeRenderers(options.plugins),
|
|
235
|
+
sanitize: options.sanitize,
|
|
236
|
+
sanitized: true
|
|
204
237
|
};
|
|
205
238
|
const state = createReconcileState();
|
|
239
|
+
let destroyed = false;
|
|
206
240
|
const renderer = new Renderer({
|
|
207
241
|
...rendererOpts,
|
|
208
|
-
onPatch: (_patches, nodes) =>
|
|
242
|
+
onPatch: (_patches, nodes) => {
|
|
243
|
+
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
244
|
+
}
|
|
209
245
|
});
|
|
210
246
|
const disposeAll = () => {
|
|
211
247
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
212
248
|
};
|
|
213
249
|
return {
|
|
214
|
-
push: (c) =>
|
|
215
|
-
|
|
250
|
+
push: (c) => {
|
|
251
|
+
if (!destroyed) renderer.push(c);
|
|
252
|
+
},
|
|
253
|
+
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
216
254
|
reset: () => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
255
|
+
if (!destroyed) {
|
|
256
|
+
disposeAll();
|
|
257
|
+
renderer.reset();
|
|
258
|
+
state.els.clear();
|
|
259
|
+
el.replaceChildren();
|
|
260
|
+
}
|
|
221
261
|
},
|
|
222
262
|
destroy: () => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
if (!destroyed) {
|
|
264
|
+
destroyed = true;
|
|
265
|
+
renderer.reset();
|
|
266
|
+
disposeAll();
|
|
267
|
+
state.els.clear();
|
|
268
|
+
el.replaceChildren();
|
|
269
|
+
}
|
|
226
270
|
}
|
|
227
271
|
};
|
|
228
272
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/vanilla",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.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
|
}
|