@makemore/agent-frontend 1.0.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.
@@ -0,0 +1,745 @@
1
+ /**
2
+ * Embeddable Chat Widget
3
+ * A standalone chat widget that can be embedded in any website.
4
+ *
5
+ * Usage:
6
+ * <script src="chat-widget.js"></script>
7
+ * <link rel="stylesheet" href="chat-widget.css">
8
+ * <script>
9
+ * ChatWidget.init({
10
+ * backendUrl: 'https://your-api.com',
11
+ * agentKey: 'your-agent',
12
+ * title: 'Support Chat',
13
+ * primaryColor: '#0066cc',
14
+ * });
15
+ * </script>
16
+ */
17
+ (function(global) {
18
+ 'use strict';
19
+
20
+ // Default configuration
21
+ const DEFAULT_CONFIG = {
22
+ backendUrl: 'http://localhost:8000',
23
+ agentKey: 'insurance-agent',
24
+ title: 'Chat Assistant',
25
+ subtitle: 'How can we help you today?',
26
+ primaryColor: '#0066cc',
27
+ position: 'bottom-right', // bottom-right, bottom-left
28
+ defaultJourneyType: 'general',
29
+ enableDebugMode: true,
30
+ enableAutoRun: true,
31
+ journeyTypes: {},
32
+ customerPrompts: {},
33
+ placeholder: 'Type your message...',
34
+ emptyStateTitle: 'Start a Conversation',
35
+ emptyStateMessage: 'Send a message to get started.',
36
+ anonymousTokenHeader: 'X-Anonymous-Token',
37
+ conversationIdKey: 'chat_widget_conversation_id',
38
+ sessionTokenKey: 'chat_widget_session_token',
39
+ };
40
+
41
+ // State
42
+ let config = { ...DEFAULT_CONFIG };
43
+ let state = {
44
+ isOpen: false,
45
+ isExpanded: false,
46
+ isLoading: false,
47
+ isSimulating: false,
48
+ autoRunMode: false,
49
+ debugMode: false,
50
+ journeyType: 'general',
51
+ messages: [],
52
+ conversationId: null,
53
+ sessionToken: null,
54
+ error: null,
55
+ eventSource: null,
56
+ };
57
+
58
+ // DOM elements
59
+ let container = null;
60
+ let widgetEl = null;
61
+ let fabEl = null;
62
+
63
+ // ============================================================================
64
+ // Utility Functions
65
+ // ============================================================================
66
+
67
+ function generateId() {
68
+ return 'msg-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
69
+ }
70
+
71
+ function escapeHtml(text) {
72
+ const div = document.createElement('div');
73
+ div.textContent = text;
74
+ return div.innerHTML;
75
+ }
76
+
77
+ function parseMarkdown(text) {
78
+ // Simple markdown parsing for common patterns
79
+ let html = escapeHtml(text);
80
+
81
+ // Bold: **text** or __text__
82
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
83
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
84
+
85
+ // Italic: *text* or _text_
86
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
87
+ html = html.replace(/_(.+?)_/g, '<em>$1</em>');
88
+
89
+ // Code: `code`
90
+ html = html.replace(/`(.+?)`/g, '<code>$1</code>');
91
+
92
+ // Links: [text](url)
93
+ html = html.replace(/\[(.+?)\]\((.+?)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>');
94
+
95
+ // Line breaks
96
+ html = html.replace(/\n/g, '<br>');
97
+
98
+ // Lists: - item or * item
99
+ html = html.replace(/^[\-\*]\s+(.+)$/gm, '<li>$1</li>');
100
+ html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
101
+
102
+ return html;
103
+ }
104
+
105
+ function getStoredValue(key) {
106
+ try {
107
+ return localStorage.getItem(key);
108
+ } catch (e) {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function setStoredValue(key, value) {
114
+ try {
115
+ if (value === null) {
116
+ localStorage.removeItem(key);
117
+ } else {
118
+ localStorage.setItem(key, value);
119
+ }
120
+ } catch (e) {
121
+ // Ignore storage errors
122
+ }
123
+ }
124
+
125
+ // ============================================================================
126
+ // Session Management
127
+ // ============================================================================
128
+
129
+ async function getOrCreateSession() {
130
+ if (state.sessionToken) {
131
+ return state.sessionToken;
132
+ }
133
+
134
+ // Try to restore from storage
135
+ const stored = getStoredValue(config.sessionTokenKey);
136
+ if (stored) {
137
+ state.sessionToken = stored;
138
+ return stored;
139
+ }
140
+
141
+ // Create new anonymous session
142
+ try {
143
+ const response = await fetch(`${config.backendUrl}/api/accounts/anonymous-session/`, {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ });
147
+
148
+ if (response.ok) {
149
+ const data = await response.json();
150
+ state.sessionToken = data.token;
151
+ setStoredValue(config.sessionTokenKey, data.token);
152
+ return data.token;
153
+ }
154
+ } catch (e) {
155
+ console.warn('[ChatWidget] Failed to create session:', e);
156
+ }
157
+
158
+ return null;
159
+ }
160
+
161
+ // ============================================================================
162
+ // API Functions
163
+ // ============================================================================
164
+
165
+ async function sendMessage(content) {
166
+ if (!content.trim() || state.isLoading) return;
167
+
168
+ state.isLoading = true;
169
+ state.error = null;
170
+
171
+ // Add user message immediately
172
+ const userMessage = {
173
+ id: generateId(),
174
+ role: 'user',
175
+ content: content.trim(),
176
+ timestamp: new Date(),
177
+ type: 'message',
178
+ };
179
+ state.messages.push(userMessage);
180
+ render();
181
+
182
+ try {
183
+ const token = await getOrCreateSession();
184
+ const headers = { 'Content-Type': 'application/json' };
185
+ if (token) {
186
+ headers[config.anonymousTokenHeader] = token;
187
+ }
188
+
189
+ // Restore conversation ID from storage if not set
190
+ if (!state.conversationId) {
191
+ state.conversationId = getStoredValue(config.conversationIdKey);
192
+ }
193
+
194
+ const response = await fetch(`${config.backendUrl}/api/agent-runtime/runs/`, {
195
+ method: 'POST',
196
+ headers,
197
+ body: JSON.stringify({
198
+ agentKey: config.agentKey,
199
+ conversationId: state.conversationId,
200
+ messages: [{ role: 'user', content: content.trim() }],
201
+ metadata: { journey_type: state.journeyType },
202
+ }),
203
+ });
204
+
205
+ if (!response.ok) {
206
+ const errorData = await response.json().catch(() => ({}));
207
+ throw new Error(errorData.error || `HTTP ${response.status}`);
208
+ }
209
+
210
+ const run = await response.json();
211
+
212
+ // Store conversation ID
213
+ if (!state.conversationId && run.conversationId) {
214
+ state.conversationId = run.conversationId;
215
+ setStoredValue(config.conversationIdKey, run.conversationId);
216
+ }
217
+
218
+ // Subscribe to SSE events
219
+ await subscribeToEvents(run.id, token);
220
+
221
+ } catch (err) {
222
+ state.error = err.message || 'Failed to send message';
223
+ state.isLoading = false;
224
+ render();
225
+ }
226
+ }
227
+
228
+ async function subscribeToEvents(runId, token) {
229
+ // Close existing connection
230
+ if (state.eventSource) {
231
+ state.eventSource.close();
232
+ }
233
+
234
+ let url = `${config.backendUrl}/api/agent-runtime/runs/${runId}/events/`;
235
+ if (token) {
236
+ url += `?anonymous_token=${encodeURIComponent(token)}`;
237
+ }
238
+
239
+ const eventSource = new EventSource(url);
240
+ state.eventSource = eventSource;
241
+
242
+ let assistantContent = '';
243
+
244
+ // Handler for assistant messages
245
+ eventSource.addEventListener('assistant.message', (event) => {
246
+ try {
247
+ const data = JSON.parse(event.data);
248
+ const content = data.payload.content;
249
+ if (content) {
250
+ assistantContent += content;
251
+
252
+ // Update or add assistant message
253
+ const lastMsg = state.messages[state.messages.length - 1];
254
+ if (lastMsg?.role === 'assistant' && lastMsg.id.startsWith('assistant-stream-')) {
255
+ lastMsg.content = assistantContent;
256
+ } else {
257
+ state.messages.push({
258
+ id: 'assistant-stream-' + Date.now(),
259
+ role: 'assistant',
260
+ content: assistantContent,
261
+ timestamp: new Date(),
262
+ type: 'message',
263
+ });
264
+ }
265
+ render();
266
+ }
267
+ } catch (err) {
268
+ console.error('[ChatWidget] Failed to parse assistant.message:', err);
269
+ }
270
+ });
271
+
272
+ // Handler for tool calls (debug mode)
273
+ eventSource.addEventListener('tool.call', (event) => {
274
+ if (!state.debugMode) return;
275
+ try {
276
+ const data = JSON.parse(event.data);
277
+ state.messages.push({
278
+ id: 'tool-call-' + Date.now(),
279
+ role: 'system',
280
+ content: `🔧 Tool: ${data.payload.name}`,
281
+ timestamp: new Date(),
282
+ type: 'tool_call',
283
+ metadata: { name: data.payload.name, arguments: data.payload.arguments },
284
+ });
285
+ render();
286
+ } catch (err) {
287
+ console.error('[ChatWidget] Failed to parse tool.call:', err);
288
+ }
289
+ });
290
+
291
+ // Handler for tool results (debug mode)
292
+ eventSource.addEventListener('tool.result', (event) => {
293
+ if (!state.debugMode) return;
294
+ try {
295
+ const data = JSON.parse(event.data);
296
+ const result = data.payload.result || '';
297
+ state.messages.push({
298
+ id: 'tool-result-' + Date.now(),
299
+ role: 'system',
300
+ content: `✅ Result: ${result.substring(0, 100)}${result.length > 100 ? '...' : ''}`,
301
+ timestamp: new Date(),
302
+ type: 'tool_result',
303
+ metadata: { result },
304
+ });
305
+ render();
306
+ } catch (err) {
307
+ console.error('[ChatWidget] Failed to parse tool.result:', err);
308
+ }
309
+ });
310
+
311
+ // Terminal event handlers
312
+ const handleTerminal = (event) => {
313
+ try {
314
+ const data = JSON.parse(event.data);
315
+ if (data.type === 'run.failed') {
316
+ state.error = data.payload.error || 'Agent run failed';
317
+ state.messages.push({
318
+ id: 'error-' + Date.now(),
319
+ role: 'system',
320
+ content: `❌ Error: ${state.error}`,
321
+ timestamp: new Date(),
322
+ type: 'error',
323
+ });
324
+ }
325
+ } catch (err) {
326
+ console.error('[ChatWidget] Failed to parse terminal event:', err);
327
+ }
328
+ state.isLoading = false;
329
+ eventSource.close();
330
+ state.eventSource = null;
331
+ render();
332
+
333
+ // Trigger auto-run if enabled
334
+ if (state.autoRunMode && !state.error) {
335
+ setTimeout(() => triggerAutoRun(), 1000);
336
+ }
337
+ };
338
+
339
+ eventSource.addEventListener('run.succeeded', handleTerminal);
340
+ eventSource.addEventListener('run.failed', handleTerminal);
341
+ eventSource.addEventListener('run.cancelled', handleTerminal);
342
+ eventSource.addEventListener('run.timed_out', handleTerminal);
343
+
344
+ eventSource.onerror = () => {
345
+ if (eventSource.readyState !== EventSource.CLOSED) {
346
+ console.debug('[ChatWidget] SSE connection closed');
347
+ }
348
+ state.isLoading = false;
349
+ eventSource.close();
350
+ state.eventSource = null;
351
+ render();
352
+ };
353
+ }
354
+
355
+ // ============================================================================
356
+ // Auto-Run / Demo Mode
357
+ // ============================================================================
358
+
359
+ async function triggerAutoRun() {
360
+ if (!state.autoRunMode || state.isLoading || state.isSimulating) return;
361
+
362
+ const lastMessage = state.messages[state.messages.length - 1];
363
+ if (lastMessage?.role !== 'assistant') return;
364
+
365
+ state.isSimulating = true;
366
+ render();
367
+
368
+ try {
369
+ const response = await fetch(`${config.backendUrl}/api/agent-runtime/simulate-customer/`, {
370
+ method: 'POST',
371
+ headers: { 'Content-Type': 'application/json' },
372
+ body: JSON.stringify({
373
+ messages: state.messages.map(m => ({ role: m.role, content: m.content })),
374
+ journey_type: state.journeyType,
375
+ }),
376
+ });
377
+
378
+ if (response.ok) {
379
+ const data = await response.json();
380
+ if (data.response) {
381
+ state.isSimulating = false;
382
+ await sendMessage(data.response);
383
+ return;
384
+ }
385
+ }
386
+ } catch (err) {
387
+ console.error('[ChatWidget] Failed to simulate customer:', err);
388
+ }
389
+
390
+ state.isSimulating = false;
391
+ render();
392
+ }
393
+
394
+ async function startDemoFlow(journeyType) {
395
+ clearMessages();
396
+ state.journeyType = journeyType;
397
+ state.autoRunMode = true;
398
+ render();
399
+
400
+ const journey = config.journeyTypes[journeyType];
401
+ if (journey?.initialMessage) {
402
+ setTimeout(() => {
403
+ state.isSimulating = true;
404
+ render();
405
+ sendMessage(journey.initialMessage).then(() => {
406
+ state.isSimulating = false;
407
+ render();
408
+ });
409
+ }, 100);
410
+ }
411
+ }
412
+
413
+ function stopAutoRun() {
414
+ state.autoRunMode = false;
415
+ render();
416
+ }
417
+
418
+ // ============================================================================
419
+ // UI Actions
420
+ // ============================================================================
421
+
422
+ function openWidget() {
423
+ state.isOpen = true;
424
+ getOrCreateSession();
425
+ render();
426
+ }
427
+
428
+ function closeWidget() {
429
+ state.isOpen = false;
430
+ state.autoRunMode = false;
431
+ render();
432
+ }
433
+
434
+ function toggleExpand() {
435
+ state.isExpanded = !state.isExpanded;
436
+ render();
437
+ }
438
+
439
+ function toggleDebugMode() {
440
+ state.debugMode = !state.debugMode;
441
+ render();
442
+ }
443
+
444
+ function clearMessages() {
445
+ state.messages = [];
446
+ state.conversationId = null;
447
+ state.error = null;
448
+ state.autoRunMode = false;
449
+ setStoredValue(config.conversationIdKey, null);
450
+ render();
451
+ }
452
+
453
+ // ============================================================================
454
+ // Render Functions
455
+ // ============================================================================
456
+
457
+ function renderMessage(msg) {
458
+ const isUser = msg.role === 'user';
459
+ const isToolCall = msg.type === 'tool_call';
460
+ const isToolResult = msg.type === 'tool_result';
461
+ const isError = msg.type === 'error';
462
+
463
+ // Hide debug messages if debug mode is off
464
+ if ((isToolCall || isToolResult) && !state.debugMode) {
465
+ return '';
466
+ }
467
+
468
+ let classes = 'cw-message';
469
+ if (isUser) classes += ' cw-message-user';
470
+ if (isToolCall) classes += ' cw-message-tool-call';
471
+ if (isToolResult) classes += ' cw-message-tool-result';
472
+ if (isError) classes += ' cw-message-error';
473
+
474
+ let content = msg.role === 'assistant' ? parseMarkdown(msg.content) : escapeHtml(msg.content);
475
+
476
+ // Add tool arguments for tool calls
477
+ if (isToolCall && msg.metadata?.arguments) {
478
+ content += `<pre class="cw-tool-args">${escapeHtml(JSON.stringify(msg.metadata.arguments, null, 2))}</pre>`;
479
+ }
480
+
481
+ return `
482
+ <div class="cw-message-row ${isUser ? 'cw-message-row-user' : ''}">
483
+ <div class="${classes}">${content}</div>
484
+ </div>
485
+ `;
486
+ }
487
+
488
+ function renderJourneyDropdown() {
489
+ if (!config.enableAutoRun || Object.keys(config.journeyTypes).length === 0) {
490
+ return '';
491
+ }
492
+
493
+ const journeyItems = Object.entries(config.journeyTypes).map(([key, journey]) => `
494
+ <button class="cw-dropdown-item" data-journey="${key}">
495
+ ${escapeHtml(journey.label)}
496
+ </button>
497
+ `).join('');
498
+
499
+ const stopButton = state.autoRunMode ? `
500
+ <div class="cw-dropdown-separator"></div>
501
+ <button class="cw-dropdown-item cw-dropdown-item-danger" data-action="stop-autorun">
502
+ ⏹️ Stop Auto-Run
503
+ </button>
504
+ ` : '';
505
+
506
+ return `
507
+ <div class="cw-dropdown">
508
+ <button class="cw-header-btn ${state.autoRunMode ? 'cw-btn-active' : ''}"
509
+ data-action="toggle-journey-dropdown"
510
+ title="Demo Flows"
511
+ ${state.isLoading || state.isSimulating ? 'disabled' : ''}>
512
+ ${state.isSimulating ? '<span class="cw-spinner"></span>' : '▶'}
513
+ </button>
514
+ <div class="cw-dropdown-menu cw-dropdown-hidden" id="cw-journey-dropdown">
515
+ <div class="cw-dropdown-label">Demo Flows</div>
516
+ <div class="cw-dropdown-separator"></div>
517
+ ${journeyItems}
518
+ ${stopButton}
519
+ </div>
520
+ </div>
521
+ `;
522
+ }
523
+
524
+ function render() {
525
+ if (!container) return;
526
+
527
+ // Render FAB (floating action button)
528
+ if (!state.isOpen) {
529
+ container.innerHTML = `
530
+ <button class="cw-fab" style="background-color: ${config.primaryColor}">
531
+ <svg class="cw-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
532
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
533
+ </svg>
534
+ </button>
535
+ `;
536
+ container.querySelector('.cw-fab').addEventListener('click', openWidget);
537
+ return;
538
+ }
539
+
540
+ // Render chat widget
541
+ const messagesHtml = state.messages.length === 0
542
+ ? `
543
+ <div class="cw-empty-state">
544
+ <svg class="cw-empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
545
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
546
+ </svg>
547
+ <h3>${escapeHtml(config.emptyStateTitle)}</h3>
548
+ <p>${escapeHtml(config.emptyStateMessage)}</p>
549
+ </div>
550
+ `
551
+ : state.messages.map(renderMessage).join('');
552
+
553
+ const typingIndicator = state.isLoading ? `
554
+ <div class="cw-message-row">
555
+ <div class="cw-typing">
556
+ <span class="cw-spinner"></span>
557
+ <span>Thinking...</span>
558
+ </div>
559
+ </div>
560
+ ` : '';
561
+
562
+ const statusBar = (state.autoRunMode || state.debugMode) ? `
563
+ <div class="cw-status-bar">
564
+ ${state.autoRunMode ? `<span>🤖 Auto-run: ${config.journeyTypes[state.journeyType]?.label || state.journeyType}</span>` : ''}
565
+ ${state.debugMode ? '<span>🐛 Debug</span>' : ''}
566
+ </div>
567
+ ` : '';
568
+
569
+ const errorBar = state.error ? `
570
+ <div class="cw-error-bar">${escapeHtml(state.error)}</div>
571
+ ` : '';
572
+
573
+ container.innerHTML = `
574
+ <div class="cw-widget ${state.isExpanded ? 'cw-widget-expanded' : ''}" style="--cw-primary: ${config.primaryColor}">
575
+ <div class="cw-header" style="background-color: ${config.primaryColor}">
576
+ <span class="cw-title">${escapeHtml(config.title)}</span>
577
+ <div class="cw-header-actions">
578
+ <button class="cw-header-btn" data-action="clear" title="Clear Conversation" ${state.isLoading || state.messages.length === 0 ? 'disabled' : ''}>
579
+ <svg class="cw-icon-sm" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
580
+ <polyline points="3 6 5 6 21 6"></polyline>
581
+ <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>
582
+ </svg>
583
+ </button>
584
+ ${config.enableDebugMode ? `
585
+ <button class="cw-header-btn ${state.debugMode ? 'cw-btn-active' : ''}" data-action="toggle-debug" title="${state.debugMode ? 'Hide Debug Info' : 'Show Debug Info'}">
586
+ <svg class="cw-icon-sm ${state.debugMode ? 'cw-icon-warning' : ''}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
587
+ <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>
588
+ <circle cx="12" cy="12" r="3"></circle>
589
+ </svg>
590
+ </button>
591
+ ` : ''}
592
+ ${renderJourneyDropdown()}
593
+ <button class="cw-header-btn" data-action="toggle-expand" title="${state.isExpanded ? 'Minimize' : 'Expand'}">
594
+ ${state.isExpanded ? '⊖' : '⊕'}
595
+ </button>
596
+ <button class="cw-header-btn" data-action="close" title="Close">
597
+
598
+ </button>
599
+ </div>
600
+ </div>
601
+ ${statusBar}
602
+ <div class="cw-messages" id="cw-messages">
603
+ ${messagesHtml}
604
+ ${typingIndicator}
605
+ </div>
606
+ ${errorBar}
607
+ <form class="cw-input-form" id="cw-input-form">
608
+ <input type="text" class="cw-input" placeholder="${escapeHtml(config.placeholder)}" ${state.isLoading ? 'disabled' : ''}>
609
+ <button type="submit" class="cw-send-btn" style="background-color: ${config.primaryColor}" ${state.isLoading ? 'disabled' : ''}>
610
+ ${state.isLoading ? '<span class="cw-spinner"></span>' : '➤'}
611
+ </button>
612
+ </form>
613
+ </div>
614
+ `;
615
+
616
+ // Attach event listeners
617
+ attachEventListeners();
618
+
619
+ // Scroll to bottom
620
+ const messagesEl = document.getElementById('cw-messages');
621
+ if (messagesEl) {
622
+ messagesEl.scrollTop = messagesEl.scrollHeight;
623
+ }
624
+ }
625
+
626
+ function attachEventListeners() {
627
+ // Header buttons
628
+ container.querySelectorAll('[data-action]').forEach(btn => {
629
+ btn.addEventListener('click', (e) => {
630
+ e.preventDefault();
631
+ e.stopPropagation();
632
+ const action = btn.dataset.action;
633
+
634
+ switch (action) {
635
+ case 'close': closeWidget(); break;
636
+ case 'toggle-expand': toggleExpand(); break;
637
+ case 'toggle-debug': toggleDebugMode(); break;
638
+ case 'clear': clearMessages(); break;
639
+ case 'stop-autorun': stopAutoRun(); break;
640
+ case 'toggle-journey-dropdown':
641
+ const dropdown = document.getElementById('cw-journey-dropdown');
642
+ if (dropdown) {
643
+ dropdown.classList.toggle('cw-dropdown-hidden');
644
+ }
645
+ break;
646
+ }
647
+ });
648
+ });
649
+
650
+ // Journey selection
651
+ container.querySelectorAll('[data-journey]').forEach(btn => {
652
+ btn.addEventListener('click', (e) => {
653
+ e.preventDefault();
654
+ const journeyType = btn.dataset.journey;
655
+ const dropdown = document.getElementById('cw-journey-dropdown');
656
+ if (dropdown) dropdown.classList.add('cw-dropdown-hidden');
657
+ startDemoFlow(journeyType);
658
+ });
659
+ });
660
+
661
+ // Form submission
662
+ const form = document.getElementById('cw-input-form');
663
+ if (form) {
664
+ form.addEventListener('submit', (e) => {
665
+ e.preventDefault();
666
+ const input = form.querySelector('.cw-input');
667
+ if (input && input.value.trim()) {
668
+ sendMessage(input.value);
669
+ input.value = '';
670
+ }
671
+ });
672
+ }
673
+
674
+ // Close dropdown when clicking outside
675
+ document.addEventListener('click', (e) => {
676
+ if (!e.target.closest('.cw-dropdown')) {
677
+ const dropdown = document.getElementById('cw-journey-dropdown');
678
+ if (dropdown) dropdown.classList.add('cw-dropdown-hidden');
679
+ }
680
+ });
681
+ }
682
+
683
+ // ============================================================================
684
+ // Public API
685
+ // ============================================================================
686
+
687
+ function init(userConfig = {}) {
688
+ config = { ...DEFAULT_CONFIG, ...userConfig };
689
+ state.journeyType = config.defaultJourneyType;
690
+
691
+ // Restore conversation ID
692
+ state.conversationId = getStoredValue(config.conversationIdKey);
693
+
694
+ // Create container
695
+ container = document.createElement('div');
696
+ container.id = 'chat-widget-container';
697
+ container.className = `cw-container cw-position-${config.position}`;
698
+ document.body.appendChild(container);
699
+
700
+ // Initial render
701
+ render();
702
+
703
+ console.log('[ChatWidget] Initialized with config:', config);
704
+ }
705
+
706
+ function destroy() {
707
+ if (state.eventSource) {
708
+ state.eventSource.close();
709
+ }
710
+ if (container) {
711
+ container.remove();
712
+ container = null;
713
+ }
714
+ }
715
+
716
+ function open() {
717
+ openWidget();
718
+ }
719
+
720
+ function close() {
721
+ closeWidget();
722
+ }
723
+
724
+ function send(message) {
725
+ if (!state.isOpen) {
726
+ openWidget();
727
+ }
728
+ sendMessage(message);
729
+ }
730
+
731
+ // Export public API
732
+ global.ChatWidget = {
733
+ init,
734
+ destroy,
735
+ open,
736
+ close,
737
+ send,
738
+ clearMessages,
739
+ startDemoFlow,
740
+ stopAutoRun,
741
+ getState: () => ({ ...state }),
742
+ getConfig: () => ({ ...config }),
743
+ };
744
+
745
+ })(typeof window !== 'undefined' ? window : this);