@ai-gui/vue 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 +12 -4
- package/dist/index.cjs +282 -34
- package/dist/index.d.cts +15 -4
- package/dist/index.d.ts +15 -4
- package/dist/index.js +282 -35
- package/package.json +15 -5
package/README.md
CHANGED
|
@@ -13,13 +13,18 @@ pnpm add @ai-gui/core @ai-gui/vue
|
|
|
13
13
|
```vue
|
|
14
14
|
<script setup lang="ts">
|
|
15
15
|
import { ref } from "vue"
|
|
16
|
-
import { CardRegistry } from "@ai-gui/core"
|
|
17
|
-
import { AIRenderer } from "@ai-gui/vue"
|
|
16
|
+
import { ActionRegistry, CardRegistry, CardStore, createActionRuntime } from "@ai-gui/core"
|
|
17
|
+
import { AIRenderer, useActionState } from "@ai-gui/vue"
|
|
18
18
|
|
|
19
19
|
const registry = new CardRegistry()
|
|
20
20
|
// A card render component receives props `data` and emits `action`.
|
|
21
21
|
registry.register({ type: "weather", description: "Weather summary", render: WeatherCard })
|
|
22
22
|
|
|
23
|
+
const actions = new ActionRegistry()
|
|
24
|
+
actions.register({ type: "refresh", run: async (params, { signal }) => fetch("/api/weather", { signal }) })
|
|
25
|
+
const cardStore = new CardStore({ registry })
|
|
26
|
+
const actionRuntime = createActionRuntime({ registry: actions, cardStore })
|
|
27
|
+
|
|
23
28
|
const r = ref<InstanceType<typeof AIRenderer>>()
|
|
24
29
|
|
|
25
30
|
async function ask() {
|
|
@@ -30,13 +35,16 @@ async function ask() {
|
|
|
30
35
|
</script>
|
|
31
36
|
|
|
32
37
|
<template>
|
|
33
|
-
<AIRenderer ref="r" :registry="registry" @card-action="(a) =>
|
|
38
|
+
<AIRenderer ref="r" :registry="registry" :card-store="cardStore" :action-runtime="actionRuntime" @card-action="(a) => console.log(a)" />
|
|
34
39
|
</template>
|
|
35
40
|
```
|
|
36
41
|
|
|
37
42
|
## Exports
|
|
38
43
|
|
|
39
|
-
- `<AIRenderer :registry :plugins :sanitize @card-action />` — a render-function component; imperative `push` / `feed` / `reset` via a template ref (exposed).
|
|
44
|
+
- `<AIRenderer :registry :card-store :plugins :sanitize :action-runtime @card-action />` — a render-function component; imperative `push` / `feed` / `reset` via a template ref (exposed).
|
|
40
45
|
- `useAIRenderer()` — composable returning `{ nodes, push, feed, reset }`.
|
|
46
|
+
- `useActionState(runtime, key)` — reactive action lifecycle state.
|
|
47
|
+
|
|
48
|
+
Cards with a top-level `id` subscribe to the supplied `CardStore`. Their Vue component receives `data` and `state` props and emits `action`; store updates preserve the component instance and local refs.
|
|
41
49
|
|
|
42
50
|
Card component contract: props `data`, `emits: ['action']`. See the [root README](../../README.md) for cards, plugins, and `buildSystemPrompt`.
|
package/dist/index.cjs
CHANGED
|
@@ -28,24 +28,46 @@ const __ai_gui_core = __toESM(require("@ai-gui/core"));
|
|
|
28
28
|
//#region src/use-ai-renderer.ts
|
|
29
29
|
function useAIRenderer(options = {}) {
|
|
30
30
|
const nodes = (0, vue.shallowRef)([]);
|
|
31
|
+
let active = true;
|
|
31
32
|
const renderer = new __ai_gui_core.Renderer({
|
|
32
33
|
...options,
|
|
33
34
|
plugins: options.plugins,
|
|
34
35
|
onPatch: (_patches, snapshot) => {
|
|
35
|
-
nodes.value = snapshot;
|
|
36
|
+
if (active) nodes.value = snapshot;
|
|
36
37
|
}
|
|
37
38
|
});
|
|
38
39
|
return {
|
|
40
|
+
renderer,
|
|
39
41
|
nodes,
|
|
40
|
-
push: (c) =>
|
|
41
|
-
|
|
42
|
+
push: (c) => {
|
|
43
|
+
if (active) renderer.push(c);
|
|
44
|
+
},
|
|
45
|
+
feed: (source, feedOptions) => active ? renderer.feed(source, feedOptions) : Promise.resolve(),
|
|
42
46
|
reset: () => {
|
|
47
|
+
if (active) {
|
|
48
|
+
renderer.reset();
|
|
49
|
+
nodes.value = [];
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
destroy: () => {
|
|
53
|
+
active = false;
|
|
43
54
|
renderer.reset();
|
|
44
55
|
nodes.value = [];
|
|
45
56
|
}
|
|
46
57
|
};
|
|
47
58
|
}
|
|
48
59
|
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/use-action-state.ts
|
|
62
|
+
function useActionState(runtime, key) {
|
|
63
|
+
const state = (0, vue.shallowRef)(runtime.getState(key));
|
|
64
|
+
const unsubscribe = runtime.subscribe((next) => {
|
|
65
|
+
if (next.key === key) state.value = next;
|
|
66
|
+
});
|
|
67
|
+
(0, vue.onScopeDispose)(unsubscribe);
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
|
|
49
71
|
//#endregion
|
|
50
72
|
//#region src/render-output.ts
|
|
51
73
|
/** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
|
|
@@ -61,6 +83,10 @@ const MountHost = (0, vue.defineComponent)({
|
|
|
61
83
|
(0, vue.onMounted)(() => {
|
|
62
84
|
if (elRef.value) cleanup = props.mount(elRef.value);
|
|
63
85
|
});
|
|
86
|
+
(0, vue.watch)(() => props.mount, (mount) => {
|
|
87
|
+
if (typeof cleanup === "function") cleanup();
|
|
88
|
+
cleanup = elRef.value ? mount(elRef.value) : void 0;
|
|
89
|
+
});
|
|
64
90
|
(0, vue.onBeforeUnmount)(() => {
|
|
65
91
|
if (typeof cleanup === "function") cleanup();
|
|
66
92
|
});
|
|
@@ -71,10 +97,10 @@ const MountHost = (0, vue.defineComponent)({
|
|
|
71
97
|
}
|
|
72
98
|
});
|
|
73
99
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
74
|
-
function renderOutput(out) {
|
|
100
|
+
function renderOutput(out, sanitize) {
|
|
75
101
|
switch (out.kind) {
|
|
76
|
-
case "html": return (0, vue.h)("div", { innerHTML: (
|
|
77
|
-
case "element": return (0, vue.h)(out.tag, out.props, (out.children ?? []).map(renderOutput));
|
|
102
|
+
case "html": return (0, vue.h)("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
|
|
103
|
+
case "element": return (0, vue.h)(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
|
|
78
104
|
case "mount": return (0, vue.h)(MountHost, { mount: out.mount });
|
|
79
105
|
case "card": return (0, vue.h)("pre", { "data-aigui-card-fallback": "" }, [(0, vue.h)("code", JSON.stringify(out.data, null, 2))]);
|
|
80
106
|
}
|
|
@@ -82,39 +108,84 @@ function renderOutput(out) {
|
|
|
82
108
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
83
109
|
const AsyncOutput = (0, vue.defineComponent)({
|
|
84
110
|
name: "AsyncOutput",
|
|
85
|
-
props: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
111
|
+
props: {
|
|
112
|
+
promise: {
|
|
113
|
+
type: Object,
|
|
114
|
+
required: true
|
|
115
|
+
},
|
|
116
|
+
sanitize: {
|
|
117
|
+
type: [Boolean, Object],
|
|
118
|
+
default: void 0
|
|
119
|
+
}
|
|
120
|
+
},
|
|
89
121
|
setup(props) {
|
|
90
122
|
const resolved = (0, vue.ref)(null);
|
|
91
|
-
|
|
92
|
-
|
|
123
|
+
const failed = (0, vue.ref)(false);
|
|
124
|
+
let generation = 0;
|
|
125
|
+
let active = true;
|
|
126
|
+
const awaitPromise = (promise) => {
|
|
127
|
+
const current = ++generation;
|
|
128
|
+
resolved.value = null;
|
|
129
|
+
failed.value = false;
|
|
130
|
+
promise.then((value) => {
|
|
131
|
+
if (active && current === generation) resolved.value = value;
|
|
132
|
+
}, () => {
|
|
133
|
+
if (active && current === generation) failed.value = true;
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
(0, vue.watch)(() => [(0, vue.toRaw)(props.promise)], ([promise]) => awaitPromise(promise), { immediate: true });
|
|
137
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
138
|
+
active = false;
|
|
139
|
+
generation++;
|
|
93
140
|
});
|
|
94
|
-
return () =>
|
|
141
|
+
return () => {
|
|
142
|
+
if (failed.value) return (0, vue.h)("span", { "data-aigui-async-error": "" });
|
|
143
|
+
if (!resolved.value) return (0, vue.h)("span", { "data-aigui-async-pending": "" });
|
|
144
|
+
try {
|
|
145
|
+
return renderOutput(resolved.value, props.sanitize);
|
|
146
|
+
} catch {
|
|
147
|
+
return (0, vue.h)("span", { "data-aigui-async-error": "" });
|
|
148
|
+
}
|
|
149
|
+
};
|
|
95
150
|
}
|
|
96
151
|
});
|
|
152
|
+
function sanitizeOutput(html, sanitize) {
|
|
153
|
+
if (sanitize === false) return html;
|
|
154
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
155
|
+
}
|
|
97
156
|
|
|
98
157
|
//#endregion
|
|
99
158
|
//#region src/render-node.ts
|
|
100
159
|
function renderNode(node, ctx) {
|
|
101
|
-
const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
|
|
160
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
102
161
|
if (r) {
|
|
103
|
-
|
|
104
|
-
if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
|
|
162
|
+
if (node.complete === false) return (0, vue.h)("div", {
|
|
105
163
|
key: node.key,
|
|
106
|
-
|
|
164
|
+
"data-aigui-block-loading": "",
|
|
165
|
+
"data-block-type": node.type
|
|
107
166
|
});
|
|
108
|
-
|
|
167
|
+
try {
|
|
168
|
+
const out = r(node);
|
|
169
|
+
if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
|
|
170
|
+
key: node.key,
|
|
171
|
+
promise: out,
|
|
172
|
+
sanitize: ctx.sanitize
|
|
173
|
+
});
|
|
174
|
+
const vnode = renderOutput(out, ctx.sanitize);
|
|
175
|
+
vnode.key = node.key;
|
|
176
|
+
return vnode;
|
|
177
|
+
} catch {
|
|
178
|
+
return renderFallback(node, ctx);
|
|
179
|
+
}
|
|
109
180
|
}
|
|
110
181
|
switch (node.type) {
|
|
111
182
|
case "heading": return (0, vue.h)(node.tag ?? "h1", {
|
|
112
183
|
key: node.key,
|
|
113
|
-
innerHTML: node.html ?? ""
|
|
184
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
114
185
|
});
|
|
115
186
|
case "paragraph": return (0, vue.h)("p", {
|
|
116
187
|
key: node.key,
|
|
117
|
-
innerHTML: node.html ?? ""
|
|
188
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
118
189
|
});
|
|
119
190
|
case "code": return (0, vue.h)("pre", {
|
|
120
191
|
key: node.key,
|
|
@@ -123,13 +194,10 @@ function renderNode(node, ctx) {
|
|
|
123
194
|
case "hr": return (0, vue.h)("hr", { key: node.key });
|
|
124
195
|
case "html": return (0, vue.h)("div", {
|
|
125
196
|
key: node.key,
|
|
126
|
-
innerHTML: node.content ?? ""
|
|
197
|
+
innerHTML: renderHtml(node.content ?? "", ctx)
|
|
127
198
|
});
|
|
128
199
|
case "card": return renderCard(node, ctx);
|
|
129
|
-
default: return (
|
|
130
|
-
key: node.key,
|
|
131
|
-
innerHTML: node.html ?? (0, __ai_gui_core.sanitizeHtml)(node.content ?? "")
|
|
132
|
-
});
|
|
200
|
+
default: return renderFallback(node, ctx);
|
|
133
201
|
}
|
|
134
202
|
}
|
|
135
203
|
function renderCard(node, ctx) {
|
|
@@ -145,11 +213,24 @@ function renderCard(node, ctx) {
|
|
|
145
213
|
"data-aigui-card-invalid": ""
|
|
146
214
|
}, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
|
|
147
215
|
const Comp = ctx.registry?.getRender(card.type);
|
|
216
|
+
if (card.id && ctx.cardStore) return (0, vue.h)(CardHost, {
|
|
217
|
+
key: node.key,
|
|
218
|
+
component: Comp ? (0, vue.markRaw)((0, vue.toRaw)(Comp)) : void 0,
|
|
219
|
+
store: ctx.cardStore,
|
|
220
|
+
cardId: card.id,
|
|
221
|
+
cardType: card.type,
|
|
222
|
+
initialData: card.data,
|
|
223
|
+
onAction: (a) => ctx.onCardAction?.({
|
|
224
|
+
...a,
|
|
225
|
+
cardType: card.type,
|
|
226
|
+
cardId: card.id
|
|
227
|
+
})
|
|
228
|
+
});
|
|
148
229
|
if (!Comp) return (0, vue.h)("pre", {
|
|
149
230
|
key: node.key,
|
|
150
231
|
"data-aigui-card-fallback": ""
|
|
151
232
|
}, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
|
|
152
|
-
return (0, vue.h)(Comp, {
|
|
233
|
+
return (0, vue.h)((0, vue.markRaw)((0, vue.toRaw)(Comp)), {
|
|
153
234
|
key: node.key,
|
|
154
235
|
data: card.data,
|
|
155
236
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -158,11 +239,101 @@ function renderCard(node, ctx) {
|
|
|
158
239
|
})
|
|
159
240
|
});
|
|
160
241
|
}
|
|
242
|
+
const CardHost = (0, vue.defineComponent)({
|
|
243
|
+
name: "AIGuiCardHost",
|
|
244
|
+
props: {
|
|
245
|
+
component: {
|
|
246
|
+
type: [Object, Function],
|
|
247
|
+
default: void 0
|
|
248
|
+
},
|
|
249
|
+
store: {
|
|
250
|
+
type: Object,
|
|
251
|
+
required: true
|
|
252
|
+
},
|
|
253
|
+
cardId: {
|
|
254
|
+
type: String,
|
|
255
|
+
required: true
|
|
256
|
+
},
|
|
257
|
+
cardType: {
|
|
258
|
+
type: String,
|
|
259
|
+
required: true
|
|
260
|
+
},
|
|
261
|
+
initialData: {
|
|
262
|
+
type: null,
|
|
263
|
+
required: true
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
emits: ["action"],
|
|
267
|
+
setup(props, { emit }) {
|
|
268
|
+
const record = (0, vue.shallowRef)();
|
|
269
|
+
const failed = (0, vue.shallowRef)(false);
|
|
270
|
+
let unsubscribe;
|
|
271
|
+
let stopWatch;
|
|
272
|
+
const bind = ([store, cardId, cardType, initialData]) => {
|
|
273
|
+
unsubscribe?.();
|
|
274
|
+
unsubscribe = void 0;
|
|
275
|
+
failed.value = false;
|
|
276
|
+
record.value = void 0;
|
|
277
|
+
unsubscribe = store.subscribe(cardId, (next) => {
|
|
278
|
+
if (!next || next.type !== cardType) {
|
|
279
|
+
failed.value = true;
|
|
280
|
+
record.value = void 0;
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
failed.value = false;
|
|
284
|
+
record.value = next;
|
|
285
|
+
});
|
|
286
|
+
try {
|
|
287
|
+
const current = store.register({
|
|
288
|
+
id: cardId,
|
|
289
|
+
type: cardType,
|
|
290
|
+
data: initialData
|
|
291
|
+
});
|
|
292
|
+
if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
|
|
293
|
+
record.value = current;
|
|
294
|
+
} catch {
|
|
295
|
+
failed.value = true;
|
|
296
|
+
record.value = void 0;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
(0, vue.onMounted)(() => {
|
|
300
|
+
stopWatch = (0, vue.watch)(() => [
|
|
301
|
+
props.store,
|
|
302
|
+
props.cardId,
|
|
303
|
+
props.cardType,
|
|
304
|
+
props.initialData
|
|
305
|
+
], bind, { immediate: true });
|
|
306
|
+
});
|
|
307
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
308
|
+
stopWatch?.();
|
|
309
|
+
unsubscribe?.();
|
|
310
|
+
});
|
|
311
|
+
return () => {
|
|
312
|
+
if (failed.value || !record.value || !props.component) return (0, vue.h)("pre", { "data-aigui-card-fallback": "" }, [(0, vue.h)("code", JSON.stringify(record.value?.data ?? props.initialData, null, 2))]);
|
|
313
|
+
return (0, vue.h)((0, vue.markRaw)((0, vue.toRaw)(props.component)), {
|
|
314
|
+
data: record.value.data,
|
|
315
|
+
state: record.value.action,
|
|
316
|
+
onAction: (action) => emit("action", action)
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
function renderFallback(node, ctx) {
|
|
322
|
+
return (0, vue.h)("div", {
|
|
323
|
+
key: node.key,
|
|
324
|
+
innerHTML: renderHtml(node.html ?? node.content ?? "", ctx)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
function renderHtml(html, ctx) {
|
|
328
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
329
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
330
|
+
}
|
|
161
331
|
|
|
162
332
|
//#endregion
|
|
163
333
|
//#region src/ai-renderer.ts
|
|
164
334
|
const AIRenderer = (0, vue.defineComponent)({
|
|
165
335
|
name: "AIRenderer",
|
|
336
|
+
emits: ["card-action"],
|
|
166
337
|
props: {
|
|
167
338
|
registry: {
|
|
168
339
|
type: Object,
|
|
@@ -173,32 +344,108 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
173
344
|
default: void 0
|
|
174
345
|
},
|
|
175
346
|
sanitize: {
|
|
176
|
-
type: Boolean,
|
|
347
|
+
type: [Boolean, Object],
|
|
177
348
|
default: void 0
|
|
178
349
|
},
|
|
179
|
-
|
|
350
|
+
actionRuntime: {
|
|
351
|
+
type: Object,
|
|
352
|
+
default: void 0
|
|
353
|
+
},
|
|
354
|
+
cardStore: {
|
|
355
|
+
type: Object,
|
|
356
|
+
default: void 0
|
|
357
|
+
},
|
|
358
|
+
debug: {
|
|
359
|
+
type: Boolean,
|
|
360
|
+
default: false
|
|
361
|
+
},
|
|
362
|
+
onDebugEvent: {
|
|
180
363
|
type: Function,
|
|
181
364
|
default: void 0
|
|
182
365
|
}
|
|
183
366
|
},
|
|
184
|
-
setup(props, { expose }) {
|
|
185
|
-
const
|
|
367
|
+
setup(props, { emit, expose }) {
|
|
368
|
+
const current = (0, vue.shallowRef)(useAIRenderer({
|
|
186
369
|
registry: props.registry,
|
|
187
370
|
sanitize: props.sanitize,
|
|
188
|
-
plugins: props.plugins
|
|
371
|
+
plugins: props.plugins,
|
|
372
|
+
debug: props.debug,
|
|
373
|
+
onDebugEvent: props.onDebugEvent
|
|
374
|
+
}));
|
|
375
|
+
const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer }));
|
|
376
|
+
let actionScope = {
|
|
377
|
+
controller: new AbortController(),
|
|
378
|
+
owner: {}
|
|
379
|
+
};
|
|
380
|
+
const resetActionScope = () => {
|
|
381
|
+
actionScope.controller.abort();
|
|
382
|
+
actionScope = {
|
|
383
|
+
controller: new AbortController(),
|
|
384
|
+
owner: {}
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
(0, vue.watch)(() => [
|
|
388
|
+
props.registry,
|
|
389
|
+
props.sanitize,
|
|
390
|
+
props.plugins,
|
|
391
|
+
props.debug,
|
|
392
|
+
props.onDebugEvent
|
|
393
|
+
], () => {
|
|
394
|
+
resetActionScope();
|
|
395
|
+
current.value.destroy();
|
|
396
|
+
current.value = useAIRenderer({
|
|
397
|
+
registry: props.registry,
|
|
398
|
+
sanitize: props.sanitize,
|
|
399
|
+
plugins: props.plugins,
|
|
400
|
+
debug: props.debug,
|
|
401
|
+
onDebugEvent: props.onDebugEvent
|
|
402
|
+
});
|
|
403
|
+
nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer });
|
|
189
404
|
});
|
|
405
|
+
(0, vue.watch)(() => props.actionRuntime, resetActionScope);
|
|
406
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
407
|
+
resetActionScope();
|
|
408
|
+
current.value.destroy();
|
|
409
|
+
});
|
|
410
|
+
const push = (chunk) => current.value.push(chunk);
|
|
411
|
+
const feed = (source, options) => current.value.feed(source, options);
|
|
412
|
+
const reset = () => {
|
|
413
|
+
resetActionScope();
|
|
414
|
+
current.value.reset();
|
|
415
|
+
};
|
|
190
416
|
expose({
|
|
417
|
+
debugSource: "renderer",
|
|
418
|
+
subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
|
|
191
419
|
push,
|
|
192
420
|
feed,
|
|
193
421
|
reset
|
|
194
422
|
});
|
|
195
423
|
return () => {
|
|
424
|
+
const onCardAction = (action) => {
|
|
425
|
+
if (props.actionRuntime) {
|
|
426
|
+
const request = {
|
|
427
|
+
type: action.type,
|
|
428
|
+
params: action.params,
|
|
429
|
+
cardType: action.cardType,
|
|
430
|
+
...action.cardId === void 0 ? {} : { cardId: action.cardId }
|
|
431
|
+
};
|
|
432
|
+
props.actionRuntime.dispatch(request, {
|
|
433
|
+
signal: actionScope.controller.signal,
|
|
434
|
+
owner: actionScope.owner
|
|
435
|
+
}).catch(() => {});
|
|
436
|
+
}
|
|
437
|
+
emit("card-action", action);
|
|
438
|
+
};
|
|
196
439
|
const ctx = {
|
|
197
440
|
registry: props.registry,
|
|
441
|
+
cardStore: props.cardStore,
|
|
198
442
|
plugins: props.plugins,
|
|
199
|
-
|
|
443
|
+
nodeRenderers: nodeRenderers.value,
|
|
444
|
+
onCardAction,
|
|
445
|
+
sanitize: props.sanitize,
|
|
446
|
+
sanitized: true
|
|
200
447
|
};
|
|
201
|
-
return (0, vue.h)("div", { "data-aigui-renderer": "" }, nodes.value.map((n) => renderNode(n, ctx)));
|
|
448
|
+
return (0, vue.h)("div", { "data-aigui-renderer": "" }, current.value.nodes.value.map((n) => renderNode(n, ctx)));
|
|
202
449
|
};
|
|
203
450
|
}
|
|
204
451
|
});
|
|
@@ -207,4 +454,5 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
207
454
|
exports.AIRenderer = AIRenderer
|
|
208
455
|
exports.renderNode = renderNode
|
|
209
456
|
exports.renderOutput = renderOutput
|
|
210
|
-
exports.useAIRenderer = useAIRenderer
|
|
457
|
+
exports.useAIRenderer = useAIRenderer
|
|
458
|
+
exports.useActionState = useActionState
|
package/dist/index.d.cts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardRegistry, CardStore, FeedOptions, FeedSource, NodeRenderer, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/use-ai-renderer.d.ts
|
|
5
5
|
interface UseAIRendererResult {
|
|
6
|
+
renderer: Renderer;
|
|
6
7
|
nodes: ShallowRef<ASTNode[]>;
|
|
7
8
|
push: (chunk: string) => void;
|
|
8
|
-
feed: (source:
|
|
9
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
9
10
|
reset: () => void;
|
|
11
|
+
destroy: () => void;
|
|
10
12
|
}
|
|
11
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
12
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/use-action-state.d.ts
|
|
17
|
+
declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
|
|
18
|
+
|
|
13
19
|
//#endregion
|
|
14
20
|
//#region src/ai-renderer.d.ts
|
|
15
21
|
declare const AIRenderer: any;
|
|
@@ -18,21 +24,26 @@ declare const AIRenderer: any;
|
|
|
18
24
|
//#region src/render-node.d.ts
|
|
19
25
|
interface RenderContext {
|
|
20
26
|
registry?: CardRegistry;
|
|
27
|
+
cardStore?: CardStore;
|
|
21
28
|
plugins?: AIGuiPlugin[];
|
|
29
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
22
30
|
onCardAction?: (action: {
|
|
23
31
|
type: string;
|
|
24
32
|
params?: unknown;
|
|
25
33
|
cardType: string;
|
|
34
|
+
cardId?: string;
|
|
26
35
|
}) => void;
|
|
36
|
+
sanitize?: RendererOptions["sanitize"];
|
|
37
|
+
sanitized?: boolean;
|
|
27
38
|
}
|
|
28
39
|
declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
|
|
29
40
|
|
|
30
41
|
//#endregion
|
|
31
42
|
//#region src/render-output.d.ts
|
|
32
43
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
33
|
-
declare function renderOutput(out: RenderOutput): VNode;
|
|
44
|
+
declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
|
|
34
45
|
|
|
35
46
|
//#endregion
|
|
36
47
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
37
48
|
|
|
38
|
-
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
|
|
49
|
+
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
|
+
import { AIGuiPlugin, ASTNode, ActionRuntime, ActionState, CardRegistry, CardStore, FeedOptions, FeedSource, NodeRenderer, RenderOutput, Renderer, RendererOptions } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/use-ai-renderer.d.ts
|
|
5
5
|
interface UseAIRendererResult {
|
|
6
|
+
renderer: Renderer;
|
|
6
7
|
nodes: ShallowRef<ASTNode[]>;
|
|
7
8
|
push: (chunk: string) => void;
|
|
8
|
-
feed: (source:
|
|
9
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
9
10
|
reset: () => void;
|
|
11
|
+
destroy: () => void;
|
|
10
12
|
}
|
|
11
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
12
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/use-action-state.d.ts
|
|
17
|
+
declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
|
|
18
|
+
|
|
13
19
|
//#endregion
|
|
14
20
|
//#region src/ai-renderer.d.ts
|
|
15
21
|
declare const AIRenderer: any;
|
|
@@ -18,21 +24,26 @@ declare const AIRenderer: any;
|
|
|
18
24
|
//#region src/render-node.d.ts
|
|
19
25
|
interface RenderContext {
|
|
20
26
|
registry?: CardRegistry;
|
|
27
|
+
cardStore?: CardStore;
|
|
21
28
|
plugins?: AIGuiPlugin[];
|
|
29
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
22
30
|
onCardAction?: (action: {
|
|
23
31
|
type: string;
|
|
24
32
|
params?: unknown;
|
|
25
33
|
cardType: string;
|
|
34
|
+
cardId?: string;
|
|
26
35
|
}) => void;
|
|
36
|
+
sanitize?: RendererOptions["sanitize"];
|
|
37
|
+
sanitized?: boolean;
|
|
27
38
|
}
|
|
28
39
|
declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
|
|
29
40
|
|
|
30
41
|
//#endregion
|
|
31
42
|
//#region src/render-output.d.ts
|
|
32
43
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
33
|
-
declare function renderOutput(out: RenderOutput): VNode;
|
|
44
|
+
declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
|
|
34
45
|
|
|
35
46
|
//#endregion
|
|
36
47
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
37
48
|
|
|
38
|
-
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
|
|
49
|
+
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.js
CHANGED
|
@@ -1,27 +1,49 @@
|
|
|
1
|
-
import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
|
|
1
|
+
import { defineComponent, h, markRaw, onBeforeUnmount, onMounted, onScopeDispose, ref, shallowRef, toRaw, watch } from "vue";
|
|
2
2
|
import { Renderer, collectNodeRenderers, sanitizeHtml } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/use-ai-renderer.ts
|
|
5
5
|
function useAIRenderer(options = {}) {
|
|
6
6
|
const nodes = shallowRef([]);
|
|
7
|
+
let active = true;
|
|
7
8
|
const renderer = new Renderer({
|
|
8
9
|
...options,
|
|
9
10
|
plugins: options.plugins,
|
|
10
11
|
onPatch: (_patches, snapshot) => {
|
|
11
|
-
nodes.value = snapshot;
|
|
12
|
+
if (active) nodes.value = snapshot;
|
|
12
13
|
}
|
|
13
14
|
});
|
|
14
15
|
return {
|
|
16
|
+
renderer,
|
|
15
17
|
nodes,
|
|
16
|
-
push: (c) =>
|
|
17
|
-
|
|
18
|
+
push: (c) => {
|
|
19
|
+
if (active) renderer.push(c);
|
|
20
|
+
},
|
|
21
|
+
feed: (source, feedOptions) => active ? renderer.feed(source, feedOptions) : Promise.resolve(),
|
|
18
22
|
reset: () => {
|
|
23
|
+
if (active) {
|
|
24
|
+
renderer.reset();
|
|
25
|
+
nodes.value = [];
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
destroy: () => {
|
|
29
|
+
active = false;
|
|
19
30
|
renderer.reset();
|
|
20
31
|
nodes.value = [];
|
|
21
32
|
}
|
|
22
33
|
};
|
|
23
34
|
}
|
|
24
35
|
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/use-action-state.ts
|
|
38
|
+
function useActionState(runtime, key) {
|
|
39
|
+
const state = shallowRef(runtime.getState(key));
|
|
40
|
+
const unsubscribe = runtime.subscribe((next) => {
|
|
41
|
+
if (next.key === key) state.value = next;
|
|
42
|
+
});
|
|
43
|
+
onScopeDispose(unsubscribe);
|
|
44
|
+
return state;
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
//#endregion
|
|
26
48
|
//#region src/render-output.ts
|
|
27
49
|
/** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
|
|
@@ -37,6 +59,10 @@ const MountHost = defineComponent({
|
|
|
37
59
|
onMounted(() => {
|
|
38
60
|
if (elRef.value) cleanup = props.mount(elRef.value);
|
|
39
61
|
});
|
|
62
|
+
watch(() => props.mount, (mount) => {
|
|
63
|
+
if (typeof cleanup === "function") cleanup();
|
|
64
|
+
cleanup = elRef.value ? mount(elRef.value) : void 0;
|
|
65
|
+
});
|
|
40
66
|
onBeforeUnmount(() => {
|
|
41
67
|
if (typeof cleanup === "function") cleanup();
|
|
42
68
|
});
|
|
@@ -47,10 +73,10 @@ const MountHost = defineComponent({
|
|
|
47
73
|
}
|
|
48
74
|
});
|
|
49
75
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
50
|
-
function renderOutput(out) {
|
|
76
|
+
function renderOutput(out, sanitize) {
|
|
51
77
|
switch (out.kind) {
|
|
52
|
-
case "html": return h("div", { innerHTML:
|
|
53
|
-
case "element": return h(out.tag, out.props, (out.children ?? []).map(renderOutput));
|
|
78
|
+
case "html": return h("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
|
|
79
|
+
case "element": return h(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
|
|
54
80
|
case "mount": return h(MountHost, { mount: out.mount });
|
|
55
81
|
case "card": return h("pre", { "data-aigui-card-fallback": "" }, [h("code", JSON.stringify(out.data, null, 2))]);
|
|
56
82
|
}
|
|
@@ -58,39 +84,84 @@ function renderOutput(out) {
|
|
|
58
84
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
59
85
|
const AsyncOutput = defineComponent({
|
|
60
86
|
name: "AsyncOutput",
|
|
61
|
-
props: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
props: {
|
|
88
|
+
promise: {
|
|
89
|
+
type: Object,
|
|
90
|
+
required: true
|
|
91
|
+
},
|
|
92
|
+
sanitize: {
|
|
93
|
+
type: [Boolean, Object],
|
|
94
|
+
default: void 0
|
|
95
|
+
}
|
|
96
|
+
},
|
|
65
97
|
setup(props) {
|
|
66
98
|
const resolved = ref(null);
|
|
67
|
-
|
|
68
|
-
|
|
99
|
+
const failed = ref(false);
|
|
100
|
+
let generation = 0;
|
|
101
|
+
let active = true;
|
|
102
|
+
const awaitPromise = (promise) => {
|
|
103
|
+
const current = ++generation;
|
|
104
|
+
resolved.value = null;
|
|
105
|
+
failed.value = false;
|
|
106
|
+
promise.then((value) => {
|
|
107
|
+
if (active && current === generation) resolved.value = value;
|
|
108
|
+
}, () => {
|
|
109
|
+
if (active && current === generation) failed.value = true;
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
watch(() => [toRaw(props.promise)], ([promise]) => awaitPromise(promise), { immediate: true });
|
|
113
|
+
onBeforeUnmount(() => {
|
|
114
|
+
active = false;
|
|
115
|
+
generation++;
|
|
69
116
|
});
|
|
70
|
-
return () =>
|
|
117
|
+
return () => {
|
|
118
|
+
if (failed.value) return h("span", { "data-aigui-async-error": "" });
|
|
119
|
+
if (!resolved.value) return h("span", { "data-aigui-async-pending": "" });
|
|
120
|
+
try {
|
|
121
|
+
return renderOutput(resolved.value, props.sanitize);
|
|
122
|
+
} catch {
|
|
123
|
+
return h("span", { "data-aigui-async-error": "" });
|
|
124
|
+
}
|
|
125
|
+
};
|
|
71
126
|
}
|
|
72
127
|
});
|
|
128
|
+
function sanitizeOutput(html, sanitize) {
|
|
129
|
+
if (sanitize === false) return html;
|
|
130
|
+
return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
131
|
+
}
|
|
73
132
|
|
|
74
133
|
//#endregion
|
|
75
134
|
//#region src/render-node.ts
|
|
76
135
|
function renderNode(node, ctx) {
|
|
77
|
-
const r = collectNodeRenderers(ctx.plugins)[node.type];
|
|
136
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
78
137
|
if (r) {
|
|
79
|
-
|
|
80
|
-
if (out && typeof out.then === "function") return h(AsyncOutput, {
|
|
138
|
+
if (node.complete === false) return h("div", {
|
|
81
139
|
key: node.key,
|
|
82
|
-
|
|
140
|
+
"data-aigui-block-loading": "",
|
|
141
|
+
"data-block-type": node.type
|
|
83
142
|
});
|
|
84
|
-
|
|
143
|
+
try {
|
|
144
|
+
const out = r(node);
|
|
145
|
+
if (out && typeof out.then === "function") return h(AsyncOutput, {
|
|
146
|
+
key: node.key,
|
|
147
|
+
promise: out,
|
|
148
|
+
sanitize: ctx.sanitize
|
|
149
|
+
});
|
|
150
|
+
const vnode = renderOutput(out, ctx.sanitize);
|
|
151
|
+
vnode.key = node.key;
|
|
152
|
+
return vnode;
|
|
153
|
+
} catch {
|
|
154
|
+
return renderFallback(node, ctx);
|
|
155
|
+
}
|
|
85
156
|
}
|
|
86
157
|
switch (node.type) {
|
|
87
158
|
case "heading": return h(node.tag ?? "h1", {
|
|
88
159
|
key: node.key,
|
|
89
|
-
innerHTML: node.html ?? ""
|
|
160
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
90
161
|
});
|
|
91
162
|
case "paragraph": return h("p", {
|
|
92
163
|
key: node.key,
|
|
93
|
-
innerHTML: node.html ?? ""
|
|
164
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
94
165
|
});
|
|
95
166
|
case "code": return h("pre", {
|
|
96
167
|
key: node.key,
|
|
@@ -99,13 +170,10 @@ function renderNode(node, ctx) {
|
|
|
99
170
|
case "hr": return h("hr", { key: node.key });
|
|
100
171
|
case "html": return h("div", {
|
|
101
172
|
key: node.key,
|
|
102
|
-
innerHTML: node.content ?? ""
|
|
173
|
+
innerHTML: renderHtml(node.content ?? "", ctx)
|
|
103
174
|
});
|
|
104
175
|
case "card": return renderCard(node, ctx);
|
|
105
|
-
default: return
|
|
106
|
-
key: node.key,
|
|
107
|
-
innerHTML: node.html ?? sanitizeHtml(node.content ?? "")
|
|
108
|
-
});
|
|
176
|
+
default: return renderFallback(node, ctx);
|
|
109
177
|
}
|
|
110
178
|
}
|
|
111
179
|
function renderCard(node, ctx) {
|
|
@@ -121,11 +189,24 @@ function renderCard(node, ctx) {
|
|
|
121
189
|
"data-aigui-card-invalid": ""
|
|
122
190
|
}, [h("code", JSON.stringify(card.data, null, 2))]);
|
|
123
191
|
const Comp = ctx.registry?.getRender(card.type);
|
|
192
|
+
if (card.id && ctx.cardStore) return h(CardHost, {
|
|
193
|
+
key: node.key,
|
|
194
|
+
component: Comp ? markRaw(toRaw(Comp)) : void 0,
|
|
195
|
+
store: ctx.cardStore,
|
|
196
|
+
cardId: card.id,
|
|
197
|
+
cardType: card.type,
|
|
198
|
+
initialData: card.data,
|
|
199
|
+
onAction: (a) => ctx.onCardAction?.({
|
|
200
|
+
...a,
|
|
201
|
+
cardType: card.type,
|
|
202
|
+
cardId: card.id
|
|
203
|
+
})
|
|
204
|
+
});
|
|
124
205
|
if (!Comp) return h("pre", {
|
|
125
206
|
key: node.key,
|
|
126
207
|
"data-aigui-card-fallback": ""
|
|
127
208
|
}, [h("code", JSON.stringify(card.data, null, 2))]);
|
|
128
|
-
return h(Comp, {
|
|
209
|
+
return h(markRaw(toRaw(Comp)), {
|
|
129
210
|
key: node.key,
|
|
130
211
|
data: card.data,
|
|
131
212
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -134,11 +215,101 @@ function renderCard(node, ctx) {
|
|
|
134
215
|
})
|
|
135
216
|
});
|
|
136
217
|
}
|
|
218
|
+
const CardHost = defineComponent({
|
|
219
|
+
name: "AIGuiCardHost",
|
|
220
|
+
props: {
|
|
221
|
+
component: {
|
|
222
|
+
type: [Object, Function],
|
|
223
|
+
default: void 0
|
|
224
|
+
},
|
|
225
|
+
store: {
|
|
226
|
+
type: Object,
|
|
227
|
+
required: true
|
|
228
|
+
},
|
|
229
|
+
cardId: {
|
|
230
|
+
type: String,
|
|
231
|
+
required: true
|
|
232
|
+
},
|
|
233
|
+
cardType: {
|
|
234
|
+
type: String,
|
|
235
|
+
required: true
|
|
236
|
+
},
|
|
237
|
+
initialData: {
|
|
238
|
+
type: null,
|
|
239
|
+
required: true
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
emits: ["action"],
|
|
243
|
+
setup(props, { emit }) {
|
|
244
|
+
const record = shallowRef();
|
|
245
|
+
const failed = shallowRef(false);
|
|
246
|
+
let unsubscribe;
|
|
247
|
+
let stopWatch;
|
|
248
|
+
const bind = ([store, cardId, cardType, initialData]) => {
|
|
249
|
+
unsubscribe?.();
|
|
250
|
+
unsubscribe = void 0;
|
|
251
|
+
failed.value = false;
|
|
252
|
+
record.value = void 0;
|
|
253
|
+
unsubscribe = store.subscribe(cardId, (next) => {
|
|
254
|
+
if (!next || next.type !== cardType) {
|
|
255
|
+
failed.value = true;
|
|
256
|
+
record.value = void 0;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
failed.value = false;
|
|
260
|
+
record.value = next;
|
|
261
|
+
});
|
|
262
|
+
try {
|
|
263
|
+
const current = store.register({
|
|
264
|
+
id: cardId,
|
|
265
|
+
type: cardType,
|
|
266
|
+
data: initialData
|
|
267
|
+
});
|
|
268
|
+
if (current.type !== cardType) throw new Error(`Card type mismatch for "${cardId}"`);
|
|
269
|
+
record.value = current;
|
|
270
|
+
} catch {
|
|
271
|
+
failed.value = true;
|
|
272
|
+
record.value = void 0;
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
onMounted(() => {
|
|
276
|
+
stopWatch = watch(() => [
|
|
277
|
+
props.store,
|
|
278
|
+
props.cardId,
|
|
279
|
+
props.cardType,
|
|
280
|
+
props.initialData
|
|
281
|
+
], bind, { immediate: true });
|
|
282
|
+
});
|
|
283
|
+
onBeforeUnmount(() => {
|
|
284
|
+
stopWatch?.();
|
|
285
|
+
unsubscribe?.();
|
|
286
|
+
});
|
|
287
|
+
return () => {
|
|
288
|
+
if (failed.value || !record.value || !props.component) return h("pre", { "data-aigui-card-fallback": "" }, [h("code", JSON.stringify(record.value?.data ?? props.initialData, null, 2))]);
|
|
289
|
+
return h(markRaw(toRaw(props.component)), {
|
|
290
|
+
data: record.value.data,
|
|
291
|
+
state: record.value.action,
|
|
292
|
+
onAction: (action) => emit("action", action)
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
function renderFallback(node, ctx) {
|
|
298
|
+
return h("div", {
|
|
299
|
+
key: node.key,
|
|
300
|
+
innerHTML: renderHtml(node.html ?? node.content ?? "", ctx)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
function renderHtml(html, ctx) {
|
|
304
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
305
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
306
|
+
}
|
|
137
307
|
|
|
138
308
|
//#endregion
|
|
139
309
|
//#region src/ai-renderer.ts
|
|
140
310
|
const AIRenderer = defineComponent({
|
|
141
311
|
name: "AIRenderer",
|
|
312
|
+
emits: ["card-action"],
|
|
142
313
|
props: {
|
|
143
314
|
registry: {
|
|
144
315
|
type: Object,
|
|
@@ -149,35 +320,111 @@ const AIRenderer = defineComponent({
|
|
|
149
320
|
default: void 0
|
|
150
321
|
},
|
|
151
322
|
sanitize: {
|
|
152
|
-
type: Boolean,
|
|
323
|
+
type: [Boolean, Object],
|
|
153
324
|
default: void 0
|
|
154
325
|
},
|
|
155
|
-
|
|
326
|
+
actionRuntime: {
|
|
327
|
+
type: Object,
|
|
328
|
+
default: void 0
|
|
329
|
+
},
|
|
330
|
+
cardStore: {
|
|
331
|
+
type: Object,
|
|
332
|
+
default: void 0
|
|
333
|
+
},
|
|
334
|
+
debug: {
|
|
335
|
+
type: Boolean,
|
|
336
|
+
default: false
|
|
337
|
+
},
|
|
338
|
+
onDebugEvent: {
|
|
156
339
|
type: Function,
|
|
157
340
|
default: void 0
|
|
158
341
|
}
|
|
159
342
|
},
|
|
160
|
-
setup(props, { expose }) {
|
|
161
|
-
const
|
|
343
|
+
setup(props, { emit, expose }) {
|
|
344
|
+
const current = shallowRef(useAIRenderer({
|
|
162
345
|
registry: props.registry,
|
|
163
346
|
sanitize: props.sanitize,
|
|
164
|
-
plugins: props.plugins
|
|
347
|
+
plugins: props.plugins,
|
|
348
|
+
debug: props.debug,
|
|
349
|
+
onDebugEvent: props.onDebugEvent
|
|
350
|
+
}));
|
|
351
|
+
const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer }));
|
|
352
|
+
let actionScope = {
|
|
353
|
+
controller: new AbortController(),
|
|
354
|
+
owner: {}
|
|
355
|
+
};
|
|
356
|
+
const resetActionScope = () => {
|
|
357
|
+
actionScope.controller.abort();
|
|
358
|
+
actionScope = {
|
|
359
|
+
controller: new AbortController(),
|
|
360
|
+
owner: {}
|
|
361
|
+
};
|
|
362
|
+
};
|
|
363
|
+
watch(() => [
|
|
364
|
+
props.registry,
|
|
365
|
+
props.sanitize,
|
|
366
|
+
props.plugins,
|
|
367
|
+
props.debug,
|
|
368
|
+
props.onDebugEvent
|
|
369
|
+
], () => {
|
|
370
|
+
resetActionScope();
|
|
371
|
+
current.value.destroy();
|
|
372
|
+
current.value = useAIRenderer({
|
|
373
|
+
registry: props.registry,
|
|
374
|
+
sanitize: props.sanitize,
|
|
375
|
+
plugins: props.plugins,
|
|
376
|
+
debug: props.debug,
|
|
377
|
+
onDebugEvent: props.onDebugEvent
|
|
378
|
+
});
|
|
379
|
+
nodeRenderers.value = collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer });
|
|
165
380
|
});
|
|
381
|
+
watch(() => props.actionRuntime, resetActionScope);
|
|
382
|
+
onBeforeUnmount(() => {
|
|
383
|
+
resetActionScope();
|
|
384
|
+
current.value.destroy();
|
|
385
|
+
});
|
|
386
|
+
const push = (chunk) => current.value.push(chunk);
|
|
387
|
+
const feed = (source, options) => current.value.feed(source, options);
|
|
388
|
+
const reset = () => {
|
|
389
|
+
resetActionScope();
|
|
390
|
+
current.value.reset();
|
|
391
|
+
};
|
|
166
392
|
expose({
|
|
393
|
+
debugSource: "renderer",
|
|
394
|
+
subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
|
|
167
395
|
push,
|
|
168
396
|
feed,
|
|
169
397
|
reset
|
|
170
398
|
});
|
|
171
399
|
return () => {
|
|
400
|
+
const onCardAction = (action) => {
|
|
401
|
+
if (props.actionRuntime) {
|
|
402
|
+
const request = {
|
|
403
|
+
type: action.type,
|
|
404
|
+
params: action.params,
|
|
405
|
+
cardType: action.cardType,
|
|
406
|
+
...action.cardId === void 0 ? {} : { cardId: action.cardId }
|
|
407
|
+
};
|
|
408
|
+
props.actionRuntime.dispatch(request, {
|
|
409
|
+
signal: actionScope.controller.signal,
|
|
410
|
+
owner: actionScope.owner
|
|
411
|
+
}).catch(() => {});
|
|
412
|
+
}
|
|
413
|
+
emit("card-action", action);
|
|
414
|
+
};
|
|
172
415
|
const ctx = {
|
|
173
416
|
registry: props.registry,
|
|
417
|
+
cardStore: props.cardStore,
|
|
174
418
|
plugins: props.plugins,
|
|
175
|
-
|
|
419
|
+
nodeRenderers: nodeRenderers.value,
|
|
420
|
+
onCardAction,
|
|
421
|
+
sanitize: props.sanitize,
|
|
422
|
+
sanitized: true
|
|
176
423
|
};
|
|
177
|
-
return h("div", { "data-aigui-renderer": "" }, nodes.value.map((n) => renderNode(n, ctx)));
|
|
424
|
+
return h("div", { "data-aigui-renderer": "" }, current.value.nodes.value.map((n) => renderNode(n, ctx)));
|
|
178
425
|
};
|
|
179
426
|
}
|
|
180
427
|
});
|
|
181
428
|
|
|
182
429
|
//#endregion
|
|
183
|
-
export { AIRenderer, renderNode, renderOutput, useAIRenderer };
|
|
430
|
+
export { AIRenderer, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Vue 3 adapter for AIGUI — stream LLM output into live Vue components with cards and plugins.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
|
@@ -23,14 +23,23 @@
|
|
|
23
23
|
"homepage": "https://github.com/liliang-cn/aigui#readme",
|
|
24
24
|
"bugs": "https://github.com/liliang-cn/aigui/issues",
|
|
25
25
|
"type": "module",
|
|
26
|
+
"sideEffects": false,
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=18"
|
|
29
|
+
},
|
|
26
30
|
"main": "./dist/index.cjs",
|
|
27
31
|
"module": "./dist/index.js",
|
|
28
32
|
"types": "./dist/index.d.ts",
|
|
29
33
|
"exports": {
|
|
30
34
|
".": {
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
"import": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"default": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"require": {
|
|
40
|
+
"types": "./dist/index.d.cts",
|
|
41
|
+
"default": "./dist/index.cjs"
|
|
42
|
+
}
|
|
34
43
|
}
|
|
35
44
|
},
|
|
36
45
|
"files": [
|
|
@@ -42,7 +51,7 @@
|
|
|
42
51
|
"access": "public"
|
|
43
52
|
},
|
|
44
53
|
"dependencies": {
|
|
45
|
-
"@ai-gui/core": "0.
|
|
54
|
+
"@ai-gui/core": "0.3.0"
|
|
46
55
|
},
|
|
47
56
|
"peerDependencies": {
|
|
48
57
|
"vue": ">=3.3"
|
|
@@ -53,6 +62,7 @@
|
|
|
53
62
|
},
|
|
54
63
|
"scripts": {
|
|
55
64
|
"build": "tsdown",
|
|
65
|
+
"test": "pnpm --dir ../.. exec vitest run --project vue",
|
|
56
66
|
"typecheck": "tsc --noEmit"
|
|
57
67
|
}
|
|
58
68
|
}
|