@luckydraw/cumulus 0.14.0 → 0.15.1

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,827 @@
1
+ /**
2
+ * Cumulus Web Chat Widget — self-contained, no external dependencies.
3
+ * Embeddable via <script src="/widget.js" data-api-key="..."></script>
4
+ * or usable standalone at /chat.
5
+ *
6
+ * < 50KB unminified.
7
+ */
8
+ (function () {
9
+ 'use strict';
10
+
11
+ // ─── Configuration ──────────────────────────────────────────
12
+ const script = document.currentScript;
13
+ const API_KEY = script?.getAttribute('data-api-key') || '';
14
+ const STANDALONE = script?.getAttribute('data-standalone') === 'true';
15
+ const WS_URL_OVERRIDE = script?.getAttribute('data-ws-url') || '';
16
+
17
+ // Derive WebSocket URL from script src or page location
18
+ function getWsUrl() {
19
+ if (WS_URL_OVERRIDE) return WS_URL_OVERRIDE;
20
+ const loc = window.location;
21
+ const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
22
+ return `${proto}//${loc.host}/ws`;
23
+ }
24
+
25
+ // Session ID — persisted in localStorage for returning visitors
26
+ function getSessionId() {
27
+ const key = 'cumulus-session-id';
28
+ let id = localStorage.getItem(key);
29
+ if (!id) {
30
+ id = 'web-' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
31
+ localStorage.setItem(key, id);
32
+ }
33
+ return id;
34
+ }
35
+
36
+ // API key — persisted in localStorage for standalone mode
37
+ function getStoredApiKey() {
38
+ return localStorage.getItem('cumulus-api-key') || '';
39
+ }
40
+ function setStoredApiKey(key) {
41
+ localStorage.setItem('cumulus-api-key', key);
42
+ }
43
+ function clearStoredApiKey() {
44
+ localStorage.removeItem('cumulus-api-key');
45
+ }
46
+
47
+ // ─── Styles ─────────────────────────────────────────────────
48
+ const STYLES = `
49
+ .cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }
50
+ .cumulus-widget {
51
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
52
+ font-size: 14px;
53
+ line-height: 1.5;
54
+ color: #e0e0e0;
55
+ }
56
+
57
+ /* Floating bubble (embeddable mode only) */
58
+ .cumulus-bubble {
59
+ position: fixed;
60
+ bottom: 24px;
61
+ right: 24px;
62
+ width: 56px;
63
+ height: 56px;
64
+ border-radius: 50%;
65
+ background: #6366f1;
66
+ border: none;
67
+ cursor: pointer;
68
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: center;
72
+ z-index: 10000;
73
+ transition: transform 0.15s ease;
74
+ }
75
+ .cumulus-bubble:hover { transform: scale(1.08); }
76
+ .cumulus-bubble svg { width: 28px; height: 28px; fill: white; }
77
+
78
+ /* Panel container */
79
+ .cumulus-panel {
80
+ background: #1a1a2e;
81
+ border: 1px solid #2a2a4a;
82
+ border-radius: 12px;
83
+ display: flex;
84
+ flex-direction: column;
85
+ overflow: hidden;
86
+ }
87
+ .cumulus-panel.floating {
88
+ position: fixed;
89
+ bottom: 92px;
90
+ right: 24px;
91
+ width: 400px;
92
+ height: 560px;
93
+ z-index: 10000;
94
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
95
+ }
96
+ .cumulus-panel.standalone {
97
+ width: 100%;
98
+ height: 100vh;
99
+ border: none;
100
+ border-radius: 0;
101
+ }
102
+
103
+ /* Header */
104
+ .cumulus-header {
105
+ display: flex;
106
+ align-items: center;
107
+ justify-content: space-between;
108
+ padding: 12px 16px;
109
+ background: #16162b;
110
+ border-bottom: 1px solid #2a2a4a;
111
+ }
112
+ .cumulus-header-title {
113
+ font-weight: 600;
114
+ font-size: 15px;
115
+ color: #f0f0ff;
116
+ }
117
+ .cumulus-header-status {
118
+ font-size: 11px;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 6px;
122
+ }
123
+ .cumulus-status-dot {
124
+ width: 8px;
125
+ height: 8px;
126
+ border-radius: 50%;
127
+ background: #666;
128
+ }
129
+ .cumulus-status-dot.connected { background: #22c55e; }
130
+ .cumulus-status-dot.connecting { background: #f59e0b; }
131
+ .cumulus-status-dot.disconnected { background: #ef4444; }
132
+ .cumulus-close-btn {
133
+ background: none;
134
+ border: none;
135
+ color: #888;
136
+ cursor: pointer;
137
+ font-size: 20px;
138
+ padding: 4px;
139
+ line-height: 1;
140
+ }
141
+ .cumulus-close-btn:hover { color: #fff; }
142
+
143
+ /* Messages area */
144
+ .cumulus-messages {
145
+ flex: 1;
146
+ overflow-y: auto;
147
+ padding: 16px;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: 12px;
151
+ }
152
+ .cumulus-messages::-webkit-scrollbar { width: 6px; }
153
+ .cumulus-messages::-webkit-scrollbar-track { background: transparent; }
154
+ .cumulus-messages::-webkit-scrollbar-thumb { background: #3a3a5a; border-radius: 3px; }
155
+
156
+ /* Message bubbles */
157
+ .cumulus-msg {
158
+ max-width: 85%;
159
+ padding: 10px 14px;
160
+ border-radius: 12px;
161
+ word-wrap: break-word;
162
+ white-space: pre-wrap;
163
+ }
164
+ .cumulus-msg.user {
165
+ align-self: flex-end;
166
+ background: #4f46e5;
167
+ color: #fff;
168
+ border-bottom-right-radius: 4px;
169
+ }
170
+ .cumulus-msg.assistant {
171
+ align-self: flex-start;
172
+ background: #2a2a4a;
173
+ color: #e0e0e0;
174
+ border-bottom-left-radius: 4px;
175
+ }
176
+
177
+ /* Streaming cursor */
178
+ .cumulus-cursor {
179
+ display: inline-block;
180
+ width: 2px;
181
+ height: 1em;
182
+ background: #6366f1;
183
+ margin-left: 2px;
184
+ vertical-align: text-bottom;
185
+ animation: cumulus-blink 0.8s step-end infinite;
186
+ }
187
+ @keyframes cumulus-blink { 50% { opacity: 0; } }
188
+
189
+ /* Loading indicator */
190
+ .cumulus-loading {
191
+ align-self: flex-start;
192
+ padding: 10px 14px;
193
+ color: #888;
194
+ font-style: italic;
195
+ }
196
+
197
+ /* Markdown basics */
198
+ .cumulus-msg code {
199
+ background: rgba(255,255,255,0.1);
200
+ padding: 2px 6px;
201
+ border-radius: 4px;
202
+ font-family: 'SF Mono', 'Fira Code', monospace;
203
+ font-size: 13px;
204
+ }
205
+ .cumulus-msg pre {
206
+ background: #0d0d1a;
207
+ padding: 12px;
208
+ border-radius: 8px;
209
+ overflow-x: auto;
210
+ margin: 8px 0;
211
+ }
212
+ .cumulus-msg pre code {
213
+ background: none;
214
+ padding: 0;
215
+ display: block;
216
+ }
217
+ .cumulus-msg a { color: #818cf8; }
218
+ .cumulus-msg strong { font-weight: 600; }
219
+ .cumulus-msg em { font-style: italic; }
220
+
221
+ /* Input area */
222
+ .cumulus-input-area {
223
+ display: flex;
224
+ gap: 8px;
225
+ padding: 12px 16px;
226
+ background: #16162b;
227
+ border-top: 1px solid #2a2a4a;
228
+ }
229
+ .cumulus-input {
230
+ flex: 1;
231
+ background: #1a1a2e;
232
+ border: 1px solid #3a3a5a;
233
+ border-radius: 8px;
234
+ color: #e0e0e0;
235
+ padding: 10px 12px;
236
+ font-size: 14px;
237
+ font-family: inherit;
238
+ resize: none;
239
+ min-height: 40px;
240
+ max-height: 120px;
241
+ outline: none;
242
+ }
243
+ .cumulus-input:focus { border-color: #6366f1; }
244
+ .cumulus-input::placeholder { color: #666; }
245
+ .cumulus-send-btn {
246
+ background: #6366f1;
247
+ border: none;
248
+ border-radius: 8px;
249
+ color: white;
250
+ padding: 0 16px;
251
+ cursor: pointer;
252
+ font-size: 14px;
253
+ font-weight: 500;
254
+ white-space: nowrap;
255
+ }
256
+ .cumulus-send-btn:hover { background: #5558e6; }
257
+ .cumulus-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
258
+
259
+ /* Empty state */
260
+ .cumulus-empty {
261
+ display: flex;
262
+ align-items: center;
263
+ justify-content: center;
264
+ flex: 1;
265
+ color: #555;
266
+ font-size: 15px;
267
+ text-align: center;
268
+ padding: 20px;
269
+ }
270
+
271
+ /* Auth panel */
272
+ .cumulus-auth {
273
+ display: flex;
274
+ flex-direction: column;
275
+ align-items: center;
276
+ justify-content: center;
277
+ flex: 1;
278
+ padding: 32px;
279
+ gap: 16px;
280
+ }
281
+ .cumulus-auth-title {
282
+ font-size: 20px;
283
+ font-weight: 600;
284
+ color: #f0f0ff;
285
+ }
286
+ .cumulus-auth-subtitle {
287
+ color: #888;
288
+ font-size: 13px;
289
+ text-align: center;
290
+ max-width: 280px;
291
+ }
292
+ .cumulus-auth-input {
293
+ width: 100%;
294
+ max-width: 320px;
295
+ background: #1a1a2e;
296
+ border: 1px solid #3a3a5a;
297
+ border-radius: 8px;
298
+ color: #e0e0e0;
299
+ padding: 12px 14px;
300
+ font-size: 14px;
301
+ font-family: 'SF Mono', 'Fira Code', monospace;
302
+ outline: none;
303
+ }
304
+ .cumulus-auth-input:focus { border-color: #6366f1; }
305
+ .cumulus-auth-input::placeholder { color: #555; }
306
+ .cumulus-auth-btn {
307
+ background: #6366f1;
308
+ border: none;
309
+ border-radius: 8px;
310
+ color: white;
311
+ padding: 10px 32px;
312
+ cursor: pointer;
313
+ font-size: 14px;
314
+ font-weight: 500;
315
+ }
316
+ .cumulus-auth-btn:hover { background: #5558e6; }
317
+ .cumulus-auth-btn:disabled { opacity: 0.5; cursor: not-allowed; }
318
+ .cumulus-auth-error {
319
+ color: #ef4444;
320
+ font-size: 13px;
321
+ text-align: center;
322
+ }
323
+ .cumulus-auth-logout {
324
+ background: none;
325
+ border: none;
326
+ color: #666;
327
+ cursor: pointer;
328
+ font-size: 11px;
329
+ padding: 2px 6px;
330
+ }
331
+ .cumulus-auth-logout:hover { color: #ef4444; }
332
+ `;
333
+
334
+ // ─── Simple Markdown Renderer ───────────────────────────────
335
+ function renderMarkdown(text) {
336
+ let html = escapeHtml(text);
337
+
338
+ // Code blocks (``` ... ```)
339
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
340
+ return '<pre><code>' + code + '</code></pre>';
341
+ });
342
+
343
+ // Inline code
344
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
345
+
346
+ // Bold
347
+ html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
348
+
349
+ // Italic
350
+ html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
351
+
352
+ // Links
353
+ html = html.replace(
354
+ /\[([^\]]+)\]\(([^)]+)\)/g,
355
+ '<a href="$2" target="_blank" rel="noopener">$1</a>'
356
+ );
357
+
358
+ // Line breaks
359
+ html = html.replace(/\n/g, '<br>');
360
+
361
+ return html;
362
+ }
363
+
364
+ function escapeHtml(text) {
365
+ const div = document.createElement('div');
366
+ div.textContent = text;
367
+ return div.innerHTML;
368
+ }
369
+
370
+ // ─── WebSocket Connection ───────────────────────────────────
371
+ function createConnection(opts) {
372
+ const { wsUrl, apiKey, sessionId, onMessage, onStatus } = opts;
373
+ let ws = null;
374
+ let reconnectTimer = null;
375
+ let reconnectDelay = 1000;
376
+
377
+ function connect() {
378
+ onStatus('connecting');
379
+ ws = new WebSocket(wsUrl);
380
+
381
+ ws.onopen = function () {
382
+ onStatus('connected');
383
+ reconnectDelay = 1000;
384
+
385
+ // Authenticate
386
+ ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
387
+
388
+ // Load history
389
+ ws.send(
390
+ JSON.stringify({
391
+ type: 'history',
392
+ threadName: sessionId,
393
+ limit: 50,
394
+ })
395
+ );
396
+ };
397
+
398
+ ws.onmessage = function (event) {
399
+ try {
400
+ var data = JSON.parse(event.data);
401
+ onMessage(data);
402
+ } catch (e) {
403
+ console.error('[Cumulus] Invalid message:', e);
404
+ }
405
+ };
406
+
407
+ ws.onclose = function () {
408
+ onStatus('disconnected');
409
+ scheduleReconnect();
410
+ };
411
+
412
+ ws.onerror = function () {
413
+ // onclose will fire after this
414
+ };
415
+ }
416
+
417
+ function scheduleReconnect() {
418
+ if (reconnectTimer) return;
419
+ reconnectTimer = setTimeout(function () {
420
+ reconnectTimer = null;
421
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
422
+ connect();
423
+ }, reconnectDelay);
424
+ }
425
+
426
+ function send(data) {
427
+ if (ws && ws.readyState === WebSocket.OPEN) {
428
+ ws.send(JSON.stringify(data));
429
+ }
430
+ }
431
+
432
+ function close() {
433
+ if (reconnectTimer) {
434
+ clearTimeout(reconnectTimer);
435
+ reconnectTimer = null;
436
+ }
437
+ if (ws) {
438
+ ws.onclose = null; // Prevent reconnect
439
+ ws.close();
440
+ ws = null;
441
+ }
442
+ }
443
+
444
+ connect();
445
+ return { send: send, close: close };
446
+ }
447
+
448
+ // ─── Widget UI ──────────────────────────────────────────────
449
+ function createWidget(opts) {
450
+ const { standalone } = opts;
451
+ const sessionId = getSessionId();
452
+ const wsUrl = getWsUrl();
453
+ // For standalone mode, use stored key or empty; for embedded, use data-api-key
454
+ var activeApiKey = standalone ? getStoredApiKey() : API_KEY;
455
+
456
+ // Inject styles
457
+ const styleEl = document.createElement('style');
458
+ styleEl.textContent = STYLES;
459
+ document.head.appendChild(styleEl);
460
+
461
+ // State
462
+ var messages = [];
463
+ var streaming = false;
464
+ var streamBuffer = '';
465
+ var connection = null;
466
+ var connectionStatus = 'disconnected';
467
+ var authenticated = false;
468
+
469
+ // ─── DOM elements ──────────────────────────────
470
+ const container = document.createElement('div');
471
+ container.className = 'cumulus-widget';
472
+
473
+ // Build panel
474
+ const panel = document.createElement('div');
475
+ panel.className = 'cumulus-panel ' + (standalone ? 'standalone' : 'floating');
476
+ panel.style.display = standalone ? 'flex' : 'none';
477
+
478
+ // Header
479
+ const header = document.createElement('div');
480
+ header.className = 'cumulus-header';
481
+ header.innerHTML =
482
+ '<span class="cumulus-header-title">Cumulus</span>' +
483
+ '<span class="cumulus-header-status">' +
484
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
485
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
486
+ '</span>' +
487
+ (standalone
488
+ ? '<button class="cumulus-auth-logout" data-testid="webchat-logout" style="display:none">Logout</button>'
489
+ : '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>');
490
+ panel.appendChild(header);
491
+
492
+ // Auth panel (standalone mode — shown when no API key)
493
+ const authPanel = document.createElement('div');
494
+ authPanel.className = 'cumulus-auth';
495
+ authPanel.setAttribute('data-testid', 'webchat-auth');
496
+ authPanel.innerHTML =
497
+ '<div class="cumulus-auth-title">Cumulus Chat</div>' +
498
+ '<div class="cumulus-auth-subtitle">Enter your API key to connect. The key will be saved in your browser.</div>' +
499
+ '<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
500
+ '<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
501
+ '<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
502
+ panel.appendChild(authPanel);
503
+
504
+ // Messages area
505
+ const messagesEl = document.createElement('div');
506
+ messagesEl.className = 'cumulus-messages';
507
+ messagesEl.setAttribute('data-testid', 'webchat-messages');
508
+ messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
509
+ messagesEl.style.display = 'none';
510
+ panel.appendChild(messagesEl);
511
+
512
+ // Input area
513
+ const inputArea = document.createElement('div');
514
+ inputArea.className = 'cumulus-input-area';
515
+ inputArea.style.display = 'none';
516
+ const input = document.createElement('textarea');
517
+ input.className = 'cumulus-input';
518
+ input.setAttribute('data-testid', 'webchat-input');
519
+ input.placeholder = 'Type a message...';
520
+ input.rows = 1;
521
+ const sendBtn = document.createElement('button');
522
+ sendBtn.className = 'cumulus-send-btn';
523
+ sendBtn.setAttribute('data-testid', 'webchat-send');
524
+ sendBtn.textContent = 'Send';
525
+ inputArea.appendChild(input);
526
+ inputArea.appendChild(sendBtn);
527
+ panel.appendChild(inputArea);
528
+
529
+ container.appendChild(panel);
530
+
531
+ // ─── Auth/Chat view switching ────────────────────
532
+ function showAuthView() {
533
+ authenticated = false;
534
+ authPanel.style.display = 'flex';
535
+ messagesEl.style.display = 'none';
536
+ inputArea.style.display = 'none';
537
+ var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
538
+ if (logoutBtn) logoutBtn.style.display = 'none';
539
+ var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
540
+ if (errEl) errEl.textContent = '';
541
+ }
542
+
543
+ function showChatView() {
544
+ authenticated = true;
545
+ authPanel.style.display = 'none';
546
+ messagesEl.style.display = 'flex';
547
+ inputArea.style.display = 'flex';
548
+ var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
549
+ if (logoutBtn) logoutBtn.style.display = 'inline-block';
550
+ input.focus();
551
+ }
552
+
553
+ // Floating bubble (embeddable mode only)
554
+ var bubble = null;
555
+ if (!standalone) {
556
+ bubble = document.createElement('button');
557
+ bubble.className = 'cumulus-bubble';
558
+ bubble.setAttribute('data-testid', 'webchat-bubble');
559
+ bubble.innerHTML =
560
+ '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>';
561
+ container.appendChild(bubble);
562
+ }
563
+
564
+ // ─── Rendering ─────────────────────────────────
565
+ function scrollToBottom() {
566
+ messagesEl.scrollTop = messagesEl.scrollHeight;
567
+ }
568
+
569
+ function renderMessages() {
570
+ messagesEl.innerHTML = '';
571
+
572
+ if (messages.length === 0 && !streaming) {
573
+ messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
574
+ return;
575
+ }
576
+
577
+ for (var i = 0; i < messages.length; i++) {
578
+ var msg = messages[i];
579
+ var el = document.createElement('div');
580
+ el.className = 'cumulus-msg ' + msg.role;
581
+ if (msg.role === 'assistant') {
582
+ el.innerHTML = renderMarkdown(msg.content);
583
+ } else {
584
+ el.textContent = msg.content;
585
+ }
586
+ messagesEl.appendChild(el);
587
+ }
588
+
589
+ // Streaming indicator
590
+ if (streaming) {
591
+ var streamEl = document.createElement('div');
592
+ streamEl.className = 'cumulus-msg assistant';
593
+ streamEl.setAttribute('data-testid', 'webchat-streaming');
594
+ if (streamBuffer) {
595
+ streamEl.innerHTML =
596
+ renderMarkdown(streamBuffer) + '<span class="cumulus-cursor"></span>';
597
+ } else {
598
+ streamEl.innerHTML =
599
+ '<span style="color:#888;font-style:italic">Thinking...</span><span class="cumulus-cursor"></span>';
600
+ }
601
+ messagesEl.appendChild(streamEl);
602
+ }
603
+
604
+ scrollToBottom();
605
+ }
606
+
607
+ function updateStatus(status) {
608
+ connectionStatus = status;
609
+ var dot = panel.querySelector('.cumulus-status-dot');
610
+ var text = panel.querySelector('[data-testid="webchat-status-text"]');
611
+ if (dot) {
612
+ dot.className = 'cumulus-status-dot ' + status;
613
+ }
614
+ if (text) {
615
+ text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
616
+ }
617
+ }
618
+
619
+ // ─── Message handling ──────────────────────────
620
+ function handleServerMessage(data) {
621
+ switch (data.type) {
622
+ case 'auth_ok':
623
+ showChatView();
624
+ break;
625
+
626
+ case 'auth_error':
627
+ console.error('[Cumulus] Auth failed:', data.error);
628
+ // If we had a stored key and it failed, show auth panel again
629
+ if (standalone) {
630
+ clearStoredApiKey();
631
+ activeApiKey = '';
632
+ if (connection) { connection.close(); connection = null; }
633
+ showAuthView();
634
+ var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
635
+ if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
636
+ }
637
+ break;
638
+
639
+ case 'history':
640
+ if (data.messages && data.messages.length > 0) {
641
+ messages = data.messages.map(function (m) {
642
+ return { role: m.role, content: m.content };
643
+ });
644
+ renderMessages();
645
+ }
646
+ break;
647
+
648
+ case 'token':
649
+ if (!streaming) {
650
+ streaming = true;
651
+ streamBuffer = '';
652
+ }
653
+ streamBuffer += data.text;
654
+ renderMessages();
655
+ break;
656
+
657
+ case 'segment':
658
+ // Tool use/result — could show in verbose mode later
659
+ break;
660
+
661
+ case 'done':
662
+ streaming = false;
663
+ messages.push({ role: 'assistant', content: data.response || streamBuffer });
664
+ streamBuffer = '';
665
+ renderMessages();
666
+ break;
667
+
668
+ case 'error':
669
+ streaming = false;
670
+ if (streamBuffer) {
671
+ messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + data.error + ']' });
672
+ } else {
673
+ messages.push({ role: 'assistant', content: '[Error: ' + data.error + ']' });
674
+ }
675
+ streamBuffer = '';
676
+ renderMessages();
677
+ break;
678
+ }
679
+ }
680
+
681
+ function sendMessage() {
682
+ var text = input.value.trim();
683
+ if (!text || streaming) return;
684
+
685
+ messages.push({ role: 'user', content: text });
686
+ input.value = '';
687
+ input.style.height = 'auto';
688
+ streaming = true;
689
+ streamBuffer = '';
690
+ renderMessages();
691
+
692
+ connection.send({
693
+ type: 'message',
694
+ threadName: sessionId,
695
+ message: text,
696
+ });
697
+ }
698
+
699
+ // ─── Event listeners ───────────────────────────
700
+ sendBtn.addEventListener('click', sendMessage);
701
+
702
+ input.addEventListener('keydown', function (e) {
703
+ if (e.key === 'Enter' && !e.shiftKey) {
704
+ e.preventDefault();
705
+ sendMessage();
706
+ }
707
+ });
708
+
709
+ // Auto-resize textarea
710
+ input.addEventListener('input', function () {
711
+ input.style.height = 'auto';
712
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
713
+ });
714
+
715
+ // Bubble toggle (embeddable mode)
716
+ if (bubble) {
717
+ bubble.addEventListener('click', function () {
718
+ var showing = panel.style.display !== 'none';
719
+ panel.style.display = showing ? 'none' : 'flex';
720
+ if (!showing && !connection) {
721
+ startConnection();
722
+ }
723
+ if (!showing) {
724
+ input.focus();
725
+ scrollToBottom();
726
+ }
727
+ });
728
+
729
+ // Close button
730
+ var closeBtn = panel.querySelector('.cumulus-close-btn');
731
+ if (closeBtn) {
732
+ closeBtn.addEventListener('click', function () {
733
+ panel.style.display = 'none';
734
+ });
735
+ }
736
+ }
737
+
738
+ // ─── Auth panel events ─────────────────────────
739
+ var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
740
+ var authBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
741
+
742
+ function submitAuth() {
743
+ var key = authInput.value.trim();
744
+ if (!key) return;
745
+ var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
746
+ if (errEl) errEl.textContent = '';
747
+ activeApiKey = key;
748
+ setStoredApiKey(key);
749
+ startConnection();
750
+ }
751
+
752
+ authBtn.addEventListener('click', submitAuth);
753
+ authInput.addEventListener('keydown', function (e) {
754
+ if (e.key === 'Enter') {
755
+ e.preventDefault();
756
+ submitAuth();
757
+ }
758
+ });
759
+
760
+ // Logout button (standalone only)
761
+ var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
762
+ if (logoutBtn) {
763
+ logoutBtn.addEventListener('click', function () {
764
+ clearStoredApiKey();
765
+ activeApiKey = '';
766
+ if (connection) { connection.close(); connection = null; }
767
+ messages = [];
768
+ streaming = false;
769
+ streamBuffer = '';
770
+ showAuthView();
771
+ authInput.value = '';
772
+ });
773
+ }
774
+
775
+ // ─── Connect ───────────────────────────────────
776
+ function startConnection() {
777
+ if (connection) { connection.close(); connection = null; }
778
+ connection = createConnection({
779
+ wsUrl: wsUrl,
780
+ apiKey: activeApiKey,
781
+ sessionId: sessionId,
782
+ onMessage: handleServerMessage,
783
+ onStatus: updateStatus,
784
+ });
785
+ }
786
+
787
+ // ─── Mount ─────────────────────────────────────
788
+ function mount(target) {
789
+ (target || document.body).appendChild(container);
790
+ if (standalone) {
791
+ if (activeApiKey) {
792
+ // Have a stored key — try connecting directly
793
+ startConnection();
794
+ } else {
795
+ // No key — show auth panel
796
+ showAuthView();
797
+ authInput.focus();
798
+ }
799
+ }
800
+ }
801
+
802
+ function destroy() {
803
+ if (connection) connection.close();
804
+ container.remove();
805
+ }
806
+
807
+ return { mount: mount, destroy: destroy, send: sendMessage };
808
+ }
809
+
810
+ // ─── Auto-mount ─────────────────────────────────────────────
811
+ if (script) {
812
+ var widget = createWidget({
813
+ standalone: STANDALONE,
814
+ });
815
+
816
+ if (document.readyState === 'loading') {
817
+ document.addEventListener('DOMContentLoaded', function () {
818
+ widget.mount();
819
+ });
820
+ } else {
821
+ widget.mount();
822
+ }
823
+
824
+ // Expose for programmatic access
825
+ window.CumulusChat = widget;
826
+ }
827
+ })();