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