@ai-gui/vanilla 0.2.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 +204 -39
- package/dist/index.d.cts +22 -2
- package/dist/index.d.ts +22 -2
- package/dist/index.js +204 -39
- package/package.json +2 -2
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
|
@@ -70,25 +70,33 @@ function sanitizeOutput(html, sanitize) {
|
|
|
70
70
|
//#region src/render-node-dom.ts
|
|
71
71
|
function renderNodeToElement(node, ctx) {
|
|
72
72
|
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
73
|
-
if (r)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
host.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
if (r) {
|
|
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);
|
|
88
99
|
}
|
|
89
|
-
return renderOutputToElement(out, ctx.sanitize);
|
|
90
|
-
} catch {
|
|
91
|
-
return renderFallback(node, ctx);
|
|
92
100
|
}
|
|
93
101
|
switch (node.type) {
|
|
94
102
|
case "heading": {
|
|
@@ -146,22 +154,153 @@ function renderCardElement(node, ctx) {
|
|
|
146
154
|
return pre;
|
|
147
155
|
}
|
|
148
156
|
const factory = ctx.registry?.getRender(card.type);
|
|
149
|
-
if (!factory)
|
|
150
|
-
const pre = document.createElement("pre");
|
|
151
|
-
pre.setAttribute("data-aigui-card-fallback", "");
|
|
152
|
-
const c = document.createElement("code");
|
|
153
|
-
c.textContent = JSON.stringify(card.data, null, 2);
|
|
154
|
-
pre.appendChild(c);
|
|
155
|
-
return pre;
|
|
156
|
-
}
|
|
157
|
+
if (!factory) return createCardFallback(card, ctx);
|
|
157
158
|
const host = document.createElement("div");
|
|
158
159
|
host.setAttribute("data-aigui-card", card.type);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}) }));
|
|
160
|
+
const controller = createCardController(host, card, factory, ctx);
|
|
161
|
+
host.__aiguiCardController = controller;
|
|
162
|
+
host.__aiguiCleanup = () => controller.destroy();
|
|
163
163
|
return host;
|
|
164
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
|
+
}
|
|
165
304
|
|
|
166
305
|
//#endregion
|
|
167
306
|
//#region src/reconcile.ts
|
|
@@ -239,7 +378,7 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
239
378
|
if (el.tagName !== "DIV") return false;
|
|
240
379
|
el.innerHTML = node.content ?? "";
|
|
241
380
|
return true;
|
|
242
|
-
case "card": return
|
|
381
|
+
case "card": return updateCardElement(el, node);
|
|
243
382
|
default:
|
|
244
383
|
if (el.tagName !== "DIV") return false;
|
|
245
384
|
el.innerHTML = node.html ?? node.content ?? "";
|
|
@@ -250,14 +389,20 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
250
389
|
//#endregion
|
|
251
390
|
//#region src/create-renderer.ts
|
|
252
391
|
function createRenderer(el, options = {}) {
|
|
253
|
-
const { onCardAction,...rendererOpts } = options;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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);
|
|
261
406
|
};
|
|
262
407
|
const state = createReconcileState();
|
|
263
408
|
let destroyed = false;
|
|
@@ -267,16 +412,35 @@ function createRenderer(el, options = {}) {
|
|
|
267
412
|
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
268
413
|
}
|
|
269
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
|
+
};
|
|
270
424
|
const disposeAll = () => {
|
|
271
425
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
272
426
|
};
|
|
427
|
+
const resetActionScope = () => {
|
|
428
|
+
actionScope.controller.abort();
|
|
429
|
+
actionScope = {
|
|
430
|
+
owner: {},
|
|
431
|
+
controller: new AbortController()
|
|
432
|
+
};
|
|
433
|
+
};
|
|
273
434
|
return {
|
|
435
|
+
debugSource: "renderer",
|
|
436
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
274
437
|
push: (c) => {
|
|
275
438
|
if (!destroyed) renderer.push(c);
|
|
276
439
|
},
|
|
277
440
|
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
278
441
|
reset: () => {
|
|
279
442
|
if (!destroyed) {
|
|
443
|
+
resetActionScope();
|
|
280
444
|
disposeAll();
|
|
281
445
|
renderer.reset();
|
|
282
446
|
state.els.clear();
|
|
@@ -286,6 +450,7 @@ function createRenderer(el, options = {}) {
|
|
|
286
450
|
destroy: () => {
|
|
287
451
|
if (!destroyed) {
|
|
288
452
|
destroyed = true;
|
|
453
|
+
actionScope.controller.abort();
|
|
289
454
|
renderer.reset();
|
|
290
455
|
disposeAll();
|
|
291
456
|
state.els.clear();
|
package/dist/index.d.cts
CHANGED
|
@@ -1,12 +1,28 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, 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[];
|
|
12
28
|
nodeRenderers?: Record<string, NodeRenderer>;
|
|
@@ -18,9 +34,13 @@ declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTML
|
|
|
18
34
|
//#endregion
|
|
19
35
|
//#region src/create-renderer.d.ts
|
|
20
36
|
interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
37
|
+
actionRuntime?: ActionRuntime;
|
|
38
|
+
cardStore?: CardStore;
|
|
21
39
|
onCardAction?: DomRenderContext["onCardAction"];
|
|
22
40
|
}
|
|
23
41
|
interface VanillaRenderer {
|
|
42
|
+
readonly debugSource: "renderer";
|
|
43
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
24
44
|
push: (chunk: string) => void;
|
|
25
45
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
26
46
|
reset: () => void;
|
|
@@ -34,4 +54,4 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
34
54
|
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
35
55
|
|
|
36
56
|
//#endregion
|
|
37
|
-
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,12 +1,28 @@
|
|
|
1
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, 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[];
|
|
12
28
|
nodeRenderers?: Record<string, NodeRenderer>;
|
|
@@ -18,9 +34,13 @@ declare function renderNodeToElement(node: ASTNode, ctx: DomRenderContext): HTML
|
|
|
18
34
|
//#endregion
|
|
19
35
|
//#region src/create-renderer.d.ts
|
|
20
36
|
interface CreateRendererOptions extends Omit<RendererOptions, "onPatch"> {
|
|
37
|
+
actionRuntime?: ActionRuntime;
|
|
38
|
+
cardStore?: CardStore;
|
|
21
39
|
onCardAction?: DomRenderContext["onCardAction"];
|
|
22
40
|
}
|
|
23
41
|
interface VanillaRenderer {
|
|
42
|
+
readonly debugSource: "renderer";
|
|
43
|
+
subscribeDebug: (listener: DebugEventListener) => () => void;
|
|
24
44
|
push: (chunk: string) => void;
|
|
25
45
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
26
46
|
reset: () => void;
|
|
@@ -34,4 +54,4 @@ declare function createRenderer(el: HTMLElement, options?: CreateRendererOptions
|
|
|
34
54
|
declare function renderOutputToElement(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): HTMLElement;
|
|
35
55
|
|
|
36
56
|
//#endregion
|
|
37
|
-
export { CreateRendererOptions, DomRenderContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
|
57
|
+
export { CreateRendererOptions, DomRenderContext, VanillaCardAction, VanillaCardFactory, VanillaCardInstance, VanillaCardUpdateContext, VanillaRenderer, createRenderer, renderNodeToElement, renderOutputToElement };
|
package/dist/index.js
CHANGED
|
@@ -46,25 +46,33 @@ function sanitizeOutput(html, sanitize) {
|
|
|
46
46
|
//#region src/render-node-dom.ts
|
|
47
47
|
function renderNodeToElement(node, ctx) {
|
|
48
48
|
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
49
|
-
if (r)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
host.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
49
|
+
if (r) {
|
|
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);
|
|
64
75
|
}
|
|
65
|
-
return renderOutputToElement(out, ctx.sanitize);
|
|
66
|
-
} catch {
|
|
67
|
-
return renderFallback(node, ctx);
|
|
68
76
|
}
|
|
69
77
|
switch (node.type) {
|
|
70
78
|
case "heading": {
|
|
@@ -122,22 +130,153 @@ function renderCardElement(node, ctx) {
|
|
|
122
130
|
return pre;
|
|
123
131
|
}
|
|
124
132
|
const factory = ctx.registry?.getRender(card.type);
|
|
125
|
-
if (!factory)
|
|
126
|
-
const pre = document.createElement("pre");
|
|
127
|
-
pre.setAttribute("data-aigui-card-fallback", "");
|
|
128
|
-
const c = document.createElement("code");
|
|
129
|
-
c.textContent = JSON.stringify(card.data, null, 2);
|
|
130
|
-
pre.appendChild(c);
|
|
131
|
-
return pre;
|
|
132
|
-
}
|
|
133
|
+
if (!factory) return createCardFallback(card, ctx);
|
|
133
134
|
const host = document.createElement("div");
|
|
134
135
|
host.setAttribute("data-aigui-card", card.type);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}) }));
|
|
136
|
+
const controller = createCardController(host, card, factory, ctx);
|
|
137
|
+
host.__aiguiCardController = controller;
|
|
138
|
+
host.__aiguiCleanup = () => controller.destroy();
|
|
139
139
|
return host;
|
|
140
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
|
+
}
|
|
141
280
|
|
|
142
281
|
//#endregion
|
|
143
282
|
//#region src/reconcile.ts
|
|
@@ -215,7 +354,7 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
215
354
|
if (el.tagName !== "DIV") return false;
|
|
216
355
|
el.innerHTML = node.content ?? "";
|
|
217
356
|
return true;
|
|
218
|
-
case "card": return
|
|
357
|
+
case "card": return updateCardElement(el, node);
|
|
219
358
|
default:
|
|
220
359
|
if (el.tagName !== "DIV") return false;
|
|
221
360
|
el.innerHTML = node.html ?? node.content ?? "";
|
|
@@ -226,14 +365,20 @@ function updateElementInPlace(el, node, ctx) {
|
|
|
226
365
|
//#endregion
|
|
227
366
|
//#region src/create-renderer.ts
|
|
228
367
|
function createRenderer(el, options = {}) {
|
|
229
|
-
const { onCardAction,...rendererOpts } = options;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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);
|
|
237
382
|
};
|
|
238
383
|
const state = createReconcileState();
|
|
239
384
|
let destroyed = false;
|
|
@@ -243,16 +388,35 @@ function createRenderer(el, options = {}) {
|
|
|
243
388
|
if (!destroyed) reconcile(el, nodes, ctx, state);
|
|
244
389
|
}
|
|
245
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
|
+
};
|
|
246
400
|
const disposeAll = () => {
|
|
247
401
|
for (const entry of state.els.values()) disposeEl(entry.el);
|
|
248
402
|
};
|
|
403
|
+
const resetActionScope = () => {
|
|
404
|
+
actionScope.controller.abort();
|
|
405
|
+
actionScope = {
|
|
406
|
+
owner: {},
|
|
407
|
+
controller: new AbortController()
|
|
408
|
+
};
|
|
409
|
+
};
|
|
249
410
|
return {
|
|
411
|
+
debugSource: "renderer",
|
|
412
|
+
subscribeDebug: (listener) => renderer.subscribeDebug(listener),
|
|
250
413
|
push: (c) => {
|
|
251
414
|
if (!destroyed) renderer.push(c);
|
|
252
415
|
},
|
|
253
416
|
feed: (source, feedOptions) => destroyed ? Promise.resolve() : renderer.feed(source, feedOptions),
|
|
254
417
|
reset: () => {
|
|
255
418
|
if (!destroyed) {
|
|
419
|
+
resetActionScope();
|
|
256
420
|
disposeAll();
|
|
257
421
|
renderer.reset();
|
|
258
422
|
state.els.clear();
|
|
@@ -262,6 +426,7 @@ function createRenderer(el, options = {}) {
|
|
|
262
426
|
destroy: () => {
|
|
263
427
|
if (!destroyed) {
|
|
264
428
|
destroyed = true;
|
|
429
|
+
actionScope.controller.abort();
|
|
265
430
|
renderer.reset();
|
|
266
431
|
disposeAll();
|
|
267
432
|
state.els.clear();
|
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",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@ai-gui/core": "0.
|
|
53
|
+
"@ai-gui/core": "0.3.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"build": "tsdown",
|