@ai-gui/vue 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +115 -30
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +116 -31
- package/package.json +15 -5
package/dist/index.cjs
CHANGED
|
@@ -28,18 +28,28 @@ 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 {
|
|
39
40
|
nodes,
|
|
40
|
-
push: (c) =>
|
|
41
|
-
|
|
41
|
+
push: (c) => {
|
|
42
|
+
if (active) renderer.push(c);
|
|
43
|
+
},
|
|
44
|
+
feed: (source, feedOptions) => active ? renderer.feed(source, feedOptions) : Promise.resolve(),
|
|
42
45
|
reset: () => {
|
|
46
|
+
if (active) {
|
|
47
|
+
renderer.reset();
|
|
48
|
+
nodes.value = [];
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
destroy: () => {
|
|
52
|
+
active = false;
|
|
43
53
|
renderer.reset();
|
|
44
54
|
nodes.value = [];
|
|
45
55
|
}
|
|
@@ -61,6 +71,10 @@ const MountHost = (0, vue.defineComponent)({
|
|
|
61
71
|
(0, vue.onMounted)(() => {
|
|
62
72
|
if (elRef.value) cleanup = props.mount(elRef.value);
|
|
63
73
|
});
|
|
74
|
+
(0, vue.watch)(() => props.mount, (mount) => {
|
|
75
|
+
if (typeof cleanup === "function") cleanup();
|
|
76
|
+
cleanup = elRef.value ? mount(elRef.value) : void 0;
|
|
77
|
+
});
|
|
64
78
|
(0, vue.onBeforeUnmount)(() => {
|
|
65
79
|
if (typeof cleanup === "function") cleanup();
|
|
66
80
|
});
|
|
@@ -71,10 +85,10 @@ const MountHost = (0, vue.defineComponent)({
|
|
|
71
85
|
}
|
|
72
86
|
});
|
|
73
87
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
74
|
-
function renderOutput(out) {
|
|
88
|
+
function renderOutput(out, sanitize) {
|
|
75
89
|
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));
|
|
90
|
+
case "html": return (0, vue.h)("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
|
|
91
|
+
case "element": return (0, vue.h)(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
|
|
78
92
|
case "mount": return (0, vue.h)(MountHost, { mount: out.mount });
|
|
79
93
|
case "card": return (0, vue.h)("pre", { "data-aigui-card-fallback": "" }, [(0, vue.h)("code", JSON.stringify(out.data, null, 2))]);
|
|
80
94
|
}
|
|
@@ -82,39 +96,77 @@ function renderOutput(out) {
|
|
|
82
96
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
83
97
|
const AsyncOutput = (0, vue.defineComponent)({
|
|
84
98
|
name: "AsyncOutput",
|
|
85
|
-
props: {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
99
|
+
props: {
|
|
100
|
+
promise: {
|
|
101
|
+
type: Object,
|
|
102
|
+
required: true
|
|
103
|
+
},
|
|
104
|
+
sanitize: {
|
|
105
|
+
type: [Boolean, Object],
|
|
106
|
+
default: void 0
|
|
107
|
+
}
|
|
108
|
+
},
|
|
89
109
|
setup(props) {
|
|
90
110
|
const resolved = (0, vue.ref)(null);
|
|
91
|
-
|
|
92
|
-
|
|
111
|
+
const failed = (0, vue.ref)(false);
|
|
112
|
+
let generation = 0;
|
|
113
|
+
let active = true;
|
|
114
|
+
const awaitPromise = (promise) => {
|
|
115
|
+
const current = ++generation;
|
|
116
|
+
resolved.value = null;
|
|
117
|
+
failed.value = false;
|
|
118
|
+
promise.then((value) => {
|
|
119
|
+
if (active && current === generation) resolved.value = value;
|
|
120
|
+
}, () => {
|
|
121
|
+
if (active && current === generation) failed.value = true;
|
|
122
|
+
});
|
|
123
|
+
};
|
|
124
|
+
(0, vue.watch)(() => [(0, vue.toRaw)(props.promise)], ([promise]) => awaitPromise(promise), { immediate: true });
|
|
125
|
+
(0, vue.onBeforeUnmount)(() => {
|
|
126
|
+
active = false;
|
|
127
|
+
generation++;
|
|
93
128
|
});
|
|
94
|
-
return () =>
|
|
129
|
+
return () => {
|
|
130
|
+
if (failed.value) return (0, vue.h)("span", { "data-aigui-async-error": "" });
|
|
131
|
+
if (!resolved.value) return (0, vue.h)("span", { "data-aigui-async-pending": "" });
|
|
132
|
+
try {
|
|
133
|
+
return renderOutput(resolved.value, props.sanitize);
|
|
134
|
+
} catch {
|
|
135
|
+
return (0, vue.h)("span", { "data-aigui-async-error": "" });
|
|
136
|
+
}
|
|
137
|
+
};
|
|
95
138
|
}
|
|
96
139
|
});
|
|
140
|
+
function sanitizeOutput(html, sanitize) {
|
|
141
|
+
if (sanitize === false) return html;
|
|
142
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
143
|
+
}
|
|
97
144
|
|
|
98
145
|
//#endregion
|
|
99
146
|
//#region src/render-node.ts
|
|
100
147
|
function renderNode(node, ctx) {
|
|
101
|
-
const r = (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins)[node.type];
|
|
102
|
-
if (r) {
|
|
148
|
+
const r = (ctx.nodeRenderers ?? (0, __ai_gui_core.collectNodeRenderers)(ctx.plugins))[node.type];
|
|
149
|
+
if (r) try {
|
|
103
150
|
const out = r(node);
|
|
104
151
|
if (out && typeof out.then === "function") return (0, vue.h)(AsyncOutput, {
|
|
105
152
|
key: node.key,
|
|
106
|
-
promise: out
|
|
153
|
+
promise: out,
|
|
154
|
+
sanitize: ctx.sanitize
|
|
107
155
|
});
|
|
108
|
-
|
|
156
|
+
const vnode = renderOutput(out, ctx.sanitize);
|
|
157
|
+
vnode.key = node.key;
|
|
158
|
+
return vnode;
|
|
159
|
+
} catch {
|
|
160
|
+
return renderFallback(node, ctx);
|
|
109
161
|
}
|
|
110
162
|
switch (node.type) {
|
|
111
163
|
case "heading": return (0, vue.h)(node.tag ?? "h1", {
|
|
112
164
|
key: node.key,
|
|
113
|
-
innerHTML: node.html ?? ""
|
|
165
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
114
166
|
});
|
|
115
167
|
case "paragraph": return (0, vue.h)("p", {
|
|
116
168
|
key: node.key,
|
|
117
|
-
innerHTML: node.html ?? ""
|
|
169
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
118
170
|
});
|
|
119
171
|
case "code": return (0, vue.h)("pre", {
|
|
120
172
|
key: node.key,
|
|
@@ -123,13 +175,10 @@ function renderNode(node, ctx) {
|
|
|
123
175
|
case "hr": return (0, vue.h)("hr", { key: node.key });
|
|
124
176
|
case "html": return (0, vue.h)("div", {
|
|
125
177
|
key: node.key,
|
|
126
|
-
innerHTML: node.content ?? ""
|
|
178
|
+
innerHTML: renderHtml(node.content ?? "", ctx)
|
|
127
179
|
});
|
|
128
180
|
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
|
-
});
|
|
181
|
+
default: return renderFallback(node, ctx);
|
|
133
182
|
}
|
|
134
183
|
}
|
|
135
184
|
function renderCard(node, ctx) {
|
|
@@ -149,7 +198,7 @@ function renderCard(node, ctx) {
|
|
|
149
198
|
key: node.key,
|
|
150
199
|
"data-aigui-card-fallback": ""
|
|
151
200
|
}, [(0, vue.h)("code", JSON.stringify(card.data, null, 2))]);
|
|
152
|
-
return (0, vue.h)(Comp, {
|
|
201
|
+
return (0, vue.h)((0, vue.markRaw)((0, vue.toRaw)(Comp)), {
|
|
153
202
|
key: node.key,
|
|
154
203
|
data: card.data,
|
|
155
204
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -158,11 +207,22 @@ function renderCard(node, ctx) {
|
|
|
158
207
|
})
|
|
159
208
|
});
|
|
160
209
|
}
|
|
210
|
+
function renderFallback(node, ctx) {
|
|
211
|
+
return (0, vue.h)("div", {
|
|
212
|
+
key: node.key,
|
|
213
|
+
innerHTML: renderHtml(node.html ?? node.content ?? "", ctx)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function renderHtml(html, ctx) {
|
|
217
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
218
|
+
return (0, __ai_gui_core.sanitizeHtml)(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
219
|
+
}
|
|
161
220
|
|
|
162
221
|
//#endregion
|
|
163
222
|
//#region src/ai-renderer.ts
|
|
164
223
|
const AIRenderer = (0, vue.defineComponent)({
|
|
165
224
|
name: "AIRenderer",
|
|
225
|
+
emits: ["card-action"],
|
|
166
226
|
props: {
|
|
167
227
|
registry: {
|
|
168
228
|
type: Object,
|
|
@@ -173,7 +233,7 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
173
233
|
default: void 0
|
|
174
234
|
},
|
|
175
235
|
sanitize: {
|
|
176
|
-
type: Boolean,
|
|
236
|
+
type: [Boolean, Object],
|
|
177
237
|
default: void 0
|
|
178
238
|
},
|
|
179
239
|
onCardAction: {
|
|
@@ -181,24 +241,49 @@ const AIRenderer = (0, vue.defineComponent)({
|
|
|
181
241
|
default: void 0
|
|
182
242
|
}
|
|
183
243
|
},
|
|
184
|
-
setup(props, { expose }) {
|
|
185
|
-
const
|
|
244
|
+
setup(props, { emit, expose }) {
|
|
245
|
+
const current = (0, vue.shallowRef)(useAIRenderer({
|
|
186
246
|
registry: props.registry,
|
|
187
247
|
sanitize: props.sanitize,
|
|
188
248
|
plugins: props.plugins
|
|
249
|
+
}));
|
|
250
|
+
const nodeRenderers = (0, vue.shallowRef)((0, __ai_gui_core.collectNodeRenderers)(props.plugins));
|
|
251
|
+
(0, vue.watch)(() => [
|
|
252
|
+
props.registry,
|
|
253
|
+
props.sanitize,
|
|
254
|
+
props.plugins
|
|
255
|
+
], () => {
|
|
256
|
+
current.value.destroy();
|
|
257
|
+
current.value = useAIRenderer({
|
|
258
|
+
registry: props.registry,
|
|
259
|
+
sanitize: props.sanitize,
|
|
260
|
+
plugins: props.plugins
|
|
261
|
+
});
|
|
262
|
+
nodeRenderers.value = (0, __ai_gui_core.collectNodeRenderers)(props.plugins);
|
|
189
263
|
});
|
|
264
|
+
(0, vue.onBeforeUnmount)(() => current.value.destroy());
|
|
265
|
+
const push = (chunk) => current.value.push(chunk);
|
|
266
|
+
const feed = (source, options) => current.value.feed(source, options);
|
|
267
|
+
const reset = () => current.value.reset();
|
|
190
268
|
expose({
|
|
191
269
|
push,
|
|
192
270
|
feed,
|
|
193
271
|
reset
|
|
194
272
|
});
|
|
195
273
|
return () => {
|
|
274
|
+
const onCardAction = (action) => {
|
|
275
|
+
props.onCardAction?.(action);
|
|
276
|
+
emit("card-action", action);
|
|
277
|
+
};
|
|
196
278
|
const ctx = {
|
|
197
279
|
registry: props.registry,
|
|
198
280
|
plugins: props.plugins,
|
|
199
|
-
|
|
281
|
+
nodeRenderers: nodeRenderers.value,
|
|
282
|
+
onCardAction,
|
|
283
|
+
sanitize: props.sanitize,
|
|
284
|
+
sanitized: true
|
|
200
285
|
};
|
|
201
|
-
return (0, vue.h)("div", { "data-aigui-renderer": "" }, nodes.value.map((n) => renderNode(n, ctx)));
|
|
286
|
+
return (0, vue.h)("div", { "data-aigui-renderer": "" }, current.value.nodes.value.map((n) => renderNode(n, ctx)));
|
|
202
287
|
};
|
|
203
288
|
}
|
|
204
289
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/use-ai-renderer.d.ts
|
|
5
5
|
interface UseAIRendererResult {
|
|
6
6
|
nodes: ShallowRef<ASTNode[]>;
|
|
7
7
|
push: (chunk: string) => void;
|
|
8
|
-
feed: (source:
|
|
8
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
9
9
|
reset: () => void;
|
|
10
|
+
destroy: () => void;
|
|
10
11
|
}
|
|
11
12
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
12
13
|
|
|
@@ -19,18 +20,21 @@ declare const AIRenderer: any;
|
|
|
19
20
|
interface RenderContext {
|
|
20
21
|
registry?: CardRegistry;
|
|
21
22
|
plugins?: AIGuiPlugin[];
|
|
23
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
22
24
|
onCardAction?: (action: {
|
|
23
25
|
type: string;
|
|
24
26
|
params?: unknown;
|
|
25
27
|
cardType: string;
|
|
26
28
|
}) => void;
|
|
29
|
+
sanitize?: RendererOptions["sanitize"];
|
|
30
|
+
sanitized?: boolean;
|
|
27
31
|
}
|
|
28
32
|
declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
|
|
29
33
|
|
|
30
34
|
//#endregion
|
|
31
35
|
//#region src/render-output.d.ts
|
|
32
36
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
33
|
-
declare function renderOutput(out: RenderOutput): VNode;
|
|
37
|
+
declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
|
|
34
38
|
|
|
35
39
|
//#endregion
|
|
36
40
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { ShallowRef, VNode } from "vue";
|
|
2
|
-
import { AIGuiPlugin, ASTNode, CardRegistry, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
2
|
+
import { AIGuiPlugin, ASTNode, CardRegistry, FeedOptions, FeedSource, NodeRenderer, RenderOutput, RendererOptions } from "@ai-gui/core";
|
|
3
3
|
|
|
4
4
|
//#region src/use-ai-renderer.d.ts
|
|
5
5
|
interface UseAIRendererResult {
|
|
6
6
|
nodes: ShallowRef<ASTNode[]>;
|
|
7
7
|
push: (chunk: string) => void;
|
|
8
|
-
feed: (source:
|
|
8
|
+
feed: (source: FeedSource, options?: FeedOptions) => Promise<void>;
|
|
9
9
|
reset: () => void;
|
|
10
|
+
destroy: () => void;
|
|
10
11
|
}
|
|
11
12
|
declare function useAIRenderer(options?: Omit<RendererOptions, "onPatch">): UseAIRendererResult;
|
|
12
13
|
|
|
@@ -19,18 +20,21 @@ declare const AIRenderer: any;
|
|
|
19
20
|
interface RenderContext {
|
|
20
21
|
registry?: CardRegistry;
|
|
21
22
|
plugins?: AIGuiPlugin[];
|
|
23
|
+
nodeRenderers?: Record<string, NodeRenderer>;
|
|
22
24
|
onCardAction?: (action: {
|
|
23
25
|
type: string;
|
|
24
26
|
params?: unknown;
|
|
25
27
|
cardType: string;
|
|
26
28
|
}) => void;
|
|
29
|
+
sanitize?: RendererOptions["sanitize"];
|
|
30
|
+
sanitized?: boolean;
|
|
27
31
|
}
|
|
28
32
|
declare function renderNode(node: ASTNode, ctx: RenderContext): VNode;
|
|
29
33
|
|
|
30
34
|
//#endregion
|
|
31
35
|
//#region src/render-output.d.ts
|
|
32
36
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
33
|
-
declare function renderOutput(out: RenderOutput): VNode;
|
|
37
|
+
declare function renderOutput(out: RenderOutput, sanitize?: RendererOptions["sanitize"]): VNode;
|
|
34
38
|
|
|
35
39
|
//#endregion
|
|
36
40
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
package/dist/index.js
CHANGED
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import { defineComponent, h, onBeforeUnmount, onMounted, ref, shallowRef } from "vue";
|
|
1
|
+
import { defineComponent, h, markRaw, onBeforeUnmount, onMounted, 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 {
|
|
15
16
|
nodes,
|
|
16
|
-
push: (c) =>
|
|
17
|
-
|
|
17
|
+
push: (c) => {
|
|
18
|
+
if (active) renderer.push(c);
|
|
19
|
+
},
|
|
20
|
+
feed: (source, feedOptions) => active ? renderer.feed(source, feedOptions) : Promise.resolve(),
|
|
18
21
|
reset: () => {
|
|
22
|
+
if (active) {
|
|
23
|
+
renderer.reset();
|
|
24
|
+
nodes.value = [];
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
destroy: () => {
|
|
28
|
+
active = false;
|
|
19
29
|
renderer.reset();
|
|
20
30
|
nodes.value = [];
|
|
21
31
|
}
|
|
@@ -37,6 +47,10 @@ const MountHost = defineComponent({
|
|
|
37
47
|
onMounted(() => {
|
|
38
48
|
if (elRef.value) cleanup = props.mount(elRef.value);
|
|
39
49
|
});
|
|
50
|
+
watch(() => props.mount, (mount) => {
|
|
51
|
+
if (typeof cleanup === "function") cleanup();
|
|
52
|
+
cleanup = elRef.value ? mount(elRef.value) : void 0;
|
|
53
|
+
});
|
|
40
54
|
onBeforeUnmount(() => {
|
|
41
55
|
if (typeof cleanup === "function") cleanup();
|
|
42
56
|
});
|
|
@@ -47,10 +61,10 @@ const MountHost = defineComponent({
|
|
|
47
61
|
}
|
|
48
62
|
});
|
|
49
63
|
/** Translate a framework-neutral RenderOutput into a Vue VNode. */
|
|
50
|
-
function renderOutput(out) {
|
|
64
|
+
function renderOutput(out, sanitize) {
|
|
51
65
|
switch (out.kind) {
|
|
52
|
-
case "html": return h("div", { innerHTML:
|
|
53
|
-
case "element": return h(out.tag, out.props, (out.children ?? []).map(renderOutput));
|
|
66
|
+
case "html": return h("div", { innerHTML: sanitizeOutput(out.html, sanitize) });
|
|
67
|
+
case "element": return h(out.tag, out.props, (out.children ?? []).map((child) => renderOutput(child, sanitize)));
|
|
54
68
|
case "mount": return h(MountHost, { mount: out.mount });
|
|
55
69
|
case "card": return h("pre", { "data-aigui-card-fallback": "" }, [h("code", JSON.stringify(out.data, null, 2))]);
|
|
56
70
|
}
|
|
@@ -58,39 +72,77 @@ function renderOutput(out) {
|
|
|
58
72
|
/** Await an async RenderOutput, rendering a placeholder until it resolves. */
|
|
59
73
|
const AsyncOutput = defineComponent({
|
|
60
74
|
name: "AsyncOutput",
|
|
61
|
-
props: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
75
|
+
props: {
|
|
76
|
+
promise: {
|
|
77
|
+
type: Object,
|
|
78
|
+
required: true
|
|
79
|
+
},
|
|
80
|
+
sanitize: {
|
|
81
|
+
type: [Boolean, Object],
|
|
82
|
+
default: void 0
|
|
83
|
+
}
|
|
84
|
+
},
|
|
65
85
|
setup(props) {
|
|
66
86
|
const resolved = ref(null);
|
|
67
|
-
|
|
68
|
-
|
|
87
|
+
const failed = ref(false);
|
|
88
|
+
let generation = 0;
|
|
89
|
+
let active = true;
|
|
90
|
+
const awaitPromise = (promise) => {
|
|
91
|
+
const current = ++generation;
|
|
92
|
+
resolved.value = null;
|
|
93
|
+
failed.value = false;
|
|
94
|
+
promise.then((value) => {
|
|
95
|
+
if (active && current === generation) resolved.value = value;
|
|
96
|
+
}, () => {
|
|
97
|
+
if (active && current === generation) failed.value = true;
|
|
98
|
+
});
|
|
99
|
+
};
|
|
100
|
+
watch(() => [toRaw(props.promise)], ([promise]) => awaitPromise(promise), { immediate: true });
|
|
101
|
+
onBeforeUnmount(() => {
|
|
102
|
+
active = false;
|
|
103
|
+
generation++;
|
|
69
104
|
});
|
|
70
|
-
return () =>
|
|
105
|
+
return () => {
|
|
106
|
+
if (failed.value) return h("span", { "data-aigui-async-error": "" });
|
|
107
|
+
if (!resolved.value) return h("span", { "data-aigui-async-pending": "" });
|
|
108
|
+
try {
|
|
109
|
+
return renderOutput(resolved.value, props.sanitize);
|
|
110
|
+
} catch {
|
|
111
|
+
return h("span", { "data-aigui-async-error": "" });
|
|
112
|
+
}
|
|
113
|
+
};
|
|
71
114
|
}
|
|
72
115
|
});
|
|
116
|
+
function sanitizeOutput(html, sanitize) {
|
|
117
|
+
if (sanitize === false) return html;
|
|
118
|
+
return sanitizeHtml(html, typeof sanitize === "object" ? sanitize : void 0);
|
|
119
|
+
}
|
|
73
120
|
|
|
74
121
|
//#endregion
|
|
75
122
|
//#region src/render-node.ts
|
|
76
123
|
function renderNode(node, ctx) {
|
|
77
|
-
const r = collectNodeRenderers(ctx.plugins)[node.type];
|
|
78
|
-
if (r) {
|
|
124
|
+
const r = (ctx.nodeRenderers ?? collectNodeRenderers(ctx.plugins))[node.type];
|
|
125
|
+
if (r) try {
|
|
79
126
|
const out = r(node);
|
|
80
127
|
if (out && typeof out.then === "function") return h(AsyncOutput, {
|
|
81
128
|
key: node.key,
|
|
82
|
-
promise: out
|
|
129
|
+
promise: out,
|
|
130
|
+
sanitize: ctx.sanitize
|
|
83
131
|
});
|
|
84
|
-
|
|
132
|
+
const vnode = renderOutput(out, ctx.sanitize);
|
|
133
|
+
vnode.key = node.key;
|
|
134
|
+
return vnode;
|
|
135
|
+
} catch {
|
|
136
|
+
return renderFallback(node, ctx);
|
|
85
137
|
}
|
|
86
138
|
switch (node.type) {
|
|
87
139
|
case "heading": return h(node.tag ?? "h1", {
|
|
88
140
|
key: node.key,
|
|
89
|
-
innerHTML: node.html ?? ""
|
|
141
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
90
142
|
});
|
|
91
143
|
case "paragraph": return h("p", {
|
|
92
144
|
key: node.key,
|
|
93
|
-
innerHTML: node.html ?? ""
|
|
145
|
+
innerHTML: renderHtml(node.html ?? "", ctx)
|
|
94
146
|
});
|
|
95
147
|
case "code": return h("pre", {
|
|
96
148
|
key: node.key,
|
|
@@ -99,13 +151,10 @@ function renderNode(node, ctx) {
|
|
|
99
151
|
case "hr": return h("hr", { key: node.key });
|
|
100
152
|
case "html": return h("div", {
|
|
101
153
|
key: node.key,
|
|
102
|
-
innerHTML: node.content ?? ""
|
|
154
|
+
innerHTML: renderHtml(node.content ?? "", ctx)
|
|
103
155
|
});
|
|
104
156
|
case "card": return renderCard(node, ctx);
|
|
105
|
-
default: return
|
|
106
|
-
key: node.key,
|
|
107
|
-
innerHTML: node.html ?? sanitizeHtml(node.content ?? "")
|
|
108
|
-
});
|
|
157
|
+
default: return renderFallback(node, ctx);
|
|
109
158
|
}
|
|
110
159
|
}
|
|
111
160
|
function renderCard(node, ctx) {
|
|
@@ -125,7 +174,7 @@ function renderCard(node, ctx) {
|
|
|
125
174
|
key: node.key,
|
|
126
175
|
"data-aigui-card-fallback": ""
|
|
127
176
|
}, [h("code", JSON.stringify(card.data, null, 2))]);
|
|
128
|
-
return h(Comp, {
|
|
177
|
+
return h(markRaw(toRaw(Comp)), {
|
|
129
178
|
key: node.key,
|
|
130
179
|
data: card.data,
|
|
131
180
|
onAction: (a) => ctx.onCardAction?.({
|
|
@@ -134,11 +183,22 @@ function renderCard(node, ctx) {
|
|
|
134
183
|
})
|
|
135
184
|
});
|
|
136
185
|
}
|
|
186
|
+
function renderFallback(node, ctx) {
|
|
187
|
+
return h("div", {
|
|
188
|
+
key: node.key,
|
|
189
|
+
innerHTML: renderHtml(node.html ?? node.content ?? "", ctx)
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
function renderHtml(html, ctx) {
|
|
193
|
+
if (ctx.sanitized || ctx.sanitize === false) return html;
|
|
194
|
+
return sanitizeHtml(html, typeof ctx.sanitize === "object" ? ctx.sanitize : void 0);
|
|
195
|
+
}
|
|
137
196
|
|
|
138
197
|
//#endregion
|
|
139
198
|
//#region src/ai-renderer.ts
|
|
140
199
|
const AIRenderer = defineComponent({
|
|
141
200
|
name: "AIRenderer",
|
|
201
|
+
emits: ["card-action"],
|
|
142
202
|
props: {
|
|
143
203
|
registry: {
|
|
144
204
|
type: Object,
|
|
@@ -149,7 +209,7 @@ const AIRenderer = defineComponent({
|
|
|
149
209
|
default: void 0
|
|
150
210
|
},
|
|
151
211
|
sanitize: {
|
|
152
|
-
type: Boolean,
|
|
212
|
+
type: [Boolean, Object],
|
|
153
213
|
default: void 0
|
|
154
214
|
},
|
|
155
215
|
onCardAction: {
|
|
@@ -157,24 +217,49 @@ const AIRenderer = defineComponent({
|
|
|
157
217
|
default: void 0
|
|
158
218
|
}
|
|
159
219
|
},
|
|
160
|
-
setup(props, { expose }) {
|
|
161
|
-
const
|
|
220
|
+
setup(props, { emit, expose }) {
|
|
221
|
+
const current = shallowRef(useAIRenderer({
|
|
162
222
|
registry: props.registry,
|
|
163
223
|
sanitize: props.sanitize,
|
|
164
224
|
plugins: props.plugins
|
|
225
|
+
}));
|
|
226
|
+
const nodeRenderers = shallowRef(collectNodeRenderers(props.plugins));
|
|
227
|
+
watch(() => [
|
|
228
|
+
props.registry,
|
|
229
|
+
props.sanitize,
|
|
230
|
+
props.plugins
|
|
231
|
+
], () => {
|
|
232
|
+
current.value.destroy();
|
|
233
|
+
current.value = useAIRenderer({
|
|
234
|
+
registry: props.registry,
|
|
235
|
+
sanitize: props.sanitize,
|
|
236
|
+
plugins: props.plugins
|
|
237
|
+
});
|
|
238
|
+
nodeRenderers.value = collectNodeRenderers(props.plugins);
|
|
165
239
|
});
|
|
240
|
+
onBeforeUnmount(() => current.value.destroy());
|
|
241
|
+
const push = (chunk) => current.value.push(chunk);
|
|
242
|
+
const feed = (source, options) => current.value.feed(source, options);
|
|
243
|
+
const reset = () => current.value.reset();
|
|
166
244
|
expose({
|
|
167
245
|
push,
|
|
168
246
|
feed,
|
|
169
247
|
reset
|
|
170
248
|
});
|
|
171
249
|
return () => {
|
|
250
|
+
const onCardAction = (action) => {
|
|
251
|
+
props.onCardAction?.(action);
|
|
252
|
+
emit("card-action", action);
|
|
253
|
+
};
|
|
172
254
|
const ctx = {
|
|
173
255
|
registry: props.registry,
|
|
174
256
|
plugins: props.plugins,
|
|
175
|
-
|
|
257
|
+
nodeRenderers: nodeRenderers.value,
|
|
258
|
+
onCardAction,
|
|
259
|
+
sanitize: props.sanitize,
|
|
260
|
+
sanitized: true
|
|
176
261
|
};
|
|
177
|
-
return h("div", { "data-aigui-renderer": "" }, nodes.value.map((n) => renderNode(n, ctx)));
|
|
262
|
+
return h("div", { "data-aigui-renderer": "" }, current.value.nodes.value.map((n) => renderNode(n, ctx)));
|
|
178
263
|
};
|
|
179
264
|
}
|
|
180
265
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-gui/vue",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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.2.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
|
}
|