vanilla-agent 1.9.0 → 1.10.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 +246 -0
- package/dist/index.cjs +7 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +277 -1
- package/dist/index.d.ts +277 -1
- package/dist/index.global.js +26 -26
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +7 -7
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client.ts +141 -12
- package/src/components/forms.ts +1 -0
- package/src/components/registry.ts +87 -0
- package/src/index.ts +22 -2
- package/src/plugins/registry.ts +1 -0
- package/src/plugins/types.ts +1 -0
- package/src/types.ts +150 -0
- package/src/ui.ts +69 -6
- package/src/utils/component-middleware.ts +137 -0
- package/src/utils/component-parser.ts +119 -0
- package/src/utils/constants.ts +1 -0
- package/src/utils/dom.ts +1 -0
- package/src/utils/formatting.ts +33 -8
- package/src/utils/positioning.ts +1 -0
- package/src/utils/theme.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vanilla-agent",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.0",
|
|
4
4
|
"description": "Themeable, plugable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
package/src/client.ts
CHANGED
|
@@ -5,7 +5,11 @@ import {
|
|
|
5
5
|
AgentWidgetStreamParser,
|
|
6
6
|
AgentWidgetContextProvider,
|
|
7
7
|
AgentWidgetRequestMiddleware,
|
|
8
|
-
AgentWidgetRequestPayload
|
|
8
|
+
AgentWidgetRequestPayload,
|
|
9
|
+
AgentWidgetCustomFetch,
|
|
10
|
+
AgentWidgetSSEEventParser,
|
|
11
|
+
AgentWidgetHeadersFunction,
|
|
12
|
+
AgentWidgetSSEEventResult
|
|
9
13
|
} from "./types";
|
|
10
14
|
import {
|
|
11
15
|
extractTextFromJson,
|
|
@@ -48,6 +52,9 @@ export class AgentWidgetClient {
|
|
|
48
52
|
private readonly createStreamParser: () => AgentWidgetStreamParser;
|
|
49
53
|
private readonly contextProviders: AgentWidgetContextProvider[];
|
|
50
54
|
private readonly requestMiddleware?: AgentWidgetRequestMiddleware;
|
|
55
|
+
private readonly customFetch?: AgentWidgetCustomFetch;
|
|
56
|
+
private readonly parseSSEEvent?: AgentWidgetSSEEventParser;
|
|
57
|
+
private readonly getHeaders?: AgentWidgetHeadersFunction;
|
|
51
58
|
|
|
52
59
|
constructor(private config: AgentWidgetConfig = {}) {
|
|
53
60
|
this.apiUrl = config.apiUrl ?? DEFAULT_ENDPOINT;
|
|
@@ -60,6 +67,9 @@ export class AgentWidgetClient {
|
|
|
60
67
|
this.createStreamParser = config.streamParser ?? getParserFromType(config.parserType);
|
|
61
68
|
this.contextProviders = config.contextProviders ?? [];
|
|
62
69
|
this.requestMiddleware = config.requestMiddleware;
|
|
70
|
+
this.customFetch = config.customFetch;
|
|
71
|
+
this.parseSSEEvent = config.parseSSEEvent;
|
|
72
|
+
this.getHeaders = config.getHeaders;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
public async dispatch(options: DispatchOptions, onEvent: SSEHandler) {
|
|
@@ -70,19 +80,54 @@ export class AgentWidgetClient {
|
|
|
70
80
|
|
|
71
81
|
onEvent({ type: "status", status: "connecting" });
|
|
72
82
|
|
|
73
|
-
const
|
|
83
|
+
const payload = await this.buildPayload(options.messages);
|
|
74
84
|
|
|
75
85
|
if (this.debug) {
|
|
76
86
|
// eslint-disable-next-line no-console
|
|
77
|
-
console.debug("[AgentWidgetClient] dispatch
|
|
87
|
+
console.debug("[AgentWidgetClient] dispatch payload", payload);
|
|
78
88
|
}
|
|
79
89
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
// Build headers - merge static headers with dynamic headers if provided
|
|
91
|
+
let headers = { ...this.headers };
|
|
92
|
+
if (this.getHeaders) {
|
|
93
|
+
try {
|
|
94
|
+
const dynamicHeaders = await this.getHeaders();
|
|
95
|
+
headers = { ...headers, ...dynamicHeaders };
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (typeof console !== "undefined") {
|
|
98
|
+
// eslint-disable-next-line no-console
|
|
99
|
+
console.error("[AgentWidget] getHeaders error:", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Use customFetch if provided, otherwise use default fetch
|
|
105
|
+
let response: Response;
|
|
106
|
+
if (this.customFetch) {
|
|
107
|
+
try {
|
|
108
|
+
response = await this.customFetch(
|
|
109
|
+
this.apiUrl,
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers,
|
|
113
|
+
body: JSON.stringify(payload),
|
|
114
|
+
signal: controller.signal
|
|
115
|
+
},
|
|
116
|
+
payload
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
120
|
+
onEvent({ type: "error", error: err });
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
response = await fetch(this.apiUrl, {
|
|
125
|
+
method: "POST",
|
|
126
|
+
headers,
|
|
127
|
+
body: JSON.stringify(payload),
|
|
128
|
+
signal: controller.signal
|
|
129
|
+
});
|
|
130
|
+
}
|
|
86
131
|
|
|
87
132
|
if (!response.ok || !response.body) {
|
|
88
133
|
const error = new Error(
|
|
@@ -112,7 +157,7 @@ export class AgentWidgetClient {
|
|
|
112
157
|
})
|
|
113
158
|
.map((message) => ({
|
|
114
159
|
role: message.role,
|
|
115
|
-
content: message.content,
|
|
160
|
+
content: message.rawContent || message.content,
|
|
116
161
|
createdAt: message.createdAt
|
|
117
162
|
}));
|
|
118
163
|
|
|
@@ -167,6 +212,70 @@ export class AgentWidgetClient {
|
|
|
167
212
|
return payload;
|
|
168
213
|
}
|
|
169
214
|
|
|
215
|
+
/**
|
|
216
|
+
* Handle custom SSE event parsing via parseSSEEvent callback
|
|
217
|
+
* Returns true if event was handled, false otherwise
|
|
218
|
+
*/
|
|
219
|
+
private async handleCustomSSEEvent(
|
|
220
|
+
payload: unknown,
|
|
221
|
+
onEvent: SSEHandler,
|
|
222
|
+
assistantMessageRef: { current: AgentWidgetMessage | null },
|
|
223
|
+
emitMessage: (msg: AgentWidgetMessage) => void,
|
|
224
|
+
nextSequence: () => number
|
|
225
|
+
): Promise<boolean> {
|
|
226
|
+
if (!this.parseSSEEvent) return false;
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const result = await this.parseSSEEvent(payload);
|
|
230
|
+
if (result === null) return false; // Event should be ignored
|
|
231
|
+
|
|
232
|
+
const ensureAssistant = () => {
|
|
233
|
+
if (assistantMessageRef.current) return assistantMessageRef.current;
|
|
234
|
+
const msg: AgentWidgetMessage = {
|
|
235
|
+
id: `assistant-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
236
|
+
role: "assistant",
|
|
237
|
+
content: "",
|
|
238
|
+
createdAt: new Date().toISOString(),
|
|
239
|
+
streaming: true,
|
|
240
|
+
variant: "assistant",
|
|
241
|
+
sequence: nextSequence()
|
|
242
|
+
};
|
|
243
|
+
assistantMessageRef.current = msg;
|
|
244
|
+
emitMessage(msg);
|
|
245
|
+
return msg;
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
if (result.text !== undefined) {
|
|
249
|
+
const assistant = ensureAssistant();
|
|
250
|
+
assistant.content += result.text;
|
|
251
|
+
emitMessage(assistant);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (result.done) {
|
|
255
|
+
if (assistantMessageRef.current) {
|
|
256
|
+
assistantMessageRef.current.streaming = false;
|
|
257
|
+
emitMessage(assistantMessageRef.current);
|
|
258
|
+
}
|
|
259
|
+
onEvent({ type: "status", status: "idle" });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (result.error) {
|
|
263
|
+
onEvent({
|
|
264
|
+
type: "error",
|
|
265
|
+
error: new Error(result.error)
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return true; // Event was handled
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (typeof console !== "undefined") {
|
|
272
|
+
// eslint-disable-next-line no-console
|
|
273
|
+
console.error("[AgentWidget] parseSSEEvent error:", error);
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
170
279
|
private async streamResponse(
|
|
171
280
|
body: ReadableStream<Uint8Array>,
|
|
172
281
|
onEvent: SSEHandler
|
|
@@ -215,6 +324,8 @@ export class AgentWidgetClient {
|
|
|
215
324
|
};
|
|
216
325
|
|
|
217
326
|
let assistantMessage: AgentWidgetMessage | null = null;
|
|
327
|
+
// Reference to track assistant message for custom event handler
|
|
328
|
+
const assistantMessageRef = { current: null as AgentWidgetMessage | null };
|
|
218
329
|
const reasoningMessages = new Map<string, AgentWidgetMessage>();
|
|
219
330
|
const toolMessages = new Map<string, AgentWidgetMessage>();
|
|
220
331
|
const reasoningContext = {
|
|
@@ -265,7 +376,6 @@ export class AgentWidgetClient {
|
|
|
265
376
|
content: "",
|
|
266
377
|
createdAt: new Date().toISOString(),
|
|
267
378
|
streaming: true,
|
|
268
|
-
variant: "assistant",
|
|
269
379
|
sequence: nextSequence()
|
|
270
380
|
};
|
|
271
381
|
emitMessage(assistantMessage);
|
|
@@ -471,6 +581,24 @@ export class AgentWidgetClient {
|
|
|
471
581
|
const payloadType =
|
|
472
582
|
eventType !== "message" ? eventType : payload.type ?? "message";
|
|
473
583
|
|
|
584
|
+
// If custom SSE event parser is provided, try it first
|
|
585
|
+
if (this.parseSSEEvent) {
|
|
586
|
+
// Keep assistant message ref in sync
|
|
587
|
+
assistantMessageRef.current = assistantMessage;
|
|
588
|
+
const handled = await this.handleCustomSSEEvent(
|
|
589
|
+
payload,
|
|
590
|
+
onEvent,
|
|
591
|
+
assistantMessageRef,
|
|
592
|
+
emitMessage,
|
|
593
|
+
nextSequence
|
|
594
|
+
);
|
|
595
|
+
// Update assistantMessage from ref (in case it was created)
|
|
596
|
+
if (assistantMessageRef.current && !assistantMessage) {
|
|
597
|
+
assistantMessage = assistantMessageRef.current;
|
|
598
|
+
}
|
|
599
|
+
if (handled) continue; // Skip default handling if custom handler processed it
|
|
600
|
+
}
|
|
601
|
+
|
|
474
602
|
if (payloadType === "reason_start") {
|
|
475
603
|
const reasoningId =
|
|
476
604
|
resolveReasoningId(payload, true) ?? `reason-${nextSequence()}`;
|
|
@@ -635,7 +763,8 @@ export class AgentWidgetClient {
|
|
|
635
763
|
continue;
|
|
636
764
|
}
|
|
637
765
|
const assistant = ensureAssistantMessage();
|
|
638
|
-
|
|
766
|
+
// Support various field names: text, delta, content, chunk (Travrse uses 'chunk')
|
|
767
|
+
const chunk = payload.text ?? payload.delta ?? payload.content ?? payload.chunk ?? "";
|
|
639
768
|
if (chunk) {
|
|
640
769
|
// Accumulate raw content for structured format parsing
|
|
641
770
|
const rawBuffer = rawContentBuffers.get(assistant.id) ?? "";
|
package/src/components/forms.ts
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { AgentWidgetConfig, AgentWidgetMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context provided to component renderers
|
|
5
|
+
*/
|
|
6
|
+
export interface ComponentContext {
|
|
7
|
+
message: AgentWidgetMessage;
|
|
8
|
+
config: AgentWidgetConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Update component props during streaming
|
|
11
|
+
*/
|
|
12
|
+
updateProps: (newProps: Record<string, unknown>) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Component renderer function signature
|
|
17
|
+
*/
|
|
18
|
+
export type ComponentRenderer = (
|
|
19
|
+
props: Record<string, unknown>,
|
|
20
|
+
context: ComponentContext
|
|
21
|
+
) => HTMLElement;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Component registry for managing custom components
|
|
25
|
+
*/
|
|
26
|
+
class ComponentRegistry {
|
|
27
|
+
private components: Map<string, ComponentRenderer> = new Map();
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Register a custom component
|
|
31
|
+
*/
|
|
32
|
+
register(name: string, renderer: ComponentRenderer): void {
|
|
33
|
+
if (this.components.has(name)) {
|
|
34
|
+
console.warn(`[ComponentRegistry] Component "${name}" is already registered. Overwriting.`);
|
|
35
|
+
}
|
|
36
|
+
this.components.set(name, renderer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Unregister a component
|
|
41
|
+
*/
|
|
42
|
+
unregister(name: string): void {
|
|
43
|
+
this.components.delete(name);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get a component renderer by name
|
|
48
|
+
*/
|
|
49
|
+
get(name: string): ComponentRenderer | undefined {
|
|
50
|
+
return this.components.get(name);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Check if a component is registered
|
|
55
|
+
*/
|
|
56
|
+
has(name: string): boolean {
|
|
57
|
+
return this.components.has(name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get all registered component names
|
|
62
|
+
*/
|
|
63
|
+
getAllNames(): string[] {
|
|
64
|
+
return Array.from(this.components.keys());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clear all registered components
|
|
69
|
+
*/
|
|
70
|
+
clear(): void {
|
|
71
|
+
this.components.clear();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Register multiple components at once
|
|
76
|
+
*/
|
|
77
|
+
registerAll(components: Record<string, ComponentRenderer>): void {
|
|
78
|
+
Object.entries(components).forEach(([name, renderer]) => {
|
|
79
|
+
this.register(name, renderer);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Global component registry instance
|
|
86
|
+
*/
|
|
87
|
+
export const componentRegistry = new ComponentRegistry();
|
package/src/index.ts
CHANGED
|
@@ -12,7 +12,12 @@ export type {
|
|
|
12
12
|
AgentWidgetLauncherConfig,
|
|
13
13
|
AgentWidgetEvent,
|
|
14
14
|
AgentWidgetStreamParser,
|
|
15
|
-
AgentWidgetStreamParserResult
|
|
15
|
+
AgentWidgetStreamParserResult,
|
|
16
|
+
AgentWidgetRequestPayload,
|
|
17
|
+
AgentWidgetCustomFetch,
|
|
18
|
+
AgentWidgetSSEEventParser,
|
|
19
|
+
AgentWidgetSSEEventResult,
|
|
20
|
+
AgentWidgetHeadersFunction
|
|
16
21
|
} from "./types";
|
|
17
22
|
|
|
18
23
|
export { initAgentWidgetFn as initAgentWidget };
|
|
@@ -36,7 +41,7 @@ export {
|
|
|
36
41
|
escapeHtml,
|
|
37
42
|
directivePostprocessor
|
|
38
43
|
} from "./postprocessors";
|
|
39
|
-
export {
|
|
44
|
+
export {
|
|
40
45
|
createPlainTextParser,
|
|
41
46
|
createJsonStreamParser,
|
|
42
47
|
createFlexibleJsonStreamParser,
|
|
@@ -49,6 +54,21 @@ export type { AgentWidgetInitHandle };
|
|
|
49
54
|
export type { AgentWidgetPlugin } from "./plugins/types";
|
|
50
55
|
export { pluginRegistry } from "./plugins/registry";
|
|
51
56
|
|
|
57
|
+
// Component system exports
|
|
58
|
+
export { componentRegistry } from "./components/registry";
|
|
59
|
+
export type { ComponentRenderer, ComponentContext } from "./components/registry";
|
|
60
|
+
export {
|
|
61
|
+
createComponentStreamParser,
|
|
62
|
+
isComponentDirectiveType
|
|
63
|
+
} from "./utils/component-parser";
|
|
64
|
+
export type { ComponentDirective } from "./utils/component-parser";
|
|
65
|
+
export {
|
|
66
|
+
renderComponentDirective,
|
|
67
|
+
createComponentMiddleware,
|
|
68
|
+
hasComponentDirective,
|
|
69
|
+
extractComponentDirectiveFromMessage
|
|
70
|
+
} from "./utils/component-middleware";
|
|
71
|
+
|
|
52
72
|
// Default configuration exports
|
|
53
73
|
export { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from "./defaults";
|
|
54
74
|
|
package/src/plugins/registry.ts
CHANGED
package/src/plugins/types.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -339,10 +339,72 @@ export interface AgentWidgetStreamParser {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Component renderer function signature for custom components
|
|
344
|
+
*/
|
|
345
|
+
export type AgentWidgetComponentRenderer = (
|
|
346
|
+
props: Record<string, unknown>,
|
|
347
|
+
context: {
|
|
348
|
+
message: AgentWidgetMessage;
|
|
349
|
+
config: AgentWidgetConfig;
|
|
350
|
+
updateProps: (newProps: Record<string, unknown>) => void;
|
|
351
|
+
}
|
|
352
|
+
) => HTMLElement;
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Result from custom SSE event parser
|
|
356
|
+
*/
|
|
357
|
+
export type AgentWidgetSSEEventResult = {
|
|
358
|
+
/** Text content to display */
|
|
359
|
+
text?: string;
|
|
360
|
+
/** Whether the stream is complete */
|
|
361
|
+
done?: boolean;
|
|
362
|
+
/** Error message if an error occurred */
|
|
363
|
+
error?: string;
|
|
364
|
+
} | null;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Custom SSE event parser function
|
|
368
|
+
* Allows transforming non-standard SSE event formats to vanilla-agent's expected format
|
|
369
|
+
*/
|
|
370
|
+
export type AgentWidgetSSEEventParser = (
|
|
371
|
+
eventData: unknown
|
|
372
|
+
) => AgentWidgetSSEEventResult | Promise<AgentWidgetSSEEventResult>;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Custom fetch function for full control over API requests
|
|
376
|
+
* Use this for custom authentication, request transformation, etc.
|
|
377
|
+
*/
|
|
378
|
+
export type AgentWidgetCustomFetch = (
|
|
379
|
+
url: string,
|
|
380
|
+
init: RequestInit,
|
|
381
|
+
payload: AgentWidgetRequestPayload
|
|
382
|
+
) => Promise<Response>;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Dynamic headers function - called before each request
|
|
386
|
+
*/
|
|
387
|
+
export type AgentWidgetHeadersFunction = () => Record<string, string> | Promise<Record<string, string>>;
|
|
388
|
+
|
|
342
389
|
export type AgentWidgetConfig = {
|
|
343
390
|
apiUrl?: string;
|
|
344
391
|
flowId?: string;
|
|
392
|
+
/**
|
|
393
|
+
* Static headers to include with each request.
|
|
394
|
+
* For dynamic headers (e.g., auth tokens), use `getHeaders` instead.
|
|
395
|
+
*/
|
|
345
396
|
headers?: Record<string, string>;
|
|
397
|
+
/**
|
|
398
|
+
* Dynamic headers function - called before each request.
|
|
399
|
+
* Useful for adding auth tokens that may change.
|
|
400
|
+
* @example
|
|
401
|
+
* ```typescript
|
|
402
|
+
* getHeaders: async () => ({
|
|
403
|
+
* 'Authorization': `Bearer ${await getAuthToken()}`
|
|
404
|
+
* })
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
getHeaders?: AgentWidgetHeadersFunction;
|
|
346
408
|
copy?: {
|
|
347
409
|
welcomeTitle?: string;
|
|
348
410
|
welcomeSubtitle?: string;
|
|
@@ -374,6 +436,32 @@ export type AgentWidgetConfig = {
|
|
|
374
436
|
actionParsers?: AgentWidgetActionParser[];
|
|
375
437
|
actionHandlers?: AgentWidgetActionHandler[];
|
|
376
438
|
storageAdapter?: AgentWidgetStorageAdapter;
|
|
439
|
+
/**
|
|
440
|
+
* Registry of custom components that can be rendered from JSON directives.
|
|
441
|
+
* Components are registered by name and can be invoked via JSON responses
|
|
442
|
+
* with the format: `{"component": "ComponentName", "props": {...}}`
|
|
443
|
+
*
|
|
444
|
+
* @example
|
|
445
|
+
* ```typescript
|
|
446
|
+
* config: {
|
|
447
|
+
* components: {
|
|
448
|
+
* ProductCard: (props, context) => {
|
|
449
|
+
* const card = document.createElement("div");
|
|
450
|
+
* card.innerHTML = `<h3>${props.title}</h3><p>$${props.price}</p>`;
|
|
451
|
+
* return card;
|
|
452
|
+
* }
|
|
453
|
+
* }
|
|
454
|
+
* }
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
components?: Record<string, AgentWidgetComponentRenderer>;
|
|
458
|
+
/**
|
|
459
|
+
* Enable component streaming. When true, component props will be updated
|
|
460
|
+
* incrementally as they stream in from the JSON response.
|
|
461
|
+
*
|
|
462
|
+
* @default true
|
|
463
|
+
*/
|
|
464
|
+
enableComponentStreaming?: boolean;
|
|
377
465
|
/**
|
|
378
466
|
* Custom stream parser for extracting text from streaming structured responses.
|
|
379
467
|
* Handles incremental parsing of JSON, XML, or other formats.
|
|
@@ -430,6 +518,68 @@ export type AgentWidgetConfig = {
|
|
|
430
518
|
* ```
|
|
431
519
|
*/
|
|
432
520
|
parserType?: "plain" | "json" | "regex-json" | "xml";
|
|
521
|
+
/**
|
|
522
|
+
* Custom fetch function for full control over API requests.
|
|
523
|
+
* Use this for custom authentication, request/response transformation, etc.
|
|
524
|
+
*
|
|
525
|
+
* When provided, this function is called instead of the default fetch.
|
|
526
|
+
* You receive the URL, RequestInit, and the payload that would be sent.
|
|
527
|
+
*
|
|
528
|
+
* @example
|
|
529
|
+
* ```typescript
|
|
530
|
+
* config: {
|
|
531
|
+
* customFetch: async (url, init, payload) => {
|
|
532
|
+
* // Transform request for your API format
|
|
533
|
+
* const myPayload = {
|
|
534
|
+
* flow: { id: 'my-flow-id' },
|
|
535
|
+
* messages: payload.messages,
|
|
536
|
+
* options: { stream_response: true }
|
|
537
|
+
* };
|
|
538
|
+
*
|
|
539
|
+
* // Add auth header
|
|
540
|
+
* const token = await getAuthToken();
|
|
541
|
+
*
|
|
542
|
+
* return fetch('/my-api/dispatch', {
|
|
543
|
+
* method: 'POST',
|
|
544
|
+
* headers: {
|
|
545
|
+
* 'Content-Type': 'application/json',
|
|
546
|
+
* 'Authorization': `Bearer ${token}`
|
|
547
|
+
* },
|
|
548
|
+
* body: JSON.stringify(myPayload),
|
|
549
|
+
* signal: init.signal
|
|
550
|
+
* });
|
|
551
|
+
* }
|
|
552
|
+
* }
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
customFetch?: AgentWidgetCustomFetch;
|
|
556
|
+
/**
|
|
557
|
+
* Custom SSE event parser for non-standard streaming response formats.
|
|
558
|
+
*
|
|
559
|
+
* Use this when your API returns SSE events in a different format than expected.
|
|
560
|
+
* Return `{ text }` for text chunks, `{ done: true }` for completion,
|
|
561
|
+
* `{ error }` for errors, or `null` to ignore the event.
|
|
562
|
+
*
|
|
563
|
+
* @example
|
|
564
|
+
* ```typescript
|
|
565
|
+
* // For Travrse API format
|
|
566
|
+
* config: {
|
|
567
|
+
* parseSSEEvent: (data) => {
|
|
568
|
+
* if (data.type === 'step_chunk' && data.chunk) {
|
|
569
|
+
* return { text: data.chunk };
|
|
570
|
+
* }
|
|
571
|
+
* if (data.type === 'flow_complete') {
|
|
572
|
+
* return { done: true };
|
|
573
|
+
* }
|
|
574
|
+
* if (data.type === 'step_error') {
|
|
575
|
+
* return { error: data.error };
|
|
576
|
+
* }
|
|
577
|
+
* return null; // Ignore other events
|
|
578
|
+
* }
|
|
579
|
+
* }
|
|
580
|
+
* ```
|
|
581
|
+
*/
|
|
582
|
+
parseSSEEvent?: AgentWidgetSSEEventParser;
|
|
433
583
|
};
|
|
434
584
|
|
|
435
585
|
export type AgentWidgetMessageRole = "user" | "assistant" | "system";
|
package/src/ui.ts
CHANGED
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
defaultActionHandlers,
|
|
32
32
|
defaultJsonActionParser
|
|
33
33
|
} from "./utils/actions";
|
|
34
|
+
import { createLocalStorageAdapter } from "./utils/storage";
|
|
35
|
+
import { componentRegistry } from "./components/registry";
|
|
36
|
+
import {
|
|
37
|
+
renderComponentDirective,
|
|
38
|
+
extractComponentDirectiveFromMessage,
|
|
39
|
+
hasComponentDirective
|
|
40
|
+
} from "./utils/component-middleware";
|
|
34
41
|
|
|
35
42
|
// Default localStorage key for chat history (automatically cleared on clear chat)
|
|
36
43
|
const DEFAULT_CHAT_HISTORY_STORAGE_KEY = "vanilla-agent-chat-history";
|
|
@@ -132,10 +139,15 @@ export const createAgentExperience = (
|
|
|
132
139
|
|
|
133
140
|
// Get plugins for this instance
|
|
134
141
|
const plugins = pluginRegistry.getForInstance(config.plugins);
|
|
142
|
+
|
|
143
|
+
// Register components from config
|
|
144
|
+
if (config.components) {
|
|
145
|
+
componentRegistry.registerAll(config.components);
|
|
146
|
+
}
|
|
135
147
|
const eventBus = createEventBus<AgentWidgetControllerEventMap>();
|
|
136
148
|
|
|
137
|
-
const storageAdapter: AgentWidgetStorageAdapter
|
|
138
|
-
config.storageAdapter;
|
|
149
|
+
const storageAdapter: AgentWidgetStorageAdapter =
|
|
150
|
+
config.storageAdapter ?? createLocalStorageAdapter();
|
|
139
151
|
let persistentMetadata: Record<string, unknown> = {};
|
|
140
152
|
let pendingStoredState: Promise<AgentWidgetStoredState | null> | null = null;
|
|
141
153
|
|
|
@@ -302,11 +314,17 @@ export const createAgentExperience = (
|
|
|
302
314
|
: [];
|
|
303
315
|
|
|
304
316
|
function persistState(messagesOverride?: AgentWidgetMessage[]) {
|
|
305
|
-
if (!storageAdapter?.save
|
|
317
|
+
if (!storageAdapter?.save) return;
|
|
318
|
+
|
|
319
|
+
// Allow saving even if session doesn't exist yet (for metadata during init)
|
|
320
|
+
const messages = messagesOverride
|
|
321
|
+
? stripStreamingFromMessages(messagesOverride)
|
|
322
|
+
: session
|
|
323
|
+
? getMessagesForPersistence()
|
|
324
|
+
: [];
|
|
325
|
+
|
|
306
326
|
const payload = {
|
|
307
|
-
messages
|
|
308
|
-
? stripStreamingFromMessages(messagesOverride)
|
|
309
|
-
: getMessagesForPersistence(),
|
|
327
|
+
messages,
|
|
310
328
|
metadata: persistentMetadata
|
|
311
329
|
};
|
|
312
330
|
try {
|
|
@@ -518,6 +536,51 @@ export const createAgentExperience = (
|
|
|
518
536
|
}
|
|
519
537
|
}
|
|
520
538
|
|
|
539
|
+
// Check for component directive if no plugin handled it
|
|
540
|
+
if (!bubble && message.role === "assistant" && !message.variant) {
|
|
541
|
+
const enableComponentStreaming = config.enableComponentStreaming !== false; // Default to true
|
|
542
|
+
if (enableComponentStreaming && hasComponentDirective(message)) {
|
|
543
|
+
const directive = extractComponentDirectiveFromMessage(message);
|
|
544
|
+
if (directive) {
|
|
545
|
+
const componentBubble = renderComponentDirective(directive, {
|
|
546
|
+
config,
|
|
547
|
+
message,
|
|
548
|
+
transform
|
|
549
|
+
});
|
|
550
|
+
if (componentBubble) {
|
|
551
|
+
// Wrap component in standard bubble styling
|
|
552
|
+
const wrapper = document.createElement("div");
|
|
553
|
+
wrapper.className = [
|
|
554
|
+
"vanilla-message-bubble",
|
|
555
|
+
"tvw-max-w-[85%]",
|
|
556
|
+
"tvw-rounded-2xl",
|
|
557
|
+
"tvw-bg-cw-surface",
|
|
558
|
+
"tvw-border",
|
|
559
|
+
"tvw-border-cw-message-border",
|
|
560
|
+
"tvw-p-4"
|
|
561
|
+
].join(" ");
|
|
562
|
+
wrapper.setAttribute("data-message-id", message.id);
|
|
563
|
+
|
|
564
|
+
// Add text content above component if present (combined text+component response)
|
|
565
|
+
if (message.content && message.content.trim()) {
|
|
566
|
+
const textDiv = document.createElement("div");
|
|
567
|
+
textDiv.className = "tvw-mb-3 tvw-text-sm tvw-leading-relaxed";
|
|
568
|
+
textDiv.innerHTML = transform({
|
|
569
|
+
text: message.content,
|
|
570
|
+
message,
|
|
571
|
+
streaming: Boolean(message.streaming),
|
|
572
|
+
raw: message.rawContent
|
|
573
|
+
});
|
|
574
|
+
wrapper.appendChild(textDiv);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
wrapper.appendChild(componentBubble);
|
|
578
|
+
bubble = wrapper;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
521
584
|
// Fallback to default rendering if plugin returned null or no plugin matched
|
|
522
585
|
if (!bubble) {
|
|
523
586
|
if (message.variant === "reasoning" && message.reasoning) {
|