@runtypelabs/persona 3.30.0 → 3.31.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 +15 -2602
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-B_Qazlm4.d.cts → types-quh7NmYD.d.cts} +9 -0
- package/dist/animations/{types-B_Qazlm4.d.ts → types-quh7NmYD.d.ts} +9 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/index.cjs +46 -46
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.global.js +55 -55
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +45 -45
- package/dist/index.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/smart-dom-reader.d.cts +50 -0
- package/dist/smart-dom-reader.d.ts +50 -0
- package/dist/theme-editor.cjs +39 -39
- package/dist/theme-editor.d.cts +50 -0
- package/dist/theme-editor.d.ts +50 -0
- package/dist/theme-editor.js +39 -39
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/widget.css +19 -0
- package/package.json +1 -1
- package/src/client.ts +6 -0
- package/src/components/approval-bubble.test.ts +52 -0
- package/src/components/approval-bubble.ts +20 -0
- package/src/components/panel.ts +7 -1
- package/src/defaults.ts +14 -0
- package/src/generated/runtype-openapi-contract.ts +4 -0
- package/src/session.test.ts +62 -0
- package/src/session.ts +27 -0
- package/src/styles/widget.css +19 -0
- package/src/theme-reference.ts +1 -1
- package/src/types.ts +50 -0
- package/src/ui.scroll.test.ts +383 -0
- package/src/ui.ts +390 -40
- package/src/utils/auto-follow.test.ts +91 -0
- package/src/utils/auto-follow.ts +68 -0
package/README.md
CHANGED
|
@@ -38,8 +38,7 @@ createAgentExperience(inlineHost, {
|
|
|
38
38
|
apiUrl: proxyUrl,
|
|
39
39
|
launcher: { enabled: false },
|
|
40
40
|
theme: {
|
|
41
|
-
|
|
42
|
-
accent: '#2563eb'
|
|
41
|
+
semantic: { colors: { accent: '#2563eb' } }
|
|
43
42
|
},
|
|
44
43
|
suggestionChips: ['What can you do?', 'Show API docs'],
|
|
45
44
|
postprocessMessage: ({ text }) => markdownPostprocessor(text)
|
|
@@ -62,7 +61,9 @@ const controller = initAgentWidget({
|
|
|
62
61
|
|
|
63
62
|
// Runtime theme update
|
|
64
63
|
document.querySelector('#dark-mode')?.addEventListener('click', () => {
|
|
65
|
-
controller.update({
|
|
64
|
+
controller.update({
|
|
65
|
+
theme: { semantic: { colors: { surface: '#0f172a', primary: '#f8fafc' } } }
|
|
66
|
+
});
|
|
66
67
|
});
|
|
67
68
|
|
|
68
69
|
// Docked panel that wraps a concrete workspace container
|
|
@@ -90,7 +91,7 @@ const docked = initAgentWidget({
|
|
|
90
91
|
| Option | Type | Description |
|
|
91
92
|
| --- | --- | --- |
|
|
92
93
|
| `target` | `string \| HTMLElement` | CSS selector or element where widget mounts. |
|
|
93
|
-
| `config` | `AgentWidgetConfig` | Widget configuration object (see [Configuration
|
|
94
|
+
| `config` | `AgentWidgetConfig` | Widget configuration object (see the [Configuration Reference](./docs/CONFIGURATION-REFERENCE.md)). |
|
|
94
95
|
| `useShadowDom` | `boolean` | Use Shadow DOM for style isolation (default: `true`). |
|
|
95
96
|
| `onChatReady` | `() => void` | Callback fired when the widget is initialized and its API is callable. |
|
|
96
97
|
| `onReady` | `() => void` | **Deprecated** alias of `onChatReady`; still works, removed in the next major. |
|
|
@@ -106,2606 +107,18 @@ With **`dock.reveal: 'resize'`** (default), a **closed** dock uses a **`0px`** c
|
|
|
106
107
|
|
|
107
108
|
> **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.
|
|
108
109
|
|
|
110
|
+
### Documentation
|
|
109
111
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`initAgentWidget` (and `createAgentExperience`) return a controller with methods to programmatically control the widget.
|
|
113
|
-
|
|
114
|
-
#### Basic controls
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
const chat = initAgentWidget({
|
|
118
|
-
target: '#launcher-root',
|
|
119
|
-
config: { /* ... */ }
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
document.getElementById('open-chat')?.addEventListener('click', () => chat.open())
|
|
123
|
-
document.getElementById('toggle-chat')?.addEventListener('click', () => chat.toggle())
|
|
124
|
-
document.getElementById('close-chat')?.addEventListener('click', () => chat.close())
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
#### Message hooks
|
|
128
|
-
|
|
129
|
-
You can programmatically set messages, submit messages, and control voice recognition:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
const chat = initAgentWidget({
|
|
133
|
-
target: '#launcher-root',
|
|
134
|
-
config: { /* ... */ }
|
|
135
|
-
})
|
|
136
|
-
|
|
137
|
-
// Set a message in the input field (doesn't submit)
|
|
138
|
-
chat.setMessage("Hello, I need help")
|
|
139
|
-
|
|
140
|
-
// Submit a message (uses textarea value if no argument provided)
|
|
141
|
-
chat.submitMessage()
|
|
142
|
-
// Or submit a specific message
|
|
143
|
-
chat.submitMessage("What are your hours?")
|
|
144
|
-
|
|
145
|
-
// Start voice recognition
|
|
146
|
-
chat.startVoiceRecognition()
|
|
147
|
-
|
|
148
|
-
// Stop voice recognition
|
|
149
|
-
chat.stopVoiceRecognition()
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
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).
|
|
153
|
-
|
|
154
|
-
#### Clear chat
|
|
155
|
-
|
|
156
|
-
```ts
|
|
157
|
-
const chat = initAgentWidget({
|
|
158
|
-
target: '#launcher-root',
|
|
159
|
-
config: { /* ... */ }
|
|
160
|
-
})
|
|
161
|
-
|
|
162
|
-
// Clear all messages programmatically
|
|
163
|
-
chat.clearChat()
|
|
164
|
-
```
|
|
165
|
-
|
|
166
|
-
#### Message Injection
|
|
167
|
-
|
|
168
|
-
Inject messages programmatically from external sources like tool call responses, system events, or third-party integrations. This is useful when local tools need to push results back into the conversation.
|
|
169
|
-
|
|
170
|
-
```ts
|
|
171
|
-
const chat = initAgentWidget({
|
|
172
|
-
target: '#launcher-root',
|
|
173
|
-
config: { /* ... */ }
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
// Simple message injection
|
|
177
|
-
chat.injectAssistantMessage({
|
|
178
|
-
content: 'Here are your search results...'
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// User message injection
|
|
182
|
-
chat.injectUserMessage({
|
|
183
|
-
content: 'Add to cart'
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// System context injection
|
|
187
|
-
chat.injectSystemMessage({
|
|
188
|
-
content: '[Context updated]',
|
|
189
|
-
llmContent: 'User is viewing product page for iPhone 15 Pro'
|
|
190
|
-
});
|
|
191
|
-
```
|
|
192
|
-
|
|
193
|
-
**Dual-Content Messages (llmContent)**
|
|
194
|
-
|
|
195
|
-
Use `llmContent` to show different content to the user versus what gets sent to the LLM. This is useful for:
|
|
196
|
-
- **Token efficiency**: Show rich content to users while sending concise summaries to the LLM
|
|
197
|
-
- **Sensitive data redaction**: Display PII to users while hiding it from the LLM
|
|
198
|
-
- **Context injection**: Provide detailed LLM context with minimal UI footprint
|
|
199
|
-
|
|
200
|
-
```ts
|
|
201
|
-
// Example: Tool callback that injects search results
|
|
202
|
-
async function handleProductSearch(query: string) {
|
|
203
|
-
const results = await searchProducts(query);
|
|
204
|
-
|
|
205
|
-
// User sees full product details with images and prices
|
|
206
|
-
// LLM receives a concise summary to save tokens
|
|
207
|
-
chat.injectAssistantMessage({
|
|
208
|
-
content: `**Found ${results.length} products:**
|
|
209
|
-
${results.map(p => `- ${p.name} - $${p.price} (SKU: ${p.sku})`).join('\n')}`,
|
|
210
|
-
|
|
211
|
-
llmContent: `[Search results: ${results.length} products found, price range $${results.minPrice}-$${results.maxPrice}]`
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// Example: Redacting sensitive information
|
|
216
|
-
chat.injectAssistantMessage({
|
|
217
|
-
// User sees their order confirmation with details
|
|
218
|
-
content: `Your order #12345 has been placed!
|
|
219
|
-
- Card ending in 4242
|
|
220
|
-
- Shipping to: 123 Main St, Anytown, USA`,
|
|
221
|
-
|
|
222
|
-
// LLM only knows an order was placed (no PII)
|
|
223
|
-
llmContent: '[Order confirmation displayed to user]'
|
|
224
|
-
});
|
|
225
|
-
```
|
|
226
|
-
|
|
227
|
-
**Content Priority**
|
|
228
|
-
|
|
229
|
-
When messages are sent to the API, content is resolved in this priority order:
|
|
230
|
-
1. `contentParts` - Multi-modal content (images, files)
|
|
231
|
-
2. `llmContent` - Explicit LLM-specific content
|
|
232
|
-
3. `content` - Display content as fallback
|
|
233
|
-
|
|
234
|
-
**Streaming Updates**
|
|
235
|
-
|
|
236
|
-
For long-running operations, use the same message ID to update content:
|
|
237
|
-
|
|
238
|
-
```ts
|
|
239
|
-
const messageId = 'search-123';
|
|
240
|
-
|
|
241
|
-
// Show loading state
|
|
242
|
-
chat.injectAssistantMessage({
|
|
243
|
-
id: messageId,
|
|
244
|
-
content: 'Searching...',
|
|
245
|
-
streaming: true
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
// Update with results
|
|
249
|
-
chat.injectAssistantMessage({
|
|
250
|
-
id: messageId,
|
|
251
|
-
content: 'Found 5 results...',
|
|
252
|
-
llmContent: '[5 search results]',
|
|
253
|
-
streaming: false
|
|
254
|
-
});
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
**Component Directives (`injectComponentDirective`)**
|
|
258
|
-
|
|
259
|
-
When you've registered a custom component via `componentRegistry.register(...)`, inject an assistant message that renders that component using the same path Persona uses for streamed JSON directives:
|
|
260
|
-
|
|
261
|
-
```ts
|
|
262
|
-
import { componentRegistry } from '@runtypelabs/persona';
|
|
263
|
-
import { DynamicForm } from './components';
|
|
264
|
-
|
|
265
|
-
componentRegistry.register('DynamicForm', DynamicForm);
|
|
266
|
-
|
|
267
|
-
chat.injectComponentDirective({
|
|
268
|
-
component: 'DynamicForm',
|
|
269
|
-
props: {
|
|
270
|
-
title: 'Book a demo',
|
|
271
|
-
fields: [
|
|
272
|
-
{ label: 'Name', type: 'text', required: true },
|
|
273
|
-
{ label: 'Email', type: 'email', required: true }
|
|
274
|
-
],
|
|
275
|
-
submit_text: 'Request meeting'
|
|
276
|
-
},
|
|
277
|
-
text: 'Share your details to book a demo.',
|
|
278
|
-
llmContent: '[Showed booking form]' // optional, redacted version for the LLM
|
|
279
|
-
});
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
The helper sets `content` to `text`, `rawContent` to the canonical directive JSON, and forwards `llmContent`. Useful for previews, replays, debug buttons, and local tools that should render a component instead of plain text.
|
|
283
|
-
|
|
284
|
-
If you already have a serialized directive, you can pass it through `rawContent` directly on any inject method:
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
chat.injectAssistantMessage({
|
|
288
|
-
content: 'Booking form',
|
|
289
|
-
rawContent: JSON.stringify({
|
|
290
|
-
text: 'Booking form',
|
|
291
|
-
component: 'DynamicForm',
|
|
292
|
-
props: { /* ... */ }
|
|
293
|
-
}),
|
|
294
|
-
llmContent: '[Showed booking form]'
|
|
295
|
-
});
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
See [docs/MESSAGE-INJECTION.md](./docs/MESSAGE-INJECTION.md#component-directive-injection) for the full reference.
|
|
299
|
-
|
|
300
|
-
#### Event Stream Control
|
|
301
|
-
|
|
302
|
-
When the `showEventStreamToggle` feature flag is enabled, you can programmatically control the event stream inspector panel:
|
|
303
|
-
|
|
304
|
-
```ts
|
|
305
|
-
const chat = initAgentWidget({
|
|
306
|
-
target: '#launcher-root',
|
|
307
|
-
config: {
|
|
308
|
-
apiUrl: '/api/chat/dispatch',
|
|
309
|
-
features: { showEventStreamToggle: true }
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
// Open the event stream panel
|
|
314
|
-
chat.showEventStream()
|
|
315
|
-
|
|
316
|
-
// Close the event stream panel
|
|
317
|
-
chat.hideEventStream()
|
|
318
|
-
|
|
319
|
-
// Check if the event stream panel is currently visible
|
|
320
|
-
chat.isEventStreamVisible() // returns boolean
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
These methods are no-ops if `showEventStreamToggle` is not enabled.
|
|
324
|
-
|
|
325
|
-
#### Input focus control
|
|
326
|
-
|
|
327
|
-
Focus the chat input programmatically:
|
|
328
|
-
|
|
329
|
-
```ts
|
|
330
|
-
const chat = initAgentWidget({
|
|
331
|
-
target: '#chat-root',
|
|
332
|
-
config: { apiUrl: '/api/chat/dispatch' }
|
|
333
|
-
})
|
|
334
|
-
|
|
335
|
-
// Focus the input (returns true if successful, false if panel is closed or unavailable)
|
|
336
|
-
chat.focusInput()
|
|
337
|
-
```
|
|
338
|
-
|
|
339
|
-
In launcher mode, `focusInput()` returns `false` when the panel is closed and does not auto-open it. Use `chat.open()` first if you want to open and focus in one flow.
|
|
340
|
-
|
|
341
|
-
#### Accessing from window
|
|
342
|
-
|
|
343
|
-
To access the controller globally (e.g., from browser console or external scripts), use the `windowKey` option:
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
const chat = initAgentWidget({
|
|
347
|
-
target: '#launcher-root',
|
|
348
|
-
windowKey: 'chatController', // Stores controller on window.chatController
|
|
349
|
-
config: { /* ... */ }
|
|
350
|
-
})
|
|
351
|
-
|
|
352
|
-
// Now accessible globally
|
|
353
|
-
window.chatController.setMessage("Hello from console!")
|
|
354
|
-
window.chatController.submitMessage("Test message")
|
|
355
|
-
window.chatController.startVoiceRecognition()
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
When using the automatic installer script (`install.global.js`), see [Programmatic access with the installer](#programmatic-access-with-the-installer) for additional approaches including the `onChatReady` callback and `persona:chat-ready` event.
|
|
359
|
-
|
|
360
|
-
#### Message Types
|
|
361
|
-
|
|
362
|
-
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.
|
|
363
|
-
|
|
364
|
-
```typescript
|
|
365
|
-
type AgentWidgetMessage = {
|
|
366
|
-
id: string; // Unique message identifier
|
|
367
|
-
role: "user" | "assistant" | "system";
|
|
368
|
-
content: string; // Message text content
|
|
369
|
-
createdAt: string; // ISO timestamp
|
|
370
|
-
streaming?: boolean; // Whether message is still streaming
|
|
371
|
-
variant?: "assistant" | "reasoning" | "tool";
|
|
372
|
-
sequence?: number; // Message ordering
|
|
373
|
-
reasoning?: AgentWidgetReasoning;
|
|
374
|
-
toolCall?: AgentWidgetToolCall;
|
|
375
|
-
tools?: AgentWidgetToolCall[];
|
|
376
|
-
viaVoice?: boolean; // Indicates if user message was sent via voice input
|
|
377
|
-
};
|
|
378
|
-
```
|
|
379
|
-
|
|
380
|
-
**`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:
|
|
381
|
-
|
|
382
|
-
```ts
|
|
383
|
-
postprocessMessage: ({ message, text, streaming }) => {
|
|
384
|
-
if (message.role === 'user' && message.viaVoice) {
|
|
385
|
-
console.log('User sent message via voice');
|
|
386
|
-
}
|
|
387
|
-
return text;
|
|
388
|
-
}
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
Alternatively, manually assign the controller:
|
|
392
|
-
|
|
393
|
-
```ts
|
|
394
|
-
const chat = initAgentWidget({ /* ... */ })
|
|
395
|
-
window.chatController = chat
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
### Enriched DOM context
|
|
399
|
-
|
|
400
|
-
Use `collectEnrichedPageContext` and `formatEnrichedContext` to summarize the visible page for tools or metadata (selectors, roles, text, and optional structured card summaries). By default the collector runs in **structured** mode: it gathers candidates, scores them with built-in `ParseRule` definitions in `defaultParseRules` (product/result-style cards), suppresses redundant descendants, then applies `maxElements`. Pass `options: { mode: "simple" }` for the legacy path (traverse with an early cap only, no rules or `formattedSummary`).
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
import {
|
|
404
|
-
collectEnrichedPageContext,
|
|
405
|
-
formatEnrichedContext,
|
|
406
|
-
defaultParseRules
|
|
407
|
-
} from '@runtypelabs/persona';
|
|
408
|
-
|
|
409
|
-
const elements = collectEnrichedPageContext({
|
|
410
|
-
options: {
|
|
411
|
-
mode: 'structured',
|
|
412
|
-
maxElements: 80,
|
|
413
|
-
excludeSelector: '.persona-host',
|
|
414
|
-
maxTextLength: 200,
|
|
415
|
-
visibleOnly: true
|
|
416
|
-
},
|
|
417
|
-
rules: defaultParseRules
|
|
418
|
-
});
|
|
419
|
-
|
|
420
|
-
const pageContext = formatEnrichedContext(elements);
|
|
421
|
-
// Structured mode: "Structured summaries:" blocks for matched cards, then grouped interactivity sections.
|
|
422
|
-
```
|
|
423
|
-
|
|
424
|
-
- Omit both `options` and `rules` → structured defaults (`defaultParseRules`, sensible limits).
|
|
425
|
-
- `options: { mode: 'structured' }` → explicit structured behavior (same as default).
|
|
426
|
-
- `rules: [...]` → custom rules with default options.
|
|
427
|
-
- `options: { mode: 'simple' }` → no relation-based scoring or rule-owned formatting. If you also pass `rules`, they are ignored and a console warning is emitted.
|
|
428
|
-
|
|
429
|
-
Pass `formatEnrichedContext(elements, { mode: 'simple' })` to ignore any `formattedSummary` fields on elements (for example when re-formatting data collected earlier).
|
|
430
|
-
|
|
431
|
-
**Where things live:** `defaultParseRules` and the rule/config types are part of the public package API — import them from `@runtypelabs/persona` (same entry as `collectEnrichedPageContext`). Exported names you will use most often:
|
|
432
|
-
|
|
433
|
-
| Export | Role |
|
|
434
|
-
| --- | --- |
|
|
435
|
-
| `defaultParseRules` | Built-in `ParseRule[]` (commerce-style cards + generic result rows). |
|
|
436
|
-
| `ParseRule` | Type for a custom rule: `id`, `scoreElement`, optional `shouldSuppressDescendant`, optional `formatSummary`. |
|
|
437
|
-
| `RuleScoringContext` | Argument to rule hooks (`doc`, `maxTextLength`). |
|
|
438
|
-
| `ParseOptionsConfig` | `mode`, `maxElements`, `maxCandidates`, `excludeSelector`, `maxTextLength`, `visibleOnly`, `root`. |
|
|
439
|
-
| `DomContextOptions` | What you pass to `collectEnrichedPageContext` (`options`, `rules`, plus legacy top-level limits). |
|
|
440
|
-
| `FormatEnrichedContextOptions` | Second argument to `formatEnrichedContext` (`mode`). |
|
|
441
|
-
| `EnrichedPageElement` | One collected node; optional `formattedSummary` in structured mode. |
|
|
442
|
-
|
|
443
|
-
Use **Go to definition** (or open `node_modules/@runtypelabs/persona/dist/index.d.ts` after install) for the authoritative field list and JSDoc. Implementation source in this repo: `packages/widget/src/utils/dom-context.ts`.
|
|
444
|
-
|
|
445
|
-
Custom rule sketch:
|
|
446
|
-
|
|
447
|
-
```ts
|
|
448
|
-
import type { ParseRule } from '@runtypelabs/persona';
|
|
449
|
-
|
|
450
|
-
const myRules: ParseRule[] = [
|
|
451
|
-
{
|
|
452
|
-
id: 'kpi-tile',
|
|
453
|
-
scoreElement: (el, enriched, ctx) =>
|
|
454
|
-
el.classList.contains('kpi-tile') ? 2000 : 0,
|
|
455
|
-
formatSummary: (el, enriched, ctx) =>
|
|
456
|
-
el.classList.contains('kpi-tile')
|
|
457
|
-
? `${enriched.text.trim()}\nselector: ${enriched.selector}`
|
|
458
|
-
: null
|
|
459
|
-
}
|
|
460
|
-
];
|
|
461
|
-
```
|
|
462
|
-
|
|
463
|
-
#### Optional: smart-dom-reader provider
|
|
464
|
-
|
|
465
|
-
The default reader above is a zero-dependency `TreeWalker` and **does not pierce shadow
|
|
466
|
-
DOM**. For pages built from web components, an optional provider backed by a
|
|
467
|
-
vendored copy of [`@mcp-b/smart-dom-reader`](https://github.com/WebMCP-org/npm-packages/tree/main/packages/smart-dom-reader)
|
|
468
|
-
ships as a **separate entry point**, `@runtypelabs/persona/smart-dom-reader`. It is **not**
|
|
469
|
-
imported by the main bundle, so consumers who never import this subpath pay nothing — no
|
|
470
|
-
extra install, no bundle weight, no IIFE/CDN impact.
|
|
471
|
-
|
|
472
|
-
It adds, over the default reader: **Shadow-DOM piercing**, form grouping, and page
|
|
473
|
-
landmarks/state — while still emitting Persona's `EnrichedPageElement[]` shape so it
|
|
474
|
-
formats and flows through the same pipeline.
|
|
475
|
-
|
|
476
|
-
```ts
|
|
477
|
-
import initAgentWidget from '@runtypelabs/persona';
|
|
478
|
-
import { createSmartDomReaderContextProvider } from '@runtypelabs/persona/smart-dom-reader';
|
|
479
|
-
|
|
480
|
-
initAgentWidget({
|
|
481
|
-
// ...config
|
|
482
|
-
contextProviders: [
|
|
483
|
-
createSmartDomReaderContextProvider({
|
|
484
|
-
// 'interactive' (default) | 'full' — full adds semantic content AND is required
|
|
485
|
-
// for shadow-DOM piercing (shadow descendants surface only in full mode).
|
|
486
|
-
mode: 'full',
|
|
487
|
-
contextKey: 'pageContext', // key under payload.context (default)
|
|
488
|
-
// root: document.querySelector('main') // optional: scope to a subtree, skip chrome
|
|
489
|
-
})
|
|
490
|
-
]
|
|
491
|
-
});
|
|
492
|
-
```
|
|
493
|
-
|
|
494
|
-
`contextProviders` are honored on the **agent** send path (`buildAgentPayload` merges each
|
|
495
|
-
provider's result into `payload.context` on every request). If you drive the legacy
|
|
496
|
-
flow/`buildPayload` path, call `collectSmartDomContext()` yourself and inject the result.
|
|
497
|
-
|
|
498
|
-
You can also use the pieces directly:
|
|
499
|
-
|
|
500
|
-
```ts
|
|
501
|
-
import {
|
|
502
|
-
collectSmartDomContext, // → EnrichedPageElement[] (parity with collectEnrichedPageContext)
|
|
503
|
-
smartDomResultToEnriched // pure mapper: SmartDOMResult → EnrichedPageElement[]
|
|
504
|
-
} from '@runtypelabs/persona/smart-dom-reader';
|
|
505
|
-
import { formatEnrichedContext } from '@runtypelabs/persona';
|
|
506
|
-
|
|
507
|
-
const pageContext = formatEnrichedContext(collectSmartDomContext({ mode: 'full' }));
|
|
508
|
-
```
|
|
509
|
-
|
|
510
|
-
Both `collectSmartDomContext()` and `createSmartDomReaderContextProvider()` accept a
|
|
511
|
-
`root` element to scope extraction to a subtree (parity with `collectEnrichedPageContext`'s
|
|
512
|
-
`root`) — useful to read only your main content region and skip nav/sidebars. Shadow DOM
|
|
513
|
-
inside the subtree is still pierced.
|
|
514
|
-
|
|
515
|
-
> **Actionability caveat.** Persona's click loop (`utils/actions.ts`) drives
|
|
516
|
-
> `document.querySelector`, which cannot pierce shadow roots or evaluate XPath. The adapter
|
|
517
|
-
> therefore prefers plain-CSS selectors; elements reachable only via shadow-piercing or
|
|
518
|
-
> XPath selectors are surfaced to the model as **context only** and are **not clickable**
|
|
519
|
-
> through the current `message_and_click` handler.
|
|
520
|
-
|
|
521
|
-
> **Why vendored, not a dependency.** Every published version of `@mcp-b/smart-dom-reader`
|
|
522
|
-
> (2.3.1–3.0.0) is mis-published — its `package.json` points to `dist/index.js` /
|
|
523
|
-
> `dist/index.d.ts` while the build only ships `.mjs` / `.d.mts`, so it cannot be imported
|
|
524
|
-
> by name in Node or any bundler. The library (MIT, zero-dep) is therefore vendored under
|
|
525
|
-
> `packages/widget/src/vendor/smart-dom-reader/`; see that directory's `README.md` for
|
|
526
|
-
> provenance and update steps. Once upstream republishes correctly this can revert to a
|
|
527
|
-
> normal optional peer dependency.
|
|
528
|
-
|
|
529
|
-
### WebMCP page tools
|
|
530
|
-
|
|
531
|
-
When `webmcp: { enabled: true }` is set, the widget consumes tools the page
|
|
532
|
-
registers on `document.modelContext` (the [WebMCP](https://github.com/webmachinelearning/webmcp)
|
|
533
|
-
producer surface), snapshots them into each request as `clientTools[]`, runs the
|
|
534
|
-
agent's calls on the page, and gates each behind a confirm bubble (override with
|
|
535
|
-
`autoApprove` / `onConfirm`).
|
|
536
|
-
|
|
537
|
-
```ts
|
|
538
|
-
initAgentWidget({
|
|
539
|
-
// ...config
|
|
540
|
-
webmcp: {
|
|
541
|
-
enabled: true,
|
|
542
|
-
autoApprove: (info) => READ_ONLY_TOOLS.has(info.toolName),
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
```
|
|
546
|
-
|
|
547
|
-
**Give your tools user-facing names.** The approval bubble (and any custom
|
|
548
|
-
`onConfirm` handler, via `info.title`) shows a human-readable label for the
|
|
549
|
-
tool being called. Declare it once at registration with the WebMCP spec's
|
|
550
|
-
top-level `title` field:
|
|
551
|
-
|
|
552
|
-
```ts
|
|
553
|
-
document.modelContext.registerTool({
|
|
554
|
-
name: "add_to_cart",
|
|
555
|
-
title: "Add to Cart", // shown to users in the approval bubble
|
|
556
|
-
description: "Add products to the shopping cart. IMPORTANT: …", // agent-facing
|
|
557
|
-
inputSchema: { /* … */ },
|
|
558
|
-
execute: async (args) => { /* … */ },
|
|
559
|
-
});
|
|
560
|
-
```
|
|
561
|
-
|
|
562
|
-
Tools without a `title` get a label derived from their name
|
|
563
|
-
(`add_to_cart` → "Add to cart"). The agent-facing `description` is never shown
|
|
564
|
-
as the headline — it sits behind the approval bubble's "Show details" toggle.
|
|
565
|
-
See [Tool Calls & Approvals](#tool-calls--approvals) for the full summary-line
|
|
566
|
-
resolution order and `approval.formatDescription` for parameter-aware copy.
|
|
567
|
-
(The legacy `annotations.title` is *not* read — the polyfill's consumer surface
|
|
568
|
-
doesn't expose annotations; use top-level `title`.)
|
|
569
|
-
|
|
570
|
-
**Using WebMCP against a non-Runtype backend (e.g. the Vercel AI SDK)?** The
|
|
571
|
-
widget's WebMCP loop expects Runtype's proxy wire protocol (a `step_await` pause
|
|
572
|
-
→ `/resume` round-trip). See
|
|
573
|
-
[`docs/webmcp-without-runtype.md`](../../docs/webmcp-without-runtype.md) for the
|
|
574
|
-
exact contract and two integration paths, with a runnable Next.js example at
|
|
575
|
-
[`examples/ai-sdk-webmcp/`](../../examples/ai-sdk-webmcp/).
|
|
576
|
-
|
|
577
|
-
### DOM Events
|
|
578
|
-
|
|
579
|
-
The widget dispatches custom DOM events that you can listen to for integration with your application:
|
|
580
|
-
|
|
581
|
-
#### `persona:clear-chat`
|
|
582
|
-
|
|
583
|
-
Dispatched when the user clicks the "Clear chat" button or when `chat.clearChat()` is called programmatically.
|
|
584
|
-
|
|
585
|
-
```ts
|
|
586
|
-
window.addEventListener("persona:clear-chat", (event) => {
|
|
587
|
-
console.log("Chat cleared at:", event.detail.timestamp);
|
|
588
|
-
// Clear your localStorage, reset state, etc.
|
|
589
|
-
});
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
**Event detail:**
|
|
593
|
-
- `timestamp`: ISO timestamp string of when the chat was cleared
|
|
594
|
-
|
|
595
|
-
**Use cases:**
|
|
596
|
-
- Clear localStorage chat history
|
|
597
|
-
- Reset application state
|
|
598
|
-
- Track analytics events
|
|
599
|
-
- Sync with backend
|
|
600
|
-
|
|
601
|
-
**Note:** The widget automatically clears the `"persona-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.
|
|
602
|
-
|
|
603
|
-
#### `persona:showEventStream` / `persona:hideEventStream`
|
|
604
|
-
|
|
605
|
-
Dispatched to programmatically open or close the event stream panel. Requires `showEventStreamToggle: true` in the widget config.
|
|
606
|
-
|
|
607
|
-
```ts
|
|
608
|
-
// Open the event stream panel on all widget instances
|
|
609
|
-
window.dispatchEvent(new CustomEvent('persona:showEventStream'))
|
|
610
|
-
|
|
611
|
-
// Close the event stream panel on all widget instances
|
|
612
|
-
window.dispatchEvent(new CustomEvent('persona:hideEventStream'))
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
**Instance scoping:** When multiple widget instances exist on the same page, use the `instanceId` detail to target a specific one. For `createAgentExperience`, the `instanceId` is the original `id` of the mount element. For `initAgentWidget`, it's the `id` of the target element.
|
|
616
|
-
|
|
617
|
-
```ts
|
|
618
|
-
// Target only the widget mounted on #inline-widget
|
|
619
|
-
window.dispatchEvent(new CustomEvent('persona:showEventStream', {
|
|
620
|
-
detail: { instanceId: 'inline-widget' }
|
|
621
|
-
}))
|
|
622
|
-
|
|
623
|
-
// Events with a non-matching instanceId are ignored
|
|
624
|
-
window.dispatchEvent(new CustomEvent('persona:showEventStream', {
|
|
625
|
-
detail: { instanceId: 'wrong-id' }
|
|
626
|
-
}))
|
|
627
|
-
// ^ No effect — no widget has this instanceId
|
|
628
|
-
```
|
|
629
|
-
|
|
630
|
-
#### `persona:focusInput`
|
|
631
|
-
|
|
632
|
-
Dispatched to programmatically focus the chat input on a widget instance.
|
|
633
|
-
|
|
634
|
-
```ts
|
|
635
|
-
// Focus input on all widget instances
|
|
636
|
-
window.dispatchEvent(new CustomEvent('persona:focusInput'))
|
|
637
|
-
|
|
638
|
-
// Focus input on a specific instance
|
|
639
|
-
window.dispatchEvent(new CustomEvent('persona:focusInput', {
|
|
640
|
-
detail: { instanceId: 'inline-widget' }
|
|
641
|
-
}))
|
|
642
|
-
```
|
|
643
|
-
|
|
644
|
-
**Instance scoping:** Same as `persona:showEventStream` — use `detail.instanceId` to target a specific widget. Without `instanceId`, all instances receive the event.
|
|
645
|
-
|
|
646
|
-
#### `persona:chat-ready`
|
|
647
|
-
|
|
648
|
-
Dispatched on `window` by the automatic installer script (`install.global.js`) when the widget is initialized and its controller API is callable. The `event.detail` contains the `AgentWidgetInitHandle` (the same object returned by `initAgentWidget()`). In a deferred install (the default floating-launcher case) this fires after the user first opens the panel; in an eager install it fires on page load.
|
|
649
|
-
|
|
650
|
-
```ts
|
|
651
|
-
window.addEventListener('persona:chat-ready', (e) => {
|
|
652
|
-
const handle = e.detail;
|
|
653
|
-
handle.on('message:sent', (msg) => console.log(msg));
|
|
654
|
-
handle.open();
|
|
655
|
-
});
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
The installer also dispatches sibling lifecycle events for diagnostics and analytics:
|
|
659
|
-
|
|
660
|
-
| Event | `detail` | Fires |
|
|
661
|
-
| --- | --- | --- |
|
|
662
|
-
| `persona:script-load` | `{ version }` | the installer script executed (before any loading) |
|
|
663
|
-
| `persona:launcher-shown` | `{ deferred, element? }` | the floating launcher painted on the page (page-load time) |
|
|
664
|
-
| `persona:chat-ready` | the widget handle | the widget is initialized and its API is callable |
|
|
665
|
-
| `persona:error` | `{ phase, error }` | a load step (`css` / `bundle` / `init`) failed |
|
|
666
|
-
|
|
667
|
-
> **Note:** These events are only dispatched by the automatic installer script. Direct calls to `initAgentWidget()` return the handle synchronously and do not fire them.
|
|
668
|
-
>
|
|
669
|
-
> `persona:ready` is still dispatched as a **deprecated** alias of `persona:chat-ready` and will be removed in the next major.
|
|
670
|
-
|
|
671
|
-
### Controller Events
|
|
672
|
-
|
|
673
|
-
The widget controller exposes an event system for reacting to chat events. Use `controller.on(eventName, callback)` to subscribe and `controller.off(eventName, callback)` to unsubscribe.
|
|
674
|
-
|
|
675
|
-
#### Available Events
|
|
676
|
-
|
|
677
|
-
| Event | Payload | Description |
|
|
678
|
-
|-------|---------|-------------|
|
|
679
|
-
| `user:message` | `AgentWidgetMessage` | Emitted when a new user message is detected. Includes `viaVoice: true` if sent via voice. |
|
|
680
|
-
| `assistant:message` | `AgentWidgetMessage` | Emitted when an assistant message starts streaming |
|
|
681
|
-
| `assistant:complete` | `AgentWidgetMessage` | Emitted when an assistant message finishes streaming |
|
|
682
|
-
| `voice:state` | `AgentWidgetVoiceStateEvent` | Emitted when voice recognition state changes |
|
|
683
|
-
| `action:detected` | `AgentWidgetActionEventPayload` | Emitted when an action is parsed from an assistant message |
|
|
684
|
-
| `widget:opened` | `AgentWidgetStateEvent` | Emitted when the widget panel opens |
|
|
685
|
-
| `widget:closed` | `AgentWidgetStateEvent` | Emitted when the widget panel closes |
|
|
686
|
-
| `widget:state` | `AgentWidgetStateSnapshot` | Emitted on any widget state change |
|
|
687
|
-
| `message:feedback` | `AgentWidgetMessageFeedback` | Emitted when user provides feedback (upvote/downvote) |
|
|
688
|
-
| `message:copy` | `AgentWidgetMessage` | Emitted when user copies a message |
|
|
689
|
-
| `eventStream:opened` | `{ timestamp: number }` | Emitted when the event stream panel opens |
|
|
690
|
-
| `eventStream:closed` | `{ timestamp: number }` | Emitted when the event stream panel closes |
|
|
691
|
-
|
|
692
|
-
#### Event Payload Types
|
|
693
|
-
|
|
694
|
-
```typescript
|
|
695
|
-
// Voice state event
|
|
696
|
-
type AgentWidgetVoiceStateEvent = {
|
|
697
|
-
active: boolean;
|
|
698
|
-
source: "user" | "auto" | "restore" | "system";
|
|
699
|
-
timestamp: number;
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
// Widget state event (for opened/closed)
|
|
703
|
-
type AgentWidgetStateEvent = {
|
|
704
|
-
open: boolean;
|
|
705
|
-
source: "user" | "auto" | "api" | "system";
|
|
706
|
-
timestamp: number;
|
|
707
|
-
};
|
|
708
|
-
|
|
709
|
-
// Widget state snapshot
|
|
710
|
-
type AgentWidgetStateSnapshot = {
|
|
711
|
-
open: boolean;
|
|
712
|
-
launcherEnabled: boolean;
|
|
713
|
-
voiceActive: boolean;
|
|
714
|
-
streaming: boolean;
|
|
715
|
-
};
|
|
716
|
-
|
|
717
|
-
// Action event payload
|
|
718
|
-
type AgentWidgetActionEventPayload = {
|
|
719
|
-
action: AgentWidgetParsedAction;
|
|
720
|
-
message: AgentWidgetMessage;
|
|
721
|
-
};
|
|
722
|
-
|
|
723
|
-
// Message feedback
|
|
724
|
-
type AgentWidgetMessageFeedback = {
|
|
725
|
-
type: "upvote" | "downvote";
|
|
726
|
-
messageId: string;
|
|
727
|
-
message: AgentWidgetMessage;
|
|
728
|
-
};
|
|
729
|
-
```
|
|
730
|
-
|
|
731
|
-
#### Example: Listening to Events
|
|
732
|
-
|
|
733
|
-
```ts
|
|
734
|
-
const chat = initAgentWidget({
|
|
735
|
-
target: 'body',
|
|
736
|
-
config: { apiUrl: '/api/chat/dispatch' }
|
|
737
|
-
});
|
|
738
|
-
|
|
739
|
-
// Listen for new user messages
|
|
740
|
-
chat.on('user:message', (message) => {
|
|
741
|
-
console.log('User sent:', message.content);
|
|
742
|
-
if (message.viaVoice) {
|
|
743
|
-
console.log('Message was sent via voice recognition');
|
|
744
|
-
}
|
|
745
|
-
});
|
|
746
|
-
|
|
747
|
-
// Listen for completed assistant responses
|
|
748
|
-
chat.on('assistant:complete', (message) => {
|
|
749
|
-
console.log('Assistant replied:', message.content);
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
// Listen for voice state changes
|
|
753
|
-
chat.on('voice:state', (event) => {
|
|
754
|
-
console.log('Voice active:', event.active, 'Source:', event.source);
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
// Listen for widget open/close
|
|
758
|
-
chat.on('widget:opened', (event) => {
|
|
759
|
-
console.log('Widget opened by:', event.source);
|
|
760
|
-
});
|
|
761
|
-
|
|
762
|
-
chat.on('widget:closed', (event) => {
|
|
763
|
-
console.log('Widget closed by:', event.source);
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
// Listen for parsed actions from assistant messages
|
|
767
|
-
chat.on('action:detected', ({ action, message }) => {
|
|
768
|
-
console.log('Action detected:', action.type, action.payload);
|
|
769
|
-
});
|
|
770
|
-
```
|
|
771
|
-
|
|
772
|
-
#### Example: Voice Mode Persistence
|
|
773
|
-
|
|
774
|
-
The `user:message` event is useful for implementing custom voice mode persistence across page navigations:
|
|
775
|
-
|
|
776
|
-
```ts
|
|
777
|
-
const chat = initAgentWidget({
|
|
778
|
-
target: 'body',
|
|
779
|
-
config: {
|
|
780
|
-
apiUrl: '/api/chat/dispatch',
|
|
781
|
-
voiceRecognition: { enabled: true }
|
|
782
|
-
}
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
// Track if the user is in "voice mode"
|
|
786
|
-
chat.on('user:message', (message) => {
|
|
787
|
-
localStorage.setItem('voice-mode', message.viaVoice ? 'true' : 'false');
|
|
788
|
-
});
|
|
789
|
-
|
|
790
|
-
// On page load, restore voice mode if the user was using voice
|
|
791
|
-
if (localStorage.getItem('voice-mode') === 'true') {
|
|
792
|
-
chat.startVoiceRecognition();
|
|
793
|
-
}
|
|
794
|
-
```
|
|
795
|
-
|
|
796
|
-
Note: The built-in `persistState` option handles this automatically when configured:
|
|
797
|
-
|
|
798
|
-
```ts
|
|
799
|
-
initAgentWidget({
|
|
800
|
-
target: 'body',
|
|
801
|
-
config: {
|
|
802
|
-
persistState: true, // Automatically persists open state and voice mode
|
|
803
|
-
voiceRecognition: { enabled: true, autoResume: 'assistant' }
|
|
804
|
-
}
|
|
805
|
-
});
|
|
806
|
-
```
|
|
807
|
-
|
|
808
|
-
### State Loaded Hook
|
|
809
|
-
|
|
810
|
-
The `onStateLoaded` hook is called after state is loaded from the storage adapter, but before the widget initializes. Use this to transform or inject messages based on external state (e.g., navigation flags, checkout returns).
|
|
811
|
-
|
|
812
|
-
Returning `{ state, open: true }` also tells the widget to open the panel after initialization — useful when injecting a post-navigation message that the user should immediately see.
|
|
813
|
-
|
|
814
|
-
```ts
|
|
815
|
-
// Plain state transform
|
|
816
|
-
initAgentWidget({
|
|
817
|
-
target: 'body',
|
|
818
|
-
config: {
|
|
819
|
-
storageAdapter: createLocalStorageAdapter('my-chat'),
|
|
820
|
-
onStateLoaded: (state) => {
|
|
821
|
-
const navMessage = consumeNavigationFlag();
|
|
822
|
-
if (navMessage) {
|
|
823
|
-
return {
|
|
824
|
-
...state,
|
|
825
|
-
messages: [...(state.messages || []), {
|
|
826
|
-
id: `nav-${Date.now()}`,
|
|
827
|
-
role: 'assistant',
|
|
828
|
-
content: navMessage,
|
|
829
|
-
createdAt: new Date().toISOString()
|
|
830
|
-
}]
|
|
831
|
-
};
|
|
832
|
-
}
|
|
833
|
-
return state;
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
// Return { state, open: true } to also open the panel
|
|
839
|
-
initAgentWidget({
|
|
840
|
-
target: 'body',
|
|
841
|
-
config: {
|
|
842
|
-
storageAdapter: createLocalStorageAdapter('my-chat'),
|
|
843
|
-
onStateLoaded: (state) => {
|
|
844
|
-
const navMessage = consumeNavigationFlag();
|
|
845
|
-
if (navMessage) {
|
|
846
|
-
return {
|
|
847
|
-
state: {
|
|
848
|
-
...state,
|
|
849
|
-
messages: [...(state.messages || []), {
|
|
850
|
-
id: `nav-${Date.now()}`,
|
|
851
|
-
role: 'assistant',
|
|
852
|
-
content: navMessage,
|
|
853
|
-
createdAt: new Date().toISOString()
|
|
854
|
-
}]
|
|
855
|
-
},
|
|
856
|
-
open: true
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
return state;
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
});
|
|
863
|
-
```
|
|
864
|
-
|
|
865
|
-
**Use cases:**
|
|
866
|
-
- Inject messages after page navigation (e.g., "Here are our products!") and open the panel
|
|
867
|
-
- Add confirmation messages after checkout/payment returns
|
|
868
|
-
- Transform or filter loaded messages
|
|
869
|
-
- Inject system messages based on external state
|
|
870
|
-
|
|
871
|
-
The hook receives the loaded state and must return the (potentially modified) state synchronously.
|
|
872
|
-
|
|
873
|
-
### Message Actions (Copy, Upvote, Downvote)
|
|
874
|
-
|
|
875
|
-
The widget includes built-in action buttons for assistant messages that allow users to copy message content and provide feedback through upvote/downvote buttons.
|
|
876
|
-
|
|
877
|
-
#### Configuration
|
|
878
|
-
|
|
879
|
-
```ts
|
|
880
|
-
const controller = initAgentWidget({
|
|
881
|
-
target: '#app',
|
|
882
|
-
config: {
|
|
883
|
-
apiUrl: '/api/chat/dispatch',
|
|
884
|
-
|
|
885
|
-
// Message actions configuration
|
|
886
|
-
messageActions: {
|
|
887
|
-
enabled: true, // Enable/disable all action buttons (default: true)
|
|
888
|
-
showCopy: true, // Show copy button (default: true)
|
|
889
|
-
showUpvote: true, // Show upvote button (default: false - requires backend)
|
|
890
|
-
showDownvote: true, // Show downvote button (default: false - requires backend)
|
|
891
|
-
visibility: 'hover', // 'hover' or 'always' (default: 'hover')
|
|
892
|
-
align: 'right', // 'left', 'center', or 'right' (default: 'right')
|
|
893
|
-
layout: 'pill-inside', // 'pill-inside' (compact floating) or 'row-inside' (full-width bar)
|
|
894
|
-
|
|
895
|
-
// Optional callbacks (called in addition to events)
|
|
896
|
-
onCopy: (message) => {
|
|
897
|
-
console.log('Copied:', message.id);
|
|
898
|
-
},
|
|
899
|
-
onFeedback: (feedback) => {
|
|
900
|
-
console.log('Feedback:', feedback.type, feedback.messageId);
|
|
901
|
-
// Send to your analytics/backend
|
|
902
|
-
fetch('/api/feedback', {
|
|
903
|
-
method: 'POST',
|
|
904
|
-
headers: { 'Content-Type': 'application/json' },
|
|
905
|
-
body: JSON.stringify(feedback)
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
```
|
|
912
|
-
|
|
913
|
-
#### Feedback Events
|
|
914
|
-
|
|
915
|
-
Listen to feedback events via the controller:
|
|
916
|
-
|
|
917
|
-
```ts
|
|
918
|
-
// Copy event - fired when user copies a message
|
|
919
|
-
controller.on('message:copy', (message) => {
|
|
920
|
-
console.log('Message copied:', message.id, message.content);
|
|
921
|
-
});
|
|
922
|
-
|
|
923
|
-
// Feedback event - fired when user upvotes or downvotes
|
|
924
|
-
controller.on('message:feedback', (feedback) => {
|
|
925
|
-
console.log('Feedback received:', {
|
|
926
|
-
type: feedback.type, // 'upvote' or 'downvote'
|
|
927
|
-
messageId: feedback.messageId,
|
|
928
|
-
message: feedback.message // Full message object
|
|
929
|
-
});
|
|
930
|
-
});
|
|
931
|
-
```
|
|
932
|
-
|
|
933
|
-
#### Feedback Types
|
|
934
|
-
|
|
935
|
-
```typescript
|
|
936
|
-
type AgentWidgetMessageFeedback = {
|
|
937
|
-
type: 'upvote' | 'downvote';
|
|
938
|
-
messageId: string;
|
|
939
|
-
message: AgentWidgetMessage;
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
type AgentWidgetMessageActionsConfig = {
|
|
943
|
-
enabled?: boolean;
|
|
944
|
-
showCopy?: boolean;
|
|
945
|
-
showUpvote?: boolean;
|
|
946
|
-
showDownvote?: boolean;
|
|
947
|
-
visibility?: 'always' | 'hover';
|
|
948
|
-
onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
|
|
949
|
-
onCopy?: (message: AgentWidgetMessage) => void;
|
|
950
|
-
};
|
|
951
|
-
```
|
|
952
|
-
|
|
953
|
-
#### Visual Behavior
|
|
954
|
-
|
|
955
|
-
- **Hover mode** (`visibility: 'hover'`): Action buttons appear when hovering over assistant messages
|
|
956
|
-
- **Always mode** (`visibility: 'always'`): Action buttons are always visible
|
|
957
|
-
- **Copy button**: Shows a checkmark briefly after successful copy
|
|
958
|
-
- **Vote buttons**: Toggle active state and are mutually exclusive (upvoting clears downvote and vice versa)
|
|
959
|
-
|
|
960
|
-
### Loading & Idle Indicators
|
|
961
|
-
|
|
962
|
-
The widget displays visual indicators during different states of the conversation:
|
|
963
|
-
|
|
964
|
-
- **Loading indicator**: Shown while waiting for a response (standalone) or when an assistant message is streaming but has no content yet (inline)
|
|
965
|
-
- **Idle indicator**: Shown when the widget is idle (not streaming) and has at least one message - useful for showing the assistant is "waiting" for user input
|
|
966
|
-
|
|
967
|
-
#### Configuration
|
|
968
|
-
|
|
969
|
-
```ts
|
|
970
|
-
const controller = initAgentWidget({
|
|
971
|
-
target: '#app',
|
|
972
|
-
config: {
|
|
973
|
-
apiUrl: '/api/chat/dispatch',
|
|
974
|
-
|
|
975
|
-
loadingIndicator: {
|
|
976
|
-
// Show/hide bubble styling around standalone indicator (default: true)
|
|
977
|
-
showBubble: false,
|
|
978
|
-
|
|
979
|
-
// Custom loading indicator renderer
|
|
980
|
-
render: ({ location, config, defaultRenderer }) => {
|
|
981
|
-
// location: 'standalone' (separate bubble) or 'inline' (inside message)
|
|
982
|
-
if (location === 'standalone') {
|
|
983
|
-
const el = document.createElement('div');
|
|
984
|
-
el.innerHTML = '<svg class="spinner">...</svg>';
|
|
985
|
-
el.setAttribute('data-preserve-animation', 'true');
|
|
986
|
-
return el;
|
|
987
|
-
}
|
|
988
|
-
// Use default 3-dot bouncing indicator for inline
|
|
989
|
-
return defaultRenderer();
|
|
990
|
-
},
|
|
991
|
-
|
|
992
|
-
// Custom idle state indicator (shown after response completes)
|
|
993
|
-
renderIdle: ({ lastMessage, messageCount, config }) => {
|
|
994
|
-
// Only show after assistant messages
|
|
995
|
-
if (lastMessage?.role !== 'assistant') return null;
|
|
996
|
-
|
|
997
|
-
const el = document.createElement('div');
|
|
998
|
-
el.textContent = 'What would you like to do next?';
|
|
999
|
-
el.setAttribute('data-preserve-animation', 'true');
|
|
1000
|
-
return el;
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
});
|
|
1005
|
-
```
|
|
1006
|
-
|
|
1007
|
-
#### Indicator Locations
|
|
1008
|
-
|
|
1009
|
-
| Location | When Shown | Description |
|
|
1010
|
-
|----------|------------|-------------|
|
|
1011
|
-
| `standalone` | Waiting for stream to start | Separate bubble shown after user sends a message |
|
|
1012
|
-
| `inline` | Streaming with empty content | Inside the assistant message bubble |
|
|
1013
|
-
| `idle` | Not streaming, has messages | After assistant finishes responding |
|
|
1014
|
-
|
|
1015
|
-
#### Animation Preservation
|
|
1016
|
-
|
|
1017
|
-
When using custom animated indicators, add the `data-preserve-animation="true"` attribute to prevent the DOM morpher from interrupting CSS animations during updates:
|
|
1018
|
-
|
|
1019
|
-
```ts
|
|
1020
|
-
render: () => {
|
|
1021
|
-
const el = document.createElement('div');
|
|
1022
|
-
el.setAttribute('data-preserve-animation', 'true');
|
|
1023
|
-
el.innerHTML = `
|
|
1024
|
-
<style>
|
|
1025
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
1026
|
-
.spinner { animation: spin 1s linear infinite; }
|
|
1027
|
-
</style>
|
|
1028
|
-
<div class="spinner">⟳</div>
|
|
1029
|
-
`;
|
|
1030
|
-
return el;
|
|
1031
|
-
}
|
|
1032
|
-
```
|
|
1033
|
-
|
|
1034
|
-
#### Hiding Indicators
|
|
1035
|
-
|
|
1036
|
-
Return `null` from any render function to hide that indicator:
|
|
1037
|
-
|
|
1038
|
-
```ts
|
|
1039
|
-
loadingIndicator: {
|
|
1040
|
-
// Hide loading indicator entirely
|
|
1041
|
-
render: () => null,
|
|
1042
|
-
|
|
1043
|
-
// Hide idle indicator (default behavior)
|
|
1044
|
-
renderIdle: () => null
|
|
1045
|
-
}
|
|
1046
|
-
```
|
|
1047
|
-
|
|
1048
|
-
#### Using Plugins
|
|
1049
|
-
|
|
1050
|
-
You can also customize indicators via plugins, which take priority over config:
|
|
1051
|
-
|
|
1052
|
-
```ts
|
|
1053
|
-
const customIndicatorPlugin = {
|
|
1054
|
-
id: 'custom-indicators',
|
|
1055
|
-
|
|
1056
|
-
renderLoadingIndicator: ({ location, defaultRenderer }) => {
|
|
1057
|
-
if (location === 'standalone') {
|
|
1058
|
-
return createCustomSpinner();
|
|
1059
|
-
}
|
|
1060
|
-
return defaultRenderer();
|
|
1061
|
-
},
|
|
1062
|
-
|
|
1063
|
-
renderIdleIndicator: ({ lastMessage, messageCount }) => {
|
|
1064
|
-
if (messageCount === 0) return null;
|
|
1065
|
-
if (lastMessage?.role !== 'assistant') return null;
|
|
1066
|
-
return createIdleAnimation();
|
|
1067
|
-
}
|
|
1068
|
-
};
|
|
1069
|
-
|
|
1070
|
-
initAgentWidget({
|
|
1071
|
-
target: '#app',
|
|
1072
|
-
config: {
|
|
1073
|
-
plugins: [customIndicatorPlugin]
|
|
1074
|
-
}
|
|
1075
|
-
});
|
|
1076
|
-
```
|
|
1077
|
-
|
|
1078
|
-
#### Type Definitions
|
|
1079
|
-
|
|
1080
|
-
```typescript
|
|
1081
|
-
// Loading indicator context
|
|
1082
|
-
type LoadingIndicatorRenderContext = {
|
|
1083
|
-
config: AgentWidgetConfig;
|
|
1084
|
-
streaming: boolean;
|
|
1085
|
-
location: 'inline' | 'standalone';
|
|
1086
|
-
defaultRenderer: () => HTMLElement;
|
|
1087
|
-
};
|
|
1088
|
-
|
|
1089
|
-
// Idle indicator context
|
|
1090
|
-
type IdleIndicatorRenderContext = {
|
|
1091
|
-
config: AgentWidgetConfig;
|
|
1092
|
-
lastMessage: AgentWidgetMessage | undefined;
|
|
1093
|
-
messageCount: number;
|
|
1094
|
-
};
|
|
1095
|
-
|
|
1096
|
-
// Configuration
|
|
1097
|
-
type AgentWidgetLoadingIndicatorConfig = {
|
|
1098
|
-
showBubble?: boolean;
|
|
1099
|
-
render?: (context: LoadingIndicatorRenderContext) => HTMLElement | null;
|
|
1100
|
-
renderIdle?: (context: IdleIndicatorRenderContext) => HTMLElement | null;
|
|
1101
|
-
};
|
|
1102
|
-
```
|
|
1103
|
-
|
|
1104
|
-
#### Priority Chain
|
|
1105
|
-
|
|
1106
|
-
Indicators are resolved in this order:
|
|
1107
|
-
1. **Plugin hook** (`renderLoadingIndicator` / `renderIdleIndicator`)
|
|
1108
|
-
2. **Config function** (`loadingIndicator.render` / `loadingIndicator.renderIdle`)
|
|
1109
|
-
3. **Default** (3-dot bouncing animation for loading, `null` for idle)
|
|
1110
|
-
|
|
1111
|
-
### Ask User Question
|
|
1112
|
-
|
|
1113
|
-
The `ask_user_question` feature turns a LOCAL agent tool into an interactive prompt with tappable option pills. When the agent calls the `ask_user_question` tool, the server pauses execution and emits a `step_await` event; the widget renders an answer-pill sheet over the composer; the user picks / types / dismisses; the widget POSTs the answer to `/v1/dispatch/resume` and the paused execution continues with a structured `tool_result`.
|
|
1114
|
-
|
|
1115
|
-
This is the recommended pattern for human-in-the-loop clarifying questions. The agent-side setup (declare `ask_user_question` as a `runtimeTools` LOCAL tool and instruct the model to call it) lives in your `RuntypeFlowConfig` in the proxy — pair it with a `POST` handler that forwards to the upstream `/resume` endpoint (see `@runtypelabs/persona-proxy` and your deployment’s `resume` route).
|
|
1116
|
-
|
|
1117
|
-
#### Configuration
|
|
1118
|
-
|
|
1119
|
-
```ts
|
|
1120
|
-
features: {
|
|
1121
|
-
askUserQuestion: {
|
|
1122
|
-
enabled: true, // default: true. When false, the tool falls through to the normal tool-bubble path.
|
|
1123
|
-
dismissible: true, // default: true. Shows a × close button on the sheet.
|
|
1124
|
-
slideInMs: 180, // slide-in animation duration.
|
|
1125
|
-
freeTextLabel: 'Other…',
|
|
1126
|
-
freeTextPlaceholder: 'Type your answer…',
|
|
1127
|
-
submitLabel: 'Send',
|
|
1128
|
-
styles: {
|
|
1129
|
-
sheetBackground: '#ffffff',
|
|
1130
|
-
sheetBorder: '#e5e7eb',
|
|
1131
|
-
sheetShadow: '0 12px 28px -10px rgba(0,0,0,0.15)',
|
|
1132
|
-
pillBackground: 'transparent',
|
|
1133
|
-
pillBackgroundSelected: '#0f0f0f',
|
|
1134
|
-
pillTextColor: '#1f2937',
|
|
1135
|
-
pillTextColorSelected: '#fafafa',
|
|
1136
|
-
pillBorderRadius: '999px',
|
|
1137
|
-
customInputBackground: '#ffffff'
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
```
|
|
1142
|
-
|
|
1143
|
-
The composer-overlay sheet is the only question UI — no transcript stub is rendered. After the user picks, the picked answer appears as a normal user bubble so the transcript reads naturally.
|
|
1144
|
-
|
|
1145
|
-
#### DOM events
|
|
1146
|
-
|
|
1147
|
-
The widget dispatches two events on the mount element so the host page can react without touching the plugin API:
|
|
1148
|
-
|
|
1149
|
-
| Event | Detail |
|
|
1150
|
-
|---|---|
|
|
1151
|
-
| `persona:askUserQuestion:answered` | `{ toolUseId, answer, values, isFreeText, source }` where `source` is `'pick' \| 'multi' \| 'free-text'` |
|
|
1152
|
-
| `persona:askUserQuestion:dismissed` | `{ toolUseId }` |
|
|
1153
|
-
|
|
1154
|
-
```ts
|
|
1155
|
-
mount.addEventListener('persona:askUserQuestion:answered', (event) => {
|
|
1156
|
-
const { answer, source } = event.detail;
|
|
1157
|
-
console.log('User picked', answer, 'via', source);
|
|
1158
|
-
});
|
|
1159
|
-
```
|
|
1160
|
-
|
|
1161
|
-
#### Custom UI via the `renderAskUserQuestion` plugin hook
|
|
1162
|
-
|
|
1163
|
-
For full control over the question UI — a modal, a sidebar form, a command palette, whatever — register a plugin with `renderAskUserQuestion`. Returning a non-null `HTMLElement` renders inline in the transcript and suppresses the built-in overlay sheet. Returning `null` falls through to the default sheet.
|
|
1164
|
-
|
|
1165
|
-
```ts
|
|
1166
|
-
import type { AgentWidgetPlugin } from '@runtypelabs/persona';
|
|
1167
|
-
|
|
1168
|
-
const customAskPlugin: AgentWidgetPlugin = {
|
|
1169
|
-
id: 'custom-ask',
|
|
1170
|
-
renderAskUserQuestion: ({ payload, complete, resolve, dismiss }) => {
|
|
1171
|
-
const prompt = payload?.questions?.[0];
|
|
1172
|
-
if (!prompt) return null; // streaming — wait for more data, or show a skeleton
|
|
1173
|
-
|
|
1174
|
-
const root = document.createElement('div');
|
|
1175
|
-
root.className = 'my-question-card';
|
|
1176
|
-
|
|
1177
|
-
const q = document.createElement('p');
|
|
1178
|
-
q.textContent = prompt.question ?? '';
|
|
1179
|
-
root.appendChild(q);
|
|
1180
|
-
|
|
1181
|
-
(prompt.options ?? []).forEach((option) => {
|
|
1182
|
-
const btn = document.createElement('button');
|
|
1183
|
-
btn.textContent = option.label;
|
|
1184
|
-
btn.addEventListener('click', () => resolve(option.label));
|
|
1185
|
-
root.appendChild(btn);
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
if (prompt.allowFreeText !== false) {
|
|
1189
|
-
const input = document.createElement('input');
|
|
1190
|
-
input.placeholder = 'Other…';
|
|
1191
|
-
input.addEventListener('keydown', (e) => {
|
|
1192
|
-
if (e.key === 'Enter' && input.value.trim()) resolve(input.value.trim());
|
|
1193
|
-
});
|
|
1194
|
-
root.appendChild(input);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
const close = document.createElement('button');
|
|
1198
|
-
close.textContent = '×';
|
|
1199
|
-
close.addEventListener('click', () => dismiss());
|
|
1200
|
-
root.appendChild(close);
|
|
1201
|
-
|
|
1202
|
-
return root;
|
|
1203
|
-
}
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
initAgentWidget({
|
|
1207
|
-
target: '#app',
|
|
1208
|
-
config: {
|
|
1209
|
-
plugins: [customAskPlugin]
|
|
1210
|
-
}
|
|
1211
|
-
});
|
|
1212
|
-
```
|
|
1213
|
-
|
|
1214
|
-
#### Type Definitions
|
|
1215
|
-
|
|
1216
|
-
```ts
|
|
1217
|
-
type AskUserQuestionOption = {
|
|
1218
|
-
label: string;
|
|
1219
|
-
description?: string;
|
|
1220
|
-
};
|
|
1221
|
-
|
|
1222
|
-
type AskUserQuestionPrompt = {
|
|
1223
|
-
question: string;
|
|
1224
|
-
header?: string; // short chip label, ≤12 chars
|
|
1225
|
-
options: AskUserQuestionOption[];
|
|
1226
|
-
multiSelect?: boolean; // allow multiple picks with a Submit button
|
|
1227
|
-
allowFreeText?: boolean; // show an "Other…" free-text pill
|
|
1228
|
-
};
|
|
1229
|
-
|
|
1230
|
-
type AskUserQuestionPayload = {
|
|
1231
|
-
questions: AskUserQuestionPrompt[];
|
|
1232
|
-
};
|
|
1233
|
-
|
|
1234
|
-
// Plugin hook signature
|
|
1235
|
-
renderAskUserQuestion?: (context: {
|
|
1236
|
-
message: AgentWidgetMessage;
|
|
1237
|
-
payload: Partial<AskUserQuestionPayload> | null; // may be partial mid-stream
|
|
1238
|
-
complete: boolean; // true once tool-call args fully stream
|
|
1239
|
-
resolve: (answer: string) => void; // posts /resume with structured toolOutput
|
|
1240
|
-
dismiss: () => void; // sends "(dismissed)" sentinel
|
|
1241
|
-
config: AgentWidgetConfig;
|
|
1242
|
-
}) => HTMLElement | null;
|
|
1243
|
-
```
|
|
1244
|
-
|
|
1245
|
-
For plugins that want to re-parse a tool message outside the hook context, the widget also exports a `parseAskUserQuestionPayload(message)` helper that returns `{ payload, complete }` using the same partial-JSON logic the built-in sheet uses.
|
|
1246
|
-
|
|
1247
|
-
#### Priority chain
|
|
1248
|
-
|
|
1249
|
-
1. **Plugin hook** (`renderAskUserQuestion` returning a non-null element) — fully owns the UI; built-in overlay is suppressed.
|
|
1250
|
-
2. **Built-in overlay sheet** — when the feature is enabled and no plugin handles it.
|
|
1251
|
-
3. **Generic tool bubble** — when `features.askUserQuestion.enabled` is `false`, the tool call renders through the normal `renderToolCall` path.
|
|
1252
|
-
|
|
1253
|
-
### Dropdown Menu
|
|
1254
|
-
|
|
1255
|
-
A reusable dropdown menu utility for building custom menus in plugins, custom components, or host-page UI that matches the widget's theme.
|
|
1256
|
-
|
|
1257
|
-
#### Basic usage
|
|
1258
|
-
|
|
1259
|
-
```ts
|
|
1260
|
-
import { createDropdownMenu } from '@runtypelabs/persona';
|
|
1261
|
-
|
|
1262
|
-
const button = document.querySelector('#my-button')!;
|
|
1263
|
-
const wrapper = document.createElement('div');
|
|
1264
|
-
wrapper.style.position = 'relative';
|
|
1265
|
-
button.parentElement!.insertBefore(wrapper, button);
|
|
1266
|
-
wrapper.appendChild(button);
|
|
1267
|
-
|
|
1268
|
-
const dropdown = createDropdownMenu({
|
|
1269
|
-
items: [
|
|
1270
|
-
{ id: 'edit', label: 'Edit', icon: 'pencil' },
|
|
1271
|
-
{ id: 'duplicate', label: 'Duplicate', icon: 'copy' },
|
|
1272
|
-
{ id: 'delete', label: 'Delete', icon: 'trash-2', destructive: true, dividerBefore: true },
|
|
1273
|
-
],
|
|
1274
|
-
onSelect: (id) => console.log('Selected:', id),
|
|
1275
|
-
anchor: wrapper,
|
|
1276
|
-
position: 'bottom-left', // or 'bottom-right'
|
|
1277
|
-
});
|
|
1278
|
-
|
|
1279
|
-
wrapper.appendChild(dropdown.element);
|
|
1280
|
-
button.addEventListener('click', () => dropdown.toggle());
|
|
1281
|
-
```
|
|
1282
|
-
|
|
1283
|
-
#### Escaping overflow containers
|
|
1284
|
-
|
|
1285
|
-
When the anchor is inside a container with `overflow: hidden`, use the `portal` option to render the menu at a higher DOM level while keeping CSS variable inheritance:
|
|
1286
|
-
|
|
1287
|
-
```ts
|
|
1288
|
-
const dropdown = createDropdownMenu({
|
|
1289
|
-
items: [...],
|
|
1290
|
-
onSelect: (id) => { /* handle */ },
|
|
1291
|
-
anchor: myButton,
|
|
1292
|
-
position: 'bottom-right',
|
|
1293
|
-
portal: document.querySelector('[data-persona-root]')!,
|
|
1294
|
-
});
|
|
1295
|
-
// No need to append — portal mode appends automatically
|
|
1296
|
-
```
|
|
1297
|
-
|
|
1298
|
-
#### Header dropdown menus
|
|
1299
|
-
|
|
1300
|
-
Trailing header actions support built-in dropdown menus via the `menuItems` property:
|
|
1301
|
-
|
|
1302
|
-
```ts
|
|
1303
|
-
createAgentExperience(mount, {
|
|
1304
|
-
layout: {
|
|
1305
|
-
header: {
|
|
1306
|
-
layout: 'minimal',
|
|
1307
|
-
trailingActions: [
|
|
1308
|
-
{
|
|
1309
|
-
id: 'options',
|
|
1310
|
-
icon: 'chevron-down',
|
|
1311
|
-
ariaLabel: 'Options',
|
|
1312
|
-
menuItems: [
|
|
1313
|
-
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
|
1314
|
-
{ id: 'help', label: 'Help', icon: 'help-circle' },
|
|
1315
|
-
{ id: 'logout', label: 'Log out', icon: 'log-out', destructive: true, dividerBefore: true },
|
|
1316
|
-
]
|
|
1317
|
-
}
|
|
1318
|
-
],
|
|
1319
|
-
onAction: (actionId) => {
|
|
1320
|
-
// Receives the menu item id when selected
|
|
1321
|
-
console.log('Action:', actionId);
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
});
|
|
1326
|
-
```
|
|
1327
|
-
|
|
1328
|
-
#### Theming
|
|
1329
|
-
|
|
1330
|
-
Dropdown menus are styled via CSS custom properties with semantic fallbacks:
|
|
1331
|
-
|
|
1332
|
-
| Variable | Description | Fallback |
|
|
1333
|
-
|----------|-------------|----------|
|
|
1334
|
-
| `--persona-dropdown-bg` | Menu background | `--persona-surface` |
|
|
1335
|
-
| `--persona-dropdown-border` | Menu border | `--persona-border` |
|
|
1336
|
-
| `--persona-dropdown-radius` | Border radius | `0.625rem` |
|
|
1337
|
-
| `--persona-dropdown-shadow` | Box shadow | `0 4px 16px rgba(0,0,0,0.12)` |
|
|
1338
|
-
| `--persona-dropdown-item-color` | Item text color | `--persona-text` |
|
|
1339
|
-
| `--persona-dropdown-item-hover-bg` | Item hover background | `--persona-container` |
|
|
1340
|
-
| `--persona-dropdown-destructive-color` | Destructive item color | `#ef4444` |
|
|
1341
|
-
|
|
1342
|
-
Artifact toolbar copy menu tokens (`copyMenuBackground`, `copyMenuBorder`, etc.) also set the dropdown variables as defaults, so dropdown theming works with the existing artifact token config.
|
|
1343
|
-
|
|
1344
|
-
#### Type definitions
|
|
1345
|
-
|
|
1346
|
-
```ts
|
|
1347
|
-
interface DropdownMenuItem {
|
|
1348
|
-
id: string;
|
|
1349
|
-
label: string;
|
|
1350
|
-
icon?: string; // Lucide icon name
|
|
1351
|
-
destructive?: boolean;
|
|
1352
|
-
dividerBefore?: boolean;
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
interface CreateDropdownOptions {
|
|
1356
|
-
items: DropdownMenuItem[];
|
|
1357
|
-
onSelect: (id: string) => void;
|
|
1358
|
-
anchor: HTMLElement;
|
|
1359
|
-
position?: 'bottom-left' | 'bottom-right';
|
|
1360
|
-
portal?: HTMLElement;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
interface DropdownMenuHandle {
|
|
1364
|
-
element: HTMLElement;
|
|
1365
|
-
show: () => void;
|
|
1366
|
-
hide: () => void;
|
|
1367
|
-
toggle: () => void;
|
|
1368
|
-
destroy: () => void;
|
|
1369
|
-
}
|
|
1370
|
-
```
|
|
1371
|
-
|
|
1372
|
-
### Button Utilities
|
|
1373
|
-
|
|
1374
|
-
Composable button factories for building custom toolbars, actions, and toggle controls that match the widget's theme.
|
|
1375
|
-
|
|
1376
|
-
#### Icon button
|
|
1377
|
-
|
|
1378
|
-
```ts
|
|
1379
|
-
import { createIconButton } from '@runtypelabs/persona';
|
|
1380
|
-
|
|
1381
|
-
const refreshBtn = createIconButton({
|
|
1382
|
-
icon: 'refresh-cw',
|
|
1383
|
-
label: 'Refresh',
|
|
1384
|
-
onClick: () => handleRefresh(),
|
|
1385
|
-
});
|
|
1386
|
-
toolbar.appendChild(refreshBtn);
|
|
1387
|
-
```
|
|
1388
|
-
|
|
1389
|
-
#### Label button
|
|
1390
|
-
|
|
1391
|
-
```ts
|
|
1392
|
-
import { createLabelButton } from '@runtypelabs/persona';
|
|
1393
|
-
|
|
1394
|
-
const copyBtn = createLabelButton({
|
|
1395
|
-
icon: 'copy',
|
|
1396
|
-
label: 'Copy',
|
|
1397
|
-
variant: 'default', // 'default' | 'primary' | 'destructive' | 'ghost'
|
|
1398
|
-
onClick: () => copyToClipboard(),
|
|
1399
|
-
});
|
|
1400
|
-
```
|
|
1401
|
-
|
|
1402
|
-
#### Toggle group
|
|
1403
|
-
|
|
1404
|
-
```ts
|
|
1405
|
-
import { createToggleGroup } from '@runtypelabs/persona';
|
|
1406
|
-
|
|
1407
|
-
const toggle = createToggleGroup({
|
|
1408
|
-
items: [
|
|
1409
|
-
{ id: 'preview', icon: 'eye', label: 'Preview' },
|
|
1410
|
-
{ id: 'source', icon: 'code-2', label: 'Source' },
|
|
1411
|
-
],
|
|
1412
|
-
selectedId: 'preview',
|
|
1413
|
-
onSelect: (id) => setViewMode(id),
|
|
1414
|
-
});
|
|
1415
|
-
toolbar.appendChild(toggle.element);
|
|
1416
|
-
|
|
1417
|
-
// Programmatic update (does not fire onSelect)
|
|
1418
|
-
toggle.setSelected('source');
|
|
1419
|
-
```
|
|
1420
|
-
|
|
1421
|
-
#### Theming
|
|
1422
|
-
|
|
1423
|
-
All button utilities are styled via CSS custom properties:
|
|
1424
|
-
|
|
1425
|
-
| Variable | Component | Description | Fallback |
|
|
1426
|
-
|----------|-----------|-------------|----------|
|
|
1427
|
-
| `--persona-icon-btn-bg` | Icon button | Background | `--persona-surface` |
|
|
1428
|
-
| `--persona-icon-btn-border` | Icon button | Border | `--persona-border` |
|
|
1429
|
-
| `--persona-icon-btn-color` | Icon button | Icon color | `--persona-text` |
|
|
1430
|
-
| `--persona-icon-btn-hover-bg` | Icon button | Hover background | `--persona-container` |
|
|
1431
|
-
| `--persona-icon-btn-hover-color` | Icon button | Hover color | `inherit` |
|
|
1432
|
-
| `--persona-icon-btn-active-bg` | Icon button | Pressed/active bg | `--persona-container` |
|
|
1433
|
-
| `--persona-icon-btn-active-border` | Icon button | Pressed/active border | `--persona-border` |
|
|
1434
|
-
| `--persona-icon-btn-padding` | Icon button | Padding | `0.25rem` |
|
|
1435
|
-
| `--persona-icon-btn-radius` | Icon button | Border radius | `--persona-radius-md` |
|
|
1436
|
-
| `--persona-label-btn-bg` | Label button | Background | `--persona-surface` |
|
|
1437
|
-
| `--persona-label-btn-border` | Label button | Border | `--persona-border` |
|
|
1438
|
-
| `--persona-label-btn-color` | Label button | Text color | `--persona-text` |
|
|
1439
|
-
| `--persona-label-btn-hover-bg` | Label button | Hover background | `--persona-container` |
|
|
1440
|
-
| `--persona-label-btn-font-size` | Label button | Font size | `0.75rem` |
|
|
1441
|
-
| `--persona-toggle-group-gap` | Toggle group | Gap between items | `0` |
|
|
1442
|
-
| `--persona-toggle-group-radius` | Toggle group | First/last radius | `--persona-icon-btn-radius` |
|
|
1443
|
-
|
|
1444
|
-
These can also be set via the widget config's theme token system:
|
|
1445
|
-
|
|
1446
|
-
```ts
|
|
1447
|
-
createAgentExperience(mount, {
|
|
1448
|
-
darkTheme: {
|
|
1449
|
-
components: {
|
|
1450
|
-
iconButton: {
|
|
1451
|
-
background: 'transparent',
|
|
1452
|
-
border: 'none',
|
|
1453
|
-
hoverBackground: '#2B2B2B',
|
|
1454
|
-
hoverColor: '#E5E5E5',
|
|
1455
|
-
},
|
|
1456
|
-
toggleGroup: {
|
|
1457
|
-
gap: '0',
|
|
1458
|
-
borderRadius: '8px',
|
|
1459
|
-
},
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
});
|
|
1463
|
-
```
|
|
1464
|
-
|
|
1465
|
-
### Runtype adapter
|
|
1466
|
-
|
|
1467
|
-
This package ships with a Runtype adapter by default. The proxy handles all flow configuration, keeping the client lightweight and flexible.
|
|
1468
|
-
|
|
1469
|
-
**Flow configuration happens server-side** - you have three options:
|
|
1470
|
-
|
|
1471
|
-
1. **Use default flow** - The proxy includes a basic streaming chat flow out of the box
|
|
1472
|
-
2. **Reference a Runtype flow ID** - Configure flows in your Runtype dashboard and reference them by ID
|
|
1473
|
-
3. **Define custom flows** - Build flow configurations directly in the proxy
|
|
1474
|
-
|
|
1475
|
-
The client simply sends messages to the proxy, which constructs the full Runtype payload. This architecture allows you to:
|
|
1476
|
-
- Change models/prompts without redeploying the widget
|
|
1477
|
-
- A/B test different flows server-side
|
|
1478
|
-
- Enforce security and cost controls centrally
|
|
1479
|
-
- Support multiple flows for different use cases
|
|
1480
|
-
|
|
1481
|
-
### Dynamic Forms (Recommended)
|
|
1482
|
-
|
|
1483
|
-
For rendering AI-generated forms, use the **component middleware** approach with the `DynamicForm` component. This allows the AI to create contextually appropriate forms with any fields:
|
|
1484
|
-
|
|
1485
|
-
```typescript
|
|
1486
|
-
import { componentRegistry, initAgentWidget } from "@runtypelabs/persona";
|
|
1487
|
-
import { DynamicForm } from "./components"; // Your DynamicForm component
|
|
1488
|
-
|
|
1489
|
-
// Register the component
|
|
1490
|
-
componentRegistry.register("DynamicForm", DynamicForm);
|
|
1491
|
-
|
|
1492
|
-
initAgentWidget({
|
|
1493
|
-
target: "#app",
|
|
1494
|
-
config: {
|
|
1495
|
-
apiUrl: "/api/chat/dispatch-directive",
|
|
1496
|
-
parserType: "json",
|
|
1497
|
-
enableComponentStreaming: true,
|
|
1498
|
-
formEndpoint: "/form",
|
|
1499
|
-
// Optional: customize form appearance
|
|
1500
|
-
formStyles: {
|
|
1501
|
-
borderRadius: "16px",
|
|
1502
|
-
borderWidth: "1px",
|
|
1503
|
-
borderColor: "#e5e7eb",
|
|
1504
|
-
padding: "1.5rem",
|
|
1505
|
-
titleFontSize: "1.25rem",
|
|
1506
|
-
buttonBorderRadius: "9999px"
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
});
|
|
1510
|
-
```
|
|
1511
|
-
|
|
1512
|
-
The AI responds with JSON like:
|
|
1513
|
-
|
|
1514
|
-
```json
|
|
1515
|
-
{
|
|
1516
|
-
"text": "Please fill out this form:",
|
|
1517
|
-
"component": "DynamicForm",
|
|
1518
|
-
"props": {
|
|
1519
|
-
"title": "Contact Us",
|
|
1520
|
-
"fields": [
|
|
1521
|
-
{ "label": "Name", "type": "text", "required": true },
|
|
1522
|
-
{ "label": "Email", "type": "email", "required": true }
|
|
1523
|
-
],
|
|
1524
|
-
"submit_text": "Submit"
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
```
|
|
1528
|
-
|
|
1529
|
-
**Demos and reference:**
|
|
1530
|
-
|
|
1531
|
-
- [`examples/embedded-app/dynamic-form.html`](../../examples/embedded-app/dynamic-form.html) — primary demo with three layout variants (Compact / Spacious / Branded) showing how `formStyles` scales between visual languages.
|
|
1532
|
-
- [`examples/embedded-app/dynamic-form-fields.html`](../../examples/embedded-app/dynamic-form-fields.html) — every field type, layout width, helper-text, required marking, and sensitive-masking pattern in one page.
|
|
1533
|
-
- [`docs/DYNAMIC-FORMS.md`](./docs/DYNAMIC-FORMS.md) — full reference: field schema, `formStyles` tokens, layout patterns, recipes, and how to extend the example component (new field types, sections, conditional fields).
|
|
1534
|
-
|
|
1535
|
-
The shipped `DynamicForm` is an **example** in [`examples/embedded-app/src/components.ts`](../../examples/embedded-app/src/components.ts) — copy it into your app and customize. It supports text/email/tel/url/number/date/time/textarea, half-width pairs, auto-grow textareas, required-asterisk marking, inline validation, a success recap card with sensitive-field masking, and edit-after-submit. See [DYNAMIC-FORMS.md](./docs/DYNAMIC-FORMS.md) for the full surface area.
|
|
1536
|
-
|
|
1537
|
-
### Directive postprocessor (Deprecated)
|
|
1538
|
-
|
|
1539
|
-
> **⚠️ Deprecated:** The `directivePostprocessor` approach is deprecated in favor of the component middleware with `DynamicForm`. The old approach only supports predefined form templates ("init" and "followup"), while the new approach allows AI-generated forms with any fields.
|
|
1540
|
-
|
|
1541
|
-
`directivePostprocessor` looks for either `<Form type="init" />` tokens or
|
|
1542
|
-
`<Directive>{"component":"form","type":"init"}</Directive>` blocks and swaps them for placeholders that the widget upgrades into interactive UI. This approach is limited to the predefined form templates in `formDefinitions`.
|
|
1543
|
-
|
|
1544
|
-
### Script tag installation
|
|
1545
|
-
|
|
1546
|
-
The widget can be installed via a simple script tag, perfect for platforms where you can't compile custom code. There are two methods:
|
|
1547
|
-
|
|
1548
|
-
#### Method 1: Automatic installer (recommended)
|
|
1549
|
-
|
|
1550
|
-
The easiest way is to use the automatic installer script. It handles loading CSS and JavaScript, then initializes the widget automatically:
|
|
1551
|
-
|
|
1552
|
-
```html
|
|
1553
|
-
<!-- Add this before the closing </body> tag -->
|
|
1554
|
-
<script>
|
|
1555
|
-
window.siteAgentConfig = {
|
|
1556
|
-
target: 'body', // or '#my-container' for specific placement
|
|
1557
|
-
config: {
|
|
1558
|
-
apiUrl: 'https://your-proxy.com/api/chat/dispatch',
|
|
1559
|
-
launcher: {
|
|
1560
|
-
enabled: true,
|
|
1561
|
-
title: 'AI Assistant',
|
|
1562
|
-
subtitle: 'How can I help you?'
|
|
1563
|
-
},
|
|
1564
|
-
theme: {
|
|
1565
|
-
accent: '#2563eb',
|
|
1566
|
-
surface: '#ffffff'
|
|
1567
|
-
},
|
|
1568
|
-
// Optional: configure stream parser for JSON/XML responses
|
|
1569
|
-
// streamParser: () => window.AgentWidget.createJsonStreamParser()
|
|
1570
|
-
}
|
|
1571
|
-
};
|
|
1572
|
-
</script>
|
|
1573
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
1574
|
-
```
|
|
1575
|
-
|
|
1576
|
-
**Installer options:**
|
|
1577
|
-
|
|
1578
|
-
- `version` - Package version to load (default: `"latest"`)
|
|
1579
|
-
- `cdn` - CDN provider: `"jsdelivr"` or `"unpkg"` (default: `"jsdelivr"`)
|
|
1580
|
-
- `cssUrl` - Custom CSS URL (overrides CDN)
|
|
1581
|
-
- `jsUrl` - Custom JS URL (overrides CDN)
|
|
1582
|
-
- `target` - CSS selector or element where widget mounts (default: `"body"`)
|
|
1583
|
-
- `config` - Widget configuration object (see Configuration reference)
|
|
1584
|
-
- `autoInit` - Automatically initialize after loading (default: `true`)
|
|
1585
|
-
- `clientToken` - Client token for authentication (alternative to proxy `apiUrl`)
|
|
1586
|
-
- `flowId` - Flow ID for client token authentication
|
|
1587
|
-
- `apiUrl` - API URL for the chat endpoint (can also be set inside `config`)
|
|
1588
|
-
- `previewQueryParam` - Query parameter key that gates widget loading; widget only loads when the parameter is present and truthy
|
|
1589
|
-
- `useShadowDom` - Use Shadow DOM for style isolation (default: `false`)
|
|
1590
|
-
- `windowKey` - If provided, stores the widget handle on `window[windowKey]` for programmatic access
|
|
1591
|
-
- `onScriptLoad` - Fired as soon as the installer script executes, before it loads or gates anything (diagnostics / timing); signature: `({ version }) => void`
|
|
1592
|
-
- `onLauncherShown` - Fired when the floating launcher is painted on the page (page-load time — for "widget appeared" analytics); signature: `({ deferred, element? }) => void`
|
|
1593
|
-
- `onChatReady` - Fired when the widget is initialized and its controller API is callable (after first open in a deferred install); signature: `(handle) => void`
|
|
1594
|
-
- `onError` - Fired when a load step fails (`css` / `bundle` / `init`), so ad-blocked / timed-out installs don't fail silently; signature: `({ phase, error }) => void`
|
|
1595
|
-
- `onReady` - **Deprecated** alias of `onChatReady`; still works, removed in the next major; signature: `(handle) => void`
|
|
1596
|
-
|
|
1597
|
-
**Example with version pinning:**
|
|
1598
|
-
|
|
1599
|
-
```html
|
|
1600
|
-
<script>
|
|
1601
|
-
window.siteAgentConfig = {
|
|
1602
|
-
version: '0.1.0', // Pin to specific version
|
|
1603
|
-
config: {
|
|
1604
|
-
apiUrl: '/api/chat/dispatch',
|
|
1605
|
-
launcher: { enabled: true, title: 'Support Chat' }
|
|
1606
|
-
}
|
|
1607
|
-
};
|
|
1608
|
-
</script>
|
|
1609
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@0.1.0/dist/install.global.js"></script>
|
|
1610
|
-
```
|
|
1611
|
-
|
|
1612
|
-
#### Programmatic access with the installer
|
|
1613
|
-
|
|
1614
|
-
The installer is fully asynchronous (it waits for framework hydration, then loads CSS and JS). To interact with the widget after it initializes, use one of these approaches:
|
|
1615
|
-
|
|
1616
|
-
**`onChatReady` callback** — best when config and access logic live in the same script:
|
|
1617
|
-
|
|
1618
|
-
```html
|
|
1619
|
-
<script>
|
|
1620
|
-
window.siteAgentConfig = {
|
|
1621
|
-
clientToken: 'YOUR_TOKEN',
|
|
1622
|
-
windowKey: 'myChat',
|
|
1623
|
-
onChatReady(handle) {
|
|
1624
|
-
handle.on('message:sent', (e) => console.log('sent:', e));
|
|
1625
|
-
handle.on('message:received', (e) => console.log('received:', e));
|
|
1626
|
-
}
|
|
1627
|
-
};
|
|
1628
|
-
</script>
|
|
1629
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
1630
|
-
```
|
|
1631
|
-
|
|
1632
|
-
**`persona:chat-ready` event** — best for decoupled integration (e.g. tag managers, separate scripts):
|
|
1633
|
-
|
|
1634
|
-
```html
|
|
1635
|
-
<script>
|
|
1636
|
-
window.addEventListener('persona:chat-ready', (e) => {
|
|
1637
|
-
const handle = e.detail;
|
|
1638
|
-
handle.on('message:sent', (e) => console.log('sent:', e));
|
|
1639
|
-
});
|
|
1640
|
-
</script>
|
|
1641
|
-
|
|
1642
|
-
<!-- Can be in a different script, tag manager snippet, etc. -->
|
|
1643
|
-
<script>
|
|
1644
|
-
window.siteAgentConfig = { clientToken: 'YOUR_TOKEN' };
|
|
1645
|
-
</script>
|
|
1646
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
1647
|
-
```
|
|
1648
|
-
|
|
1649
|
-
**`windowKey`** — stores the handle on `window[windowKey]` for persistent global access. Combine with `onChatReady` or `persona:chat-ready` to know when it's available:
|
|
1650
|
-
|
|
1651
|
-
```html
|
|
1652
|
-
<script>
|
|
1653
|
-
window.siteAgentConfig = {
|
|
1654
|
-
clientToken: 'YOUR_TOKEN',
|
|
1655
|
-
windowKey: 'myChat'
|
|
1656
|
-
};
|
|
1657
|
-
</script>
|
|
1658
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
1659
|
-
|
|
1660
|
-
<script>
|
|
1661
|
-
window.addEventListener('persona:chat-ready', () => {
|
|
1662
|
-
// window.myChat is now available and persists until destroy()
|
|
1663
|
-
window.myChat.open();
|
|
1664
|
-
});
|
|
1665
|
-
</script>
|
|
1666
|
-
```
|
|
1667
|
-
|
|
1668
|
-
#### Method 2: Manual installation
|
|
1669
|
-
|
|
1670
|
-
For more control, manually load CSS and JavaScript:
|
|
1671
|
-
|
|
1672
|
-
```html
|
|
1673
|
-
<!-- Load CSS -->
|
|
1674
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/widget.css" />
|
|
1675
|
-
|
|
1676
|
-
<!-- Load JavaScript -->
|
|
1677
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/index.global.js"></script>
|
|
1678
|
-
|
|
1679
|
-
<!-- Initialize widget -->
|
|
1680
|
-
<script>
|
|
1681
|
-
const chatController = window.AgentWidget.initAgentWidget({
|
|
1682
|
-
target: '#persona-anchor', // or 'body' for floating launcher
|
|
1683
|
-
windowKey: 'chatWidget', // Optional: stores controller on window.chatWidget
|
|
1684
|
-
config: {
|
|
1685
|
-
apiUrl: '/api/chat/dispatch',
|
|
1686
|
-
launcher: {
|
|
1687
|
-
enabled: true,
|
|
1688
|
-
title: 'AI Assistant',
|
|
1689
|
-
subtitle: 'Here to help'
|
|
1690
|
-
},
|
|
1691
|
-
theme: {
|
|
1692
|
-
accent: '#111827',
|
|
1693
|
-
surface: '#f5f5f5'
|
|
1694
|
-
},
|
|
1695
|
-
// Optional: configure stream parser for JSON/XML responses
|
|
1696
|
-
streamParser: window.AgentWidget.createJsonStreamParser // or createXmlParser, createPlainTextParser
|
|
1697
|
-
}
|
|
1698
|
-
});
|
|
1699
|
-
|
|
1700
|
-
// Controller is now available as window.chatWidget (if windowKey was used)
|
|
1701
|
-
// or use the returned chatController variable
|
|
1702
|
-
</script>
|
|
1703
|
-
```
|
|
1704
|
-
|
|
1705
|
-
**CDN options:**
|
|
1706
|
-
|
|
1707
|
-
- **jsDelivr** (recommended): `https://cdn.jsdelivr.net/npm/@runtypelabs/persona@VERSION/dist/`
|
|
1708
|
-
- **unpkg**: `https://unpkg.com/@runtypelabs/persona@VERSION/dist/`
|
|
1709
|
-
|
|
1710
|
-
Replace `VERSION` with `latest` for auto-updates, or a specific version like `0.1.0` for stability.
|
|
1711
|
-
|
|
1712
|
-
**Available files:**
|
|
1713
|
-
|
|
1714
|
-
- `widget.css` - Stylesheet (required)
|
|
1715
|
-
- `index.global.js` - Widget JavaScript (IIFE format)
|
|
1716
|
-
- `install.global.js` - Automatic installer script
|
|
1717
|
-
|
|
1718
|
-
The script build exposes a `window.AgentWidget` global with `initAgentWidget()` and other exports, including parser functions:
|
|
1719
|
-
|
|
1720
|
-
- `window.AgentWidget.initAgentWidget()` - Initialize the widget
|
|
1721
|
-
- `window.AgentWidget.createPlainTextParser()` - Plain text parser (default)
|
|
1722
|
-
- `window.AgentWidget.createJsonStreamParser()` - JSON parser using schema-stream
|
|
1723
|
-
- `window.AgentWidget.createXmlParser()` - XML parser
|
|
1724
|
-
- `window.AgentWidget.markdownPostprocessor()` - Markdown postprocessor
|
|
1725
|
-
- `window.AgentWidget.directivePostprocessor()` - Directive postprocessor *(deprecated)*
|
|
1726
|
-
- `window.AgentWidget.componentRegistry` - Component registry for custom components
|
|
1727
|
-
|
|
1728
|
-
### React Framework Integration
|
|
1729
|
-
|
|
1730
|
-
The widget is fully compatible with React frameworks. Use the ESM imports to integrate it as a client component.
|
|
1731
|
-
|
|
1732
|
-
#### Framework Compatibility
|
|
1733
|
-
|
|
1734
|
-
| Framework | Compatible | Implementation Notes |
|
|
1735
|
-
|-----------|------------|---------------------|
|
|
1736
|
-
| **Vite** | ✅ Yes | No special requirements - works out of the box |
|
|
1737
|
-
| **Create React App** | ✅ Yes | No special requirements - works out of the box |
|
|
1738
|
-
| **Next.js** | ✅ Yes | Requires `'use client'` directive (App Router) |
|
|
1739
|
-
| **Remix** | ✅ Yes | Use dynamic import or `useEffect` guard for SSR |
|
|
1740
|
-
| **Gatsby** | ✅ Yes | Use in `wrapRootElement` or check `typeof window !== 'undefined'` |
|
|
1741
|
-
| **Astro** | ✅ Yes | Use `client:load` or `client:only="react"` directive |
|
|
1742
|
-
|
|
1743
|
-
#### Quick Start with Vite or Create React App
|
|
1744
|
-
|
|
1745
|
-
For client-side-only React frameworks (Vite, CRA), create a component:
|
|
1746
|
-
|
|
1747
|
-
```typescript
|
|
1748
|
-
// src/components/ChatWidget.tsx
|
|
1749
|
-
import { useEffect } from 'react';
|
|
1750
|
-
import '@runtypelabs/persona/widget.css';
|
|
1751
|
-
import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';
|
|
1752
|
-
import type { AgentWidgetInitHandle } from '@runtypelabs/persona';
|
|
1753
|
-
|
|
1754
|
-
export function ChatWidget() {
|
|
1755
|
-
useEffect(() => {
|
|
1756
|
-
let handle: AgentWidgetInitHandle | null = null;
|
|
1757
|
-
|
|
1758
|
-
handle = initAgentWidget({
|
|
1759
|
-
target: 'body',
|
|
1760
|
-
config: {
|
|
1761
|
-
apiUrl: "/api/chat/dispatch",
|
|
1762
|
-
theme: {
|
|
1763
|
-
primary: "#111827",
|
|
1764
|
-
accent: "#1d4ed8",
|
|
1765
|
-
},
|
|
1766
|
-
launcher: {
|
|
1767
|
-
enabled: true,
|
|
1768
|
-
title: "Chat Assistant",
|
|
1769
|
-
subtitle: "Here to help you get answers fast"
|
|
1770
|
-
},
|
|
1771
|
-
postprocessMessage: ({ text }) => markdownPostprocessor(text)
|
|
1772
|
-
}
|
|
1773
|
-
});
|
|
1774
|
-
|
|
1775
|
-
// Cleanup on unmount
|
|
1776
|
-
return () => {
|
|
1777
|
-
if (handle) {
|
|
1778
|
-
handle.destroy();
|
|
1779
|
-
}
|
|
1780
|
-
};
|
|
1781
|
-
}, []);
|
|
1782
|
-
|
|
1783
|
-
return null; // Widget injects itself into the DOM
|
|
1784
|
-
}
|
|
1785
|
-
```
|
|
1786
|
-
|
|
1787
|
-
Then use it in your app:
|
|
1788
|
-
|
|
1789
|
-
```typescript
|
|
1790
|
-
// src/App.tsx
|
|
1791
|
-
import { ChatWidget } from './components/ChatWidget';
|
|
1792
|
-
|
|
1793
|
-
function App() {
|
|
1794
|
-
return (
|
|
1795
|
-
<div>
|
|
1796
|
-
{/* Your app content */}
|
|
1797
|
-
<ChatWidget />
|
|
1798
|
-
</div>
|
|
1799
|
-
);
|
|
1800
|
-
}
|
|
1801
|
-
|
|
1802
|
-
export default App;
|
|
1803
|
-
```
|
|
1804
|
-
|
|
1805
|
-
#### Next.js Integration
|
|
1806
|
-
|
|
1807
|
-
For Next.js App Router, add the `'use client'` directive:
|
|
1808
|
-
|
|
1809
|
-
```typescript
|
|
1810
|
-
// components/ChatWidget.tsx
|
|
1811
|
-
'use client';
|
|
1812
|
-
|
|
1813
|
-
import { useEffect } from 'react';
|
|
1814
|
-
import '@runtypelabs/persona/widget.css';
|
|
1815
|
-
import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';
|
|
1816
|
-
import type { AgentWidgetInitHandle } from '@runtypelabs/persona';
|
|
1817
|
-
|
|
1818
|
-
export function ChatWidget() {
|
|
1819
|
-
useEffect(() => {
|
|
1820
|
-
let handle: AgentWidgetInitHandle | null = null;
|
|
1821
|
-
|
|
1822
|
-
handle = initAgentWidget({
|
|
1823
|
-
target: 'body',
|
|
1824
|
-
config: {
|
|
1825
|
-
apiUrl: "/api/chat/dispatch",
|
|
1826
|
-
launcher: {
|
|
1827
|
-
enabled: true,
|
|
1828
|
-
title: "Chat Assistant",
|
|
1829
|
-
},
|
|
1830
|
-
postprocessMessage: ({ text }) => markdownPostprocessor(text)
|
|
1831
|
-
}
|
|
1832
|
-
});
|
|
1833
|
-
|
|
1834
|
-
return () => {
|
|
1835
|
-
if (handle) {
|
|
1836
|
-
handle.destroy();
|
|
1837
|
-
}
|
|
1838
|
-
};
|
|
1839
|
-
}, []);
|
|
1840
|
-
|
|
1841
|
-
return null;
|
|
1842
|
-
}
|
|
1843
|
-
```
|
|
1844
|
-
|
|
1845
|
-
Use it in your layout or page:
|
|
1846
|
-
|
|
1847
|
-
```typescript
|
|
1848
|
-
// app/layout.tsx
|
|
1849
|
-
import { ChatWidget } from '@/components/ChatWidget';
|
|
1850
|
-
|
|
1851
|
-
export default function RootLayout({ children }) {
|
|
1852
|
-
return (
|
|
1853
|
-
<html lang="en">
|
|
1854
|
-
<body>
|
|
1855
|
-
{children}
|
|
1856
|
-
<ChatWidget />
|
|
1857
|
-
</body>
|
|
1858
|
-
</html>
|
|
1859
|
-
);
|
|
1860
|
-
}
|
|
1861
|
-
```
|
|
1862
|
-
|
|
1863
|
-
**Alternative: Dynamic Import (SSR-Safe)**
|
|
1864
|
-
|
|
1865
|
-
If you encounter SSR issues, use Next.js dynamic imports:
|
|
1866
|
-
|
|
1867
|
-
```typescript
|
|
1868
|
-
// app/layout.tsx
|
|
1869
|
-
import dynamic from 'next/dynamic';
|
|
1870
|
-
|
|
1871
|
-
const ChatWidget = dynamic(
|
|
1872
|
-
() => import('@/components/ChatWidget').then(mod => mod.ChatWidget),
|
|
1873
|
-
{ ssr: false }
|
|
1874
|
-
);
|
|
1875
|
-
|
|
1876
|
-
export default function RootLayout({ children }) {
|
|
1877
|
-
return (
|
|
1878
|
-
<html lang="en">
|
|
1879
|
-
<body>
|
|
1880
|
-
{children}
|
|
1881
|
-
<ChatWidget />
|
|
1882
|
-
</body>
|
|
1883
|
-
</html>
|
|
1884
|
-
);
|
|
1885
|
-
}
|
|
1886
|
-
```
|
|
1887
|
-
|
|
1888
|
-
#### Remix Integration
|
|
1889
|
-
|
|
1890
|
-
For Remix, guard the widget initialization with a client-side check:
|
|
1891
|
-
|
|
1892
|
-
```typescript
|
|
1893
|
-
// app/components/ChatWidget.tsx
|
|
1894
|
-
import { useEffect, useState } from 'react';
|
|
1895
|
-
|
|
1896
|
-
export function ChatWidget() {
|
|
1897
|
-
const [mounted, setMounted] = useState(false);
|
|
1898
|
-
|
|
1899
|
-
useEffect(() => {
|
|
1900
|
-
setMounted(true);
|
|
1901
|
-
|
|
1902
|
-
// Dynamic import to avoid SSR issues
|
|
1903
|
-
import('@runtypelabs/persona/widget.css');
|
|
1904
|
-
import('@runtypelabs/persona').then(({ initAgentWidget, markdownPostprocessor }) => {
|
|
1905
|
-
const handle = initAgentWidget({
|
|
1906
|
-
target: 'body',
|
|
1907
|
-
config: {
|
|
1908
|
-
apiUrl: "/api/chat/dispatch",
|
|
1909
|
-
launcher: { enabled: true },
|
|
1910
|
-
postprocessMessage: ({ text }) => markdownPostprocessor(text)
|
|
1911
|
-
}
|
|
1912
|
-
});
|
|
1913
|
-
|
|
1914
|
-
return () => handle?.destroy();
|
|
1915
|
-
});
|
|
1916
|
-
}, []);
|
|
1917
|
-
|
|
1918
|
-
if (!mounted) return null;
|
|
1919
|
-
return null;
|
|
1920
|
-
}
|
|
1921
|
-
```
|
|
1922
|
-
|
|
1923
|
-
#### Gatsby Integration
|
|
1924
|
-
|
|
1925
|
-
Use Gatsby's `wrapRootElement` API:
|
|
1926
|
-
|
|
1927
|
-
```typescript
|
|
1928
|
-
// gatsby-browser.js
|
|
1929
|
-
import { ChatWidget } from './src/components/ChatWidget';
|
|
1930
|
-
|
|
1931
|
-
export const wrapRootElement = ({ element }) => (
|
|
1932
|
-
<>
|
|
1933
|
-
{element}
|
|
1934
|
-
<ChatWidget />
|
|
1935
|
-
</>
|
|
1936
|
-
);
|
|
1937
|
-
```
|
|
1938
|
-
|
|
1939
|
-
#### Astro Integration
|
|
1940
|
-
|
|
1941
|
-
Use Astro's client directives with React islands:
|
|
1942
|
-
|
|
1943
|
-
```astro
|
|
1944
|
-
---
|
|
1945
|
-
// src/components/ChatWidget.astro
|
|
1946
|
-
import { ChatWidget } from './ChatWidget.tsx';
|
|
1947
|
-
---
|
|
1948
|
-
|
|
1949
|
-
<ChatWidget client:load />
|
|
1950
|
-
```
|
|
1951
|
-
|
|
1952
|
-
#### Using the Theme Configurator
|
|
1953
|
-
|
|
1954
|
-
For easy configuration generation, use the [Theme Configurator](https://github.com/becomevocal/chaty/tree/main/examples/embedded-app) which includes a "React (Client Component)" export option. It generates a complete React component with your custom theme, launcher settings, and all configuration options.
|
|
1955
|
-
|
|
1956
|
-
#### Installation
|
|
1957
|
-
|
|
1958
|
-
```bash
|
|
1959
|
-
npm install @runtypelabs/persona
|
|
1960
|
-
# or
|
|
1961
|
-
pnpm add @runtypelabs/persona
|
|
1962
|
-
# or
|
|
1963
|
-
yarn add @runtypelabs/persona
|
|
1964
|
-
```
|
|
1965
|
-
|
|
1966
|
-
#### Key Considerations
|
|
1967
|
-
|
|
1968
|
-
1. **CSS Import**: The CSS import (`import '@runtypelabs/persona/widget.css'`) works natively with all modern React build tools
|
|
1969
|
-
2. **Client-Side Only**: The widget manipulates the DOM, so it must run client-side only
|
|
1970
|
-
3. **Cleanup**: Always call `handle.destroy()` in the cleanup function to prevent memory leaks
|
|
1971
|
-
4. **API Routes**: Ensure your `apiUrl` points to a valid backend endpoint
|
|
1972
|
-
5. **TypeScript Support**: Full TypeScript definitions are included for all exports
|
|
1973
|
-
|
|
1974
|
-
### Using default configuration
|
|
1975
|
-
|
|
1976
|
-
The package exports a complete default configuration that you can use as a base:
|
|
1977
|
-
|
|
1978
|
-
```ts
|
|
1979
|
-
import { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from '@runtypelabs/persona';
|
|
1980
|
-
|
|
1981
|
-
// Option 1: Use defaults with selective overrides
|
|
1982
|
-
const controller = initAgentWidget({
|
|
1983
|
-
target: '#app',
|
|
1984
|
-
config: {
|
|
1985
|
-
...DEFAULT_WIDGET_CONFIG,
|
|
1986
|
-
apiUrl: '/api/chat/dispatch',
|
|
1987
|
-
theme: {
|
|
1988
|
-
...DEFAULT_WIDGET_CONFIG.theme,
|
|
1989
|
-
accent: '#custom-color' // Override only what you need
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
});
|
|
1993
|
-
|
|
1994
|
-
// Option 2: Use the merge helper
|
|
1995
|
-
const controller = initAgentWidget({
|
|
1996
|
-
target: '#app',
|
|
1997
|
-
config: mergeWithDefaults({
|
|
1998
|
-
apiUrl: '/api/chat/dispatch',
|
|
1999
|
-
theme: { accent: '#custom-color' }
|
|
2000
|
-
})
|
|
2001
|
-
});
|
|
2002
|
-
```
|
|
2003
|
-
|
|
2004
|
-
This ensures all configuration values are set to sensible defaults while allowing you to customize only what you need.
|
|
2005
|
-
|
|
2006
|
-
### Configuration reference
|
|
2007
|
-
|
|
2008
|
-
All options are safe to mutate via `initAgentWidget(...).update(newConfig)`.
|
|
2009
|
-
|
|
2010
|
-
For detailed theme styling properties, see [THEME-CONFIG.md](./THEME-CONFIG.md).
|
|
2011
|
-
|
|
2012
|
-
#### Core
|
|
2013
|
-
|
|
2014
|
-
| Option | Type | Description |
|
|
2015
|
-
| --- | --- | --- |
|
|
2016
|
-
| `apiUrl` | `string` | Proxy endpoint for your chat backend. Defaults to Runtype's cloud API. |
|
|
2017
|
-
| `flowId` | `string` | Runtype flow ID. The client sends it to the proxy to select a specific flow. |
|
|
2018
|
-
| `debug` | `boolean` | Emits verbose logs to `console`. Default: `false`. |
|
|
2019
|
-
| `headers` | `Record<string, string>` | Static headers forwarded with each request. |
|
|
2020
|
-
| `getHeaders` | `() => Record<string, string> \| Promise<...>` | Dynamic headers function called before each request. Use for auth tokens that may change. |
|
|
2021
|
-
| `customFetch` | `(url, init, payload) => Promise<Response>` | Replace the default `fetch` entirely. Receives URL, RequestInit, and the payload. |
|
|
2022
|
-
| `parseSSEEvent` | `(eventData) => { text?, done?, error? } \| null` | Transform non-standard SSE events into the expected format. Return `null` to ignore an event. |
|
|
2023
|
-
|
|
2024
|
-
#### Client Token Mode
|
|
2025
|
-
|
|
2026
|
-
When `clientToken` is set, the widget uses `/v1/client/*` endpoints directly from the browser instead of `/v1/dispatch`.
|
|
2027
|
-
|
|
2028
|
-
| Option | Type | Description |
|
|
2029
|
-
| --- | --- | --- |
|
|
2030
|
-
| `clientToken` | `string` | Client token for direct browser-to-API communication (e.g. `ct_live_flow01k7_...`). Mutually exclusive with `headers` auth. |
|
|
2031
|
-
| `onSessionInit` | `(session: ClientSession) => void` | Called when the session is initialized. Receives session ID, expiry, flow info. |
|
|
2032
|
-
| `onSessionExpired` | `() => void` | Called when the session expires or errors. Prompt the user to refresh. |
|
|
2033
|
-
| `getStoredSessionId` | `() => string \| null` | Return a previously stored session ID for session resumption. |
|
|
2034
|
-
| `setStoredSessionId` | `(sessionId: string) => void` | Persist the session ID so conversations can be resumed later. |
|
|
2035
|
-
|
|
2036
|
-
```typescript
|
|
2037
|
-
config: {
|
|
2038
|
-
clientToken: 'ct_live_flow01k7_a8b9c0d1e2f3g4h5i6j7k8l9',
|
|
2039
|
-
onSessionInit: (session) => console.log('Session:', session.sessionId),
|
|
2040
|
-
onSessionExpired: () => alert('Session expired — please refresh.'),
|
|
2041
|
-
getStoredSessionId: () => localStorage.getItem('session_id'),
|
|
2042
|
-
setStoredSessionId: (id) => localStorage.setItem('session_id', id)
|
|
2043
|
-
}
|
|
2044
|
-
```
|
|
2045
|
-
|
|
2046
|
-
#### Agent Mode
|
|
2047
|
-
|
|
2048
|
-
Use agent loop execution instead of flow dispatch. Mutually exclusive with `flowId`.
|
|
2049
|
-
|
|
2050
|
-
| Option | Type | Description |
|
|
2051
|
-
| --- | --- | --- |
|
|
2052
|
-
| `agent` | `AgentConfig` | Agent configuration (see sub-table below). Enables agent loop execution. |
|
|
2053
|
-
| `agentOptions` | `AgentRequestOptions` | Options for agent execution requests. Default: `{ streamResponse: true, recordMode: 'virtual' }`. |
|
|
2054
|
-
| `iterationDisplay` | `'separate' \| 'merged'` | How multi-iteration output is shown. `'separate'`: new bubble per iteration. `'merged'`: single bubble. Default: `'separate'`. |
|
|
2055
|
-
|
|
2056
|
-
**`AgentConfig`**
|
|
2057
|
-
|
|
2058
|
-
| Property | Type | Description |
|
|
2059
|
-
| --- | --- | --- |
|
|
2060
|
-
| `name` | `string` | Agent display name. |
|
|
2061
|
-
| `model` | `string` | Model identifier (e.g. `'openai:gpt-4o-mini'`). |
|
|
2062
|
-
| `systemPrompt` | `string` | System prompt for the agent. |
|
|
2063
|
-
| `temperature` | `number?` | Temperature for model responses. |
|
|
2064
|
-
| `loopConfig` | `AgentLoopConfig?` | Loop behavior configuration (see below). |
|
|
2065
|
-
|
|
2066
|
-
**`AgentLoopConfig`**
|
|
2067
|
-
|
|
2068
|
-
| Property | Type | Description |
|
|
2069
|
-
| --- | --- | --- |
|
|
2070
|
-
| `maxTurns` | `number` | Maximum number of agent turns (1-100). The loop continues while the model calls tools. |
|
|
2071
|
-
| `maxCost` | `number?` | Maximum cost budget in USD. Agent stops when exceeded. |
|
|
2072
|
-
| `enableReflection` | `boolean?` | Enable periodic reflection during execution. |
|
|
2073
|
-
| `reflectionInterval` | `number?` | Number of iterations between reflections (1-50). |
|
|
2074
|
-
|
|
2075
|
-
**`AgentToolsConfig`**
|
|
2076
|
-
|
|
2077
|
-
| Property | Type | Description |
|
|
2078
|
-
| --- | --- | --- |
|
|
2079
|
-
| `toolIds` | `string[]?` | Tool IDs to enable (e.g., `"builtin:exa"`, `"builtin:dalle"`). |
|
|
2080
|
-
| `toolConfigs` | `Record<string, Record<string, unknown>>?` | Per-tool configuration overrides keyed by tool ID. |
|
|
2081
|
-
| `runtimeTools` | `Array<Record<string, unknown>>?` | Inline tool definitions for runtime-defined tools. |
|
|
2082
|
-
| `mcpServers` | `Array<Record<string, unknown>>?` | Custom MCP server connections. |
|
|
2083
|
-
| `maxToolCalls` | `number?` | Maximum number of tool invocations per execution. |
|
|
2084
|
-
| `approval` | `{ require: string[] \| boolean; timeout?: number }?` | Tool approval configuration for human-in-the-loop workflows. |
|
|
2085
|
-
|
|
2086
|
-
**`AgentRequestOptions`**
|
|
2087
|
-
|
|
2088
|
-
| Property | Type | Description |
|
|
2089
|
-
| --- | --- | --- |
|
|
2090
|
-
| `streamResponse` | `boolean?` | Stream the response (should be `true` for widget usage). |
|
|
2091
|
-
| `recordMode` | `'virtual' \| 'existing' \| 'create'?` | Record persistence mode. |
|
|
2092
|
-
| `storeResults` | `boolean?` | Store results server-side. |
|
|
2093
|
-
| `debugMode` | `boolean?` | Enable debug mode for additional event data. |
|
|
2094
|
-
|
|
2095
|
-
```typescript
|
|
2096
|
-
config: {
|
|
2097
|
-
agent: {
|
|
2098
|
-
name: 'Research Assistant',
|
|
2099
|
-
model: 'qwen/qwen3-8b',
|
|
2100
|
-
systemPrompt: 'You are a research assistant with access to web search.',
|
|
2101
|
-
tools: { toolIds: ['builtin:exa'] },
|
|
2102
|
-
loopConfig: { maxTurns: 5 }
|
|
2103
|
-
},
|
|
2104
|
-
agentOptions: { streamResponse: true, recordMode: 'virtual' },
|
|
2105
|
-
iterationDisplay: 'merged'
|
|
2106
|
-
}
|
|
2107
|
-
```
|
|
2108
|
-
|
|
2109
|
-
#### UI & Theme
|
|
2110
|
-
|
|
2111
|
-
| Option | Type | Description |
|
|
2112
|
-
| --- | --- | --- |
|
|
2113
|
-
| `theme` | `DeepPartial<PersonaTheme>` | Semantic tokens (`palette`, `semantic`, `components`). See [THEME-CONFIG.md](./THEME-CONFIG.md). Flat v1-style objects are still accepted at runtime (with a console warning) and migrated internally. |
|
|
2114
|
-
| `darkTheme` | `DeepPartial<PersonaTheme>` | Dark-mode token overrides, merged over `theme` when the active scheme is dark. |
|
|
2115
|
-
| `colorScheme` | `'light' \| 'dark' \| 'auto'` | Color scheme mode. `'auto'` detects from `<html class="dark">` or `prefers-color-scheme`. Default: `'light'`. |
|
|
2116
|
-
| `copy` | `{ welcomeTitle?, welcomeSubtitle?, inputPlaceholder?, sendButtonLabel? }` | Customize user-facing text strings. |
|
|
2117
|
-
| `autoFocusInput` | `boolean` | Focus the chat input after the panel opens. Skips when voice is active. Default: `false`. |
|
|
2118
|
-
| `launcherWidth` | `string` | CSS width for the floating launcher panel (e.g. `'320px'`). Default: `'min(440px, calc(100vw - 24px))'`. |
|
|
2119
|
-
|
|
2120
|
-
#### Launcher
|
|
2121
|
-
|
|
2122
|
-
Controls the floating launcher button and panel.
|
|
2123
|
-
|
|
2124
|
-
| Option | Type | Description |
|
|
2125
|
-
| --- | --- | --- |
|
|
2126
|
-
| `launcher` | `AgentWidgetLauncherConfig` | Launcher button configuration (see key properties below). See [THEME-CONFIG.md](./THEME-CONFIG.md) for the full list of icon, button, and style properties. |
|
|
2127
|
-
|
|
2128
|
-
**Key `launcher` properties**
|
|
2129
|
-
|
|
2130
|
-
| Property | Type | Description |
|
|
2131
|
-
| --- | --- | --- |
|
|
2132
|
-
| `enabled` | `boolean?` | Show the launcher button. |
|
|
2133
|
-
| `autoExpand` | `boolean?` | Auto-open the chat panel on load. |
|
|
2134
|
-
| `title` | `string?` | Launcher header title text. |
|
|
2135
|
-
| `subtitle` | `string?` | Launcher header subtitle text. |
|
|
2136
|
-
| `iconUrl` | `string?` | URL for the launcher icon image. |
|
|
2137
|
-
| `position` | `'bottom-right' \| 'bottom-left' \| 'top-right' \| 'top-left'?` | Screen corner position. |
|
|
2138
|
-
| `mountMode` | `'floating' \| 'docked'?` | Mount as the existing floating launcher or wrap the target with a docked side panel. Default: `'floating'`. |
|
|
2139
|
-
| `dock` | `{ side?, width?, animate?, reveal? }?` | Dock layout. Defaults: right / `420px` / `animate: true` / `reveal: 'resize'`. `reveal: 'emerge'` = content column animates like resize but the panel stays fixed `dock.width` (clip-in). `reveal: 'overlay'` = transform overlay; `reveal: 'push'` = sliding track. `animate: false` snaps without transition. |
|
|
2140
|
-
| `width` | `string?` | Width of the launcher button. |
|
|
2141
|
-
| `fullHeight` | `boolean?` | Fill the full height of the container. Default: `false`. |
|
|
2142
|
-
| `sidebarMode` | `boolean?` | Flush sidebar layout with no border-radius or margins. Default: `false`. |
|
|
2143
|
-
| `sidebarWidth` | `string?` | Width when `sidebarMode` is true. Default: `'420px'`. |
|
|
2144
|
-
| `heightOffset` | `number?` | Pixels to subtract from panel height (for fixed headers, etc.). Default: `0`. |
|
|
2145
|
-
| `clearChat` | `AgentWidgetClearChatConfig?` | Clear chat button configuration (enabled, placement, icon, styling). |
|
|
2146
|
-
| `border` | `string?` | Border style for the launcher button. Default: `'1px solid #e5e7eb'`. |
|
|
2147
|
-
| `shadow` | `string?` | Box shadow for the launcher button. |
|
|
2148
|
-
| `collapsedMaxWidth` | `string?` | CSS `max-width` for the floating launcher pill when the panel is closed (title/subtitle truncate with ellipsis; full text in `title` tooltip). Does not affect the open panel (`width`). |
|
|
2149
|
-
|
|
2150
|
-
In docked mode, `position`, `fullHeight`, and `sidebarMode` are ignored because the widget fills the dock slot created around the target container.
|
|
2151
|
-
|
|
2152
|
-
#### Layout
|
|
2153
|
-
|
|
2154
|
-
| Option | Type | Description |
|
|
2155
|
-
| --- | --- | --- |
|
|
2156
|
-
| `layout` | `AgentWidgetLayoutConfig` | Layout configuration (see sub-properties below). |
|
|
2157
|
-
|
|
2158
|
-
**`AgentWidgetLayoutConfig`**
|
|
2159
|
-
|
|
2160
|
-
| Property | Type | Description |
|
|
2161
|
-
| --- | --- | --- |
|
|
2162
|
-
| `showHeader` | `boolean?` | Show/hide the header section entirely. Default: `true`. |
|
|
2163
|
-
| `showFooter` | `boolean?` | Show/hide the footer/composer section entirely. Default: `true`. |
|
|
2164
|
-
| `header` | `AgentWidgetHeaderLayoutConfig?` | Header customization (see below). |
|
|
2165
|
-
| `messages` | `AgentWidgetMessageLayoutConfig?` | Message display customization (see below). |
|
|
2166
|
-
| `slots` | `Record<WidgetLayoutSlot, SlotRenderer>?` | Content injection into named slots. |
|
|
2167
|
-
|
|
2168
|
-
**`header`** — `AgentWidgetHeaderLayoutConfig`
|
|
2169
|
-
|
|
2170
|
-
| Property | Type | Description |
|
|
2171
|
-
| --- | --- | --- |
|
|
2172
|
-
| `layout` | `'default' \| 'minimal'?` | Header preset. |
|
|
2173
|
-
| `showIcon` | `boolean?` | Show/hide the header icon. |
|
|
2174
|
-
| `showTitle` | `boolean?` | Show/hide the title. |
|
|
2175
|
-
| `showSubtitle` | `boolean?` | Show/hide the subtitle. |
|
|
2176
|
-
| `showCloseButton` | `boolean?` | Show/hide the close button. |
|
|
2177
|
-
| `showClearChat` | `boolean?` | Show/hide the clear chat button. |
|
|
2178
|
-
| `render` | `(ctx: HeaderRenderContext) => HTMLElement?` | Custom renderer that replaces the entire header. |
|
|
2179
|
-
|
|
2180
|
-
**`messages`** — `AgentWidgetMessageLayoutConfig`
|
|
2181
|
-
|
|
2182
|
-
| Property | Type | Description |
|
|
2183
|
-
| --- | --- | --- |
|
|
2184
|
-
| `layout` | `'bubble' \| 'flat' \| 'minimal'?` | Message style preset. Default: `'bubble'`. |
|
|
2185
|
-
| `avatar` | `{ show?, position?, userAvatar?, assistantAvatar? }?` | Avatar configuration. |
|
|
2186
|
-
| `timestamp` | `{ show?, position?, format? }?` | Timestamp configuration. |
|
|
2187
|
-
| `groupConsecutive` | `boolean?` | Group consecutive messages from the same role. |
|
|
2188
|
-
| `renderUserMessage` | `(ctx: MessageRenderContext) => HTMLElement?` | Custom user message renderer. |
|
|
2189
|
-
| `renderAssistantMessage` | `(ctx: MessageRenderContext) => HTMLElement?` | Custom assistant message renderer. |
|
|
2190
|
-
|
|
2191
|
-
**Available `slots`**: `header-left`, `header-center`, `header-right`, `body-top`, `messages`, `body-bottom`, `footer-top`, `composer`, `footer-bottom`.
|
|
2192
|
-
|
|
2193
|
-
#### Message Display
|
|
2194
|
-
|
|
2195
|
-
| Option | Type | Description |
|
|
2196
|
-
| --- | --- | --- |
|
|
2197
|
-
| `postprocessMessage` | `(ctx: { text, message, streaming, raw? }) => string` | Transform message text before rendering (return HTML). |
|
|
2198
|
-
| `markdown` | `AgentWidgetMarkdownConfig` | Markdown rendering configuration (see sub-table). |
|
|
2199
|
-
| `messageActions` | `AgentWidgetMessageActionsConfig` | Action buttons on assistant messages (see sub-table). |
|
|
2200
|
-
| `loadingIndicator` | `AgentWidgetLoadingIndicatorConfig` | Customize the loading indicator (see sub-table). |
|
|
2201
|
-
|
|
2202
|
-
**`markdown`** — `AgentWidgetMarkdownConfig`
|
|
2203
|
-
|
|
2204
|
-
| Property | Type | Description |
|
|
2205
|
-
| --- | --- | --- |
|
|
2206
|
-
| `options` | `AgentWidgetMarkdownOptions?` | Marked parser options: `gfm` (default: `true`), `breaks` (default: `true`), `pedantic`, `headerIds`, `headerPrefix`, `mangle`, `silent`. |
|
|
2207
|
-
| `renderer` | `AgentWidgetMarkdownRendererOverrides?` | Custom renderers for elements: `heading`, `code`, `blockquote`, `table`, `link`, `image`, `list`, `listitem`, `paragraph`, `codespan`, `strong`, `em`, `hr`, `br`, `del`, `checkbox`, `html`, `text`. Return `false` to use default. |
|
|
2208
|
-
| `disableDefaultStyles` | `boolean?` | Skip all default markdown CSS styles. Default: `false`. |
|
|
2209
|
-
|
|
2210
|
-
**`messageActions`** — `AgentWidgetMessageActionsConfig`
|
|
2211
|
-
|
|
2212
|
-
| Property | Type | Description |
|
|
2213
|
-
| --- | --- | --- |
|
|
2214
|
-
| `enabled` | `boolean?` | Enable/disable message actions. Default: `true`. |
|
|
2215
|
-
| `showCopy` | `boolean?` | Show copy button. Default: `true`. |
|
|
2216
|
-
| `showUpvote` | `boolean?` | Show upvote button. Auto-submitted with `clientToken`. Default: `false`. |
|
|
2217
|
-
| `showDownvote` | `boolean?` | Show downvote button. Auto-submitted with `clientToken`. Default: `false`. |
|
|
2218
|
-
| `visibility` | `'always' \| 'hover'?` | Button visibility mode. Default: `'hover'`. |
|
|
2219
|
-
| `align` | `'left' \| 'center' \| 'right'?` | Horizontal alignment. Default: `'right'`. |
|
|
2220
|
-
| `layout` | `'pill-inside' \| 'row-inside'?` | Button layout style. Default: `'pill-inside'`. |
|
|
2221
|
-
| `onFeedback` | `(feedback: AgentWidgetMessageFeedback) => void?` | Callback on upvote/downvote. Called in addition to automatic submission with `clientToken`. |
|
|
2222
|
-
| `onCopy` | `(message: AgentWidgetMessage) => void?` | Callback on copy. Called in addition to automatic tracking with `clientToken`. |
|
|
2223
|
-
|
|
2224
|
-
**`loadingIndicator`** — `AgentWidgetLoadingIndicatorConfig`
|
|
2225
|
-
|
|
2226
|
-
| Property | Type | Description |
|
|
2227
|
-
| --- | --- | --- |
|
|
2228
|
-
| `showBubble` | `boolean?` | Show bubble background around standalone indicator. Default: `true`. |
|
|
2229
|
-
| `render` | `(ctx: LoadingIndicatorRenderContext) => HTMLElement \| null?` | Custom render function. Return `null` to hide. Add `data-preserve-animation="true"` for custom animations. |
|
|
2230
|
-
| `renderIdle` | `(ctx: IdleIndicatorRenderContext) => HTMLElement \| null?` | Custom idle state renderer (shown when not streaming). Return `null` to hide. |
|
|
2231
|
-
|
|
2232
|
-
#### Streaming & Parsing
|
|
2233
|
-
|
|
2234
|
-
| Option | Type | Description |
|
|
2235
|
-
| --- | --- | --- |
|
|
2236
|
-
| `parserType` | `'plain' \| 'json' \| 'regex-json' \| 'xml'` | Built-in parser selector. `'plain'` (default), `'json'` (partial-json), `'regex-json'` (regex-based), `'xml'`. |
|
|
2237
|
-
| `streamParser` | `() => AgentWidgetStreamParser` | Custom stream parser factory. Takes precedence over `parserType`. See [Stream Parser Configuration](#stream-parser-configuration). |
|
|
2238
|
-
| `enableComponentStreaming` | `boolean` | Update component props incrementally as they stream in. Default: `true`. |
|
|
2239
|
-
|
|
2240
|
-
#### Components
|
|
2241
|
-
|
|
2242
|
-
| Option | Type | Description |
|
|
2243
|
-
| --- | --- | --- |
|
|
2244
|
-
| `components` | `Record<string, AgentWidgetComponentRenderer>` | Registry of custom components rendered from JSON directives (`{"component": "Name", "props": {...}}`). Each renderer receives `(props, context)` and returns an `HTMLElement`. Event listeners attached via `addEventListener` (and any other imperative state on the returned element) are preserved across transcript updates — the widget injects the live element directly into the morphed wrapper so listeners survive subsequent re-renders. The renderer is re-invoked when the directive's props change; for state you want to persist across prop changes, hold it in a closure outside the render. |
|
|
2245
|
-
|
|
2246
|
-
```typescript
|
|
2247
|
-
config: {
|
|
2248
|
-
components: {
|
|
2249
|
-
ProductCard: (props, { message, config, updateProps }) => {
|
|
2250
|
-
const card = document.createElement('div');
|
|
2251
|
-
card.innerHTML = `<h3>${props.title}</h3><p>$${props.price}</p>`;
|
|
2252
|
-
return card;
|
|
2253
|
-
}
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
```
|
|
2257
|
-
|
|
2258
|
-
#### Voice Recognition
|
|
2259
|
-
|
|
2260
|
-
| Option | Type | Description |
|
|
2261
|
-
| --- | --- | --- |
|
|
2262
|
-
| `voiceRecognition` | `AgentWidgetVoiceRecognitionConfig` | Voice input configuration (see sub-table). |
|
|
2263
|
-
|
|
2264
|
-
**`voiceRecognition`** — `AgentWidgetVoiceRecognitionConfig`
|
|
2265
|
-
|
|
2266
|
-
| Property | Type | Description |
|
|
2267
|
-
| --- | --- | --- |
|
|
2268
|
-
| `enabled` | `boolean?` | Enable voice recognition. |
|
|
2269
|
-
| `pauseDuration` | `number?` | Silence duration (ms) before auto-stop. |
|
|
2270
|
-
| `processingText` | `string?` | Placeholder text while processing voice. Default: `'Processing voice...'`. |
|
|
2271
|
-
| `processingErrorText` | `string?` | Error text on voice failure. Default: `'Voice processing failed. Please try again.'`. |
|
|
2272
|
-
| `autoResume` | `boolean \| 'assistant'?` | Auto-resume listening after playback. `'assistant'` resumes after assistant finishes. |
|
|
2273
|
-
| `provider` | `{ type, browser?, runtype?, custom? }?` | Voice provider configuration (see below). |
|
|
2274
|
-
| `iconName`, `iconSize`, `iconColor`, `backgroundColor`, `borderColor`, `borderWidth`, `paddingX`, `paddingY`, `tooltipText`, `showTooltip`, `recordingIconColor`, `recordingBackgroundColor`, `recordingBorderColor`, `showRecordingIndicator` | various | Styling options for the voice button. See [THEME-CONFIG.md](./THEME-CONFIG.md). |
|
|
2275
|
-
|
|
2276
|
-
**`provider.browser`**
|
|
2277
|
-
|
|
2278
|
-
| Property | Type | Description |
|
|
2279
|
-
| --- | --- | --- |
|
|
2280
|
-
| `language` | `string?` | Recognition language (e.g. `'en-US'`). |
|
|
2281
|
-
| `continuous` | `boolean?` | Continuous listening mode. |
|
|
2282
|
-
|
|
2283
|
-
**`provider.runtype`**
|
|
2284
|
-
|
|
2285
|
-
| Property | Type | Description |
|
|
2286
|
-
| --- | --- | --- |
|
|
2287
|
-
| `agentId` | `string` | Runtype agent ID for server-side voice. |
|
|
2288
|
-
| `clientToken` | `string` | Client token for authentication. |
|
|
2289
|
-
| `host` | `string?` | API host override. |
|
|
2290
|
-
| `voiceId` | `string?` | Voice ID for TTS. |
|
|
2291
|
-
| `pauseDuration` | `number?` | Silence duration (ms) before auto-stop. Default: `2000`. |
|
|
2292
|
-
| `silenceThreshold` | `number?` | RMS volume threshold for silence detection. Default: `0.01`. |
|
|
2293
|
-
|
|
2294
|
-
```typescript
|
|
2295
|
-
// Browser voice recognition
|
|
2296
|
-
config: {
|
|
2297
|
-
voiceRecognition: {
|
|
2298
|
-
enabled: true,
|
|
2299
|
-
provider: { type: 'browser', browser: { language: 'en-US' } }
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
|
|
2303
|
-
// Runtype server-side voice
|
|
2304
|
-
config: {
|
|
2305
|
-
voiceRecognition: {
|
|
2306
|
-
enabled: true,
|
|
2307
|
-
provider: {
|
|
2308
|
-
type: 'runtype',
|
|
2309
|
-
runtype: { agentId: 'agent_01abc', clientToken: 'ct_live_...' }
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
```
|
|
2314
|
-
|
|
2315
|
-
#### Text-to-Speech
|
|
2316
|
-
|
|
2317
|
-
| Option | Type | Description |
|
|
2318
|
-
| --- | --- | --- |
|
|
2319
|
-
| `textToSpeech` | `TextToSpeechConfig` | TTS configuration (see sub-table). |
|
|
2320
|
-
|
|
2321
|
-
**`textToSpeech`** — `TextToSpeechConfig`
|
|
2322
|
-
|
|
2323
|
-
| Property | Type | Description |
|
|
2324
|
-
| --- | --- | --- |
|
|
2325
|
-
| `enabled` | `boolean` | Enable text-to-speech for assistant messages. |
|
|
2326
|
-
| `provider` | `'browser' \| 'runtype'?` | `'browser'` uses Web Speech API (default). `'runtype'` delegates to server. |
|
|
2327
|
-
| `browserFallback` | `boolean?` | When provider is `'runtype'`, fall back to browser TTS for text-typed responses. Default: `false`. |
|
|
2328
|
-
| `voice` | `string?` | Browser TTS voice name (e.g. `'Google US English'`). |
|
|
2329
|
-
| `pickVoice` | `(voices: SpeechSynthesisVoice[]) => SpeechSynthesisVoice?` | Custom voice picker when `voice` is not set. |
|
|
2330
|
-
| `rate` | `number?` | Speech rate (0.1–10). Default: `1`. |
|
|
2331
|
-
| `pitch` | `number?` | Speech pitch (0–2). Default: `1`. |
|
|
2332
|
-
|
|
2333
|
-
```typescript
|
|
2334
|
-
config: {
|
|
2335
|
-
textToSpeech: {
|
|
2336
|
-
enabled: true,
|
|
2337
|
-
provider: 'browser',
|
|
2338
|
-
voice: 'Google US English',
|
|
2339
|
-
rate: 1.2,
|
|
2340
|
-
pitch: 1.0
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
```
|
|
2344
|
-
|
|
2345
|
-
#### Tool Calls & Approvals
|
|
2346
|
-
|
|
2347
|
-
| Option | Type | Description |
|
|
2348
|
-
| --- | --- | --- |
|
|
2349
|
-
| `toolCall` | `AgentWidgetToolCallConfig` | Styling for tool call bubbles: `backgroundColor`, `borderColor`, `borderWidth`, `borderRadius`, `headerBackgroundColor`, `headerTextColor`, `contentBackgroundColor`, `contentTextColor`, `codeBlockBackgroundColor`, `codeBlockBorderColor`, `codeBlockTextColor`, `toggleTextColor`, `labelTextColor`, and padding options. |
|
|
2350
|
-
| `approval` | `AgentWidgetApprovalConfig \| false` | Tool approval bubble configuration. Set to `false` to disable built-in approval handling. |
|
|
2351
|
-
|
|
2352
|
-
**`approval`** — `AgentWidgetApprovalConfig`
|
|
2353
|
-
|
|
2354
|
-
| Property | Type | Description |
|
|
2355
|
-
| --- | --- | --- |
|
|
2356
|
-
| `title` | `string?` | Title text above the description. |
|
|
2357
|
-
| `approveLabel` | `string?` | Label for the approve button. |
|
|
2358
|
-
| `denyLabel` | `string?` | Label for the deny button. |
|
|
2359
|
-
| `detailsDisplay` | `'collapsed' \| 'expanded' \| 'hidden'?` | How the technical details (agent-facing tool description + raw parameters JSON) are presented. Default: `'collapsed'` (behind a "Show details" toggle). `'expanded'` shows them open; `'hidden'` never renders them. |
|
|
2360
|
-
| `showDetailsLabel`, `hideDetailsLabel` | `string?` | Labels for the details toggle. Defaults: `"Show details"` / `"Hide details"`. |
|
|
2361
|
-
| `formatDescription` | `(approval) => string \| undefined` | Build the user-facing summary line. Receives `{ toolName, toolType, description, parameters, displayTitle }`. Return a falsy value to fall back to the default copy for that approval. |
|
|
2362
|
-
| `backgroundColor`, `borderColor`, `titleColor`, `descriptionColor` | `string?` | Bubble styling. |
|
|
2363
|
-
| `approveButtonColor`, `approveButtonTextColor` | `string?` | Approve button styling. |
|
|
2364
|
-
| `denyButtonColor`, `denyButtonTextColor` | `string?` | Deny button styling. |
|
|
2365
|
-
| `parameterBackgroundColor`, `parameterTextColor` | `string?` | Parameters block styling. |
|
|
2366
|
-
| `onDecision` | `(data, decision) => Promise<Response \| ReadableStream \| void>?` | Custom approval handler. Return `void` for SDK auto-resolve. |
|
|
2367
|
-
|
|
2368
|
-
**How the summary line is chosen.** A tool's wire `description` is written for the
|
|
2369
|
-
agent (usage rules, prompt prose), not for end users, so the bubble doesn't lead
|
|
2370
|
-
with it. The user-facing summary resolves in priority order:
|
|
2371
|
-
|
|
2372
|
-
1. `formatDescription(...)` from your config, when it returns a non-empty string
|
|
2373
|
-
2. The display title the tool declared via the WebMCP spec's `ToolDescriptor.title`
|
|
2374
|
-
(e.g. `"Add to Cart"`) — WebMCP tools only
|
|
2375
|
-
3. The humanized tool name — `add_to_cart` / `webmcp:add_to_cart` →
|
|
2376
|
-
"Add to cart", `getProductDetails` → "Get product details"
|
|
2377
|
-
4. The raw `description` (only when the approval carries no tool name at all)
|
|
2378
|
-
|
|
2379
|
-
The agent-facing description and the raw parameters JSON stay available behind
|
|
2380
|
-
the "Show details" toggle (see `detailsDisplay`).
|
|
2381
|
-
|
|
2382
|
-
```ts
|
|
2383
|
-
initAgentWidget({
|
|
2384
|
-
// ...config
|
|
2385
|
-
approval: {
|
|
2386
|
-
// Optional: fully custom, parameter-aware copy per tool. Falsy returns
|
|
2387
|
-
// fall back to the default summary, so a sparse map is fine.
|
|
2388
|
-
formatDescription: ({ toolName, parameters }) =>
|
|
2389
|
-
({
|
|
2390
|
-
add_to_cart: "Add these items to your shopping cart",
|
|
2391
|
-
apply_promo_code: `Apply promo code “${(parameters as { code?: string })?.code}”`,
|
|
2392
|
-
})[toolName.replace(/^webmcp:/, "")],
|
|
2393
|
-
},
|
|
2394
|
-
});
|
|
2395
|
-
```
|
|
2396
|
-
|
|
2397
|
-
#### Suggestion Chips
|
|
2398
|
-
|
|
2399
|
-
| Option | Type | Description |
|
|
2400
|
-
| --- | --- | --- |
|
|
2401
|
-
| `suggestionChips` | `string[]` | Render quick reply buttons above the composer. |
|
|
2402
|
-
| `suggestionChipsConfig` | `AgentWidgetSuggestionChipsConfig` | Chip styling: `fontFamily` (`'sans-serif' \| 'serif' \| 'mono'`), `fontWeight`, `paddingX`, `paddingY`. |
|
|
2403
|
-
|
|
2404
|
-
#### Input & Composer
|
|
2405
|
-
|
|
2406
|
-
| Option | Type | Description |
|
|
2407
|
-
| --- | --- | --- |
|
|
2408
|
-
| `sendButton` | `AgentWidgetSendButtonConfig` | Send button customization: `borderWidth`, `borderColor`, `paddingX`, `paddingY`, `iconText`, `iconName`, `useIcon`, `tooltipText`, `showTooltip`, `backgroundColor`, `textColor`, `size`. |
|
|
2409
|
-
| `attachments` | `AgentWidgetAttachmentsConfig` | File attachment configuration (see sub-table). |
|
|
2410
|
-
| `formEndpoint` | `string` | Endpoint used by built-in directives. Default: `'/form'`. |
|
|
2411
|
-
|
|
2412
|
-
**`attachments`** — `AgentWidgetAttachmentsConfig`
|
|
2413
|
-
|
|
2414
|
-
| Property | Type | Description |
|
|
2415
|
-
| --- | --- | --- |
|
|
2416
|
-
| `enabled` | `boolean?` | Enable file attachments. Default: `false`. |
|
|
2417
|
-
| `allowedTypes` | `string[]?` | Allowed MIME types. Default: `['image/png', 'image/jpeg', 'image/gif', 'image/webp']`. |
|
|
2418
|
-
| `maxFileSize` | `number?` | Maximum file size in bytes. Default: `10485760` (10 MB). |
|
|
2419
|
-
| `maxFiles` | `number?` | Maximum files per message. Default: `4`. |
|
|
2420
|
-
| `buttonIconName` | `string?` | Lucide icon name. Default: `'image-plus'`. |
|
|
2421
|
-
| `buttonTooltipText` | `string?` | Tooltip text. Default: `'Attach image'`. |
|
|
2422
|
-
| `onFileRejected` | `(file, reason: 'type' \| 'size' \| 'count') => void?` | Callback when a file is rejected. |
|
|
2423
|
-
|
|
2424
|
-
#### Status Indicator
|
|
2425
|
-
|
|
2426
|
-
| Option | Type | Description |
|
|
2427
|
-
| --- | --- | --- |
|
|
2428
|
-
| `statusIndicator` | `AgentWidgetStatusIndicatorConfig` | Connection status display: `visible`, `idleText`, `connectingText`, `connectedText`, `errorText`. |
|
|
2429
|
-
|
|
2430
|
-
#### Features
|
|
2431
|
-
|
|
2432
|
-
| Option | Type | Description |
|
|
2433
|
-
| --- | --- | --- |
|
|
2434
|
-
| `features` | `AgentWidgetFeatureFlags` | Feature flag toggles (see sub-table). |
|
|
2435
|
-
|
|
2436
|
-
**`features`** — `AgentWidgetFeatureFlags`
|
|
2437
|
-
|
|
2438
|
-
| Property | Type | Description |
|
|
2439
|
-
| --- | --- | --- |
|
|
2440
|
-
| `showReasoning` | `boolean?` | Show thinking/reasoning bubbles. Default: `true`. |
|
|
2441
|
-
| `showToolCalls` | `boolean?` | Show tool usage bubbles. Default: `true`. |
|
|
2442
|
-
| `showEventStreamToggle` | `boolean?` | Show the event stream inspector toggle in the header. Default: `false`. |
|
|
2443
|
-
| `composerHistory` | `boolean?` | `Up`/`Down` arrows in the composer navigate previously sent user messages for re-entry/editing (shell / Slack style). Entered only when the caret is at the start of the input, so multi-line cursor movement is preserved. Default: `true`. |
|
|
2444
|
-
| `eventStream` | `EventStreamConfig?` | Event stream inspector configuration: `badgeColors`, `timestampFormat`, `showSequenceNumbers`, `maxEvents`, `descriptionFields`, `classNames`. |
|
|
2445
|
-
| `artifacts` | `AgentWidgetArtifactsFeature?` | Artifact sidebar: `enabled`, `allowedTypes`, optional `layout` (see below). |
|
|
2446
|
-
|
|
2447
|
-
**`features.artifacts`** — `AgentWidgetArtifactsFeature`
|
|
2448
|
-
|
|
2449
|
-
| Property | Type | Description |
|
|
2450
|
-
| --- | --- | --- |
|
|
2451
|
-
| `enabled` | `boolean?` | When `true`, shows the artifact pane and handles `artifact_*` SSE events. |
|
|
2452
|
-
| `allowedTypes` | `('markdown' \| 'component')[]?` | If set, other artifact kinds are ignored client-side. |
|
|
2453
|
-
| `layout` | `AgentWidgetArtifactsLayoutConfig?` | Split/drawer sizing and launcher widen behavior. |
|
|
2454
|
-
|
|
2455
|
-
**`features.artifacts.layout`** — `AgentWidgetArtifactsLayoutConfig`
|
|
2456
|
-
|
|
2457
|
-
| Property | Type | Description |
|
|
2458
|
-
| --- | --- | --- |
|
|
2459
|
-
| `splitGap` | `string?` | CSS gap between chat column and artifact pane. Default: `0.5rem`. |
|
|
2460
|
-
| `paneWidth` | `string?` | Artifact column width in split mode. Default: `40%`. |
|
|
2461
|
-
| `paneMaxWidth` | `string?` | Max width of artifact column. Default: `28rem`. |
|
|
2462
|
-
| `paneMinWidth` | `string?` | Optional min width of artifact column. |
|
|
2463
|
-
| `narrowHostMaxWidth` | `number?` | If the **panel** is at most this many px wide, artifacts use an in-panel drawer instead of split. Default: `520`. |
|
|
2464
|
-
| `expandLauncherPanelWhenOpen` | `boolean?` | When not `false`, the floating panel grows while artifacts are visible (not user-dismissed). Default: widens for launcher mode. |
|
|
2465
|
-
| `expandedPanelWidth` | `string?` | CSS width when expanded. Default: `min(720px, calc(100vw - 24px))`. |
|
|
2466
|
-
| `resizable` | `boolean?` | When `true`, draggable handle between chat and artifact in desktop split mode. Default: `false`. |
|
|
2467
|
-
| `resizableMinWidth` | `string?` | Min artifact width while resizing; `px` only (e.g. `"200px"`). Default: `200px`. |
|
|
2468
|
-
| `resizableMaxWidth` | `string?` | Optional max artifact width cap (`px` only); layout still limits by panel width. |
|
|
2469
|
-
| `paneAppearance` | `'panel' \| 'seamless'?` | `panel` (default) — bordered sidebar with left border, gap, and shadow. `seamless` — flush with chat: no border or shadow, container background, zero gap (with `resizable`, the drag handle overlays the seam). |
|
|
2470
|
-
| `paneBorderRadius` | `string?` | Border radius on the artifact pane. Works with any `paneAppearance`. |
|
|
2471
|
-
| `paneShadow` | `string?` | CSS `box-shadow` on the artifact pane. Set `"none"` to suppress the default shadow. |
|
|
2472
|
-
| `paneBorder` | `string?` | Full CSS `border` shorthand on the artifact pane (e.g. `"1px solid #cccccc"`). Overrides default/`rounded` borders. If set, `paneBorderLeft` is ignored. |
|
|
2473
|
-
| `paneBorderLeft` | `string?` | `border-left` shorthand only — typical for the split edge next to chat (works with or without `resizable`). Example: `"1px solid #cccccc"`. |
|
|
2474
|
-
| `unifiedSplitChrome` | `boolean?` | Desktop split only: square the main chat card’s **top-right / bottom-right** radii and round the artifact pane’s **top-right / bottom-right** to match the panel (`--persona-radius-lg`) so both columns read as one shell. |
|
|
2475
|
-
| `unifiedSplitOuterRadius` | `string?` | Outer-right radius on the artifact side when `unifiedSplitChrome` is true. If omitted, uses `--persona-radius-lg`, or `paneBorderRadius` when `paneAppearance: 'rounded'`. |
|
|
2476
|
-
|
|
2477
|
-
#### State & Storage
|
|
2478
|
-
|
|
2479
|
-
| Option | Type | Description |
|
|
2480
|
-
| --- | --- | --- |
|
|
2481
|
-
| `initialMessages` | `AgentWidgetMessage[]` | Seed the conversation transcript with initial messages. |
|
|
2482
|
-
| `persistState` | `boolean \| AgentWidgetPersistStateConfig` | Persist widget state across page navigations. `true` uses defaults (sessionStorage). |
|
|
2483
|
-
| `storageAdapter` | `AgentWidgetStorageAdapter` | Custom storage adapter with `load()`, `save(state)`, and `clear()` methods. |
|
|
2484
|
-
| `onStateLoaded` | `(state: AgentWidgetStoredState) => AgentWidgetStoredState \| { state: AgentWidgetStoredState; open?: boolean }` | Transform state after loading from storage but before widget initialization. Return `{ state, open: true }` to also open the panel. |
|
|
2485
|
-
| `clearChatHistoryStorageKey` | `string` | Additional localStorage key to clear on chat reset. The widget clears `"persona-chat-history"` by default. |
|
|
2486
|
-
|
|
2487
|
-
**`persistState`** — `AgentWidgetPersistStateConfig`
|
|
2488
|
-
|
|
2489
|
-
| Property | Type | Description |
|
|
2490
|
-
| --- | --- | --- |
|
|
2491
|
-
| `storage` | `'local' \| 'session'?` | Storage type. Default: `'session'`. |
|
|
2492
|
-
| `keyPrefix` | `string?` | Prefix for storage keys. Default: `'persona-'`. |
|
|
2493
|
-
| `persist.openState` | `boolean?` | Persist widget open/closed state. Default: `true`. |
|
|
2494
|
-
| `persist.voiceState` | `boolean?` | Persist voice recognition state. Default: `true`. |
|
|
2495
|
-
| `persist.focusInput` | `boolean?` | Focus input when restoring open state. Default: `true`. |
|
|
2496
|
-
| `clearOnChatClear` | `boolean?` | Clear persisted state when chat is cleared. Default: `true`. |
|
|
2497
|
-
|
|
2498
|
-
#### Extensibility
|
|
2499
|
-
|
|
2500
|
-
| Option | Type | Description |
|
|
2501
|
-
| --- | --- | --- |
|
|
2502
|
-
| `plugins` | `AgentWidgetPlugin[]` | Plugin array for extending widget functionality. |
|
|
2503
|
-
| `contextProviders` | `AgentWidgetContextProvider[]` | Functions that inject additional context into each request payload. |
|
|
2504
|
-
| `requestMiddleware` | `AgentWidgetRequestMiddleware` | Transform the request payload before it is sent. |
|
|
2505
|
-
| `actionParsers` | `AgentWidgetActionParser[]` | Parse structured directives from assistant messages. |
|
|
2506
|
-
| `actionHandlers` | `AgentWidgetActionHandler[]` | Handle parsed actions (navigation, UI updates, etc.). |
|
|
2507
|
-
|
|
2508
|
-
### Stream Parser Configuration
|
|
2509
|
-
|
|
2510
|
-
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 plain text parser. You can easily select a built-in parser using `parserType`, or provide a custom parser via `streamParser`.
|
|
2511
|
-
|
|
2512
|
-
**Key benefits of the unified stream parser:**
|
|
2513
|
-
- **Format detection**: Automatically detects if content matches your parser's format
|
|
2514
|
-
- **Extensible**: Handle JSON, XML, or any custom structured format
|
|
2515
|
-
- **Incremental parsing**: Extract text as it streams in, not just when complete
|
|
2516
|
-
|
|
2517
|
-
**Quick start with `parserType` (recommended):**
|
|
2518
|
-
|
|
2519
|
-
The easiest way to use a built-in parser is with the `parserType` option:
|
|
2520
|
-
|
|
2521
|
-
```javascript
|
|
2522
|
-
import { initAgentWidget } from '@runtypelabs/persona';
|
|
2523
|
-
|
|
2524
|
-
const controller = initAgentWidget({
|
|
2525
|
-
target: '#chat-root',
|
|
2526
|
-
config: {
|
|
2527
|
-
apiUrl: '/api/chat/dispatch',
|
|
2528
|
-
parserType: 'json' // Options: 'plain', 'json', 'regex-json', 'xml'
|
|
2529
|
-
}
|
|
2530
|
-
});
|
|
2531
|
-
```
|
|
2532
|
-
|
|
2533
|
-
**Using built-in parsers with `streamParser` (ESM/Modules):**
|
|
2534
|
-
|
|
2535
|
-
```javascript
|
|
2536
|
-
import { initAgentWidget, createPlainTextParser, createJsonStreamParser, createXmlParser } from '@runtypelabs/persona';
|
|
2537
|
-
|
|
2538
|
-
const controller = initAgentWidget({
|
|
2539
|
-
target: '#chat-root',
|
|
2540
|
-
config: {
|
|
2541
|
-
apiUrl: '/api/chat/dispatch',
|
|
2542
|
-
streamParser: createJsonStreamParser // Use JSON parser
|
|
2543
|
-
// Or: createXmlParser for XML, createPlainTextParser for plain text (default)
|
|
2544
|
-
}
|
|
2545
|
-
});
|
|
2546
|
-
```
|
|
2547
|
-
|
|
2548
|
-
**Using built-in parsers with CDN Script Tags:**
|
|
2549
|
-
|
|
2550
|
-
```html
|
|
2551
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/index.global.js"></script>
|
|
2552
|
-
<script>
|
|
2553
|
-
window.AgentWidget.initAgentWidget({
|
|
2554
|
-
target: '#chat-root',
|
|
2555
|
-
config: {
|
|
2556
|
-
apiUrl: '/api/chat/dispatch',
|
|
2557
|
-
streamParser: window.AgentWidget.createJsonStreamParser // JSON parser
|
|
2558
|
-
// Or: window.AgentWidget.createXmlParser for XML
|
|
2559
|
-
// Or: window.AgentWidget.createPlainTextParser for plain text (default)
|
|
2560
|
-
}
|
|
2561
|
-
});
|
|
2562
|
-
</script>
|
|
2563
|
-
```
|
|
2564
|
-
|
|
2565
|
-
**Using with automatic installer script:**
|
|
2566
|
-
|
|
2567
|
-
```html
|
|
2568
|
-
<script>
|
|
2569
|
-
window.siteAgentConfig = {
|
|
2570
|
-
target: 'body',
|
|
2571
|
-
config: {
|
|
2572
|
-
apiUrl: '/api/chat/dispatch',
|
|
2573
|
-
parserType: 'json' // Simple way to select parser - no function imports needed!
|
|
2574
|
-
}
|
|
2575
|
-
};
|
|
2576
|
-
</script>
|
|
2577
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
2578
|
-
```
|
|
2579
|
-
|
|
2580
|
-
**Alternative: Using `streamParser` with installer script:**
|
|
2581
|
-
|
|
2582
|
-
If you need a custom parser, you can still use `streamParser`:
|
|
2583
|
-
|
|
2584
|
-
```html
|
|
2585
|
-
<script>
|
|
2586
|
-
window.siteAgentConfig = {
|
|
2587
|
-
target: 'body',
|
|
2588
|
-
config: {
|
|
2589
|
-
apiUrl: '/api/chat/dispatch',
|
|
2590
|
-
// Note: streamParser must be set after the script loads, or use a function
|
|
2591
|
-
streamParser: function() {
|
|
2592
|
-
return window.AgentWidget.createJsonStreamParser();
|
|
2593
|
-
}
|
|
2594
|
-
}
|
|
2595
|
-
};
|
|
2596
|
-
</script>
|
|
2597
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
2598
|
-
```
|
|
2599
|
-
|
|
2600
|
-
Alternatively, you can set it after the script loads:
|
|
2601
|
-
|
|
2602
|
-
```html
|
|
2603
|
-
<script>
|
|
2604
|
-
window.siteAgentConfig = {
|
|
2605
|
-
target: 'body',
|
|
2606
|
-
config: {
|
|
2607
|
-
apiUrl: '/api/chat/dispatch'
|
|
2608
|
-
}
|
|
2609
|
-
};
|
|
2610
|
-
</script>
|
|
2611
|
-
<script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
|
|
2612
|
-
<script>
|
|
2613
|
-
// Set parser after AgentWidget is loaded
|
|
2614
|
-
if (window.siteAgentConfig && window.AgentWidget) {
|
|
2615
|
-
window.siteAgentConfig.config.streamParser = window.AgentWidget.createJsonStreamParser;
|
|
2616
|
-
}
|
|
2617
|
-
</script>
|
|
2618
|
-
```
|
|
2619
|
-
|
|
2620
|
-
**Custom JSON parser example:**
|
|
2621
|
-
|
|
2622
|
-
```javascript
|
|
2623
|
-
const jsonParser = () => {
|
|
2624
|
-
let extractedText = null;
|
|
2625
|
-
|
|
2626
|
-
return {
|
|
2627
|
-
// Extract text field from JSON as it streams in
|
|
2628
|
-
// Return null if not JSON or text not available yet
|
|
2629
|
-
processChunk(accumulatedContent) {
|
|
2630
|
-
const trimmed = accumulatedContent.trim();
|
|
2631
|
-
// Return null if not JSON format
|
|
2632
|
-
if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
|
|
2633
|
-
return null;
|
|
2634
|
-
}
|
|
2635
|
-
|
|
2636
|
-
const match = accumulatedContent.match(/"text"\s*:\s*"([^"]*(?:\\.[^"]*)*)"/);
|
|
2637
|
-
if (match) {
|
|
2638
|
-
extractedText = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n');
|
|
2639
|
-
return extractedText;
|
|
2640
|
-
}
|
|
2641
|
-
return null;
|
|
2642
|
-
},
|
|
2643
|
-
|
|
2644
|
-
getExtractedText() {
|
|
2645
|
-
return extractedText;
|
|
2646
|
-
}
|
|
2647
|
-
};
|
|
2648
|
-
};
|
|
2649
|
-
|
|
2650
|
-
initAgentWidget({
|
|
2651
|
-
target: '#chat-root',
|
|
2652
|
-
config: {
|
|
2653
|
-
apiUrl: '/api/chat/dispatch',
|
|
2654
|
-
streamParser: jsonParser,
|
|
2655
|
-
postprocessMessage: ({ text, raw }) => {
|
|
2656
|
-
// raw contains the structured payload (JSON, XML, etc.)
|
|
2657
|
-
return markdownPostprocessor(text);
|
|
2658
|
-
}
|
|
2659
|
-
}
|
|
2660
|
-
});
|
|
2661
|
-
```
|
|
2662
|
-
|
|
2663
|
-
**Custom XML parser example:**
|
|
2664
|
-
|
|
2665
|
-
```javascript
|
|
2666
|
-
const xmlParser = () => {
|
|
2667
|
-
let extractedText = null;
|
|
2668
|
-
|
|
2669
|
-
return {
|
|
2670
|
-
processChunk(accumulatedContent) {
|
|
2671
|
-
// Return null if not XML format
|
|
2672
|
-
if (!accumulatedContent.trim().startsWith('<')) {
|
|
2673
|
-
return null;
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
// Extract text from <text>...</text> tags
|
|
2677
|
-
const match = accumulatedContent.match(/<text[^>]*>([\s\S]*?)<\/text>/);
|
|
2678
|
-
if (match) {
|
|
2679
|
-
extractedText = match[1];
|
|
2680
|
-
return extractedText;
|
|
2681
|
-
}
|
|
2682
|
-
return null;
|
|
2683
|
-
},
|
|
2684
|
-
|
|
2685
|
-
getExtractedText() {
|
|
2686
|
-
return extractedText;
|
|
2687
|
-
}
|
|
2688
|
-
};
|
|
2689
|
-
};
|
|
2690
|
-
```
|
|
2691
|
-
|
|
2692
|
-
**Parser interface:**
|
|
2693
|
-
|
|
2694
|
-
```typescript
|
|
2695
|
-
interface AgentWidgetStreamParser {
|
|
2696
|
-
// Process a chunk and return extracted text (if available)
|
|
2697
|
-
// Return null if the content doesn't match this parser's format or text is not yet available
|
|
2698
|
-
processChunk(accumulatedContent: string): Promise<string | null> | string | null;
|
|
2699
|
-
|
|
2700
|
-
// Get the currently extracted text (may be partial)
|
|
2701
|
-
getExtractedText(): string | null;
|
|
2702
|
-
|
|
2703
|
-
// Optional cleanup when parsing is complete
|
|
2704
|
-
close?(): Promise<void> | void;
|
|
2705
|
-
}
|
|
2706
|
-
```
|
|
112
|
+
The full reference lives in [`docs/`](./docs/) and the theming guide:
|
|
2707
113
|
|
|
2708
|
-
|
|
114
|
+
- [Programmatic Control & Events](./docs/PROGRAMMATIC-CONTROL.md) — controller API, message hooks and injection, enriched DOM context, WebMCP page tools, DOM and controller events, state loading
|
|
115
|
+
- [UI Features & Components](./docs/UI-COMPONENTS.md) — message actions and feedback, loading indicators, ask-user-question, dropdown menus, button utilities, dynamic forms
|
|
116
|
+
- [Script Tag Installation & Framework Integration](./docs/INSTALLATION-FRAMEWORKS.md) — automatic installer, manual script tag setup, React, Next.js, Remix, Gatsby, and Astro guides
|
|
117
|
+
- [Configuration Reference](./docs/CONFIGURATION-REFERENCE.md) — every config option: core, client token mode, agent mode, UI & theme, launcher, layout, voice, tool calls, suggestion chips, state & storage
|
|
118
|
+
- [Stream Parser Configuration](./docs/STREAM-PARSERS.md) — JSON, XML, and plain-text stream parsers and custom parser factories
|
|
119
|
+
- [THEME-CONFIG.md](./THEME-CONFIG.md) — the complete theme and design-token reference
|
|
120
|
+
- [Message Injection](./docs/MESSAGE-INJECTION.md) — full injection and component-directive reference
|
|
121
|
+
- [Dynamic Forms](./docs/DYNAMIC-FORMS.md) — field schema, form styles, and recipes
|
|
2709
122
|
|
|
2710
123
|
### Optional proxy server
|
|
2711
124
|
|