@makemore/agent-frontend 1.6.1 → 1.8.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
@@ -174,6 +174,159 @@ See `django-tts-example.py` for the complete Django backend implementation.
174
174
  | `showTTSButton` | boolean | `true` | Show TTS toggle button in header |
175
175
  | `showVoiceSettings` | boolean | `true` | Show voice settings button in header (works with proxy and direct API) |
176
176
  | `showExpandButton` | boolean | `true` | Show expand/minimize button in header |
177
+ | `onEvent` | function | `null` | Callback for SSE events: `(eventType, payload) => void` |
178
+ | `authStrategy` | string | `null` | Auth strategy: `'token'`, `'jwt'`, `'session'`, `'anonymous'`, `'none'` (auto-detected if null) |
179
+ | `authToken` | string | `null` | Token value for `'token'` or `'jwt'` strategies |
180
+ | `authHeader` | string | `null` | Custom header name (defaults based on strategy) |
181
+ | `authTokenPrefix` | string | `null` | Custom token prefix (defaults based on strategy) |
182
+ | `anonymousSessionEndpoint` | string | `null` | Endpoint for anonymous session (defaults to `apiPaths.anonymousSession`) |
183
+ | `anonymousTokenKey` | string | `'chat_widget_anonymous_token'` | localStorage key for anonymous token |
184
+ | `onAuthError` | function | `null` | Callback for auth errors: `(error) => void` |
185
+
186
+ ### Authentication
187
+
188
+ The widget supports multiple authentication strategies with sensible defaults:
189
+
190
+ #### Token Authentication (Django REST Framework)
191
+
192
+ ```javascript
193
+ ChatWidget.init({
194
+ backendUrl: 'https://api.example.com',
195
+ agentKey: 'my-agent',
196
+ authStrategy: 'token',
197
+ authToken: 'abc123...',
198
+ // Sends: Authorization: Token abc123...
199
+ });
200
+ ```
201
+
202
+ #### JWT/Bearer Authentication
203
+
204
+ ```javascript
205
+ ChatWidget.init({
206
+ backendUrl: 'https://api.example.com',
207
+ agentKey: 'my-agent',
208
+ authStrategy: 'jwt',
209
+ authToken: 'eyJ...',
210
+ // Sends: Authorization: Bearer eyJ...
211
+ });
212
+ ```
213
+
214
+ #### Session-Based Authentication (Cookies)
215
+
216
+ ```javascript
217
+ ChatWidget.init({
218
+ backendUrl: 'https://api.example.com',
219
+ agentKey: 'my-agent',
220
+ authStrategy: 'session',
221
+ // Sends requests with credentials: 'include'
222
+ // No auth header, relies on session cookie
223
+ });
224
+ ```
225
+
226
+ #### Anonymous Session Tokens
227
+
228
+ ```javascript
229
+ ChatWidget.init({
230
+ backendUrl: 'https://api.example.com',
231
+ agentKey: 'my-agent',
232
+ authStrategy: 'anonymous',
233
+ anonymousSessionEndpoint: '/api/accounts/anonymous-session/',
234
+ // On first request: fetches anonymous token from endpoint
235
+ // Persists token to localStorage
236
+ // Sends: X-Anonymous-Token: {token}
237
+ });
238
+ ```
239
+
240
+ #### No Authentication (Public Endpoints)
241
+
242
+ ```javascript
243
+ ChatWidget.init({
244
+ backendUrl: 'https://api.example.com',
245
+ agentKey: 'my-agent',
246
+ authStrategy: 'none',
247
+ // No auth headers sent
248
+ });
249
+ ```
250
+
251
+ #### Custom Headers and Prefixes
252
+
253
+ ```javascript
254
+ ChatWidget.init({
255
+ authStrategy: 'token',
256
+ authToken: 'mytoken123',
257
+ authHeader: 'X-API-Key', // Custom header name
258
+ authTokenPrefix: '', // No prefix (just the token)
259
+ // Sends: X-API-Key: mytoken123
260
+ });
261
+ ```
262
+
263
+ #### Dynamic Token Updates
264
+
265
+ Update authentication after initialization (e.g., after user login):
266
+
267
+ ```javascript
268
+ // After user logs in
269
+ ChatWidget.setAuth({
270
+ strategy: 'jwt',
271
+ token: 'new-jwt-token-after-login'
272
+ });
273
+
274
+ // After user logs out
275
+ ChatWidget.clearAuth();
276
+
277
+ // Handle token refresh on auth errors
278
+ ChatWidget.init({
279
+ authStrategy: 'jwt',
280
+ authToken: initialToken,
281
+ onAuthError: async (error) => {
282
+ if (error.status === 401) {
283
+ const newToken = await refreshToken();
284
+ ChatWidget.setAuth({ token: newToken });
285
+ }
286
+ }
287
+ });
288
+ ```
289
+
290
+ #### Auto-Detection
291
+
292
+ If no `authStrategy` is specified, the widget auto-detects based on config:
293
+ - If `authToken` is provided → uses `'token'` strategy
294
+ - If `anonymousSessionEndpoint` or `apiPaths.anonymousSession` is configured → uses `'anonymous'` strategy
295
+ - Otherwise → uses `'none'`
296
+
297
+ ### Event Callback
298
+
299
+ The `onEvent` callback allows your application to react to all SSE events from the agent:
300
+
301
+ ```javascript
302
+ ChatWidget.init({
303
+ backendUrl: 'http://localhost:8000',
304
+ agentKey: 'your-agent',
305
+ onEvent: (eventType, payload) => {
306
+ console.log('Event:', eventType, payload);
307
+
308
+ // Example: Navigate when a session is created
309
+ if (eventType === 'tool.result' && payload.result?.session_id) {
310
+ window.location.href = `/session/${payload.result.session_id}`;
311
+ }
312
+
313
+ // Example: Track tool usage
314
+ if (eventType === 'tool.call') {
315
+ analytics.track('Tool Called', { tool: payload.name });
316
+ }
317
+ },
318
+ });
319
+ ```
320
+
321
+ **Event Types:**
322
+ - `assistant.message` - Streaming assistant responses (payload: `{ content: string }`)
323
+ - `tool.call` - Tool being called (payload: `{ name: string, arguments: object }`)
324
+ - `tool.result` - Tool result (payload: `{ result: any }`)
325
+ - `run.succeeded` - Run completed successfully
326
+ - `run.failed` - Run failed (payload: `{ error: string }`)
327
+ - `run.cancelled` - Run was cancelled
328
+ - `run.timed_out` - Run timed out
329
+ - Custom events emitted by your agent
177
330
 
178
331
  ### Text-to-Speech (ElevenLabs)
179
332
 
@@ -442,6 +595,10 @@ ChatWidget.setAutoRunMode('automatic'); // 'automatic', 'confirm', or 'manual'
442
595
  // Change auto-run delay (in milliseconds)
443
596
  ChatWidget.setAutoRunDelay(2000);
444
597
 
598
+ // Authentication methods
599
+ ChatWidget.setAuth({ strategy: 'jwt', token: 'new-token' }); // Update auth
600
+ ChatWidget.clearAuth(); // Clear authentication
601
+
445
602
  // Remove the widget from the page
446
603
  ChatWidget.destroy();
447
604
 
@@ -33,9 +33,18 @@
33
33
  placeholder: 'Type your message...',
34
34
  emptyStateTitle: 'Start a Conversation',
35
35
  emptyStateMessage: 'Send a message to get started.',
36
+ // Authentication configuration
37
+ authStrategy: null, // 'token' | 'jwt' | 'session' | 'anonymous' | 'none' (auto-detected if null)
38
+ authToken: null, // Token value for 'token' or 'jwt' strategies
39
+ authHeader: null, // Custom header name (defaults based on strategy)
40
+ authTokenPrefix: null, // Custom token prefix (defaults based on strategy)
41
+ anonymousSessionEndpoint: null, // Endpoint for anonymous session (defaults to apiPaths.anonymousSession)
42
+ anonymousTokenKey: 'chat_widget_anonymous_token', // Storage key for anonymous token
43
+ onAuthError: null, // Callback for auth errors: (error) => void
44
+ // Legacy config (deprecated but still supported)
36
45
  anonymousTokenHeader: 'X-Anonymous-Token',
37
46
  conversationIdKey: 'chat_widget_conversation_id',
38
- sessionTokenKey: 'chat_widget_session_token',
47
+ sessionTokenKey: 'chat_widget_session_token', // Deprecated: use anonymousTokenKey
39
48
  // API endpoint paths (can be customized for different backend setups)
40
49
  apiPaths: {
41
50
  anonymousSession: '/api/accounts/anonymous-session/',
@@ -70,6 +79,8 @@
70
79
  showTTSButton: true,
71
80
  showVoiceSettings: true,
72
81
  showExpandButton: true,
82
+ // Event callback
83
+ onEvent: null, // Callback for SSE events: (eventType, payload) => void
73
84
  };
74
85
 
75
86
  // State
@@ -85,7 +96,8 @@
85
96
  journeyType: 'general',
86
97
  messages: [],
87
98
  conversationId: null,
88
- sessionToken: null,
99
+ sessionToken: null, // Deprecated: use authToken
100
+ authToken: null, // Current auth token (for token/jwt/anonymous strategies)
89
101
  error: null,
90
102
  eventSource: null,
91
103
  currentAudio: null,
@@ -217,17 +229,16 @@
217
229
 
218
230
  if (config.ttsProxyUrl) {
219
231
  // Use Django proxy
220
- response = await fetch(config.ttsProxyUrl, {
232
+ response = await fetch(config.ttsProxyUrl, getFetchOptions({
221
233
  method: 'POST',
222
234
  headers: {
223
235
  'Content-Type': 'application/json',
224
- ...(state.sessionToken ? { [config.anonymousTokenHeader]: state.sessionToken } : {}),
225
236
  },
226
237
  body: JSON.stringify({
227
238
  text: text,
228
239
  role: role,
229
240
  }),
230
- });
241
+ }));
231
242
  } else {
232
243
  // Direct ElevenLabs API call
233
244
  const voiceId = role === 'assistant' ? config.ttsVoices.assistant : config.ttsVoices.user;
@@ -306,19 +317,15 @@
306
317
  // If using proxy, notify backend of voice change
307
318
  if (config.ttsProxyUrl) {
308
319
  try {
309
- const token = await getOrCreateSession();
310
- const headers = {
311
- 'Content-Type': 'application/json',
312
- };
313
- if (token) {
314
- headers[config.anonymousTokenHeader] = token;
315
- }
320
+ await getOrCreateSession();
316
321
 
317
- await fetch(`${config.backendUrl}${config.apiPaths.ttsSetVoice}`, {
322
+ await fetch(`${config.backendUrl}${config.apiPaths.ttsSetVoice}`, getFetchOptions({
318
323
  method: 'POST',
319
- headers,
324
+ headers: {
325
+ 'Content-Type': 'application/json',
326
+ },
320
327
  body: JSON.stringify({ role, voice_id: voiceId }),
321
- });
328
+ }));
322
329
  } catch (err) {
323
330
  console.error('[ChatWidget] Failed to set voice on backend:', err);
324
331
  }
@@ -333,15 +340,9 @@
333
340
 
334
341
  if (config.ttsProxyUrl) {
335
342
  // Fetch voices from Django backend
336
- const token = await getOrCreateSession();
337
- const headers = {};
338
- if (token) {
339
- headers[config.anonymousTokenHeader] = token;
340
- }
343
+ await getOrCreateSession();
341
344
 
342
- const response = await fetch(`${config.backendUrl}${config.apiPaths.ttsVoices}`, {
343
- headers,
344
- });
345
+ const response = await fetch(`${config.backendUrl}${config.apiPaths.ttsVoices}`, getFetchOptions());
345
346
 
346
347
  if (response.ok) {
347
348
  const data = await response.json();
@@ -368,34 +369,162 @@
368
369
  }
369
370
  }
370
371
 
372
+ // ============================================================================
373
+ // Authentication
374
+ // ============================================================================
375
+
376
+ /**
377
+ * Determine the effective auth strategy based on config
378
+ */
379
+ function getAuthStrategy() {
380
+ if (config.authStrategy) {
381
+ return config.authStrategy;
382
+ }
383
+
384
+ // Auto-detect strategy based on config
385
+ if (config.authToken) {
386
+ return 'token'; // Default to token auth if token provided
387
+ }
388
+
389
+ // Check for legacy anonymous session config
390
+ if (config.apiPaths.anonymousSession || config.anonymousSessionEndpoint) {
391
+ return 'anonymous';
392
+ }
393
+
394
+ return 'none';
395
+ }
396
+
397
+ /**
398
+ * Get auth headers based on current strategy
399
+ */
400
+ function getAuthHeaders() {
401
+ const strategy = getAuthStrategy();
402
+ const headers = {};
403
+
404
+ switch (strategy) {
405
+ case 'token': {
406
+ const token = config.authToken || state.authToken;
407
+ if (token) {
408
+ const headerName = config.authHeader || 'Authorization';
409
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Token';
410
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
411
+ }
412
+ break;
413
+ }
414
+
415
+ case 'jwt': {
416
+ const token = config.authToken || state.authToken;
417
+ if (token) {
418
+ const headerName = config.authHeader || 'Authorization';
419
+ const prefix = config.authTokenPrefix !== undefined ? config.authTokenPrefix : 'Bearer';
420
+ headers[headerName] = prefix ? `${prefix} ${token}` : token;
421
+ }
422
+ break;
423
+ }
424
+
425
+ case 'anonymous': {
426
+ const token = state.authToken || state.sessionToken; // Support legacy sessionToken
427
+ if (token) {
428
+ const headerName = config.authHeader || config.anonymousTokenHeader || 'X-Anonymous-Token';
429
+ headers[headerName] = token;
430
+ }
431
+ break;
432
+ }
433
+
434
+ case 'session':
435
+ // Session auth uses cookies, no headers needed
436
+ break;
437
+
438
+ case 'none':
439
+ // No auth
440
+ break;
441
+ }
442
+
443
+ return headers;
444
+ }
445
+
446
+ /**
447
+ * Get fetch options including auth credentials
448
+ */
449
+ function getFetchOptions(options = {}) {
450
+ const strategy = getAuthStrategy();
451
+ const fetchOptions = { ...options };
452
+
453
+ // Add auth headers
454
+ fetchOptions.headers = {
455
+ ...fetchOptions.headers,
456
+ ...getAuthHeaders(),
457
+ };
458
+
459
+ // For session auth, include credentials
460
+ if (strategy === 'session') {
461
+ fetchOptions.credentials = 'include';
462
+ }
463
+
464
+ return fetchOptions;
465
+ }
466
+
467
+ /**
468
+ * Handle auth errors (401, 403)
469
+ */
470
+ function handleAuthError(error, response) {
471
+ if (config.onAuthError && typeof config.onAuthError === 'function') {
472
+ const authError = new Error(error.message || 'Authentication failed');
473
+ authError.status = response?.status;
474
+ authError.response = response;
475
+ config.onAuthError(authError);
476
+ }
477
+ }
478
+
371
479
  // ============================================================================
372
480
  // Session Management
373
481
  // ============================================================================
374
482
 
375
483
  async function getOrCreateSession() {
484
+ const strategy = getAuthStrategy();
485
+
486
+ // For non-anonymous strategies, return the configured token
487
+ if (strategy !== 'anonymous') {
488
+ return config.authToken || state.authToken;
489
+ }
490
+
491
+ // Anonymous strategy: get or create anonymous token
492
+ if (state.authToken) {
493
+ return state.authToken;
494
+ }
495
+
496
+ // Support legacy sessionToken
376
497
  if (state.sessionToken) {
377
- return state.sessionToken;
498
+ state.authToken = state.sessionToken;
499
+ return state.authToken;
378
500
  }
379
501
 
380
502
  // Try to restore from storage
381
- const stored = getStoredValue(config.sessionTokenKey);
503
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
504
+ const stored = getStoredValue(storageKey);
382
505
  if (stored) {
383
- state.sessionToken = stored;
506
+ state.authToken = stored;
507
+ state.sessionToken = stored; // Keep legacy field in sync
384
508
  return stored;
385
509
  }
386
510
 
387
511
  // Create new anonymous session
388
512
  try {
389
- const response = await fetch(`${config.backendUrl}${config.apiPaths.anonymousSession}`, {
513
+ const endpoint = config.anonymousSessionEndpoint || config.apiPaths.anonymousSession;
514
+ const response = await fetch(`${config.backendUrl}${endpoint}`, {
390
515
  method: 'POST',
391
516
  headers: { 'Content-Type': 'application/json' },
392
517
  });
393
518
 
394
519
  if (response.ok) {
395
520
  const data = await response.json();
396
- state.sessionToken = data.token;
397
- setStoredValue(config.sessionTokenKey, data.token);
398
- return data.token;
521
+ const token = data.token;
522
+ state.authToken = token;
523
+ state.sessionToken = token; // Keep legacy field in sync
524
+ setStoredValue(storageKey, token);
525
+ return token;
526
+ } else if (response.status === 401 || response.status === 403) {
527
+ handleAuthError(new Error('Failed to create anonymous session'), response);
399
528
  }
400
529
  } catch (e) {
401
530
  console.warn('[ChatWidget] Failed to create session:', e);
@@ -426,31 +555,35 @@
426
555
  render();
427
556
 
428
557
  try {
558
+ // Get auth token (if using anonymous strategy)
429
559
  const token = await getOrCreateSession();
430
- const headers = { 'Content-Type': 'application/json' };
431
- if (token) {
432
- headers[config.anonymousTokenHeader] = token;
433
- }
434
560
 
435
561
  // Restore conversation ID from storage if not set
436
562
  if (!state.conversationId) {
437
563
  state.conversationId = getStoredValue(config.conversationIdKey);
438
564
  }
439
565
 
440
- const response = await fetch(`${config.backendUrl}${config.apiPaths.runs}`, {
566
+ const response = await fetch(`${config.backendUrl}${config.apiPaths.runs}`, getFetchOptions({
441
567
  method: 'POST',
442
- headers,
568
+ headers: { 'Content-Type': 'application/json' },
443
569
  body: JSON.stringify({
444
570
  agentKey: config.agentKey,
445
571
  conversationId: state.conversationId,
446
572
  messages: [{ role: 'user', content: content.trim() }],
447
573
  metadata: { journey_type: state.journeyType },
448
574
  }),
449
- });
575
+ }));
450
576
 
451
577
  if (!response.ok) {
452
578
  const errorData = await response.json().catch(() => ({}));
453
- throw new Error(errorData.error || `HTTP ${response.status}`);
579
+ const error = new Error(errorData.error || `HTTP ${response.status}`);
580
+
581
+ // Handle auth errors
582
+ if (response.status === 401 || response.status === 403) {
583
+ handleAuthError(error, response);
584
+ }
585
+
586
+ throw error;
454
587
  }
455
588
 
456
589
  const run = await response.json();
@@ -492,6 +625,12 @@
492
625
  eventSource.addEventListener('assistant.message', (event) => {
493
626
  try {
494
627
  const data = JSON.parse(event.data);
628
+
629
+ // Call onEvent callback if provided
630
+ if (config.onEvent && typeof config.onEvent === 'function') {
631
+ config.onEvent('assistant.message', data.payload);
632
+ }
633
+
495
634
  const content = data.payload.content;
496
635
  if (content) {
497
636
  assistantContent += content;
@@ -518,18 +657,25 @@
518
657
 
519
658
  // Handler for tool calls (debug mode)
520
659
  eventSource.addEventListener('tool.call', (event) => {
521
- if (!state.debugMode) return;
522
660
  try {
523
661
  const data = JSON.parse(event.data);
524
- state.messages.push({
525
- id: 'tool-call-' + Date.now(),
526
- role: 'system',
527
- content: `🔧 Tool: ${data.payload.name}`,
528
- timestamp: new Date(),
529
- type: 'tool_call',
530
- metadata: { name: data.payload.name, arguments: data.payload.arguments },
531
- });
532
- render();
662
+
663
+ // Call onEvent callback if provided
664
+ if (config.onEvent && typeof config.onEvent === 'function') {
665
+ config.onEvent('tool.call', data.payload);
666
+ }
667
+
668
+ if (state.debugMode) {
669
+ state.messages.push({
670
+ id: 'tool-call-' + Date.now(),
671
+ role: 'system',
672
+ content: `🔧 Tool: ${data.payload.name}`,
673
+ timestamp: new Date(),
674
+ type: 'tool_call',
675
+ metadata: { name: data.payload.name, arguments: data.payload.arguments },
676
+ });
677
+ render();
678
+ }
533
679
  } catch (err) {
534
680
  console.error('[ChatWidget] Failed to parse tool.call:', err);
535
681
  }
@@ -537,19 +683,26 @@
537
683
 
538
684
  // Handler for tool results (debug mode)
539
685
  eventSource.addEventListener('tool.result', (event) => {
540
- if (!state.debugMode) return;
541
686
  try {
542
687
  const data = JSON.parse(event.data);
543
- const result = data.payload.result || '';
544
- state.messages.push({
545
- id: 'tool-result-' + Date.now(),
546
- role: 'system',
547
- content: `✅ Result: ${result.substring(0, 100)}${result.length > 100 ? '...' : ''}`,
548
- timestamp: new Date(),
549
- type: 'tool_result',
550
- metadata: { result },
551
- });
552
- render();
688
+
689
+ // Call onEvent callback if provided
690
+ if (config.onEvent && typeof config.onEvent === 'function') {
691
+ config.onEvent('tool.result', data.payload);
692
+ }
693
+
694
+ if (state.debugMode) {
695
+ const result = data.payload.result || '';
696
+ state.messages.push({
697
+ id: 'tool-result-' + Date.now(),
698
+ role: 'system',
699
+ content: `✅ Result: ${result.substring(0, 100)}${result.length > 100 ? '...' : ''}`,
700
+ timestamp: new Date(),
701
+ type: 'tool_result',
702
+ metadata: { result },
703
+ });
704
+ render();
705
+ }
553
706
  } catch (err) {
554
707
  console.error('[ChatWidget] Failed to parse tool.result:', err);
555
708
  }
@@ -559,6 +712,12 @@
559
712
  const handleTerminal = (event) => {
560
713
  try {
561
714
  const data = JSON.parse(event.data);
715
+
716
+ // Call onEvent callback if provided
717
+ if (config.onEvent && typeof config.onEvent === 'function') {
718
+ config.onEvent(data.type, data.payload);
719
+ }
720
+
562
721
  if (data.type === 'run.failed') {
563
722
  state.error = data.payload.error || 'Agent run failed';
564
723
  state.messages.push({
@@ -604,6 +763,22 @@
604
763
  eventSource.addEventListener('run.cancelled', handleTerminal);
605
764
  eventSource.addEventListener('run.timed_out', handleTerminal);
606
765
 
766
+ // Generic handler for any other custom events
767
+ eventSource.onmessage = (event) => {
768
+ try {
769
+ const data = JSON.parse(event.data);
770
+
771
+ // Call onEvent callback for any unhandled events
772
+ if (config.onEvent && typeof config.onEvent === 'function') {
773
+ // Extract event type from data or use 'message' as default
774
+ const eventType = data.type || 'message';
775
+ config.onEvent(eventType, data.payload || data);
776
+ }
777
+ } catch (err) {
778
+ console.debug('[ChatWidget] Received non-JSON SSE message:', event.data);
779
+ }
780
+ };
781
+
607
782
  eventSource.onerror = () => {
608
783
  if (eventSource.readyState !== EventSource.CLOSED) {
609
784
  console.debug('[ChatWidget] SSE connection closed');
@@ -1015,6 +1190,13 @@
1015
1190
  if (messagesEl) {
1016
1191
  messagesEl.scrollTop = messagesEl.scrollHeight;
1017
1192
  }
1193
+
1194
+ // Focus input field
1195
+ const inputEl = container.querySelector('.cw-input');
1196
+ if (inputEl && !state.isLoading) {
1197
+ // Use setTimeout to ensure focus happens after render completes
1198
+ setTimeout(() => inputEl.focus(), 0);
1199
+ }
1018
1200
  }
1019
1201
 
1020
1202
  function attachEventListeners() {
@@ -1064,6 +1246,8 @@
1064
1246
  if (input && input.value.trim()) {
1065
1247
  sendMessage(input.value);
1066
1248
  input.value = '';
1249
+ // Keep focus on input after sending
1250
+ input.focus();
1067
1251
  }
1068
1252
  });
1069
1253
  }
@@ -1095,6 +1279,11 @@
1095
1279
  };
1096
1280
  state.journeyType = config.defaultJourneyType;
1097
1281
 
1282
+ // Initialize auth token from config
1283
+ if (config.authToken) {
1284
+ state.authToken = config.authToken;
1285
+ }
1286
+
1098
1287
  // Restore conversation ID
1099
1288
  state.conversationId = getStoredValue(config.conversationIdKey);
1100
1289
 
@@ -1140,6 +1329,42 @@
1140
1329
  sendMessage(message);
1141
1330
  }
1142
1331
 
1332
+ /**
1333
+ * Update authentication configuration
1334
+ * @param {Object} authConfig - { strategy?: string, token?: string }
1335
+ */
1336
+ function setAuth(authConfig = {}) {
1337
+ if (authConfig.strategy) {
1338
+ config.authStrategy = authConfig.strategy;
1339
+ }
1340
+ if (authConfig.token !== undefined) {
1341
+ config.authToken = authConfig.token;
1342
+ state.authToken = authConfig.token;
1343
+
1344
+ // For anonymous strategy, also persist to storage
1345
+ if (getAuthStrategy() === 'anonymous' && authConfig.token) {
1346
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
1347
+ setStoredValue(storageKey, authConfig.token);
1348
+ }
1349
+ }
1350
+ console.log('[ChatWidget] Auth updated:', { strategy: getAuthStrategy(), hasToken: !!state.authToken });
1351
+ }
1352
+
1353
+ /**
1354
+ * Clear authentication
1355
+ */
1356
+ function clearAuth() {
1357
+ config.authToken = null;
1358
+ state.authToken = null;
1359
+ state.sessionToken = null;
1360
+
1361
+ // Clear from storage
1362
+ const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
1363
+ setStoredValue(storageKey, null);
1364
+
1365
+ console.log('[ChatWidget] Auth cleared');
1366
+ }
1367
+
1143
1368
  // Export public API
1144
1369
  global.ChatWidget = {
1145
1370
  init,
@@ -1156,6 +1381,8 @@
1156
1381
  toggleTTS,
1157
1382
  stopSpeech,
1158
1383
  setVoice,
1384
+ setAuth,
1385
+ clearAuth,
1159
1386
  getState: () => ({ ...state }),
1160
1387
  getConfig: () => ({ ...config }),
1161
1388
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makemore/agent-frontend",
3
- "version": "1.6.1",
3
+ "version": "1.8.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": [