@luckydraw/cumulus 0.15.1 → 0.15.2

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.
@@ -3,29 +3,27 @@
3
3
  * Embeddable via <script src="/widget.js" data-api-key="..."></script>
4
4
  * or usable standalone at /chat.
5
5
  *
6
- * < 50KB unminified.
6
+ * < 80KB unminified.
7
7
  */
8
8
  (function () {
9
9
  'use strict';
10
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') || '';
11
+ // ─── Configuration ──────────────────────────────────────────────────────────
12
+ var script = document.currentScript;
13
+ var API_KEY = (script && script.getAttribute('data-api-key')) || '';
14
+ var STANDALONE = (script && script.getAttribute('data-standalone')) === 'true';
15
+ var WS_URL_OVERRIDE = (script && script.getAttribute('data-ws-url')) || '';
16
16
 
17
- // Derive WebSocket URL from script src or page location
18
17
  function getWsUrl() {
19
18
  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`;
19
+ var loc = window.location;
20
+ var proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
21
+ return proto + '//' + loc.host + '/ws';
23
22
  }
24
23
 
25
- // Session ID — persisted in localStorage for returning visitors
26
24
  function getSessionId() {
27
- const key = 'cumulus-session-id';
28
- let id = localStorage.getItem(key);
25
+ var key = 'cumulus-session-id';
26
+ var id = localStorage.getItem(key);
29
27
  if (!id) {
30
28
  id = 'web-' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
31
29
  localStorage.setItem(key, id);
@@ -33,366 +31,686 @@
33
31
  return id;
34
32
  }
35
33
 
36
- // API key — persisted in localStorage for standalone mode
37
34
  function getStoredApiKey() {
38
35
  return localStorage.getItem('cumulus-api-key') || '';
39
36
  }
40
- function setStoredApiKey(key) {
41
- localStorage.setItem('cumulus-api-key', key);
37
+ function setStoredApiKey(k) {
38
+ localStorage.setItem('cumulus-api-key', k);
42
39
  }
43
40
  function clearStoredApiKey() {
44
41
  localStorage.removeItem('cumulus-api-key');
45
42
  }
46
43
 
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
- }
44
+ // ─── Styles ─────────────────────────────────────────────────────────────────
45
+ var STYLES = [
46
+ '.cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }',
47
+
48
+ '.cumulus-widget {',
49
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
50
+ ' font-size: 14px;',
51
+ ' line-height: 1.6;',
52
+ ' color: #e0e0e0;',
53
+ '}',
54
+
55
+ /* ── Floating bubble ── */
56
+ '.cumulus-bubble {',
57
+ ' position: fixed; bottom: 24px; right: 24px;',
58
+ ' width: 56px; height: 56px;',
59
+ ' border-radius: 50%;',
60
+ ' background: #0066cc;',
61
+ ' border: none; cursor: pointer;',
62
+ ' box-shadow: 0 4px 14px rgba(0,0,0,0.45);',
63
+ ' display: flex; align-items: center; justify-content: center;',
64
+ ' z-index: 10000;',
65
+ ' transition: background 0.15s ease, transform 0.15s ease;',
66
+ '}',
67
+ '.cumulus-bubble:hover { background: #0077ee; transform: scale(1.07); }',
68
+ '.cumulus-bubble svg { width: 26px; height: 26px; fill: white; }',
69
+
70
+ /* ── Panel ── */
71
+ '.cumulus-panel {',
72
+ ' background: #1e1e1e;',
73
+ ' border: 1px solid #3a3a3a;',
74
+ ' display: flex; flex-direction: column; overflow: hidden;',
75
+ '}',
76
+ '.cumulus-panel.floating {',
77
+ ' position: fixed; bottom: 90px; right: 24px;',
78
+ ' width: 420px; height: 580px;',
79
+ ' border-radius: 10px;',
80
+ ' z-index: 10000;',
81
+ ' box-shadow: 0 10px 36px rgba(0,0,0,0.55);',
82
+ '}',
83
+ '.cumulus-panel.standalone {',
84
+ ' width: 100%; height: 100vh;',
85
+ ' border: none; border-radius: 0;',
86
+ '}',
87
+
88
+ /* ── Header ── */
89
+ '.cumulus-header {',
90
+ ' display: flex; align-items: center; justify-content: space-between;',
91
+ ' padding: 0.4em 0.85em;',
92
+ ' background: #2d2d2d;',
93
+ ' border-bottom: 1px solid #3a3a3a;',
94
+ ' flex-shrink: 0;',
95
+ '}',
96
+ '.cumulus-header-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
97
+ '.cumulus-header-status { font-size: 11px; display: flex; align-items: center; gap: 6px; color: #aaa; }',
98
+ '.cumulus-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }',
99
+ '.cumulus-status-dot.connected { background: #22c55e; }',
100
+ '.cumulus-status-dot.connecting { background: #f59e0b; }',
101
+ '.cumulus-status-dot.disconnected { background: #ef4444; }',
102
+ '.cumulus-close-btn { background: none; border: none; color: #888; cursor: pointer; font-size: 20px; padding: 2px 4px; line-height: 1; }',
103
+ '.cumulus-close-btn:hover { color: #e0e0e0; }',
104
+
105
+ /* ── Messages scroll area ── */
106
+ '.cumulus-messages {',
107
+ ' flex: 1; overflow-y: auto;',
108
+ ' padding: 1.5em 1em;',
109
+ ' display: flex; flex-direction: column;',
110
+ ' gap: 1.5em;',
111
+ '}',
112
+ '.cumulus-messages::-webkit-scrollbar { width: 6px; }',
113
+ '.cumulus-messages::-webkit-scrollbar-track { background: transparent; }',
114
+ '.cumulus-messages::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }',
115
+
116
+ /* ── Message inner container (max-width centering) ── */
117
+ '.cumulus-msg-row {',
118
+ ' display: flex; flex-direction: column;',
119
+ ' max-width: 78ch; width: 100%;',
120
+ ' margin: 0 auto;',
121
+ '}',
122
+
123
+ /* ── User messages ── */
124
+ '.cumulus-msg.user {',
125
+ ' align-self: flex-end;',
126
+ ' background: #2a3a4a;',
127
+ ' color: #d4e8f8;',
128
+ ' border: 1px solid #3a5060;',
129
+ ' border-radius: 0.85em 0.85em 0.25em 0.85em;',
130
+ ' padding: 0.55em 0.9em;',
131
+ ' max-width: 42ch;',
132
+ ' word-wrap: break-word;',
133
+ ' white-space: pre-wrap;',
134
+ '}',
135
+ '.cumulus-msg.user.wide { max-width: 100%; }',
136
+
137
+ /* ── User message attachments ── */
138
+ '.cumulus-msg-attachments { display: flex; flex-wrap: wrap; gap: 0.4em; margin-top: 0.4em; }',
139
+ '.cumulus-msg-img { max-width: 16em; max-height: 12em; border-radius: 0.3em; object-fit: cover; border: 1px solid #3a5060; display: block; }',
140
+ '.cumulus-msg-file-badge {',
141
+ ' display: inline-flex; align-items: center; gap: 0.3em;',
142
+ ' background: #1e2a35; border: 1px solid #3a5060; border-radius: 0.3em;',
143
+ ' padding: 0.2em 0.5em; font-size: 0.8em; color: #8ab4cc;',
144
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
145
+ '}',
146
+
147
+ /* ── Assistant messages ── */
148
+ '.cumulus-msg.assistant {',
149
+ ' align-self: flex-start;',
150
+ ' background: transparent;',
151
+ ' color: #e0e0e0;',
152
+ ' padding: 0.4em 0;',
153
+ ' width: 100%;',
154
+ ' word-wrap: break-word;',
155
+ '}',
156
+
157
+ /* ── Markdown rendered inside assistant messages ── */
158
+ '.cumulus-msg.assistant p { margin: 0.4em 0; }',
159
+ '.cumulus-msg.assistant p:first-child { margin-top: 0; }',
160
+ '.cumulus-msg.assistant p:last-child { margin-bottom: 0; }',
161
+
162
+ '.cumulus-msg a { color: #4da6ff; text-decoration: none; }',
163
+ '.cumulus-msg a:hover { text-decoration: underline; }',
164
+ '.cumulus-msg strong { font-weight: 600; }',
165
+ '.cumulus-msg em { font-style: italic; }',
166
+ '.cumulus-msg del { text-decoration: line-through; color: #888; }',
167
+
168
+ /* Headings */
169
+ '.cumulus-msg h1,.cumulus-msg h2,.cumulus-msg h3,.cumulus-msg h4,.cumulus-msg h5,.cumulus-msg h6 {',
170
+ ' font-weight: 600; margin: 0.7em 0 0.3em; line-height: 1.3; color: #e8e8e8;',
171
+ '}',
172
+ '.cumulus-msg h1 { font-size: 1.55em; border-bottom: 1px solid #3a3a3a; padding-bottom: 0.2em; }',
173
+ '.cumulus-msg h2 { font-size: 1.3em; border-bottom: 1px solid #3a3a3a; padding-bottom: 0.2em; }',
174
+ '.cumulus-msg h3 { font-size: 1.12em; }',
175
+ '.cumulus-msg h4 { font-size: 1em; }',
176
+ '.cumulus-msg h5 { font-size: 0.9em; }',
177
+ '.cumulus-msg h6 { font-size: 0.85em; color: #aaa; }',
178
+
179
+ /* Horizontal rule */
180
+ '.cumulus-msg hr { border: none; border-top: 1px solid #3a3a3a; margin: 0.75em 0; }',
181
+
182
+ /* Blockquote */
183
+ '.cumulus-msg blockquote {',
184
+ ' background: #252525;',
185
+ ' border-left: 0.2em solid #0066cc;',
186
+ ' margin: 0.5em 0;',
187
+ ' padding: 0.4em 0.8em;',
188
+ ' border-radius: 0 0.25em 0.25em 0;',
189
+ ' color: #bbb;',
190
+ '}',
191
+
192
+ /* Lists */
193
+ '.cumulus-msg ul,.cumulus-msg ol { padding-left: 1.5em; margin: 0.4em 0; }',
194
+ '.cumulus-msg li { margin: 0.15em 0; }',
195
+ '.cumulus-msg li > ul,.cumulus-msg li > ol { margin: 0.1em 0; }',
196
+
197
+ /* Inline code */
198
+ '.cumulus-msg code {',
199
+ ' background: #2a2a2a;',
200
+ ' border: 1px solid #3a3a3a;',
201
+ ' border-radius: 0.25em;',
202
+ ' color: #ce9178;',
203
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
204
+ ' font-size: 0.875em;',
205
+ ' padding: 0.1em 0.35em;',
206
+ '}',
207
+
208
+ /* Code block wrapper */
209
+ '.code-block-wrapper {',
210
+ ' border: 1px solid #3a3a3a;',
211
+ ' border-radius: 0.4em;',
212
+ ' background: #1e1e1e;',
213
+ ' overflow: hidden;',
214
+ ' margin: 0.6em 0;',
215
+ '}',
216
+ '.code-block-header {',
217
+ ' background: #3d3d3d;',
218
+ ' padding: 0.35em 0.85em;',
219
+ ' border-bottom: 1px solid #4a4a4a;',
220
+ ' display: flex; justify-content: space-between; align-items: center;',
221
+ '}',
222
+ '.code-block-language {',
223
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
224
+ ' font-size: 0.8em; color: #aaa; text-transform: lowercase;',
225
+ '}',
226
+ '.code-block-copy-btn {',
227
+ ' background: transparent;',
228
+ ' border: 1px solid #555;',
229
+ ' border-radius: 0.3em;',
230
+ ' color: #aaa;',
231
+ ' font-size: 0.8em;',
232
+ ' padding: 0.15em 0.55em;',
233
+ ' cursor: pointer;',
234
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
235
+ '}',
236
+ '.code-block-copy-btn:hover { border-color: #0066cc; color: #ddd; }',
237
+ '.code-block-wrapper pre {',
238
+ ' padding: 1em 1.15em; margin: 0;',
239
+ ' overflow-x: auto;',
240
+ ' font-size: 0.925em; line-height: 1.5;',
241
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
242
+ '}',
243
+ '.code-block-wrapper pre code {',
244
+ ' background: none; border: none; color: #e0e0e0;',
245
+ ' padding: 0; font-size: inherit;',
246
+ '}',
247
+
248
+ /* Tables */
249
+ '.cumulus-msg table {',
250
+ ' border-collapse: collapse; width: 100%;',
251
+ ' margin: 0.6em 0; font-size: 0.93em;',
252
+ '}',
253
+ '.cumulus-msg th {',
254
+ ' background: #2a2a2a;',
255
+ ' border: 1px solid #3a3a3a;',
256
+ ' padding: 0.4em 0.7em;',
257
+ ' text-align: left; font-weight: 600; color: #ccc;',
258
+ '}',
259
+ '.cumulus-msg td {',
260
+ ' border: 1px solid #3a3a3a;',
261
+ ' padding: 0.35em 0.7em; color: #ddd;',
262
+ '}',
263
+ '.cumulus-msg tr:nth-child(even) td { background: #222; }',
264
+ '.cumulus-msg tr:nth-child(odd) td { background: #1e1e1e; }',
265
+
266
+ /* ── Streaming cursor ── */
267
+ '.cumulus-cursor {',
268
+ ' display: inline-block;',
269
+ ' width: 0.55em; height: 1em;',
270
+ ' background: #0066cc;',
271
+ ' margin-left: 2px;',
272
+ ' vertical-align: text-bottom;',
273
+ ' animation: cumulus-blink 0.8s step-end infinite;',
274
+ '}',
275
+ '@keyframes cumulus-blink { 50% { opacity: 0; } }',
276
+
277
+ /* ── Loading indicator ── */
278
+ '.cumulus-loading {',
279
+ ' color: #666; font-style: italic; padding: 0.4em 0;',
280
+ ' align-self: flex-start;',
281
+ '}',
282
+
283
+ /* ── Empty state ── */
284
+ '.cumulus-empty {',
285
+ ' display: flex; align-items: center; justify-content: center;',
286
+ ' flex: 1; color: #555; font-size: 15px;',
287
+ ' text-align: center; padding: 20px;',
288
+ '}',
289
+
290
+ /* ── Input area ── */
291
+ '.cumulus-input-area {',
292
+ ' display: flex; flex-direction: column;',
293
+ ' padding: 0.6em 0.85em 0.75em;',
294
+ ' background: #2d2d2d;',
295
+ ' border-top: 1px solid #3a3a3a;',
296
+ ' gap: 0.45em;',
297
+ ' flex-shrink: 0;',
298
+ '}',
299
+
300
+ /* Attachment strip */
301
+ '.cumulus-attach-strip {',
302
+ ' display: flex; flex-direction: row;',
303
+ ' gap: 0.5em; overflow-x: auto;',
304
+ ' max-width: 78ch; align-self: center; width: 100%;',
305
+ ' padding-bottom: 0.1em;',
306
+ '}',
307
+ '.cumulus-attach-strip::-webkit-scrollbar { height: 4px; }',
308
+ '.cumulus-attach-strip::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }',
309
+
310
+ /* Attachment chip */
311
+ '.cumulus-attach-chip {',
312
+ ' position: relative;',
313
+ ' display: flex; flex-direction: column; align-items: center;',
314
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
315
+ ' border-radius: 0.4em; padding: 0.3em;',
316
+ ' flex-shrink: 0; width: 5.5em; overflow: hidden;',
317
+ '}',
318
+ '.cumulus-attach-chip-thumb {',
319
+ ' width: 3em; height: 3em; object-fit: cover;',
320
+ ' border-radius: 0.25em; display: block;',
321
+ '}',
322
+ '.cumulus-attach-chip-icon {',
323
+ ' width: 3em; height: 3em;',
324
+ ' background: #2a2a2a; border-radius: 0.25em;',
325
+ ' display: flex; align-items: center; justify-content: center;',
326
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
327
+ ' font-size: 0.65em; color: #aaa; text-transform: uppercase;',
328
+ '}',
329
+ '.cumulus-attach-chip-name {',
330
+ ' font-size: 0.72em; color: #ccc;',
331
+ ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis;',
332
+ ' width: 100%; text-align: center; margin-top: 0.2em;',
333
+ '}',
334
+ '.cumulus-attach-chip-remove {',
335
+ ' position: absolute; top: 0.1em; right: 0.15em;',
336
+ ' background: none; border: none; color: #888;',
337
+ ' font-size: 0.85em; line-height: 1; cursor: pointer; padding: 0 0.1em;',
338
+ '}',
339
+ '.cumulus-attach-chip-remove:hover { color: #ff6666; }',
340
+
341
+ /* Input row (attach btn + textarea + send btn) */
342
+ '.cumulus-input-row {',
343
+ ' display: flex; flex-direction: row;',
344
+ ' gap: 0.5em; align-items: flex-end;',
345
+ ' max-width: 78ch; align-self: center; width: 100%;',
346
+ '}',
347
+ '.cumulus-attach-btn {',
348
+ ' width: 2.7em; height: 2.7em; flex-shrink: 0;',
349
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
350
+ ' border-radius: 0.55em; color: #aaa;',
351
+ ' font-size: 1.4em; line-height: 1; cursor: pointer;',
352
+ ' display: flex; align-items: center; justify-content: center;',
353
+ ' padding: 0;',
354
+ '}',
355
+ '.cumulus-attach-btn:hover { border-color: #0066cc; color: #ddd; }',
356
+
357
+ '.cumulus-input {',
358
+ ' flex: 1;',
359
+ ' background: #3d3d3d;',
360
+ ' border: 1px solid #4a4a4a;',
361
+ ' border-radius: 0.55em;',
362
+ ' color: #e0e0e0;',
363
+ ' padding: 0.55em 0.75em;',
364
+ ' font-size: 14px;',
365
+ ' font-family: inherit;',
366
+ ' resize: none;',
367
+ ' min-height: 2.7em;',
368
+ ' max-height: 120px;',
369
+ ' outline: none;',
370
+ ' line-height: 1.5;',
371
+ '}',
372
+ '.cumulus-input:focus { border-color: #0066cc; }',
373
+ '.cumulus-input::placeholder { color: #666; }',
374
+
375
+ '.cumulus-send-btn {',
376
+ ' background: #0066cc;',
377
+ ' border: none;',
378
+ ' border-radius: 0.55em;',
379
+ ' color: white;',
380
+ ' padding: 0 1em;',
381
+ ' height: 2.7em;',
382
+ ' flex-shrink: 0;',
383
+ ' cursor: pointer;',
384
+ ' font-size: 14px;',
385
+ ' font-weight: 600;',
386
+ ' font-family: inherit;',
387
+ ' white-space: nowrap;',
388
+ '}',
389
+ '.cumulus-send-btn:hover:not(:disabled) { background: #0077ee; }',
390
+ '.cumulus-send-btn:disabled { background: #334455; color: #668; cursor: not-allowed; }',
391
+ '.cumulus-send-btn.stop { background: #8b2222; color: #ffcccc; }',
392
+ '.cumulus-send-btn.stop:hover:not(:disabled) { background: #a02828; }',
393
+
394
+ /* ── Auth panel ── */
395
+ '.cumulus-auth {',
396
+ ' display: flex; flex-direction: column;',
397
+ ' align-items: center; justify-content: center;',
398
+ ' flex: 1; padding: 2em 2em; gap: 1em;',
399
+ '}',
400
+ '.cumulus-auth-title { font-size: 20px; font-weight: 600; color: #e0e0e0; }',
401
+ '.cumulus-auth-subtitle { color: #888; font-size: 13px; text-align: center; max-width: 280px; }',
402
+ '.cumulus-auth-input {',
403
+ ' width: 100%; max-width: 320px;',
404
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
405
+ ' border-radius: 0.55em; color: #e0e0e0;',
406
+ ' padding: 0.7em 0.85em; font-size: 14px;',
407
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
408
+ ' outline: none;',
409
+ '}',
410
+ '.cumulus-auth-input:focus { border-color: #0066cc; }',
411
+ '.cumulus-auth-input::placeholder { color: #555; }',
412
+ '.cumulus-auth-btn {',
413
+ ' background: #0066cc; border: none; border-radius: 0.55em;',
414
+ ' color: white; padding: 0.6em 2em;',
415
+ ' cursor: pointer; font-size: 14px; font-weight: 600; font-family: inherit;',
416
+ '}',
417
+ '.cumulus-auth-btn:hover { background: #0077ee; }',
418
+ '.cumulus-auth-btn:disabled { background: #334455; color: #668; cursor: not-allowed; }',
419
+ '.cumulus-auth-error { color: #ef4444; font-size: 13px; text-align: center; min-height: 1.2em; }',
420
+ '.cumulus-auth-logout {',
421
+ ' background: none; border: none; color: #666;',
422
+ ' cursor: pointer; font-size: 11px; padding: 2px 6px; font-family: inherit;',
423
+ '}',
424
+ '.cumulus-auth-logout:hover { color: #ef4444; }',
425
+ ].join('\n');
426
+
427
+ // ─── HTML Escaping ───────────────────────────────────────────────────────────
428
+ function escapeHtml(text) {
429
+ var div = document.createElement('div');
430
+ div.textContent = text;
431
+ return div.innerHTML;
432
+ }
56
433
 
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
- }
434
+ // ─── Markdown Renderer ───────────────────────────────────────────────────────
435
+ // Uses a placeholder strategy: extract code blocks first, render everything
436
+ // else, then reinsert code blocks. This prevents markdown patterns inside
437
+ // code from being processed.
102
438
 
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
- }
439
+ function renderMarkdown(text) {
440
+ // Phase 1: Extract fenced code blocks — replace with unique tokens
441
+ var codeBlocks = [];
442
+ var TOKEN_PREFIX = '\x00CODEBLOCK_';
443
+ var processed = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, function (_, lang, code) {
444
+ var idx = codeBlocks.length;
445
+ var langLabel = lang.trim() || 'text';
446
+ var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
447
+ var tokenId = 'cb' + idx;
448
+ codeBlocks.push(buildCodeBlock(langLabel, escapedCode, tokenId));
449
+ return TOKEN_PREFIX + idx + '\x00';
450
+ });
176
451
 
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
- }
452
+ // Phase 2: Escape HTML in the non-code portions
453
+ // We need to escape line by line between tokens
454
+ var parts = processed.split(/(\x00CODEBLOCK_\d+\x00)/);
455
+ var escapedParts = parts.map(function (part) {
456
+ if (/^\x00CODEBLOCK_\d+\x00$/.test(part)) return part; // keep token as-is
457
+ return escapeHtml(part);
458
+ });
459
+ var html = escapedParts.join('');
196
460
 
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
- }
461
+ // Phase 3: Process tables (before other inline markdown)
462
+ html = renderTables(html);
270
463
 
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
- `;
464
+ // Phase 4: Process block-level markdown elements
465
+ // Horizontal rules
466
+ html = html.replace(/^[ \t]*(---+|___+|\*\*\*+)[ \t]*$/gm, '<hr>');
333
467
 
334
- // ─── Simple Markdown Renderer ───────────────────────────────
335
- function renderMarkdown(text) {
336
- let html = escapeHtml(text);
468
+ // Headers
469
+ html = html.replace(/^######[ \t]+(.+)$/gm, '<h6>$1</h6>');
470
+ html = html.replace(/^#####[ \t]+(.+)$/gm, '<h5>$1</h5>');
471
+ html = html.replace(/^####[ \t]+(.+)$/gm, '<h4>$1</h4>');
472
+ html = html.replace(/^###[ \t]+(.+)$/gm, '<h3>$1</h3>');
473
+ html = html.replace(/^##[ \t]+(.+)$/gm, '<h2>$1</h2>');
474
+ html = html.replace(/^#[ \t]+(.+)$/gm, '<h1>$1</h1>');
337
475
 
338
- // Code blocks (``` ... ```)
339
- html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
340
- return '<pre><code>' + code + '</code></pre>';
341
- });
476
+ // Blockquotes (simple single-level)
477
+ html = renderBlockquotes(html);
342
478
 
343
- // Inline code
344
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
479
+ // Lists
480
+ html = renderLists(html);
345
481
 
346
- // Bold
482
+ // Phase 5: Inline markdown
483
+ // Bold (** and __)
347
484
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
485
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
348
486
 
349
- // Italic
350
- html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
487
+ // Italic (* and _) — careful not to match inside words for _
488
+ html = html.replace(/\*(?!\s)(.+?)(?<!\s)\*/g, '<em>$1</em>');
489
+ html = html.replace(/(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)/g, '<em>$1</em>');
490
+
491
+ // Strikethrough
492
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
493
+
494
+ // Inline code (backtick)
495
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
351
496
 
352
497
  // Links
353
498
  html = html.replace(
354
499
  /\[([^\]]+)\]\(([^)]+)\)/g,
355
- '<a href="$2" target="_blank" rel="noopener">$1</a>'
500
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
356
501
  );
357
502
 
358
- // Line breaks
359
- html = html.replace(/\n/g, '<br>');
503
+ // Phase 6: Convert remaining newlines to <br> (skip block-level elements)
504
+ // Wrap sequences of plain text lines in <p> tags
505
+ html = renderParagraphs(html);
506
+
507
+ // Phase 7: Reinsert code blocks
508
+ html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, function (_, idx) {
509
+ return codeBlocks[parseInt(idx, 10)];
510
+ });
360
511
 
361
512
  return html;
362
513
  }
363
514
 
364
- function escapeHtml(text) {
365
- const div = document.createElement('div');
366
- div.textContent = text;
367
- return div.innerHTML;
515
+ function buildCodeBlock(lang, escapedCode, tokenId) {
516
+ return (
517
+ '<div class="code-block-wrapper">' +
518
+ '<div class="code-block-header">' +
519
+ '<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
520
+ '<button class="code-block-copy-btn" data-copy-target="' + tokenId + '" data-testid="webchat-copy-code">Copy</button>' +
521
+ '</div>' +
522
+ '<pre><code data-code-id="' + tokenId + '">' + escapedCode + '</code></pre>' +
523
+ '</div>'
524
+ );
525
+ }
526
+
527
+ function renderTables(html) {
528
+ // Split on double newlines to find table blocks
529
+ // A table block: lines where most lines start with |
530
+ var lines = html.split('\n');
531
+ var result = [];
532
+ var i = 0;
533
+ while (i < lines.length) {
534
+ var line = lines[i];
535
+ // Check if this line looks like a table row
536
+ if (/^\|.+\|/.test(line.trim())) {
537
+ // Collect contiguous table lines
538
+ var tableLines = [];
539
+ while (i < lines.length && /^\|/.test(lines[i].trim())) {
540
+ tableLines.push(lines[i]);
541
+ i++;
542
+ }
543
+ result.push(buildTable(tableLines));
544
+ } else {
545
+ result.push(line);
546
+ i++;
547
+ }
548
+ }
549
+ return result.join('\n');
550
+ }
551
+
552
+ function buildTable(lines) {
553
+ // lines[0] = header row, lines[1] = separator row (|---|), lines[2+] = data rows
554
+ if (lines.length < 2) return lines.join('\n');
555
+
556
+ var isSepRow = /^\|[\s|:-]+\|$/.test(lines[1].trim());
557
+ if (!isSepRow) return lines.join('\n');
558
+
559
+ var headerCells = parseTableRow(lines[0]);
560
+ var dataRows = lines.slice(2);
561
+
562
+ var thead = '<thead><tr>' + headerCells.map(function (c) {
563
+ return '<th>' + c + '</th>';
564
+ }).join('') + '</tr></thead>';
565
+
566
+ var tbody = '<tbody>' + dataRows.map(function (row) {
567
+ var cells = parseTableRow(row);
568
+ return '<tr>' + cells.map(function (c) {
569
+ return '<td>' + c + '</td>';
570
+ }).join('') + '</tr>';
571
+ }).join('') + '</tbody>';
572
+
573
+ return '<table>' + thead + tbody + '</table>';
574
+ }
575
+
576
+ function parseTableRow(line) {
577
+ // Split on | and trim, filter empty first/last from leading/trailing |
578
+ var parts = line.split('|');
579
+ // Remove first and last if empty (from leading/trailing |)
580
+ if (parts.length > 0 && parts[0].trim() === '') parts = parts.slice(1);
581
+ if (parts.length > 0 && parts[parts.length - 1].trim() === '') parts = parts.slice(0, -1);
582
+ return parts.map(function (c) { return c.trim(); });
583
+ }
584
+
585
+ function renderBlockquotes(html) {
586
+ // Find consecutive lines starting with &gt; (escaped >)
587
+ var lines = html.split('\n');
588
+ var result = [];
589
+ var i = 0;
590
+ while (i < lines.length) {
591
+ if (/^&gt;[ \t]?/.test(lines[i])) {
592
+ var bqLines = [];
593
+ while (i < lines.length && /^&gt;[ \t]?/.test(lines[i])) {
594
+ bqLines.push(lines[i].replace(/^&gt;[ \t]?/, ''));
595
+ i++;
596
+ }
597
+ result.push('<blockquote>' + bqLines.join('<br>') + '</blockquote>');
598
+ } else {
599
+ result.push(lines[i]);
600
+ i++;
601
+ }
602
+ }
603
+ return result.join('\n');
604
+ }
605
+
606
+ function renderLists(html) {
607
+ // Process unordered then ordered lists
608
+ // Match blocks of consecutive list lines
609
+ var lines = html.split('\n');
610
+ var result = [];
611
+ var i = 0;
612
+
613
+ while (i < lines.length) {
614
+ // Unordered list item: "- " or "* " or "+ " (with optional indent)
615
+ if (/^[ \t]*[-*+][ \t]+/.test(lines[i])) {
616
+ var listLines = [];
617
+ while (i < lines.length && /^[ \t]*[-*+][ \t]+/.test(lines[i])) {
618
+ listLines.push(lines[i].replace(/^[ \t]*[-*+][ \t]+/, ''));
619
+ i++;
620
+ }
621
+ result.push('<ul>' + listLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ul>');
622
+ }
623
+ // Ordered list item: "1. " "2. " etc.
624
+ else if (/^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
625
+ var olLines = [];
626
+ while (i < lines.length && /^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
627
+ olLines.push(lines[i].replace(/^[ \t]*\d+\.[ \t]+/, ''));
628
+ i++;
629
+ }
630
+ result.push('<ol>' + olLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ol>');
631
+ }
632
+ else {
633
+ result.push(lines[i]);
634
+ i++;
635
+ }
636
+ }
637
+ return result.join('\n');
638
+ }
639
+
640
+ function renderParagraphs(html) {
641
+ // Split on double newlines; wrap plain-text segments in <p> while preserving
642
+ // existing block-level HTML elements.
643
+ // Strategy: split into chunks on blank lines (two+ newlines), then for each
644
+ // chunk: if it starts with a block-level tag, keep as-is; else wrap in <p>.
645
+ var blockTags = /^<(h[1-6]|hr|ul|ol|blockquote|table|div|pre)\b/i;
646
+
647
+ var chunks = html.split(/\n{2,}/);
648
+ var parts = chunks.map(function (chunk) {
649
+ chunk = chunk.trim();
650
+ if (!chunk) return '';
651
+ if (blockTags.test(chunk)) return chunk;
652
+ // It's a text/inline-html chunk — convert single newlines to <br>
653
+ // but only outside of block tags within the chunk
654
+ var inner = chunk.replace(/\n/g, '<br>');
655
+ return '<p>' + inner + '</p>';
656
+ });
657
+
658
+ return parts.filter(function (p) { return p !== ''; }).join('\n');
659
+ }
660
+
661
+ // ─── Attachment helpers ──────────────────────────────────────────────────────
662
+ function fileExtension(name) {
663
+ var parts = name.split('.');
664
+ return parts.length > 1 ? parts[parts.length - 1].slice(0, 5) : 'file';
665
+ }
666
+
667
+ function readFileAsBase64(file) {
668
+ return new Promise(function (resolve, reject) {
669
+ var reader = new FileReader();
670
+ reader.onload = function (e) {
671
+ // result is "data:mime/type;base64,XXXX" — strip the prefix
672
+ var result = e.target.result;
673
+ var comma = result.indexOf(',');
674
+ resolve({ base64: result.slice(comma + 1), mimeType: file.type || 'application/octet-stream', name: file.name });
675
+ };
676
+ reader.onerror = reject;
677
+ reader.readAsDataURL(file);
678
+ });
679
+ }
680
+
681
+ function readFileAsDataUrl(file) {
682
+ return new Promise(function (resolve, reject) {
683
+ var reader = new FileReader();
684
+ reader.onload = function (e) { resolve(e.target.result); };
685
+ reader.onerror = reject;
686
+ reader.readAsDataURL(file);
687
+ });
368
688
  }
369
689
 
370
- // ─── WebSocket Connection ───────────────────────────────────
690
+ // ─── WebSocket Connection ────────────────────────────────────────────────────
371
691
  function createConnection(opts) {
372
- const { wsUrl, apiKey, sessionId, onMessage, onStatus } = opts;
373
- let ws = null;
374
- let reconnectTimer = null;
375
- let reconnectDelay = 1000;
692
+ var wsUrl = opts.wsUrl;
693
+ var apiKey = opts.apiKey;
694
+ var sessionId = opts.sessionId;
695
+ var onMessage = opts.onMessage;
696
+ var onStatus = opts.onStatus;
697
+
698
+ var ws = null;
699
+ var reconnectTimer = null;
700
+ var reconnectDelay = 1000;
701
+ var destroyed = false;
376
702
 
377
703
  function connect() {
704
+ if (destroyed) return;
378
705
  onStatus('connecting');
379
706
  ws = new WebSocket(wsUrl);
380
707
 
381
708
  ws.onopen = function () {
709
+ if (destroyed) { ws.close(); return; }
382
710
  onStatus('connected');
383
711
  reconnectDelay = 1000;
384
-
385
- // Authenticate
386
712
  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
- );
713
+ ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
396
714
  };
397
715
 
398
716
  ws.onmessage = function (event) {
@@ -405,17 +723,18 @@
405
723
  };
406
724
 
407
725
  ws.onclose = function () {
726
+ if (destroyed) return;
408
727
  onStatus('disconnected');
409
728
  scheduleReconnect();
410
729
  };
411
730
 
412
731
  ws.onerror = function () {
413
- // onclose will fire after this
732
+ // onclose fires after onerror
414
733
  };
415
734
  }
416
735
 
417
736
  function scheduleReconnect() {
418
- if (reconnectTimer) return;
737
+ if (reconnectTimer || destroyed) return;
419
738
  reconnectTimer = setTimeout(function () {
420
739
  reconnectTimer = null;
421
740
  reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
@@ -430,12 +749,13 @@
430
749
  }
431
750
 
432
751
  function close() {
752
+ destroyed = true;
433
753
  if (reconnectTimer) {
434
754
  clearTimeout(reconnectTimer);
435
755
  reconnectTimer = null;
436
756
  }
437
757
  if (ws) {
438
- ws.onclose = null; // Prevent reconnect
758
+ ws.onclose = null;
439
759
  ws.close();
440
760
  ws = null;
441
761
  }
@@ -445,52 +765,54 @@
445
765
  return { send: send, close: close };
446
766
  }
447
767
 
448
- // ─── Widget UI ──────────────────────────────────────────────
768
+ // ─── Widget ──────────────────────────────────────────────────────────────────
449
769
  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
770
+ var standalone = opts.standalone;
771
+ var sessionId = getSessionId();
772
+ var wsUrl = getWsUrl();
454
773
  var activeApiKey = standalone ? getStoredApiKey() : API_KEY;
455
774
 
456
775
  // Inject styles
457
- const styleEl = document.createElement('style');
776
+ var styleEl = document.createElement('style');
458
777
  styleEl.textContent = STYLES;
459
778
  document.head.appendChild(styleEl);
460
779
 
461
- // State
462
- var messages = [];
780
+ // ── State ──
781
+ var messages = []; // { role, content, attachments? }
463
782
  var streaming = false;
783
+ var stopRequested = false;
464
784
  var streamBuffer = '';
465
785
  var connection = null;
466
786
  var connectionStatus = 'disconnected';
467
787
  var authenticated = false;
788
+ var pendingAttachments = []; // { base64, mimeType, name, dataUrl, isImage }
468
789
 
469
- // ─── DOM elements ──────────────────────────────
470
- const container = document.createElement('div');
790
+ // ── Root container ──
791
+ var container = document.createElement('div');
471
792
  container.className = 'cumulus-widget';
472
793
 
473
- // Build panel
474
- const panel = document.createElement('div');
794
+ // ── Panel ──
795
+ var panel = document.createElement('div');
475
796
  panel.className = 'cumulus-panel ' + (standalone ? 'standalone' : 'floating');
476
797
  panel.style.display = standalone ? 'flex' : 'none';
798
+ panel.setAttribute('data-testid', 'webchat-panel');
477
799
 
478
- // Header
479
- const header = document.createElement('div');
800
+ // ── Header ──
801
+ var header = document.createElement('div');
480
802
  header.className = 'cumulus-header';
481
803
  header.innerHTML =
482
804
  '<span class="cumulus-header-title">Cumulus</span>' +
483
805
  '<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>' +
806
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
807
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
486
808
  '</span>' +
487
809
  (standalone
488
810
  ? '<button class="cumulus-auth-logout" data-testid="webchat-logout" style="display:none">Logout</button>'
489
811
  : '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>');
490
812
  panel.appendChild(header);
491
813
 
492
- // Auth panel (standalone mode — shown when no API key)
493
- const authPanel = document.createElement('div');
814
+ // ── Auth panel ──
815
+ var authPanel = document.createElement('div');
494
816
  authPanel.className = 'cumulus-auth';
495
817
  authPanel.setAttribute('data-testid', 'webchat-auth');
496
818
  authPanel.innerHTML =
@@ -501,34 +823,80 @@
501
823
  '<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
502
824
  panel.appendChild(authPanel);
503
825
 
504
- // Messages area
505
- const messagesEl = document.createElement('div');
826
+ // ── Messages area ──
827
+ var messagesEl = document.createElement('div');
506
828
  messagesEl.className = 'cumulus-messages';
507
829
  messagesEl.setAttribute('data-testid', 'webchat-messages');
508
830
  messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
509
831
  messagesEl.style.display = 'none';
510
832
  panel.appendChild(messagesEl);
511
833
 
512
- // Input area
513
- const inputArea = document.createElement('div');
834
+ // ── Input area ──
835
+ var inputArea = document.createElement('div');
514
836
  inputArea.className = 'cumulus-input-area';
515
837
  inputArea.style.display = 'none';
516
- const input = document.createElement('textarea');
838
+
839
+ // Attachment strip (conditionally shown)
840
+ var attachStrip = document.createElement('div');
841
+ attachStrip.className = 'cumulus-attach-strip';
842
+ attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
843
+ attachStrip.style.display = 'none';
844
+ inputArea.appendChild(attachStrip);
845
+
846
+ // Input row
847
+ var inputRow = document.createElement('div');
848
+ inputRow.className = 'cumulus-input-row';
849
+
850
+ // Hidden file input
851
+ var fileInput = document.createElement('input');
852
+ fileInput.type = 'file';
853
+ fileInput.multiple = true;
854
+ fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
855
+ fileInput.style.display = 'none';
856
+ fileInput.setAttribute('data-testid', 'webchat-file-input');
857
+
858
+ // Attach button
859
+ var attachBtn = document.createElement('button');
860
+ attachBtn.className = 'cumulus-attach-btn';
861
+ attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
862
+ attachBtn.setAttribute('title', 'Attach files');
863
+ attachBtn.textContent = '+';
864
+
865
+ // Textarea
866
+ var input = document.createElement('textarea');
517
867
  input.className = 'cumulus-input';
518
868
  input.setAttribute('data-testid', 'webchat-input');
519
- input.placeholder = 'Type a message...';
869
+ input.placeholder = 'Type a message';
520
870
  input.rows = 1;
521
- const sendBtn = document.createElement('button');
871
+
872
+ // Send/Stop button
873
+ var sendBtn = document.createElement('button');
522
874
  sendBtn.className = 'cumulus-send-btn';
523
875
  sendBtn.setAttribute('data-testid', 'webchat-send');
524
876
  sendBtn.textContent = 'Send';
525
- inputArea.appendChild(input);
526
- inputArea.appendChild(sendBtn);
877
+
878
+ inputRow.appendChild(fileInput);
879
+ inputRow.appendChild(attachBtn);
880
+ inputRow.appendChild(input);
881
+ inputRow.appendChild(sendBtn);
882
+ inputArea.appendChild(inputRow);
527
883
  panel.appendChild(inputArea);
528
884
 
529
885
  container.appendChild(panel);
530
886
 
531
- // ─── Auth/Chat view switching ────────────────────
887
+ // ── Floating bubble (embedded mode only) ──
888
+ var bubble = null;
889
+ if (!standalone) {
890
+ bubble = document.createElement('button');
891
+ bubble.className = 'cumulus-bubble';
892
+ bubble.setAttribute('data-testid', 'webchat-bubble');
893
+ bubble.setAttribute('title', 'Open chat');
894
+ bubble.innerHTML =
895
+ '<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>';
896
+ container.appendChild(bubble);
897
+ }
898
+
899
+ // ── View switching ──
532
900
  function showAuthView() {
533
901
  authenticated = false;
534
902
  authPanel.style.display = 'flex';
@@ -550,22 +918,77 @@
550
918
  input.focus();
551
919
  }
552
920
 
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 ─────────────────────────────────
921
+ // ── Rendering ──
565
922
  function scrollToBottom() {
566
923
  messagesEl.scrollTop = messagesEl.scrollHeight;
567
924
  }
568
925
 
926
+ function isWideUserMessage(content) {
927
+ // Wide if it contains a code fence or has more than 3 lines
928
+ if (/```/.test(content)) return true;
929
+ var lineCount = (content.match(/\n/g) || []).length + 1;
930
+ return lineCount > 3;
931
+ }
932
+
933
+ function buildUserMsgEl(msg) {
934
+ var el = document.createElement('div');
935
+ el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
936
+ el.textContent = msg.content;
937
+
938
+ if (msg.attachments && msg.attachments.length > 0) {
939
+ var attRow = document.createElement('div');
940
+ attRow.className = 'cumulus-msg-attachments';
941
+ msg.attachments.forEach(function (att) {
942
+ if (att.isImage) {
943
+ var img = document.createElement('img');
944
+ img.className = 'cumulus-msg-img';
945
+ img.src = att.dataUrl;
946
+ img.alt = att.name;
947
+ img.setAttribute('data-testid', 'webchat-msg-img');
948
+ attRow.appendChild(img);
949
+ } else {
950
+ var badge = document.createElement('span');
951
+ badge.className = 'cumulus-msg-file-badge';
952
+ badge.setAttribute('data-testid', 'webchat-msg-file');
953
+ badge.textContent = '\uD83D\uDCCE ' + att.name; // 📎
954
+ attRow.appendChild(badge);
955
+ }
956
+ });
957
+ el.appendChild(attRow);
958
+ }
959
+ return el;
960
+ }
961
+
962
+ function buildAssistantMsgEl(content, isStreaming) {
963
+ var el = document.createElement('div');
964
+ el.className = 'cumulus-msg assistant';
965
+ if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
966
+ if (content) {
967
+ el.innerHTML = renderMarkdown(content);
968
+ if (isStreaming) {
969
+ el.innerHTML += '<span class="cumulus-cursor"></span>';
970
+ }
971
+ // Wire up copy buttons
972
+ el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
973
+ btn.addEventListener('click', function () {
974
+ var targetId = btn.getAttribute('data-copy-target');
975
+ var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
976
+ if (codeEl && navigator.clipboard) {
977
+ navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
978
+ btn.textContent = 'Copied!';
979
+ setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
980
+ });
981
+ }
982
+ });
983
+ });
984
+ } else if (isStreaming) {
985
+ el.innerHTML =
986
+ '<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
987
+ '<span class="cumulus-cursor"></span>';
988
+ }
989
+ return el;
990
+ }
991
+
569
992
  function renderMessages() {
570
993
  messagesEl.innerHTML = '';
571
994
 
@@ -576,47 +999,116 @@
576
999
 
577
1000
  for (var i = 0; i < messages.length; i++) {
578
1001
  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);
1002
+ var row = document.createElement('div');
1003
+ row.className = 'cumulus-msg-row';
1004
+
1005
+ if (msg.role === 'user') {
1006
+ row.appendChild(buildUserMsgEl(msg));
583
1007
  } else {
584
- el.textContent = msg.content;
1008
+ row.appendChild(buildAssistantMsgEl(msg.content, false));
585
1009
  }
586
- messagesEl.appendChild(el);
1010
+ messagesEl.appendChild(row);
587
1011
  }
588
1012
 
589
- // Streaming indicator
590
1013
  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);
1014
+ var row = document.createElement('div');
1015
+ row.className = 'cumulus-msg-row';
1016
+ row.appendChild(buildAssistantMsgEl(streamBuffer, true));
1017
+ messagesEl.appendChild(row);
602
1018
  }
603
1019
 
604
1020
  scrollToBottom();
605
1021
  }
606
1022
 
1023
+ function updateSendBtn() {
1024
+ if (streaming) {
1025
+ sendBtn.textContent = 'Stop';
1026
+ sendBtn.classList.add('stop');
1027
+ sendBtn.disabled = false;
1028
+ } else {
1029
+ sendBtn.textContent = 'Send';
1030
+ sendBtn.classList.remove('stop');
1031
+ sendBtn.disabled = false;
1032
+ }
1033
+ }
1034
+
607
1035
  function updateStatus(status) {
608
1036
  connectionStatus = status;
609
1037
  var dot = panel.querySelector('.cumulus-status-dot');
610
1038
  var text = panel.querySelector('[data-testid="webchat-status-text"]');
611
- if (dot) {
612
- dot.className = 'cumulus-status-dot ' + status;
1039
+ if (dot) dot.className = 'cumulus-status-dot ' + status;
1040
+ if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1041
+ }
1042
+
1043
+ // ── Attachment strip ──
1044
+ function renderAttachStrip() {
1045
+ attachStrip.innerHTML = '';
1046
+ if (pendingAttachments.length === 0) {
1047
+ attachStrip.style.display = 'none';
1048
+ return;
613
1049
  }
614
- if (text) {
615
- text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1050
+ attachStrip.style.display = 'flex';
1051
+ pendingAttachments.forEach(function (att, idx) {
1052
+ var chip = document.createElement('div');
1053
+ chip.className = 'cumulus-attach-chip';
1054
+ chip.setAttribute('data-testid', 'webchat-attach-chip');
1055
+
1056
+ if (att.isImage) {
1057
+ var thumb = document.createElement('img');
1058
+ thumb.className = 'cumulus-attach-chip-thumb';
1059
+ thumb.src = att.dataUrl;
1060
+ thumb.alt = att.name;
1061
+ chip.appendChild(thumb);
1062
+ } else {
1063
+ var icon = document.createElement('div');
1064
+ icon.className = 'cumulus-attach-chip-icon';
1065
+ icon.textContent = fileExtension(att.name);
1066
+ chip.appendChild(icon);
1067
+ }
1068
+
1069
+ var nameEl = document.createElement('div');
1070
+ nameEl.className = 'cumulus-attach-chip-name';
1071
+ nameEl.textContent = att.name;
1072
+ chip.appendChild(nameEl);
1073
+
1074
+ var removeBtn = document.createElement('button');
1075
+ removeBtn.className = 'cumulus-attach-chip-remove';
1076
+ removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
1077
+ removeBtn.textContent = '\xD7'; // ×
1078
+ removeBtn.setAttribute('title', 'Remove attachment');
1079
+ (function (index) {
1080
+ removeBtn.addEventListener('click', function () {
1081
+ pendingAttachments.splice(index, 1);
1082
+ renderAttachStrip();
1083
+ });
1084
+ })(idx);
1085
+
1086
+ chip.appendChild(removeBtn);
1087
+ attachStrip.appendChild(chip);
1088
+ });
1089
+ }
1090
+
1091
+ async function addFilesToPending(files) {
1092
+ for (var i = 0; i < files.length; i++) {
1093
+ var file = files[i];
1094
+ try {
1095
+ var info = await readFileAsBase64(file);
1096
+ var dataUrl = await readFileAsDataUrl(file);
1097
+ pendingAttachments.push({
1098
+ base64: info.base64,
1099
+ mimeType: info.mimeType,
1100
+ name: file.name,
1101
+ dataUrl: dataUrl,
1102
+ isImage: file.type.startsWith('image/'),
1103
+ });
1104
+ } catch (e) {
1105
+ console.error('[Cumulus] Failed to read file:', file.name, e);
1106
+ }
616
1107
  }
1108
+ renderAttachStrip();
617
1109
  }
618
1110
 
619
- // ─── Message handling ──────────────────────────
1111
+ // ── Message handling ──
620
1112
  function handleServerMessage(data) {
621
1113
  switch (data.type) {
622
1114
  case 'auth_ok':
@@ -625,7 +1117,6 @@
625
1117
 
626
1118
  case 'auth_error':
627
1119
  console.error('[Cumulus] Auth failed:', data.error);
628
- // If we had a stored key and it failed, show auth panel again
629
1120
  if (standalone) {
630
1121
  clearStoredApiKey();
631
1122
  activeApiKey = '';
@@ -646,57 +1137,103 @@
646
1137
  break;
647
1138
 
648
1139
  case 'token':
1140
+ if (stopRequested) break; // discard after stop
649
1141
  if (!streaming) {
650
1142
  streaming = true;
651
1143
  streamBuffer = '';
1144
+ updateSendBtn();
652
1145
  }
653
1146
  streamBuffer += data.text;
654
1147
  renderMessages();
655
1148
  break;
656
1149
 
657
1150
  case 'segment':
658
- // Tool use/resultcould show in verbose mode later
1151
+ // Tool use/segment info reserved for future verbose display
659
1152
  break;
660
1153
 
661
1154
  case 'done':
1155
+ if (stopRequested) {
1156
+ // Stopped by user — finalize with what we have
1157
+ stopRequested = false;
1158
+ streaming = false;
1159
+ if (streamBuffer) {
1160
+ messages.push({ role: 'assistant', content: streamBuffer });
1161
+ }
1162
+ streamBuffer = '';
1163
+ updateSendBtn();
1164
+ renderMessages();
1165
+ break;
1166
+ }
662
1167
  streaming = false;
663
1168
  messages.push({ role: 'assistant', content: data.response || streamBuffer });
664
1169
  streamBuffer = '';
1170
+ updateSendBtn();
665
1171
  renderMessages();
666
1172
  break;
667
1173
 
668
1174
  case 'error':
1175
+ stopRequested = false;
669
1176
  streaming = false;
670
1177
  if (streamBuffer) {
671
- messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + data.error + ']' });
1178
+ messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
672
1179
  } else {
673
- messages.push({ role: 'assistant', content: '[Error: ' + data.error + ']' });
1180
+ messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
674
1181
  }
675
1182
  streamBuffer = '';
1183
+ updateSendBtn();
676
1184
  renderMessages();
677
1185
  break;
678
1186
  }
679
1187
  }
680
1188
 
681
1189
  function sendMessage() {
1190
+ // If streaming, treat as Stop
1191
+ if (streaming) {
1192
+ stopRequested = true;
1193
+ streaming = false;
1194
+ updateSendBtn();
1195
+ // Finalize current buffer
1196
+ if (streamBuffer) {
1197
+ messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
1198
+ streamBuffer = '';
1199
+ }
1200
+ renderMessages();
1201
+ return;
1202
+ }
1203
+
682
1204
  var text = input.value.trim();
683
- if (!text || streaming) return;
1205
+ if (!text && pendingAttachments.length === 0) return;
1206
+ if (!connection) return;
684
1207
 
685
- messages.push({ role: 'user', content: text });
1208
+ var attachSnapshot = pendingAttachments.slice();
1209
+ var displayText = text || '(attachment)';
1210
+
1211
+ messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
686
1212
  input.value = '';
687
1213
  input.style.height = 'auto';
1214
+ pendingAttachments = [];
1215
+ renderAttachStrip();
688
1216
  streaming = true;
1217
+ stopRequested = false;
689
1218
  streamBuffer = '';
1219
+ updateSendBtn();
690
1220
  renderMessages();
691
1221
 
692
- connection.send({
1222
+ var imagePayload = attachSnapshot
1223
+ .filter(function (a) { return a.isImage; })
1224
+ .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
1225
+
1226
+ var payload = {
693
1227
  type: 'message',
694
1228
  threadName: sessionId,
695
- message: text,
696
- });
1229
+ message: text || ' ',
1230
+ };
1231
+ if (imagePayload.length > 0) payload.images = imagePayload;
1232
+
1233
+ connection.send(payload);
697
1234
  }
698
1235
 
699
- // ─── Event listeners ───────────────────────────
1236
+ // ── Event listeners ──
700
1237
  sendBtn.addEventListener('click', sendMessage);
701
1238
 
702
1239
  input.addEventListener('keydown', function (e) {
@@ -706,13 +1243,48 @@
706
1243
  }
707
1244
  });
708
1245
 
709
- // Auto-resize textarea
710
1246
  input.addEventListener('input', function () {
711
1247
  input.style.height = 'auto';
712
1248
  input.style.height = Math.min(input.scrollHeight, 120) + 'px';
713
1249
  });
714
1250
 
715
- // Bubble toggle (embeddable mode)
1251
+ // Paste handler
1252
+ input.addEventListener('paste', function (e) {
1253
+ var items = e.clipboardData && e.clipboardData.items;
1254
+ if (!items) return;
1255
+
1256
+ var hasNonText = false;
1257
+ var filesToAdd = [];
1258
+ for (var i = 0; i < items.length; i++) {
1259
+ var item = items[i];
1260
+ if (item.kind === 'file') {
1261
+ hasNonText = true;
1262
+ var file = item.getAsFile();
1263
+ if (file) filesToAdd.push(file);
1264
+ }
1265
+ }
1266
+
1267
+ if (filesToAdd.length > 0) {
1268
+ e.preventDefault(); // prevent default paste of binary data
1269
+ addFilesToPending(filesToAdd);
1270
+ }
1271
+ // If only text, let default browser paste behavior handle it
1272
+ });
1273
+
1274
+ // File input change
1275
+ fileInput.addEventListener('change', function () {
1276
+ if (fileInput.files && fileInput.files.length > 0) {
1277
+ addFilesToPending(Array.from(fileInput.files));
1278
+ fileInput.value = ''; // reset so same file can be re-added
1279
+ }
1280
+ });
1281
+
1282
+ // Attach button click
1283
+ attachBtn.addEventListener('click', function () {
1284
+ fileInput.click();
1285
+ });
1286
+
1287
+ // Bubble toggle (embedded mode)
716
1288
  if (bubble) {
717
1289
  bubble.addEventListener('click', function () {
718
1290
  var showing = panel.style.display !== 'none';
@@ -726,7 +1298,6 @@
726
1298
  }
727
1299
  });
728
1300
 
729
- // Close button
730
1301
  var closeBtn = panel.querySelector('.cumulus-close-btn');
731
1302
  if (closeBtn) {
732
1303
  closeBtn.addEventListener('click', function () {
@@ -735,9 +1306,9 @@
735
1306
  }
736
1307
  }
737
1308
 
738
- // ─── Auth panel events ─────────────────────────
1309
+ // ── Auth panel events ──
739
1310
  var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
740
- var authBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
1311
+ var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
741
1312
 
742
1313
  function submitAuth() {
743
1314
  var key = authInput.value.trim();
@@ -749,15 +1320,12 @@
749
1320
  startConnection();
750
1321
  }
751
1322
 
752
- authBtn.addEventListener('click', submitAuth);
1323
+ authSubmitBtn.addEventListener('click', submitAuth);
753
1324
  authInput.addEventListener('keydown', function (e) {
754
- if (e.key === 'Enter') {
755
- e.preventDefault();
756
- submitAuth();
757
- }
1325
+ if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
758
1326
  });
759
1327
 
760
- // Logout button (standalone only)
1328
+ // Logout (standalone only)
761
1329
  var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
762
1330
  if (logoutBtn) {
763
1331
  logoutBtn.addEventListener('click', function () {
@@ -766,13 +1334,17 @@
766
1334
  if (connection) { connection.close(); connection = null; }
767
1335
  messages = [];
768
1336
  streaming = false;
1337
+ stopRequested = false;
769
1338
  streamBuffer = '';
1339
+ pendingAttachments = [];
1340
+ renderAttachStrip();
1341
+ updateSendBtn();
770
1342
  showAuthView();
771
1343
  authInput.value = '';
772
1344
  });
773
1345
  }
774
1346
 
775
- // ─── Connect ───────────────────────────────────
1347
+ // ── Connection ──
776
1348
  function startConnection() {
777
1349
  if (connection) { connection.close(); connection = null; }
778
1350
  connection = createConnection({
@@ -784,15 +1356,13 @@
784
1356
  });
785
1357
  }
786
1358
 
787
- // ─── Mount ─────────────────────────────────────
1359
+ // ── Mount / Destroy ──
788
1360
  function mount(target) {
789
1361
  (target || document.body).appendChild(container);
790
1362
  if (standalone) {
791
1363
  if (activeApiKey) {
792
- // Have a stored key — try connecting directly
793
1364
  startConnection();
794
1365
  } else {
795
- // No key — show auth panel
796
1366
  showAuthView();
797
1367
  authInput.focus();
798
1368
  }
@@ -807,11 +1377,9 @@
807
1377
  return { mount: mount, destroy: destroy, send: sendMessage };
808
1378
  }
809
1379
 
810
- // ─── Auto-mount ─────────────────────────────────────────────
1380
+ // ─── Auto-mount ──────────────────────────────────────────────────────────────
811
1381
  if (script) {
812
- var widget = createWidget({
813
- standalone: STANDALONE,
814
- });
1382
+ var widget = createWidget({ standalone: STANDALONE });
815
1383
 
816
1384
  if (document.readyState === 'loading') {
817
1385
  document.addEventListener('DOMContentLoaded', function () {
@@ -821,7 +1389,6 @@
821
1389
  widget.mount();
822
1390
  }
823
1391
 
824
- // Expose for programmatic access
825
1392
  window.CumulusChat = widget;
826
1393
  }
827
1394
  })();