@makemore/agent-frontend 1.4.0 → 1.6.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
@@ -131,6 +131,9 @@ See `django-tts-example.py` for the complete Django backend implementation.
131
131
  anonymousSession: '/api/auth/session/',
132
132
  runs: '/api/chat/runs/',
133
133
  runEvents: '/api/chat/runs/{runId}/events/',
134
+ simulateCustomer: '/api/chat/simulate-customer/',
135
+ ttsVoices: '/api/tts/voices/', // For voice settings UI (proxy mode)
136
+ ttsSetVoice: '/api/tts/set-voice/', // For voice settings UI (proxy mode)
134
137
  },
135
138
  });
136
139
  </script>
@@ -165,6 +168,12 @@ See `django-tts-example.py` for the complete Django backend implementation.
165
168
  | `ttsVoices` | object | `{ assistant: null, user: null }` | Voice IDs (only if not using proxy) |
166
169
  | `ttsModel` | string | `'eleven_turbo_v2_5'` | ElevenLabs model (only if not using proxy) |
167
170
  | `ttsSettings` | object | See below | ElevenLabs voice settings (only if not using proxy) |
171
+ | `availableVoices` | array | `[]` | List of available voices (auto-populated from ElevenLabs API) |
172
+ | `showClearButton` | boolean | `true` | Show clear conversation button in header |
173
+ | `showDebugButton` | boolean | `true` | Show debug mode toggle button in header |
174
+ | `showTTSButton` | boolean | `true` | Show TTS toggle button in header |
175
+ | `showVoiceSettings` | boolean | `true` | Show voice settings button in header (works with proxy and direct API) |
176
+ | `showExpandButton` | boolean | `true` | Show expand/minimize button in header |
168
177
 
169
178
  ### Text-to-Speech (ElevenLabs)
170
179
 
@@ -195,8 +204,66 @@ ELEVENLABS_VOICES = {
195
204
  'user': 'pNInz6obpgDQGcFmaJgB', # Adam
196
205
  }
197
206
  ```
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)`
207
+ 3. Add views from `django-tts-example.py` to your Django app
208
+ 4. Add URL routes:
209
+ ```python
210
+ path('api/tts/speak/', views.text_to_speech),
211
+ path('api/tts/voices/', views.get_voices), # For voice settings UI
212
+ path('api/tts/set-voice/', views.set_voice), # For voice settings UI
213
+ ```
214
+
215
+ **Voice Settings Support:**
216
+
217
+ The widget now supports voice settings UI in proxy mode! Add these endpoints to enable the voice picker:
218
+
219
+ ```python
220
+ # Get available voices
221
+ @api_view(['GET'])
222
+ def get_voices(request):
223
+ """Fetch available voices from ElevenLabs"""
224
+ try:
225
+ response = requests.get(
226
+ 'https://api.elevenlabs.io/v1/voices',
227
+ headers={'xi-api-key': settings.ELEVENLABS_API_KEY}
228
+ )
229
+ return JsonResponse(response.json())
230
+ except Exception as e:
231
+ return JsonResponse({'error': str(e)}, status=500)
232
+
233
+ # Set voice for user session
234
+ @api_view(['POST'])
235
+ def set_voice(request):
236
+ """Update voice preference for user's session"""
237
+ role = request.data.get('role') # 'assistant' or 'user'
238
+ voice_id = request.data.get('voice_id')
239
+
240
+ # Store in session or database
241
+ if not hasattr(request, 'session'):
242
+ return JsonResponse({'error': 'Session not available'}, status=400)
243
+
244
+ if role not in ['assistant', 'user']:
245
+ return JsonResponse({'error': 'Invalid role'}, status=400)
246
+
247
+ # Store voice preference in session
248
+ if 'tts_voices' not in request.session:
249
+ request.session['tts_voices'] = {}
250
+ request.session['tts_voices'][role] = voice_id
251
+ request.session.modified = True
252
+
253
+ return JsonResponse({'success': True, 'role': role, 'voice_id': voice_id})
254
+
255
+ # Update text_to_speech view to use session voices
256
+ @api_view(['POST'])
257
+ def text_to_speech(request):
258
+ text = request.data.get('text', '')
259
+ role = request.data.get('role', 'assistant')
260
+
261
+ # Get voice from session or fall back to settings
262
+ session_voices = request.session.get('tts_voices', {})
263
+ voice_id = session_voices.get(role) or settings.ELEVENLABS_VOICES.get(role)
264
+
265
+ # ... rest of TTS implementation
266
+ ```
200
267
 
201
268
  #### Option 2: Direct API (Client-Side)
202
269
 
@@ -237,6 +304,28 @@ ChatWidget.init({
237
304
  ```javascript
238
305
  ChatWidget.toggleTTS(); // Toggle on/off
239
306
  ChatWidget.stopSpeech(); // Stop current speech and clear queue
307
+ ChatWidget.setVoice('assistant', 'voice_id'); // Change assistant voice
308
+ ChatWidget.setVoice('user', 'voice_id'); // Change user voice
309
+ ```
310
+
311
+ **Voice Settings UI:**
312
+
313
+ A voice settings button (🎙️) appears in the header when TTS is enabled. Click it to:
314
+ - Select assistant voice from dropdown
315
+ - Select customer voice for demo mode
316
+ - Voices are automatically fetched from your ElevenLabs account (direct API) or Django backend (proxy mode)
317
+
318
+ **Works with both proxy and direct API modes!** Just implement the `/api/tts/voices/` and `/api/tts/set-voice/` endpoints in your Django backend (see above).
319
+
320
+ **Customize Header Buttons:**
321
+ ```javascript
322
+ ChatWidget.init({
323
+ showClearButton: true, // Show/hide clear button
324
+ showDebugButton: true, // Show/hide debug button
325
+ showTTSButton: true, // Show/hide TTS toggle
326
+ showVoiceSettings: true, // Show/hide voice settings (direct API only)
327
+ showExpandButton: true, // Show/hide expand button
328
+ });
240
329
  ```
241
330
 
242
331
  ### Demo Flow Control
@@ -335,6 +424,8 @@ ChatWidget.clearMessages();
335
424
  // Text-to-speech controls
336
425
  ChatWidget.toggleTTS(); // Toggle TTS on/off
337
426
  ChatWidget.stopSpeech(); // Stop current speech and clear queue
427
+ ChatWidget.setVoice('assistant', 'voice_id'); // Change assistant voice
428
+ ChatWidget.setVoice('user', 'voice_id'); // Change user voice
338
429
 
339
430
  // Start a demo flow
340
431
  ChatWidget.startDemoFlow('quote');
@@ -172,6 +172,95 @@
172
172
  }
173
173
  }
174
174
 
175
+ /* Voice Settings */
176
+ .cw-voice-settings {
177
+ background: var(--cw-bg-muted);
178
+ border-bottom: 1px solid var(--cw-border);
179
+ animation: slideDown 0.2s ease-out;
180
+ }
181
+
182
+ @keyframes slideDown {
183
+ from {
184
+ max-height: 0;
185
+ opacity: 0;
186
+ }
187
+ to {
188
+ max-height: 200px;
189
+ opacity: 1;
190
+ }
191
+ }
192
+
193
+ .cw-voice-settings-header {
194
+ display: flex;
195
+ justify-content: space-between;
196
+ align-items: center;
197
+ padding: 8px 16px;
198
+ font-size: 13px;
199
+ font-weight: 600;
200
+ color: var(--cw-text);
201
+ border-bottom: 1px solid var(--cw-border);
202
+ }
203
+
204
+ .cw-voice-settings-close {
205
+ all: initial;
206
+ font-family: inherit;
207
+ background: none;
208
+ border: none;
209
+ color: var(--cw-text-muted);
210
+ cursor: pointer;
211
+ font-size: 16px;
212
+ padding: 4px;
213
+ line-height: 1;
214
+ transition: color 0.15s;
215
+ }
216
+
217
+ .cw-voice-settings-close:hover {
218
+ color: var(--cw-text);
219
+ }
220
+
221
+ .cw-voice-settings-content {
222
+ padding: 12px 16px;
223
+ display: flex;
224
+ flex-direction: column;
225
+ gap: 12px;
226
+ }
227
+
228
+ .cw-voice-setting {
229
+ display: flex;
230
+ flex-direction: column;
231
+ gap: 4px;
232
+ }
233
+
234
+ .cw-voice-setting label {
235
+ font-size: 12px;
236
+ font-weight: 500;
237
+ color: var(--cw-text-muted);
238
+ }
239
+
240
+ .cw-voice-select {
241
+ all: initial;
242
+ font-family: inherit;
243
+ width: 100%;
244
+ padding: 8px 12px;
245
+ border: 1px solid var(--cw-border);
246
+ border-radius: 6px;
247
+ background: var(--cw-bg);
248
+ color: var(--cw-text);
249
+ font-size: 13px;
250
+ cursor: pointer;
251
+ transition: border-color 0.15s;
252
+ }
253
+
254
+ .cw-voice-select:hover {
255
+ border-color: var(--cw-primary);
256
+ }
257
+
258
+ .cw-voice-select:focus {
259
+ outline: none;
260
+ border-color: var(--cw-primary);
261
+ box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
262
+ }
263
+
175
264
  /* Status bar */
176
265
  .cw-status-bar {
177
266
  display: flex;
@@ -42,6 +42,8 @@
42
42
  runs: '/api/agent-runtime/runs/',
43
43
  runEvents: '/api/agent-runtime/runs/{runId}/events/',
44
44
  simulateCustomer: '/api/agent-runtime/simulate-customer/',
45
+ ttsVoices: '/api/tts/voices/', // For fetching available voices (proxy mode)
46
+ ttsSetVoice: '/api/tts/set-voice/', // For setting voice (proxy mode)
45
47
  },
46
48
  // Demo flow control
47
49
  autoRunDelay: 1000, // Delay in ms before auto-generating next message
@@ -61,6 +63,13 @@
61
63
  style: 0.0,
62
64
  use_speaker_boost: true,
63
65
  },
66
+ availableVoices: [], // List of available voices for UI dropdown
67
+ // UI visibility controls
68
+ showClearButton: true,
69
+ showDebugButton: true,
70
+ showTTSButton: true,
71
+ showVoiceSettings: true,
72
+ showExpandButton: true,
64
73
  };
65
74
 
66
75
  // State
@@ -82,6 +91,7 @@
82
91
  currentAudio: null,
83
92
  isSpeaking: false,
84
93
  speechQueue: [],
94
+ voiceSettingsOpen: false,
85
95
  };
86
96
 
87
97
  // DOM elements
@@ -285,6 +295,79 @@
285
295
  render();
286
296
  }
287
297
 
298
+ function toggleVoiceSettings() {
299
+ state.voiceSettingsOpen = !state.voiceSettingsOpen;
300
+ render();
301
+ }
302
+
303
+ async function setVoice(role, voiceId) {
304
+ config.ttsVoices[role] = voiceId;
305
+
306
+ // If using proxy, notify backend of voice change
307
+ if (config.ttsProxyUrl) {
308
+ try {
309
+ const token = await getOrCreateSession();
310
+ const headers = {
311
+ 'Content-Type': 'application/json',
312
+ };
313
+ if (token) {
314
+ headers[config.anonymousTokenHeader] = token;
315
+ }
316
+
317
+ await fetch(`${config.backendUrl}${config.apiPaths.ttsSetVoice}`, {
318
+ method: 'POST',
319
+ headers,
320
+ body: JSON.stringify({ role, voice_id: voiceId }),
321
+ });
322
+ } catch (err) {
323
+ console.error('[ChatWidget] Failed to set voice on backend:', err);
324
+ }
325
+ }
326
+
327
+ render();
328
+ }
329
+
330
+ async function fetchAvailableVoices() {
331
+ try {
332
+ let voices = [];
333
+
334
+ if (config.ttsProxyUrl) {
335
+ // Fetch voices from Django backend
336
+ const token = await getOrCreateSession();
337
+ const headers = {};
338
+ if (token) {
339
+ headers[config.anonymousTokenHeader] = token;
340
+ }
341
+
342
+ const response = await fetch(`${config.backendUrl}${config.apiPaths.ttsVoices}`, {
343
+ headers,
344
+ });
345
+
346
+ if (response.ok) {
347
+ const data = await response.json();
348
+ voices = data.voices || [];
349
+ }
350
+ } else if (config.elevenLabsApiKey) {
351
+ // Fetch voices directly from ElevenLabs
352
+ const response = await fetch('https://api.elevenlabs.io/v1/voices', {
353
+ headers: {
354
+ 'xi-api-key': config.elevenLabsApiKey,
355
+ },
356
+ });
357
+
358
+ if (response.ok) {
359
+ const data = await response.json();
360
+ voices = data.voices || [];
361
+ }
362
+ }
363
+
364
+ config.availableVoices = voices;
365
+ render(); // Re-render to update dropdowns
366
+ } catch (err) {
367
+ console.error('[ChatWidget] Failed to fetch voices:', err);
368
+ }
369
+ }
370
+
288
371
  // ============================================================================
289
372
  // Session Management
290
373
  // ============================================================================
@@ -694,6 +777,44 @@
694
777
  `;
695
778
  }
696
779
 
780
+ function renderVoiceSettings() {
781
+ if (!state.voiceSettingsOpen) return '';
782
+
783
+ const voiceOptions = (role) => {
784
+ if (config.availableVoices.length === 0) {
785
+ return '<option value="">Loading voices...</option>';
786
+ }
787
+ return config.availableVoices.map(voice => `
788
+ <option value="${voice.voice_id}" ${config.ttsVoices[role] === voice.voice_id ? 'selected' : ''}>
789
+ ${escapeHtml(voice.name)}
790
+ </option>
791
+ `).join('');
792
+ };
793
+
794
+ return `
795
+ <div class="cw-voice-settings">
796
+ <div class="cw-voice-settings-header">
797
+ <span>🎙️ Voice Settings</span>
798
+ <button class="cw-voice-settings-close" data-action="toggle-voice-settings">✕</button>
799
+ </div>
800
+ <div class="cw-voice-settings-content">
801
+ <div class="cw-voice-setting">
802
+ <label>Assistant Voice</label>
803
+ <select class="cw-voice-select" data-role="assistant" onchange="ChatWidget.setVoice('assistant', this.value)">
804
+ ${voiceOptions('assistant')}
805
+ </select>
806
+ </div>
807
+ <div class="cw-voice-setting">
808
+ <label>Customer Voice (Demo)</label>
809
+ <select class="cw-voice-select" data-role="user" onchange="ChatWidget.setVoice('user', this.value)">
810
+ ${voiceOptions('user')}
811
+ </select>
812
+ </div>
813
+ </div>
814
+ </div>
815
+ `;
816
+ }
817
+
697
818
  function renderJourneyDropdown() {
698
819
  if (!config.enableAutoRun || Object.keys(config.journeyTypes).length === 0) {
699
820
  return '';
@@ -824,13 +945,15 @@
824
945
  <div class="cw-header" style="background-color: ${config.primaryColor}">
825
946
  <span class="cw-title">${escapeHtml(config.title)}</span>
826
947
  <div class="cw-header-actions">
827
- <button class="cw-header-btn" data-action="clear" title="Clear Conversation" ${state.isLoading || state.messages.length === 0 ? 'disabled' : ''}>
828
- <svg class="cw-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
829
- <polyline points="3 6 5 6 21 6"></polyline>
830
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
831
- </svg>
832
- </button>
833
- ${config.enableDebugMode ? `
948
+ ${config.showClearButton ? `
949
+ <button class="cw-header-btn" data-action="clear" title="Clear Conversation" ${state.isLoading || state.messages.length === 0 ? 'disabled' : ''}>
950
+ <svg class="cw-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
951
+ <polyline points="3 6 5 6 21 6"></polyline>
952
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
953
+ </svg>
954
+ </button>
955
+ ` : ''}
956
+ ${config.showDebugButton && config.enableDebugMode ? `
834
957
  <button class="cw-header-btn ${state.debugMode ? 'cw-btn-active' : ''}" data-action="toggle-debug" title="${state.debugMode ? 'Hide Debug Info' : 'Show Debug Info'}">
835
958
  <svg class="cw-icon-sm ${state.debugMode ? 'cw-icon-warning' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
836
959
  <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
@@ -838,22 +961,30 @@
838
961
  </svg>
839
962
  </button>
840
963
  ` : ''}
841
- ${config.elevenLabsApiKey ? `
964
+ ${config.showTTSButton && (config.elevenLabsApiKey || config.ttsProxyUrl) ? `
842
965
  <button class="cw-header-btn ${config.enableTTS ? 'cw-btn-active' : ''} ${state.isSpeaking ? 'cw-btn-speaking' : ''}"
843
966
  data-action="toggle-tts"
844
967
  title="${config.enableTTS ? (state.isSpeaking ? 'Speaking...' : 'TTS Enabled') : 'TTS Disabled'}">
845
968
  ${state.isSpeaking ? '🔊' : (config.enableTTS ? '🔉' : '🔇')}
846
969
  </button>
847
970
  ` : ''}
971
+ ${config.showVoiceSettings && (config.elevenLabsApiKey || config.ttsProxyUrl) ? `
972
+ <button class="cw-header-btn ${state.voiceSettingsOpen ? 'cw-btn-active' : ''}" data-action="toggle-voice-settings" title="Voice Settings">
973
+ 🎙️
974
+ </button>
975
+ ` : ''}
848
976
  ${renderJourneyDropdown()}
849
- <button class="cw-header-btn" data-action="toggle-expand" title="${state.isExpanded ? 'Minimize' : 'Expand'}">
850
- ${state.isExpanded ? '' : ''}
851
- </button>
977
+ ${config.showExpandButton ? `
978
+ <button class="cw-header-btn" data-action="toggle-expand" title="${state.isExpanded ? 'Minimize' : 'Expand'}">
979
+ ${state.isExpanded ? '⊖' : '⊕'}
980
+ </button>
981
+ ` : ''}
852
982
  <button class="cw-header-btn" data-action="close" title="Close">
853
983
 
854
984
  </button>
855
985
  </div>
856
986
  </div>
987
+ ${renderVoiceSettings()}
857
988
  ${statusBar}
858
989
  <div class="cw-messages" id="cw-messages">
859
990
  ${messagesHtml}
@@ -893,6 +1024,7 @@
893
1024
  case 'toggle-expand': toggleExpand(); break;
894
1025
  case 'toggle-debug': toggleDebugMode(); break;
895
1026
  case 'toggle-tts': toggleTTS(); break;
1027
+ case 'toggle-voice-settings': toggleVoiceSettings(); break;
896
1028
  case 'clear': clearMessages(); break;
897
1029
  case 'stop-autorun': stopAutoRun(); break;
898
1030
  case 'continue-autorun': continueAutoRun(); break;
@@ -969,6 +1101,11 @@
969
1101
  // Initial render
970
1102
  render();
971
1103
 
1104
+ // Fetch available voices if TTS is configured
1105
+ if (config.elevenLabsApiKey || config.ttsProxyUrl) {
1106
+ fetchAvailableVoices();
1107
+ }
1108
+
972
1109
  console.log('[ChatWidget] Initialized with config:', config);
973
1110
  }
974
1111
 
@@ -1012,6 +1149,7 @@
1012
1149
  setAutoRunDelay,
1013
1150
  toggleTTS,
1014
1151
  stopSpeech,
1152
+ setVoice,
1015
1153
  getState: () => ({ ...state }),
1016
1154
  getConfig: () => ({ ...config }),
1017
1155
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "1.4.0",
3
+ "version": "1.6.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": [