vanilla-agent 1.2.0 → 1.4.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 CHANGED
@@ -49,6 +49,7 @@ createAgentExperience(inlineHost, {
49
49
  // Floating launcher with runtime updates
50
50
  const controller = initAgentWidget({
51
51
  target: '#launcher-root',
52
+ windowKey: 'chatController', // Optional: stores controller on window.chatController
52
53
  config: {
53
54
  ...DEFAULT_WIDGET_CONFIG,
54
55
  apiUrl: proxyUrl,
@@ -69,12 +70,26 @@ controller.update({
69
70
  });
70
71
  ```
71
72
 
73
+ ### Initialization options
74
+
75
+ `initAgentWidget` accepts the following options:
76
+
77
+ | Option | Type | Description |
78
+ | --- | --- | --- |
79
+ | `target` | `string \| HTMLElement` | CSS selector or element where widget mounts. |
80
+ | `config` | `AgentWidgetConfig` | Widget configuration object (see [Configuration reference](#configuration-reference) below). |
81
+ | `useShadowDom` | `boolean` | Use Shadow DOM for style isolation (default: `true`). |
82
+ | `onReady` | `() => void` | Callback fired when widget is initialized. |
83
+ | `windowKey` | `string` | If provided, stores the controller on `window[windowKey]` for global access. Automatically cleaned up on `destroy()`. |
84
+
72
85
  > **Security note:** When you return HTML from `postprocessMessage`, make sure you sanitise it before injecting into the page. The provided postprocessors (`markdownPostprocessor`, `directivePostprocessor`) do not perform sanitisation.
73
86
 
74
87
 
75
88
  ### Programmatic control
76
89
 
77
- `initAgentWidget` (and `createAgentExperience`) return a controller with `open()`, `close()`, and `toggle()` helpers so you can launch the widget from your own UI elements.
90
+ `initAgentWidget` (and `createAgentExperience`) return a controller with methods to programmatically control the widget.
91
+
92
+ #### Basic controls
78
93
 
79
94
  ```ts
80
95
  const chat = initAgentWidget({
@@ -84,8 +99,129 @@ const chat = initAgentWidget({
84
99
 
85
100
  document.getElementById('open-chat')?.addEventListener('click', () => chat.open())
86
101
  document.getElementById('toggle-chat')?.addEventListener('click', () => chat.toggle())
102
+ document.getElementById('close-chat')?.addEventListener('click', () => chat.close())
103
+ ```
104
+
105
+ #### Message hooks
106
+
107
+ You can programmatically set messages, submit messages, and control voice recognition:
108
+
109
+ ```ts
110
+ const chat = initAgentWidget({
111
+ target: '#launcher-root',
112
+ config: { /* ... */ }
113
+ })
114
+
115
+ // Set a message in the input field (doesn't submit)
116
+ chat.setMessage("Hello, I need help")
117
+
118
+ // Submit a message (uses textarea value if no argument provided)
119
+ chat.submitMessage()
120
+ // Or submit a specific message
121
+ chat.submitMessage("What are your hours?")
122
+
123
+ // Start voice recognition
124
+ chat.startVoiceRecognition()
125
+
126
+ // Stop voice recognition
127
+ chat.stopVoiceRecognition()
128
+ ```
129
+
130
+ All hook methods return `boolean` indicating success (`true`) or failure (`false`). They will automatically open the widget if it's currently closed (when launcher is enabled).
131
+
132
+ #### Clear chat
133
+
134
+ ```ts
135
+ const chat = initAgentWidget({
136
+ target: '#launcher-root',
137
+ config: { /* ... */ }
138
+ })
139
+
140
+ // Clear all messages programmatically
141
+ chat.clearChat()
142
+ ```
143
+
144
+ #### Accessing from window
145
+
146
+ To access the controller globally (e.g., from browser console or external scripts), use the `windowKey` option:
147
+
148
+ ```ts
149
+ const chat = initAgentWidget({
150
+ target: '#launcher-root',
151
+ windowKey: 'chatController', // Stores controller on window.chatController
152
+ config: { /* ... */ }
153
+ })
154
+
155
+ // Now accessible globally
156
+ window.chatController.setMessage("Hello from console!")
157
+ window.chatController.submitMessage("Test message")
158
+ window.chatController.startVoiceRecognition()
159
+ ```
160
+
161
+ #### Message Types
162
+
163
+ The widget uses `AgentWidgetMessage` objects to represent messages in the conversation. You can access these through `postprocessMessage` callbacks or by inspecting the session's message array.
164
+
165
+ ```typescript
166
+ type AgentWidgetMessage = {
167
+ id: string; // Unique message identifier
168
+ role: "user" | "assistant" | "system";
169
+ content: string; // Message text content
170
+ createdAt: string; // ISO timestamp
171
+ streaming?: boolean; // Whether message is still streaming
172
+ variant?: "assistant" | "reasoning" | "tool";
173
+ sequence?: number; // Message ordering
174
+ reasoning?: AgentWidgetReasoning;
175
+ toolCall?: AgentWidgetToolCall;
176
+ tools?: AgentWidgetToolCall[];
177
+ viaVoice?: boolean; // Indicates if user message was sent via voice input
178
+ };
179
+ ```
180
+
181
+ **`viaVoice` field**: Set to `true` when a user message is sent through voice recognition. This allows you to implement voice-specific behaviors, such as automatically reactivating voice recognition after assistant responses. You can check this field in your `postprocessMessage` callback:
182
+
183
+ ```ts
184
+ postprocessMessage: ({ message, text, streaming }) => {
185
+ if (message.role === 'user' && message.viaVoice) {
186
+ console.log('User sent message via voice');
187
+ }
188
+ return text;
189
+ }
87
190
  ```
88
191
 
192
+ Alternatively, manually assign the controller:
193
+
194
+ ```ts
195
+ const chat = initAgentWidget({ /* ... */ })
196
+ window.chatController = chat
197
+ ```
198
+
199
+ ### Events
200
+
201
+ The widget dispatches custom events that you can listen to for integration with your application:
202
+
203
+ #### `vanilla-agent:clear-chat`
204
+
205
+ Dispatched when the user clicks the "Clear chat" button or when `chat.clearChat()` is called programmatically.
206
+
207
+ ```ts
208
+ window.addEventListener("vanilla-agent:clear-chat", (event) => {
209
+ console.log("Chat cleared at:", event.detail.timestamp);
210
+ // Clear your localStorage, reset state, etc.
211
+ });
212
+ ```
213
+
214
+ **Event detail:**
215
+ - `timestamp`: ISO timestamp string of when the chat was cleared
216
+
217
+ **Use cases:**
218
+ - Clear localStorage chat history
219
+ - Reset application state
220
+ - Track analytics events
221
+ - Sync with backend
222
+
223
+ **Note:** The widget automatically clears the `"vanilla-agent-chat-history"` localStorage key by default when chat is cleared. If you set `clearChatHistoryStorageKey` in the config, it will also clear that additional key. You can still listen to this event for additional custom behavior.
224
+
89
225
  ### Travrse adapter
90
226
 
91
227
  This package ships with a Travrse adapter by default. The proxy handles all flow configuration, keeping the client lightweight and flexible.
@@ -130,7 +266,9 @@ The easiest way is to use the automatic installer script. It handles loading CSS
130
266
  theme: {
131
267
  accent: '#2563eb',
132
268
  surface: '#ffffff'
133
- }
269
+ },
270
+ // Optional: configure stream parser for JSON/XML responses
271
+ // streamParser: () => window.AgentWidget.createJsonStreamParser()
134
272
  }
135
273
  };
136
274
  </script>
@@ -175,8 +313,9 @@ For more control, manually load CSS and JavaScript:
175
313
 
176
314
  <!-- Initialize widget -->
177
315
  <script>
178
- window.AgentWidget.initAgentWidget({
316
+ const chatController = window.AgentWidget.initAgentWidget({
179
317
  target: '#vanilla-agent-anchor', // or 'body' for floating launcher
318
+ windowKey: 'chatWidget', // Optional: stores controller on window.chatWidget
180
319
  config: {
181
320
  apiUrl: '/api/chat/dispatch',
182
321
  launcher: {
@@ -187,9 +326,14 @@ For more control, manually load CSS and JavaScript:
187
326
  theme: {
188
327
  accent: '#111827',
189
328
  surface: '#f5f5f5'
190
- }
329
+ },
330
+ // Optional: configure stream parser for JSON/XML responses
331
+ streamParser: window.AgentWidget.createJsonStreamParser // or createXmlParser, createPlainTextParser
191
332
  }
192
333
  });
334
+
335
+ // Controller is now available as window.chatWidget (if windowKey was used)
336
+ // or use the returned chatController variable
193
337
  </script>
194
338
  ```
195
339
 
@@ -206,7 +350,14 @@ Replace `VERSION` with `latest` for auto-updates, or a specific version like `0.
206
350
  - `index.global.js` - Widget JavaScript (IIFE format)
207
351
  - `install.global.js` - Automatic installer script
208
352
 
209
- The script build exposes a `window.AgentWidget` global with `initAgentWidget()` and other exports.
353
+ The script build exposes a `window.AgentWidget` global with `initAgentWidget()` and other exports, including parser functions:
354
+
355
+ - `window.AgentWidget.initAgentWidget()` - Initialize the widget
356
+ - `window.AgentWidget.createPlainTextParser()` - Plain text parser (default)
357
+ - `window.AgentWidget.createJsonStreamParser()` - JSON parser using schema-stream
358
+ - `window.AgentWidget.createXmlParser()` - XML parser
359
+ - `window.AgentWidget.markdownPostprocessor()` - Markdown postprocessor
360
+ - `window.AgentWidget.directivePostprocessor()` - Directive postprocessor
210
361
 
211
362
  ### Using default configuration
212
363
 
@@ -254,12 +405,179 @@ This ensures all configuration values are set to sensible defaults while allowin
254
405
  | `initialMessages` | `AgentWidgetMessage[]` | Seed the conversation transcript. |
255
406
  | `suggestionChips` | `string[]` | Render quick reply buttons above the composer. |
256
407
  | `postprocessMessage` | `(ctx) => string` | Transform message text before it renders (return HTML). Combine with `markdownPostprocessor` for rich output. |
408
+ | `streamParser` | `() => AgentWidgetStreamParser` | Custom stream parser for detecting formats and extracting text from streaming responses. Handles JSON, XML, or custom formats. See [Stream Parser Configuration](#stream-parser-configuration) below. |
409
+ | `clearChatHistoryStorageKey` | `string` | Additional localStorage key to clear when the clear chat button is clicked. The widget automatically clears `"vanilla-agent-chat-history"` by default. Use this option to clear additional keys (e.g., if you're using a custom storage key). |
257
410
  | `formEndpoint` | `string` | Endpoint used by built-in directives (defaults to `/form`). |
258
411
  | `launcherWidth` | `string` | CSS width applied to the floating launcher panel (e.g. `320px`, `90vw`). Defaults to `min(400px, calc(100vw - 24px))`. |
259
412
  | `debug` | `boolean` | Emits verbose logs to `console`. |
260
413
 
261
414
  All options are safe to mutate via `initAgentWidget(...).update(newConfig)`.
262
415
 
416
+ ### Stream Parser Configuration
417
+
418
+ The widget can parse structured responses (JSON, XML, etc.) that stream in chunk by chunk, extracting the `text` field for display. By default, it uses a schema-stream based JSON parser. You can provide a custom parser to handle different formats, structures, or parsing strategies.
419
+
420
+ **Key benefits of the unified stream parser:**
421
+ - **Format detection**: Automatically detects if content matches your parser's format
422
+ - **Extensible**: Handle JSON, XML, or any custom structured format
423
+ - **Incremental parsing**: Extract text as it streams in, not just when complete
424
+
425
+ **Using built-in parsers with ESM/Modules:**
426
+
427
+ ```javascript
428
+ import { initAgentWidget, createPlainTextParser, createJsonStreamParser, createXmlParser } from 'vanilla-agent';
429
+
430
+ const controller = initAgentWidget({
431
+ target: '#chat-root',
432
+ config: {
433
+ apiUrl: '/api/chat/dispatch',
434
+ streamParser: createJsonStreamParser // Use JSON parser
435
+ // Or: createXmlParser for XML, createPlainTextParser for plain text (default)
436
+ }
437
+ });
438
+ ```
439
+
440
+ **Using built-in parsers with CDN Script Tags:**
441
+
442
+ ```html
443
+ <script src="https://cdn.jsdelivr.net/npm/vanilla-agent@latest/dist/index.global.js"></script>
444
+ <script>
445
+ window.AgentWidget.initAgentWidget({
446
+ target: '#chat-root',
447
+ config: {
448
+ apiUrl: '/api/chat/dispatch',
449
+ streamParser: window.AgentWidget.createJsonStreamParser // JSON parser
450
+ // Or: window.AgentWidget.createXmlParser for XML
451
+ // Or: window.AgentWidget.createPlainTextParser for plain text (default)
452
+ }
453
+ });
454
+ </script>
455
+ ```
456
+
457
+ **Using with automatic installer script:**
458
+
459
+ ```html
460
+ <script>
461
+ window.siteAgentConfig = {
462
+ target: 'body',
463
+ config: {
464
+ apiUrl: '/api/chat/dispatch',
465
+ // Note: streamParser must be set after the script loads, or use a function
466
+ streamParser: function() {
467
+ return window.AgentWidget.createJsonStreamParser();
468
+ }
469
+ }
470
+ };
471
+ </script>
472
+ <script src="https://cdn.jsdelivr.net/npm/vanilla-agent@latest/dist/install.global.js"></script>
473
+ ```
474
+
475
+ Alternatively, you can set it after the script loads:
476
+
477
+ ```html
478
+ <script>
479
+ window.siteAgentConfig = {
480
+ target: 'body',
481
+ config: {
482
+ apiUrl: '/api/chat/dispatch'
483
+ }
484
+ };
485
+ </script>
486
+ <script src="https://cdn.jsdelivr.net/npm/vanilla-agent@latest/dist/install.global.js"></script>
487
+ <script>
488
+ // Set parser after AgentWidget is loaded
489
+ if (window.siteAgentConfig && window.AgentWidget) {
490
+ window.siteAgentConfig.config.streamParser = window.AgentWidget.createJsonStreamParser;
491
+ }
492
+ </script>
493
+ ```
494
+
495
+ **Custom JSON parser example:**
496
+
497
+ ```javascript
498
+ const jsonParser = () => {
499
+ let extractedText = null;
500
+
501
+ return {
502
+ // Extract text field from JSON as it streams in
503
+ // Return null if not JSON or text not available yet
504
+ processChunk(accumulatedContent) {
505
+ const trimmed = accumulatedContent.trim();
506
+ // Return null if not JSON format
507
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
508
+ return null;
509
+ }
510
+
511
+ const match = accumulatedContent.match(/"text"\s*:\s*"([^"]*(?:\\.[^"]*)*)"/);
512
+ if (match) {
513
+ extractedText = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n');
514
+ return extractedText;
515
+ }
516
+ return null;
517
+ },
518
+
519
+ getExtractedText() {
520
+ return extractedText;
521
+ }
522
+ };
523
+ };
524
+
525
+ const controller = initAgentWidget({
526
+ target: '#chat-root',
527
+ config: {
528
+ apiUrl: '/api/chat/dispatch',
529
+ streamParser: jsonParser
530
+ }
531
+ });
532
+ ```
533
+
534
+ **Custom XML parser example:**
535
+
536
+ ```javascript
537
+ const xmlParser = () => {
538
+ let extractedText = null;
539
+
540
+ return {
541
+ processChunk(accumulatedContent) {
542
+ // Return null if not XML format
543
+ if (!accumulatedContent.trim().startsWith('<')) {
544
+ return null;
545
+ }
546
+
547
+ // Extract text from <text>...</text> tags
548
+ const match = accumulatedContent.match(/<text[^>]*>([\s\S]*?)<\/text>/);
549
+ if (match) {
550
+ extractedText = match[1];
551
+ return extractedText;
552
+ }
553
+ return null;
554
+ },
555
+
556
+ getExtractedText() {
557
+ return extractedText;
558
+ }
559
+ };
560
+ };
561
+ ```
562
+
563
+ **Parser interface:**
564
+
565
+ ```typescript
566
+ interface AgentWidgetStreamParser {
567
+ // Process a chunk and return extracted text (if available)
568
+ // Return null if the content doesn't match this parser's format or text is not yet available
569
+ processChunk(accumulatedContent: string): Promise<string | null> | string | null;
570
+
571
+ // Get the currently extracted text (may be partial)
572
+ getExtractedText(): string | null;
573
+
574
+ // Optional cleanup when parsing is complete
575
+ close?(): Promise<void> | void;
576
+ }
577
+ ```
578
+
579
+ The parser's `processChunk` method is called for each chunk. If the content matches your parser's format, return the extracted text. If it doesn't match (or text isn't available yet), return `null` and the content will be treated as plain text. This allows you to display the text value as it streams in without showing raw structured characters.
580
+
263
581
  ### Optional proxy server
264
582
 
265
583
  The proxy server handles flow configuration and forwards requests to Travrse. You can configure it in three ways: