@luckydraw/cumulus 0.15.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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,841 @@
|
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
.cumulus-panel {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.cumulus-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
}
|
|
117
|
-
.cumulus-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
.cumulus-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
.cumulus-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
.cumulus-msg
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
.
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.
|
|
218
|
-
|
|
219
|
-
.
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
}
|
|
229
|
-
.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
}
|
|
256
|
-
.cumulus-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
.
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
.cumulus-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
.cumulus-
|
|
305
|
-
|
|
306
|
-
.
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
+
|
|
426
|
+
/* ── Standalone layout ── */
|
|
427
|
+
'.cumulus-standalone-root {',
|
|
428
|
+
' display: flex; flex-direction: row;',
|
|
429
|
+
' width: 100%; height: 100vh;',
|
|
430
|
+
' background: #1e1e1e; overflow: hidden;',
|
|
431
|
+
'}',
|
|
432
|
+
|
|
433
|
+
/* ── Sidebar ── */
|
|
434
|
+
'.cumulus-sidebar {',
|
|
435
|
+
' width: 220px; flex-shrink: 0;',
|
|
436
|
+
' background: #252525;',
|
|
437
|
+
' border-right: 1px solid #333;',
|
|
438
|
+
' display: flex; flex-direction: column;',
|
|
439
|
+
' overflow: hidden;',
|
|
440
|
+
'}',
|
|
441
|
+
'.cumulus-sidebar-header {',
|
|
442
|
+
' padding: 10px 12px 6px;',
|
|
443
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
444
|
+
' border-bottom: 1px solid #333;',
|
|
445
|
+
' flex-shrink: 0;',
|
|
446
|
+
'}',
|
|
447
|
+
'.cumulus-sidebar-title {',
|
|
448
|
+
' font-size: 11px; font-weight: 600; color: #888;',
|
|
449
|
+
' text-transform: uppercase; letter-spacing: 0.05em;',
|
|
450
|
+
'}',
|
|
451
|
+
'.cumulus-sidebar-status {',
|
|
452
|
+
' display: flex; align-items: center; gap: 5px;',
|
|
453
|
+
'}',
|
|
454
|
+
'.cumulus-sidebar-scroll {',
|
|
455
|
+
' flex: 1; overflow-y: auto; padding: 6px 0;',
|
|
456
|
+
'}',
|
|
457
|
+
'.cumulus-sidebar-scroll::-webkit-scrollbar { width: 4px; }',
|
|
458
|
+
'.cumulus-sidebar-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }',
|
|
459
|
+
'.cumulus-sidebar-section-label {',
|
|
460
|
+
' font-size: 10px; font-weight: 600; color: #666;',
|
|
461
|
+
' text-transform: uppercase; letter-spacing: 0.06em;',
|
|
462
|
+
' padding: 8px 12px 4px;',
|
|
463
|
+
'}',
|
|
464
|
+
'.cumulus-sidebar-divider {',
|
|
465
|
+
' border: none; border-top: 1px solid #333;',
|
|
466
|
+
' margin: 6px 0;',
|
|
467
|
+
'}',
|
|
468
|
+
'.cumulus-thread-item {',
|
|
469
|
+
' display: flex; align-items: center; gap: 8px;',
|
|
470
|
+
' padding: 7px 12px;',
|
|
471
|
+
' cursor: pointer;',
|
|
472
|
+
' font-size: 13px; color: #ccc;',
|
|
473
|
+
' border-left: 2px solid transparent;',
|
|
474
|
+
' user-select: none;',
|
|
475
|
+
' transition: background 0.1s ease;',
|
|
476
|
+
'}',
|
|
477
|
+
'.cumulus-thread-item:hover { background: #2a2a2a; }',
|
|
478
|
+
'.cumulus-thread-item.selected {',
|
|
479
|
+
' border-left-color: #0066cc;',
|
|
480
|
+
' background: #1e2a3a;',
|
|
481
|
+
' color: #e0e0e0;',
|
|
482
|
+
'}',
|
|
483
|
+
'.cumulus-thread-dot {',
|
|
484
|
+
' width: 7px; height: 7px; border-radius: 50%;',
|
|
485
|
+
' flex-shrink: 0;',
|
|
486
|
+
'}',
|
|
487
|
+
'.cumulus-thread-dot.active { background: #22c55e; }',
|
|
488
|
+
'.cumulus-thread-dot.inactive {',
|
|
489
|
+
' background: transparent;',
|
|
490
|
+
' border: 1px solid #555;',
|
|
491
|
+
'}',
|
|
492
|
+
'.cumulus-thread-name {',
|
|
493
|
+
' flex: 1; overflow: hidden;',
|
|
494
|
+
' text-overflow: ellipsis; white-space: nowrap;',
|
|
495
|
+
'}',
|
|
496
|
+
'.cumulus-thread-count {',
|
|
497
|
+
' font-size: 11px; color: #666;',
|
|
498
|
+
' flex-shrink: 0;',
|
|
499
|
+
'}',
|
|
500
|
+
'.cumulus-sidebar-footer {',
|
|
501
|
+
' padding: 8px 10px;',
|
|
502
|
+
' border-top: 1px solid #333;',
|
|
503
|
+
' flex-shrink: 0;',
|
|
504
|
+
'}',
|
|
505
|
+
'.cumulus-new-thread-btn {',
|
|
506
|
+
' width: 100%;',
|
|
507
|
+
' background: #2a2a2a; border: 1px solid #3a3a3a;',
|
|
508
|
+
' border-radius: 0.45em; color: #aaa;',
|
|
509
|
+
' padding: 7px 10px; font-size: 13px;',
|
|
510
|
+
' cursor: pointer; font-family: inherit;',
|
|
511
|
+
' text-align: left;',
|
|
512
|
+
'}',
|
|
513
|
+
'.cumulus-new-thread-btn:hover { background: #333; color: #ddd; border-color: #0066cc; }',
|
|
514
|
+
'.cumulus-new-thread-input {',
|
|
515
|
+
' width: 100%;',
|
|
516
|
+
' background: #3d3d3d; border: 1px solid #0066cc;',
|
|
517
|
+
' border-radius: 0.45em; color: #e0e0e0;',
|
|
518
|
+
' padding: 7px 10px; font-size: 13px;',
|
|
519
|
+
' font-family: inherit; outline: none;',
|
|
520
|
+
'}',
|
|
521
|
+
|
|
522
|
+
/* ── Content area (holds panels) ── */
|
|
523
|
+
'.cumulus-content-area {',
|
|
524
|
+
' flex: 1; display: flex; flex-direction: row;',
|
|
525
|
+
' overflow: hidden;',
|
|
526
|
+
'}',
|
|
527
|
+
|
|
528
|
+
/* ── Thread panel (inside content area) ── */
|
|
529
|
+
'.cumulus-thread-panel {',
|
|
530
|
+
' flex: 1; display: flex; flex-direction: column;',
|
|
531
|
+
' overflow: hidden; background: #1e1e1e;',
|
|
532
|
+
' border-left: 1px solid #333;',
|
|
533
|
+
' min-width: 0;',
|
|
534
|
+
'}',
|
|
535
|
+
'.cumulus-thread-panel:first-child { border-left: none; }',
|
|
536
|
+
'.cumulus-thread-panel-header {',
|
|
537
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
538
|
+
' padding: 0 0.85em;',
|
|
539
|
+
' height: 40px;',
|
|
540
|
+
' background: #2d2d2d;',
|
|
541
|
+
' border-bottom: 1px solid #3a3a3a;',
|
|
542
|
+
' flex-shrink: 0;',
|
|
543
|
+
'}',
|
|
544
|
+
'.cumulus-thread-panel-title {',
|
|
545
|
+
' font-weight: 600; font-size: 14px; color: #e0e0e0;',
|
|
546
|
+
' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
|
547
|
+
'}',
|
|
548
|
+
'.cumulus-thread-panel-close {',
|
|
549
|
+
' background: none; border: none; color: #666;',
|
|
550
|
+
' cursor: pointer; font-size: 18px; padding: 2px 4px;',
|
|
551
|
+
' line-height: 1; flex-shrink: 0;',
|
|
552
|
+
'}',
|
|
553
|
+
'.cumulus-thread-panel-close:hover { color: #e0e0e0; }',
|
|
554
|
+
|
|
555
|
+
/* ── Standalone empty state (no threads selected) ── */
|
|
556
|
+
'.cumulus-standalone-empty {',
|
|
557
|
+
' flex: 1; display: flex; flex-direction: column;',
|
|
558
|
+
' align-items: center; justify-content: center;',
|
|
559
|
+
' color: #555; font-size: 15px; text-align: center; padding: 20px;',
|
|
560
|
+
' gap: 10px;',
|
|
561
|
+
'}',
|
|
562
|
+
'.cumulus-standalone-empty-hint {',
|
|
563
|
+
' font-size: 12px; color: #444;',
|
|
564
|
+
'}',
|
|
565
|
+
|
|
566
|
+
/* ── Top bar for standalone (connection status + logout) ── */
|
|
567
|
+
'.cumulus-topbar {',
|
|
568
|
+
' display: flex; align-items: center; justify-content: space-between;',
|
|
569
|
+
' padding: 0 14px;',
|
|
570
|
+
' height: 40px;',
|
|
571
|
+
' background: #2d2d2d;',
|
|
572
|
+
' border-bottom: 1px solid #3a3a3a;',
|
|
573
|
+
' flex-shrink: 0;',
|
|
574
|
+
'}',
|
|
575
|
+
'.cumulus-topbar-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
|
|
576
|
+
'.cumulus-topbar-right { display: flex; align-items: center; gap: 10px; }',
|
|
577
|
+
].join('\n');
|
|
578
|
+
|
|
579
|
+
// ─── HTML Escaping ───────────────────────────────────────────────────────────
|
|
580
|
+
function escapeHtml(text) {
|
|
581
|
+
var div = document.createElement('div');
|
|
582
|
+
div.textContent = text;
|
|
583
|
+
return div.innerHTML;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// ─── Markdown Renderer ───────────────────────────────────────────────────────
|
|
587
|
+
// Uses a placeholder strategy: extract code blocks first, render everything
|
|
588
|
+
// else, then reinsert code blocks. This prevents markdown patterns inside
|
|
589
|
+
// code from being processed.
|
|
590
|
+
|
|
335
591
|
function renderMarkdown(text) {
|
|
336
|
-
|
|
592
|
+
// Phase 1: Extract fenced code blocks — replace with unique tokens
|
|
593
|
+
var codeBlocks = [];
|
|
594
|
+
var TOKEN_PREFIX = '\x00CODEBLOCK_';
|
|
595
|
+
var processed = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, function (_, lang, code) {
|
|
596
|
+
var idx = codeBlocks.length;
|
|
597
|
+
var langLabel = lang.trim() || 'text';
|
|
598
|
+
var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
|
|
599
|
+
var tokenId = 'cb' + idx;
|
|
600
|
+
codeBlocks.push(buildCodeBlock(langLabel, escapedCode, tokenId));
|
|
601
|
+
return TOKEN_PREFIX + idx + '\x00';
|
|
602
|
+
});
|
|
337
603
|
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
604
|
+
// Phase 2: Escape HTML in the non-code portions
|
|
605
|
+
// We need to escape line by line between tokens
|
|
606
|
+
var parts = processed.split(/(\x00CODEBLOCK_\d+\x00)/);
|
|
607
|
+
var escapedParts = parts.map(function (part) {
|
|
608
|
+
if (/^\x00CODEBLOCK_\d+\x00$/.test(part)) return part; // keep token as-is
|
|
609
|
+
return escapeHtml(part);
|
|
341
610
|
});
|
|
611
|
+
var html = escapedParts.join('');
|
|
342
612
|
|
|
343
|
-
//
|
|
344
|
-
html = html
|
|
613
|
+
// Phase 3: Process tables (before other inline markdown)
|
|
614
|
+
html = renderTables(html);
|
|
615
|
+
|
|
616
|
+
// Phase 4: Process block-level markdown elements
|
|
617
|
+
// Horizontal rules
|
|
618
|
+
html = html.replace(/^[ \t]*(---+|___+|\*\*\*+)[ \t]*$/gm, '<hr>');
|
|
619
|
+
|
|
620
|
+
// Headers
|
|
621
|
+
html = html.replace(/^######[ \t]+(.+)$/gm, '<h6>$1</h6>');
|
|
622
|
+
html = html.replace(/^#####[ \t]+(.+)$/gm, '<h5>$1</h5>');
|
|
623
|
+
html = html.replace(/^####[ \t]+(.+)$/gm, '<h4>$1</h4>');
|
|
624
|
+
html = html.replace(/^###[ \t]+(.+)$/gm, '<h3>$1</h3>');
|
|
625
|
+
html = html.replace(/^##[ \t]+(.+)$/gm, '<h2>$1</h2>');
|
|
626
|
+
html = html.replace(/^#[ \t]+(.+)$/gm, '<h1>$1</h1>');
|
|
627
|
+
|
|
628
|
+
// Blockquotes (simple single-level)
|
|
629
|
+
html = renderBlockquotes(html);
|
|
345
630
|
|
|
346
|
-
//
|
|
631
|
+
// Lists
|
|
632
|
+
html = renderLists(html);
|
|
633
|
+
|
|
634
|
+
// Phase 5: Inline markdown
|
|
635
|
+
// Bold (** and __)
|
|
347
636
|
html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
|
637
|
+
html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
|
|
638
|
+
|
|
639
|
+
// Italic (* and _) — careful not to match inside words for _
|
|
640
|
+
html = html.replace(/\*(?!\s)(.+?)(?<!\s)\*/g, '<em>$1</em>');
|
|
641
|
+
html = html.replace(/(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)/g, '<em>$1</em>');
|
|
642
|
+
|
|
643
|
+
// Strikethrough
|
|
644
|
+
html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
|
|
348
645
|
|
|
349
|
-
//
|
|
350
|
-
html = html.replace(
|
|
646
|
+
// Inline code (backtick)
|
|
647
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
351
648
|
|
|
352
649
|
// Links
|
|
353
650
|
html = html.replace(
|
|
354
651
|
/\[([^\]]+)\]\(([^)]+)\)/g,
|
|
355
|
-
'<a href="$2" target="_blank" rel="noopener">$1</a>'
|
|
652
|
+
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
|
|
356
653
|
);
|
|
357
654
|
|
|
358
|
-
//
|
|
359
|
-
|
|
655
|
+
// Phase 6: Convert remaining newlines to <br> (skip block-level elements)
|
|
656
|
+
// Wrap sequences of plain text lines in <p> tags
|
|
657
|
+
html = renderParagraphs(html);
|
|
658
|
+
|
|
659
|
+
// Phase 7: Reinsert code blocks
|
|
660
|
+
html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, function (_, idx) {
|
|
661
|
+
return codeBlocks[parseInt(idx, 10)];
|
|
662
|
+
});
|
|
360
663
|
|
|
361
664
|
return html;
|
|
362
665
|
}
|
|
363
666
|
|
|
364
|
-
function
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
667
|
+
function buildCodeBlock(lang, escapedCode, tokenId) {
|
|
668
|
+
return (
|
|
669
|
+
'<div class="code-block-wrapper">' +
|
|
670
|
+
'<div class="code-block-header">' +
|
|
671
|
+
'<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
|
|
672
|
+
'<button class="code-block-copy-btn" data-copy-target="' + tokenId + '" data-testid="webchat-copy-code">Copy</button>' +
|
|
673
|
+
'</div>' +
|
|
674
|
+
'<pre><code data-code-id="' + tokenId + '">' + escapedCode + '</code></pre>' +
|
|
675
|
+
'</div>'
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function renderTables(html) {
|
|
680
|
+
// Split on double newlines to find table blocks
|
|
681
|
+
// A table block: lines where most lines start with |
|
|
682
|
+
var lines = html.split('\n');
|
|
683
|
+
var result = [];
|
|
684
|
+
var i = 0;
|
|
685
|
+
while (i < lines.length) {
|
|
686
|
+
var line = lines[i];
|
|
687
|
+
// Check if this line looks like a table row
|
|
688
|
+
if (/^\|.+\|/.test(line.trim())) {
|
|
689
|
+
// Collect contiguous table lines
|
|
690
|
+
var tableLines = [];
|
|
691
|
+
while (i < lines.length && /^\|/.test(lines[i].trim())) {
|
|
692
|
+
tableLines.push(lines[i]);
|
|
693
|
+
i++;
|
|
694
|
+
}
|
|
695
|
+
result.push(buildTable(tableLines));
|
|
696
|
+
} else {
|
|
697
|
+
result.push(line);
|
|
698
|
+
i++;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return result.join('\n');
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function buildTable(lines) {
|
|
705
|
+
// lines[0] = header row, lines[1] = separator row (|---|), lines[2+] = data rows
|
|
706
|
+
if (lines.length < 2) return lines.join('\n');
|
|
707
|
+
|
|
708
|
+
var isSepRow = /^\|[\s|:-]+\|$/.test(lines[1].trim());
|
|
709
|
+
if (!isSepRow) return lines.join('\n');
|
|
710
|
+
|
|
711
|
+
var headerCells = parseTableRow(lines[0]);
|
|
712
|
+
var dataRows = lines.slice(2);
|
|
713
|
+
|
|
714
|
+
var thead = '<thead><tr>' + headerCells.map(function (c) {
|
|
715
|
+
return '<th>' + c + '</th>';
|
|
716
|
+
}).join('') + '</tr></thead>';
|
|
717
|
+
|
|
718
|
+
var tbody = '<tbody>' + dataRows.map(function (row) {
|
|
719
|
+
var cells = parseTableRow(row);
|
|
720
|
+
return '<tr>' + cells.map(function (c) {
|
|
721
|
+
return '<td>' + c + '</td>';
|
|
722
|
+
}).join('') + '</tr>';
|
|
723
|
+
}).join('') + '</tbody>';
|
|
724
|
+
|
|
725
|
+
return '<table>' + thead + tbody + '</table>';
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function parseTableRow(line) {
|
|
729
|
+
// Split on | and trim, filter empty first/last from leading/trailing |
|
|
730
|
+
var parts = line.split('|');
|
|
731
|
+
// Remove first and last if empty (from leading/trailing |)
|
|
732
|
+
if (parts.length > 0 && parts[0].trim() === '') parts = parts.slice(1);
|
|
733
|
+
if (parts.length > 0 && parts[parts.length - 1].trim() === '') parts = parts.slice(0, -1);
|
|
734
|
+
return parts.map(function (c) { return c.trim(); });
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function renderBlockquotes(html) {
|
|
738
|
+
// Find consecutive lines starting with > (escaped >)
|
|
739
|
+
var lines = html.split('\n');
|
|
740
|
+
var result = [];
|
|
741
|
+
var i = 0;
|
|
742
|
+
while (i < lines.length) {
|
|
743
|
+
if (/^>[ \t]?/.test(lines[i])) {
|
|
744
|
+
var bqLines = [];
|
|
745
|
+
while (i < lines.length && /^>[ \t]?/.test(lines[i])) {
|
|
746
|
+
bqLines.push(lines[i].replace(/^>[ \t]?/, ''));
|
|
747
|
+
i++;
|
|
748
|
+
}
|
|
749
|
+
result.push('<blockquote>' + bqLines.join('<br>') + '</blockquote>');
|
|
750
|
+
} else {
|
|
751
|
+
result.push(lines[i]);
|
|
752
|
+
i++;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return result.join('\n');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function renderLists(html) {
|
|
759
|
+
// Process unordered then ordered lists
|
|
760
|
+
// Match blocks of consecutive list lines
|
|
761
|
+
var lines = html.split('\n');
|
|
762
|
+
var result = [];
|
|
763
|
+
var i = 0;
|
|
764
|
+
|
|
765
|
+
while (i < lines.length) {
|
|
766
|
+
// Unordered list item: "- " or "* " or "+ " (with optional indent)
|
|
767
|
+
if (/^[ \t]*[-*+][ \t]+/.test(lines[i])) {
|
|
768
|
+
var listLines = [];
|
|
769
|
+
while (i < lines.length && /^[ \t]*[-*+][ \t]+/.test(lines[i])) {
|
|
770
|
+
listLines.push(lines[i].replace(/^[ \t]*[-*+][ \t]+/, ''));
|
|
771
|
+
i++;
|
|
772
|
+
}
|
|
773
|
+
result.push('<ul>' + listLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ul>');
|
|
774
|
+
}
|
|
775
|
+
// Ordered list item: "1. " "2. " etc.
|
|
776
|
+
else if (/^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
|
|
777
|
+
var olLines = [];
|
|
778
|
+
while (i < lines.length && /^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
|
|
779
|
+
olLines.push(lines[i].replace(/^[ \t]*\d+\.[ \t]+/, ''));
|
|
780
|
+
i++;
|
|
781
|
+
}
|
|
782
|
+
result.push('<ol>' + olLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ol>');
|
|
783
|
+
}
|
|
784
|
+
else {
|
|
785
|
+
result.push(lines[i]);
|
|
786
|
+
i++;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return result.join('\n');
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function renderParagraphs(html) {
|
|
793
|
+
// Split on double newlines; wrap plain-text segments in <p> while preserving
|
|
794
|
+
// existing block-level HTML elements.
|
|
795
|
+
// Strategy: split into chunks on blank lines (two+ newlines), then for each
|
|
796
|
+
// chunk: if it starts with a block-level tag, keep as-is; else wrap in <p>.
|
|
797
|
+
var blockTags = /^<(h[1-6]|hr|ul|ol|blockquote|table|div|pre)\b/i;
|
|
798
|
+
|
|
799
|
+
var chunks = html.split(/\n{2,}/);
|
|
800
|
+
var parts = chunks.map(function (chunk) {
|
|
801
|
+
chunk = chunk.trim();
|
|
802
|
+
if (!chunk) return '';
|
|
803
|
+
if (blockTags.test(chunk)) return chunk;
|
|
804
|
+
// It's a text/inline-html chunk — convert single newlines to <br>
|
|
805
|
+
// but only outside of block tags within the chunk
|
|
806
|
+
var inner = chunk.replace(/\n/g, '<br>');
|
|
807
|
+
return '<p>' + inner + '</p>';
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
return parts.filter(function (p) { return p !== ''; }).join('\n');
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ─── Attachment helpers ──────────────────────────────────────────────────────
|
|
814
|
+
function fileExtension(name) {
|
|
815
|
+
var parts = name.split('.');
|
|
816
|
+
return parts.length > 1 ? parts[parts.length - 1].slice(0, 5) : 'file';
|
|
368
817
|
}
|
|
369
818
|
|
|
370
|
-
|
|
819
|
+
function readFileAsBase64(file) {
|
|
820
|
+
return new Promise(function (resolve, reject) {
|
|
821
|
+
var reader = new FileReader();
|
|
822
|
+
reader.onload = function (e) {
|
|
823
|
+
// result is "data:mime/type;base64,XXXX" — strip the prefix
|
|
824
|
+
var result = e.target.result;
|
|
825
|
+
var comma = result.indexOf(',');
|
|
826
|
+
resolve({ base64: result.slice(comma + 1), mimeType: file.type || 'application/octet-stream', name: file.name });
|
|
827
|
+
};
|
|
828
|
+
reader.onerror = reject;
|
|
829
|
+
reader.readAsDataURL(file);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function readFileAsDataUrl(file) {
|
|
834
|
+
return new Promise(function (resolve, reject) {
|
|
835
|
+
var reader = new FileReader();
|
|
836
|
+
reader.onload = function (e) { resolve(e.target.result); };
|
|
837
|
+
reader.onerror = reject;
|
|
838
|
+
reader.readAsDataURL(file);
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ─── WebSocket Connection ────────────────────────────────────────────────────
|
|
371
843
|
function createConnection(opts) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
844
|
+
var wsUrl = opts.wsUrl;
|
|
845
|
+
var apiKey = opts.apiKey;
|
|
846
|
+
var sessionId = opts.sessionId;
|
|
847
|
+
var onMessage = opts.onMessage;
|
|
848
|
+
var onStatus = opts.onStatus;
|
|
849
|
+
var skipHistory = opts.skipHistory || false;
|
|
850
|
+
|
|
851
|
+
var ws = null;
|
|
852
|
+
var reconnectTimer = null;
|
|
853
|
+
var reconnectDelay = 1000;
|
|
854
|
+
var destroyed = false;
|
|
376
855
|
|
|
377
856
|
function connect() {
|
|
857
|
+
if (destroyed) return;
|
|
378
858
|
onStatus('connecting');
|
|
379
859
|
ws = new WebSocket(wsUrl);
|
|
380
860
|
|
|
381
861
|
ws.onopen = function () {
|
|
862
|
+
if (destroyed) { ws.close(); return; }
|
|
382
863
|
onStatus('connected');
|
|
383
864
|
reconnectDelay = 1000;
|
|
384
|
-
|
|
385
|
-
// Authenticate
|
|
386
865
|
ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
JSON.stringify({
|
|
391
|
-
type: 'history',
|
|
392
|
-
threadName: sessionId,
|
|
393
|
-
limit: 50,
|
|
394
|
-
})
|
|
395
|
-
);
|
|
866
|
+
if (!skipHistory) {
|
|
867
|
+
ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
|
|
868
|
+
}
|
|
396
869
|
};
|
|
397
870
|
|
|
398
871
|
ws.onmessage = function (event) {
|
|
@@ -405,17 +878,18 @@
|
|
|
405
878
|
};
|
|
406
879
|
|
|
407
880
|
ws.onclose = function () {
|
|
881
|
+
if (destroyed) return;
|
|
408
882
|
onStatus('disconnected');
|
|
409
883
|
scheduleReconnect();
|
|
410
884
|
};
|
|
411
885
|
|
|
412
886
|
ws.onerror = function () {
|
|
413
|
-
// onclose
|
|
887
|
+
// onclose fires after onerror
|
|
414
888
|
};
|
|
415
889
|
}
|
|
416
890
|
|
|
417
891
|
function scheduleReconnect() {
|
|
418
|
-
if (reconnectTimer) return;
|
|
892
|
+
if (reconnectTimer || destroyed) return;
|
|
419
893
|
reconnectTimer = setTimeout(function () {
|
|
420
894
|
reconnectTimer = null;
|
|
421
895
|
reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
|
|
@@ -430,12 +904,13 @@
|
|
|
430
904
|
}
|
|
431
905
|
|
|
432
906
|
function close() {
|
|
907
|
+
destroyed = true;
|
|
433
908
|
if (reconnectTimer) {
|
|
434
909
|
clearTimeout(reconnectTimer);
|
|
435
910
|
reconnectTimer = null;
|
|
436
911
|
}
|
|
437
912
|
if (ws) {
|
|
438
|
-
ws.onclose = null;
|
|
913
|
+
ws.onclose = null;
|
|
439
914
|
ws.close();
|
|
440
915
|
ws = null;
|
|
441
916
|
}
|
|
@@ -445,52 +920,565 @@
|
|
|
445
920
|
return { send: send, close: close };
|
|
446
921
|
}
|
|
447
922
|
|
|
448
|
-
// ─── Widget
|
|
923
|
+
// ─── Widget ──────────────────────────────────────────────────────────────────
|
|
449
924
|
function createWidget(opts) {
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
// For standalone mode, use stored key or empty; for embedded, use data-api-key
|
|
925
|
+
var standalone = opts.standalone;
|
|
926
|
+
var sessionId = getSessionId();
|
|
927
|
+
var wsUrl = getWsUrl();
|
|
454
928
|
var activeApiKey = standalone ? getStoredApiKey() : API_KEY;
|
|
455
929
|
|
|
456
930
|
// Inject styles
|
|
457
|
-
|
|
931
|
+
var styleEl = document.createElement('style');
|
|
458
932
|
styleEl.textContent = STYLES;
|
|
459
933
|
document.head.appendChild(styleEl);
|
|
460
934
|
|
|
461
|
-
//
|
|
462
|
-
var
|
|
463
|
-
|
|
464
|
-
|
|
935
|
+
// ── Root container ──
|
|
936
|
+
var container = document.createElement('div');
|
|
937
|
+
container.className = 'cumulus-widget';
|
|
938
|
+
|
|
939
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
940
|
+
// EMBEDDED MODE — unchanged from original behavior
|
|
941
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
942
|
+
if (!standalone) {
|
|
943
|
+
// ── State ──
|
|
944
|
+
var messages = [];
|
|
945
|
+
var streaming = false;
|
|
946
|
+
var stopRequested = false;
|
|
947
|
+
var streamBuffer = '';
|
|
948
|
+
var connection = null;
|
|
949
|
+
var connectionStatus = 'disconnected';
|
|
950
|
+
var authenticated = false;
|
|
951
|
+
var pendingAttachments = [];
|
|
952
|
+
|
|
953
|
+
// ── Panel ──
|
|
954
|
+
var panel = document.createElement('div');
|
|
955
|
+
panel.className = 'cumulus-panel floating';
|
|
956
|
+
panel.style.display = 'none';
|
|
957
|
+
panel.setAttribute('data-testid', 'webchat-panel');
|
|
958
|
+
|
|
959
|
+
// ── Header ──
|
|
960
|
+
var header = document.createElement('div');
|
|
961
|
+
header.className = 'cumulus-header';
|
|
962
|
+
header.innerHTML =
|
|
963
|
+
'<span class="cumulus-header-title">Cumulus</span>' +
|
|
964
|
+
'<span class="cumulus-header-status">' +
|
|
965
|
+
'<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
|
|
966
|
+
'<span data-testid="webchat-status-text">Disconnected</span>' +
|
|
967
|
+
'</span>' +
|
|
968
|
+
'<button class="cumulus-close-btn" data-testid="webchat-close">×</button>';
|
|
969
|
+
panel.appendChild(header);
|
|
970
|
+
|
|
971
|
+
// ── Auth panel ──
|
|
972
|
+
var authPanel = document.createElement('div');
|
|
973
|
+
authPanel.className = 'cumulus-auth';
|
|
974
|
+
authPanel.setAttribute('data-testid', 'webchat-auth');
|
|
975
|
+
authPanel.innerHTML =
|
|
976
|
+
'<div class="cumulus-auth-title">Cumulus Chat</div>' +
|
|
977
|
+
'<div class="cumulus-auth-subtitle">Enter your API key to connect. The key will be saved in your browser.</div>' +
|
|
978
|
+
'<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
|
|
979
|
+
'<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
|
|
980
|
+
'<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
|
|
981
|
+
panel.appendChild(authPanel);
|
|
982
|
+
|
|
983
|
+
// ── Messages area ──
|
|
984
|
+
var messagesEl = document.createElement('div');
|
|
985
|
+
messagesEl.className = 'cumulus-messages';
|
|
986
|
+
messagesEl.setAttribute('data-testid', 'webchat-messages');
|
|
987
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
988
|
+
messagesEl.style.display = 'none';
|
|
989
|
+
panel.appendChild(messagesEl);
|
|
990
|
+
|
|
991
|
+
// ── Input area ──
|
|
992
|
+
var inputArea = document.createElement('div');
|
|
993
|
+
inputArea.className = 'cumulus-input-area';
|
|
994
|
+
inputArea.style.display = 'none';
|
|
995
|
+
|
|
996
|
+
var attachStrip = document.createElement('div');
|
|
997
|
+
attachStrip.className = 'cumulus-attach-strip';
|
|
998
|
+
attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
|
|
999
|
+
attachStrip.style.display = 'none';
|
|
1000
|
+
inputArea.appendChild(attachStrip);
|
|
1001
|
+
|
|
1002
|
+
var inputRow = document.createElement('div');
|
|
1003
|
+
inputRow.className = 'cumulus-input-row';
|
|
1004
|
+
|
|
1005
|
+
var fileInput = document.createElement('input');
|
|
1006
|
+
fileInput.type = 'file';
|
|
1007
|
+
fileInput.multiple = true;
|
|
1008
|
+
fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
|
|
1009
|
+
fileInput.style.display = 'none';
|
|
1010
|
+
fileInput.setAttribute('data-testid', 'webchat-file-input');
|
|
1011
|
+
|
|
1012
|
+
var attachBtn = document.createElement('button');
|
|
1013
|
+
attachBtn.className = 'cumulus-attach-btn';
|
|
1014
|
+
attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
|
|
1015
|
+
attachBtn.setAttribute('title', 'Attach files');
|
|
1016
|
+
attachBtn.textContent = '+';
|
|
1017
|
+
|
|
1018
|
+
var input = document.createElement('textarea');
|
|
1019
|
+
input.className = 'cumulus-input';
|
|
1020
|
+
input.setAttribute('data-testid', 'webchat-input');
|
|
1021
|
+
input.placeholder = 'Type a message\u2026';
|
|
1022
|
+
input.rows = 1;
|
|
1023
|
+
|
|
1024
|
+
var sendBtn = document.createElement('button');
|
|
1025
|
+
sendBtn.className = 'cumulus-send-btn';
|
|
1026
|
+
sendBtn.setAttribute('data-testid', 'webchat-send');
|
|
1027
|
+
sendBtn.textContent = 'Send';
|
|
1028
|
+
|
|
1029
|
+
inputRow.appendChild(fileInput);
|
|
1030
|
+
inputRow.appendChild(attachBtn);
|
|
1031
|
+
inputRow.appendChild(input);
|
|
1032
|
+
inputRow.appendChild(sendBtn);
|
|
1033
|
+
inputArea.appendChild(inputRow);
|
|
1034
|
+
panel.appendChild(inputArea);
|
|
1035
|
+
container.appendChild(panel);
|
|
1036
|
+
|
|
1037
|
+
// ── Floating bubble ──
|
|
1038
|
+
var bubble = document.createElement('button');
|
|
1039
|
+
bubble.className = 'cumulus-bubble';
|
|
1040
|
+
bubble.setAttribute('data-testid', 'webchat-bubble');
|
|
1041
|
+
bubble.setAttribute('title', 'Open chat');
|
|
1042
|
+
bubble.innerHTML =
|
|
1043
|
+
'<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>';
|
|
1044
|
+
container.appendChild(bubble);
|
|
1045
|
+
|
|
1046
|
+
// ── View switching ──
|
|
1047
|
+
function showAuthView() {
|
|
1048
|
+
authenticated = false;
|
|
1049
|
+
authPanel.style.display = 'flex';
|
|
1050
|
+
messagesEl.style.display = 'none';
|
|
1051
|
+
inputArea.style.display = 'none';
|
|
1052
|
+
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
1053
|
+
if (errEl) errEl.textContent = '';
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function showChatView() {
|
|
1057
|
+
authenticated = true;
|
|
1058
|
+
authPanel.style.display = 'none';
|
|
1059
|
+
messagesEl.style.display = 'flex';
|
|
1060
|
+
inputArea.style.display = 'flex';
|
|
1061
|
+
input.focus();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
// ── Rendering ──
|
|
1065
|
+
function scrollToBottom() {
|
|
1066
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function isWideUserMessage(content) {
|
|
1070
|
+
if (/```/.test(content)) return true;
|
|
1071
|
+
var lineCount = (content.match(/\n/g) || []).length + 1;
|
|
1072
|
+
return lineCount > 3;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function buildUserMsgEl(msg) {
|
|
1076
|
+
var el = document.createElement('div');
|
|
1077
|
+
el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
|
|
1078
|
+
el.textContent = msg.content;
|
|
1079
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
1080
|
+
var attRow = document.createElement('div');
|
|
1081
|
+
attRow.className = 'cumulus-msg-attachments';
|
|
1082
|
+
msg.attachments.forEach(function (att) {
|
|
1083
|
+
if (att.isImage) {
|
|
1084
|
+
var img = document.createElement('img');
|
|
1085
|
+
img.className = 'cumulus-msg-img';
|
|
1086
|
+
img.src = att.dataUrl;
|
|
1087
|
+
img.alt = att.name;
|
|
1088
|
+
img.setAttribute('data-testid', 'webchat-msg-img');
|
|
1089
|
+
attRow.appendChild(img);
|
|
1090
|
+
} else {
|
|
1091
|
+
var badge = document.createElement('span');
|
|
1092
|
+
badge.className = 'cumulus-msg-file-badge';
|
|
1093
|
+
badge.setAttribute('data-testid', 'webchat-msg-file');
|
|
1094
|
+
badge.textContent = '\uD83D\uDCCE ' + att.name;
|
|
1095
|
+
attRow.appendChild(badge);
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
el.appendChild(attRow);
|
|
1099
|
+
}
|
|
1100
|
+
return el;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
function buildAssistantMsgEl(content, isStreaming) {
|
|
1104
|
+
var el = document.createElement('div');
|
|
1105
|
+
el.className = 'cumulus-msg assistant';
|
|
1106
|
+
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
1107
|
+
if (content) {
|
|
1108
|
+
el.innerHTML = renderMarkdown(content);
|
|
1109
|
+
if (isStreaming) {
|
|
1110
|
+
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
1111
|
+
}
|
|
1112
|
+
el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
|
|
1113
|
+
btn.addEventListener('click', function () {
|
|
1114
|
+
var targetId = btn.getAttribute('data-copy-target');
|
|
1115
|
+
var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
|
|
1116
|
+
if (codeEl && navigator.clipboard) {
|
|
1117
|
+
navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
|
|
1118
|
+
btn.textContent = 'Copied!';
|
|
1119
|
+
setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
});
|
|
1123
|
+
});
|
|
1124
|
+
} else if (isStreaming) {
|
|
1125
|
+
el.innerHTML =
|
|
1126
|
+
'<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
|
|
1127
|
+
'<span class="cumulus-cursor"></span>';
|
|
1128
|
+
}
|
|
1129
|
+
return el;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
function renderMessages() {
|
|
1133
|
+
messagesEl.innerHTML = '';
|
|
1134
|
+
if (messages.length === 0 && !streaming) {
|
|
1135
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
for (var i = 0; i < messages.length; i++) {
|
|
1139
|
+
var msg = messages[i];
|
|
1140
|
+
var row = document.createElement('div');
|
|
1141
|
+
row.className = 'cumulus-msg-row';
|
|
1142
|
+
if (msg.role === 'user') {
|
|
1143
|
+
row.appendChild(buildUserMsgEl(msg));
|
|
1144
|
+
} else {
|
|
1145
|
+
row.appendChild(buildAssistantMsgEl(msg.content, false));
|
|
1146
|
+
}
|
|
1147
|
+
messagesEl.appendChild(row);
|
|
1148
|
+
}
|
|
1149
|
+
if (streaming) {
|
|
1150
|
+
var row = document.createElement('div');
|
|
1151
|
+
row.className = 'cumulus-msg-row';
|
|
1152
|
+
row.appendChild(buildAssistantMsgEl(streamBuffer, true));
|
|
1153
|
+
messagesEl.appendChild(row);
|
|
1154
|
+
}
|
|
1155
|
+
scrollToBottom();
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
function updateSendBtn() {
|
|
1159
|
+
if (streaming) {
|
|
1160
|
+
sendBtn.textContent = 'Stop';
|
|
1161
|
+
sendBtn.classList.add('stop');
|
|
1162
|
+
sendBtn.disabled = false;
|
|
1163
|
+
} else {
|
|
1164
|
+
sendBtn.textContent = 'Send';
|
|
1165
|
+
sendBtn.classList.remove('stop');
|
|
1166
|
+
sendBtn.disabled = false;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function updateStatus(status) {
|
|
1171
|
+
connectionStatus = status;
|
|
1172
|
+
var dot = panel.querySelector('.cumulus-status-dot');
|
|
1173
|
+
var text = panel.querySelector('[data-testid="webchat-status-text"]');
|
|
1174
|
+
if (dot) dot.className = 'cumulus-status-dot ' + status;
|
|
1175
|
+
if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function renderAttachStrip() {
|
|
1179
|
+
attachStrip.innerHTML = '';
|
|
1180
|
+
if (pendingAttachments.length === 0) {
|
|
1181
|
+
attachStrip.style.display = 'none';
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
attachStrip.style.display = 'flex';
|
|
1185
|
+
pendingAttachments.forEach(function (att, idx) {
|
|
1186
|
+
var chip = document.createElement('div');
|
|
1187
|
+
chip.className = 'cumulus-attach-chip';
|
|
1188
|
+
chip.setAttribute('data-testid', 'webchat-attach-chip');
|
|
1189
|
+
if (att.isImage) {
|
|
1190
|
+
var thumb = document.createElement('img');
|
|
1191
|
+
thumb.className = 'cumulus-attach-chip-thumb';
|
|
1192
|
+
thumb.src = att.dataUrl;
|
|
1193
|
+
thumb.alt = att.name;
|
|
1194
|
+
chip.appendChild(thumb);
|
|
1195
|
+
} else {
|
|
1196
|
+
var icon = document.createElement('div');
|
|
1197
|
+
icon.className = 'cumulus-attach-chip-icon';
|
|
1198
|
+
icon.textContent = fileExtension(att.name);
|
|
1199
|
+
chip.appendChild(icon);
|
|
1200
|
+
}
|
|
1201
|
+
var nameEl = document.createElement('div');
|
|
1202
|
+
nameEl.className = 'cumulus-attach-chip-name';
|
|
1203
|
+
nameEl.textContent = att.name;
|
|
1204
|
+
chip.appendChild(nameEl);
|
|
1205
|
+
var removeBtn = document.createElement('button');
|
|
1206
|
+
removeBtn.className = 'cumulus-attach-chip-remove';
|
|
1207
|
+
removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
|
|
1208
|
+
removeBtn.textContent = '\xD7';
|
|
1209
|
+
removeBtn.setAttribute('title', 'Remove attachment');
|
|
1210
|
+
(function (index) {
|
|
1211
|
+
removeBtn.addEventListener('click', function () {
|
|
1212
|
+
pendingAttachments.splice(index, 1);
|
|
1213
|
+
renderAttachStrip();
|
|
1214
|
+
});
|
|
1215
|
+
})(idx);
|
|
1216
|
+
chip.appendChild(removeBtn);
|
|
1217
|
+
attachStrip.appendChild(chip);
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
async function addFilesToPending(files) {
|
|
1222
|
+
for (var i = 0; i < files.length; i++) {
|
|
1223
|
+
var file = files[i];
|
|
1224
|
+
try {
|
|
1225
|
+
var info = await readFileAsBase64(file);
|
|
1226
|
+
var dataUrl = await readFileAsDataUrl(file);
|
|
1227
|
+
pendingAttachments.push({
|
|
1228
|
+
base64: info.base64,
|
|
1229
|
+
mimeType: info.mimeType,
|
|
1230
|
+
name: file.name,
|
|
1231
|
+
dataUrl: dataUrl,
|
|
1232
|
+
isImage: file.type.startsWith('image/'),
|
|
1233
|
+
});
|
|
1234
|
+
} catch (e) {
|
|
1235
|
+
console.error('[Cumulus] Failed to read file:', file.name, e);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
renderAttachStrip();
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
function handleServerMessage(data) {
|
|
1242
|
+
switch (data.type) {
|
|
1243
|
+
case 'auth_ok':
|
|
1244
|
+
showChatView();
|
|
1245
|
+
break;
|
|
1246
|
+
case 'auth_error':
|
|
1247
|
+
console.error('[Cumulus] Auth failed:', data.error);
|
|
1248
|
+
break;
|
|
1249
|
+
case 'history':
|
|
1250
|
+
if (data.messages && data.messages.length > 0) {
|
|
1251
|
+
messages = data.messages.map(function (m) {
|
|
1252
|
+
return { role: m.role, content: m.content };
|
|
1253
|
+
});
|
|
1254
|
+
renderMessages();
|
|
1255
|
+
}
|
|
1256
|
+
break;
|
|
1257
|
+
case 'token':
|
|
1258
|
+
if (stopRequested) break;
|
|
1259
|
+
if (!streaming) {
|
|
1260
|
+
streaming = true;
|
|
1261
|
+
streamBuffer = '';
|
|
1262
|
+
updateSendBtn();
|
|
1263
|
+
}
|
|
1264
|
+
streamBuffer += data.text;
|
|
1265
|
+
renderMessages();
|
|
1266
|
+
break;
|
|
1267
|
+
case 'segment':
|
|
1268
|
+
break;
|
|
1269
|
+
case 'done':
|
|
1270
|
+
if (stopRequested) {
|
|
1271
|
+
stopRequested = false;
|
|
1272
|
+
streaming = false;
|
|
1273
|
+
if (streamBuffer) {
|
|
1274
|
+
messages.push({ role: 'assistant', content: streamBuffer });
|
|
1275
|
+
}
|
|
1276
|
+
streamBuffer = '';
|
|
1277
|
+
updateSendBtn();
|
|
1278
|
+
renderMessages();
|
|
1279
|
+
break;
|
|
1280
|
+
}
|
|
1281
|
+
streaming = false;
|
|
1282
|
+
messages.push({ role: 'assistant', content: data.response || streamBuffer });
|
|
1283
|
+
streamBuffer = '';
|
|
1284
|
+
updateSendBtn();
|
|
1285
|
+
renderMessages();
|
|
1286
|
+
break;
|
|
1287
|
+
case 'error':
|
|
1288
|
+
stopRequested = false;
|
|
1289
|
+
streaming = false;
|
|
1290
|
+
if (streamBuffer) {
|
|
1291
|
+
messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
1292
|
+
} else {
|
|
1293
|
+
messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
1294
|
+
}
|
|
1295
|
+
streamBuffer = '';
|
|
1296
|
+
updateSendBtn();
|
|
1297
|
+
renderMessages();
|
|
1298
|
+
break;
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function sendMessage() {
|
|
1303
|
+
if (streaming) {
|
|
1304
|
+
stopRequested = true;
|
|
1305
|
+
streaming = false;
|
|
1306
|
+
updateSendBtn();
|
|
1307
|
+
if (streamBuffer) {
|
|
1308
|
+
messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
|
|
1309
|
+
streamBuffer = '';
|
|
1310
|
+
}
|
|
1311
|
+
renderMessages();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
var text = input.value.trim();
|
|
1315
|
+
if (!text && pendingAttachments.length === 0) return;
|
|
1316
|
+
if (!connection) return;
|
|
1317
|
+
var attachSnapshot = pendingAttachments.slice();
|
|
1318
|
+
var displayText = text || '(attachment)';
|
|
1319
|
+
messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
|
|
1320
|
+
input.value = '';
|
|
1321
|
+
input.style.height = 'auto';
|
|
1322
|
+
pendingAttachments = [];
|
|
1323
|
+
renderAttachStrip();
|
|
1324
|
+
streaming = true;
|
|
1325
|
+
stopRequested = false;
|
|
1326
|
+
streamBuffer = '';
|
|
1327
|
+
updateSendBtn();
|
|
1328
|
+
renderMessages();
|
|
1329
|
+
var imagePayload = attachSnapshot
|
|
1330
|
+
.filter(function (a) { return a.isImage; })
|
|
1331
|
+
.map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
|
|
1332
|
+
var payload = {
|
|
1333
|
+
type: 'message',
|
|
1334
|
+
threadName: sessionId,
|
|
1335
|
+
message: text || ' ',
|
|
1336
|
+
};
|
|
1337
|
+
if (imagePayload.length > 0) payload.images = imagePayload;
|
|
1338
|
+
connection.send(payload);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// ── Event listeners ──
|
|
1342
|
+
sendBtn.addEventListener('click', sendMessage);
|
|
1343
|
+
input.addEventListener('keydown', function (e) {
|
|
1344
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1345
|
+
e.preventDefault();
|
|
1346
|
+
sendMessage();
|
|
1347
|
+
}
|
|
1348
|
+
});
|
|
1349
|
+
input.addEventListener('input', function () {
|
|
1350
|
+
input.style.height = 'auto';
|
|
1351
|
+
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
1352
|
+
});
|
|
1353
|
+
input.addEventListener('paste', function (e) {
|
|
1354
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
1355
|
+
if (!items) return;
|
|
1356
|
+
var filesToAdd = [];
|
|
1357
|
+
for (var i = 0; i < items.length; i++) {
|
|
1358
|
+
var item = items[i];
|
|
1359
|
+
if (item.kind === 'file') {
|
|
1360
|
+
var file = item.getAsFile();
|
|
1361
|
+
if (file) filesToAdd.push(file);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (filesToAdd.length > 0) {
|
|
1365
|
+
e.preventDefault();
|
|
1366
|
+
addFilesToPending(filesToAdd);
|
|
1367
|
+
}
|
|
1368
|
+
});
|
|
1369
|
+
fileInput.addEventListener('change', function () {
|
|
1370
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
1371
|
+
addFilesToPending(Array.from(fileInput.files));
|
|
1372
|
+
fileInput.value = '';
|
|
1373
|
+
}
|
|
1374
|
+
});
|
|
1375
|
+
attachBtn.addEventListener('click', function () {
|
|
1376
|
+
fileInput.click();
|
|
1377
|
+
});
|
|
1378
|
+
|
|
1379
|
+
bubble.addEventListener('click', function () {
|
|
1380
|
+
var showing = panel.style.display !== 'none';
|
|
1381
|
+
panel.style.display = showing ? 'none' : 'flex';
|
|
1382
|
+
if (!showing && !connection) {
|
|
1383
|
+
startConnection();
|
|
1384
|
+
}
|
|
1385
|
+
if (!showing) {
|
|
1386
|
+
input.focus();
|
|
1387
|
+
scrollToBottom();
|
|
1388
|
+
}
|
|
1389
|
+
});
|
|
1390
|
+
|
|
1391
|
+
var closeBtn = panel.querySelector('.cumulus-close-btn');
|
|
1392
|
+
if (closeBtn) {
|
|
1393
|
+
closeBtn.addEventListener('click', function () {
|
|
1394
|
+
panel.style.display = 'none';
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Embedded mode: no auth panel handling needed (API key from attribute)
|
|
1399
|
+
function startConnection() {
|
|
1400
|
+
if (connection) { connection.close(); connection = null; }
|
|
1401
|
+
connection = createConnection({
|
|
1402
|
+
wsUrl: wsUrl,
|
|
1403
|
+
apiKey: activeApiKey,
|
|
1404
|
+
sessionId: sessionId,
|
|
1405
|
+
onMessage: handleServerMessage,
|
|
1406
|
+
onStatus: updateStatus,
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function mount(target) {
|
|
1411
|
+
(target || document.body).appendChild(container);
|
|
1412
|
+
// Embedded: connect on first open (bubble click), not eagerly
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function destroy() {
|
|
1416
|
+
if (connection) connection.close();
|
|
1417
|
+
container.remove();
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
return { mount: mount, destroy: destroy, send: sendMessage };
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1424
|
+
// STANDALONE MODE — sidebar + multi-panel layout
|
|
1425
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1426
|
+
|
|
1427
|
+
// ── Standalone state ──
|
|
465
1428
|
var connection = null;
|
|
466
1429
|
var connectionStatus = 'disconnected';
|
|
467
1430
|
var authenticated = false;
|
|
468
1431
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
1432
|
+
// Thread registry: threadName -> { messages, streaming, streamBuffer, stopRequested, pendingAttachments }
|
|
1433
|
+
var threadStates = {};
|
|
1434
|
+
|
|
1435
|
+
// Thread list from server: [{ name, messageCount, lastActivity }]
|
|
1436
|
+
var allThreads = [];
|
|
1437
|
+
|
|
1438
|
+
// Currently visible panel names (ordered, max 3)
|
|
1439
|
+
var visibleThreads = [];
|
|
1440
|
+
|
|
1441
|
+
// Periodic refresh timer
|
|
1442
|
+
var refreshTimer = null;
|
|
1443
|
+
|
|
1444
|
+
// ── Helper: get or create per-thread state ──
|
|
1445
|
+
function getThreadState(threadName) {
|
|
1446
|
+
if (!threadStates[threadName]) {
|
|
1447
|
+
threadStates[threadName] = {
|
|
1448
|
+
messages: [],
|
|
1449
|
+
streaming: false,
|
|
1450
|
+
streamBuffer: '',
|
|
1451
|
+
stopRequested: false,
|
|
1452
|
+
pendingAttachments: [],
|
|
1453
|
+
historyLoaded: false,
|
|
1454
|
+
};
|
|
1455
|
+
}
|
|
1456
|
+
return threadStates[threadName];
|
|
1457
|
+
}
|
|
477
1458
|
|
|
478
|
-
//
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
1459
|
+
// ── Root layout ──
|
|
1460
|
+
var standaloneRoot = document.createElement('div');
|
|
1461
|
+
standaloneRoot.className = 'cumulus-standalone-root';
|
|
1462
|
+
|
|
1463
|
+
// ── Outer wrapper (auth view or app view) ──
|
|
1464
|
+
// Auth panel — shown before connection
|
|
1465
|
+
var authWrapper = document.createElement('div');
|
|
1466
|
+
authWrapper.className = 'cumulus-panel standalone';
|
|
1467
|
+
authWrapper.setAttribute('data-testid', 'webchat-panel');
|
|
1468
|
+
authWrapper.style.display = 'flex';
|
|
1469
|
+
|
|
1470
|
+
// Auth header
|
|
1471
|
+
var authHeader = document.createElement('div');
|
|
1472
|
+
authHeader.className = 'cumulus-header';
|
|
1473
|
+
authHeader.innerHTML =
|
|
482
1474
|
'<span class="cumulus-header-title">Cumulus</span>' +
|
|
483
1475
|
'<span class="cumulus-header-status">' +
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
'</span>'
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
panel.appendChild(header);
|
|
491
|
-
|
|
492
|
-
// Auth panel (standalone mode — shown when no API key)
|
|
493
|
-
const authPanel = document.createElement('div');
|
|
1476
|
+
'<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
|
|
1477
|
+
'<span data-testid="webchat-status-text">Disconnected</span>' +
|
|
1478
|
+
'</span>';
|
|
1479
|
+
authWrapper.appendChild(authHeader);
|
|
1480
|
+
|
|
1481
|
+
var authPanel = document.createElement('div');
|
|
494
1482
|
authPanel.className = 'cumulus-auth';
|
|
495
1483
|
authPanel.setAttribute('data-testid', 'webchat-auth');
|
|
496
1484
|
authPanel.innerHTML =
|
|
@@ -499,245 +1487,836 @@
|
|
|
499
1487
|
'<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
|
|
500
1488
|
'<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
|
|
501
1489
|
'<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
//
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
//
|
|
1490
|
+
authWrapper.appendChild(authPanel);
|
|
1491
|
+
|
|
1492
|
+
// App layout — shown after auth
|
|
1493
|
+
var appLayout = document.createElement('div');
|
|
1494
|
+
appLayout.style.cssText = 'display:none; flex-direction:column; width:100%; height:100vh; overflow:hidden;';
|
|
1495
|
+
|
|
1496
|
+
// Top bar
|
|
1497
|
+
var topbar = document.createElement('div');
|
|
1498
|
+
topbar.className = 'cumulus-topbar';
|
|
1499
|
+
topbar.innerHTML =
|
|
1500
|
+
'<span class="cumulus-topbar-title">Cumulus</span>' +
|
|
1501
|
+
'<div class="cumulus-topbar-right">' +
|
|
1502
|
+
'<span class="cumulus-header-status">' +
|
|
1503
|
+
'<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
|
|
1504
|
+
'<span data-testid="webchat-status-text-app">Disconnected</span>' +
|
|
1505
|
+
'</span>' +
|
|
1506
|
+
'<button class="cumulus-auth-logout" data-testid="webchat-logout">Logout</button>' +
|
|
1507
|
+
'</div>';
|
|
1508
|
+
appLayout.appendChild(topbar);
|
|
1509
|
+
|
|
1510
|
+
// Sidebar + content row
|
|
1511
|
+
var appRow = document.createElement('div');
|
|
1512
|
+
appRow.style.cssText = 'display:flex; flex-direction:row; flex:1; overflow:hidden;';
|
|
1513
|
+
|
|
1514
|
+
// ── Sidebar ──
|
|
1515
|
+
var sidebar = document.createElement('div');
|
|
1516
|
+
sidebar.className = 'cumulus-sidebar';
|
|
1517
|
+
sidebar.setAttribute('data-testid', 'webchat-sidebar');
|
|
1518
|
+
|
|
1519
|
+
// Sidebar header
|
|
1520
|
+
var sidebarHeader = document.createElement('div');
|
|
1521
|
+
sidebarHeader.className = 'cumulus-sidebar-header';
|
|
1522
|
+
sidebarHeader.innerHTML = '<span class="cumulus-sidebar-title">Threads</span>';
|
|
1523
|
+
sidebar.appendChild(sidebarHeader);
|
|
1524
|
+
|
|
1525
|
+
// Sidebar scroll area
|
|
1526
|
+
var sidebarScroll = document.createElement('div');
|
|
1527
|
+
sidebarScroll.className = 'cumulus-sidebar-scroll';
|
|
1528
|
+
sidebar.appendChild(sidebarScroll);
|
|
1529
|
+
|
|
1530
|
+
// Sidebar footer with + New button
|
|
1531
|
+
var sidebarFooter = document.createElement('div');
|
|
1532
|
+
sidebarFooter.className = 'cumulus-sidebar-footer';
|
|
1533
|
+
|
|
1534
|
+
var newThreadBtn = document.createElement('button');
|
|
1535
|
+
newThreadBtn.className = 'cumulus-new-thread-btn';
|
|
1536
|
+
newThreadBtn.setAttribute('data-testid', 'webchat-thread-new');
|
|
1537
|
+
newThreadBtn.textContent = '+ New thread';
|
|
1538
|
+
|
|
1539
|
+
var newThreadInput = document.createElement('input');
|
|
1540
|
+
newThreadInput.className = 'cumulus-new-thread-input';
|
|
1541
|
+
newThreadInput.setAttribute('data-testid', 'webchat-thread-new-input');
|
|
1542
|
+
newThreadInput.placeholder = 'Thread name\u2026';
|
|
1543
|
+
newThreadInput.style.display = 'none';
|
|
1544
|
+
|
|
1545
|
+
sidebarFooter.appendChild(newThreadBtn);
|
|
1546
|
+
sidebarFooter.appendChild(newThreadInput);
|
|
1547
|
+
sidebar.appendChild(sidebarFooter);
|
|
1548
|
+
|
|
1549
|
+
// ── Content area ──
|
|
1550
|
+
var contentArea = document.createElement('div');
|
|
1551
|
+
contentArea.className = 'cumulus-content-area';
|
|
1552
|
+
|
|
1553
|
+
appRow.appendChild(sidebar);
|
|
1554
|
+
appRow.appendChild(contentArea);
|
|
1555
|
+
appLayout.appendChild(appRow);
|
|
1556
|
+
|
|
1557
|
+
standaloneRoot.appendChild(authWrapper);
|
|
1558
|
+
standaloneRoot.appendChild(appLayout);
|
|
1559
|
+
container.appendChild(standaloneRoot);
|
|
1560
|
+
|
|
1561
|
+
// ── Status update (updates both auth-phase and app-phase dots) ──
|
|
1562
|
+
function updateStatus(status) {
|
|
1563
|
+
connectionStatus = status;
|
|
1564
|
+
// Auth phase dot
|
|
1565
|
+
var dot = authWrapper.querySelector('[data-testid="webchat-status-dot"]');
|
|
1566
|
+
var text = authWrapper.querySelector('[data-testid="webchat-status-text"]');
|
|
1567
|
+
if (dot) dot.className = 'cumulus-status-dot ' + status;
|
|
1568
|
+
if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1569
|
+
// App phase dot
|
|
1570
|
+
var dotApp = appLayout.querySelector('[data-testid="webchat-status-dot-app"]');
|
|
1571
|
+
var textApp = appLayout.querySelector('[data-testid="webchat-status-text-app"]');
|
|
1572
|
+
if (dotApp) dotApp.className = 'cumulus-status-dot ' + status;
|
|
1573
|
+
if (textApp) textApp.textContent = status.charAt(0).toUpperCase() + status.slice(1);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// ── View switching ──
|
|
532
1577
|
function showAuthView() {
|
|
533
1578
|
authenticated = false;
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1579
|
+
authWrapper.style.display = 'flex';
|
|
1580
|
+
appLayout.style.display = 'none';
|
|
1581
|
+
visibleThreads = [];
|
|
1582
|
+
threadStates = {};
|
|
1583
|
+
allThreads = [];
|
|
1584
|
+
if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
|
|
539
1585
|
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
540
1586
|
if (errEl) errEl.textContent = '';
|
|
541
1587
|
}
|
|
542
1588
|
|
|
543
|
-
function
|
|
1589
|
+
function showAppView() {
|
|
544
1590
|
authenticated = true;
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1591
|
+
authWrapper.style.display = 'none';
|
|
1592
|
+
appLayout.style.display = 'flex';
|
|
1593
|
+
// Request thread list from server
|
|
1594
|
+
requestThreadList();
|
|
1595
|
+
// Periodic refresh every 30s
|
|
1596
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
1597
|
+
refreshTimer = setInterval(requestThreadList, 30000);
|
|
551
1598
|
}
|
|
552
1599
|
|
|
553
|
-
//
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
1600
|
+
// ── Thread list ──
|
|
1601
|
+
function requestThreadList() {
|
|
1602
|
+
if (connection) {
|
|
1603
|
+
connection.send({ type: 'threads' });
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// ── Sidebar rendering ──
|
|
1608
|
+
function renderSidebar() {
|
|
1609
|
+
sidebarScroll.innerHTML = '';
|
|
1610
|
+
|
|
1611
|
+
var visibleSet = {};
|
|
1612
|
+
for (var i = 0; i < visibleThreads.length; i++) {
|
|
1613
|
+
visibleSet[visibleThreads[i]] = true;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
// Active section: threads currently visible as panels
|
|
1617
|
+
var activeThreads = visibleThreads.slice(); // preserve order
|
|
1618
|
+
if (activeThreads.length > 0) {
|
|
1619
|
+
var activeLabel = document.createElement('div');
|
|
1620
|
+
activeLabel.className = 'cumulus-sidebar-section-label';
|
|
1621
|
+
activeLabel.textContent = 'Active';
|
|
1622
|
+
sidebarScroll.appendChild(activeLabel);
|
|
1623
|
+
|
|
1624
|
+
var activeSection = document.createElement('div');
|
|
1625
|
+
activeSection.setAttribute('data-testid', 'webchat-sidebar-active');
|
|
1626
|
+
activeThreads.forEach(function (name) {
|
|
1627
|
+
activeSection.appendChild(buildThreadItem(name, true, true));
|
|
1628
|
+
});
|
|
1629
|
+
sidebarScroll.appendChild(activeSection);
|
|
1630
|
+
|
|
1631
|
+
var divider = document.createElement('hr');
|
|
1632
|
+
divider.className = 'cumulus-sidebar-divider';
|
|
1633
|
+
sidebarScroll.appendChild(divider);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// All section: all threads from server, sorted by lastActivity desc
|
|
1637
|
+
var allLabel = document.createElement('div');
|
|
1638
|
+
allLabel.className = 'cumulus-sidebar-section-label';
|
|
1639
|
+
allLabel.textContent = 'All';
|
|
1640
|
+
sidebarScroll.appendChild(allLabel);
|
|
1641
|
+
|
|
1642
|
+
var allSection = document.createElement('div');
|
|
1643
|
+
allSection.setAttribute('data-testid', 'webchat-sidebar-all');
|
|
1644
|
+
|
|
1645
|
+
if (allThreads.length === 0) {
|
|
1646
|
+
var emptyNote = document.createElement('div');
|
|
1647
|
+
emptyNote.style.cssText = 'padding: 8px 12px; font-size: 12px; color: #555; font-style: italic;';
|
|
1648
|
+
emptyNote.textContent = 'No threads yet';
|
|
1649
|
+
allSection.appendChild(emptyNote);
|
|
1650
|
+
} else {
|
|
1651
|
+
// Sort by lastActivity descending
|
|
1652
|
+
var sorted = allThreads.slice().sort(function (a, b) {
|
|
1653
|
+
return (b.lastActivity || 0) - (a.lastActivity || 0);
|
|
1654
|
+
});
|
|
1655
|
+
sorted.forEach(function (t) {
|
|
1656
|
+
allSection.appendChild(buildThreadItem(t.name, visibleSet[t.name] || false, false, t.messageCount));
|
|
1657
|
+
});
|
|
1658
|
+
}
|
|
1659
|
+
sidebarScroll.appendChild(allSection);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function buildThreadItem(name, isSelected, isActive, messageCount) {
|
|
1663
|
+
var item = document.createElement('div');
|
|
1664
|
+
item.className = 'cumulus-thread-item' + (isSelected ? ' selected' : '');
|
|
1665
|
+
item.setAttribute('data-testid', 'webchat-thread-item');
|
|
1666
|
+
item.setAttribute('data-thread-name', name);
|
|
1667
|
+
|
|
1668
|
+
var dot = document.createElement('span');
|
|
1669
|
+
dot.className = 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
|
|
1670
|
+
item.appendChild(dot);
|
|
1671
|
+
|
|
1672
|
+
var nameEl = document.createElement('span');
|
|
1673
|
+
nameEl.className = 'cumulus-thread-name';
|
|
1674
|
+
nameEl.textContent = name;
|
|
1675
|
+
nameEl.setAttribute('title', name);
|
|
1676
|
+
item.appendChild(nameEl);
|
|
1677
|
+
|
|
1678
|
+
if (messageCount !== undefined && messageCount !== null) {
|
|
1679
|
+
var countEl = document.createElement('span');
|
|
1680
|
+
countEl.className = 'cumulus-thread-count';
|
|
1681
|
+
countEl.textContent = messageCount;
|
|
1682
|
+
item.appendChild(countEl);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
item.addEventListener('click', function (e) {
|
|
1686
|
+
var multiSelect = e.metaKey || e.ctrlKey;
|
|
1687
|
+
if (multiSelect) {
|
|
1688
|
+
togglePanelThread(name);
|
|
1689
|
+
} else {
|
|
1690
|
+
soloThread(name);
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
|
|
1694
|
+
return item;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// ── Panel management ──
|
|
1698
|
+
function soloThread(name) {
|
|
1699
|
+
visibleThreads = [name];
|
|
1700
|
+
renderSidebar();
|
|
1701
|
+
renderContentArea();
|
|
1702
|
+
loadThreadHistoryIfNeeded(name);
|
|
562
1703
|
}
|
|
563
1704
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
1705
|
+
function togglePanelThread(name) {
|
|
1706
|
+
var idx = visibleThreads.indexOf(name);
|
|
1707
|
+
if (idx !== -1) {
|
|
1708
|
+
// Remove from visible
|
|
1709
|
+
visibleThreads.splice(idx, 1);
|
|
1710
|
+
} else {
|
|
1711
|
+
// Add if under max
|
|
1712
|
+
if (visibleThreads.length < 3) {
|
|
1713
|
+
visibleThreads.push(name);
|
|
1714
|
+
}
|
|
1715
|
+
// If already at max 3, replace the last one
|
|
1716
|
+
else {
|
|
1717
|
+
visibleThreads[visibleThreads.length - 1] = name;
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
renderSidebar();
|
|
1721
|
+
renderContentArea();
|
|
1722
|
+
if (idx === -1) {
|
|
1723
|
+
loadThreadHistoryIfNeeded(name);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
function removeThreadFromView(name) {
|
|
1728
|
+
var idx = visibleThreads.indexOf(name);
|
|
1729
|
+
if (idx !== -1) {
|
|
1730
|
+
visibleThreads.splice(idx, 1);
|
|
1731
|
+
// Clear stale DOM render hooks so background messages don't update detached nodes
|
|
1732
|
+
var state = threadStates[name];
|
|
1733
|
+
if (state) { state._renderMessages = null; state._updateSendBtn = null; }
|
|
1734
|
+
renderSidebar();
|
|
1735
|
+
renderContentArea();
|
|
1736
|
+
}
|
|
567
1737
|
}
|
|
568
1738
|
|
|
569
|
-
function
|
|
570
|
-
|
|
1739
|
+
function loadThreadHistoryIfNeeded(name) {
|
|
1740
|
+
var state = getThreadState(name);
|
|
1741
|
+
if (!state.historyLoaded && connection) {
|
|
1742
|
+
state.historyLoaded = true;
|
|
1743
|
+
connection.send({ type: 'history', threadName: name, limit: 50 });
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
571
1746
|
|
|
572
|
-
|
|
573
|
-
|
|
1747
|
+
// ── Content area rendering ──
|
|
1748
|
+
function renderContentArea() {
|
|
1749
|
+
// Clear stale render hooks before wiping the DOM
|
|
1750
|
+
for (var _ci = 0; _ci < visibleThreads.length; _ci++) {
|
|
1751
|
+
var _cs = threadStates[visibleThreads[_ci]];
|
|
1752
|
+
if (_cs) { _cs._renderMessages = null; _cs._updateSendBtn = null; }
|
|
1753
|
+
}
|
|
1754
|
+
contentArea.innerHTML = '';
|
|
1755
|
+
|
|
1756
|
+
if (visibleThreads.length === 0) {
|
|
1757
|
+
var emptyEl = document.createElement('div');
|
|
1758
|
+
emptyEl.className = 'cumulus-standalone-empty';
|
|
1759
|
+
emptyEl.innerHTML =
|
|
1760
|
+
'<div>Select a thread from the sidebar or create a new one</div>' +
|
|
1761
|
+
'<div class="cumulus-standalone-empty-hint">Cmd/Ctrl+Click to open multiple threads side by side</div>';
|
|
1762
|
+
contentArea.appendChild(emptyEl);
|
|
574
1763
|
return;
|
|
575
1764
|
}
|
|
576
1765
|
|
|
577
|
-
for (var i = 0; i <
|
|
578
|
-
|
|
1766
|
+
for (var i = 0; i < visibleThreads.length; i++) {
|
|
1767
|
+
contentArea.appendChild(buildThreadPanel(visibleThreads[i]));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
// ── Thread panel builder ──
|
|
1772
|
+
function buildThreadPanel(threadName) {
|
|
1773
|
+
var state = getThreadState(threadName);
|
|
1774
|
+
|
|
1775
|
+
var panel = document.createElement('div');
|
|
1776
|
+
panel.className = 'cumulus-thread-panel';
|
|
1777
|
+
panel.setAttribute('data-testid', 'webchat-panel-' + threadName);
|
|
1778
|
+
panel.setAttribute('data-thread-panel', threadName);
|
|
1779
|
+
|
|
1780
|
+
// Panel header
|
|
1781
|
+
var panelHeader = document.createElement('div');
|
|
1782
|
+
panelHeader.className = 'cumulus-thread-panel-header';
|
|
1783
|
+
|
|
1784
|
+
var titleLeft = document.createElement('div');
|
|
1785
|
+
titleLeft.style.cssText = 'display:flex; align-items:center; gap:7px; overflow:hidden;';
|
|
1786
|
+
|
|
1787
|
+
var statusDot = document.createElement('span');
|
|
1788
|
+
statusDot.className = 'cumulus-status-dot ' + connectionStatus;
|
|
1789
|
+
statusDot.setAttribute('data-thread-status-dot', threadName);
|
|
1790
|
+
titleLeft.appendChild(statusDot);
|
|
1791
|
+
|
|
1792
|
+
var titleEl = document.createElement('span');
|
|
1793
|
+
titleEl.className = 'cumulus-thread-panel-title';
|
|
1794
|
+
titleEl.textContent = threadName;
|
|
1795
|
+
titleEl.setAttribute('title', threadName);
|
|
1796
|
+
titleLeft.appendChild(titleEl);
|
|
1797
|
+
|
|
1798
|
+
panelHeader.appendChild(titleLeft);
|
|
1799
|
+
|
|
1800
|
+
var closeBtn = document.createElement('button');
|
|
1801
|
+
closeBtn.className = 'cumulus-thread-panel-close';
|
|
1802
|
+
closeBtn.setAttribute('data-testid', 'webchat-panel-close');
|
|
1803
|
+
closeBtn.setAttribute('title', 'Close panel');
|
|
1804
|
+
closeBtn.textContent = '\xD7';
|
|
1805
|
+
closeBtn.addEventListener('click', function () {
|
|
1806
|
+
removeThreadFromView(threadName);
|
|
1807
|
+
});
|
|
1808
|
+
panelHeader.appendChild(closeBtn);
|
|
1809
|
+
panel.appendChild(panelHeader);
|
|
1810
|
+
|
|
1811
|
+
// Messages area
|
|
1812
|
+
var messagesEl = document.createElement('div');
|
|
1813
|
+
messagesEl.className = 'cumulus-messages';
|
|
1814
|
+
messagesEl.setAttribute('data-testid', 'webchat-messages');
|
|
1815
|
+
messagesEl.setAttribute('data-thread-messages', threadName);
|
|
1816
|
+
panel.appendChild(messagesEl);
|
|
1817
|
+
|
|
1818
|
+
// Input area
|
|
1819
|
+
var inputArea = document.createElement('div');
|
|
1820
|
+
inputArea.className = 'cumulus-input-area';
|
|
1821
|
+
|
|
1822
|
+
// Attachment strip
|
|
1823
|
+
var attachStrip = document.createElement('div');
|
|
1824
|
+
attachStrip.className = 'cumulus-attach-strip';
|
|
1825
|
+
attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
|
|
1826
|
+
attachStrip.style.display = 'none';
|
|
1827
|
+
inputArea.appendChild(attachStrip);
|
|
1828
|
+
|
|
1829
|
+
// Input row
|
|
1830
|
+
var inputRow = document.createElement('div');
|
|
1831
|
+
inputRow.className = 'cumulus-input-row';
|
|
1832
|
+
|
|
1833
|
+
var fileInput = document.createElement('input');
|
|
1834
|
+
fileInput.type = 'file';
|
|
1835
|
+
fileInput.multiple = true;
|
|
1836
|
+
fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
|
|
1837
|
+
fileInput.style.display = 'none';
|
|
1838
|
+
fileInput.setAttribute('data-testid', 'webchat-file-input');
|
|
1839
|
+
|
|
1840
|
+
var attachBtn = document.createElement('button');
|
|
1841
|
+
attachBtn.className = 'cumulus-attach-btn';
|
|
1842
|
+
attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
|
|
1843
|
+
attachBtn.setAttribute('title', 'Attach files');
|
|
1844
|
+
attachBtn.textContent = '+';
|
|
1845
|
+
|
|
1846
|
+
var inputEl = document.createElement('textarea');
|
|
1847
|
+
inputEl.className = 'cumulus-input';
|
|
1848
|
+
inputEl.setAttribute('data-testid', 'webchat-input');
|
|
1849
|
+
inputEl.placeholder = 'Message ' + threadName + '\u2026';
|
|
1850
|
+
inputEl.rows = 1;
|
|
1851
|
+
|
|
1852
|
+
var sendBtn = document.createElement('button');
|
|
1853
|
+
sendBtn.className = 'cumulus-send-btn';
|
|
1854
|
+
sendBtn.setAttribute('data-testid', 'webchat-send');
|
|
1855
|
+
sendBtn.textContent = 'Send';
|
|
1856
|
+
|
|
1857
|
+
inputRow.appendChild(fileInput);
|
|
1858
|
+
inputRow.appendChild(attachBtn);
|
|
1859
|
+
inputRow.appendChild(inputEl);
|
|
1860
|
+
inputRow.appendChild(sendBtn);
|
|
1861
|
+
inputArea.appendChild(inputRow);
|
|
1862
|
+
panel.appendChild(inputArea);
|
|
1863
|
+
|
|
1864
|
+
// ── Panel-local render functions ──
|
|
1865
|
+
function scrollToBottom() {
|
|
1866
|
+
messagesEl.scrollTop = messagesEl.scrollHeight;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1869
|
+
function isWideUserMessage(content) {
|
|
1870
|
+
if (/```/.test(content)) return true;
|
|
1871
|
+
var lineCount = (content.match(/\n/g) || []).length + 1;
|
|
1872
|
+
return lineCount > 3;
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
function buildUserMsgEl(msg) {
|
|
579
1876
|
var el = document.createElement('div');
|
|
580
|
-
el.className = 'cumulus-msg ' + msg.
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1877
|
+
el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
|
|
1878
|
+
el.textContent = msg.content;
|
|
1879
|
+
if (msg.attachments && msg.attachments.length > 0) {
|
|
1880
|
+
var attRow = document.createElement('div');
|
|
1881
|
+
attRow.className = 'cumulus-msg-attachments';
|
|
1882
|
+
msg.attachments.forEach(function (att) {
|
|
1883
|
+
if (att.isImage) {
|
|
1884
|
+
var img = document.createElement('img');
|
|
1885
|
+
img.className = 'cumulus-msg-img';
|
|
1886
|
+
img.src = att.dataUrl;
|
|
1887
|
+
img.alt = att.name;
|
|
1888
|
+
img.setAttribute('data-testid', 'webchat-msg-img');
|
|
1889
|
+
attRow.appendChild(img);
|
|
1890
|
+
} else {
|
|
1891
|
+
var badge = document.createElement('span');
|
|
1892
|
+
badge.className = 'cumulus-msg-file-badge';
|
|
1893
|
+
badge.setAttribute('data-testid', 'webchat-msg-file');
|
|
1894
|
+
badge.textContent = '\uD83D\uDCCE ' + att.name;
|
|
1895
|
+
attRow.appendChild(badge);
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
el.appendChild(attRow);
|
|
585
1899
|
}
|
|
586
|
-
|
|
1900
|
+
return el;
|
|
587
1901
|
}
|
|
588
1902
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
1903
|
+
function buildAssistantMsgEl(content, isStreaming) {
|
|
1904
|
+
var el = document.createElement('div');
|
|
1905
|
+
el.className = 'cumulus-msg assistant';
|
|
1906
|
+
if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
|
|
1907
|
+
if (content) {
|
|
1908
|
+
el.innerHTML = renderMarkdown(content);
|
|
1909
|
+
if (isStreaming) {
|
|
1910
|
+
el.innerHTML += '<span class="cumulus-cursor"></span>';
|
|
1911
|
+
}
|
|
1912
|
+
el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
|
|
1913
|
+
btn.addEventListener('click', function () {
|
|
1914
|
+
var targetId = btn.getAttribute('data-copy-target');
|
|
1915
|
+
var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
|
|
1916
|
+
if (codeEl && navigator.clipboard) {
|
|
1917
|
+
navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
|
|
1918
|
+
btn.textContent = 'Copied!';
|
|
1919
|
+
setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
|
|
1920
|
+
});
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
});
|
|
1924
|
+
} else if (isStreaming) {
|
|
1925
|
+
el.innerHTML =
|
|
1926
|
+
'<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
|
|
1927
|
+
'<span class="cumulus-cursor"></span>';
|
|
1928
|
+
}
|
|
1929
|
+
return el;
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1932
|
+
function renderPanelMessages() {
|
|
1933
|
+
messagesEl.innerHTML = '';
|
|
1934
|
+
if (state.messages.length === 0 && !state.streaming) {
|
|
1935
|
+
messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
for (var i = 0; i < state.messages.length; i++) {
|
|
1939
|
+
var msg = state.messages[i];
|
|
1940
|
+
var row = document.createElement('div');
|
|
1941
|
+
row.className = 'cumulus-msg-row';
|
|
1942
|
+
if (msg.role === 'user') {
|
|
1943
|
+
row.appendChild(buildUserMsgEl(msg));
|
|
1944
|
+
} else {
|
|
1945
|
+
row.appendChild(buildAssistantMsgEl(msg.content, false));
|
|
1946
|
+
}
|
|
1947
|
+
messagesEl.appendChild(row);
|
|
1948
|
+
}
|
|
1949
|
+
if (state.streaming) {
|
|
1950
|
+
var row = document.createElement('div');
|
|
1951
|
+
row.className = 'cumulus-msg-row';
|
|
1952
|
+
row.appendChild(buildAssistantMsgEl(state.streamBuffer, true));
|
|
1953
|
+
messagesEl.appendChild(row);
|
|
1954
|
+
}
|
|
1955
|
+
scrollToBottom();
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function updatePanelSendBtn() {
|
|
1959
|
+
if (state.streaming) {
|
|
1960
|
+
sendBtn.textContent = 'Stop';
|
|
1961
|
+
sendBtn.classList.add('stop');
|
|
1962
|
+
sendBtn.disabled = false;
|
|
597
1963
|
} else {
|
|
598
|
-
|
|
599
|
-
|
|
1964
|
+
sendBtn.textContent = 'Send';
|
|
1965
|
+
sendBtn.classList.remove('stop');
|
|
1966
|
+
sendBtn.disabled = false;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
function renderPanelAttachStrip() {
|
|
1971
|
+
attachStrip.innerHTML = '';
|
|
1972
|
+
if (state.pendingAttachments.length === 0) {
|
|
1973
|
+
attachStrip.style.display = 'none';
|
|
1974
|
+
return;
|
|
600
1975
|
}
|
|
601
|
-
|
|
1976
|
+
attachStrip.style.display = 'flex';
|
|
1977
|
+
state.pendingAttachments.forEach(function (att, idx) {
|
|
1978
|
+
var chip = document.createElement('div');
|
|
1979
|
+
chip.className = 'cumulus-attach-chip';
|
|
1980
|
+
chip.setAttribute('data-testid', 'webchat-attach-chip');
|
|
1981
|
+
if (att.isImage) {
|
|
1982
|
+
var thumb = document.createElement('img');
|
|
1983
|
+
thumb.className = 'cumulus-attach-chip-thumb';
|
|
1984
|
+
thumb.src = att.dataUrl;
|
|
1985
|
+
thumb.alt = att.name;
|
|
1986
|
+
chip.appendChild(thumb);
|
|
1987
|
+
} else {
|
|
1988
|
+
var icon = document.createElement('div');
|
|
1989
|
+
icon.className = 'cumulus-attach-chip-icon';
|
|
1990
|
+
icon.textContent = fileExtension(att.name);
|
|
1991
|
+
chip.appendChild(icon);
|
|
1992
|
+
}
|
|
1993
|
+
var nameEl = document.createElement('div');
|
|
1994
|
+
nameEl.className = 'cumulus-attach-chip-name';
|
|
1995
|
+
nameEl.textContent = att.name;
|
|
1996
|
+
chip.appendChild(nameEl);
|
|
1997
|
+
var removeBtn = document.createElement('button');
|
|
1998
|
+
removeBtn.className = 'cumulus-attach-chip-remove';
|
|
1999
|
+
removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
|
|
2000
|
+
removeBtn.textContent = '\xD7';
|
|
2001
|
+
removeBtn.setAttribute('title', 'Remove attachment');
|
|
2002
|
+
(function (index) {
|
|
2003
|
+
removeBtn.addEventListener('click', function () {
|
|
2004
|
+
state.pendingAttachments.splice(index, 1);
|
|
2005
|
+
renderPanelAttachStrip();
|
|
2006
|
+
});
|
|
2007
|
+
})(idx);
|
|
2008
|
+
chip.appendChild(removeBtn);
|
|
2009
|
+
attachStrip.appendChild(chip);
|
|
2010
|
+
});
|
|
602
2011
|
}
|
|
603
2012
|
|
|
604
|
-
|
|
2013
|
+
async function addFilesToPending(files) {
|
|
2014
|
+
for (var i = 0; i < files.length; i++) {
|
|
2015
|
+
var file = files[i];
|
|
2016
|
+
try {
|
|
2017
|
+
var info = await readFileAsBase64(file);
|
|
2018
|
+
var dataUrl = await readFileAsDataUrl(file);
|
|
2019
|
+
state.pendingAttachments.push({
|
|
2020
|
+
base64: info.base64,
|
|
2021
|
+
mimeType: info.mimeType,
|
|
2022
|
+
name: file.name,
|
|
2023
|
+
dataUrl: dataUrl,
|
|
2024
|
+
isImage: file.type.startsWith('image/'),
|
|
2025
|
+
});
|
|
2026
|
+
} catch (e) {
|
|
2027
|
+
console.error('[Cumulus] Failed to read file:', file.name, e);
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
renderPanelAttachStrip();
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// Store references on the state so the router can call re-render
|
|
2034
|
+
state._renderMessages = renderPanelMessages;
|
|
2035
|
+
state._updateSendBtn = updatePanelSendBtn;
|
|
2036
|
+
|
|
2037
|
+
// ── Send message for this panel ──
|
|
2038
|
+
function sendPanelMessage() {
|
|
2039
|
+
if (state.streaming) {
|
|
2040
|
+
state.stopRequested = true;
|
|
2041
|
+
state.streaming = false;
|
|
2042
|
+
updatePanelSendBtn();
|
|
2043
|
+
if (state.streamBuffer) {
|
|
2044
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer + ' [stopped]' });
|
|
2045
|
+
state.streamBuffer = '';
|
|
2046
|
+
}
|
|
2047
|
+
renderPanelMessages();
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
var text = inputEl.value.trim();
|
|
2051
|
+
if (!text && state.pendingAttachments.length === 0) return;
|
|
2052
|
+
if (!connection) return;
|
|
2053
|
+
|
|
2054
|
+
var attachSnapshot = state.pendingAttachments.slice();
|
|
2055
|
+
var displayText = text || '(attachment)';
|
|
2056
|
+
state.messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
|
|
2057
|
+
inputEl.value = '';
|
|
2058
|
+
inputEl.style.height = 'auto';
|
|
2059
|
+
state.pendingAttachments = [];
|
|
2060
|
+
renderPanelAttachStrip();
|
|
2061
|
+
state.streaming = true;
|
|
2062
|
+
state.stopRequested = false;
|
|
2063
|
+
state.streamBuffer = '';
|
|
2064
|
+
updatePanelSendBtn();
|
|
2065
|
+
renderPanelMessages();
|
|
2066
|
+
|
|
2067
|
+
var imagePayload = attachSnapshot
|
|
2068
|
+
.filter(function (a) { return a.isImage; })
|
|
2069
|
+
.map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
|
|
2070
|
+
|
|
2071
|
+
var payload = {
|
|
2072
|
+
type: 'message',
|
|
2073
|
+
threadName: threadName,
|
|
2074
|
+
message: text || ' ',
|
|
2075
|
+
};
|
|
2076
|
+
if (imagePayload.length > 0) payload.images = imagePayload;
|
|
2077
|
+
connection.send(payload);
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
// ── Panel event listeners ──
|
|
2081
|
+
sendBtn.addEventListener('click', sendPanelMessage);
|
|
2082
|
+
|
|
2083
|
+
inputEl.addEventListener('keydown', function (e) {
|
|
2084
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
2085
|
+
e.preventDefault();
|
|
2086
|
+
sendPanelMessage();
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
|
|
2090
|
+
inputEl.addEventListener('input', function () {
|
|
2091
|
+
inputEl.style.height = 'auto';
|
|
2092
|
+
inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
inputEl.addEventListener('paste', function (e) {
|
|
2096
|
+
var items = e.clipboardData && e.clipboardData.items;
|
|
2097
|
+
if (!items) return;
|
|
2098
|
+
var filesToAdd = [];
|
|
2099
|
+
for (var i = 0; i < items.length; i++) {
|
|
2100
|
+
var item = items[i];
|
|
2101
|
+
if (item.kind === 'file') {
|
|
2102
|
+
var file = item.getAsFile();
|
|
2103
|
+
if (file) filesToAdd.push(file);
|
|
2104
|
+
}
|
|
2105
|
+
}
|
|
2106
|
+
if (filesToAdd.length > 0) {
|
|
2107
|
+
e.preventDefault();
|
|
2108
|
+
addFilesToPending(filesToAdd);
|
|
2109
|
+
}
|
|
2110
|
+
});
|
|
2111
|
+
|
|
2112
|
+
fileInput.addEventListener('change', function () {
|
|
2113
|
+
if (fileInput.files && fileInput.files.length > 0) {
|
|
2114
|
+
addFilesToPending(Array.from(fileInput.files));
|
|
2115
|
+
fileInput.value = '';
|
|
2116
|
+
}
|
|
2117
|
+
});
|
|
2118
|
+
|
|
2119
|
+
attachBtn.addEventListener('click', function () {
|
|
2120
|
+
fileInput.click();
|
|
2121
|
+
});
|
|
2122
|
+
|
|
2123
|
+
// Initial render
|
|
2124
|
+
renderPanelMessages();
|
|
2125
|
+
renderPanelAttachStrip();
|
|
2126
|
+
|
|
2127
|
+
return panel;
|
|
605
2128
|
}
|
|
606
2129
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
var
|
|
611
|
-
if (
|
|
612
|
-
|
|
2130
|
+
// ── Re-render a visible thread panel's messages in-place ──
|
|
2131
|
+
// Called by the message router after state mutation
|
|
2132
|
+
function refreshThreadPanel(threadName) {
|
|
2133
|
+
var state = threadStates[threadName];
|
|
2134
|
+
if (!state) return;
|
|
2135
|
+
|
|
2136
|
+
// Re-render messages in-place if panel is visible
|
|
2137
|
+
if (state._renderMessages) {
|
|
2138
|
+
state._renderMessages();
|
|
613
2139
|
}
|
|
614
|
-
if (
|
|
615
|
-
|
|
2140
|
+
if (state._updateSendBtn) {
|
|
2141
|
+
state._updateSendBtn();
|
|
616
2142
|
}
|
|
617
2143
|
}
|
|
618
2144
|
|
|
619
|
-
//
|
|
2145
|
+
// ── Message router ──
|
|
620
2146
|
function handleServerMessage(data) {
|
|
621
2147
|
switch (data.type) {
|
|
622
2148
|
case 'auth_ok':
|
|
623
|
-
|
|
2149
|
+
showAppView();
|
|
624
2150
|
break;
|
|
625
2151
|
|
|
626
2152
|
case 'auth_error':
|
|
627
2153
|
console.error('[Cumulus] Auth failed:', data.error);
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
2154
|
+
clearStoredApiKey();
|
|
2155
|
+
activeApiKey = '';
|
|
2156
|
+
if (connection) { connection.close(); connection = null; }
|
|
2157
|
+
showAuthView();
|
|
2158
|
+
var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
|
|
2159
|
+
if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
|
|
2160
|
+
break;
|
|
2161
|
+
|
|
2162
|
+
case 'threads':
|
|
2163
|
+
// Server response to { type: 'threads' } request
|
|
2164
|
+
if (data.threads) {
|
|
2165
|
+
allThreads = data.threads;
|
|
2166
|
+
renderSidebar();
|
|
2167
|
+
}
|
|
2168
|
+
break;
|
|
2169
|
+
|
|
2170
|
+
case 'thread_created':
|
|
2171
|
+
// Server confirms new thread; add to list and select it
|
|
2172
|
+
if (data.threadName) {
|
|
2173
|
+
// Add to allThreads if not already present
|
|
2174
|
+
var exists = false;
|
|
2175
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2176
|
+
if (allThreads[i].name === data.threadName) { exists = true; break; }
|
|
2177
|
+
}
|
|
2178
|
+
if (!exists) {
|
|
2179
|
+
allThreads.push({ name: data.threadName, messageCount: 0, lastActivity: Date.now() });
|
|
2180
|
+
}
|
|
2181
|
+
soloThread(data.threadName);
|
|
636
2182
|
}
|
|
637
2183
|
break;
|
|
638
2184
|
|
|
639
2185
|
case 'history':
|
|
640
|
-
if (data.
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
2186
|
+
if (data.threadName) {
|
|
2187
|
+
var state = getThreadState(data.threadName);
|
|
2188
|
+
if (data.messages && data.messages.length > 0) {
|
|
2189
|
+
state.messages = data.messages.map(function (m) {
|
|
2190
|
+
return { role: m.role, content: m.content };
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
refreshThreadPanel(data.threadName);
|
|
645
2194
|
}
|
|
646
2195
|
break;
|
|
647
2196
|
|
|
648
2197
|
case 'token':
|
|
649
|
-
if (
|
|
650
|
-
|
|
651
|
-
|
|
2198
|
+
if (data.threadName) {
|
|
2199
|
+
var state = getThreadState(data.threadName);
|
|
2200
|
+
if (state.stopRequested) break;
|
|
2201
|
+
if (!state.streaming) {
|
|
2202
|
+
state.streaming = true;
|
|
2203
|
+
state.streamBuffer = '';
|
|
2204
|
+
}
|
|
2205
|
+
state.streamBuffer += data.text;
|
|
2206
|
+
// Update lastActivity for this thread in allThreads
|
|
2207
|
+
updateThreadActivity(data.threadName);
|
|
2208
|
+
refreshThreadPanel(data.threadName);
|
|
652
2209
|
}
|
|
653
|
-
streamBuffer += data.text;
|
|
654
|
-
renderMessages();
|
|
655
2210
|
break;
|
|
656
2211
|
|
|
657
2212
|
case 'segment':
|
|
658
|
-
//
|
|
2213
|
+
// Reserved for future verbose display
|
|
659
2214
|
break;
|
|
660
2215
|
|
|
661
2216
|
case 'done':
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
2217
|
+
if (data.threadName) {
|
|
2218
|
+
var state = getThreadState(data.threadName);
|
|
2219
|
+
if (state.stopRequested) {
|
|
2220
|
+
state.stopRequested = false;
|
|
2221
|
+
state.streaming = false;
|
|
2222
|
+
if (state.streamBuffer) {
|
|
2223
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer });
|
|
2224
|
+
}
|
|
2225
|
+
state.streamBuffer = '';
|
|
2226
|
+
updateThreadActivity(data.threadName);
|
|
2227
|
+
refreshThreadPanel(data.threadName);
|
|
2228
|
+
break;
|
|
2229
|
+
}
|
|
2230
|
+
state.streaming = false;
|
|
2231
|
+
state.messages.push({ role: 'assistant', content: data.response || state.streamBuffer });
|
|
2232
|
+
state.streamBuffer = '';
|
|
2233
|
+
updateThreadActivity(data.threadName);
|
|
2234
|
+
refreshThreadPanel(data.threadName);
|
|
2235
|
+
}
|
|
666
2236
|
break;
|
|
667
2237
|
|
|
668
2238
|
case 'error':
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
2239
|
+
if (data.threadName) {
|
|
2240
|
+
var state = getThreadState(data.threadName);
|
|
2241
|
+
state.stopRequested = false;
|
|
2242
|
+
state.streaming = false;
|
|
2243
|
+
if (state.streamBuffer) {
|
|
2244
|
+
state.messages.push({ role: 'assistant', content: state.streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
2245
|
+
} else {
|
|
2246
|
+
state.messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
|
|
2247
|
+
}
|
|
2248
|
+
state.streamBuffer = '';
|
|
2249
|
+
refreshThreadPanel(data.threadName);
|
|
674
2250
|
}
|
|
675
|
-
streamBuffer = '';
|
|
676
|
-
renderMessages();
|
|
677
2251
|
break;
|
|
678
2252
|
}
|
|
679
2253
|
}
|
|
680
2254
|
|
|
681
|
-
function
|
|
682
|
-
var
|
|
683
|
-
|
|
2255
|
+
function updateThreadActivity(threadName) {
|
|
2256
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2257
|
+
if (allThreads[i].name === threadName) {
|
|
2258
|
+
allThreads[i].lastActivity = Date.now();
|
|
2259
|
+
break;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
// Re-render sidebar to reflect new sort order / move thread up
|
|
2263
|
+
renderSidebar();
|
|
2264
|
+
}
|
|
684
2265
|
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
input.style.height = 'auto';
|
|
688
|
-
streaming = true;
|
|
689
|
-
streamBuffer = '';
|
|
690
|
-
renderMessages();
|
|
2266
|
+
// ── New thread creation ──
|
|
2267
|
+
var newThreadCreating = false;
|
|
691
2268
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
2269
|
+
function showNewThreadInput() {
|
|
2270
|
+
if (newThreadCreating) return;
|
|
2271
|
+
newThreadCreating = true;
|
|
2272
|
+
newThreadBtn.style.display = 'none';
|
|
2273
|
+
newThreadInput.style.display = 'block';
|
|
2274
|
+
newThreadInput.value = '';
|
|
2275
|
+
newThreadInput.focus();
|
|
697
2276
|
}
|
|
698
2277
|
|
|
699
|
-
|
|
700
|
-
|
|
2278
|
+
function hideNewThreadInput() {
|
|
2279
|
+
newThreadCreating = false;
|
|
2280
|
+
newThreadBtn.style.display = 'block';
|
|
2281
|
+
newThreadInput.style.display = 'none';
|
|
2282
|
+
newThreadInput.value = '';
|
|
2283
|
+
}
|
|
701
2284
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
2285
|
+
function submitNewThread() {
|
|
2286
|
+
var name = newThreadInput.value.trim();
|
|
2287
|
+
if (!name) { hideNewThreadInput(); return; }
|
|
2288
|
+
if (connection) {
|
|
2289
|
+
connection.send({ type: 'create_thread', threadName: name });
|
|
706
2290
|
}
|
|
707
|
-
|
|
2291
|
+
hideNewThreadInput();
|
|
2292
|
+
// Optimistically add and show (server will confirm with thread_created)
|
|
2293
|
+
var exists = false;
|
|
2294
|
+
for (var i = 0; i < allThreads.length; i++) {
|
|
2295
|
+
if (allThreads[i].name === name) { exists = true; break; }
|
|
2296
|
+
}
|
|
2297
|
+
if (!exists) {
|
|
2298
|
+
allThreads.push({ name: name, messageCount: 0, lastActivity: Date.now() });
|
|
2299
|
+
}
|
|
2300
|
+
soloThread(name);
|
|
2301
|
+
}
|
|
708
2302
|
|
|
709
|
-
|
|
710
|
-
input.addEventListener('input', function () {
|
|
711
|
-
input.style.height = 'auto';
|
|
712
|
-
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
713
|
-
});
|
|
2303
|
+
newThreadBtn.addEventListener('click', showNewThreadInput);
|
|
714
2304
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
panel.style.display = showing ? 'none' : 'flex';
|
|
720
|
-
if (!showing && !connection) {
|
|
721
|
-
startConnection();
|
|
722
|
-
}
|
|
723
|
-
if (!showing) {
|
|
724
|
-
input.focus();
|
|
725
|
-
scrollToBottom();
|
|
726
|
-
}
|
|
727
|
-
});
|
|
2305
|
+
newThreadInput.addEventListener('keydown', function (e) {
|
|
2306
|
+
if (e.key === 'Enter') { e.preventDefault(); submitNewThread(); }
|
|
2307
|
+
if (e.key === 'Escape') { hideNewThreadInput(); }
|
|
2308
|
+
});
|
|
728
2309
|
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
}
|
|
2310
|
+
newThreadInput.addEventListener('blur', function () {
|
|
2311
|
+
// Small delay so click on submit doesn't race with blur
|
|
2312
|
+
setTimeout(function () {
|
|
2313
|
+
if (newThreadCreating) hideNewThreadInput();
|
|
2314
|
+
}, 150);
|
|
2315
|
+
});
|
|
737
2316
|
|
|
738
|
-
//
|
|
2317
|
+
// ── Auth events ──
|
|
739
2318
|
var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
|
|
740
|
-
var
|
|
2319
|
+
var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
|
|
741
2320
|
|
|
742
2321
|
function submitAuth() {
|
|
743
2322
|
var key = authInput.value.trim();
|
|
@@ -749,30 +2328,24 @@
|
|
|
749
2328
|
startConnection();
|
|
750
2329
|
}
|
|
751
2330
|
|
|
752
|
-
|
|
2331
|
+
authSubmitBtn.addEventListener('click', submitAuth);
|
|
753
2332
|
authInput.addEventListener('keydown', function (e) {
|
|
754
|
-
if (e.key === 'Enter') {
|
|
755
|
-
e.preventDefault();
|
|
756
|
-
submitAuth();
|
|
757
|
-
}
|
|
2333
|
+
if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
|
|
758
2334
|
});
|
|
759
2335
|
|
|
760
|
-
// Logout
|
|
761
|
-
var logoutBtn =
|
|
2336
|
+
// ── Logout ──
|
|
2337
|
+
var logoutBtn = appLayout.querySelector('[data-testid="webchat-logout"]');
|
|
762
2338
|
if (logoutBtn) {
|
|
763
2339
|
logoutBtn.addEventListener('click', function () {
|
|
764
2340
|
clearStoredApiKey();
|
|
765
2341
|
activeApiKey = '';
|
|
766
2342
|
if (connection) { connection.close(); connection = null; }
|
|
767
|
-
messages = [];
|
|
768
|
-
streaming = false;
|
|
769
|
-
streamBuffer = '';
|
|
770
2343
|
showAuthView();
|
|
771
2344
|
authInput.value = '';
|
|
772
2345
|
});
|
|
773
2346
|
}
|
|
774
2347
|
|
|
775
|
-
//
|
|
2348
|
+
// ── Connection ──
|
|
776
2349
|
function startConnection() {
|
|
777
2350
|
if (connection) { connection.close(); connection = null; }
|
|
778
2351
|
connection = createConnection({
|
|
@@ -781,37 +2354,33 @@
|
|
|
781
2354
|
sessionId: sessionId,
|
|
782
2355
|
onMessage: handleServerMessage,
|
|
783
2356
|
onStatus: updateStatus,
|
|
2357
|
+
skipHistory: true, // standalone manages history per-thread
|
|
784
2358
|
});
|
|
785
2359
|
}
|
|
786
2360
|
|
|
787
|
-
//
|
|
2361
|
+
// ── Mount / Destroy ──
|
|
788
2362
|
function mount(target) {
|
|
789
2363
|
(target || document.body).appendChild(container);
|
|
790
|
-
if (
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
// No key — show auth panel
|
|
796
|
-
showAuthView();
|
|
797
|
-
authInput.focus();
|
|
798
|
-
}
|
|
2364
|
+
if (activeApiKey) {
|
|
2365
|
+
startConnection();
|
|
2366
|
+
} else {
|
|
2367
|
+
showAuthView();
|
|
2368
|
+
authInput.focus();
|
|
799
2369
|
}
|
|
800
2370
|
}
|
|
801
2371
|
|
|
802
2372
|
function destroy() {
|
|
803
2373
|
if (connection) connection.close();
|
|
2374
|
+
if (refreshTimer) clearInterval(refreshTimer);
|
|
804
2375
|
container.remove();
|
|
805
2376
|
}
|
|
806
2377
|
|
|
807
|
-
return { mount: mount, destroy: destroy
|
|
2378
|
+
return { mount: mount, destroy: destroy };
|
|
808
2379
|
}
|
|
809
2380
|
|
|
810
|
-
// ─── Auto-mount
|
|
2381
|
+
// ─── Auto-mount ──────────────────────────────────────────────────────────────
|
|
811
2382
|
if (script) {
|
|
812
|
-
var widget = createWidget({
|
|
813
|
-
standalone: STANDALONE,
|
|
814
|
-
});
|
|
2383
|
+
var widget = createWidget({ standalone: STANDALONE });
|
|
815
2384
|
|
|
816
2385
|
if (document.readyState === 'loading') {
|
|
817
2386
|
document.addEventListener('DOMContentLoaded', function () {
|
|
@@ -821,7 +2390,6 @@
|
|
|
821
2390
|
widget.mount();
|
|
822
2391
|
}
|
|
823
2392
|
|
|
824
|
-
// Expose for programmatic access
|
|
825
2393
|
window.CumulusChat = widget;
|
|
826
2394
|
}
|
|
827
2395
|
})();
|