@ai-gui/vue 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 +12 -4
- package/dist/index.cjs +183 -20
- package/dist/index.d.cts +9 -2
- package/dist/index.d.ts +9 -2
- package/dist/index.js +183 -21
- package/package.json +2 -2
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
|
@@ -37,6 +37,7 @@ function useAIRenderer(options = {}) {
|
|
|
37
37
|
}
|
|
38
38
|
});
|
|
39
39
|
return {
|
|
40
|
+
renderer,
|
|
40
41
|
nodes,
|
|
41
42
|
push: (c) => {
|
|
42
43
|
if (active) renderer.push(c);
|
|
@@ -56,6 +57,17 @@ function useAIRenderer(options = {}) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
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
|
+
|
|
59
71
|
//#endregion
|
|
60
72
|
//#region src/render-output.ts
|
|
61
73
|
/** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
|
|
@@ -146,18 +158,25 @@ function sanitizeOutput(html, sanitize) {
|
|
|
146
158
|
//#region src/render-node.ts
|
|
147
159
|
function renderNode(node, ctx) {
|
|
148
160
|
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
149
|
-
if (r)
|
|
150
|
-
|
|
151
|
-
if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
|
|
161
|
+
if (r) {
|
|
162
|
+
if (node.complete === false) return (0, vue.h)("div", {
|
|
152
163
|
key: node.key,
|
|
153
|
-
|
|
154
|
-
|
|
164
|
+
"data-aigui-block-loading": "",
|
|
165
|
+
"data-block-type": node.type
|
|
155
166
|
});
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
+
}
|
|
161
180
|
}
|
|
162
181
|
switch (node.type) {
|
|
163
182
|
case "heading": return (0, vue.h)(node.tag ?? "h1", {
|
|
@@ -194,6 +213,19 @@ function renderCard(node, ctx) {
|
|
|
194
213
|
"data-aigui-card-invalid": ""
|
|
195
214
|
}, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
|
|
196
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
|
+
});
|
|
197
229
|
if (!Comp) return (0, vue.h)("pre", {
|
|
198
230
|
key: node.key,
|
|
199
231
|
"data-aigui-card-fallback": ""
|
|
@@ -207,6 +239,85 @@ function renderCard(node, ctx) {
|
|
|
207
239
|
})
|
|
208
240
|
});
|
|
209
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
|
+
});
|
|
210
321
|
function renderFallback(node, ctx) {
|
|
211
322
|
return (0, vue.h)("div", {
|
|
212
323
|
key: node.key,
|
|
@@ -236,7 +347,19 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
236
347
|
type: [Boolean, Object],
|
|
237
348
|
default: void 0
|
|
238
349
|
},
|
|
239
|
-
|
|
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: {
|
|
240
363
|
type: Function,
|
|
241
364
|
default: void 0
|
|
242
365
|
}
|
|
@@ -245,38 +368,77 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
245
368
|
const current = (0, vue.shallowRef)(useAIRenderer({
|
|
246
369
|
registry: props.registry,
|
|
247
370
|
sanitize: props.sanitize,
|
|
248
|
-
plugins: props.plugins
|
|
371
|
+
plugins: props.plugins,
|
|
372
|
+
debug: props.debug,
|
|
373
|
+
onDebugEvent: props.onDebugEvent
|
|
249
374
|
}));
|
|
250
|
-
const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins));
|
|
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
|
+
};
|
|
251
387
|
(0, vue.watch)(() => [
|
|
252
388
|
props.registry,
|
|
253
389
|
props.sanitize,
|
|
254
|
-
props.plugins
|
|
390
|
+
props.plugins,
|
|
391
|
+
props.debug,
|
|
392
|
+
props.onDebugEvent
|
|
255
393
|
], () => {
|
|
394
|
+
resetActionScope();
|
|
256
395
|
current.value.destroy();
|
|
257
396
|
current.value = useAIRenderer({
|
|
258
397
|
registry: props.registry,
|
|
259
398
|
sanitize: props.sanitize,
|
|
260
|
-
plugins: props.plugins
|
|
399
|
+
plugins: props.plugins,
|
|
400
|
+
debug: props.debug,
|
|
401
|
+
onDebugEvent: props.onDebugEvent
|
|
261
402
|
});
|
|
262
|
-
nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins);
|
|
403
|
+
nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins, { debugTarget: current.value.renderer });
|
|
404
|
+
});
|
|
405
|
+
(0, vue.watch)(() => props.actionRuntime, resetActionScope);
|
|
406
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
407
|
+
resetActionScope();
|
|
408
|
+
current.value.destroy();
|
|
263
409
|
});
|
|
264
|
-
(0, vue.onBeforeUnmount)(() => current.value.destroy());
|
|
265
410
|
const push = (chunk) => current.value.push(chunk);
|
|
266
411
|
const feed = (source, options) => current.value.feed(source, options);
|
|
267
|
-
const reset = () =>
|
|
412
|
+
const reset = () => {
|
|
413
|
+
resetActionScope();
|
|
414
|
+
current.value.reset();
|
|
415
|
+
};
|
|
268
416
|
expose({
|
|
417
|
+
debugSource: "renderer",
|
|
418
|
+
subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
|
|
269
419
|
push,
|
|
270
420
|
feed,
|
|
271
421
|
reset
|
|
272
422
|
});
|
|
273
423
|
return () => {
|
|
274
424
|
const onCardAction = (action) => {
|
|
275
|
-
props.
|
|
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
|
+
}
|
|
276
437
|
emit("card-action", action);
|
|
277
438
|
};
|
|
278
439
|
const ctx = {
|
|
279
440
|
registry: props.registry,
|
|
441
|
+
cardStore: props.cardStore,
|
|
280
442
|
plugins: props.plugins,
|
|
281
443
|
nodeRenderers: nodeRenderers.value,
|
|
282
444
|
onCardAction,
|
|
@@ -292,4 +454,5 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
292
454
|
exports.AIRenderer = AIRenderer
|
|
293
455
|
exports.renderNode = renderNode
|
|
294
456
|
exports.renderOutput = renderOutput
|
|
295
|
-
exports.useAIRenderer = useAIRenderer
|
|
457
|
+
exports.useAIRenderer = useAIRenderer
|
|
458
|
+
exports.useActionState = useActionState
|
package/dist/index.d.cts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, 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
9
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
@@ -11,6 +12,10 @@ interface UseAIRendererResult {
|
|
|
11
12
|
}
|
|
12
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
13
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/use-action-state.d.ts
|
|
17
|
+
declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
|
|
18
|
+
|
|
14
19
|
//#endregion
|
|
15
20
|
//#region src/ai-renderer.d.ts
|
|
16
21
|
declare const AIRenderer: any;
|
|
@@ -19,12 +24,14 @@ declare const AIRenderer: any;
|
|
|
19
24
|
//#region src/render-node.d.ts
|
|
20
25
|
interface RenderContext {
|
|
21
26
|
registry?: CardRegistry;
|
|
27
|
+
cardStore?: CardStore;
|
|
22
28
|
plugins?: AIGuiPlugin[];
|
|
23
29
|
nodeRenderers?: Record<string, NodeRenderer>;
|
|
24
30
|
onCardAction?: (action: {
|
|
25
31
|
type: string;
|
|
26
32
|
params?: unknown;
|
|
27
33
|
cardType: string;
|
|
34
|
+
cardId?: string;
|
|
28
35
|
}) => void;
|
|
29
36
|
sanitize?: RendererOptions["sanitize"];
|
|
30
37
|
sanitized?: boolean;
|
|
@@ -39,4 +46,4 @@ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["san
|
|
|
39
46
|
//#endregion
|
|
40
47
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
41
48
|
|
|
42
|
-
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
|
|
49
|
+
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, 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
9
|
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
@@ -11,6 +12,10 @@ interface UseAIRendererResult {
|
|
|
11
12
|
}
|
|
12
13
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
13
14
|
|
|
15
|
+
//#endregion
|
|
16
|
+
//#region src/use-action-state.d.ts
|
|
17
|
+
declare function useActionState(runtime: ActionRuntime, key: string): ShallowRef<ActionState>;
|
|
18
|
+
|
|
14
19
|
//#endregion
|
|
15
20
|
//#region src/ai-renderer.d.ts
|
|
16
21
|
declare const AIRenderer: any;
|
|
@@ -19,12 +24,14 @@ declare const AIRenderer: any;
|
|
|
19
24
|
//#region src/render-node.d.ts
|
|
20
25
|
interface RenderContext {
|
|
21
26
|
registry?: CardRegistry;
|
|
27
|
+
cardStore?: CardStore;
|
|
22
28
|
plugins?: AIGuiPlugin[];
|
|
23
29
|
nodeRenderers?: Record<string, NodeRenderer>;
|
|
24
30
|
onCardAction?: (action: {
|
|
25
31
|
type: string;
|
|
26
32
|
params?: unknown;
|
|
27
33
|
cardType: string;
|
|
34
|
+
cardId?: string;
|
|
28
35
|
}) => void;
|
|
29
36
|
sanitize?: RendererOptions["sanitize"];
|
|
30
37
|
sanitized?: boolean;
|
|
@@ -39,4 +46,4 @@ declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["san
|
|
|
39
46
|
//#endregion
|
|
40
47
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
41
48
|
|
|
42
|
-
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer };
|
|
49
|
+
export { AIRenderer, RenderContext, UseAIRendererResult, renderNode, renderOutput, useAIRenderer, useActionState };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineComponent, h, markRaw, onBeforeUnmount, onMounted, ref, shallowRef, toRaw, watch } 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
|
|
@@ -13,6 +13,7 @@ function useAIRenderer(options = {}) {
|
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
15
|
return {
|
|
16
|
+
renderer,
|
|
16
17
|
nodes,
|
|
17
18
|
push: (c) => {
|
|
18
19
|
if (active) renderer.push(c);
|
|
@@ -32,6 +33,17 @@ function useAIRenderer(options = {}) {
|
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
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
|
+
|
|
35
47
|
//#endregion
|
|
36
48
|
//#region src/render-output.ts
|
|
37
49
|
/** Host element for imperative `mount` outputs, wired to Vue's lifecycle. */
|
|
@@ -122,18 +134,25 @@ function sanitizeOutput(html, sanitize) {
|
|
|
122
134
|
//#region src/render-node.ts
|
|
123
135
|
function renderNode(node, ctx) {
|
|
124
136
|
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
125
|
-
if (r)
|
|
126
|
-
|
|
127
|
-
if (out && typeof out.then === "function") return h(AsyncOutput, {
|
|
137
|
+
if (r) {
|
|
138
|
+
if (node.complete === false) return h("div", {
|
|
128
139
|
key: node.key,
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
"data-aigui-block-loading": "",
|
|
141
|
+
"data-block-type": node.type
|
|
131
142
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
156
|
}
|
|
138
157
|
switch (node.type) {
|
|
139
158
|
case "heading": return h(node.tag ?? "h1", {
|
|
@@ -170,6 +189,19 @@ function renderCard(node, ctx) {
|
|
|
170
189
|
"data-aigui-card-invalid": ""
|
|
171
190
|
}, [h("code", JSON.stringify(card.data, null, 2))]);
|
|
172
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
|
+
});
|
|
173
205
|
if (!Comp) return h("pre", {
|
|
174
206
|
key: node.key,
|
|
175
207
|
"data-aigui-card-fallback": ""
|
|
@@ -183,6 +215,85 @@ function renderCard(node, ctx) {
|
|
|
183
215
|
})
|
|
184
216
|
});
|
|
185
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
|
+
});
|
|
186
297
|
function renderFallback(node, ctx) {
|
|
187
298
|
return h("div", {
|
|
188
299
|
key: node.key,
|
|
@@ -212,7 +323,19 @@ const AIRenderer = defineComponent({
|
|
|
212
323
|
type: [Boolean, Object],
|
|
213
324
|
default: void 0
|
|
214
325
|
},
|
|
215
|
-
|
|
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: {
|
|
216
339
|
type: Function,
|
|
217
340
|
default: void 0
|
|
218
341
|
}
|
|
@@ -221,38 +344,77 @@ const AIRenderer = defineComponent({
|
|
|
221
344
|
const current = shallowRef(useAIRenderer({
|
|
222
345
|
registry: props.registry,
|
|
223
346
|
sanitize: props.sanitize,
|
|
224
|
-
plugins: props.plugins
|
|
347
|
+
plugins: props.plugins,
|
|
348
|
+
debug: props.debug,
|
|
349
|
+
onDebugEvent: props.onDebugEvent
|
|
225
350
|
}));
|
|
226
|
-
const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins));
|
|
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
|
+
};
|
|
227
363
|
watch(() => [
|
|
228
364
|
props.registry,
|
|
229
365
|
props.sanitize,
|
|
230
|
-
props.plugins
|
|
366
|
+
props.plugins,
|
|
367
|
+
props.debug,
|
|
368
|
+
props.onDebugEvent
|
|
231
369
|
], () => {
|
|
370
|
+
resetActionScope();
|
|
232
371
|
current.value.destroy();
|
|
233
372
|
current.value = useAIRenderer({
|
|
234
373
|
registry: props.registry,
|
|
235
374
|
sanitize: props.sanitize,
|
|
236
|
-
plugins: props.plugins
|
|
375
|
+
plugins: props.plugins,
|
|
376
|
+
debug: props.debug,
|
|
377
|
+
onDebugEvent: props.onDebugEvent
|
|
237
378
|
});
|
|
238
|
-
nodeRenderers.value = collectNodeRenderers(props.plugins);
|
|
379
|
+
nodeRenderers.value = collectNodeRenderers(props.plugins, { debugTarget: current.value.renderer });
|
|
380
|
+
});
|
|
381
|
+
watch(() => props.actionRuntime, resetActionScope);
|
|
382
|
+
onBeforeUnmount(() => {
|
|
383
|
+
resetActionScope();
|
|
384
|
+
current.value.destroy();
|
|
239
385
|
});
|
|
240
|
-
onBeforeUnmount(() => current.value.destroy());
|
|
241
386
|
const push = (chunk) => current.value.push(chunk);
|
|
242
387
|
const feed = (source, options) => current.value.feed(source, options);
|
|
243
|
-
const reset = () =>
|
|
388
|
+
const reset = () => {
|
|
389
|
+
resetActionScope();
|
|
390
|
+
current.value.reset();
|
|
391
|
+
};
|
|
244
392
|
expose({
|
|
393
|
+
debugSource: "renderer",
|
|
394
|
+
subscribeDebug: (listener) => current.value.renderer.subscribeDebug(listener),
|
|
245
395
|
push,
|
|
246
396
|
feed,
|
|
247
397
|
reset
|
|
248
398
|
});
|
|
249
399
|
return () => {
|
|
250
400
|
const onCardAction = (action) => {
|
|
251
|
-
props.
|
|
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
|
+
}
|
|
252
413
|
emit("card-action", action);
|
|
253
414
|
};
|
|
254
415
|
const ctx = {
|
|
255
416
|
registry: props.registry,
|
|
417
|
+
cardStore: props.cardStore,
|
|
256
418
|
plugins: props.plugins,
|
|
257
419
|
nodeRenderers: nodeRenderers.value,
|
|
258
420
|
onCardAction,
|
|
@@ -265,4 +427,4 @@ const AIRenderer = defineComponent({
|
|
|
265
427
|
});
|
|
266
428
|
|
|
267
429
|
//#endregion
|
|
268
|
-
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",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"access": "public"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"@ai-gui/core": "0.
|
|
54
|
+
"@ai-gui/core": "0.3.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vue": ">=3.3"
|