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