@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.
- package/dist/gateway/static/chat.html +1 -1
- package/dist/gateway/static/widget.js +995 -428
- package/package.json +1 -1
|
@@ -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
|
-
* <
|
|
6
|
+
* < 80KB unminified.
|
|
7
7
|
*/
|
|
8
8
|
(function () {
|
|
9
9
|
'use strict';
|
|
10
10
|
|
|
11
|
-
// ─── Configuration
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
return
|
|
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
|
-
|
|
28
|
-
|
|
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(
|
|
41
|
-
localStorage.setItem('cumulus-api-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
|
-
|
|
49
|
-
.cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
//
|
|
339
|
-
html = html
|
|
340
|
-
return '<pre><code>' + code + '</code></pre>';
|
|
341
|
-
});
|
|
476
|
+
// Blockquotes (simple single-level)
|
|
477
|
+
html = renderBlockquotes(html);
|
|
342
478
|
|
|
343
|
-
//
|
|
344
|
-
html = html
|
|
479
|
+
// Lists
|
|
480
|
+
html = renderLists(html);
|
|
345
481
|
|
|
346
|
-
//
|
|
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
|
-
//
|
|
359
|
-
|
|
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
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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 > (escaped >)
|
|
587
|
+
var lines = html.split('\n');
|
|
588
|
+
var result = [];
|
|
589
|
+
var i = 0;
|
|
590
|
+
while (i < lines.length) {
|
|
591
|
+
if (/^>[ \t]?/.test(lines[i])) {
|
|
592
|
+
var bqLines = [];
|
|
593
|
+
while (i < lines.length && /^>[ \t]?/.test(lines[i])) {
|
|
594
|
+
bqLines.push(lines[i].replace(/^>[ \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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
|
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;
|
|
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
|
|
768
|
+
// ─── Widget ──────────────────────────────────────────────────────────────────
|
|
449
769
|
function createWidget(opts) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
470
|
-
|
|
790
|
+
// ── Root container ──
|
|
791
|
+
var container = document.createElement('div');
|
|
471
792
|
container.className = 'cumulus-widget';
|
|
472
793
|
|
|
473
|
-
//
|
|
474
|
-
|
|
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
|
-
|
|
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
|
-
|
|
485
|
-
|
|
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">×</button>');
|
|
490
812
|
panel.appendChild(header);
|
|
491
813
|
|
|
492
|
-
// Auth panel
|
|
493
|
-
|
|
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
|
-
|
|
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
|
-
|
|
834
|
+
// ── Input area ──
|
|
835
|
+
var inputArea = document.createElement('div');
|
|
514
836
|
inputArea.className = 'cumulus-input-area';
|
|
515
837
|
inputArea.style.display = 'none';
|
|
516
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
-
|
|
1008
|
+
row.appendChild(buildAssistantMsgEl(msg.content, false));
|
|
585
1009
|
}
|
|
586
|
-
messagesEl.appendChild(
|
|
1010
|
+
messagesEl.appendChild(row);
|
|
587
1011
|
}
|
|
588
1012
|
|
|
589
|
-
// Streaming indicator
|
|
590
1013
|
if (streaming) {
|
|
591
|
-
var
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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
|
-
|
|
615
|
-
|
|
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
|
-
//
|
|
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/
|
|
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
|
|
1205
|
+
if (!text && pendingAttachments.length === 0) return;
|
|
1206
|
+
if (!connection) return;
|
|
684
1207
|
|
|
685
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1309
|
+
// ── Auth panel events ──
|
|
739
1310
|
var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
|
|
740
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
})();
|