@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 +157 -0
- package/dist/chat-widget.js +286 -59
- package/package.json +1 -1
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
|
|
package/dist/chat-widget.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
498
|
+
state.authToken = state.sessionToken;
|
|
499
|
+
return state.authToken;
|
|
378
500
|
}
|
|
379
501
|
|
|
380
502
|
// Try to restore from storage
|
|
381
|
-
const
|
|
503
|
+
const storageKey = config.anonymousTokenKey || config.sessionTokenKey;
|
|
504
|
+
const stored = getStoredValue(storageKey);
|
|
382
505
|
if (stored) {
|
|
383
|
-
state.
|
|
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
|
|
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
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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.
|
|
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": [
|