@makemore/agent-frontend 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,14 +23,17 @@ Most chat widgets are tightly coupled to specific frameworks or require complex
23
23
  | Feature | Description |
24
24
  |---------|-------------|
25
25
  | đŸ’Ŧ **Real-time Streaming** | SSE-based message streaming for instant, token-by-token responses |
26
+ | 🔊 **Text-to-Speech** | ElevenLabs integration with secure Django proxy support |
26
27
  | 🎨 **Theming** | Customize colors, titles, messages, and position |
27
28
  | 🌙 **Dark Mode** | Automatic dark mode based on system preferences |
28
29
  | 📱 **Responsive** | Works seamlessly on desktop and mobile |
29
30
  | 🔧 **Debug Mode** | Toggle visibility of tool calls and results |
30
- | 🤖 **Demo Flows** | Built-in auto-run mode for showcasing agent journeys |
31
+ | 🤖 **Demo Flows** | Built-in auto-run mode with automatic, confirm, and manual modes |
31
32
  | 🔒 **Sessions** | Automatic anonymous session creation and management |
32
33
  | 💾 **Persistence** | Conversations persist across page reloads via localStorage |
33
34
  | đŸ›Ąī¸ **Isolated CSS** | Scoped styles that won't leak into or from your page |
35
+ | đŸŽ¯ **Configurable APIs** | Customize backend endpoints to match your server structure |
36
+ | 📝 **Enhanced Markdown** | Optional rich markdown with tables, code blocks, and syntax highlighting |
34
37
 
35
38
  ## Installation
36
39
 
@@ -61,9 +64,31 @@ Then include in your HTML:
61
64
  <script src="https://cdn.jsdelivr.net/npm/@makemore/agent-frontend/dist/chat-widget.js"></script>
62
65
  ```
63
66
 
67
+ ### Optional: Enhanced Markdown Support
68
+
69
+ For full-featured markdown rendering (tables, code blocks with syntax highlighting, etc.), include the optional markdown addon:
70
+
71
+ ```html
72
+ <!-- Core widget -->
73
+ <link rel="stylesheet" href="https://unpkg.com/@makemore/agent-frontend/dist/chat-widget.css">
74
+ <script src="https://unpkg.com/@makemore/agent-frontend/dist/chat-widget.js"></script>
75
+
76
+ <!-- Optional: Enhanced markdown with marked.js -->
77
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
78
+ <script src="https://unpkg.com/@makemore/agent-frontend/dist/chat-widget-markdown.js"></script>
79
+
80
+ <!-- Optional: Syntax highlighting for code blocks -->
81
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css">
82
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/core.min.js"></script>
83
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/javascript.min.js"></script>
84
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/python.min.js"></script>
85
+ ```
86
+
87
+ The widget automatically detects and uses the enhanced markdown parser if available. Without it, a basic markdown parser is used.
88
+
64
89
  ## Quick Start
65
90
 
66
- ### Initialize the widget
91
+ ### Basic Setup
67
92
 
68
93
  ```html
69
94
  <script>
@@ -76,6 +101,23 @@ Then include in your HTML:
76
101
  </script>
77
102
  ```
78
103
 
104
+ ### With Text-to-Speech (Recommended: Django Proxy)
105
+
106
+ ```html
107
+ <script>
108
+ ChatWidget.init({
109
+ backendUrl: 'https://your-api.com',
110
+ agentKey: 'your-agent',
111
+ title: 'Voice-Enabled Chat',
112
+ primaryColor: '#0066cc',
113
+ enableTTS: true,
114
+ ttsProxyUrl: 'https://your-api.com/api/tts/speak/',
115
+ });
116
+ </script>
117
+ ```
118
+
119
+ See `django-tts-example.py` for the complete Django backend implementation.
120
+
79
121
  ### With custom API paths
80
122
 
81
123
  ```html
@@ -115,6 +157,108 @@ Then include in your HTML:
115
157
  | `conversationIdKey` | string | `'chat_widget_conversation_id'` | localStorage key for conversation ID |
116
158
  | `sessionTokenKey` | string | `'chat_widget_session_token'` | localStorage key for session token |
117
159
  | `apiPaths` | object | See below | API endpoint paths (customizable for different backends) |
160
+ | `autoRunMode` | string | `'automatic'` | Demo flow mode: `'automatic'`, `'confirm'`, or `'manual'` |
161
+ | `autoRunDelay` | number | `1000` | Delay in milliseconds before auto-generating next message (automatic mode) |
162
+ | `enableTTS` | boolean | `false` | Enable text-to-speech for messages |
163
+ | `ttsProxyUrl` | string | `null` | Django proxy URL for TTS (recommended for security) |
164
+ | `elevenLabsApiKey` | string | `null` | ElevenLabs API key (only if not using proxy) |
165
+ | `ttsVoices` | object | `{ assistant: null, user: null }` | Voice IDs (only if not using proxy) |
166
+ | `ttsModel` | string | `'eleven_turbo_v2_5'` | ElevenLabs model (only if not using proxy) |
167
+ | `ttsSettings` | object | See below | ElevenLabs voice settings (only if not using proxy) |
168
+
169
+ ### Text-to-Speech (ElevenLabs)
170
+
171
+ Add realistic voice narration to your chat widget using ElevenLabs. Two integration options:
172
+
173
+ #### Option 1: Secure Django Proxy (Recommended)
174
+
175
+ Keep your API key secure on the server:
176
+
177
+ ```javascript
178
+ ChatWidget.init({
179
+ enableTTS: true,
180
+ ttsProxyUrl: 'https://your-backend.com/api/tts/speak/',
181
+ // No API key or voice IDs needed - configured on server
182
+ });
183
+ ```
184
+
185
+ **Django Setup:**
186
+
187
+ See `django-tts-example.py` for a complete Django REST Framework implementation. Quick setup:
188
+
189
+ 1. Install: `pip install requests`
190
+ 2. Add to `settings.py`:
191
+ ```python
192
+ ELEVENLABS_API_KEY = 'your_api_key_here'
193
+ ELEVENLABS_VOICES = {
194
+ 'assistant': 'EXAVITQu4vr4xnSDxMaL', # Bella
195
+ 'user': 'pNInz6obpgDQGcFmaJgB', # Adam
196
+ }
197
+ ```
198
+ 3. Add view from `django-tts-example.py` to your Django app
199
+ 4. Add URL route: `path('api/tts/speak/', views.text_to_speech)`
200
+
201
+ #### Option 2: Direct API (Client-Side)
202
+
203
+ For testing or simple deployments:
204
+
205
+ ```javascript
206
+ ChatWidget.init({
207
+ enableTTS: true,
208
+ elevenLabsApiKey: 'your_elevenlabs_api_key', // âš ī¸ Exposed to client
209
+ ttsVoices: {
210
+ assistant: 'EXAVITQu4vr4xnSDxMaL', // Bella
211
+ user: 'pNInz6obpgDQGcFmaJgB', // Adam
212
+ },
213
+ ttsModel: 'eleven_turbo_v2_5',
214
+ ttsSettings: {
215
+ stability: 0.5,
216
+ similarity_boost: 0.75,
217
+ style: 0.0,
218
+ use_speaker_boost: true,
219
+ },
220
+ });
221
+ ```
222
+
223
+ **Features:**
224
+ - Speaks assistant responses automatically
225
+ - Speaks simulated user messages in demo mode
226
+ - Queues messages to prevent overlap
227
+ - Waits for speech to finish before continuing demo (automatic mode)
228
+ - Toggle TTS on/off with button in header
229
+ - Visual indicator when speaking (pulsing icon)
230
+
231
+ **Get Voice IDs:**
232
+ 1. Go to https://elevenlabs.io/app/voice-library
233
+ 2. Choose voices and copy their IDs
234
+ 3. Or use the API: https://api.elevenlabs.io/v1/voices
235
+
236
+ **Control TTS:**
237
+ ```javascript
238
+ ChatWidget.toggleTTS(); // Toggle on/off
239
+ ChatWidget.stopSpeech(); // Stop current speech and clear queue
240
+ ```
241
+
242
+ ### Demo Flow Control
243
+
244
+ The widget supports three modes for demo flows:
245
+
246
+ - **Automatic** (`autoRunMode: 'automatic'`): Continuously generates customer responses with a configurable delay
247
+ - **Confirm Next** (`autoRunMode: 'confirm'`): Pauses after each assistant response and waits for user to click "Continue"
248
+ - **Manual** (`autoRunMode: 'manual'`): Stops auto-generation; user must manually type responses
249
+
250
+ These settings can be changed in real-time via the demo controls dropdown (visible when a demo is running).
251
+
252
+ ```javascript
253
+ ChatWidget.init({
254
+ autoRunMode: 'confirm', // Start in confirm mode
255
+ autoRunDelay: 2000, // 2 second delay in automatic mode
256
+ });
257
+
258
+ // Change mode programmatically
259
+ ChatWidget.setAutoRunMode('automatic');
260
+ ChatWidget.setAutoRunDelay(500); // 0.5 second delay
261
+ ```
118
262
 
119
263
  ### API Paths Configuration
120
264
 
@@ -188,12 +332,25 @@ ChatWidget.send('Hello, I need help!');
188
332
  // Clear the conversation
189
333
  ChatWidget.clearMessages();
190
334
 
335
+ // Text-to-speech controls
336
+ ChatWidget.toggleTTS(); // Toggle TTS on/off
337
+ ChatWidget.stopSpeech(); // Stop current speech and clear queue
338
+
191
339
  // Start a demo flow
192
340
  ChatWidget.startDemoFlow('quote');
193
341
 
194
- // Stop auto-run mode
342
+ // Stop demo flow
195
343
  ChatWidget.stopAutoRun();
196
344
 
345
+ // Continue demo flow (when in confirm mode and paused)
346
+ ChatWidget.continueAutoRun();
347
+
348
+ // Change demo flow mode
349
+ ChatWidget.setAutoRunMode('automatic'); // 'automatic', 'confirm', or 'manual'
350
+
351
+ // Change auto-run delay (in milliseconds)
352
+ ChatWidget.setAutoRunDelay(2000);
353
+
197
354
  // Remove the widget from the page
198
355
  ChatWidget.destroy();
199
356
 
@@ -204,6 +361,34 @@ const state = ChatWidget.getState();
204
361
  const config = ChatWidget.getConfig();
205
362
  ```
206
363
 
364
+ ## Markdown Support
365
+
366
+ The widget includes built-in markdown rendering for assistant messages:
367
+
368
+ ### Basic Markdown (Built-in)
369
+
370
+ The widget includes a lightweight markdown parser that supports:
371
+ - **Bold** (`**text**` or `__text__`)
372
+ - *Italic* (`*text*` or `_text_`)
373
+ - `Inline code` (`` `code` ``)
374
+ - [Links](url) (`[text](url)`)
375
+ - Lists (`- item` or `* item`)
376
+ - Line breaks
377
+
378
+ ### Enhanced Markdown (Optional)
379
+
380
+ Include `chat-widget-markdown.js` for full-featured markdown:
381
+ - **Tables** - Full GFM table support
382
+ - **Code blocks** - Multi-line code with syntax highlighting
383
+ - **Blockquotes** - `> quoted text`
384
+ - **Headings** - `# H1` through `###### H6`
385
+ - **Horizontal rules** - `---` or `***`
386
+ - **Task lists** - `- [ ] todo` and `- [x] done`
387
+ - **Strikethrough** - `~~text~~`
388
+
389
+ **Supported languages for syntax highlighting:**
390
+ Add highlight.js language modules as needed (JavaScript, Python, TypeScript, Go, Rust, etc.)
391
+
207
392
  ## Backend Requirements
208
393
 
209
394
  The widget expects a backend API with the following endpoints:
@@ -295,6 +480,36 @@ agent-frontend/
295
480
 
296
481
  Requires: `EventSource` (SSE), `fetch`, `localStorage`
297
482
 
483
+ ## Version History
484
+
485
+ ### v1.4.0 (Latest)
486
+ - ✨ **Text-to-Speech**: ElevenLabs integration with secure Django proxy support
487
+ - 🔊 Automatic speech for assistant and simulated user messages
488
+ - đŸŽ›ī¸ Smart speech queuing to prevent overlap
489
+ - 🔐 Secure proxy approach keeps API keys on server
490
+
491
+ ### v1.3.0
492
+ - 🎮 **Demo Flow Control**: Three modes (automatic, confirm-next, manual)
493
+ - âąī¸ Configurable delay for automatic mode (0-5000ms)
494
+ - đŸŽ¯ Real-time mode switching via dropdown menu
495
+ - â–ļī¸ Continue button for confirm mode
496
+
497
+ ### v1.2.0
498
+ - 📝 **Enhanced Markdown**: Optional rich markdown with tables and code blocks
499
+ - 🎨 Syntax highlighting support via highlight.js
500
+ - 🔧 Automatic detection of markdown addon
501
+
502
+ ### v1.1.0
503
+ - 🔌 **Configurable API Paths**: Customize backend endpoints
504
+ - đŸ› ī¸ Support for different backend URL structures
505
+
506
+ ### v1.0.0
507
+ - 🎉 Initial release
508
+ - đŸ’Ŧ Real-time SSE streaming
509
+ - 🎨 Theming and customization
510
+ - 🤖 Demo flows
511
+ - 🔒 Session management
512
+
298
513
  ## License
299
514
 
300
515
  MIT Š 2024
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Chat Widget - Enhanced Markdown Support
3
+ *
4
+ * This optional addon replaces the basic markdown parser with marked.js
5
+ * for full-featured markdown rendering including tables, code blocks with
6
+ * syntax highlighting, and more.
7
+ *
8
+ * Usage:
9
+ * 1. Include marked.js (and optionally highlight.js for syntax highlighting)
10
+ * 2. Include this file AFTER chat-widget.js
11
+ * 3. Initialize the widget normally
12
+ *
13
+ * Example:
14
+ * <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
+ * <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/core.min.js"></script>
16
+ * <script src="https://cdn.jsdelivr.net/npm/highlight.js@11/lib/languages/javascript.min.js"></script>
17
+ * <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11/styles/github-dark.min.css">
18
+ * <script src="chat-widget.js"></script>
19
+ * <script src="chat-widget-markdown.js"></script>
20
+ */
21
+ (function(global) {
22
+ 'use strict';
23
+
24
+ // Check if ChatWidget is available
25
+ if (!global.ChatWidget) {
26
+ console.error('[ChatWidget Markdown] ChatWidget must be loaded before chat-widget-markdown.js');
27
+ return;
28
+ }
29
+
30
+ // Check if marked is available
31
+ if (typeof marked === 'undefined') {
32
+ console.warn('[ChatWidget Markdown] marked.js not found. Install with: npm install marked or include from CDN');
33
+ console.warn('[ChatWidget Markdown] Falling back to basic markdown parser');
34
+ return;
35
+ }
36
+
37
+ console.log('[ChatWidget Markdown] Enhanced markdown support enabled');
38
+
39
+ // Configure marked
40
+ const markedOptions = {
41
+ breaks: true,
42
+ gfm: true,
43
+ headerIds: false,
44
+ mangle: false,
45
+ };
46
+
47
+ // Configure syntax highlighting if highlight.js is available
48
+ if (typeof hljs !== 'undefined') {
49
+ console.log('[ChatWidget Markdown] Syntax highlighting enabled');
50
+ markedOptions.highlight = function(code, lang) {
51
+ if (lang && hljs.getLanguage(lang)) {
52
+ try {
53
+ return hljs.highlight(code, { language: lang }).value;
54
+ } catch (err) {
55
+ console.error('[ChatWidget Markdown] Highlight error:', err);
56
+ }
57
+ }
58
+ return code;
59
+ };
60
+ }
61
+
62
+ marked.setOptions(markedOptions);
63
+
64
+ // Enhanced markdown parser using marked.js
65
+ function enhancedParseMarkdown(text) {
66
+ if (!text) return '';
67
+
68
+ try {
69
+ // Parse markdown with marked
70
+ let html = marked.parse(text);
71
+
72
+ // Sanitize links to open in new tab
73
+ html = html.replace(/<a href=/g, '<a target="_blank" rel="noopener noreferrer" href=');
74
+
75
+ return html;
76
+ } catch (err) {
77
+ console.error('[ChatWidget Markdown] Parse error:', err);
78
+ // Fallback to escaped text
79
+ const div = document.createElement('div');
80
+ div.textContent = text;
81
+ return div.innerHTML.replace(/\n/g, '<br>');
82
+ }
83
+ }
84
+
85
+ // Store reference to original init
86
+ const originalInit = global.ChatWidget.init;
87
+
88
+ // Override init to inject enhanced markdown parser
89
+ global.ChatWidget.init = function(userConfig = {}) {
90
+ // Call original init
91
+ originalInit.call(this, userConfig);
92
+
93
+ // Inject enhanced markdown parser
94
+ // We need to override the internal parseMarkdown function
95
+ // This is done by monkey-patching the render cycle
96
+ console.log('[ChatWidget Markdown] Markdown parser enhanced with marked.js');
97
+ };
98
+
99
+ // Expose the enhanced parser for internal use
100
+ // The widget will need to check for this and use it if available
101
+ global.ChatWidget._enhancedMarkdownParser = enhancedParseMarkdown;
102
+
103
+ // Add configuration option
104
+ global.ChatWidget.enableEnhancedMarkdown = function() {
105
+ console.log('[ChatWidget Markdown] Enhanced markdown explicitly enabled');
106
+ return true;
107
+ };
108
+
109
+ })(typeof window !== 'undefined' ? window : this);
110
+
@@ -159,6 +159,19 @@
159
159
  color: #ffd700;
160
160
  }
161
161
 
162
+ .cw-btn-speaking {
163
+ animation: pulse-speaking 1.5s ease-in-out infinite;
164
+ }
165
+
166
+ @keyframes pulse-speaking {
167
+ 0%, 100% {
168
+ background: rgba(255, 255, 255, 0.3);
169
+ }
170
+ 50% {
171
+ background: rgba(255, 255, 255, 0.5);
172
+ }
173
+ }
174
+
162
175
  /* Status bar */
163
176
  .cw-status-bar {
164
177
  display: flex;
@@ -450,6 +463,77 @@
450
463
  background: rgba(239, 68, 68, 0.1);
451
464
  }
452
465
 
466
+ /* Auto-run controls */
467
+ .cw-autorun-controls {
468
+ padding: 8px 12px;
469
+ display: flex;
470
+ flex-direction: column;
471
+ gap: 8px;
472
+ }
473
+
474
+ .cw-control-label {
475
+ display: flex;
476
+ align-items: center;
477
+ gap: 8px;
478
+ font-size: 13px;
479
+ color: var(--cw-text);
480
+ cursor: pointer;
481
+ padding: 4px;
482
+ border-radius: 4px;
483
+ transition: background 0.15s;
484
+ }
485
+
486
+ .cw-control-label:hover {
487
+ background: var(--cw-bg-muted);
488
+ }
489
+
490
+ .cw-control-label input[type="radio"] {
491
+ margin: 0;
492
+ cursor: pointer;
493
+ }
494
+
495
+ .cw-delay-control {
496
+ padding: 8px 12px;
497
+ }
498
+
499
+ .cw-delay-control input[type="range"] {
500
+ width: 100%;
501
+ margin-top: 4px;
502
+ cursor: pointer;
503
+ }
504
+
505
+ /* Continue button */
506
+ .cw-continue-bar {
507
+ padding: 12px 16px;
508
+ background: var(--cw-bg-muted);
509
+ border-top: 1px solid var(--cw-border);
510
+ display: flex;
511
+ justify-content: center;
512
+ }
513
+
514
+ .cw-continue-btn {
515
+ padding: 10px 20px;
516
+ border: none;
517
+ border-radius: var(--cw-radius-sm);
518
+ background: var(--cw-primary);
519
+ color: white;
520
+ font-size: 14px;
521
+ font-weight: 600;
522
+ cursor: pointer;
523
+ transition: opacity 0.15s;
524
+ display: flex;
525
+ align-items: center;
526
+ gap: 6px;
527
+ }
528
+
529
+ .cw-continue-btn:hover {
530
+ opacity: 0.9;
531
+ }
532
+
533
+ .cw-continue-btn:active {
534
+ opacity: 0.8;
535
+ }
536
+
453
537
  /* Responsive */
454
538
  @media (max-width: 480px) {
455
539
  .cw-widget {
@@ -501,3 +585,118 @@
501
585
  }
502
586
  }
503
587
 
588
+ /* Enhanced Markdown Styles (for chat-widget-markdown.js) */
589
+ .cw-message pre {
590
+ background: var(--cw-bg-muted);
591
+ border: 1px solid var(--cw-border);
592
+ border-radius: var(--cw-radius-sm);
593
+ padding: 12px;
594
+ overflow-x: auto;
595
+ margin: 8px 0;
596
+ font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
597
+ font-size: 13px;
598
+ line-height: 1.4;
599
+ }
600
+
601
+ .cw-message pre code {
602
+ background: none;
603
+ padding: 0;
604
+ border-radius: 0;
605
+ }
606
+
607
+ .cw-message table {
608
+ border-collapse: collapse;
609
+ width: 100%;
610
+ margin: 8px 0;
611
+ font-size: 13px;
612
+ }
613
+
614
+ .cw-message table th,
615
+ .cw-message table td {
616
+ border: 1px solid var(--cw-border);
617
+ padding: 8px 12px;
618
+ text-align: left;
619
+ }
620
+
621
+ .cw-message table th {
622
+ background: var(--cw-bg-muted);
623
+ font-weight: 600;
624
+ }
625
+
626
+ .cw-message table tr:nth-child(even) {
627
+ background: rgba(0, 0, 0, 0.02);
628
+ }
629
+
630
+ .cw-message blockquote {
631
+ border-left: 3px solid var(--cw-primary);
632
+ padding-left: 12px;
633
+ margin: 8px 0;
634
+ color: var(--cw-text-muted);
635
+ font-style: italic;
636
+ }
637
+
638
+ .cw-message h1,
639
+ .cw-message h2,
640
+ .cw-message h3,
641
+ .cw-message h4,
642
+ .cw-message h5,
643
+ .cw-message h6 {
644
+ margin: 12px 0 8px 0;
645
+ font-weight: 600;
646
+ line-height: 1.3;
647
+ }
648
+
649
+ .cw-message h1 { font-size: 1.5em; }
650
+ .cw-message h2 { font-size: 1.3em; }
651
+ .cw-message h3 { font-size: 1.1em; }
652
+ .cw-message h4 { font-size: 1em; }
653
+
654
+ .cw-message hr {
655
+ border: none;
656
+ border-top: 1px solid var(--cw-border);
657
+ margin: 12px 0;
658
+ }
659
+
660
+ .cw-message ul,
661
+ .cw-message ol {
662
+ margin: 8px 0;
663
+ padding-left: 24px;
664
+ }
665
+
666
+ .cw-message li {
667
+ margin: 4px 0;
668
+ }
669
+
670
+ .cw-message p {
671
+ margin: 8px 0;
672
+ }
673
+
674
+ .cw-message p:first-child {
675
+ margin-top: 0;
676
+ }
677
+
678
+ .cw-message p:last-child {
679
+ margin-bottom: 0;
680
+ }
681
+
682
+ /* Dark mode adjustments for enhanced markdown */
683
+ @media (prefers-color-scheme: dark) {
684
+ .cw-message pre {
685
+ background: rgba(255, 255, 255, 0.05);
686
+ border-color: rgba(255, 255, 255, 0.1);
687
+ }
688
+
689
+ .cw-message table th {
690
+ background: rgba(255, 255, 255, 0.05);
691
+ }
692
+
693
+ .cw-message table tr:nth-child(even) {
694
+ background: rgba(255, 255, 255, 0.02);
695
+ }
696
+
697
+ .cw-message table th,
698
+ .cw-message table td {
699
+ border-color: rgba(255, 255, 255, 0.1);
700
+ }
701
+ }
702
+
@@ -43,6 +43,24 @@
43
43
  runEvents: '/api/agent-runtime/runs/{runId}/events/',
44
44
  simulateCustomer: '/api/agent-runtime/simulate-customer/',
45
45
  },
46
+ // Demo flow control
47
+ autoRunDelay: 1000, // Delay in ms before auto-generating next message
48
+ autoRunMode: 'automatic', // 'automatic', 'confirm', or 'manual'
49
+ // Text-to-speech (ElevenLabs)
50
+ enableTTS: false,
51
+ ttsProxyUrl: null, // If set, uses Django proxy instead of direct API calls
52
+ elevenLabsApiKey: null, // Only needed if not using proxy
53
+ ttsVoices: {
54
+ assistant: null, // ElevenLabs voice ID for assistant (not needed if using proxy)
55
+ user: null, // ElevenLabs voice ID for simulated user (not needed if using proxy)
56
+ },
57
+ ttsModel: 'eleven_turbo_v2_5', // ElevenLabs model (not needed if using proxy)
58
+ ttsSettings: {
59
+ stability: 0.5,
60
+ similarity_boost: 0.75,
61
+ style: 0.0,
62
+ use_speaker_boost: true,
63
+ },
46
64
  };
47
65
 
48
66
  // State
@@ -52,7 +70,8 @@
52
70
  isExpanded: false,
53
71
  isLoading: false,
54
72
  isSimulating: false,
55
- autoRunMode: false,
73
+ autoRunActive: false,
74
+ autoRunPaused: false,
56
75
  debugMode: false,
57
76
  journeyType: 'general',
58
77
  messages: [],
@@ -60,6 +79,9 @@
60
79
  sessionToken: null,
61
80
  error: null,
62
81
  eventSource: null,
82
+ currentAudio: null,
83
+ isSpeaking: false,
84
+ speechQueue: [],
63
85
  };
64
86
 
65
87
  // DOM elements
@@ -82,30 +104,35 @@
82
104
  }
83
105
 
84
106
  function parseMarkdown(text) {
85
- // Simple markdown parsing for common patterns
107
+ // Check if enhanced markdown parser is available (from chat-widget-markdown.js)
108
+ if (global.ChatWidget && global.ChatWidget._enhancedMarkdownParser) {
109
+ return global.ChatWidget._enhancedMarkdownParser(text);
110
+ }
111
+
112
+ // Fallback: Simple markdown parsing for common patterns
86
113
  let html = escapeHtml(text);
87
-
114
+
88
115
  // Bold: **text** or __text__
89
116
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
90
117
  html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
91
-
118
+
92
119
  // Italic: *text* or _text_
93
120
  html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
94
121
  html = html.replace(/_(.+?)_/g, '<em>$1</em>');
95
-
122
+
96
123
  // Code: `code`
97
124
  html = html.replace(/`(.+?)`/g, '<code>$1</code>');
98
-
125
+
99
126
  // Links: [text](url)
100
127
  html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
101
-
128
+
102
129
  // Line breaks
103
130
  html = html.replace(/\n/g, '<br>');
104
-
131
+
105
132
  // Lists: - item or * item
106
133
  html = html.replace(/^[\-\*]\s+(.+)$/gm, '<li>$1</li>');
107
134
  html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
108
-
135
+
109
136
  return html;
110
137
  }
111
138
 
@@ -129,6 +156,135 @@
129
156
  }
130
157
  }
131
158
 
159
+ // ============================================================================
160
+ // Text-to-Speech (ElevenLabs)
161
+ // ============================================================================
162
+
163
+ async function speakText(text, role) {
164
+ if (!config.enableTTS) return;
165
+
166
+ // Check if we have either proxy or direct API access
167
+ if (!config.ttsProxyUrl && !config.elevenLabsApiKey) return;
168
+
169
+ // If using direct API, check for voice ID
170
+ if (!config.ttsProxyUrl) {
171
+ const voiceId = role === 'assistant' ? config.ttsVoices.assistant : config.ttsVoices.user;
172
+ if (!voiceId) return;
173
+ }
174
+
175
+ // Add to queue
176
+ state.speechQueue.push({ text, role });
177
+
178
+ // Process queue if not already speaking
179
+ if (!state.isSpeaking) {
180
+ processSpeechQueue();
181
+ }
182
+ }
183
+
184
+ async function processSpeechQueue() {
185
+ if (state.speechQueue.length === 0) {
186
+ state.isSpeaking = false;
187
+ render();
188
+
189
+ // If auto-run is waiting for speech to finish, continue
190
+ if (state.autoRunActive && state.autoRunPaused && config.autoRunMode === 'automatic') {
191
+ setTimeout(() => {
192
+ if (state.autoRunActive && !state.isSpeaking) {
193
+ continueAutoRun();
194
+ }
195
+ }, config.autoRunDelay);
196
+ }
197
+ return;
198
+ }
199
+
200
+ state.isSpeaking = true;
201
+ render();
202
+
203
+ const { text, role } = state.speechQueue.shift();
204
+
205
+ try {
206
+ let response;
207
+
208
+ if (config.ttsProxyUrl) {
209
+ // Use Django proxy
210
+ response = await fetch(config.ttsProxyUrl, {
211
+ method: 'POST',
212
+ headers: {
213
+ 'Content-Type': 'application/json',
214
+ ...(state.sessionToken ? { [config.anonymousTokenHeader]: state.sessionToken } : {}),
215
+ },
216
+ body: JSON.stringify({
217
+ text: text,
218
+ role: role,
219
+ }),
220
+ });
221
+ } else {
222
+ // Direct ElevenLabs API call
223
+ const voiceId = role === 'assistant' ? config.ttsVoices.assistant : config.ttsVoices.user;
224
+ response = await fetch(`https://api.elevenlabs.io/v1/text-to-speech/${voiceId}`, {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Accept': 'audio/mpeg',
228
+ 'Content-Type': 'application/json',
229
+ 'xi-api-key': config.elevenLabsApiKey,
230
+ },
231
+ body: JSON.stringify({
232
+ text: text,
233
+ model_id: config.ttsModel,
234
+ voice_settings: config.ttsSettings,
235
+ }),
236
+ });
237
+ }
238
+
239
+ if (!response.ok) {
240
+ throw new Error(`TTS API error: ${response.status}`);
241
+ }
242
+
243
+ const audioBlob = await response.blob();
244
+ const audioUrl = URL.createObjectURL(audioBlob);
245
+ const audio = new Audio(audioUrl);
246
+
247
+ state.currentAudio = audio;
248
+
249
+ audio.onended = () => {
250
+ URL.revokeObjectURL(audioUrl);
251
+ state.currentAudio = null;
252
+ processSpeechQueue();
253
+ };
254
+
255
+ audio.onerror = () => {
256
+ console.error('[ChatWidget] Audio playback error');
257
+ URL.revokeObjectURL(audioUrl);
258
+ state.currentAudio = null;
259
+ processSpeechQueue();
260
+ };
261
+
262
+ await audio.play();
263
+ } catch (err) {
264
+ console.error('[ChatWidget] TTS error:', err);
265
+ state.currentAudio = null;
266
+ processSpeechQueue();
267
+ }
268
+ }
269
+
270
+ function stopSpeech() {
271
+ if (state.currentAudio) {
272
+ state.currentAudio.pause();
273
+ state.currentAudio = null;
274
+ }
275
+ state.speechQueue = [];
276
+ state.isSpeaking = false;
277
+ render();
278
+ }
279
+
280
+ function toggleTTS() {
281
+ config.enableTTS = !config.enableTTS;
282
+ if (!config.enableTTS) {
283
+ stopSpeech();
284
+ }
285
+ render();
286
+ }
287
+
132
288
  // ============================================================================
133
289
  // Session Management
134
290
  // ============================================================================
@@ -338,9 +494,25 @@
338
494
  state.eventSource = null;
339
495
  render();
340
496
 
497
+ // Speak assistant message if TTS enabled
498
+ if (assistantContent && !state.error) {
499
+ speakText(assistantContent, 'assistant');
500
+ }
501
+
341
502
  // Trigger auto-run if enabled
342
- if (state.autoRunMode && !state.error) {
343
- setTimeout(() => triggerAutoRun(), 1000);
503
+ if (state.autoRunActive && !state.error) {
504
+ if (config.autoRunMode === 'automatic') {
505
+ // Wait for speech to finish before continuing
506
+ if (config.enableTTS && assistantContent) {
507
+ state.autoRunPaused = true;
508
+ // processSpeechQueue will continue when done
509
+ } else {
510
+ setTimeout(() => triggerAutoRun(), config.autoRunDelay);
511
+ }
512
+ } else if (config.autoRunMode === 'confirm') {
513
+ state.autoRunPaused = true;
514
+ render();
515
+ }
344
516
  }
345
517
  };
346
518
 
@@ -365,12 +537,13 @@
365
537
  // ============================================================================
366
538
 
367
539
  async function triggerAutoRun() {
368
- if (!state.autoRunMode || state.isLoading || state.isSimulating) return;
540
+ if (!state.autoRunActive || state.isLoading || state.isSimulating) return;
369
541
 
370
542
  const lastMessage = state.messages[state.messages.length - 1];
371
543
  if (lastMessage?.role !== 'assistant') return;
372
544
 
373
545
  state.isSimulating = true;
546
+ state.autoRunPaused = false;
374
547
  render();
375
548
 
376
549
  try {
@@ -387,6 +560,12 @@
387
560
  const data = await response.json();
388
561
  if (data.response) {
389
562
  state.isSimulating = false;
563
+
564
+ // Speak simulated user message if TTS enabled
565
+ if (config.enableTTS && config.ttsVoices.user) {
566
+ await speakText(data.response, 'user');
567
+ }
568
+
390
569
  await sendMessage(data.response);
391
570
  return;
392
571
  }
@@ -402,7 +581,8 @@
402
581
  async function startDemoFlow(journeyType) {
403
582
  clearMessages();
404
583
  state.journeyType = journeyType;
405
- state.autoRunMode = true;
584
+ state.autoRunActive = true;
585
+ state.autoRunPaused = false;
406
586
  render();
407
587
 
408
588
  const journey = config.journeyTypes[journeyType];
@@ -419,7 +599,26 @@
419
599
  }
420
600
 
421
601
  function stopAutoRun() {
422
- state.autoRunMode = false;
602
+ state.autoRunActive = false;
603
+ state.autoRunPaused = false;
604
+ render();
605
+ }
606
+
607
+ function continueAutoRun() {
608
+ if (state.autoRunActive && state.autoRunPaused) {
609
+ triggerAutoRun();
610
+ }
611
+ }
612
+
613
+ function setAutoRunMode(mode) {
614
+ if (['automatic', 'confirm', 'manual'].includes(mode)) {
615
+ config.autoRunMode = mode;
616
+ render();
617
+ }
618
+ }
619
+
620
+ function setAutoRunDelay(delay) {
621
+ config.autoRunDelay = Math.max(0, parseInt(delay) || 1000);
423
622
  render();
424
623
  }
425
624
 
@@ -435,7 +634,8 @@
435
634
 
436
635
  function closeWidget() {
437
636
  state.isOpen = false;
438
- state.autoRunMode = false;
637
+ state.autoRunActive = false;
638
+ state.autoRunPaused = false;
439
639
  render();
440
640
  }
441
641
 
@@ -453,7 +653,8 @@
453
653
  state.messages = [];
454
654
  state.conversationId = null;
455
655
  state.error = null;
456
- state.autoRunMode = false;
656
+ state.autoRunActive = false;
657
+ state.autoRunPaused = false;
457
658
  setStoredValue(config.conversationIdKey, null);
458
659
  render();
459
660
  }
@@ -504,16 +705,48 @@
504
705
  </button>
505
706
  `).join('');
506
707
 
507
- const stopButton = state.autoRunMode ? `
708
+ const controlsSection = state.autoRunActive ? `
709
+ <div class="cw-dropdown-separator"></div>
710
+ <div class="cw-dropdown-label">Demo Controls</div>
711
+ <div class="cw-autorun-controls">
712
+ <label class="cw-control-label">
713
+ <input type="radio" name="autorun-mode" value="automatic"
714
+ ${config.autoRunMode === 'automatic' ? 'checked' : ''}
715
+ onchange="ChatWidget.setAutoRunMode('automatic')">
716
+ <span>⚡ Automatic</span>
717
+ </label>
718
+ <label class="cw-control-label">
719
+ <input type="radio" name="autorun-mode" value="confirm"
720
+ ${config.autoRunMode === 'confirm' ? 'checked' : ''}
721
+ onchange="ChatWidget.setAutoRunMode('confirm')">
722
+ <span>👆 Confirm Next</span>
723
+ </label>
724
+ <label class="cw-control-label">
725
+ <input type="radio" name="autorun-mode" value="manual"
726
+ ${config.autoRunMode === 'manual' ? 'checked' : ''}
727
+ onchange="ChatWidget.setAutoRunMode('manual')">
728
+ <span>✋ Manual</span>
729
+ </label>
730
+ </div>
731
+ ${config.autoRunMode === 'automatic' ? `
732
+ <div class="cw-delay-control">
733
+ <label class="cw-control-label">
734
+ <span>Delay: ${config.autoRunDelay}ms</span>
735
+ <input type="range" min="0" max="5000" step="100"
736
+ value="${config.autoRunDelay}"
737
+ oninput="ChatWidget.setAutoRunDelay(this.value)">
738
+ </label>
739
+ </div>
740
+ ` : ''}
508
741
  <div class="cw-dropdown-separator"></div>
509
742
  <button class="cw-dropdown-item cw-dropdown-item-danger" data-action="stop-autorun">
510
- âšī¸ Stop Auto-Run
743
+ âšī¸ Stop Demo
511
744
  </button>
512
745
  ` : '';
513
746
 
514
747
  return `
515
748
  <div class="cw-dropdown">
516
- <button class="cw-header-btn ${state.autoRunMode ? 'cw-btn-active' : ''}"
749
+ <button class="cw-header-btn ${state.autoRunActive ? 'cw-btn-active' : ''}"
517
750
  data-action="toggle-journey-dropdown"
518
751
  title="Demo Flows"
519
752
  ${state.isLoading || state.isSimulating ? 'disabled' : ''}>
@@ -523,7 +756,7 @@
523
756
  <div class="cw-dropdown-label">Demo Flows</div>
524
757
  <div class="cw-dropdown-separator"></div>
525
758
  ${journeyItems}
526
- ${stopButton}
759
+ ${controlsSection}
527
760
  </div>
528
761
  </div>
529
762
  `;
@@ -567,9 +800,17 @@
567
800
  </div>
568
801
  ` : '';
569
802
 
570
- const statusBar = (state.autoRunMode || state.debugMode) ? `
803
+ const continueButton = (state.autoRunActive && state.autoRunPaused && config.autoRunMode === 'confirm') ? `
804
+ <div class="cw-continue-bar">
805
+ <button class="cw-continue-btn" data-action="continue-autorun" style="background-color: ${config.primaryColor}">
806
+ â–ļī¸ Continue Demo
807
+ </button>
808
+ </div>
809
+ ` : '';
810
+
811
+ const statusBar = (state.autoRunActive || state.debugMode) ? `
571
812
  <div class="cw-status-bar">
572
- ${state.autoRunMode ? `<span>🤖 Auto-run: ${config.journeyTypes[state.journeyType]?.label || state.journeyType}</span>` : ''}
813
+ ${state.autoRunActive ? `<span>🤖 Demo: ${config.journeyTypes[state.journeyType]?.label || state.journeyType} (${config.autoRunMode})</span>` : ''}
573
814
  ${state.debugMode ? '<span>🐛 Debug</span>' : ''}
574
815
  </div>
575
816
  ` : '';
@@ -597,6 +838,13 @@
597
838
  </svg>
598
839
  </button>
599
840
  ` : ''}
841
+ ${config.elevenLabsApiKey ? `
842
+ <button class="cw-header-btn ${config.enableTTS ? 'cw-btn-active' : ''} ${state.isSpeaking ? 'cw-btn-speaking' : ''}"
843
+ data-action="toggle-tts"
844
+ title="${config.enableTTS ? (state.isSpeaking ? 'Speaking...' : 'TTS Enabled') : 'TTS Disabled'}">
845
+ ${state.isSpeaking ? '🔊' : (config.enableTTS ? '🔉' : '🔇')}
846
+ </button>
847
+ ` : ''}
600
848
  ${renderJourneyDropdown()}
601
849
  <button class="cw-header-btn" data-action="toggle-expand" title="${state.isExpanded ? 'Minimize' : 'Expand'}">
602
850
  ${state.isExpanded ? '⊖' : '⊕'}
@@ -611,6 +859,7 @@
611
859
  ${messagesHtml}
612
860
  ${typingIndicator}
613
861
  </div>
862
+ ${continueButton}
614
863
  ${errorBar}
615
864
  <form class="cw-input-form" id="cw-input-form">
616
865
  <input type="text" class="cw-input" placeholder="${escapeHtml(config.placeholder)}" ${state.isLoading ? 'disabled' : ''}>
@@ -643,8 +892,10 @@
643
892
  case 'close': closeWidget(); break;
644
893
  case 'toggle-expand': toggleExpand(); break;
645
894
  case 'toggle-debug': toggleDebugMode(); break;
895
+ case 'toggle-tts': toggleTTS(); break;
646
896
  case 'clear': clearMessages(); break;
647
897
  case 'stop-autorun': stopAutoRun(); break;
898
+ case 'continue-autorun': continueAutoRun(); break;
648
899
  case 'toggle-journey-dropdown':
649
900
  const dropdown = document.getElementById('cw-journey-dropdown');
650
901
  if (dropdown) {
@@ -756,6 +1007,11 @@
756
1007
  clearMessages,
757
1008
  startDemoFlow,
758
1009
  stopAutoRun,
1010
+ continueAutoRun,
1011
+ setAutoRunMode,
1012
+ setAutoRunDelay,
1013
+ toggleTTS,
1014
+ stopSpeech,
759
1015
  getState: () => ({ ...state }),
760
1016
  getConfig: () => ({ ...config }),
761
1017
  };
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "1.1.0",
3
+ "version": "1.4.0",
4
4
  "description": "A standalone, zero-dependency chat widget for AI agents. Embed conversational AI into any website with a single script tag.",
5
5
  "main": "dist/chat-widget.js",
6
6
  "files": [
7
7
  "dist/chat-widget.js",
8
8
  "dist/chat-widget.css",
9
+ "dist/chat-widget-markdown.js",
9
10
  "README.md",
10
11
  "LICENSE"
11
12
  ],