@runtypelabs/persona 1.36.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.
Files changed (61) hide show
  1. package/README.md +1080 -0
  2. package/dist/index.cjs +140 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +2626 -0
  5. package/dist/index.d.ts +2626 -0
  6. package/dist/index.global.js +1843 -0
  7. package/dist/index.global.js.map +1 -0
  8. package/dist/index.js +140 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/install.global.js +2 -0
  11. package/dist/install.global.js.map +1 -0
  12. package/dist/widget.css +1627 -0
  13. package/package.json +79 -0
  14. package/src/@types/idiomorph.d.ts +37 -0
  15. package/src/client.test.ts +387 -0
  16. package/src/client.ts +1589 -0
  17. package/src/components/composer-builder.ts +530 -0
  18. package/src/components/feedback.ts +379 -0
  19. package/src/components/forms.ts +170 -0
  20. package/src/components/header-builder.ts +455 -0
  21. package/src/components/header-layouts.ts +303 -0
  22. package/src/components/launcher.ts +193 -0
  23. package/src/components/message-bubble.ts +528 -0
  24. package/src/components/messages.ts +54 -0
  25. package/src/components/panel.ts +204 -0
  26. package/src/components/reasoning-bubble.ts +144 -0
  27. package/src/components/registry.ts +87 -0
  28. package/src/components/suggestions.ts +97 -0
  29. package/src/components/tool-bubble.ts +288 -0
  30. package/src/defaults.ts +321 -0
  31. package/src/index.ts +175 -0
  32. package/src/install.ts +284 -0
  33. package/src/plugins/registry.ts +77 -0
  34. package/src/plugins/types.ts +95 -0
  35. package/src/postprocessors.ts +194 -0
  36. package/src/runtime/init.ts +162 -0
  37. package/src/session.ts +376 -0
  38. package/src/styles/tailwind.css +20 -0
  39. package/src/styles/widget.css +1627 -0
  40. package/src/types.ts +1635 -0
  41. package/src/ui.ts +3341 -0
  42. package/src/utils/actions.ts +227 -0
  43. package/src/utils/attachment-manager.ts +384 -0
  44. package/src/utils/code-generators.test.ts +500 -0
  45. package/src/utils/code-generators.ts +1806 -0
  46. package/src/utils/component-middleware.ts +137 -0
  47. package/src/utils/component-parser.ts +119 -0
  48. package/src/utils/constants.ts +16 -0
  49. package/src/utils/content.ts +306 -0
  50. package/src/utils/dom.ts +25 -0
  51. package/src/utils/events.ts +41 -0
  52. package/src/utils/formatting.test.ts +166 -0
  53. package/src/utils/formatting.ts +470 -0
  54. package/src/utils/icons.ts +92 -0
  55. package/src/utils/message-id.ts +37 -0
  56. package/src/utils/morph.ts +36 -0
  57. package/src/utils/positioning.ts +17 -0
  58. package/src/utils/storage.ts +72 -0
  59. package/src/utils/theme.ts +105 -0
  60. package/src/widget.css +1 -0
  61. package/widget.css +1 -0
package/README.md ADDED
@@ -0,0 +1,1080 @@
1
+ ## Streaming Agent Widget
2
+
3
+ Installable vanilla JavaScript widget for embedding a streaming AI assistant on any website.
4
+
5
+ ### Installation
6
+
7
+ ```bash
8
+ npm install @runtypelabs/persona
9
+ ```
10
+
11
+ ### Building locally
12
+
13
+ ```bash
14
+ pnpm build
15
+ ```
16
+
17
+ - `dist/index.js` (ESM), `dist/index.cjs` (CJS), and `dist/index.global.js` (IIFE) provide different module formats.
18
+ - `dist/widget.css` is the prefixed Tailwind bundle.
19
+ - `dist/install.global.js` is the automatic installer script for easy script tag installation.
20
+
21
+ ### Using with modules
22
+
23
+ ```ts
24
+ import '@runtypelabs/persona/widget.css';
25
+ import {
26
+ initAgentWidget,
27
+ createAgentExperience,
28
+ markdownPostprocessor,
29
+ DEFAULT_WIDGET_CONFIG
30
+ } from '@runtypelabs/persona';
31
+
32
+ const proxyUrl = '/api/chat/dispatch';
33
+
34
+ // Inline embed
35
+ const inlineHost = document.querySelector('#inline-widget')!;
36
+ createAgentExperience(inlineHost, {
37
+ ...DEFAULT_WIDGET_CONFIG,
38
+ apiUrl: proxyUrl,
39
+ launcher: { enabled: false },
40
+ theme: {
41
+ ...DEFAULT_WIDGET_CONFIG.theme,
42
+ accent: '#2563eb'
43
+ },
44
+ suggestionChips: ['What can you do?', 'Show API docs'],
45
+ postprocessMessage: ({ text }) => markdownPostprocessor(text)
46
+ });
47
+
48
+ // Floating launcher with runtime updates
49
+ const controller = initAgentWidget({
50
+ target: '#launcher-root',
51
+ windowKey: 'chatController', // Optional: stores controller on window.chatController
52
+ config: {
53
+ ...DEFAULT_WIDGET_CONFIG,
54
+ apiUrl: proxyUrl,
55
+ launcher: {
56
+ ...DEFAULT_WIDGET_CONFIG.launcher,
57
+ title: 'AI Assistant',
58
+ subtitle: 'Here to help you get answers fast'
59
+ }
60
+ }
61
+ });
62
+
63
+ // Runtime theme update
64
+ document.querySelector('#dark-mode')?.addEventListener('click', () => {
65
+ controller.update({ theme: { surface: '#0f172a', primary: '#f8fafc' } });
66
+ });
67
+ ```
68
+
69
+ ### Initialization options
70
+
71
+ `initAgentWidget` accepts the following options:
72
+
73
+ | Option | Type | Description |
74
+ | --- | --- | --- |
75
+ | `target` | `string \| HTMLElement` | CSS selector or element where widget mounts. |
76
+ | `config` | `AgentWidgetConfig` | Widget configuration object (see [Configuration reference](#configuration-reference) below). |
77
+ | `useShadowDom` | `boolean` | Use Shadow DOM for style isolation (default: `true`). |
78
+ | `onReady` | `() => void` | Callback fired when widget is initialized. |
79
+ | `windowKey` | `string` | If provided, stores the controller on `window[windowKey]` for global access. Automatically cleaned up on `destroy()`. |
80
+
81
+ > **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.
82
+
83
+
84
+ ### Programmatic control
85
+
86
+ `initAgentWidget` (and `createAgentExperience`) return a controller with methods to programmatically control the widget.
87
+
88
+ #### Basic controls
89
+
90
+ ```ts
91
+ const chat = initAgentWidget({
92
+ target: '#launcher-root',
93
+ config: { /* ... */ }
94
+ })
95
+
96
+ document.getElementById('open-chat')?.addEventListener('click', () => chat.open())
97
+ document.getElementById('toggle-chat')?.addEventListener('click', () => chat.toggle())
98
+ document.getElementById('close-chat')?.addEventListener('click', () => chat.close())
99
+ ```
100
+
101
+ #### Message hooks
102
+
103
+ You can programmatically set messages, submit messages, and control voice recognition:
104
+
105
+ ```ts
106
+ const chat = initAgentWidget({
107
+ target: '#launcher-root',
108
+ config: { /* ... */ }
109
+ })
110
+
111
+ // Set a message in the input field (doesn't submit)
112
+ chat.setMessage("Hello, I need help")
113
+
114
+ // Submit a message (uses textarea value if no argument provided)
115
+ chat.submitMessage()
116
+ // Or submit a specific message
117
+ chat.submitMessage("What are your hours?")
118
+
119
+ // Start voice recognition
120
+ chat.startVoiceRecognition()
121
+
122
+ // Stop voice recognition
123
+ chat.stopVoiceRecognition()
124
+ ```
125
+
126
+ 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).
127
+
128
+ #### Clear chat
129
+
130
+ ```ts
131
+ const chat = initAgentWidget({
132
+ target: '#launcher-root',
133
+ config: { /* ... */ }
134
+ })
135
+
136
+ // Clear all messages programmatically
137
+ chat.clearChat()
138
+ ```
139
+
140
+ #### Accessing from window
141
+
142
+ To access the controller globally (e.g., from browser console or external scripts), use the `windowKey` option:
143
+
144
+ ```ts
145
+ const chat = initAgentWidget({
146
+ target: '#launcher-root',
147
+ windowKey: 'chatController', // Stores controller on window.chatController
148
+ config: { /* ... */ }
149
+ })
150
+
151
+ // Now accessible globally
152
+ window.chatController.setMessage("Hello from console!")
153
+ window.chatController.submitMessage("Test message")
154
+ window.chatController.startVoiceRecognition()
155
+ ```
156
+
157
+ #### Message Types
158
+
159
+ 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.
160
+
161
+ ```typescript
162
+ type AgentWidgetMessage = {
163
+ id: string; // Unique message identifier
164
+ role: "user" | "assistant" | "system";
165
+ content: string; // Message text content
166
+ createdAt: string; // ISO timestamp
167
+ streaming?: boolean; // Whether message is still streaming
168
+ variant?: "assistant" | "reasoning" | "tool";
169
+ sequence?: number; // Message ordering
170
+ reasoning?: AgentWidgetReasoning;
171
+ toolCall?: AgentWidgetToolCall;
172
+ tools?: AgentWidgetToolCall[];
173
+ viaVoice?: boolean; // Indicates if user message was sent via voice input
174
+ };
175
+ ```
176
+
177
+ **`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:
178
+
179
+ ```ts
180
+ postprocessMessage: ({ message, text, streaming }) => {
181
+ if (message.role === 'user' && message.viaVoice) {
182
+ console.log('User sent message via voice');
183
+ }
184
+ return text;
185
+ }
186
+ ```
187
+
188
+ Alternatively, manually assign the controller:
189
+
190
+ ```ts
191
+ const chat = initAgentWidget({ /* ... */ })
192
+ window.chatController = chat
193
+ ```
194
+
195
+ ### Events
196
+
197
+ The widget dispatches custom events that you can listen to for integration with your application:
198
+
199
+ #### `persona:clear-chat`
200
+
201
+ Dispatched when the user clicks the "Clear chat" button or when `chat.clearChat()` is called programmatically.
202
+
203
+ ```ts
204
+ window.addEventListener("persona:clear-chat", (event) => {
205
+ console.log("Chat cleared at:", event.detail.timestamp);
206
+ // Clear your localStorage, reset state, etc.
207
+ });
208
+ ```
209
+
210
+ **Event detail:**
211
+ - `timestamp`: ISO timestamp string of when the chat was cleared
212
+
213
+ **Use cases:**
214
+ - Clear localStorage chat history
215
+ - Reset application state
216
+ - Track analytics events
217
+ - Sync with backend
218
+
219
+ **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.
220
+
221
+ ### Message Actions (Copy, Upvote, Downvote)
222
+
223
+ The widget includes built-in action buttons for assistant messages that allow users to copy message content and provide feedback through upvote/downvote buttons.
224
+
225
+ #### Configuration
226
+
227
+ ```ts
228
+ const controller = initAgentWidget({
229
+ target: '#app',
230
+ config: {
231
+ apiUrl: '/api/chat/dispatch',
232
+
233
+ // Message actions configuration
234
+ messageActions: {
235
+ enabled: true, // Enable/disable all action buttons (default: true)
236
+ showCopy: true, // Show copy button (default: true)
237
+ showUpvote: true, // Show upvote button (default: false - requires backend)
238
+ showDownvote: true, // Show downvote button (default: false - requires backend)
239
+ visibility: 'hover', // 'hover' or 'always' (default: 'hover')
240
+ align: 'right', // 'left', 'center', or 'right' (default: 'right')
241
+ layout: 'pill-inside', // 'pill-inside' (compact floating) or 'row-inside' (full-width bar)
242
+
243
+ // Optional callbacks (called in addition to events)
244
+ onCopy: (message) => {
245
+ console.log('Copied:', message.id);
246
+ },
247
+ onFeedback: (feedback) => {
248
+ console.log('Feedback:', feedback.type, feedback.messageId);
249
+ // Send to your analytics/backend
250
+ fetch('/api/feedback', {
251
+ method: 'POST',
252
+ headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify(feedback)
254
+ });
255
+ }
256
+ }
257
+ }
258
+ });
259
+ ```
260
+
261
+ #### Feedback Events
262
+
263
+ Listen to feedback events via the controller:
264
+
265
+ ```ts
266
+ // Copy event - fired when user copies a message
267
+ controller.on('message:copy', (message) => {
268
+ console.log('Message copied:', message.id, message.content);
269
+ });
270
+
271
+ // Feedback event - fired when user upvotes or downvotes
272
+ controller.on('message:feedback', (feedback) => {
273
+ console.log('Feedback received:', {
274
+ type: feedback.type, // 'upvote' or 'downvote'
275
+ messageId: feedback.messageId,
276
+ message: feedback.message // Full message object
277
+ });
278
+ });
279
+ ```
280
+
281
+ #### Feedback Types
282
+
283
+ ```typescript
284
+ type AgentWidgetMessageFeedback = {
285
+ type: 'upvote' | 'downvote';
286
+ messageId: string;
287
+ message: AgentWidgetMessage;
288
+ };
289
+
290
+ type AgentWidgetMessageActionsConfig = {
291
+ enabled?: boolean;
292
+ showCopy?: boolean;
293
+ showUpvote?: boolean;
294
+ showDownvote?: boolean;
295
+ visibility?: 'always' | 'hover';
296
+ onFeedback?: (feedback: AgentWidgetMessageFeedback) => void;
297
+ onCopy?: (message: AgentWidgetMessage) => void;
298
+ };
299
+ ```
300
+
301
+ #### Visual Behavior
302
+
303
+ - **Hover mode** (`visibility: 'hover'`): Action buttons appear when hovering over assistant messages
304
+ - **Always mode** (`visibility: 'always'`): Action buttons are always visible
305
+ - **Copy button**: Shows a checkmark briefly after successful copy
306
+ - **Vote buttons**: Toggle active state and are mutually exclusive (upvoting clears downvote and vice versa)
307
+
308
+ ### Travrse adapter
309
+
310
+ This package ships with a Travrse adapter by default. The proxy handles all flow configuration, keeping the client lightweight and flexible.
311
+
312
+ **Flow configuration happens server-side** - you have three options:
313
+
314
+ 1. **Use default flow** - The proxy includes a basic streaming chat flow out of the box
315
+ 2. **Reference a Travrse flow ID** - Configure flows in your Travrse dashboard and reference them by ID
316
+ 3. **Define custom flows** - Build flow configurations directly in the proxy
317
+
318
+ The client simply sends messages to the proxy, which constructs the full Travrse payload. This architecture allows you to:
319
+ - Change models/prompts without redeploying the widget
320
+ - A/B test different flows server-side
321
+ - Enforce security and cost controls centrally
322
+ - Support multiple flows for different use cases
323
+
324
+ ### Dynamic Forms (Recommended)
325
+
326
+ 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:
327
+
328
+ ```typescript
329
+ import { componentRegistry, initAgentWidget } from "@runtypelabs/persona";
330
+ import { DynamicForm } from "./components"; // Your DynamicForm component
331
+
332
+ // Register the component
333
+ componentRegistry.register("DynamicForm", DynamicForm);
334
+
335
+ initAgentWidget({
336
+ target: "#app",
337
+ config: {
338
+ apiUrl: "/api/chat/dispatch-directive",
339
+ parserType: "json",
340
+ enableComponentStreaming: true,
341
+ formEndpoint: "/form",
342
+ // Optional: customize form appearance
343
+ formStyles: {
344
+ borderRadius: "16px",
345
+ borderWidth: "1px",
346
+ borderColor: "#e5e7eb",
347
+ padding: "1.5rem",
348
+ titleFontSize: "1.25rem",
349
+ buttonBorderRadius: "9999px"
350
+ }
351
+ }
352
+ });
353
+ ```
354
+
355
+ The AI responds with JSON like:
356
+
357
+ ```json
358
+ {
359
+ "text": "Please fill out this form:",
360
+ "component": "DynamicForm",
361
+ "props": {
362
+ "title": "Contact Us",
363
+ "fields": [
364
+ { "label": "Name", "type": "text", "required": true },
365
+ { "label": "Email", "type": "email", "required": true }
366
+ ],
367
+ "submit_text": "Submit"
368
+ }
369
+ }
370
+ ```
371
+
372
+ See `examples/embedded-app/json.html` for a full working example.
373
+
374
+ ### Directive postprocessor (Deprecated)
375
+
376
+ > **⚠️ 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.
377
+
378
+ `directivePostprocessor` looks for either `<Form type="init" />` tokens or
379
+ `<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`.
380
+
381
+ ### Script tag installation
382
+
383
+ The widget can be installed via a simple script tag, perfect for platforms where you can't compile custom code. There are two methods:
384
+
385
+ #### Method 1: Automatic installer (recommended)
386
+
387
+ The easiest way is to use the automatic installer script. It handles loading CSS and JavaScript, then initializes the widget automatically:
388
+
389
+ ```html
390
+ <!-- Add this before the closing </body> tag -->
391
+ <script>
392
+ window.siteAgentConfig = {
393
+ target: 'body', // or '#my-container' for specific placement
394
+ config: {
395
+ apiUrl: 'https://your-proxy.com/api/chat/dispatch',
396
+ launcher: {
397
+ enabled: true,
398
+ title: 'AI Assistant',
399
+ subtitle: 'How can I help you?'
400
+ },
401
+ theme: {
402
+ accent: '#2563eb',
403
+ surface: '#ffffff'
404
+ },
405
+ // Optional: configure stream parser for JSON/XML responses
406
+ // streamParser: () => window.AgentWidget.createJsonStreamParser()
407
+ }
408
+ };
409
+ </script>
410
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
411
+ ```
412
+
413
+ **Installer options:**
414
+
415
+ - `version` - Package version to load (default: `"latest"`)
416
+ - `cdn` - CDN provider: `"jsdelivr"` or `"unpkg"` (default: `"jsdelivr"`)
417
+ - `cssUrl` - Custom CSS URL (overrides CDN)
418
+ - `jsUrl` - Custom JS URL (overrides CDN)
419
+ - `target` - CSS selector or element where widget mounts (default: `"body"`)
420
+ - `config` - Widget configuration object (see Configuration reference)
421
+ - `autoInit` - Automatically initialize after loading (default: `true`)
422
+
423
+ **Example with version pinning:**
424
+
425
+ ```html
426
+ <script>
427
+ window.siteAgentConfig = {
428
+ version: '0.1.0', // Pin to specific version
429
+ config: {
430
+ apiUrl: '/api/chat/dispatch',
431
+ launcher: { enabled: true, title: 'Support Chat' }
432
+ }
433
+ };
434
+ </script>
435
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@0.1.0/dist/install.global.js"></script>
436
+ ```
437
+
438
+ #### Method 2: Manual installation
439
+
440
+ For more control, manually load CSS and JavaScript:
441
+
442
+ ```html
443
+ <!-- Load CSS -->
444
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/widget.css" />
445
+
446
+ <!-- Load JavaScript -->
447
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/index.global.js"></script>
448
+
449
+ <!-- Initialize widget -->
450
+ <script>
451
+ const chatController = window.AgentWidget.initAgentWidget({
452
+ target: '#persona-anchor', // or 'body' for floating launcher
453
+ windowKey: 'chatWidget', // Optional: stores controller on window.chatWidget
454
+ config: {
455
+ apiUrl: '/api/chat/dispatch',
456
+ launcher: {
457
+ enabled: true,
458
+ title: 'AI Assistant',
459
+ subtitle: 'Here to help'
460
+ },
461
+ theme: {
462
+ accent: '#111827',
463
+ surface: '#f5f5f5'
464
+ },
465
+ // Optional: configure stream parser for JSON/XML responses
466
+ streamParser: window.AgentWidget.createJsonStreamParser // or createXmlParser, createPlainTextParser
467
+ }
468
+ });
469
+
470
+ // Controller is now available as window.chatWidget (if windowKey was used)
471
+ // or use the returned chatController variable
472
+ </script>
473
+ ```
474
+
475
+ **CDN options:**
476
+
477
+ - **jsDelivr** (recommended): `https://cdn.jsdelivr.net/npm/@runtypelabs/persona@VERSION/dist/`
478
+ - **unpkg**: `https://unpkg.com/@runtypelabs/persona@VERSION/dist/`
479
+
480
+ Replace `VERSION` with `latest` for auto-updates, or a specific version like `0.1.0` for stability.
481
+
482
+ **Available files:**
483
+
484
+ - `widget.css` - Stylesheet (required)
485
+ - `index.global.js` - Widget JavaScript (IIFE format)
486
+ - `install.global.js` - Automatic installer script
487
+
488
+ The script build exposes a `window.AgentWidget` global with `initAgentWidget()` and other exports, including parser functions:
489
+
490
+ - `window.AgentWidget.initAgentWidget()` - Initialize the widget
491
+ - `window.AgentWidget.createPlainTextParser()` - Plain text parser (default)
492
+ - `window.AgentWidget.createJsonStreamParser()` - JSON parser using schema-stream
493
+ - `window.AgentWidget.createXmlParser()` - XML parser
494
+ - `window.AgentWidget.markdownPostprocessor()` - Markdown postprocessor
495
+ - `window.AgentWidget.directivePostprocessor()` - Directive postprocessor *(deprecated)*
496
+ - `window.AgentWidget.componentRegistry` - Component registry for custom components
497
+
498
+ ### React Framework Integration
499
+
500
+ The widget is fully compatible with React frameworks. Use the ESM imports to integrate it as a client component.
501
+
502
+ #### Framework Compatibility
503
+
504
+ | Framework | Compatible | Implementation Notes |
505
+ |-----------|------------|---------------------|
506
+ | **Vite** | ✅ Yes | No special requirements - works out of the box |
507
+ | **Create React App** | ✅ Yes | No special requirements - works out of the box |
508
+ | **Next.js** | ✅ Yes | Requires `'use client'` directive (App Router) |
509
+ | **Remix** | ✅ Yes | Use dynamic import or `useEffect` guard for SSR |
510
+ | **Gatsby** | ✅ Yes | Use in `wrapRootElement` or check `typeof window !== 'undefined'` |
511
+ | **Astro** | ✅ Yes | Use `client:load` or `client:only="react"` directive |
512
+
513
+ #### Quick Start with Vite or Create React App
514
+
515
+ For client-side-only React frameworks (Vite, CRA), create a component:
516
+
517
+ ```typescript
518
+ // src/components/ChatWidget.tsx
519
+ import { useEffect } from 'react';
520
+ import '@runtypelabs/persona/widget.css';
521
+ import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';
522
+ import type { AgentWidgetInitHandle } from '@runtypelabs/persona';
523
+
524
+ export function ChatWidget() {
525
+ useEffect(() => {
526
+ let handle: AgentWidgetInitHandle | null = null;
527
+
528
+ handle = initAgentWidget({
529
+ target: 'body',
530
+ config: {
531
+ apiUrl: "/api/chat/dispatch",
532
+ theme: {
533
+ primary: "#111827",
534
+ accent: "#1d4ed8",
535
+ },
536
+ launcher: {
537
+ enabled: true,
538
+ title: "Chat Assistant",
539
+ subtitle: "Here to help you get answers fast"
540
+ },
541
+ postprocessMessage: ({ text }) => markdownPostprocessor(text)
542
+ }
543
+ });
544
+
545
+ // Cleanup on unmount
546
+ return () => {
547
+ if (handle) {
548
+ handle.destroy();
549
+ }
550
+ };
551
+ }, []);
552
+
553
+ return null; // Widget injects itself into the DOM
554
+ }
555
+ ```
556
+
557
+ Then use it in your app:
558
+
559
+ ```typescript
560
+ // src/App.tsx
561
+ import { ChatWidget } from './components/ChatWidget';
562
+
563
+ function App() {
564
+ return (
565
+ <div>
566
+ {/* Your app content */}
567
+ <ChatWidget />
568
+ </div>
569
+ );
570
+ }
571
+
572
+ export default App;
573
+ ```
574
+
575
+ #### Next.js Integration
576
+
577
+ For Next.js App Router, add the `'use client'` directive:
578
+
579
+ ```typescript
580
+ // components/ChatWidget.tsx
581
+ 'use client';
582
+
583
+ import { useEffect } from 'react';
584
+ import '@runtypelabs/persona/widget.css';
585
+ import { initAgentWidget, markdownPostprocessor } from '@runtypelabs/persona';
586
+ import type { AgentWidgetInitHandle } from '@runtypelabs/persona';
587
+
588
+ export function ChatWidget() {
589
+ useEffect(() => {
590
+ let handle: AgentWidgetInitHandle | null = null;
591
+
592
+ handle = initAgentWidget({
593
+ target: 'body',
594
+ config: {
595
+ apiUrl: "/api/chat/dispatch",
596
+ launcher: {
597
+ enabled: true,
598
+ title: "Chat Assistant",
599
+ },
600
+ postprocessMessage: ({ text }) => markdownPostprocessor(text)
601
+ }
602
+ });
603
+
604
+ return () => {
605
+ if (handle) {
606
+ handle.destroy();
607
+ }
608
+ };
609
+ }, []);
610
+
611
+ return null;
612
+ }
613
+ ```
614
+
615
+ Use it in your layout or page:
616
+
617
+ ```typescript
618
+ // app/layout.tsx
619
+ import { ChatWidget } from '@/components/ChatWidget';
620
+
621
+ export default function RootLayout({ children }) {
622
+ return (
623
+ <html lang="en">
624
+ <body>
625
+ {children}
626
+ <ChatWidget />
627
+ </body>
628
+ </html>
629
+ );
630
+ }
631
+ ```
632
+
633
+ **Alternative: Dynamic Import (SSR-Safe)**
634
+
635
+ If you encounter SSR issues, use Next.js dynamic imports:
636
+
637
+ ```typescript
638
+ // app/layout.tsx
639
+ import dynamic from 'next/dynamic';
640
+
641
+ const ChatWidget = dynamic(
642
+ () => import('@/components/ChatWidget').then(mod => mod.ChatWidget),
643
+ { ssr: false }
644
+ );
645
+
646
+ export default function RootLayout({ children }) {
647
+ return (
648
+ <html lang="en">
649
+ <body>
650
+ {children}
651
+ <ChatWidget />
652
+ </body>
653
+ </html>
654
+ );
655
+ }
656
+ ```
657
+
658
+ #### Remix Integration
659
+
660
+ For Remix, guard the widget initialization with a client-side check:
661
+
662
+ ```typescript
663
+ // app/components/ChatWidget.tsx
664
+ import { useEffect, useState } from 'react';
665
+
666
+ export function ChatWidget() {
667
+ const [mounted, setMounted] = useState(false);
668
+
669
+ useEffect(() => {
670
+ setMounted(true);
671
+
672
+ // Dynamic import to avoid SSR issues
673
+ import('@runtypelabs/persona/widget.css');
674
+ import('@runtypelabs/persona').then(({ initAgentWidget, markdownPostprocessor }) => {
675
+ const handle = initAgentWidget({
676
+ target: 'body',
677
+ config: {
678
+ apiUrl: "/api/chat/dispatch",
679
+ launcher: { enabled: true },
680
+ postprocessMessage: ({ text }) => markdownPostprocessor(text)
681
+ }
682
+ });
683
+
684
+ return () => handle?.destroy();
685
+ });
686
+ }, []);
687
+
688
+ if (!mounted) return null;
689
+ return null;
690
+ }
691
+ ```
692
+
693
+ #### Gatsby Integration
694
+
695
+ Use Gatsby's `wrapRootElement` API:
696
+
697
+ ```typescript
698
+ // gatsby-browser.js
699
+ import { ChatWidget } from './src/components/ChatWidget';
700
+
701
+ export const wrapRootElement = ({ element }) => (
702
+ <>
703
+ {element}
704
+ <ChatWidget />
705
+ </>
706
+ );
707
+ ```
708
+
709
+ #### Astro Integration
710
+
711
+ Use Astro's client directives with React islands:
712
+
713
+ ```astro
714
+ ---
715
+ // src/components/ChatWidget.astro
716
+ import { ChatWidget } from './ChatWidget.tsx';
717
+ ---
718
+
719
+ <ChatWidget client:load />
720
+ ```
721
+
722
+ #### Using the Theme Configurator
723
+
724
+ 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.
725
+
726
+ #### Installation
727
+
728
+ ```bash
729
+ npm install @runtypelabs/persona
730
+ # or
731
+ pnpm add @runtypelabs/persona
732
+ # or
733
+ yarn add @runtypelabs/persona
734
+ ```
735
+
736
+ #### Key Considerations
737
+
738
+ 1. **CSS Import**: The CSS import (`import '@runtypelabs/persona/widget.css'`) works natively with all modern React build tools
739
+ 2. **Client-Side Only**: The widget manipulates the DOM, so it must run client-side only
740
+ 3. **Cleanup**: Always call `handle.destroy()` in the cleanup function to prevent memory leaks
741
+ 4. **API Routes**: Ensure your `apiUrl` points to a valid backend endpoint
742
+ 5. **TypeScript Support**: Full TypeScript definitions are included for all exports
743
+
744
+ ### Using default configuration
745
+
746
+ The package exports a complete default configuration that you can use as a base:
747
+
748
+ ```ts
749
+ import { DEFAULT_WIDGET_CONFIG, mergeWithDefaults } from '@runtypelabs/persona';
750
+
751
+ // Option 1: Use defaults with selective overrides
752
+ const controller = initAgentWidget({
753
+ target: '#app',
754
+ config: {
755
+ ...DEFAULT_WIDGET_CONFIG,
756
+ apiUrl: '/api/chat/dispatch',
757
+ theme: {
758
+ ...DEFAULT_WIDGET_CONFIG.theme,
759
+ accent: '#custom-color' // Override only what you need
760
+ }
761
+ }
762
+ });
763
+
764
+ // Option 2: Use the merge helper
765
+ const controller = initAgentWidget({
766
+ target: '#app',
767
+ config: mergeWithDefaults({
768
+ apiUrl: '/api/chat/dispatch',
769
+ theme: { accent: '#custom-color' }
770
+ })
771
+ });
772
+ ```
773
+
774
+ This ensures all configuration values are set to sensible defaults while allowing you to customize only what you need.
775
+
776
+ ### Configuration reference
777
+
778
+ | Option | Type | Description |
779
+ | --- | --- | --- |
780
+ | `apiUrl` | `string` | Proxy endpoint for your chat backend (defaults to Travrse's cloud API). |
781
+ | `flowId` | `string` | Optional Travrse flow ID. If provided, the client sends it to the proxy which can use it to select a specific flow. |
782
+ | `headers` | `Record<string, string>` | Extra headers forwarded to your proxy. |
783
+ | `copy` | `{ welcomeTitle?, welcomeSubtitle?, inputPlaceholder?, sendButtonLabel? }` | Customize user-facing text. |
784
+ | `theme` | `{ primary?, secondary?, surface?, muted?, accent?, radiusSm?, radiusMd?, radiusLg?, radiusFull? }` | Override CSS variables for the widget. Colors: `primary` (text/UI), `secondary` (unused), `surface` (backgrounds), `muted` (secondary text), `accent` (buttons/links). Border radius: `radiusSm` (0.75rem, inputs), `radiusMd` (1rem, cards), `radiusLg` (1.5rem, panels/bubbles), `radiusFull` (9999px, pills/buttons). |
785
+ | `features` | `AgentWidgetFeatureFlags` | Toggle UI features: `showReasoning?` (show thinking bubbles, default: `true`), `showToolCalls?` (show tool usage bubbles, default: `true`). |
786
+ | `launcher` | `{ enabled?, autoExpand?, title?, subtitle?, iconUrl?, position? }` | Controls the floating launcher button. |
787
+ | `initialMessages` | `AgentWidgetMessage[]` | Seed the conversation transcript. |
788
+ | `suggestionChips` | `string[]` | Render quick reply buttons above the composer. |
789
+ | `postprocessMessage` | `(ctx) => string` | Transform message text before it renders (return HTML). Combine with `markdownPostprocessor` for rich output. |
790
+ | `parserType` | `"plain" \| "json" \| "regex-json" \| "xml"` | Built-in parser type selector. Easy way to choose a parser without importing functions. Options: `"plain"` (default), `"json"` (partial-json), `"regex-json"` (regex-based), `"xml"`. If both `parserType` and `streamParser` are provided, `streamParser` takes precedence. |
791
+ | `streamParser` | `() => AgentWidgetStreamParser` | Custom stream parser for detecting formats and extracting text from streaming responses. Handles JSON, XML, or custom formats. See [Stream Parser Configuration](#stream-parser-configuration) below. |
792
+ | `clearChatHistoryStorageKey` | `string` | Additional localStorage key to clear when the clear chat button is clicked. The widget automatically clears `"persona-chat-history"` by default. Use this option to clear additional keys (e.g., if you're using a custom storage key). |
793
+ | `formEndpoint` | `string` | Endpoint used by built-in directives (defaults to `/form`). |
794
+ | `launcherWidth` | `string` | CSS width applied to the floating launcher panel (e.g. `320px`, `90vw`). Defaults to `min(400px, calc(100vw - 24px))`. |
795
+ | `debug` | `boolean` | Emits verbose logs to `console`. |
796
+
797
+ All options are safe to mutate via `initAgentWidget(...).update(newConfig)`.
798
+
799
+ ### Stream Parser Configuration
800
+
801
+ 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`.
802
+
803
+ **Key benefits of the unified stream parser:**
804
+ - **Format detection**: Automatically detects if content matches your parser's format
805
+ - **Extensible**: Handle JSON, XML, or any custom structured format
806
+ - **Incremental parsing**: Extract text as it streams in, not just when complete
807
+
808
+ **Quick start with `parserType` (recommended):**
809
+
810
+ The easiest way to use a built-in parser is with the `parserType` option:
811
+
812
+ ```javascript
813
+ import { initAgentWidget } from '@runtypelabs/persona';
814
+
815
+ const controller = initAgentWidget({
816
+ target: '#chat-root',
817
+ config: {
818
+ apiUrl: '/api/chat/dispatch',
819
+ parserType: 'json' // Options: 'plain', 'json', 'regex-json', 'xml'
820
+ }
821
+ });
822
+ ```
823
+
824
+ **Using built-in parsers with `streamParser` (ESM/Modules):**
825
+
826
+ ```javascript
827
+ import { initAgentWidget, createPlainTextParser, createJsonStreamParser, createXmlParser } from '@runtypelabs/persona';
828
+
829
+ const controller = initAgentWidget({
830
+ target: '#chat-root',
831
+ config: {
832
+ apiUrl: '/api/chat/dispatch',
833
+ streamParser: createJsonStreamParser // Use JSON parser
834
+ // Or: createXmlParser for XML, createPlainTextParser for plain text (default)
835
+ }
836
+ });
837
+ ```
838
+
839
+ **Using built-in parsers with CDN Script Tags:**
840
+
841
+ ```html
842
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/index.global.js"></script>
843
+ <script>
844
+ window.AgentWidget.initAgentWidget({
845
+ target: '#chat-root',
846
+ config: {
847
+ apiUrl: '/api/chat/dispatch',
848
+ streamParser: window.AgentWidget.createJsonStreamParser // JSON parser
849
+ // Or: window.AgentWidget.createXmlParser for XML
850
+ // Or: window.AgentWidget.createPlainTextParser for plain text (default)
851
+ }
852
+ });
853
+ </script>
854
+ ```
855
+
856
+ **Using with automatic installer script:**
857
+
858
+ ```html
859
+ <script>
860
+ window.siteAgentConfig = {
861
+ target: 'body',
862
+ config: {
863
+ apiUrl: '/api/chat/dispatch',
864
+ parserType: 'json' // Simple way to select parser - no function imports needed!
865
+ }
866
+ };
867
+ </script>
868
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
869
+ ```
870
+
871
+ **Alternative: Using `streamParser` with installer script:**
872
+
873
+ If you need a custom parser, you can still use `streamParser`:
874
+
875
+ ```html
876
+ <script>
877
+ window.siteAgentConfig = {
878
+ target: 'body',
879
+ config: {
880
+ apiUrl: '/api/chat/dispatch',
881
+ // Note: streamParser must be set after the script loads, or use a function
882
+ streamParser: function() {
883
+ return window.AgentWidget.createJsonStreamParser();
884
+ }
885
+ }
886
+ };
887
+ </script>
888
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
889
+ ```
890
+
891
+ Alternatively, you can set it after the script loads:
892
+
893
+ ```html
894
+ <script>
895
+ window.siteAgentConfig = {
896
+ target: 'body',
897
+ config: {
898
+ apiUrl: '/api/chat/dispatch'
899
+ }
900
+ };
901
+ </script>
902
+ <script src="https://cdn.jsdelivr.net/npm/@runtypelabs/persona@latest/dist/install.global.js"></script>
903
+ <script>
904
+ // Set parser after AgentWidget is loaded
905
+ if (window.siteAgentConfig && window.AgentWidget) {
906
+ window.siteAgentConfig.config.streamParser = window.AgentWidget.createJsonStreamParser;
907
+ }
908
+ </script>
909
+ ```
910
+
911
+ **Custom JSON parser example:**
912
+
913
+ ```javascript
914
+ const jsonParser = () => {
915
+ let extractedText = null;
916
+
917
+ return {
918
+ // Extract text field from JSON as it streams in
919
+ // Return null if not JSON or text not available yet
920
+ processChunk(accumulatedContent) {
921
+ const trimmed = accumulatedContent.trim();
922
+ // Return null if not JSON format
923
+ if (!trimmed.startsWith('{') && !trimmed.startsWith('[')) {
924
+ return null;
925
+ }
926
+
927
+ const match = accumulatedContent.match(/"text"\s*:\s*"([^"]*(?:\\.[^"]*)*)"/);
928
+ if (match) {
929
+ extractedText = match[1].replace(/\\"/g, '"').replace(/\\n/g, '\n');
930
+ return extractedText;
931
+ }
932
+ return null;
933
+ },
934
+
935
+ getExtractedText() {
936
+ return extractedText;
937
+ }
938
+ };
939
+ };
940
+
941
+ initAgentWidget({
942
+ target: '#chat-root',
943
+ config: {
944
+ apiUrl: '/api/chat/dispatch',
945
+ streamParser: jsonParser,
946
+ postprocessMessage: ({ text, raw }) => {
947
+ // raw contains the structured payload (JSON, XML, etc.)
948
+ return markdownPostprocessor(text);
949
+ }
950
+ }
951
+ });
952
+ ```
953
+
954
+ **Custom XML parser example:**
955
+
956
+ ```javascript
957
+ const xmlParser = () => {
958
+ let extractedText = null;
959
+
960
+ return {
961
+ processChunk(accumulatedContent) {
962
+ // Return null if not XML format
963
+ if (!accumulatedContent.trim().startsWith('<')) {
964
+ return null;
965
+ }
966
+
967
+ // Extract text from <text>...</text> tags
968
+ const match = accumulatedContent.match(/<text[^>]*>([\s\S]*?)<\/text>/);
969
+ if (match) {
970
+ extractedText = match[1];
971
+ return extractedText;
972
+ }
973
+ return null;
974
+ },
975
+
976
+ getExtractedText() {
977
+ return extractedText;
978
+ }
979
+ };
980
+ };
981
+ ```
982
+
983
+ **Parser interface:**
984
+
985
+ ```typescript
986
+ interface AgentWidgetStreamParser {
987
+ // Process a chunk and return extracted text (if available)
988
+ // Return null if the content doesn't match this parser's format or text is not yet available
989
+ processChunk(accumulatedContent: string): Promise<string | null> | string | null;
990
+
991
+ // Get the currently extracted text (may be partial)
992
+ getExtractedText(): string | null;
993
+
994
+ // Optional cleanup when parsing is complete
995
+ close?(): Promise<void> | void;
996
+ }
997
+ ```
998
+
999
+ The parser's `processChunk` method is called for each chunk. If the content matches your parser's format, return the extracted text and the raw payload. Built-in parsers already do this, so action handlers and middleware can read the original structured content without re-implementing a parser. Return `null` if the chunk isn't ready yet—the widget will keep waiting or fall back to plain text.
1000
+
1001
+ ### Optional proxy server
1002
+
1003
+ The proxy server handles flow configuration and forwards requests to Travrse. You can configure it in three ways:
1004
+
1005
+ **Option 1: Use default flow (recommended for getting started)**
1006
+
1007
+ ```ts
1008
+ // api/chat.ts
1009
+ import { createChatProxyApp } from '@runtypelabs/persona-proxy';
1010
+
1011
+ export default createChatProxyApp({
1012
+ path: '/api/chat/dispatch',
1013
+ allowedOrigins: ['https://www.example.com']
1014
+ });
1015
+ ```
1016
+
1017
+ **Option 2: Reference a Travrse flow ID**
1018
+
1019
+ ```ts
1020
+ import { createChatProxyApp } from '@runtypelabs/persona-proxy';
1021
+
1022
+ export default createChatProxyApp({
1023
+ path: '/api/chat/dispatch',
1024
+ allowedOrigins: ['https://www.example.com'],
1025
+ flowId: 'flow_abc123' // Flow created in Travrse dashboard or API
1026
+ });
1027
+ ```
1028
+
1029
+ **Option 3: Define a custom flow**
1030
+
1031
+ ```ts
1032
+ import { createChatProxyApp } from '@runtypelabs/persona-proxy';
1033
+
1034
+ export default createChatProxyApp({
1035
+ path: '/api/chat/dispatch',
1036
+ allowedOrigins: ['https://www.example.com'],
1037
+ flowConfig: {
1038
+ name: "Custom Chat Flow",
1039
+ description: "Specialized assistant flow",
1040
+ steps: [
1041
+ {
1042
+ id: "custom_prompt",
1043
+ name: "Custom Prompt",
1044
+ type: "prompt",
1045
+ enabled: true,
1046
+ config: {
1047
+ model: "meta/llama3.1-8b-instruct-free",
1048
+ responseFormat: "markdown",
1049
+ outputVariable: "prompt_result",
1050
+ userPrompt: "{{user_message}}",
1051
+ systemPrompt: "you are a helpful assistant, chatting with a user",
1052
+ previousMessages: "{{messages}}"
1053
+ }
1054
+ }
1055
+ ]
1056
+ }
1057
+ });
1058
+ ```
1059
+
1060
+ **Hosting on Vercel:**
1061
+
1062
+ ```ts
1063
+ import { createVercelHandler } from '@runtypelabs/persona-proxy';
1064
+
1065
+ export default createVercelHandler({
1066
+ allowedOrigins: ['https://www.example.com'],
1067
+ flowId: 'flow_abc123' // Optional
1068
+ });
1069
+ ```
1070
+
1071
+ **Environment setup:**
1072
+
1073
+ Add `TRAVRSE_API_KEY` to your environment. The proxy constructs the Travrse payload (including flow configuration) and streams the response back to the client.
1074
+
1075
+ ### Development notes
1076
+
1077
+ - The widget streams results using SSE and mirrors the backend `flow_complete`/`step_chunk` events.
1078
+ - Tailwind-esc classes are prefixed with `tvw-` and scoped to `#persona-root`, so they won't collide with the host page.
1079
+ - Run `pnpm dev` from the repository root to boot the example proxy (`examples/proxy`) and the vanilla demo (`examples/embedded-app`).
1080
+ - The proxy prefers port `43111` but automatically selects the next free port if needed.