@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.
- package/LICENSE +22 -0
- package/README.md +229 -0
- package/dist/chat-widget.css +503 -0
- package/dist/chat-widget.js +745 -0
- package/package.json +46 -0
|
@@ -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);
|