dewee-webchat 0.0.0-stage → 0.1.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,1265 @@
1
+ import {
2
+ HEADER_GATEWAY_TOKEN,
3
+ HEADER_GATEWAY_URL
4
+ } from "./chunk-MKTHKYLY.js";
5
+
6
+ // src/widget/api-client.ts
7
+ var ApiError = class extends Error {
8
+ constructor(message, status, code) {
9
+ super(message);
10
+ this.status = status;
11
+ this.code = code;
12
+ }
13
+ status;
14
+ code;
15
+ };
16
+ var store = {
17
+ get(k) {
18
+ try {
19
+ return localStorage.getItem(k);
20
+ } catch {
21
+ return null;
22
+ }
23
+ },
24
+ set(k, v) {
25
+ try {
26
+ localStorage.setItem(k, v);
27
+ } catch {
28
+ }
29
+ },
30
+ del(k) {
31
+ try {
32
+ localStorage.removeItem(k);
33
+ } catch {
34
+ }
35
+ }
36
+ };
37
+ var ApiClient = class {
38
+ constructor(o) {
39
+ this.o = o;
40
+ }
41
+ o;
42
+ session = null;
43
+ config = null;
44
+ sessionPromise = null;
45
+ url(action, qs) {
46
+ const base = this.o.endpoint.replace(/\/+$/, "");
47
+ const q = qs ? "?" + new URLSearchParams(qs).toString() : "";
48
+ return `${base}/${action}${q}`;
49
+ }
50
+ get storageKey() {
51
+ return `dewee-webchat:session:${this.o.endpoint}`;
52
+ }
53
+ headers(auth = false) {
54
+ const h = { "content-type": "application/json" };
55
+ if (this.o.gatewayUrl && this.o.gatewayToken) {
56
+ h[HEADER_GATEWAY_URL] = this.o.gatewayUrl;
57
+ h[HEADER_GATEWAY_TOKEN] = this.o.gatewayToken;
58
+ }
59
+ if (auth && this.session) h.authorization = `Bearer ${this.session.token}`;
60
+ return h;
61
+ }
62
+ async parse(res) {
63
+ const body = await res.json().catch(() => ({}));
64
+ if (!res.ok) throw new ApiError(body?.error?.message || `HTTP ${res.status}`, res.status, body?.error?.code);
65
+ return body;
66
+ }
67
+ async getConfig(force = false) {
68
+ if (this.config && !force) return this.config;
69
+ this.config = await this.parse(await fetch(this.url("config"), { headers: this.headers() }));
70
+ return this.config;
71
+ }
72
+ /** Returns a valid visitor session, creating or refreshing it when needed. */
73
+ ensureSession() {
74
+ if (this.session && this.session.expiresAt - Date.now() > 864e5) return Promise.resolve(this.session);
75
+ if (!this.session) {
76
+ const saved = store.get(this.storageKey);
77
+ if (saved) {
78
+ try {
79
+ const s = JSON.parse(saved);
80
+ if (s.expiresAt - Date.now() > 864e5) return Promise.resolve(this.session = s);
81
+ this.session = s;
82
+ } catch {
83
+ }
84
+ }
85
+ }
86
+ return this.sessionPromise ??= this.createSession().finally(() => this.sessionPromise = null);
87
+ }
88
+ async createSession() {
89
+ const cfg = await this.getConfig();
90
+ const body = {};
91
+ if (this.session?.token) body.token = this.session.token;
92
+ if (cfg.turnstileSiteKey) {
93
+ if (!this.o.turnstile) throw new ApiError("Verification is required but no Turnstile handler is set", 403, "turnstile_required");
94
+ body.turnstileToken = await this.o.turnstile(cfg.turnstileSiteKey);
95
+ }
96
+ const s = await this.parse(
97
+ await fetch(this.url("session"), { method: "POST", headers: this.headers(), body: JSON.stringify(body) })
98
+ );
99
+ this.session = s;
100
+ store.set(this.storageKey, JSON.stringify(s));
101
+ return s;
102
+ }
103
+ forgetSession() {
104
+ this.session = null;
105
+ store.del(this.storageKey);
106
+ }
107
+ async history(agent, conversationId) {
108
+ await this.ensureSession();
109
+ const res = await fetch(this.url("history", { agent, conversationId }), { headers: this.headers(true) });
110
+ return (await this.parse(res)).messages;
111
+ }
112
+ /** Sends a message and calls `onEvent` for every streamed event. Resolves when the run ends. */
113
+ async chat(input, onEvent, signal) {
114
+ await this.ensureSession();
115
+ let res = await fetch(this.url("chat"), { method: "POST", headers: this.headers(true), body: JSON.stringify(input), signal });
116
+ if (res.status === 401) {
117
+ this.forgetSession();
118
+ await this.ensureSession();
119
+ res = await fetch(this.url("chat"), { method: "POST", headers: this.headers(true), body: JSON.stringify(input), signal });
120
+ }
121
+ if (!res.ok || !res.body) await this.parse(res);
122
+ const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
123
+ let buf = "";
124
+ while (true) {
125
+ const { done, value } = await reader.read();
126
+ if (done) break;
127
+ buf += value;
128
+ let idx;
129
+ while ((idx = buf.indexOf("\n\n")) >= 0) {
130
+ const frame = buf.slice(0, idx);
131
+ buf = buf.slice(idx + 2);
132
+ const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trimStart()).join("\n");
133
+ if (!data) continue;
134
+ try {
135
+ onEvent(JSON.parse(data));
136
+ } catch {
137
+ }
138
+ }
139
+ }
140
+ }
141
+ };
142
+
143
+ // src/widget/i18n.ts
144
+ var en = {
145
+ title: "Assistant",
146
+ greeting: "Hi there \u{1F44B}",
147
+ greetingBody: "Ask me anything. I can look things up and use tools to help you.",
148
+ placeholder: "Type a message\u2026",
149
+ send: "Send",
150
+ stop: "Stop",
151
+ newChat: "New conversation",
152
+ close: "Close chat",
153
+ open: "Open chat",
154
+ thinking: "Thinking\u2026",
155
+ thoughtFor: (s) => `Thought for ${s}s`,
156
+ reasoning: "Reasoning",
157
+ toolRunning: "running",
158
+ toolDone: "done",
159
+ toolError: "error",
160
+ args: "Arguments",
161
+ result: "Result",
162
+ online: "Online",
163
+ poweredBy: "Powered by",
164
+ errorGeneric: "Something went wrong. Please try again.",
165
+ stopped: "Stopped."
166
+ };
167
+ var vi = {
168
+ title: "Tr\u1EE3 l\xFD",
169
+ greeting: "Xin ch\xE0o \u{1F44B}",
170
+ greetingBody: "B\u1EA1n c\u1EA7n h\u1ECFi g\xEC c\u1EE9 nh\u1EAFn nh\xE9. M\xECnh c\xF3 th\u1EC3 tra c\u1EE9u v\xE0 d\xF9ng c\xF4ng c\u1EE5 \u0111\u1EC3 h\u1ED7 tr\u1EE3 b\u1EA1n.",
171
+ placeholder: "Nh\u1EADp tin nh\u1EAFn\u2026",
172
+ send: "G\u1EEDi",
173
+ stop: "D\u1EEBng",
174
+ newChat: "Cu\u1ED9c tr\xF2 chuy\u1EC7n m\u1EDBi",
175
+ close: "\u0110\xF3ng khung chat",
176
+ open: "M\u1EDF khung chat",
177
+ thinking: "\u0110ang suy ngh\u0129\u2026",
178
+ thoughtFor: (s) => `\u0110\xE3 suy ngh\u0129 ${s} gi\xE2y`,
179
+ reasoning: "Suy lu\u1EADn",
180
+ toolRunning: "\u0111ang ch\u1EA1y",
181
+ toolDone: "xong",
182
+ toolError: "l\u1ED7i",
183
+ args: "Tham s\u1ED1",
184
+ result: "K\u1EBFt qu\u1EA3",
185
+ online: "\u0110ang tr\u1EF1c tuy\u1EBFn",
186
+ poweredBy: "V\u1EADn h\xE0nh b\u1EDFi",
187
+ errorGeneric: "C\xF3 l\u1ED7i x\u1EA3y ra. B\u1EA1n th\u1EED l\u1EA1i gi\xFAp m\xECnh nh\xE9.",
188
+ stopped: "\u0110\xE3 d\u1EEBng."
189
+ };
190
+ function pickStrings(locale) {
191
+ const l = (locale || (typeof document !== "undefined" ? document.documentElement.lang : "") || (typeof navigator !== "undefined" ? navigator.language : "") || "en").toLowerCase();
192
+ return l.startsWith("vi") ? vi : en;
193
+ }
194
+ var icons = {
195
+ chat: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8.5 8.5 0 0 1-12.4 7.6L3 21l1.4-5.1A8.5 8.5 0 1 1 21 12Z"/><path d="M8.5 11h.01M12 11h.01M15.5 11h.01"/></svg>`,
196
+ close: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg>`,
197
+ down: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg>`,
198
+ newChat: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 20h9"/><path d="M16.4 3.6a2.1 2.1 0 1 1 3 3L7 19l-4 1 1-4Z"/></svg>`,
199
+ send: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 19V5M5 12l7-7 7 7"/></svg>`,
200
+ stop: `<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2.5"/></svg>`,
201
+ brain: `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44 2.5 2.5 0 0 1-2.96-3.08 3 3 0 0 1-.34-5.58 2.5 2.5 0 0 1 1.32-4.24A2.5 2.5 0 0 1 9.5 2Z"/><path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44 2.5 2.5 0 0 0 2.96-3.08 3 3 0 0 0 .34-5.58 2.5 2.5 0 0 0-1.32-4.24A2.5 2.5 0 0 0 14.5 2Z"/></svg>`,
202
+ tool: `<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z"/></svg>`
203
+ };
204
+
205
+ // src/widget/markdown.ts
206
+ function el(doc, tag, cls) {
207
+ const e = doc.createElement(tag);
208
+ if (cls) e.className = cls;
209
+ return e;
210
+ }
211
+ function safeHref(url) {
212
+ const u = url.trim();
213
+ if (/^(https?:|mailto:|tel:)/i.test(u)) return u;
214
+ if (/^[/#?]/.test(u) && !u.startsWith("//")) return u;
215
+ return null;
216
+ }
217
+ var INLINE_SRC = /(`+)([\s\S]*?[^`])\1(?!`)|\*\*([^*]+?)\*\*|__([^_]+?)__|~~([^~]+?)~~|\*([^*\s][^*]*?)\*|(?<![\w])_([^_\s][^_]*?)_(?![\w])|\[([^\]]+)\]\(([^)\s]+)(?:\s+"[^"]*")?\)|(https?:\/\/[^\s<>()]+[^\s<>().,;:!?'"])/;
218
+ function renderInline(doc, text, parent) {
219
+ const re = new RegExp(INLINE_SRC.source, "g");
220
+ let last = 0;
221
+ for (let m; m = re.exec(text); ) {
222
+ if (m.index > last) parent.appendChild(doc.createTextNode(text.slice(last, m.index)));
223
+ last = m.index + m[0].length;
224
+ if (m[2] !== void 0) {
225
+ const c = el(doc, "code");
226
+ c.textContent = m[2];
227
+ parent.appendChild(c);
228
+ } else if (m[3] !== void 0 || m[4] !== void 0) {
229
+ const s = el(doc, "strong");
230
+ renderInline(doc, m[3] ?? m[4], s);
231
+ parent.appendChild(s);
232
+ } else if (m[5] !== void 0) {
233
+ const s = el(doc, "del");
234
+ renderInline(doc, m[5], s);
235
+ parent.appendChild(s);
236
+ } else if (m[6] !== void 0 || m[7] !== void 0) {
237
+ const s = el(doc, "em");
238
+ renderInline(doc, m[6] ?? m[7], s);
239
+ parent.appendChild(s);
240
+ } else if (m[8] !== void 0) {
241
+ const href = safeHref(m[9]);
242
+ if (href) {
243
+ const a = el(doc, "a");
244
+ a.href = href;
245
+ a.target = "_blank";
246
+ a.rel = "noopener noreferrer nofollow";
247
+ renderInline(doc, m[8], a);
248
+ parent.appendChild(a);
249
+ } else parent.appendChild(doc.createTextNode(m[8]));
250
+ } else if (m[10] !== void 0) {
251
+ const a = el(doc, "a");
252
+ a.href = m[10];
253
+ a.target = "_blank";
254
+ a.rel = "noopener noreferrer nofollow";
255
+ a.textContent = m[10];
256
+ parent.appendChild(a);
257
+ }
258
+ }
259
+ if (last < text.length) parent.appendChild(doc.createTextNode(text.slice(last)));
260
+ }
261
+ var isTableSep = (l) => /^\s*\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)*\|?\s*$/.test(l);
262
+ var splitRow = (l) => l.trim().replace(/^\||\|$/g, "").split("|").map((c) => c.trim());
263
+ function renderMarkdown(doc, src) {
264
+ const frag = doc.createDocumentFragment();
265
+ const lines = src.replace(/\r\n?/g, "\n").split("\n");
266
+ let i = 0;
267
+ while (i < lines.length) {
268
+ const line = lines[i];
269
+ if (!line.trim()) {
270
+ i++;
271
+ continue;
272
+ }
273
+ const fence = line.match(/^\s*(```+|~~~+)\s*([\w+-]*)/);
274
+ if (fence) {
275
+ const body = [];
276
+ i++;
277
+ while (i < lines.length && !lines[i].trim().startsWith(fence[1])) body.push(lines[i++]);
278
+ i++;
279
+ const pre = el(doc, "pre");
280
+ const code = el(doc, "code");
281
+ if (fence[2]) code.dataset.lang = fence[2];
282
+ code.textContent = body.join("\n");
283
+ pre.appendChild(code);
284
+ frag.appendChild(pre);
285
+ continue;
286
+ }
287
+ const h = line.match(/^(#{1,6})\s+(.*)$/);
288
+ if (h) {
289
+ const tag = `h${Math.min(6, h[1].length + 2)}`;
290
+ const e = el(doc, tag);
291
+ renderInline(doc, h[2], e);
292
+ frag.appendChild(e);
293
+ i++;
294
+ continue;
295
+ }
296
+ if (/^\s*([-*_])(\s*\1){2,}\s*$/.test(line)) {
297
+ frag.appendChild(el(doc, "hr"));
298
+ i++;
299
+ continue;
300
+ }
301
+ if (line.includes("|") && i + 1 < lines.length && isTableSep(lines[i + 1])) {
302
+ const wrap = el(doc, "div", "table");
303
+ const table = el(doc, "table");
304
+ const thead = el(doc, "thead");
305
+ const tr = el(doc, "tr");
306
+ for (const c of splitRow(line)) {
307
+ const th = el(doc, "th");
308
+ renderInline(doc, c, th);
309
+ tr.appendChild(th);
310
+ }
311
+ thead.appendChild(tr);
312
+ table.appendChild(thead);
313
+ const tbody = el(doc, "tbody");
314
+ i += 2;
315
+ while (i < lines.length && lines[i].includes("|") && lines[i].trim()) {
316
+ const r = el(doc, "tr");
317
+ for (const c of splitRow(lines[i])) {
318
+ const td = el(doc, "td");
319
+ renderInline(doc, c, td);
320
+ r.appendChild(td);
321
+ }
322
+ tbody.appendChild(r);
323
+ i++;
324
+ }
325
+ table.appendChild(tbody);
326
+ wrap.appendChild(table);
327
+ frag.appendChild(wrap);
328
+ continue;
329
+ }
330
+ if (/^\s*>/.test(line)) {
331
+ const body = [];
332
+ while (i < lines.length && /^\s*>/.test(lines[i])) body.push(lines[i++].replace(/^\s*>\s?/, ""));
333
+ const q = el(doc, "blockquote");
334
+ q.appendChild(renderMarkdown(doc, body.join("\n")));
335
+ frag.appendChild(q);
336
+ continue;
337
+ }
338
+ const li = line.match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
339
+ if (li) {
340
+ const ordered = /\d/.test(li[2]);
341
+ const list = el(doc, ordered ? "ol" : "ul");
342
+ if (ordered && parseInt(li[2], 10) !== 1) list.setAttribute("start", String(parseInt(li[2], 10)));
343
+ const baseIndent = li[1].length;
344
+ while (i < lines.length) {
345
+ const m = lines[i].match(/^(\s*)([-*+]|\d+[.)])\s+(.*)$/);
346
+ if (!m || m[1].length < baseIndent || /\d/.test(m[2]) !== ordered) break;
347
+ if (m[1].length > baseIndent) {
348
+ const sub = [];
349
+ while (i < lines.length && (/^\s+/.test(lines[i]) && lines[i].match(/^(\s*)/)[1].length > baseIndent)) sub.push(lines[i++].slice(baseIndent + 2));
350
+ list.lastElementChild?.appendChild(renderMarkdown(doc, sub.join("\n")));
351
+ continue;
352
+ }
353
+ const item = el(doc, "li");
354
+ renderInline(doc, m[3], item);
355
+ list.appendChild(item);
356
+ i++;
357
+ }
358
+ frag.appendChild(list);
359
+ continue;
360
+ }
361
+ const para = [];
362
+ while (i < lines.length && lines[i].trim() && !/^\s*(```|~~~|#{1,6}\s|>|([-*+]|\d+[.)])\s)/.test(lines[i]) && !(lines[i].includes("|") && i + 1 < lines.length && isTableSep(lines[i + 1]))) para.push(lines[i++]);
363
+ if (!para.length) para.push(lines[i++]);
364
+ const p = el(doc, "p");
365
+ para.forEach((l, idx) => {
366
+ if (idx) p.appendChild(el(doc, "br"));
367
+ renderInline(doc, l, p);
368
+ });
369
+ frag.appendChild(p);
370
+ }
371
+ return frag;
372
+ }
373
+
374
+ // src/widget/styles.ts
375
+ var styles = (
376
+ /* css */
377
+ `
378
+ :host {
379
+ --dw-accent: #5446e8;
380
+ --dw-accent-contrast: #fff;
381
+ --dw-bg: #ffffff;
382
+ --dw-surface: #f5f4fb;
383
+ --dw-fg: #1b1f4b;
384
+ --dw-muted: #6b6f8f;
385
+ --dw-border: #e4e2f1;
386
+ --dw-radius: 18px;
387
+ --dw-font: "Be Vietnam Pro", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
388
+ --dw-mono: ui-monospace, "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
389
+ --dw-width: 400px;
390
+ --dw-height: 640px;
391
+ --dw-z: 2147483000;
392
+ --dw-shadow: 0 24px 64px -16px rgba(27, 31, 75, .28), 0 4px 16px -4px rgba(27, 31, 75, .12);
393
+ all: initial;
394
+ font-family: var(--dw-font);
395
+ color: var(--dw-fg);
396
+ }
397
+ :host([data-theme="dark"]) {
398
+ --dw-bg: #14152b;
399
+ --dw-surface: #1e2040;
400
+ --dw-fg: #ecebfa;
401
+ --dw-muted: #a2a4c6;
402
+ --dw-border: #2d2f55;
403
+ --dw-shadow: 0 24px 64px -12px rgba(0, 0, 0, .6);
404
+ }
405
+ * { box-sizing: border-box; }
406
+ button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; padding: 0; }
407
+ .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
408
+
409
+ /* ---------- launcher ---------- */
410
+ .launcher {
411
+ position: fixed; bottom: 20px; right: 20px; z-index: var(--dw-z);
412
+ width: 58px; height: 58px; border-radius: 50%;
413
+ background: var(--dw-accent); color: var(--dw-accent-contrast);
414
+ display: grid; place-items: center;
415
+ box-shadow: 0 10px 30px -6px color-mix(in srgb, var(--dw-accent) 60%, transparent);
416
+ transition: transform .2s cubic-bezier(.3,1.4,.6,1), box-shadow .2s;
417
+ }
418
+ .launcher:hover { transform: translateY(-2px) scale(1.04); }
419
+ .launcher:focus-visible { outline: 3px solid color-mix(in srgb, var(--dw-accent) 45%, transparent); outline-offset: 3px; }
420
+ .launcher svg { width: 26px; height: 26px; transition: transform .25s, opacity .2s; }
421
+ .launcher img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; }
422
+ :host([data-position="bottom-left"]) .launcher { right: auto; left: 20px; }
423
+ .launcher .badge {
424
+ position: absolute; top: -2px; right: -2px; width: 14px; height: 14px; border-radius: 50%;
425
+ background: #ff5a7a; border: 2px solid var(--dw-bg); display: none;
426
+ }
427
+ :host([data-unread]) .launcher .badge { display: block; }
428
+
429
+ /* ---------- panel ---------- */
430
+ .panel {
431
+ position: fixed; bottom: 92px; right: 20px; z-index: var(--dw-z);
432
+ width: min(var(--dw-width), calc(100vw - 32px));
433
+ height: min(var(--dw-height), calc(100dvh - 120px));
434
+ background: var(--dw-bg); color: var(--dw-fg);
435
+ border: 1px solid var(--dw-border); border-radius: var(--dw-radius);
436
+ box-shadow: var(--dw-shadow);
437
+ display: flex; flex-direction: column; overflow: hidden;
438
+ transform-origin: bottom right;
439
+ transition: opacity .18s ease, transform .22s cubic-bezier(.2,.9,.3,1.1), visibility .2s;
440
+ }
441
+ :host([data-position="bottom-left"]) .panel { right: auto; left: 20px; transform-origin: bottom left; }
442
+ :host(:not([data-open])) .panel { opacity: 0; transform: translateY(12px) scale(.96); visibility: hidden; pointer-events: none; }
443
+ :host([data-mode="inline"]) .panel {
444
+ position: relative; inset: auto; width: 100%; height: 100%; min-height: 420px;
445
+ opacity: 1; transform: none; visibility: visible; pointer-events: auto; box-shadow: none;
446
+ }
447
+ :host([data-mode="inline"]) { display: block; height: var(--dw-height); }
448
+ :host([data-mode="inline"]) .launcher, :host([data-mode="inline"]) .close { display: none; }
449
+ @media (max-width: 520px) {
450
+ :host(:not([data-mode="inline"])) .panel { inset: 0; width: 100vw; height: 100dvh; border-radius: 0; border: 0; }
451
+ :host([data-open]:not([data-mode="inline"])) .launcher { display: none; }
452
+ }
453
+
454
+ /* ---------- header ---------- */
455
+ .head {
456
+ display: flex; align-items: center; gap: 12px; padding: 14px 14px 14px 16px;
457
+ border-bottom: 1px solid var(--dw-border);
458
+ background: linear-gradient(180deg, color-mix(in srgb, var(--dw-accent) 7%, var(--dw-bg)), var(--dw-bg));
459
+ }
460
+ .avatar {
461
+ flex: none; width: 38px; height: 38px; border-radius: 12px;
462
+ background: var(--dw-accent); color: var(--dw-accent-contrast);
463
+ display: grid; place-items: center; font-weight: 700; font-size: 16px; overflow: hidden;
464
+ }
465
+ .avatar img { width: 100%; height: 100%; object-fit: cover; }
466
+ .who { flex: 1; min-width: 0; }
467
+ .title { font-weight: 650; font-size: 15px; line-height: 1.25; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
468
+ .subtitle { font-size: 12px; color: var(--dw-muted); display: flex; align-items: center; gap: 6px; margin-top: 2px; }
469
+ .dot { width: 7px; height: 7px; border-radius: 50%; background: #22c58b; box-shadow: 0 0 0 3px color-mix(in srgb, #22c58b 22%, transparent); }
470
+ .agent-select {
471
+ font: inherit; font-size: 12px; color: var(--dw-muted); background: transparent; border: 0;
472
+ padding: 0 14px 0 0; margin: 0; max-width: 170px; cursor: pointer; appearance: none; field-sizing: content; text-overflow: ellipsis;
473
+ background-image: linear-gradient(45deg, transparent 50%, currentColor 50%), linear-gradient(135deg, currentColor 50%, transparent 50%);
474
+ background-position: right 4px top 55%, right 0 top 55%; background-size: 4px 4px; background-repeat: no-repeat;
475
+ }
476
+ .agent-select:focus-visible { outline: 2px solid var(--dw-accent); border-radius: 4px; }
477
+ .icon-btn {
478
+ width: 34px; height: 34px; border-radius: 10px; display: grid; place-items: center; color: var(--dw-muted);
479
+ transition: background .15s, color .15s;
480
+ }
481
+ .icon-btn:hover { background: var(--dw-surface); color: var(--dw-fg); }
482
+ .icon-btn:focus-visible { outline: 2px solid var(--dw-accent); }
483
+ .icon-btn svg { width: 18px; height: 18px; }
484
+
485
+ /* ---------- log ---------- */
486
+ .log { flex: 1; overflow-y: auto; padding: 18px 16px 8px; scroll-behavior: smooth; overscroll-behavior: contain; }
487
+ .log::-webkit-scrollbar { width: 8px; }
488
+ .log::-webkit-scrollbar-thumb { background: var(--dw-border); border-radius: 8px; }
489
+ .welcome { text-align: left; padding: 6px 2px 10px; animation: rise .35s ease both; }
490
+ .welcome h2 { margin: 0 0 6px; font-size: 20px; line-height: 1.3; font-weight: 700; letter-spacing: -.01em; }
491
+ .welcome p { margin: 0; color: var(--dw-muted); font-size: 14px; line-height: 1.55; }
492
+ .suggestions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 16px; }
493
+ .chip {
494
+ font-size: 13px; line-height: 1.3; padding: 8px 12px; border-radius: 999px;
495
+ border: 1px solid var(--dw-border); background: var(--dw-bg); text-align: left;
496
+ transition: border-color .15s, background .15s, transform .15s;
497
+ }
498
+ .chip:hover { border-color: var(--dw-accent); background: color-mix(in srgb, var(--dw-accent) 6%, var(--dw-bg)); transform: translateY(-1px); }
499
+
500
+ .msg { display: flex; margin: 0 0 16px; animation: rise .25s ease both; }
501
+ .msg.user { justify-content: flex-end; }
502
+ .bubble { max-width: 86%; font-size: 14.5px; line-height: 1.6; overflow-wrap: anywhere; }
503
+ .msg.user .bubble {
504
+ background: var(--dw-accent); color: var(--dw-accent-contrast);
505
+ padding: 9px 14px; border-radius: 18px 18px 6px 18px; white-space: pre-wrap;
506
+ }
507
+ .msg.assistant .bubble { max-width: 100%; width: 100%; }
508
+ .msg.error .bubble {
509
+ color: #c2334f; background: color-mix(in srgb, #ff5a7a 9%, var(--dw-bg));
510
+ border: 1px solid color-mix(in srgb, #ff5a7a 30%, transparent); padding: 9px 12px; border-radius: 12px; font-size: 13.5px;
511
+ }
512
+ @keyframes rise { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
513
+
514
+ /* markdown */
515
+ .md > :first-child { margin-top: 0; }
516
+ .md > :last-child { margin-bottom: 0; }
517
+ .md p, .md ul, .md ol, .md blockquote, .md pre, .md .table { margin: 0 0 10px; }
518
+ .md ul, .md ol { padding-left: 22px; }
519
+ .md li { margin: 3px 0; }
520
+ .md h3, .md h4, .md h5, .md h6 { margin: 14px 0 6px; font-size: 15px; line-height: 1.35; }
521
+ .md a { color: var(--dw-accent); text-decoration: underline; text-underline-offset: 2px; }
522
+ .md code { font-family: var(--dw-mono); font-size: .86em; background: var(--dw-surface); padding: 1.5px 5px; border-radius: 5px; }
523
+ .md pre { background: #12132a; color: #e6e6f5; padding: 12px 14px; border-radius: 12px; overflow-x: auto; font-size: 12.5px; line-height: 1.55; }
524
+ .md pre code { background: none; padding: 0; color: inherit; font-size: inherit; }
525
+ .md blockquote { border-left: 3px solid var(--dw-accent); padding: 2px 0 2px 12px; color: var(--dw-muted); }
526
+ .md hr { border: 0; border-top: 1px solid var(--dw-border); margin: 14px 0; }
527
+ .md .table { overflow-x: auto; border: 1px solid var(--dw-border); border-radius: 10px; }
528
+ .md table { border-collapse: collapse; width: 100%; font-size: 13px; }
529
+ .md th, .md td { padding: 7px 10px; border-bottom: 1px solid var(--dw-border); text-align: left; vertical-align: top; overflow-wrap: break-word; min-width: 4.5em; }
530
+ .md th { background: var(--dw-surface); font-weight: 600; }
531
+ .md tr:last-child td { border-bottom: 0; }
532
+ .cursor::after { content: ""; display: inline-block; width: 7px; height: 15px; margin-left: 2px; vertical-align: -2px; background: var(--dw-accent); border-radius: 2px; animation: blink 1s steps(2) infinite; }
533
+ @keyframes blink { 50% { opacity: 0; } }
534
+
535
+ /* reasoning + tools */
536
+ .trace { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
537
+ details.thinking, details.tool {
538
+ border: 1px solid var(--dw-border); border-radius: 12px; background: var(--dw-surface); overflow: hidden;
539
+ }
540
+ details > summary {
541
+ list-style: none; cursor: pointer; display: flex; align-items: center; gap: 8px;
542
+ padding: 7px 10px; font-size: 12.5px; color: var(--dw-muted); user-select: none;
543
+ }
544
+ details > summary::-webkit-details-marker { display: none; }
545
+ details > summary:hover { color: var(--dw-fg); }
546
+ details > summary .chev { margin-left: auto; transition: transform .2s; width: 14px; height: 14px; flex: none; }
547
+ details[open] > summary .chev { transform: rotate(180deg); }
548
+ details > summary svg.ic { width: 14px; height: 14px; flex: none; }
549
+ .thinking-body {
550
+ padding: 2px 12px 10px; font-size: 12.5px; line-height: 1.55; color: var(--dw-muted);
551
+ white-space: pre-wrap; max-height: 220px; overflow-y: auto;
552
+ }
553
+ .tool-name { font-family: var(--dw-mono); font-size: 12px; color: var(--dw-fg); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
554
+ .tool-state { font-size: 11px; padding: 1px 7px; border-radius: 999px; background: var(--dw-bg); border: 1px solid var(--dw-border); flex: none; }
555
+ .tool[data-status="done"] .tool-state { color: #13a06f; border-color: color-mix(in srgb, #13a06f 35%, transparent); }
556
+ .tool[data-status="error"] .tool-state { color: #c2334f; border-color: color-mix(in srgb, #c2334f 35%, transparent); }
557
+ .tool-body { padding: 0 10px 10px; display: grid; gap: 6px; }
558
+ .tool-body .label { font-size: 10.5px; text-transform: uppercase; letter-spacing: .06em; color: var(--dw-muted); }
559
+ .tool-body pre {
560
+ margin: 0; font-family: var(--dw-mono); font-size: 11.5px; line-height: 1.5; background: var(--dw-bg);
561
+ border: 1px solid var(--dw-border); border-radius: 8px; padding: 8px; max-height: 180px; overflow: auto; white-space: pre-wrap; overflow-wrap: anywhere;
562
+ }
563
+ .spin { width: 13px; height: 13px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--dw-accent) 25%, transparent); border-top-color: var(--dw-accent); animation: spin .7s linear infinite; flex: none; }
564
+ @keyframes spin { to { transform: rotate(360deg); } }
565
+ .typing { display: inline-flex; gap: 4px; padding: 10px 2px; }
566
+ .typing i { width: 7px; height: 7px; border-radius: 50%; background: var(--dw-muted); opacity: .4; animation: bounce 1.2s infinite; }
567
+ .typing i:nth-child(2) { animation-delay: .15s; } .typing i:nth-child(3) { animation-delay: .3s; }
568
+ @keyframes bounce { 0%, 60%, 100% { transform: none; opacity: .35; } 30% { transform: translateY(-4px); opacity: 1; } }
569
+
570
+ /* ---------- composer ---------- */
571
+ .composer { padding: 10px 12px 12px; border-top: 1px solid var(--dw-border); background: var(--dw-bg); }
572
+ .box {
573
+ display: flex; align-items: flex-end; gap: 8px; border: 1px solid var(--dw-border); border-radius: 14px;
574
+ padding: 6px 6px 6px 12px; background: var(--dw-bg); transition: border-color .15s, box-shadow .15s;
575
+ }
576
+ .box:focus-within { border-color: var(--dw-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--dw-accent) 16%, transparent); }
577
+ textarea {
578
+ flex: 1; resize: none; border: 0; outline: 0; background: transparent; color: var(--dw-fg);
579
+ font: inherit; font-size: 14.5px; line-height: 1.5; max-height: 140px; padding: 6px 0; min-height: 34px;
580
+ }
581
+ textarea::placeholder { color: var(--dw-muted); }
582
+ .send {
583
+ width: 36px; height: 36px; border-radius: 10px; flex: none; display: grid; place-items: center;
584
+ background: var(--dw-accent); color: var(--dw-accent-contrast); transition: opacity .15s, transform .15s;
585
+ }
586
+ .send:disabled { opacity: .35; cursor: default; }
587
+ .send:not(:disabled):hover { transform: translateY(-1px); }
588
+ .send svg { width: 17px; height: 17px; }
589
+ .foot { display: flex; justify-content: space-between; align-items: center; margin-top: 7px; font-size: 11px; color: var(--dw-muted); }
590
+ .foot a { color: inherit; text-decoration: none; font-weight: 600; }
591
+ .foot a:hover { color: var(--dw-accent); }
592
+ .challenge:empty { display: none; }
593
+ .challenge { display: flex; justify-content: center; padding: 8px 0; }
594
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important; } }
595
+ `
596
+ );
597
+
598
+ // src/widget/element.ts
599
+ var BOOL_ATTRS = ["show-reasoning", "show-tool-calls", "allow-agent-switch", "open", "persist", "hide-branding", "launcher-avatar"];
600
+ var ATTRS = [
601
+ "endpoint",
602
+ "agent",
603
+ "title",
604
+ "subtitle",
605
+ "greeting",
606
+ "intro",
607
+ "placeholder",
608
+ "suggestions",
609
+ "avatar",
610
+ "position",
611
+ "mode",
612
+ "theme",
613
+ "accent-color",
614
+ "locale",
615
+ "gateway-url",
616
+ "gateway-token",
617
+ "width",
618
+ "height",
619
+ "z-index",
620
+ ...BOOL_ATTRS
621
+ ];
622
+ var camel = (s) => s.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
623
+ var parseBool = (v) => v !== null && !/^(false|0|off|no)$/i.test(v);
624
+ var randomConversationId = () => {
625
+ const b = crypto.getRandomValues(new Uint8Array(9));
626
+ return Array.from(b, (x) => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"[x & 63]).join("");
627
+ };
628
+ var prettyToolName = (n) => n.replace(/^mcp_[^_]+(?:_[^_]+)*?__/, "").replace(/_/g, " ");
629
+ var store2 = {
630
+ get(k) {
631
+ try {
632
+ return localStorage.getItem(k);
633
+ } catch {
634
+ return null;
635
+ }
636
+ },
637
+ set(k, v) {
638
+ try {
639
+ localStorage.setItem(k, v);
640
+ } catch {
641
+ }
642
+ }
643
+ };
644
+ var turnstileScript = null;
645
+ function loadTurnstile() {
646
+ return turnstileScript ??= new Promise((resolve, reject) => {
647
+ if (window.turnstile) return resolve();
648
+ const s = document.createElement("script");
649
+ s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit";
650
+ s.async = true;
651
+ s.onload = () => resolve();
652
+ s.onerror = () => reject(new Error("Could not load Turnstile"));
653
+ document.head.appendChild(s);
654
+ });
655
+ }
656
+ var ElementBase = typeof HTMLElement === "undefined" ? class {
657
+ } : HTMLElement;
658
+ var DeweeChatElement = class extends ElementBase {
659
+ static get observedAttributes() {
660
+ return ATTRS;
661
+ }
662
+ root;
663
+ overrides = {};
664
+ api = null;
665
+ apiKey = "";
666
+ server = null;
667
+ agentId = "";
668
+ conversationId = "";
669
+ messages = [];
670
+ running = null;
671
+ activated = false;
672
+ strings = pickStrings();
673
+ mql = null;
674
+ els;
675
+ constructor() {
676
+ super();
677
+ this.root = this.attachShadow({ mode: "open" });
678
+ }
679
+ // ---------- public API ----------
680
+ /** Current effective configuration (attributes merged with `configure()` overrides). */
681
+ get config() {
682
+ const c = { endpoint: "/api/dewee" };
683
+ for (const a of ATTRS) {
684
+ const v = this.getAttribute(a);
685
+ if (v === null) continue;
686
+ if (BOOL_ATTRS.includes(a)) c[camel(a)] = parseBool(v);
687
+ else if (a === "suggestions") c.suggestions = v.trim().startsWith("[") ? safeJson(v, []) : v.split("|").map((s) => s.trim()).filter(Boolean);
688
+ else if (a === "z-index") c.zIndex = Number(v);
689
+ else c[camel(a)] = v;
690
+ }
691
+ return { ...c, ...this.overrides };
692
+ }
693
+ /** Updates configuration at runtime (wins over attributes). */
694
+ configure(partial) {
695
+ this.overrides = { ...this.overrides, ...partial };
696
+ if (this.els) this.applyConfig();
697
+ }
698
+ open() {
699
+ if (this.config.mode === "inline") return;
700
+ this.setAttribute("data-open", "");
701
+ this.removeAttribute("data-unread");
702
+ this.els.launcher.setAttribute("aria-expanded", "true");
703
+ this.activate();
704
+ setTimeout(() => this.els.input.focus(), 120);
705
+ this.emit("dewee:open");
706
+ }
707
+ close() {
708
+ if (this.config.mode === "inline") return;
709
+ this.removeAttribute("data-open");
710
+ this.els.launcher.setAttribute("aria-expanded", "false");
711
+ this.emit("dewee:close");
712
+ }
713
+ toggle() {
714
+ this.hasAttribute("data-open") ? this.close() : this.open();
715
+ }
716
+ /** Sends a message programmatically (opens the widget first). */
717
+ async send(text) {
718
+ const message = text.trim();
719
+ if (!message || this.running) return;
720
+ if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.open();
721
+ await this.activate();
722
+ this.els.input.value = "";
723
+ this.autosize();
724
+ this.pushMessage({ role: "user", content: message });
725
+ const reply = { role: "assistant", content: "", status: "streaming", startedAt: Date.now() };
726
+ this.pushMessage(reply);
727
+ this.setRunning(new AbortController());
728
+ this.emit("dewee:send", { message, agent: this.agentId, conversationId: this.conversationId });
729
+ try {
730
+ await this.client().chat(
731
+ { message, agent: this.agentId, conversationId: this.conversationId },
732
+ (e) => this.onStreamEvent(reply, e),
733
+ this.running.signal
734
+ );
735
+ if (reply.status === "streaming") reply.status = "done";
736
+ } catch (e) {
737
+ if (e.name === "AbortError") {
738
+ reply.status = "done";
739
+ if (!reply.content) reply.content = `_${this.strings.stopped}_`;
740
+ } else {
741
+ reply.status = "error";
742
+ this.showError(reply, e);
743
+ }
744
+ } finally {
745
+ this.finishThinking(reply);
746
+ this.updateMessage(reply);
747
+ this.setRunning(null);
748
+ if (reply.status === "done") {
749
+ this.emit("dewee:reply", { content: reply.content, agent: this.agentId, conversationId: this.conversationId });
750
+ if (this.config.mode !== "inline" && !this.hasAttribute("data-open")) this.setAttribute("data-unread", "");
751
+ }
752
+ }
753
+ }
754
+ /** Stops the running reply. */
755
+ stop() {
756
+ this.running?.abort();
757
+ }
758
+ /** Starts a fresh conversation with the current agent. */
759
+ newConversation() {
760
+ this.stop();
761
+ this.conversationId = randomConversationId();
762
+ if (this.config.persist !== false) store2.set(this.convKey(), this.conversationId);
763
+ this.messages = [];
764
+ this.renderLog();
765
+ this.els.input.focus();
766
+ }
767
+ // ---------- lifecycle ----------
768
+ connectedCallback() {
769
+ if (!this.els) this.renderShell();
770
+ this.applyConfig();
771
+ if (this.config.mode === "inline" || this.config.open) {
772
+ if (this.config.mode !== "inline") this.open();
773
+ else this.activate();
774
+ }
775
+ }
776
+ disconnectedCallback() {
777
+ this.stop();
778
+ this.mql?.removeEventListener("change", this.onScheme);
779
+ }
780
+ attributeChangedCallback() {
781
+ if (this.els) this.applyConfig();
782
+ }
783
+ // ---------- internals ----------
784
+ emit(name, detail) {
785
+ this.dispatchEvent(new CustomEvent(name, { detail, bubbles: true, composed: true }));
786
+ }
787
+ client() {
788
+ const c = this.config;
789
+ const key = [c.endpoint, c.gatewayUrl, c.gatewayToken].join("|");
790
+ if (!this.api || key !== this.apiKey) {
791
+ this.apiKey = key;
792
+ this.api = new ApiClient({
793
+ endpoint: c.endpoint,
794
+ gatewayUrl: c.gatewayUrl,
795
+ gatewayToken: c.gatewayToken,
796
+ turnstile: (siteKey) => this.challenge(siteKey)
797
+ });
798
+ }
799
+ return this.api;
800
+ }
801
+ convKey() {
802
+ return `dewee-webchat:conv:${this.config.endpoint}:${this.config.gatewayUrl || ""}:${this.agentId}`;
803
+ }
804
+ onScheme = () => this.applyTheme();
805
+ applyTheme() {
806
+ const t = this.config.theme || "light";
807
+ const dark = t === "dark" || t === "auto" && !!this.mql?.matches;
808
+ this.setAttribute("data-theme", dark ? "dark" : "light");
809
+ }
810
+ applyConfig() {
811
+ const c = this.config;
812
+ this.strings = pickStrings(c.locale);
813
+ const s = this.strings;
814
+ this.setAttribute("data-position", c.position || "bottom-right");
815
+ this.setAttribute("data-mode", c.mode || "floating");
816
+ if (c.theme === "auto" && !this.mql) {
817
+ this.mql = matchMedia("(prefers-color-scheme: dark)");
818
+ this.mql.addEventListener("change", this.onScheme);
819
+ }
820
+ this.applyTheme();
821
+ const setVar = (k, v) => v !== void 0 && v !== "" ? this.style.setProperty(k, String(v)) : this.style.removeProperty(k);
822
+ setVar("--dw-accent", c.accentColor);
823
+ setVar("--dw-width", c.width);
824
+ setVar("--dw-height", c.height);
825
+ setVar("--dw-z", c.zIndex);
826
+ const e = this.els;
827
+ e.title.textContent = c.title || s.title;
828
+ e.avatar.replaceChildren();
829
+ if (c.avatar) {
830
+ const img = document.createElement("img");
831
+ img.src = c.avatar;
832
+ img.alt = "";
833
+ e.avatar.appendChild(img);
834
+ } else e.avatar.textContent = (c.title || s.title).trim().charAt(0).toUpperCase();
835
+ e.launcher.innerHTML = c.avatar && c.launcherAvatar ? "" : icons.chat;
836
+ if (c.avatar && c.launcherAvatar) {
837
+ const img = document.createElement("img");
838
+ img.src = c.avatar;
839
+ img.alt = "";
840
+ e.launcher.appendChild(img);
841
+ }
842
+ e.launcher.insertAdjacentHTML("beforeend", `<span class="badge"></span>`);
843
+ e.launcher.setAttribute("aria-label", s.open);
844
+ e.close.setAttribute("aria-label", s.close);
845
+ e.close.title = s.close;
846
+ e.newChat.setAttribute("aria-label", s.newChat);
847
+ e.newChat.title = s.newChat;
848
+ e.input.placeholder = c.placeholder || s.placeholder;
849
+ e.foot.hidden = !!c.hideBranding;
850
+ e.foot.querySelector(".by").textContent = s.poweredBy + " ";
851
+ this.renderSubtitle();
852
+ this.updateSendButton();
853
+ if (this.activated && this.api && this.apiKey !== [c.endpoint, c.gatewayUrl, c.gatewayToken].join("|")) {
854
+ this.activated = false;
855
+ this.server = null;
856
+ this.activate();
857
+ } else if (this.activated && c.agent && c.agent !== this.agentId && this.server) {
858
+ this.switchAgent(c.agent);
859
+ } else if (this.activated) {
860
+ this.renderLog();
861
+ }
862
+ }
863
+ renderShell() {
864
+ this.root.innerHTML = `
865
+ <style>${styles}</style>
866
+ <button class="launcher" part="launcher" aria-expanded="false" aria-haspopup="dialog"></button>
867
+ <section class="panel" part="panel" role="dialog" aria-label="Chat">
868
+ <header class="head" part="header">
869
+ <div class="avatar"></div>
870
+ <div class="who">
871
+ <div class="title"></div>
872
+ <div class="subtitle"><span class="dot"></span><span class="status"></span><select class="agent-select" hidden></select></div>
873
+ </div>
874
+ <button class="icon-btn new">${icons.newChat}</button>
875
+ <button class="icon-btn close">${icons.close}</button>
876
+ </header>
877
+ <div class="log" part="log" role="log" aria-live="polite"></div>
878
+ <div class="challenge"><slot name="challenge"></slot></div>
879
+ <form class="composer" part="composer">
880
+ <div class="box">
881
+ <textarea rows="1" maxlength="4000"></textarea>
882
+ <button class="send" type="submit"></button>
883
+ </div>
884
+ <div class="foot"><span><span class="by"></span><a href="https://dewee.sh" target="_blank" rel="noopener">dewee</a></span><span class="meta"></span></div>
885
+ </form>
886
+ </section>`;
887
+ const $ = (sel) => this.root.querySelector(sel);
888
+ this.els = {
889
+ launcher: $(".launcher"),
890
+ panel: $(".panel"),
891
+ log: $(".log"),
892
+ title: $(".title"),
893
+ subtitle: $(".subtitle"),
894
+ status: $(".status"),
895
+ avatar: $(".avatar"),
896
+ select: $(".agent-select"),
897
+ input: $("textarea"),
898
+ send: $(".send"),
899
+ newChat: $(".new"),
900
+ close: $(".close"),
901
+ foot: $(".foot")
902
+ };
903
+ const e = this.els;
904
+ e.launcher.addEventListener("click", () => this.toggle());
905
+ e.close.addEventListener("click", () => this.close());
906
+ e.log.addEventListener("scroll", () => {
907
+ this.following = this.nearBottom();
908
+ }, { passive: true });
909
+ e.newChat.addEventListener("click", () => this.newConversation());
910
+ e.select.addEventListener("change", () => this.switchAgent(e.select.value));
911
+ $("form").addEventListener("submit", (ev) => {
912
+ ev.preventDefault();
913
+ this.running ? this.stop() : this.send(e.input.value);
914
+ });
915
+ e.input.addEventListener("keydown", (ev) => {
916
+ if (ev.key === "Enter" && !ev.shiftKey && !ev.isComposing && ev.keyCode !== 229) {
917
+ ev.preventDefault();
918
+ if (!this.running) this.send(e.input.value);
919
+ }
920
+ });
921
+ e.input.addEventListener("input", () => {
922
+ this.autosize();
923
+ this.updateSendButton();
924
+ });
925
+ e.panel.addEventListener("keydown", (ev) => {
926
+ if (ev.key === "Escape") this.close();
927
+ });
928
+ }
929
+ autosize() {
930
+ const t = this.els.input;
931
+ t.style.height = "auto";
932
+ t.style.height = Math.min(t.scrollHeight, 140) + "px";
933
+ }
934
+ updateSendButton() {
935
+ const b = this.els.send;
936
+ b.innerHTML = this.running ? icons.stop : icons.send;
937
+ b.setAttribute("aria-label", this.running ? this.strings.stop : this.strings.send);
938
+ b.title = b.getAttribute("aria-label");
939
+ b.disabled = !this.running && !this.els.input.value.trim();
940
+ }
941
+ setRunning(ctrl) {
942
+ this.running = ctrl;
943
+ this.updateSendButton();
944
+ }
945
+ renderSubtitle() {
946
+ const e = this.els;
947
+ const c = this.config;
948
+ const agents = this.server?.agents ?? [];
949
+ const canSwitch = c.allowAgentSwitch !== false && !!this.server?.allowAgentSwitch && agents.length > 1;
950
+ e.select.hidden = !canSwitch;
951
+ e.status.hidden = canSwitch;
952
+ if (canSwitch) {
953
+ e.select.replaceChildren(...agents.map((a) => new Option(a.name || a.id, a.id, false, a.id === this.agentId)));
954
+ e.select.setAttribute("aria-label", "Agent");
955
+ }
956
+ const current = agents.find((a) => a.id === this.agentId);
957
+ e.status.textContent = c.subtitle || (current?.name && current.name !== c.title ? `${this.strings.online} \xB7 ${current.name}` : this.strings.online);
958
+ }
959
+ /** Loads backend config and the persisted conversation once. */
960
+ async activate() {
961
+ if (this.activated) return;
962
+ this.activated = true;
963
+ this.renderLog();
964
+ try {
965
+ this.server = await this.client().getConfig(true);
966
+ this.els.input.maxLength = this.server.maxMessageLength;
967
+ const wanted = this.config.agent;
968
+ const known = (id) => !!id && (this.server.agents.length === 0 || this.server.agents.some((a) => a.id === id));
969
+ this.agentId = known(wanted) ? wanted : this.server.defaultAgent || this.server.agents[0]?.id || wanted || "";
970
+ await this.loadConversation();
971
+ } catch (e) {
972
+ this.activated = false;
973
+ this.renderLog();
974
+ this.showError(null, e);
975
+ }
976
+ this.renderSubtitle();
977
+ this.emit("dewee:ready", { agent: this.agentId, config: this.server });
978
+ }
979
+ async switchAgent(id) {
980
+ if (id === this.agentId) return;
981
+ this.stop();
982
+ this.agentId = id;
983
+ this.renderSubtitle();
984
+ await this.loadConversation();
985
+ }
986
+ async loadConversation() {
987
+ const persist = this.config.persist !== false;
988
+ const saved = persist ? store2.get(this.convKey()) : null;
989
+ this.conversationId = saved || randomConversationId();
990
+ if (persist && !saved) store2.set(this.convKey(), this.conversationId);
991
+ this.messages = [];
992
+ this.renderLog();
993
+ if (!saved) return;
994
+ try {
995
+ const history = await this.client().history(this.agentId, this.conversationId);
996
+ this.messages = history.map((m) => ({ ...m, status: "done" }));
997
+ this.renderLog();
998
+ } catch {
999
+ }
1000
+ }
1001
+ renderLog() {
1002
+ const log = this.els.log;
1003
+ log.replaceChildren();
1004
+ if (!this.messages.length) log.appendChild(this.renderWelcome());
1005
+ for (const m of this.messages) log.appendChild(this.renderMessage(m));
1006
+ log.scrollTop = log.scrollHeight;
1007
+ }
1008
+ renderWelcome() {
1009
+ const c = this.config;
1010
+ const s = this.strings;
1011
+ const w = document.createElement("div");
1012
+ w.className = "welcome";
1013
+ const h = document.createElement("h2");
1014
+ h.textContent = c.greeting || s.greeting;
1015
+ const p = document.createElement("p");
1016
+ p.textContent = c.intro || s.greetingBody;
1017
+ w.append(h, p);
1018
+ if (c.suggestions?.length) {
1019
+ const list = document.createElement("div");
1020
+ list.className = "suggestions";
1021
+ for (const text of c.suggestions) {
1022
+ const b = document.createElement("button");
1023
+ b.className = "chip";
1024
+ b.type = "button";
1025
+ b.textContent = text;
1026
+ b.addEventListener("click", () => this.send(text));
1027
+ list.appendChild(b);
1028
+ }
1029
+ w.appendChild(list);
1030
+ }
1031
+ return w;
1032
+ }
1033
+ pushMessage(m) {
1034
+ if (!this.messages.length) this.els.log.replaceChildren();
1035
+ this.messages.push(m);
1036
+ this.els.log.appendChild(this.renderMessage(m));
1037
+ this.scrollToBottom(true);
1038
+ }
1039
+ renderMessage(m) {
1040
+ const root = document.createElement("div");
1041
+ root.className = `msg ${m.role}${m.status === "error" ? " error" : ""}`;
1042
+ const bubble = document.createElement("div");
1043
+ bubble.className = "bubble";
1044
+ root.appendChild(bubble);
1045
+ const trace = document.createElement("div");
1046
+ trace.className = "trace";
1047
+ const md = document.createElement("div");
1048
+ md.className = m.role === "assistant" ? "md" : "";
1049
+ if (m.role === "assistant") bubble.appendChild(trace);
1050
+ bubble.appendChild(md);
1051
+ m.view = { root, trace, md, tools: /* @__PURE__ */ new Map() };
1052
+ this.updateMessage(m);
1053
+ return root;
1054
+ }
1055
+ updateMessage(m) {
1056
+ const v = m.view;
1057
+ if (!v) return;
1058
+ const c = this.config;
1059
+ if (m.role === "user") {
1060
+ v.md.textContent = m.content;
1061
+ return;
1062
+ }
1063
+ v.root.classList.toggle("error", m.status === "error");
1064
+ if (c.showReasoning !== false && m.thinking) {
1065
+ if (!v.thinking) {
1066
+ v.thinking = document.createElement("details");
1067
+ v.thinking.className = "thinking";
1068
+ v.thinking.innerHTML = `<summary></summary><div class="thinking-body"></div>`;
1069
+ v.trace.prepend(v.thinking);
1070
+ }
1071
+ const streaming = m.status === "streaming" && !m.thinkingMs;
1072
+ const label = streaming ? this.strings.thinking : m.thinkingMs ? this.strings.thoughtFor(Math.max(1, Math.round(m.thinkingMs / 1e3))) : this.strings.reasoning;
1073
+ const summary = v.thinking.querySelector("summary");
1074
+ summary.innerHTML = `${streaming ? '<span class="spin"></span>' : icons.brain}<span></span>${icons.down.replace("<svg", '<svg class="chev"')}`;
1075
+ summary.querySelector("span:not(.spin)").textContent = label;
1076
+ v.thinking.querySelector(".thinking-body").textContent = m.thinking;
1077
+ } else if (v.thinking) {
1078
+ v.thinking.remove();
1079
+ v.thinking = void 0;
1080
+ }
1081
+ if (c.showToolCalls !== false) {
1082
+ for (const t of m.toolCalls ?? []) this.renderTool(v, t);
1083
+ } else {
1084
+ v.tools.forEach((d) => d.remove());
1085
+ v.tools.clear();
1086
+ }
1087
+ v.trace.hidden = !v.trace.childElementCount;
1088
+ const typing = m.status === "streaming" && !m.content;
1089
+ if (typing) {
1090
+ v.md.innerHTML = `<span class="typing" aria-label="${this.strings.thinking}"><i></i><i></i><i></i></span>`;
1091
+ } else {
1092
+ v.md.replaceChildren(renderMarkdown(document, m.content));
1093
+ v.md.classList.toggle("cursor", m.status === "streaming");
1094
+ }
1095
+ }
1096
+ renderTool(v, t) {
1097
+ let d = v.tools.get(t.id);
1098
+ if (!d) {
1099
+ d = document.createElement("details");
1100
+ d.className = "tool";
1101
+ d.innerHTML = `<summary><span class="lead"></span><span class="tool-name"></span><span class="tool-state"></span>${icons.down.replace("<svg", '<svg class="chev"')}</summary><div class="tool-body"></div>`;
1102
+ v.tools.set(t.id, d);
1103
+ v.trace.appendChild(d);
1104
+ }
1105
+ d.dataset.status = t.status;
1106
+ d.querySelector(".lead").innerHTML = t.status === "running" ? '<span class="spin"></span>' : icons.tool;
1107
+ const name = d.querySelector(".tool-name");
1108
+ name.textContent = prettyToolName(t.name);
1109
+ name.setAttribute("title", t.name);
1110
+ d.querySelector(".tool-state").textContent = t.status === "running" ? this.strings.toolRunning : t.status === "error" ? this.strings.toolError : this.strings.toolDone;
1111
+ const body = d.querySelector(".tool-body");
1112
+ body.replaceChildren();
1113
+ const block = (label, value) => {
1114
+ if (value === void 0 || value === null || value === "") return;
1115
+ const l = document.createElement("div");
1116
+ l.className = "label";
1117
+ l.textContent = label;
1118
+ const pre = document.createElement("pre");
1119
+ pre.textContent = typeof value === "string" ? tryPretty(value) : JSON.stringify(value, null, 2);
1120
+ body.append(l, pre);
1121
+ };
1122
+ block(this.strings.args, t.arguments);
1123
+ block(this.strings.result, t.result);
1124
+ }
1125
+ onStreamEvent(m, e) {
1126
+ switch (e.type) {
1127
+ case "start":
1128
+ if (e.conversationId && e.conversationId !== this.conversationId) {
1129
+ this.conversationId = e.conversationId;
1130
+ if (this.config.persist !== false) store2.set(this.convKey(), e.conversationId);
1131
+ }
1132
+ break;
1133
+ case "thinking":
1134
+ m.thinking = (m.thinking || "") + e.text;
1135
+ break;
1136
+ case "delta":
1137
+ this.finishThinking(m);
1138
+ m.content += e.text;
1139
+ break;
1140
+ case "tool_call":
1141
+ this.finishThinking(m);
1142
+ (m.toolCalls ??= []).push({ id: e.id, name: e.name, arguments: e.arguments, status: "running" });
1143
+ break;
1144
+ case "tool_result": {
1145
+ const t = m.toolCalls?.find((x) => x.id === e.id) ?? (m.toolCalls ??= [])[m.toolCalls.push({ id: e.id, name: e.name, status: "running" }) - 1];
1146
+ t.status = e.isError ? "error" : "done";
1147
+ t.result = e.result;
1148
+ t.isError = e.isError;
1149
+ break;
1150
+ }
1151
+ case "done":
1152
+ this.finishThinking(m);
1153
+ if (e.content && e.content.length >= m.content.length) m.content = e.content;
1154
+ m.status = "done";
1155
+ break;
1156
+ case "error":
1157
+ m.status = "error";
1158
+ this.showError(m, new ApiError(e.message, 500, e.code));
1159
+ break;
1160
+ }
1161
+ this.scheduleUpdate(m);
1162
+ }
1163
+ pendingFrame = 0;
1164
+ following = true;
1165
+ scheduleUpdate(m) {
1166
+ if (this.pendingFrame) return;
1167
+ this.pendingFrame = requestAnimationFrame(() => {
1168
+ this.pendingFrame = 0;
1169
+ this.updateMessage(m);
1170
+ if (this.following) this.scrollToBottom(true);
1171
+ });
1172
+ }
1173
+ finishThinking(m) {
1174
+ if (m.thinking && !m.thinkingMs && m.startedAt) m.thinkingMs = Date.now() - m.startedAt;
1175
+ }
1176
+ nearBottom() {
1177
+ const l = this.els.log;
1178
+ return l.scrollHeight - l.scrollTop - l.clientHeight < 96;
1179
+ }
1180
+ scrollToBottom(force = false) {
1181
+ const l = this.els.log;
1182
+ if (force || this.nearBottom()) {
1183
+ l.scrollTop = l.scrollHeight;
1184
+ this.following = true;
1185
+ }
1186
+ }
1187
+ showError(m, err) {
1188
+ const text = err.message || this.strings.errorGeneric;
1189
+ this.emit("dewee:error", { message: text, code: err.code });
1190
+ if (m) {
1191
+ m.content = m.content ? `${m.content}
1192
+
1193
+ > \u26A0\uFE0F ${text}` : `\u26A0\uFE0F ${text}`;
1194
+ m.status = "error";
1195
+ this.updateMessage(m);
1196
+ return;
1197
+ }
1198
+ const div = document.createElement("div");
1199
+ div.className = "msg error";
1200
+ div.innerHTML = `<div class="bubble"></div>`;
1201
+ div.firstElementChild.textContent = `\u26A0\uFE0F ${text}`;
1202
+ this.els.log.appendChild(div);
1203
+ }
1204
+ /** Renders a Turnstile challenge in light DOM (slotted) and resolves with its token. */
1205
+ async challenge(siteKey) {
1206
+ await loadTurnstile();
1207
+ const holder = document.createElement("div");
1208
+ holder.slot = "challenge";
1209
+ this.appendChild(holder);
1210
+ try {
1211
+ return await new Promise((resolve, reject) => {
1212
+ window.turnstile.render(holder, {
1213
+ sitekey: siteKey,
1214
+ appearance: "interaction-only",
1215
+ theme: this.getAttribute("data-theme") === "dark" ? "dark" : "light",
1216
+ callback: resolve,
1217
+ "error-callback": () => reject(new ApiError("Verification failed", 403, "turnstile_failed"))
1218
+ });
1219
+ });
1220
+ } finally {
1221
+ holder.remove();
1222
+ }
1223
+ }
1224
+ };
1225
+ function safeJson(s, fallback) {
1226
+ try {
1227
+ return JSON.parse(s);
1228
+ } catch {
1229
+ return fallback;
1230
+ }
1231
+ }
1232
+ function tryPretty(s) {
1233
+ const t = s.trim();
1234
+ if (!/^[[{]/.test(t)) return s;
1235
+ try {
1236
+ return JSON.stringify(JSON.parse(t), null, 2);
1237
+ } catch {
1238
+ return s;
1239
+ }
1240
+ }
1241
+
1242
+ // src/index.ts
1243
+ function defineDeweeChat(tag = "dewee-chat") {
1244
+ if (typeof window === "undefined" || !window.customElements) return;
1245
+ if (!customElements.get(tag)) customElements.define(tag, class extends DeweeChatElement {
1246
+ });
1247
+ }
1248
+ function mount(config = {}, target) {
1249
+ defineDeweeChat();
1250
+ const el2 = document.createElement("dewee-chat");
1251
+ el2.configure(config);
1252
+ const parent = typeof target === "string" ? document.querySelector(target) : target;
1253
+ (parent ?? document.body).appendChild(el2);
1254
+ return el2;
1255
+ }
1256
+ defineDeweeChat();
1257
+
1258
+ export {
1259
+ ApiError,
1260
+ ApiClient,
1261
+ renderMarkdown,
1262
+ DeweeChatElement,
1263
+ defineDeweeChat,
1264
+ mount
1265
+ };