sagedesk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1454 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/vanilla/index.ts
31
+ var vanilla_exports = {};
32
+ __export(vanilla_exports, {
33
+ SageDeskWidget: () => SageDeskWidget,
34
+ init: () => init
35
+ });
36
+ module.exports = __toCommonJS(vanilla_exports);
37
+
38
+ // src/vanilla/ui.ts
39
+ function getWidgetStyles(accent, theme = "classic") {
40
+ if (theme === "light") return getLightStyles(accent);
41
+ if (theme === "dark") return getDarkStyles(accent);
42
+ return getClassicStyles(accent);
43
+ }
44
+ function getClassicStyles(accent) {
45
+ return `
46
+ :host {
47
+ display: block;
48
+ position: fixed;
49
+ inset: 0;
50
+ z-index: 9999;
51
+ pointer-events: none;
52
+ overflow: visible;
53
+ font-family: inherit;
54
+ --sd-accent: ${accent};
55
+ }
56
+
57
+ *, *::before, *::after { box-sizing: border-box; font-family: inherit; }
58
+
59
+ /* \u2500\u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
60
+
61
+ .sd-trigger {
62
+ position: fixed;
63
+ bottom: 28px;
64
+ right: 24px;
65
+ width: 56px;
66
+ height: 56px;
67
+ border-radius: 50%;
68
+ background: linear-gradient(135deg, ${accent} 0%, color-mix(in oklab, ${accent} 78%, #1a1340) 100%);
69
+ border: none;
70
+ cursor: pointer;
71
+ display: grid;
72
+ place-items: center;
73
+ pointer-events: all;
74
+ transition: transform 150ms ease;
75
+ z-index: 9999;
76
+ outline: none;
77
+ padding: 0;
78
+ color: #fff;
79
+ box-shadow: 0 14px 28px -6px color-mix(in oklab, ${accent} 55%, transparent), 0 4px 10px rgba(40,30,90,0.15);
80
+ }
81
+ .sd-trigger:hover { transform: scale(1.06); }
82
+ .sd-trigger:focus-visible { outline: 2px solid ${accent}; outline-offset: 3px; }
83
+ .sd-trigger.pos-left { right: auto; left: 24px; }
84
+
85
+ /* \u2500\u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
86
+
87
+ .sd-panel {
88
+ position: fixed;
89
+ bottom: 96px;
90
+ right: 24px;
91
+ width: 380px;
92
+ height: 580px;
93
+ max-height: 580px;
94
+ border-radius: 20px;
95
+ border: 1px solid rgba(20,20,40,0.04);
96
+ background: #fff;
97
+ box-shadow: 0 1px 0 rgba(20,20,40,0.04), 0 24px 48px -16px rgba(40,30,90,0.22), 0 4px 14px rgba(40,30,90,0.08);
98
+ overflow: hidden;
99
+ display: flex;
100
+ flex-direction: column;
101
+ pointer-events: all;
102
+ z-index: 9999;
103
+ transform-origin: bottom right;
104
+ }
105
+ .sd-panel.pos-left { right: auto; left: 24px; transform-origin: bottom left; }
106
+ .sd-panel[data-open="true"] { animation: sd-panel-open 200ms cubic-bezier(0.34,1.56,0.64,1) both; }
107
+ .sd-panel[data-closing="true"] { animation: sd-panel-close 150ms ease-in both; }
108
+ .sd-panel[data-open="false"]:not([data-closing="true"]) { display: none; }
109
+
110
+ @keyframes sd-panel-open { from { transform: scale(0.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
111
+ @keyframes sd-panel-close { from { transform: scale(1) translateY(0); opacity: 1; } to { transform: scale(0.94) translateY(6px); opacity: 0; } }
112
+
113
+ /* \u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
114
+
115
+ .sd-header {
116
+ padding: 18px 20px;
117
+ background: linear-gradient(135deg, ${accent} 0%, color-mix(in oklab, ${accent} 78%, #1a1340) 100%);
118
+ color: #fff;
119
+ display: flex;
120
+ align-items: center;
121
+ gap: 12px;
122
+ flex-shrink: 0;
123
+ position: relative;
124
+ }
125
+
126
+ .sd-avatar-wrap { position: relative; width: 40px; height: 40px; flex-shrink: 0; }
127
+
128
+ .sd-avatar {
129
+ width: 40px;
130
+ height: 40px;
131
+ border-radius: 50%;
132
+ background: rgba(255,255,255,0.18);
133
+ display: grid;
134
+ place-items: center;
135
+ overflow: hidden;
136
+ }
137
+ .sd-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
138
+
139
+ .sd-online-dot {
140
+ position: absolute;
141
+ right: -1px;
142
+ bottom: -1px;
143
+ width: 12px;
144
+ height: 12px;
145
+ border-radius: 50%;
146
+ background: #22c55e;
147
+ box-shadow: 0 0 0 2.5px ${accent};
148
+ }
149
+
150
+ .sd-agent-info { flex: 1; min-width: 0; }
151
+ .sd-agent-name { font-size: 15px; font-weight: 600; color: #fff; line-height: 1.2; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; padding: 0; letter-spacing: -0.01em; }
152
+ .sd-status { font-size: 12px; color: rgba(255,255,255,0.85); display: flex; align-items: center; gap: 6px; margin-top: 3px; }
153
+ .sd-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #4ade80; flex-shrink: 0; box-shadow: 0 0 0 2px rgba(74,222,128,0.25); }
154
+
155
+ .sd-close {
156
+ width: 30px; height: 30px;
157
+ background: rgba(255,255,255,0.14);
158
+ border: none; color: #fff; border-radius: 8px;
159
+ cursor: pointer; display: grid; place-items: center;
160
+ flex-shrink: 0; outline: none; padding: 0;
161
+ transition: background 120ms;
162
+ }
163
+ .sd-close:hover { background: rgba(255,255,255,0.22); }
164
+ .sd-close:focus-visible { outline: 2px solid rgba(255,255,255,0.6); }
165
+
166
+ /* \u2500\u2500\u2500 Thread \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
167
+
168
+ .sd-thread {
169
+ flex: 1; overflow-y: auto; overflow-x: hidden;
170
+ padding: 22px 18px 18px;
171
+ background: #fbfbfa;
172
+ min-height: 280px;
173
+ display: flex; flex-direction: column; gap: 18px;
174
+ scrollbar-width: thin; scrollbar-color: rgba(20,20,40,0.12) transparent;
175
+ overscroll-behavior: contain;
176
+ }
177
+ .sd-thread::-webkit-scrollbar { width: 4px; }
178
+ .sd-thread::-webkit-scrollbar-track { background: transparent; }
179
+ .sd-thread::-webkit-scrollbar-thumb { background: rgba(20,20,40,0.12); border-radius: 2px; }
180
+
181
+ /* \u2500\u2500\u2500 Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
182
+
183
+ .sd-msg-wrap { display: flex; flex-direction: column; }
184
+ .sd-msg-wrap.bot { align-items: flex-start; }
185
+ .sd-msg-wrap.user { align-items: flex-end; }
186
+
187
+ .sd-fallback-label { font-size: 11px; font-weight: 500; color: #9b9aa3; margin: 0 0 4px; padding: 0 4px; }
188
+
189
+ .sd-bubble {
190
+ max-width: 82%; padding: 10px 14px;
191
+ font-size: 14px; line-height: 1.5;
192
+ word-break: break-word; white-space: pre-wrap;
193
+ }
194
+ .sd-bubble.bot {
195
+ background: #fff;
196
+ border: 1px solid rgba(20,20,40,0.06);
197
+ border-radius: 16px 16px 16px 6px;
198
+ color: #1a1a2e;
199
+ box-shadow: 0 1px 2px rgba(20,20,40,0.04);
200
+ }
201
+ .sd-bubble.user {
202
+ background: ${accent};
203
+ color: #fff;
204
+ border-radius: 16px 16px 6px 16px;
205
+ box-shadow: 0 6px 16px -6px color-mix(in oklab, ${accent} 60%, transparent);
206
+ }
207
+
208
+ .sd-timestamp { font-size: 11px; color: #a8a8b0; padding: 0 4px; margin-top: 4px; font-variant-numeric: tabular-nums; }
209
+ .sd-msg-wrap.user .sd-timestamp { padding: 0 4px 0 0; }
210
+ .sd-msg-wrap.bot .sd-timestamp { padding: 0 0 0 4px; }
211
+
212
+ /* \u2500\u2500\u2500 Typing indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
213
+
214
+ .sd-typing {
215
+ display: flex; align-items: center; gap: 4px;
216
+ padding: 10px 14px;
217
+ background: #fff; border: 1px solid rgba(20,20,40,0.06);
218
+ border-radius: 16px 16px 16px 6px;
219
+ box-shadow: 0 1px 2px rgba(20,20,40,0.04);
220
+ align-self: flex-start;
221
+ }
222
+ .sd-dot { width: 6px; height: 6px; border-radius: 50%; background: #c8c8ce; animation: sd-bounce 1.2s ease-in-out infinite; }
223
+ .sd-dot:nth-child(2) { animation-delay: 0.2s; }
224
+ .sd-dot:nth-child(3) { animation-delay: 0.4s; }
225
+ @keyframes sd-bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-5px)} }
226
+
227
+ /* \u2500\u2500\u2500 Chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
228
+
229
+ .sd-chips { padding: 0 18px 16px; background: #fbfbfa; flex-shrink: 0; }
230
+ .sd-chips.scrollable { padding: 0 0 16px; }
231
+ .sd-chips-label { font-size: 11px; color: #9b9aa3; letter-spacing: 0.06em; text-transform: uppercase; font-weight: 500; padding-left: 4px; margin-bottom: 6px; }
232
+ .sd-chips.scrollable .sd-chips-label { padding-left: 18px; }
233
+ .sd-chips-row { display: flex; flex-wrap: wrap; gap: 6px; }
234
+ .sd-chips.scrollable .sd-chips-row {
235
+ flex-wrap: nowrap;
236
+ overflow-x: auto;
237
+ padding: 0 18px;
238
+ scrollbar-width: none;
239
+ -ms-overflow-style: none;
240
+ }
241
+ .sd-chips.scrollable .sd-chips-row::-webkit-scrollbar { display: none; }
242
+ .sd-chips.scrollable .sd-chip { flex-shrink: 0; }
243
+
244
+ .sd-chip {
245
+ font-size: 12.5px; padding: 7px 12px; border-radius: 999px;
246
+ background: #fff; border: 1px solid color-mix(in oklab, ${accent} 24%, transparent);
247
+ color: ${accent}; cursor: pointer; font-weight: 500; letter-spacing: -0.005em;
248
+ outline: none; transition: opacity 100ms;
249
+ }
250
+ .sd-chip:hover { opacity: 0.8; }
251
+ .sd-chip:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
252
+
253
+ /* \u2500\u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
254
+
255
+ .sd-composer { padding: 14px 14px 16px; border-top: 1px solid rgba(20,20,40,0.06); background: #fff; flex-shrink: 0; }
256
+
257
+ .sd-input-wrap {
258
+ display: flex; align-items: center; gap: 6px;
259
+ background: #f5f4f0; border-radius: 12px; padding: 5px 6px 5px 14px;
260
+ border: 1px solid transparent; transition: border-color .15s, box-shadow .15s;
261
+ }
262
+ .sd-input-wrap:focus-within {
263
+ border-color: color-mix(in oklab, ${accent} 30%, transparent);
264
+ box-shadow: 0 0 0 4px color-mix(in oklab, ${accent} 12%, transparent);
265
+ }
266
+
267
+ .sd-input { flex: 1; background: transparent; border: none; outline: none; font-size: 14px; padding: 8px 0; color: #1a1a2e; min-width: 0; }
268
+ .sd-input::placeholder { color: #a8a8b0; }
269
+
270
+ .sd-send {
271
+ width: 32px; height: 32px; border-radius: 8px;
272
+ background: color-mix(in oklab, ${accent} 22%, #e5e3dc);
273
+ border: none; color: #fff; cursor: pointer; display: grid; place-items: center;
274
+ flex-shrink: 0; padding: 0; outline: none; transition: background .15s, opacity .1s, transform .1s;
275
+ }
276
+ .sd-send.active { background: ${accent}; }
277
+ .sd-send:hover { opacity: 0.85; }
278
+ .sd-send:active { transform: scale(0.95); }
279
+ .sd-send:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
280
+
281
+ /* \u2500\u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
282
+
283
+ .sd-footer {
284
+ height: 34px; background: #fff; border-top: 1px solid rgba(20,20,40,0.04);
285
+ display: flex; align-items: center; justify-content: center;
286
+ font-size: 11px; color: #a8a8b0; gap: 5px; flex-shrink: 0;
287
+ }
288
+ .sd-footer[hidden] { display: none; }
289
+ .sd-footer-link { color: #5a5a64; font-weight: 500; text-decoration: none; }
290
+ .sd-footer-link:hover { text-decoration: underline; }
291
+
292
+ /* \u2500\u2500\u2500 Mobile \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
293
+
294
+ @media (max-width: 420px) {
295
+ .sd-panel {
296
+ bottom: 0; right: 0; left: 0; width: auto; max-width: 100%;
297
+ height: auto; max-height: 85vh;
298
+ border-radius: 20px 20px 0 0;
299
+ transform-origin: bottom center;
300
+ }
301
+ .sd-panel.pos-left { right: 0; transform-origin: bottom center; }
302
+ }
303
+ `;
304
+ }
305
+ function getLightStyles(accent) {
306
+ return `
307
+ :host {
308
+ display: block; position: fixed; inset: 0;
309
+ z-index: 9999; pointer-events: none; overflow: visible; font-family: inherit;
310
+ --sd-accent: ${accent};
311
+ }
312
+ *, *::before, *::after { box-sizing: border-box; font-family: inherit; }
313
+
314
+ /* \u2500\u2500\u2500 Trigger (pill) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
315
+
316
+ .sd-trigger {
317
+ position: fixed; bottom: 28px; right: 24px;
318
+ height: 52px; padding: 0 8px 0 20px; border-radius: 999px;
319
+ background: #fdfcf9; border: 1px solid rgba(20,20,40,0.08);
320
+ cursor: pointer; display: flex; align-items: center; gap: 14px;
321
+ pointer-events: all; z-index: 9999; font-family: inherit;
322
+ box-shadow: 0 12px 26px -8px rgba(40,30,90,0.18), 0 2px 8px rgba(40,30,90,0.06);
323
+ transition: box-shadow 150ms ease; outline: none;
324
+ }
325
+ .sd-trigger:hover { box-shadow: 0 16px 32px -10px rgba(40,30,90,0.24), 0 2px 10px rgba(40,30,90,0.08); }
326
+ .sd-trigger:focus-visible { outline: 2px solid ${accent}; outline-offset: 3px; }
327
+ .sd-trigger.pos-left { right: auto; left: 24px; }
328
+
329
+ .sd-trigger-label { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; }
330
+ .sd-trigger-label-main { font-size: 14px; font-weight: 600; color: #1a1a2e; }
331
+ .sd-trigger-label-sub { font-size: 11px; color: #9b9aa3; margin-top: 2px; }
332
+
333
+ .sd-trigger-circle {
334
+ width: 36px; height: 36px; border-radius: 50%;
335
+ background: ${accent}; color: #fff;
336
+ display: grid; place-items: center; flex-shrink: 0;
337
+ }
338
+
339
+ /* \u2500\u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
340
+
341
+ .sd-panel {
342
+ position: fixed; bottom: 92px; right: 24px;
343
+ width: 400px; height: 580px; max-height: 580px; border-radius: 22px;
344
+ border: 1px solid rgba(20,20,40,0.05);
345
+ background: #fdfcf9;
346
+ box-shadow: 0 1px 0 rgba(20,20,40,0.04), 0 30px 60px -20px rgba(40,30,90,0.18), 0 6px 16px rgba(40,30,90,0.06);
347
+ overflow: hidden; display: flex; flex-direction: column;
348
+ pointer-events: all; z-index: 9999; transform-origin: bottom right;
349
+ }
350
+ .sd-panel.pos-left { right: auto; left: 24px; transform-origin: bottom left; }
351
+ .sd-panel[data-open="true"] { animation: sd-panel-open 200ms cubic-bezier(0.34,1.56,0.64,1) both; }
352
+ .sd-panel[data-closing="true"] { animation: sd-panel-close 150ms ease-in both; }
353
+ .sd-panel[data-open="false"]:not([data-closing="true"]) { display: none; }
354
+ @keyframes sd-panel-open { from { transform: scale(0.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
355
+ @keyframes sd-panel-close { from { transform: scale(1) translateY(0); opacity: 1; } to { transform: scale(0.94) translateY(6px); opacity: 0; } }
356
+
357
+ /* \u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
358
+
359
+ .sd-header {
360
+ padding: 18px 20px 14px; background: #fdfcf9;
361
+ border-bottom: 1px solid rgba(20,20,40,0.05);
362
+ display: flex; align-items: center; gap: 12px; flex-shrink: 0;
363
+ }
364
+ .sd-avatar-wrap { width: 36px; height: 36px; flex-shrink: 0; }
365
+ .sd-avatar {
366
+ width: 36px; height: 36px; border-radius: 50%; overflow: hidden;
367
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 55%, #fff));
368
+ display: grid; place-items: center; color: #fff;
369
+ }
370
+ .sd-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
371
+ .sd-agent-info { flex: 1; min-width: 0; }
372
+ .sd-agent-name { font-size: 14.5px; font-weight: 600; color: #1a1a2e; letter-spacing: -0.01em; margin: 0; padding: 0; }
373
+ .sd-status { font-size: 12px; color: #7a7a82; display: flex; align-items: center; gap: 6px; margin-top: 2px; }
374
+ .sd-status-dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; flex-shrink: 0; }
375
+ .sd-close {
376
+ width: 30px; height: 30px; background: transparent; border: none;
377
+ color: #9b9aa3; border-radius: 8px; cursor: pointer;
378
+ display: grid; place-items: center; padding: 0; outline: none;
379
+ }
380
+ .sd-close:hover { color: #5a5a64; }
381
+ .sd-close:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
382
+
383
+ /* \u2500\u2500\u2500 Thread \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
384
+
385
+ .sd-thread {
386
+ flex: 1; overflow-y: auto; overflow-x: hidden;
387
+ padding: 22px 20px 16px; min-height: 280px;
388
+ display: flex; flex-direction: column; gap: 22px;
389
+ scrollbar-width: thin; scrollbar-color: rgba(20,20,40,0.1) transparent;
390
+ overscroll-behavior: contain;
391
+ }
392
+ .sd-thread::-webkit-scrollbar { width: 4px; }
393
+ .sd-thread::-webkit-scrollbar-thumb { background: rgba(20,20,40,0.1); border-radius: 2px; }
394
+
395
+ /* \u2500\u2500\u2500 Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
396
+
397
+ .sd-msg-wrap { display: flex; flex-direction: column; }
398
+ .sd-msg-wrap.bot { align-items: flex-start; flex-direction: row; gap: 10px; }
399
+ .sd-msg-wrap.user { align-items: flex-end; }
400
+
401
+ /* Bot: avatar + text column */
402
+ .sd-bot-avatar {
403
+ width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0; margin-top: 2px;
404
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 60%, #fff));
405
+ }
406
+ .sd-bot-body { flex: 1; min-width: 0; }
407
+ .sd-bot-meta { display: flex; align-items: baseline; gap: 8px; margin-bottom: 4px; }
408
+ .sd-bot-name { font-size: 13px; font-weight: 600; color: #1a1a2e; }
409
+ .sd-timestamp { font-size: 11px; color: #a8a89e; font-variant-numeric: tabular-nums; }
410
+
411
+ .sd-fallback-label { font-size: 11px; color: #9b9aa3; margin: 0 0 4px; }
412
+
413
+ .sd-bubble { word-break: break-word; white-space: pre-wrap; }
414
+ .sd-bubble.bot { font-size: 14px; line-height: 1.55; color: #2a2a36; }
415
+ .sd-bubble.user {
416
+ max-width: 78%; padding: 11px 15px; font-size: 14px; line-height: 1.5;
417
+ border-radius: 16px 16px 4px 16px;
418
+ background: color-mix(in oklab, ${accent} 10%, white);
419
+ color: ${accent};
420
+ border: 1px solid color-mix(in oklab, ${accent} 22%, transparent);
421
+ font-weight: 500;
422
+ }
423
+
424
+ /* \u2500\u2500\u2500 Typing indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
425
+
426
+ .sd-typing {
427
+ display: flex; align-items: flex-start; gap: 10px;
428
+ }
429
+ .sd-typing-avatar {
430
+ width: 28px; height: 28px; border-radius: 50%; flex-shrink: 0; margin-top: 2px;
431
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 60%, #fff));
432
+ }
433
+ .sd-typing-dots {
434
+ display: inline-flex; align-items: center; gap: 4px;
435
+ padding: 10px 14px; border-radius: 16px 16px 16px 4px;
436
+ background: #fff; border: 1px solid rgba(20,20,40,0.07);
437
+ box-shadow: 0 1px 2px rgba(20,20,40,0.04);
438
+ }
439
+ .sd-dot { width: 6px; height: 6px; border-radius: 50%; background: #c4c4be; animation: sd-bounce 1.2s ease-in-out infinite; }
440
+ .sd-dot:nth-child(2) { animation-delay: 0.2s; }
441
+ .sd-dot:nth-child(3) { animation-delay: 0.4s; }
442
+ @keyframes sd-bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-5px)} }
443
+
444
+ /* \u2500\u2500\u2500 Chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
445
+
446
+ .sd-chips { padding: 0 20px 14px; display: flex; flex-wrap: wrap; gap: 6px; flex-shrink: 0; }
447
+ .sd-chips.scrollable {
448
+ flex-wrap: nowrap;
449
+ overflow-x: auto;
450
+ scrollbar-width: none;
451
+ -ms-overflow-style: none;
452
+ }
453
+ .sd-chips.scrollable::-webkit-scrollbar { display: none; }
454
+ .sd-chips.scrollable .sd-chip { flex-shrink: 0; }
455
+
456
+ .sd-chip {
457
+ font-size: 12px; padding: 5px 10px; border-radius: 6px;
458
+ background: #f4f3ee; border: none; color: #5a5a64;
459
+ cursor: pointer; font-weight: 500; outline: none; transition: opacity 100ms;
460
+ }
461
+ .sd-chip:hover { opacity: 0.75; }
462
+ .sd-chip:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
463
+
464
+ /* \u2500\u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
465
+
466
+ .sd-composer { padding: 14px; border-top: 1px solid rgba(20,20,40,0.05); background: #fdfcf9; flex-shrink: 0; }
467
+ .sd-input-wrap {
468
+ background: #fff; border-radius: 14px; padding: 10px 12px;
469
+ border: 1px solid rgba(20,20,40,0.1); transition: border-color .15s, box-shadow .15s;
470
+ }
471
+ .sd-input-wrap:focus-within {
472
+ border-color: color-mix(in oklab, ${accent} 40%, transparent);
473
+ box-shadow: 0 0 0 4px color-mix(in oklab, ${accent} 12%, transparent);
474
+ }
475
+ .sd-input { width: 100%; background: transparent; border: none; outline: none; font-size: 14px; color: #1a1a2e; display: block; }
476
+ .sd-input::placeholder { color: #a8a89e; }
477
+ .sd-input-actions { display: flex; align-items: center; margin-top: 8px; }
478
+ .sd-send {
479
+ width: 28px; height: 28px; border-radius: 7px; margin-left: auto;
480
+ background: #e8e7e0; border: none; color: #a8a89e;
481
+ cursor: pointer; display: grid; place-items: center; padding: 0; outline: none;
482
+ transition: background .15s, color .15s, opacity .1s, transform .1s;
483
+ }
484
+ .sd-send.active { background: ${accent}; color: #fff; }
485
+ .sd-send:hover { opacity: 0.85; }
486
+ .sd-send:active { transform: scale(0.95); }
487
+ .sd-send:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
488
+
489
+ /* \u2500\u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
490
+
491
+ .sd-footer {
492
+ height: 34px; background: #fdfcf9; border-top: 1px solid rgba(20,20,40,0.04);
493
+ display: flex; align-items: center; justify-content: center;
494
+ font-size: 11px; color: #a8a89e; gap: 5px; flex-shrink: 0;
495
+ }
496
+ .sd-footer[hidden] { display: none; }
497
+ .sd-footer-link { color: #5a5a64; font-weight: 500; text-decoration: none; }
498
+ .sd-footer-link:hover { text-decoration: underline; }
499
+
500
+ @media (max-width: 420px) {
501
+ .sd-panel { bottom: 0; right: 0; left: 0; width: auto; max-width: 100%; height: auto; max-height: 85vh; border-radius: 22px 22px 0 0; transform-origin: bottom center; }
502
+ .sd-panel.pos-left { right: 0; transform-origin: bottom center; }
503
+ }
504
+ `;
505
+ }
506
+ function getDarkStyles(accent) {
507
+ return `
508
+ :host {
509
+ display: block; position: fixed; inset: 0;
510
+ z-index: 9999; pointer-events: none; overflow: visible; font-family: inherit;
511
+ --sd-accent: ${accent};
512
+ }
513
+ *, *::before, *::after { box-sizing: border-box; font-family: inherit; }
514
+
515
+ /* \u2500\u2500\u2500 Launcher glow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
516
+
517
+ .sd-trigger-glow {
518
+ position: fixed; bottom: 18px; right: 18px;
519
+ width: 80px; height: 80px; border-radius: 50%;
520
+ background: radial-gradient(circle, color-mix(in oklab, ${accent} 50%, transparent), transparent 70%);
521
+ filter: blur(10px); pointer-events: none; z-index: 9998;
522
+ }
523
+ .sd-trigger-glow.pos-left { right: auto; left: 18px; }
524
+
525
+ /* \u2500\u2500\u2500 Trigger \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
526
+
527
+ .sd-trigger {
528
+ position: fixed; bottom: 28px; right: 24px;
529
+ width: 60px; height: 60px; border-radius: 50%;
530
+ background: linear-gradient(135deg, color-mix(in oklab, ${accent} 70%, #1a1340), #1a1a2e);
531
+ border: 1px solid rgba(255,255,255,0.12); color: #fff;
532
+ cursor: pointer; display: grid; place-items: center;
533
+ pointer-events: all; z-index: 9999; padding: 0;
534
+ transition: transform 150ms ease; outline: none;
535
+ box-shadow: 0 16px 32px -8px rgba(0,0,0,0.5), inset 0 1px 0 rgba(255,255,255,0.1);
536
+ }
537
+ .sd-trigger:hover { transform: scale(1.04); }
538
+ .sd-trigger:focus-visible { outline: 2px solid ${accent}; outline-offset: 3px; }
539
+ .sd-trigger.pos-left { right: auto; left: 24px; }
540
+
541
+ /* \u2500\u2500\u2500 Panel \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
542
+
543
+ .sd-panel {
544
+ position: fixed; bottom: 100px; right: 24px;
545
+ width: 380px; height: 580px; max-height: 580px; border-radius: 22px;
546
+ background: rgba(18, 16, 32, 0.86);
547
+ backdrop-filter: blur(40px) saturate(180%);
548
+ -webkit-backdrop-filter: blur(40px) saturate(180%);
549
+ box-shadow: 0 30px 60px -20px rgba(0,0,0,0.4), 0 4px 14px rgba(0,0,0,0.2), inset 0 1px 0 rgba(255,255,255,0.06);
550
+ border: 1px solid rgba(255,255,255,0.08);
551
+ overflow: hidden; display: flex; flex-direction: column;
552
+ pointer-events: all; z-index: 9999; color: #fff;
553
+ transform-origin: bottom right;
554
+ }
555
+ .sd-panel.pos-left { right: auto; left: 24px; transform-origin: bottom left; }
556
+ .sd-panel[data-open="true"] { animation: sd-panel-open 200ms cubic-bezier(0.34,1.56,0.64,1) both; }
557
+ .sd-panel[data-closing="true"] { animation: sd-panel-close 150ms ease-in both; }
558
+ .sd-panel[data-open="false"]:not([data-closing="true"]) { display: none; }
559
+ @keyframes sd-panel-open { from { transform: scale(0.94) translateY(6px); opacity: 0; } to { transform: scale(1) translateY(0); opacity: 1; } }
560
+ @keyframes sd-panel-close { from { transform: scale(1) translateY(0); opacity: 1; } to { transform: scale(0.94) translateY(6px); opacity: 0; } }
561
+
562
+ /* \u2500\u2500\u2500 Panel top-glow overlay \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
563
+
564
+ .sd-panel-glow {
565
+ position: absolute; top: 0; left: 0; right: 0; height: 200px; pointer-events: none;
566
+ background: radial-gradient(ellipse 80% 100% at 50% 0%, color-mix(in oklab, ${accent} 40%, transparent) 0%, transparent 70%);
567
+ }
568
+
569
+ /* \u2500\u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
570
+
571
+ .sd-header {
572
+ padding: 20px 20px 16px; display: flex; align-items: center; gap: 12px;
573
+ position: relative; z-index: 1; flex-shrink: 0;
574
+ }
575
+ .sd-avatar-wrap { position: relative; flex-shrink: 0; }
576
+ .sd-avatar {
577
+ width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; overflow: hidden;
578
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 50%, #fff));
579
+ box-shadow: 0 0 24px color-mix(in oklab, ${accent} 50%, transparent);
580
+ }
581
+ .sd-avatar-img { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
582
+ .sd-online-dot {
583
+ position: absolute; right: -2px; bottom: -2px;
584
+ width: 12px; height: 12px; border-radius: 50%;
585
+ background: #22c55e; box-shadow: 0 0 0 2.5px rgba(18,16,32,1);
586
+ }
587
+ .sd-agent-info { flex: 1; }
588
+ .sd-agent-name { font-size: 15px; font-weight: 600; letter-spacing: -0.01em; margin: 0; padding: 0; }
589
+ .sd-status { font-size: 12px; color: rgba(255,255,255,0.55); display: flex; align-items: center; gap: 6px; margin-top: 3px; }
590
+ .sd-status-dot { width: 5px; height: 5px; border-radius: 50%; background: #4ade80; box-shadow: 0 0 6px #4ade80; flex-shrink: 0; }
591
+ .sd-close {
592
+ width: 30px; height: 30px;
593
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08);
594
+ color: rgba(255,255,255,0.7); border-radius: 8px;
595
+ cursor: pointer; display: grid; place-items: center; padding: 0; outline: none;
596
+ }
597
+ .sd-close:hover { background: rgba(255,255,255,0.1); color: #fff; }
598
+ .sd-close:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
599
+
600
+ /* \u2500\u2500\u2500 Thread \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
601
+
602
+ .sd-thread {
603
+ flex: 1; overflow-y: auto; overflow-x: hidden;
604
+ padding: 8px 20px 16px; min-height: 280px;
605
+ display: flex; flex-direction: column; gap: 14px;
606
+ position: relative; z-index: 1;
607
+ scrollbar-width: thin; scrollbar-color: rgba(255,255,255,0.1) transparent;
608
+ overscroll-behavior: contain;
609
+ }
610
+ .sd-thread::-webkit-scrollbar { width: 4px; }
611
+ .sd-thread::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 2px; }
612
+
613
+ /* \u2500\u2500\u2500 Messages \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
614
+
615
+ .sd-msg-wrap { display: flex; flex-direction: column; }
616
+ .sd-msg-wrap.bot { align-items: flex-start; }
617
+ .sd-msg-wrap.user { align-items: flex-end; }
618
+
619
+ .sd-fallback-label { font-size: 11px; color: rgba(255,255,255,0.5); margin: 0 0 4px; }
620
+
621
+ .sd-bubble { max-width: 85%; padding: 12px 14px; font-size: 14px; word-break: break-word; white-space: pre-wrap; }
622
+ .sd-bubble.bot {
623
+ line-height: 1.6; border-radius: 14px 14px 14px 6px;
624
+ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.06);
625
+ color: rgba(255,255,255,0.92);
626
+ }
627
+ .sd-bubble.user {
628
+ line-height: 1.5; border-radius: 14px 14px 6px 14px;
629
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 75%, #1a1340));
630
+ color: #fff;
631
+ box-shadow: 0 8px 20px -8px color-mix(in oklab, ${accent} 70%, transparent);
632
+ }
633
+
634
+ .sd-timestamp { font-size: 11px; color: rgba(255,255,255,0.3); padding: 0 4px; margin-top: 4px; font-variant-numeric: tabular-nums; }
635
+
636
+ /* \u2500\u2500\u2500 Typing indicator \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
637
+
638
+ .sd-typing {
639
+ max-width: 85%; padding: 12px 14px;
640
+ border-radius: 14px 14px 14px 6px;
641
+ background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.06);
642
+ display: flex; align-items: center; gap: 4px; align-self: flex-start;
643
+ }
644
+ .sd-dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,0.4); animation: sd-bounce 1.2s ease-in-out infinite; }
645
+ .sd-dot:nth-child(2) { animation-delay: 0.2s; }
646
+ .sd-dot:nth-child(3) { animation-delay: 0.4s; }
647
+ @keyframes sd-bounce { 0%,60%,100%{transform:translateY(0)} 30%{transform:translateY(-5px)} }
648
+
649
+ /* \u2500\u2500\u2500 "Try asking" chips \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
650
+
651
+ .sd-chips { padding: 0 20px 16px; position: relative; z-index: 1; flex-shrink: 0; }
652
+ .sd-chips.scrollable {
653
+ display: flex;
654
+ flex-direction: row;
655
+ flex-wrap: nowrap;
656
+ overflow-x: auto;
657
+ scrollbar-width: none;
658
+ -ms-overflow-style: none;
659
+ }
660
+ .sd-chips.scrollable::-webkit-scrollbar { display: none; }
661
+ .sd-chips.scrollable .sd-chips-label { display: none; }
662
+ .sd-chips.scrollable .sd-chip { flex-shrink: 0; margin-bottom: 0; width: auto; white-space: nowrap; }
663
+
664
+ .sd-chips-label { font-size: 11px; color: rgba(255,255,255,0.4); letter-spacing: 0.08em; text-transform: uppercase; font-weight: 500; margin-bottom: 8px; }
665
+ .sd-chip {
666
+ display: flex; align-items: center; gap: 8px;
667
+ width: 100%; text-align: left; padding: 10px 14px; border-radius: 10px;
668
+ background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.07);
669
+ color: rgba(255,255,255,0.85); cursor: pointer; font-size: 13px;
670
+ margin-bottom: 6px; outline: none; transition: background .12s;
671
+ }
672
+ .sd-chip:last-child { margin-bottom: 0; }
673
+ .sd-chip:hover { background: rgba(255,255,255,0.07); }
674
+ .sd-chip:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
675
+
676
+ /* \u2500\u2500\u2500 Composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
677
+
678
+ .sd-composer {
679
+ padding: 14px; border-top: 1px solid rgba(255,255,255,0.06);
680
+ position: relative; z-index: 1; flex-shrink: 0;
681
+ }
682
+ .sd-input-wrap {
683
+ display: flex; align-items: center; gap: 8px;
684
+ background: rgba(255,255,255,0.05);
685
+ border: 1px solid rgba(255,255,255,0.08);
686
+ border-radius: 12px; padding: 4px 4px 4px 14px;
687
+ transition: border-color .15s;
688
+ }
689
+ .sd-input-wrap:focus-within { border-color: color-mix(in oklab, ${accent} 60%, transparent); }
690
+ .sd-input { flex: 1; background: transparent; border: none; outline: none; font-size: 14px; padding: 9px 0; color: #fff; min-width: 0; }
691
+ .sd-input::placeholder { color: rgba(255,255,255,0.35); }
692
+ .sd-send {
693
+ width: 32px; height: 32px; border-radius: 9px; border: none; padding: 0;
694
+ background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.4);
695
+ cursor: pointer; display: grid; place-items: center; flex-shrink: 0;
696
+ transition: background .15s, color .15s, box-shadow .15s, opacity .1s, transform .1s; outline: none;
697
+ }
698
+ .sd-send.active {
699
+ background: linear-gradient(135deg, ${accent}, color-mix(in oklab, ${accent} 60%, #fff));
700
+ color: #fff;
701
+ box-shadow: 0 4px 12px -2px color-mix(in oklab, ${accent} 60%, transparent);
702
+ }
703
+ .sd-send:hover { opacity: 0.85; }
704
+ .sd-send:active { transform: scale(0.95); }
705
+ .sd-send:focus-visible { outline: 2px solid ${accent}; outline-offset: 2px; }
706
+
707
+ /* \u2500\u2500\u2500 Footer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
708
+
709
+ .sd-footer {
710
+ height: 34px; border-top: 1px solid rgba(255,255,255,0.05);
711
+ display: flex; align-items: center; justify-content: center;
712
+ font-size: 11px; color: rgba(255,255,255,0.35); gap: 5px; flex-shrink: 0;
713
+ position: relative; z-index: 1;
714
+ }
715
+ .sd-footer[hidden] { display: none; }
716
+ .sd-footer-link { color: rgba(255,255,255,0.7); font-weight: 500; text-decoration: none; }
717
+ .sd-footer-link:hover { text-decoration: underline; }
718
+
719
+ @media (max-width: 420px) {
720
+ .sd-panel { bottom: 0; right: 0; left: 0; width: auto; max-width: 100%; height: auto; max-height: 85vh; border-radius: 22px 22px 0 0; transform-origin: bottom center; }
721
+ .sd-panel.pos-left { right: 0; transform-origin: bottom center; }
722
+ }
723
+ `;
724
+ }
725
+ var ICON_CHAT = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 6.5A2.5 2.5 0 017.5 4h9A2.5 2.5 0 0119 6.5v7A2.5 2.5 0 0116.5 16H11l-4 3.5V16H7.5A2.5 2.5 0 015 13.5v-7z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>`;
726
+ var ICON_CHAT_SM = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 6.5A2.5 2.5 0 017.5 4h9A2.5 2.5 0 0119 6.5v7A2.5 2.5 0 0116.5 16H11l-4 3.5V16H7.5A2.5 2.5 0 015 13.5v-7z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>`;
727
+ var ICON_PERSON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 12.5a4 4 0 100-8 4 4 0 000 8z"/><path d="M5 20.5c0-3.5 3.13-6 7-6s7 2.5 7 6"/></svg>`;
728
+ var ICON_SEND = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 12L20 4l-4 16-4-7-8-1z" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round"/></svg>`;
729
+ var ICON_CLOSE = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`;
730
+ var ICON_BOT = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="4" y="8" width="16" height="12" rx="3" stroke="currentColor" stroke-width="1.6"/><circle cx="9" cy="13.5" r="1.5" fill="currentColor"/><circle cx="15" cy="13.5" r="1.5" fill="currentColor"/><path d="M9.5 16.5h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M12 8V5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="12" cy="4" r="1.2" fill="currentColor"/></svg>`;
731
+ var ICON_BOT_SM = `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="4" y="8" width="16" height="12" rx="3" stroke="currentColor" stroke-width="1.6"/><circle cx="9" cy="13.5" r="1.5" fill="currentColor"/><circle cx="15" cy="13.5" r="1.5" fill="currentColor"/><path d="M9.5 16.5h5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><path d="M12 8V5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/><circle cx="12" cy="4" r="1.2" fill="currentColor"/></svg>`;
732
+ var ICON_CHEVRON = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
733
+
734
+ // src/core/embedder.ts
735
+ var XENOVA_IDS = {
736
+ "all-MiniLM-L6-v2": "Xenova/all-MiniLM-L6-v2",
737
+ "bge-small-en-v1-5": "Xenova/bge-small-en-v1.5",
738
+ "paraphrase-multilingual-MiniLM-L12-v2": "Xenova/paraphrase-multilingual-MiniLM-L12-v2",
739
+ "all-mpnet-base-v2": "Xenova/all-mpnet-base-v2"
740
+ };
741
+ var _EmbedderRuntime = class _EmbedderRuntime {
742
+ constructor() {
743
+ this._ready = false;
744
+ this._failed = false;
745
+ }
746
+ async load(model = "all-MiniLM-L6-v2") {
747
+ if (this._ready) return;
748
+ if (this._failed) throw new Error("EmbedderRuntime previously failed to load");
749
+ if (_EmbedderRuntime._loadingPromise) {
750
+ await _EmbedderRuntime._loadingPromise;
751
+ this._ready = true;
752
+ return;
753
+ }
754
+ const modelId = XENOVA_IDS[model];
755
+ _EmbedderRuntime._loadingPromise = (async () => {
756
+ try {
757
+ const { pipeline } = await import("@huggingface/transformers");
758
+ _EmbedderRuntime._pipelineInstance = await pipeline(
759
+ "feature-extraction",
760
+ modelId,
761
+ { dtype: "q8" }
762
+ );
763
+ } catch (err) {
764
+ _EmbedderRuntime._loadingPromise = null;
765
+ _EmbedderRuntime._pipelineInstance = null;
766
+ throw err;
767
+ }
768
+ })();
769
+ try {
770
+ await _EmbedderRuntime._loadingPromise;
771
+ this._ready = true;
772
+ } catch (err) {
773
+ this._failed = true;
774
+ throw err;
775
+ }
776
+ }
777
+ async embed(text) {
778
+ if (!_EmbedderRuntime._pipelineInstance) {
779
+ await this.load();
780
+ }
781
+ try {
782
+ const output = await _EmbedderRuntime._pipelineInstance(text, {
783
+ pooling: "mean",
784
+ normalize: true
785
+ });
786
+ return output.data;
787
+ } catch (err) {
788
+ throw new Error(`Embedding failed: ${String(err)}`);
789
+ }
790
+ }
791
+ get isReady() {
792
+ return this._ready;
793
+ }
794
+ get hasFailed() {
795
+ return this._failed;
796
+ }
797
+ /** @internal */
798
+ static _reset() {
799
+ _EmbedderRuntime._pipelineInstance = null;
800
+ _EmbedderRuntime._loadingPromise = null;
801
+ }
802
+ };
803
+ // Module-level singleton so the WASM model is loaded at most once per page,
804
+ // regardless of how many widget instances exist.
805
+ _EmbedderRuntime._pipelineInstance = null;
806
+ _EmbedderRuntime._loadingPromise = null;
807
+ var EmbedderRuntime = _EmbedderRuntime;
808
+
809
+ // src/core/search.ts
810
+ function dotProduct(a, b) {
811
+ if (a.length !== b.length) {
812
+ throw new Error(`Vector dimension mismatch: query(${a.length}) vs index(${b.length})`);
813
+ }
814
+ let dot = 0;
815
+ for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
816
+ return dot;
817
+ }
818
+ function search(queryVector, index, topK = 3, minScore = 0.42) {
819
+ const results = [];
820
+ for (const chunk of index) {
821
+ const score = dotProduct(queryVector, chunk.vector384);
822
+ if (score < minScore) continue;
823
+ if (results.length < topK) {
824
+ results.push({ chunk, score });
825
+ results.sort((a, b) => b.score - a.score);
826
+ } else if (score > results[topK - 1].score) {
827
+ results[topK - 1] = { chunk, score };
828
+ results.sort((a, b) => b.score - a.score);
829
+ }
830
+ }
831
+ return results;
832
+ }
833
+ function keywordSearch(query, index, topK = 3) {
834
+ const terms = query.toLowerCase().split(/\s+/).filter((w) => w.length > 2).map((w) => w.replace(/[^a-z0-9]/g, ""));
835
+ if (terms.length === 0) return [];
836
+ const results = [];
837
+ for (const chunk of index) {
838
+ const chunkLower = chunk.textLower || chunk.text.toLowerCase();
839
+ const matchCount = terms.filter((t) => chunkLower.includes(t)).length;
840
+ const score = matchCount / terms.length;
841
+ if (score <= 0) continue;
842
+ if (results.length < topK) {
843
+ results.push({ chunk, score });
844
+ results.sort((a, b) => b.score - a.score);
845
+ } else if (score > results[topK - 1].score) {
846
+ results[topK - 1] = { chunk, score };
847
+ results.sort((a, b) => b.score - a.score);
848
+ }
849
+ }
850
+ return results;
851
+ }
852
+ async function loadIndex(url) {
853
+ const res = await fetch(url);
854
+ if (!res.ok) {
855
+ throw new Error(`Failed to fetch index (HTTP ${res.status}): ${url}`);
856
+ }
857
+ const data = await res.json();
858
+ const chunks = Array.isArray(data) ? data : data.chunks;
859
+ for (const chunk of chunks) {
860
+ chunk.textLower = chunk.text.toLowerCase();
861
+ if (Array.isArray(chunk.vector384)) {
862
+ chunk.vector384 = new Float32Array(chunk.vector384);
863
+ }
864
+ if (Array.isArray(chunk.vector768)) {
865
+ chunk.vector768 = new Float32Array(chunk.vector768);
866
+ }
867
+ }
868
+ return chunks;
869
+ }
870
+
871
+ // src/core/retriever.ts
872
+ async function fetchIndex(url) {
873
+ try {
874
+ return await loadIndex(url);
875
+ } catch (err) {
876
+ throw new Error(`Could not load knowledge index: ${String(err)}`);
877
+ }
878
+ }
879
+ async function retrieve(text, index, embedder, config) {
880
+ const topK = config?.topK ?? 3;
881
+ const minScore = config?.minScore ?? 0.42;
882
+ if (embedder.isReady) {
883
+ try {
884
+ const vector = await embedder.embed(text);
885
+ const results2 = search(vector, index, topK, minScore);
886
+ return { results: results2, mode: "vector" };
887
+ } catch {
888
+ }
889
+ }
890
+ const results = keywordSearch(text, index, topK);
891
+ return { results, mode: "keyword" };
892
+ }
893
+
894
+ // src/core/renderer.ts
895
+ function buildAnswer(results) {
896
+ if (results.length === 0) return "";
897
+ const seen = /* @__PURE__ */ new Set();
898
+ const parts = [];
899
+ for (const r of results) {
900
+ if (!seen.has(r.chunk.sourceId)) {
901
+ seen.add(r.chunk.sourceId);
902
+ parts.push(r.chunk.text);
903
+ }
904
+ }
905
+ return parts.join("\n\n");
906
+ }
907
+ function extractChips(index, override) {
908
+ if (override && override.length > 0) return override.slice(0, 5);
909
+ const chips = [];
910
+ const seenText = /* @__PURE__ */ new Set();
911
+ const seenSource = /* @__PURE__ */ new Set();
912
+ for (const chunk of index) {
913
+ if (chips.length >= 5) break;
914
+ if (chunk.sourceId) {
915
+ if (seenSource.has(chunk.sourceId)) continue;
916
+ seenSource.add(chunk.sourceId);
917
+ }
918
+ const candidate = chunk.question ?? extractFirstSentence(chunk.text);
919
+ if (candidate && !seenText.has(candidate)) {
920
+ seenText.add(candidate);
921
+ chips.push(candidate);
922
+ }
923
+ }
924
+ return chips;
925
+ }
926
+ function extractFirstSentence(text) {
927
+ const match = text.match(/^[^\n.!?]{10,80}[.!?\n]?/);
928
+ if (!match) return text.slice(0, 60);
929
+ return match[0].trim();
930
+ }
931
+
932
+ // src/core/fallback.ts
933
+ var DEFAULT_POOL = [
934
+ "That one's a bit outside what I have notes on right now. Feel free to reach out directly and I'll make sure you get a proper answer.",
935
+ "Hmm, I don't have a great answer for that one yet. You're welcome to get in touch and a real person will help.",
936
+ "I want to give you the right answer, not a guess. If you reach out through the contact page someone will follow up with you."
937
+ ];
938
+ var rotationIndex = 0;
939
+ function getFallback(config) {
940
+ const pool = config.fallbackPool && config.fallbackPool.length > 0 ? config.fallbackPool : config.fallback ? [config.fallback] : DEFAULT_POOL;
941
+ const message = pool[rotationIndex % pool.length];
942
+ rotationIndex = (rotationIndex + 1) % pool.length;
943
+ if (config.contactUrl) {
944
+ return `${message} You can reach us at: ${config.contactUrl}`;
945
+ }
946
+ return message;
947
+ }
948
+
949
+ // src/vanilla/widget.ts
950
+ var SageDeskWidget = class extends HTMLElement {
951
+ constructor() {
952
+ super(...arguments);
953
+ this._index = null;
954
+ this._embedder = null;
955
+ this._messages = [];
956
+ this._isOpen = false;
957
+ this._isTyping = false;
958
+ this._engineReady = false;
959
+ this._engineError = null;
960
+ this._msgCounter = 0;
961
+ this._hasSentMessage = false;
962
+ }
963
+ static get observedAttributes() {
964
+ return ["config"];
965
+ }
966
+ init(config) {
967
+ this._config = config;
968
+ this._mount();
969
+ }
970
+ get _theme() {
971
+ return this._config.agent.theme ?? "classic";
972
+ }
973
+ _mount() {
974
+ const accent = this._config.agent.accentColor ?? "#534AB7";
975
+ const position = this._config.agent.position ?? "bottom-right";
976
+ const theme = this._theme;
977
+ this._shadow = this.attachShadow({ mode: "open" });
978
+ const style = document.createElement("style");
979
+ style.textContent = getWidgetStyles(accent, theme);
980
+ this._shadow.appendChild(style);
981
+ this._buildDOM(accent, position, theme);
982
+ this._bindEvents();
983
+ }
984
+ _buildDOM(accent, position, theme) {
985
+ const posClass = position === "bottom-left" ? "pos-left" : "";
986
+ const agentName = this._config.agent.name ?? "Support";
987
+ if (theme === "dark") {
988
+ const glow = document.createElement("div");
989
+ glow.className = `sd-trigger-glow${posClass ? " " + posClass : ""}`;
990
+ this._shadow.appendChild(glow);
991
+ }
992
+ const trigger = document.createElement("button");
993
+ trigger.className = `sd-trigger${posClass ? " " + posClass : ""}`;
994
+ trigger.setAttribute("aria-label", "Open support chat");
995
+ trigger.setAttribute("aria-expanded", "false");
996
+ trigger.innerHTML = theme === "dark" ? ICON_BOT : ICON_CHAT;
997
+ this._shadow.appendChild(trigger);
998
+ const panel = document.createElement("div");
999
+ panel.className = `sd-panel${posClass ? " " + posClass : ""}`;
1000
+ panel.setAttribute("data-open", "false");
1001
+ panel.setAttribute("role", "dialog");
1002
+ panel.setAttribute("aria-label", agentName);
1003
+ panel.innerHTML = this._buildPanelHTML(agentName, accent, theme);
1004
+ this._shadow.appendChild(panel);
1005
+ }
1006
+ _buildPanelHTML(agentName, accent, theme) {
1007
+ const showPoweredBy = this._config.agent.poweredBy !== false;
1008
+ const avatarUrl = this._config.agent.avatarUrl;
1009
+ const footerHTML = showPoweredBy ? `<div class="sd-footer">Powered by <a class="sd-footer-link" href="https://github.com/mzeeshanwahid/sagedesk" target="_blank" rel="noopener noreferrer">sagedesk</a></div>` : "";
1010
+ if (theme === "dark") {
1011
+ const avatarContent2 = avatarUrl ? `<img class="sd-avatar-img" src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(agentName)}">` : ICON_BOT_SM;
1012
+ return `
1013
+ <div class="sd-panel-glow" aria-hidden="true"></div>
1014
+ <div class="sd-header">
1015
+ <div class="sd-avatar-wrap">
1016
+ <div class="sd-avatar">${avatarContent2}</div>
1017
+ <span class="sd-online-dot"></span>
1018
+ </div>
1019
+ <div class="sd-agent-info">
1020
+ <p class="sd-agent-name">${escapeHtml(agentName)}</p>
1021
+ <div class="sd-status">
1022
+ <span class="sd-status-dot"></span>
1023
+ <span>Trained on this site \xB7 always on</span>
1024
+ </div>
1025
+ </div>
1026
+ <button class="sd-close" aria-label="Close chat">${ICON_CLOSE}</button>
1027
+ </div>
1028
+ <div class="sd-thread" role="log" aria-live="polite" aria-label="Chat messages"></div>
1029
+ <div class="sd-chips"></div>
1030
+ <div class="sd-composer">
1031
+ <div class="sd-input-wrap">
1032
+ <input class="sd-input" type="text" placeholder="Write a message\u2026" aria-label="Type your question" autocomplete="off" />
1033
+ <button class="sd-send" aria-label="Send message">${ICON_SEND}</button>
1034
+ </div>
1035
+ </div>
1036
+ ${footerHTML}
1037
+ `;
1038
+ }
1039
+ if (theme === "light") {
1040
+ const avatarContent2 = avatarUrl ? `<img class="sd-avatar-img" src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(agentName)}">` : ICON_PERSON;
1041
+ return `
1042
+ <div class="sd-header">
1043
+ <div class="sd-avatar-wrap">
1044
+ <div class="sd-avatar">${avatarContent2}</div>
1045
+ </div>
1046
+ <div class="sd-agent-info">
1047
+ <p class="sd-agent-name">${escapeHtml(agentName)}</p>
1048
+ <div class="sd-status">
1049
+ <span class="sd-status-dot"></span>
1050
+ <span>Online \xB7 replies in under a minute</span>
1051
+ </div>
1052
+ </div>
1053
+ <button class="sd-close" aria-label="Close chat">${ICON_CLOSE}</button>
1054
+ </div>
1055
+ <div class="sd-trigger-label" hidden>
1056
+ <div class="sd-trigger-label-main">Chat with us</div>
1057
+ <div class="sd-trigger-label-sub">We typically reply in 1m</div>
1058
+ </div>
1059
+ <div class="sd-thread" role="log" aria-live="polite" aria-label="Chat messages"></div>
1060
+ <div class="sd-chips"></div>
1061
+ <div class="sd-composer">
1062
+ <div class="sd-input-wrap">
1063
+ <input class="sd-input" type="text" placeholder="Write a message\u2026" aria-label="Type your question" autocomplete="off" />
1064
+ <div class="sd-input-actions">
1065
+ <button class="sd-send" aria-label="Send message">${ICON_SEND}</button>
1066
+ </div>
1067
+ </div>
1068
+ </div>
1069
+ ${footerHTML}
1070
+ `;
1071
+ }
1072
+ const avatarContent = avatarUrl ? `<img class="sd-avatar-img" src="${escapeHtml(avatarUrl)}" alt="${escapeHtml(agentName)}">` : ICON_PERSON;
1073
+ return `
1074
+ <div class="sd-header">
1075
+ <div class="sd-avatar-wrap">
1076
+ <div class="sd-avatar">${avatarContent}</div>
1077
+ <span class="sd-online-dot"></span>
1078
+ </div>
1079
+ <div class="sd-agent-info">
1080
+ <p class="sd-agent-name">${escapeHtml(agentName)}</p>
1081
+ <div class="sd-status">
1082
+ <span class="sd-status-dot"></span>
1083
+ <span>Typically replies in under a minute</span>
1084
+ </div>
1085
+ </div>
1086
+ <button class="sd-close" aria-label="Close chat">${ICON_CLOSE}</button>
1087
+ </div>
1088
+ <div class="sd-thread" role="log" aria-live="polite" aria-label="Chat messages"></div>
1089
+ <div class="sd-chips"></div>
1090
+ <div class="sd-composer">
1091
+ <div class="sd-input-wrap">
1092
+ <input class="sd-input" type="text" placeholder="Write a message\u2026" aria-label="Type your question" autocomplete="off" />
1093
+ <button class="sd-send" aria-label="Send message">${ICON_SEND}</button>
1094
+ </div>
1095
+ </div>
1096
+ ${footerHTML}
1097
+ `;
1098
+ }
1099
+ _buildTriggerHTML(theme) {
1100
+ if (theme === "light") {
1101
+ return `
1102
+ <span class="sd-trigger-label">
1103
+ <span class="sd-trigger-label-main">Chat with us</span>
1104
+ <span class="sd-trigger-label-sub">We typically reply in 1m</span>
1105
+ </span>
1106
+ <span class="sd-trigger-circle">${ICON_CHAT_SM}</span>
1107
+ `;
1108
+ }
1109
+ if (theme === "dark") return ICON_BOT;
1110
+ return ICON_CHAT;
1111
+ }
1112
+ _bindEvents() {
1113
+ const shadow = this._shadow;
1114
+ const trigger = shadow.querySelector(".sd-trigger");
1115
+ const closeBtn = shadow.querySelector(".sd-close");
1116
+ const input = shadow.querySelector(".sd-input");
1117
+ const sendBtn = shadow.querySelector(".sd-send");
1118
+ const panel = shadow.querySelector(".sd-panel");
1119
+ if (this._theme === "light") {
1120
+ trigger.innerHTML = this._buildTriggerHTML("light");
1121
+ }
1122
+ trigger.addEventListener("click", () => {
1123
+ if (this._isOpen) {
1124
+ this._close();
1125
+ } else {
1126
+ this._open();
1127
+ }
1128
+ });
1129
+ closeBtn.addEventListener("click", () => this._close());
1130
+ sendBtn.addEventListener("click", () => this._submit());
1131
+ input.addEventListener("keydown", (e) => {
1132
+ if (e.key === "Enter" && !e.shiftKey) {
1133
+ e.preventDefault();
1134
+ this._submit();
1135
+ }
1136
+ });
1137
+ input.addEventListener("input", () => {
1138
+ sendBtn.classList.toggle("active", input.value.trim().length > 0);
1139
+ });
1140
+ panel.addEventListener("keydown", (e) => {
1141
+ if (e.key === "Escape") this._close();
1142
+ });
1143
+ panel.addEventListener("keydown", (e) => {
1144
+ if (e.key !== "Tab") return;
1145
+ const focusable = Array.from(
1146
+ panel.querySelectorAll(
1147
+ 'button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
1148
+ )
1149
+ );
1150
+ if (focusable.length === 0) return;
1151
+ const first = focusable[0];
1152
+ const last = focusable[focusable.length - 1];
1153
+ const active = shadow.activeElement;
1154
+ if (e.shiftKey) {
1155
+ if (active === first) {
1156
+ e.preventDefault();
1157
+ last.focus();
1158
+ }
1159
+ } else {
1160
+ if (active === last) {
1161
+ e.preventDefault();
1162
+ first.focus();
1163
+ }
1164
+ }
1165
+ });
1166
+ }
1167
+ async _open() {
1168
+ if (this._isOpen) return;
1169
+ this._isOpen = true;
1170
+ const trigger = this._shadow.querySelector(".sd-trigger");
1171
+ const panel = this._shadow.querySelector(".sd-panel");
1172
+ trigger.setAttribute("aria-expanded", "true");
1173
+ panel.setAttribute("data-open", "true");
1174
+ panel.removeAttribute("data-closing");
1175
+ if (this._messages.length === 0) {
1176
+ const greeting = this._config.agent.greeting ?? "Hey, how can I help you today?";
1177
+ this._appendMessage({ role: "bot", text: greeting });
1178
+ }
1179
+ this._renderChips();
1180
+ const input = this._shadow.querySelector(".sd-input");
1181
+ setTimeout(() => input.focus(), 50);
1182
+ if (!this._engineReady && !this._engineError) {
1183
+ this._startEngine();
1184
+ }
1185
+ }
1186
+ _close() {
1187
+ if (!this._isOpen) return;
1188
+ const trigger = this._shadow.querySelector(".sd-trigger");
1189
+ const panel = this._shadow.querySelector(".sd-panel");
1190
+ panel.setAttribute("data-closing", "true");
1191
+ panel.addEventListener(
1192
+ "animationend",
1193
+ () => {
1194
+ this._isOpen = false;
1195
+ panel.setAttribute("data-open", "false");
1196
+ panel.removeAttribute("data-closing");
1197
+ trigger.setAttribute("aria-expanded", "false");
1198
+ trigger.focus();
1199
+ },
1200
+ { once: true }
1201
+ );
1202
+ }
1203
+ async _startEngine() {
1204
+ try {
1205
+ this._index = await fetchIndex(this._config.indexUrl);
1206
+ } catch {
1207
+ this._engineError = "Could not load knowledge base.";
1208
+ this._appendMessage({
1209
+ role: "bot",
1210
+ text: "I'm having trouble loading right now. Please try again in a moment."
1211
+ });
1212
+ return;
1213
+ }
1214
+ try {
1215
+ this._embedder = new EmbedderRuntime();
1216
+ await this._embedder.load(this._config.agent.model);
1217
+ this._engineReady = true;
1218
+ } catch {
1219
+ this._embedder = new EmbedderRuntime();
1220
+ this._engineReady = true;
1221
+ }
1222
+ }
1223
+ async _submit() {
1224
+ const input = this._shadow.querySelector(".sd-input");
1225
+ const text = input.value.trim();
1226
+ if (!text) return;
1227
+ const typingStart = Date.now();
1228
+ input.value = "";
1229
+ const sendBtn = this._shadow.querySelector(".sd-send");
1230
+ sendBtn.classList.remove("active");
1231
+ this._hasSentMessage = true;
1232
+ this._appendMessage({ role: "user", text });
1233
+ this._showTyping();
1234
+ if (!this._engineReady && !this._engineError) {
1235
+ await this._waitForEngine();
1236
+ }
1237
+ let botText;
1238
+ let isFallback = false;
1239
+ let mode = "keyword";
1240
+ if (this._engineError || !this._index) {
1241
+ botText = getFallback(this._config.agent);
1242
+ isFallback = true;
1243
+ } else {
1244
+ try {
1245
+ const res = await retrieve(text, this._index, this._embedder, this._config.search);
1246
+ mode = res.mode;
1247
+ if (res.results.length > 0) {
1248
+ botText = buildAnswer(res.results);
1249
+ } else {
1250
+ botText = getFallback(this._config.agent);
1251
+ isFallback = true;
1252
+ }
1253
+ } catch {
1254
+ botText = getFallback(this._config.agent);
1255
+ isFallback = true;
1256
+ }
1257
+ }
1258
+ const elapsed = Date.now() - typingStart;
1259
+ const delayBase = mode === "keyword" || isFallback ? 800 : 3e3;
1260
+ const minTypingMsActual = delayBase + Math.random() * 2e3;
1261
+ const remaining = minTypingMsActual - elapsed;
1262
+ if (remaining > 0) await new Promise((r) => setTimeout(r, remaining));
1263
+ this._hideTyping();
1264
+ this._appendMessage({ role: "bot", text: botText, isFallback });
1265
+ }
1266
+ _waitForEngine() {
1267
+ return new Promise((resolve) => {
1268
+ const check = () => {
1269
+ if (this._engineReady || this._engineError) resolve();
1270
+ else setTimeout(check, 100);
1271
+ };
1272
+ check();
1273
+ });
1274
+ }
1275
+ _appendMessage(msg) {
1276
+ const thread = this._shadow.querySelector(".sd-thread");
1277
+ const theme = this._theme;
1278
+ const id = `msg-${++this._msgCounter}`;
1279
+ this._messages.push({
1280
+ id,
1281
+ role: msg.role,
1282
+ text: msg.text,
1283
+ isFallback: msg.isFallback,
1284
+ timestamp: /* @__PURE__ */ new Date()
1285
+ });
1286
+ const wrap = document.createElement("div");
1287
+ wrap.id = id;
1288
+ if (theme === "light" && msg.role === "bot") {
1289
+ wrap.className = "sd-msg-wrap bot";
1290
+ const fallbackLabel = msg.isFallback ? `<p class="sd-fallback-label">Not sure about that one</p>` : "";
1291
+ const agentName = escapeHtml(this._config.agent.name ?? "Support");
1292
+ wrap.innerHTML = `
1293
+ <div class="sd-bot-avatar" aria-hidden="true"></div>
1294
+ <div class="sd-bot-body">
1295
+ <div class="sd-bot-meta">
1296
+ <span class="sd-bot-name">${agentName}</span>
1297
+ <span class="sd-timestamp">just now</span>
1298
+ </div>
1299
+ ${fallbackLabel}
1300
+ <div class="sd-bubble bot">${escapeHtml(msg.text)}</div>
1301
+ </div>
1302
+ `;
1303
+ } else {
1304
+ wrap.className = `sd-msg-wrap ${msg.role}`;
1305
+ let inner = "";
1306
+ if (msg.isFallback) {
1307
+ inner += `<p class="sd-fallback-label">Not sure about that one</p>`;
1308
+ }
1309
+ inner += `<div class="sd-bubble ${msg.role}">${escapeHtml(msg.text)}</div>`;
1310
+ inner += `<span class="sd-timestamp">just now</span>`;
1311
+ wrap.innerHTML = inner;
1312
+ }
1313
+ thread.appendChild(wrap);
1314
+ thread.scrollTop = thread.scrollHeight;
1315
+ this._renderChips();
1316
+ }
1317
+ _showTyping() {
1318
+ if (this._isTyping) return;
1319
+ this._isTyping = true;
1320
+ const thread = this._shadow.querySelector(".sd-thread");
1321
+ const theme = this._theme;
1322
+ const indicator = document.createElement("div");
1323
+ indicator.id = "sd-typing-indicator";
1324
+ if (theme === "light") {
1325
+ indicator.className = "sd-typing";
1326
+ indicator.innerHTML = `
1327
+ <div class="sd-typing-avatar" aria-hidden="true"></div>
1328
+ <div class="sd-typing-dots">
1329
+ <span class="sd-dot"></span><span class="sd-dot"></span><span class="sd-dot"></span>
1330
+ </div>
1331
+ `;
1332
+ } else {
1333
+ indicator.className = "sd-typing";
1334
+ indicator.innerHTML = '<span class="sd-dot"></span><span class="sd-dot"></span><span class="sd-dot"></span>';
1335
+ }
1336
+ thread.appendChild(indicator);
1337
+ thread.scrollTop = thread.scrollHeight;
1338
+ }
1339
+ _hideTyping() {
1340
+ this._isTyping = false;
1341
+ const indicator = this._shadow.getElementById("sd-typing-indicator");
1342
+ if (indicator) indicator.remove();
1343
+ }
1344
+ _renderChips() {
1345
+ const container = this._shadow.querySelector(".sd-chips");
1346
+ container.innerHTML = "";
1347
+ const allChips = extractChips(this._index ?? [], this._config.agent.suggestedChips);
1348
+ const askedTexts = new Set(
1349
+ this._messages.filter((m) => m.role === "user").map((m) => m.text.toLowerCase().trim())
1350
+ );
1351
+ const chips = allChips.filter((chip) => !askedTexts.has(chip.toLowerCase().trim()));
1352
+ if (chips.length === 0) {
1353
+ container.style.display = "none";
1354
+ return;
1355
+ }
1356
+ container.style.display = "";
1357
+ const theme = this._theme;
1358
+ if (this._hasSentMessage) {
1359
+ container.classList.add("scrollable");
1360
+ } else {
1361
+ container.classList.remove("scrollable");
1362
+ }
1363
+ if (theme === "classic") {
1364
+ const label = document.createElement("div");
1365
+ label.className = "sd-chips-label";
1366
+ label.textContent = "Suggested";
1367
+ container.appendChild(label);
1368
+ const row = document.createElement("div");
1369
+ row.className = "sd-chips-row";
1370
+ container.appendChild(row);
1371
+ for (const chipText of chips) {
1372
+ const btn = document.createElement("button");
1373
+ btn.className = "sd-chip";
1374
+ btn.textContent = chipText;
1375
+ btn.addEventListener("click", () => this._submitChip(chipText));
1376
+ row.appendChild(btn);
1377
+ }
1378
+ return;
1379
+ }
1380
+ if (theme === "dark" && !this._hasSentMessage) {
1381
+ const label = document.createElement("div");
1382
+ label.className = "sd-chips-label";
1383
+ label.textContent = "Try asking";
1384
+ container.appendChild(label);
1385
+ }
1386
+ for (const chipText of chips) {
1387
+ const btn = document.createElement("button");
1388
+ btn.className = "sd-chip";
1389
+ if (theme === "dark") {
1390
+ btn.innerHTML = `${ICON_CHEVRON} ${escapeHtml(chipText)}`;
1391
+ } else {
1392
+ btn.textContent = chipText;
1393
+ }
1394
+ btn.addEventListener("click", () => this._submitChip(chipText));
1395
+ container.appendChild(btn);
1396
+ }
1397
+ }
1398
+ _submitChip(chipText) {
1399
+ const input = this._shadow.querySelector(".sd-input");
1400
+ input.value = chipText;
1401
+ this._submit();
1402
+ }
1403
+ };
1404
+ function escapeHtml(str) {
1405
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;").replace(/\n/g, "<br>");
1406
+ }
1407
+
1408
+ // src/vanilla/index.ts
1409
+ var ELEMENT_TAG = "sagedesk-widget";
1410
+ function assertConfig(config) {
1411
+ if (!config || typeof config !== "object") {
1412
+ throw new Error("[sagedesk] init() requires a config object.");
1413
+ }
1414
+ if (!config.indexUrl || typeof config.indexUrl !== "string") {
1415
+ throw new Error(
1416
+ "[sagedesk] config.indexUrl is required. Run `npx sagedesk build` and pass the output path as indexUrl."
1417
+ );
1418
+ }
1419
+ if (!config.agent || typeof config.agent !== "object") {
1420
+ throw new Error("[sagedesk] config.agent is required.");
1421
+ }
1422
+ if (!config.agent.name || typeof config.agent.name !== "string") {
1423
+ throw new Error("[sagedesk] config.agent.name is required.");
1424
+ }
1425
+ }
1426
+ function init(config) {
1427
+ assertConfig(config);
1428
+ try {
1429
+ if (typeof customElements === "undefined") return;
1430
+ if (typeof document === "undefined") return;
1431
+ if (!customElements.get(ELEMENT_TAG)) {
1432
+ customElements.define(ELEMENT_TAG, SageDeskWidget);
1433
+ }
1434
+ const existing = document.querySelector(ELEMENT_TAG);
1435
+ if (existing) {
1436
+ existing.init(config);
1437
+ return;
1438
+ }
1439
+ const el = document.createElement(ELEMENT_TAG);
1440
+ document.body.appendChild(el);
1441
+ el.init(config);
1442
+ } catch (err) {
1443
+ console.warn("[sagedesk] Failed to initialise:", err);
1444
+ }
1445
+ }
1446
+ if (typeof window !== "undefined") {
1447
+ window.SageDesk = { init };
1448
+ }
1449
+ // Annotate the CommonJS export names for ESM import in node:
1450
+ 0 && (module.exports = {
1451
+ SageDeskWidget,
1452
+ init
1453
+ });
1454
+ //# sourceMappingURL=index.cjs.map