arcy.js 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3860 @@
1
+ import { CHAT_CONTRACT, CHAT_GLOBAL, FALLBACK_CHROME, resolveImageUrl, carriesFiles, droppedFiles, FONT_KEY_PROPERTY, PANEL_GAP, dragTransform, MOBILE_MARGIN, surfaceWidth, PANEL_MARGIN, clampOffsetX, panelHeight, TOOLTIP_CSS, BAR_TRAY_CLASS, ATTACHABLE_TYPES, POLICY_LINK_CLASS } from './chunk-DW3GUQ6J.js';
2
+ export { resolveImageUrl } from './chunk-DW3GUQ6J.js';
3
+ import { normalizeOptionReply, validateFillReply, fetchFlowCatalog } from './chunk-CQ2DESAJ.js';
4
+ import { injectStyles, applyNonce } from './chunk-LGWYJSYX.js';
5
+
6
+ /* arcy.js — https://arcyai.com */
7
+
8
+ // src/shell/markdown.ts
9
+ var HEADER_RE = /^(#{1,3})\s+(.*)$/;
10
+ var BULLET_RE = /^[-*]\s+(.*)$/;
11
+ var NUMBERED_RE = /^\d+\.\s+(.*)$/;
12
+ var FENCE = "```";
13
+ var INLINE_RE = /`([^`]+)`|\*\*(.+?)\*\*|\*(.+?)\*|\[([^\]]+)\]\(([^)]+)\)/g;
14
+ function isSafeUrl(url) {
15
+ return /^https?:\/\//i.test(url);
16
+ }
17
+ function appendInline(doc, parent, text) {
18
+ let lastIndex = 0;
19
+ INLINE_RE.lastIndex = 0;
20
+ let match;
21
+ while (match = INLINE_RE.exec(text)) {
22
+ if (match.index > lastIndex) {
23
+ parent.appendChild(doc.createTextNode(text.slice(lastIndex, match.index)));
24
+ }
25
+ const [whole, code, bold, italic, linkText, linkUrl] = match;
26
+ if (code !== void 0) {
27
+ const el = doc.createElement("code");
28
+ el.textContent = code;
29
+ parent.appendChild(el);
30
+ } else if (bold !== void 0) {
31
+ const strong = doc.createElement("strong");
32
+ strong.textContent = bold;
33
+ parent.appendChild(strong);
34
+ } else if (italic !== void 0) {
35
+ const em = doc.createElement("em");
36
+ em.textContent = italic;
37
+ parent.appendChild(em);
38
+ } else if (linkText !== void 0 && linkUrl !== void 0) {
39
+ if (isSafeUrl(linkUrl)) {
40
+ const a = doc.createElement("a");
41
+ a.setAttribute("href", linkUrl);
42
+ a.setAttribute("target", "_blank");
43
+ a.setAttribute("rel", "noopener noreferrer");
44
+ a.textContent = linkText;
45
+ parent.appendChild(a);
46
+ } else {
47
+ parent.appendChild(doc.createTextNode(linkText));
48
+ }
49
+ }
50
+ lastIndex = match.index + whole.length;
51
+ }
52
+ if (lastIndex < text.length) {
53
+ parent.appendChild(doc.createTextNode(text.slice(lastIndex)));
54
+ }
55
+ }
56
+ function splitFences(lines) {
57
+ const out = [];
58
+ let open = false;
59
+ for (const line of lines) {
60
+ if (!line.includes(FENCE)) {
61
+ out.push(line);
62
+ continue;
63
+ }
64
+ const parts = line.split(FENCE);
65
+ parts.forEach((part, i) => {
66
+ if (i > 0) {
67
+ out.push(FENCE);
68
+ open = !open;
69
+ }
70
+ if (open && i > 0 && /^\s*[A-Za-z0-9+#-]*\s*$/.test(part)) return;
71
+ if (part !== "") out.push(part);
72
+ });
73
+ }
74
+ return out;
75
+ }
76
+ function renderMarkdown(doc, container, text) {
77
+ try {
78
+ while (container.firstChild) container.removeChild(container.firstChild);
79
+ const lines = splitFences(text.split("\n"));
80
+ let paragraphLines = [];
81
+ let list = null;
82
+ let codeLines = null;
83
+ const flushParagraph = () => {
84
+ if (paragraphLines.length === 0) return;
85
+ const p = doc.createElement("p");
86
+ appendInline(doc, p, paragraphLines.join(" "));
87
+ container.appendChild(p);
88
+ paragraphLines = [];
89
+ };
90
+ const flushList = () => {
91
+ list = null;
92
+ };
93
+ const flushCode = () => {
94
+ if (codeLines === null) return;
95
+ const pre = doc.createElement("pre");
96
+ const code = doc.createElement("code");
97
+ while (codeLines.length > 0 && codeLines[0].trim() === "") codeLines.shift();
98
+ while (codeLines.length > 0 && codeLines[codeLines.length - 1].trim() === "")
99
+ codeLines.pop();
100
+ code.textContent = codeLines.join("\n");
101
+ pre.appendChild(code);
102
+ container.appendChild(pre);
103
+ codeLines = null;
104
+ };
105
+ for (const rawLine of lines) {
106
+ if (rawLine.trim() === FENCE) {
107
+ if (codeLines === null) {
108
+ flushParagraph();
109
+ flushList();
110
+ codeLines = [];
111
+ } else {
112
+ flushCode();
113
+ }
114
+ continue;
115
+ }
116
+ if (codeLines !== null) {
117
+ codeLines.push(rawLine);
118
+ continue;
119
+ }
120
+ const line = rawLine.trim();
121
+ if (line === "") {
122
+ flushParagraph();
123
+ flushList();
124
+ continue;
125
+ }
126
+ const header = HEADER_RE.exec(line);
127
+ if (header) {
128
+ flushParagraph();
129
+ flushList();
130
+ const level = Math.max(2, header[1].length);
131
+ const h = doc.createElement(`h${level}`);
132
+ appendInline(doc, h, header[2]);
133
+ container.appendChild(h);
134
+ continue;
135
+ }
136
+ const bullet = BULLET_RE.exec(line);
137
+ if (bullet) {
138
+ flushParagraph();
139
+ if (!list || list.kind !== "ul") {
140
+ list = { kind: "ul", el: doc.createElement("ul") };
141
+ container.appendChild(list.el);
142
+ }
143
+ const li = doc.createElement("li");
144
+ appendInline(doc, li, bullet[1]);
145
+ list.el.appendChild(li);
146
+ continue;
147
+ }
148
+ const numbered = NUMBERED_RE.exec(line);
149
+ if (numbered) {
150
+ flushParagraph();
151
+ if (!list || list.kind !== "ol") {
152
+ list = { kind: "ol", el: doc.createElement("ol") };
153
+ container.appendChild(list.el);
154
+ }
155
+ const li = doc.createElement("li");
156
+ appendInline(doc, li, numbered[1]);
157
+ list.el.appendChild(li);
158
+ continue;
159
+ }
160
+ flushList();
161
+ paragraphLines.push(line);
162
+ }
163
+ flushCode();
164
+ flushParagraph();
165
+ } catch {
166
+ }
167
+ }
168
+
169
+ // src/shell/flow-offer.ts
170
+ var OFFER_CLASS = "arcy-flow-offer";
171
+ var HINT_CLASS = "arcy-flow-offer-hint";
172
+ var BUTTON_CLASS = "arcy-flow-offer-button";
173
+ var ICON_CLASS = "arcy-flow-offer-icon";
174
+ var SVG_NS = "http://www.w3.org/2000/svg";
175
+ var FLOW_ICON_PATH = "M13 3L5 14h6l-1 7 8-11h-6l1-7z";
176
+ var MAX_FLOW_OFFERS = 1;
177
+ function flowIcon(doc) {
178
+ const svg = doc.createElementNS(SVG_NS, "svg");
179
+ svg.setAttribute("viewBox", "0 0 24 24");
180
+ svg.setAttribute("aria-hidden", "true");
181
+ svg.setAttribute("width", "15");
182
+ svg.setAttribute("height", "15");
183
+ svg.setAttribute("class", ICON_CLASS);
184
+ const path = doc.createElementNS(SVG_NS, "path");
185
+ path.setAttribute("d", FLOW_ICON_PATH);
186
+ path.setAttribute("fill", "none");
187
+ path.setAttribute("stroke", "currentColor");
188
+ path.setAttribute("stroke-width", "2");
189
+ path.setAttribute("stroke-linecap", "round");
190
+ path.setAttribute("stroke-linejoin", "round");
191
+ svg.appendChild(path);
192
+ return svg;
193
+ }
194
+ var FLOW_OFFER_CSS = `
195
+ .${OFFER_CLASS} {
196
+ display: flex;
197
+ flex-direction: column;
198
+ align-items: flex-start;
199
+ gap: 8px;
200
+ margin-top: 10px;
201
+ }
202
+
203
+ .${HINT_CLASS} {
204
+ font-size: 13px;
205
+ line-height: 1.4;
206
+ }
207
+
208
+ /* Filled in the operator's brand, not outlined: this is the one control in
209
+ the answer, and an outlined chip beside a filled user bubble reads as
210
+ secondary to the very thing it is offering to do. */
211
+ .${BUTTON_CLASS} {
212
+ appearance: none;
213
+ display: inline-flex;
214
+ align-items: center;
215
+ gap: 7px;
216
+ border: 0;
217
+ border-radius: 999px;
218
+ padding: 8px 14px;
219
+ background: var(--_arcy-brand-bg, #101828);
220
+ color: var(--_arcy-brand-text, #ffffff);
221
+ font: inherit;
222
+ font-size: 13px;
223
+ font-weight: 600;
224
+ line-height: 1.2;
225
+ text-align: left;
226
+ cursor: pointer;
227
+ -webkit-tap-highlight-color: transparent;
228
+ transition: background 120ms ease;
229
+ }
230
+
231
+ .${ICON_CLASS} {
232
+ flex-shrink: 0;
233
+ }
234
+
235
+ .${BUTTON_CLASS}:hover {
236
+ background: var(--_arcy-brand-bg-hover, var(--_arcy-brand-bg, #101828));
237
+ }
238
+
239
+ .${BUTTON_CLASS}:active {
240
+ background: var(--_arcy-brand-bg-active, var(--_arcy-brand-bg, #101828));
241
+ }
242
+
243
+ .${BUTTON_CLASS}:focus-visible {
244
+ outline: 2px solid var(--_arcy-brand-bg, #101828);
245
+ outline-offset: 2px;
246
+ }
247
+
248
+ @media (prefers-reduced-motion: reduce) {
249
+ .${BUTTON_CLASS} {
250
+ transition: none;
251
+ }
252
+ }
253
+ `;
254
+ function buildFlowOffer(doc, offers, hint, onStart) {
255
+ if (offers.length === 0) return null;
256
+ const block = doc.createElement("div");
257
+ block.className = OFFER_CLASS;
258
+ if (hint) {
259
+ const line = doc.createElement("div");
260
+ line.className = HINT_CLASS;
261
+ line.textContent = hint;
262
+ block.appendChild(line);
263
+ }
264
+ for (const offer of offers.slice(0, MAX_FLOW_OFFERS)) {
265
+ const button = doc.createElement("button");
266
+ button.type = "button";
267
+ button.className = BUTTON_CLASS;
268
+ button.appendChild(flowIcon(doc));
269
+ const label = doc.createElement("span");
270
+ label.textContent = offer.publicName ?? "Start";
271
+ button.appendChild(label);
272
+ button.addEventListener("click", () => {
273
+ button.disabled = true;
274
+ try {
275
+ onStart(offer.flowCvid);
276
+ } catch {
277
+ }
278
+ });
279
+ block.appendChild(button);
280
+ }
281
+ return block;
282
+ }
283
+
284
+ // src/shell/transcript.ts
285
+ var LIST_CLASS = "arcy-chat-list";
286
+ var MSG_CLASS = "arcy-chat-msg";
287
+ var ATTACHMENT_CLASS = "arcy-chat-msg-images";
288
+ var SENT_IMAGES_CLASS = "arcy-chat-msg-thumbs";
289
+ var META_CLASS = "arcy-chat-msg-meta";
290
+ var TIME_CLASS = "arcy-chat-msg-time";
291
+ var RATE_CLASS = "arcy-chat-rate";
292
+ var GROUP_CLASS = "arcy-chat-msg-group";
293
+ var CITATIONS_CLASS = "arcy-chat-citations";
294
+ var CITATION_LINK_CLASS = "arcy-chat-citation-link";
295
+ var CITATION_LABEL_CLASS = "arcy-chat-citation-label";
296
+ var PENDING_CLASS = "arcy-chat-pending";
297
+ var THUMB_UP_PATH = "M7 10v10H4V10zM7 10l4.5-7a2 2 0 0 1 3.4 2L13.5 9H19a2 2 0 0 1 2 2.3l-1 6A2 2 0 0 1 18 19H7";
298
+ var THUMB_DOWN_PATH = "M7 14V4H4v10zM7 14l4.5 7a2 2 0 0 0 3.4-2L13.5 15H19a2 2 0 0 0 2-2.3l-1-6A2 2 0 0 0 18 5H7";
299
+ var SVG_NS2 = "http://www.w3.org/2000/svg";
300
+ function thumbIcon(doc, path) {
301
+ const svg = doc.createElementNS(SVG_NS2, "svg");
302
+ svg.setAttribute("viewBox", "0 0 24 24");
303
+ svg.setAttribute("aria-hidden", "true");
304
+ svg.setAttribute("width", "14");
305
+ svg.setAttribute("height", "14");
306
+ const node = doc.createElementNS(SVG_NS2, "path");
307
+ node.setAttribute("d", path);
308
+ node.setAttribute("fill", "none");
309
+ node.setAttribute("stroke", "currentColor");
310
+ node.setAttribute("stroke-width", "2");
311
+ node.setAttribute("stroke-linecap", "round");
312
+ node.setAttribute("stroke-linejoin", "round");
313
+ svg.appendChild(node);
314
+ return svg;
315
+ }
316
+ function formatTime(at) {
317
+ try {
318
+ return at.toLocaleTimeString(void 0, {
319
+ hour: "2-digit",
320
+ minute: "2-digit"
321
+ });
322
+ } catch {
323
+ return "";
324
+ }
325
+ }
326
+ var TRANSCRIPT_CSS = `
327
+ .${LIST_CLASS} {
328
+ display: flex;
329
+ flex-direction: column;
330
+ gap: 8px;
331
+ padding: 12px;
332
+ }
333
+
334
+ .${MSG_CLASS} {
335
+ max-width: 85%;
336
+ padding: 8px 12px;
337
+ border-radius: 12px;
338
+ font-size: 13px;
339
+ line-height: 1.4;
340
+ white-space: pre-wrap;
341
+ overflow-wrap: break-word;
342
+ }
343
+
344
+ .${MSG_CLASS}[data-role="assistant"],
345
+ .${MSG_CLASS}[data-role="system"] {
346
+ align-self: flex-start;
347
+ background: var(--_arcy-assistant-bg, #f2f4f7);
348
+ color: var(--_arcy-main-text, #101828);
349
+ }
350
+
351
+ /* \u2500\u2500 Rendered markdown inside an answer (D1023, ADR 0129; D1034) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
352
+
353
+ /* The bubble is a chat message, not a document. Browser defaults give an h2
354
+ a 1.5em face and a full blank line above and below it, which turns a
355
+ three-step answer into something that reads like a landing page. These
356
+ rules keep every block on the bubble's own scale. */
357
+ .${MSG_CLASS} > :first-child {
358
+ margin-top: 0;
359
+ }
360
+
361
+ .${MSG_CLASS} > :last-child {
362
+ margin-bottom: 0;
363
+ }
364
+
365
+ .${MSG_CLASS} p {
366
+ margin: 0 0 8px;
367
+ }
368
+
369
+ .${MSG_CLASS} h2,
370
+ .${MSG_CLASS} h3 {
371
+ margin: 12px 0 6px;
372
+ line-height: 1.3;
373
+ font-weight: 600;
374
+ }
375
+
376
+ .${MSG_CLASS} h2 {
377
+ font-size: 15px;
378
+ }
379
+
380
+ .${MSG_CLASS} h3 {
381
+ font-size: 13px;
382
+ }
383
+
384
+ .${MSG_CLASS} ul,
385
+ .${MSG_CLASS} ol {
386
+ margin: 0 0 8px;
387
+ padding-left: 20px;
388
+ }
389
+
390
+ .${MSG_CLASS} li {
391
+ margin: 2px 0;
392
+ }
393
+
394
+ /* D1034: code. The agent answers install questions with real snippets, so
395
+ these have to read as code rather than as a sentence with backticks in
396
+ it. The block scrolls on its own rather than widening the bubble, since a
397
+ long line would otherwise push the panel sideways. */
398
+ .${MSG_CLASS} code {
399
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
400
+ font-size: 0.9em;
401
+ padding: 1px 4px;
402
+ border-radius: 4px;
403
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
404
+ white-space: pre-wrap;
405
+ overflow-wrap: break-word;
406
+ }
407
+
408
+ .${MSG_CLASS} pre {
409
+ margin: 0 0 8px;
410
+ padding: 10px 12px;
411
+ border-radius: 8px;
412
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 6%, transparent);
413
+ border: 1px solid color-mix(in srgb, var(--_arcy-main-text, #101828) 10%, transparent);
414
+ overflow-x: auto;
415
+ white-space: pre;
416
+ }
417
+
418
+ .${MSG_CLASS} pre code {
419
+ padding: 0;
420
+ background: none;
421
+ white-space: pre;
422
+ overflow-wrap: normal;
423
+ }
424
+
425
+ /* Above the bubble, as a sibling of it: see addMessage(). */
426
+ .${ATTACHMENT_CLASS} {
427
+ display: block;
428
+ font-size: 11px;
429
+ opacity: 0.75;
430
+ }
431
+
432
+ .${SENT_IMAGES_CLASS} {
433
+ display: flex;
434
+ flex-wrap: wrap;
435
+ gap: 4px;
436
+ max-width: 100%;
437
+ }
438
+
439
+ /* Sized by height, the width left to the picture: a fixed square cropped
440
+ every screenshot to its middle. */
441
+ .${SENT_IMAGES_CLASS} img {
442
+ display: block;
443
+ width: auto;
444
+ height: 120px;
445
+ max-width: 100%;
446
+ object-fit: cover;
447
+ border-radius: 12px;
448
+ border: 1px solid
449
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 12%, transparent);
450
+ }
451
+
452
+ .${MSG_CLASS}[data-role="user"] {
453
+ align-self: flex-end;
454
+ background: var(--_arcy-brand-bg, #101828);
455
+ color: var(--_arcy-brand-text, #ffffff);
456
+ }
457
+
458
+ /* One turn: the bubble, then its meta row. The group carries the alignment
459
+ so the row sits under the bubble's own edge rather than the panel's. */
460
+ .${GROUP_CLASS} {
461
+ display: flex;
462
+ flex-direction: column;
463
+ gap: 4px;
464
+ max-width: 85%;
465
+ }
466
+
467
+ .${GROUP_CLASS}[data-role="user"] {
468
+ align-self: flex-end;
469
+ align-items: flex-end;
470
+ }
471
+
472
+ .${GROUP_CLASS}[data-role="assistant"],
473
+ .${GROUP_CLASS}[data-role="system"] {
474
+ align-self: flex-start;
475
+ align-items: flex-start;
476
+ }
477
+
478
+ /* The bubble is the whole width of its group now, so its own alignment
479
+ would fight the group's. */
480
+ .${GROUP_CLASS} .${MSG_CLASS} {
481
+ max-width: 100%;
482
+ align-self: auto;
483
+ }
484
+
485
+ .${META_CLASS} {
486
+ display: flex;
487
+ align-items: center;
488
+ gap: 2px;
489
+ padding: 0 4px;
490
+ min-height: 20px;
491
+ }
492
+
493
+ .${TIME_CLASS} {
494
+ font-size: 11px;
495
+ line-height: 1;
496
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 45%, transparent);
497
+ font-variant-numeric: tabular-nums;
498
+ margin-right: 4px;
499
+ }
500
+
501
+ /* No background at rest, one on hover. The rating controls sit under every
502
+ answer, and two filled chips per turn would read as buttons the visitor
503
+ is expected to press. */
504
+ .${RATE_CLASS} {
505
+ appearance: none;
506
+ border: 0;
507
+ cursor: pointer;
508
+ font: inherit;
509
+ display: flex;
510
+ align-items: center;
511
+ justify-content: center;
512
+ padding: 4px;
513
+ border-radius: 6px;
514
+ background: transparent;
515
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 45%, transparent);
516
+ -webkit-tap-highlight-color: transparent;
517
+ transition: background 120ms ease, color 120ms ease;
518
+ }
519
+
520
+ .${RATE_CLASS}:hover {
521
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
522
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 80%, transparent);
523
+ }
524
+
525
+ .${RATE_CLASS}[aria-pressed="true"] {
526
+ color: var(--_arcy-brand-bg, #101828);
527
+ background: color-mix(in srgb, var(--_arcy-brand-bg, #101828) 12%, transparent);
528
+ }
529
+
530
+ .${RATE_CLASS}:focus-visible {
531
+ outline: 2px solid currentColor;
532
+ outline-offset: 1px;
533
+ }
534
+
535
+ @media (prefers-reduced-motion: reduce) {
536
+ .${RATE_CLASS} {
537
+ transition: none;
538
+ }
539
+ }
540
+
541
+ /* \u2500\u2500 Citations (D1023, ADR 0129) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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
+ .${CITATIONS_CLASS} {
544
+ display: flex;
545
+ flex-wrap: wrap;
546
+ gap: 6px;
547
+ padding: 0 4px;
548
+ font-size: 11px;
549
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 55%, transparent);
550
+ }
551
+
552
+ .${CITATION_LABEL_CLASS} {
553
+ font-weight: 600;
554
+ }
555
+
556
+ /* D1034: a citation is a link, so it has to answer the pointer like one.
557
+ At rest it stays quiet enough not to compete with the answer above it. */
558
+ .${CITATION_LINK_CLASS} {
559
+ color: inherit;
560
+ text-decoration: underline;
561
+ text-decoration-color: color-mix(in srgb, currentColor 35%, transparent);
562
+ text-underline-offset: 2px;
563
+ border-radius: 3px;
564
+ transition: color 120ms ease, text-decoration-color 120ms ease;
565
+ }
566
+
567
+ .${CITATION_LINK_CLASS}:hover {
568
+ color: var(--_arcy-main-text, #101828);
569
+ text-decoration-color: currentColor;
570
+ }
571
+
572
+ .${CITATION_LINK_CLASS}:focus-visible {
573
+ outline: 2px solid currentColor;
574
+ outline-offset: 2px;
575
+ }
576
+
577
+ @media (prefers-reduced-motion: reduce) {
578
+ .${CITATION_LINK_CLASS} {
579
+ transition: none;
580
+ }
581
+ }
582
+
583
+ /* \u2500\u2500 "The agent is answering" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
584
+
585
+ /* Deliberately the assistant bubble's own background and corner, at the
586
+ assistant's own alignment: the answer replaces this in place, so the
587
+ surface should not move when it lands. */
588
+ .${PENDING_CLASS} {
589
+ align-self: flex-start;
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 4px;
593
+ padding: 12px;
594
+ border-radius: 12px;
595
+ background: var(--_arcy-assistant-bg, #f2f4f7);
596
+ }
597
+
598
+ .${PENDING_CLASS} span {
599
+ width: 6px;
600
+ height: 6px;
601
+ border-radius: 999px;
602
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 45%, transparent);
603
+ animation: arcy-chat-typing 1.2s ease-in-out infinite;
604
+ }
605
+
606
+ .${PENDING_CLASS} span:nth-child(2) {
607
+ animation-delay: 0.15s;
608
+ }
609
+
610
+ .${PENDING_CLASS} span:nth-child(3) {
611
+ animation-delay: 0.3s;
612
+ }
613
+
614
+ @keyframes arcy-chat-typing {
615
+ 0%, 60%, 100% {
616
+ opacity: 0.35;
617
+ transform: translateY(0);
618
+ }
619
+ 30% {
620
+ opacity: 1;
621
+ transform: translateY(-3px);
622
+ }
623
+ }
624
+
625
+ /* Reduced motion keeps the bubble, and with it the answer to "is anything
626
+ happening", and drops only the movement. */
627
+ @media (prefers-reduced-motion: reduce) {
628
+ .${PENDING_CLASS} span {
629
+ animation: none;
630
+ opacity: 0.55;
631
+ }
632
+ }
633
+ `;
634
+ function mountTranscript(container, doc, options = {}) {
635
+ const list = doc.createElement("div");
636
+ list.className = LIST_CLASS;
637
+ container.appendChild(list);
638
+ let destroyed = false;
639
+ const heldPreviews = [];
640
+ function releasePreviews() {
641
+ while (heldPreviews.length > 0) {
642
+ const url = heldPreviews.pop();
643
+ if (!url) continue;
644
+ try {
645
+ URL.revokeObjectURL(url);
646
+ } catch {
647
+ }
648
+ }
649
+ }
650
+ function scrollToEnd() {
651
+ try {
652
+ container.scrollTop = container.scrollHeight;
653
+ } catch {
654
+ }
655
+ }
656
+ let count = 0;
657
+ let pendingNode = null;
658
+ function setPending(pending) {
659
+ if (destroyed) return;
660
+ try {
661
+ if (!pending) {
662
+ pendingNode?.remove();
663
+ pendingNode = null;
664
+ return;
665
+ }
666
+ if (pendingNode) return;
667
+ const node = doc.createElement("div");
668
+ node.className = PENDING_CLASS;
669
+ node.setAttribute("role", "status");
670
+ node.setAttribute("aria-live", "polite");
671
+ node.setAttribute("aria-label", options.answeringLabel?.() ?? "Answering");
672
+ for (let i = 0; i < 3; i += 1) {
673
+ node.appendChild(doc.createElement("span"));
674
+ }
675
+ list.appendChild(node);
676
+ pendingNode = node;
677
+ scrollToEnd();
678
+ } catch {
679
+ }
680
+ }
681
+ function buildRating(meta, messageId, initial) {
682
+ const labels = options.labels;
683
+ if (!labels) return;
684
+ let value = initial;
685
+ const make = (own, label, path) => {
686
+ const button = doc.createElement("button");
687
+ button.type = "button";
688
+ button.className = RATE_CLASS;
689
+ button.setAttribute("aria-label", label);
690
+ button.setAttribute("data-tip", label);
691
+ button.setAttribute("aria-pressed", initial === own ? "true" : "false");
692
+ button.appendChild(thumbIcon(doc, path));
693
+ button.addEventListener("click", () => {
694
+ try {
695
+ value = value === own ? null : own;
696
+ up.setAttribute("aria-pressed", value === "up" ? "true" : "false");
697
+ down.setAttribute("aria-pressed", value === "down" ? "true" : "false");
698
+ options.onFeedback?.(value, messageId);
699
+ } catch {
700
+ }
701
+ });
702
+ return button;
703
+ };
704
+ const up = make("up", labels.likeAnswer, THUMB_UP_PATH);
705
+ const down = make("down", labels.dislikeAnswer, THUMB_DOWN_PATH);
706
+ meta.appendChild(up);
707
+ meta.appendChild(down);
708
+ }
709
+ function buildCitations(group, citedSources) {
710
+ const label = options.searchedSourcesLabel;
711
+ if (!label || citedSources.length === 0) return;
712
+ const row = doc.createElement("div");
713
+ row.className = CITATIONS_CLASS;
714
+ if (options.sourcesLabel) {
715
+ const intro = doc.createElement("span");
716
+ intro.className = CITATION_LABEL_CLASS;
717
+ intro.textContent = options.sourcesLabel;
718
+ row.appendChild(intro);
719
+ }
720
+ let unlinkedCount = 0;
721
+ for (const source of citedSources) {
722
+ if (source.sourceUrl) {
723
+ const a = doc.createElement("a");
724
+ a.className = CITATION_LINK_CLASS;
725
+ a.setAttribute("href", source.sourceUrl);
726
+ a.setAttribute("target", "_blank");
727
+ a.setAttribute("rel", "noopener noreferrer");
728
+ a.textContent = source.title;
729
+ row.appendChild(a);
730
+ } else {
731
+ unlinkedCount += 1;
732
+ }
733
+ }
734
+ if (unlinkedCount > 0) {
735
+ const span = doc.createElement("span");
736
+ span.textContent = label.replace("{count}", String(unlinkedCount));
737
+ row.appendChild(span);
738
+ }
739
+ group.appendChild(row);
740
+ }
741
+ function addMessage(role, text, messageOptions = {}) {
742
+ if (destroyed) return;
743
+ try {
744
+ const {
745
+ images = 0,
746
+ imageUrls = [],
747
+ at,
748
+ messageId = null,
749
+ feedback = null,
750
+ citedSources = [],
751
+ flowOffers = []
752
+ } = messageOptions;
753
+ const group = doc.createElement("div");
754
+ group.className = GROUP_CLASS;
755
+ group.setAttribute("data-role", role);
756
+ const el = doc.createElement("div");
757
+ el.className = MSG_CLASS;
758
+ el.setAttribute("data-role", role);
759
+ if (role === "assistant") {
760
+ renderMarkdown(doc, el, text);
761
+ } else {
762
+ el.textContent = text;
763
+ }
764
+ let attachments = null;
765
+ if (imageUrls.length > 0) {
766
+ attachments = doc.createElement("div");
767
+ attachments.className = SENT_IMAGES_CLASS;
768
+ for (const url of imageUrls) {
769
+ const image = doc.createElement("img");
770
+ image.src = url;
771
+ image.alt = "";
772
+ attachments.appendChild(image);
773
+ heldPreviews.push(url);
774
+ }
775
+ } else if (images > 0) {
776
+ attachments = doc.createElement("div");
777
+ attachments.className = ATTACHMENT_CLASS;
778
+ attachments.textContent = images === 1 ? "1 image" : `${images} images`;
779
+ }
780
+ if (attachments) group.appendChild(attachments);
781
+ if (role === "assistant" && flowOffers.length > 0 && options.onStartFlow) {
782
+ const start = options.onStartFlow.bind(options);
783
+ const offer = buildFlowOffer(
784
+ doc,
785
+ flowOffers,
786
+ options.flowOfferHint?.(),
787
+ start
788
+ );
789
+ if (offer) el.appendChild(offer);
790
+ }
791
+ if (text !== "" || !attachments) group.appendChild(el);
792
+ if (role === "assistant" && citedSources.length > 0) {
793
+ buildCitations(group, citedSources);
794
+ }
795
+ if (role !== "system") {
796
+ const meta = doc.createElement("div");
797
+ meta.className = META_CLASS;
798
+ const stamp = doc.createElement("span");
799
+ stamp.className = TIME_CLASS;
800
+ stamp.textContent = formatTime(at ?? (options.now?.() ?? /* @__PURE__ */ new Date()));
801
+ meta.appendChild(stamp);
802
+ if (role === "assistant") buildRating(meta, messageId, feedback);
803
+ group.appendChild(meta);
804
+ }
805
+ list.insertBefore(group, pendingNode);
806
+ count += 1;
807
+ scrollToEnd();
808
+ } catch {
809
+ }
810
+ }
811
+ return {
812
+ addMessage,
813
+ setPending,
814
+ isEmpty: () => count === 0,
815
+ scrollToEnd,
816
+ clear() {
817
+ if (destroyed) return;
818
+ try {
819
+ while (list.firstChild) list.removeChild(list.firstChild);
820
+ releasePreviews();
821
+ count = 0;
822
+ pendingNode = null;
823
+ } catch {
824
+ }
825
+ },
826
+ destroy() {
827
+ if (destroyed) return;
828
+ destroyed = true;
829
+ try {
830
+ releasePreviews();
831
+ list.remove();
832
+ } catch {
833
+ }
834
+ }
835
+ };
836
+ }
837
+
838
+ // src/shell/drawer.ts
839
+ var DRAWER_CLASS = "arcy-drawer";
840
+ var SCRIM_CLASS = "arcy-drawer-scrim";
841
+ var HEADER_CLASS = "arcy-drawer-header";
842
+ var BACK_CLASS = "arcy-drawer-back";
843
+ var CLOSE_CLASS = "arcy-drawer-close";
844
+ var TITLE_CLASS = "arcy-drawer-title";
845
+ var LIST_CLASS2 = "arcy-drawer-list";
846
+ var ITEM_CLASS = "arcy-drawer-item";
847
+ var ITEM_TITLE_CLASS = "arcy-drawer-item-title";
848
+ var ITEM_SUB_CLASS = "arcy-drawer-item-sub";
849
+ var ITEM_TIP_CLASS = "arcy-drawer-item-tip";
850
+ var ITEM_ICON_CLASS = "arcy-drawer-item-icon";
851
+ var ITEM_TEXT_CLASS = "arcy-drawer-item-text";
852
+ var EMPTY_CLASS = "arcy-drawer-empty";
853
+ var ACTION_CLASS = "arcy-drawer-action";
854
+ var FOOTER_CLASS = "arcy-drawer-footer";
855
+ var DRAWER_CSS = `
856
+ /* A sheet over PART of the panel, sliding in from the left (D899). It used
857
+ to be inset:0 and appear instantly, so opening Recent chats replaced the
858
+ whole surface with no sense of where the conversation went or how to get
859
+ back. Covering part of the panel keeps the conversation visible behind it,
860
+ which is what makes the back gesture obvious. */
861
+ .${DRAWER_CLASS} {
862
+ position: absolute;
863
+ top: 0;
864
+ bottom: 0;
865
+ left: 0;
866
+ width: min(78%, 320px);
867
+ display: flex;
868
+ flex-direction: column;
869
+ background: var(--_arcy-main-bg, #ffffff);
870
+ color: var(--_arcy-main-text, #101828);
871
+ border-right: 1px solid
872
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 10%, transparent);
873
+ z-index: 2;
874
+ transform: translateX(-101%);
875
+ visibility: hidden;
876
+ transition: transform 220ms cubic-bezier(0.32, 0.72, 0, 1),
877
+ visibility 0s linear 220ms;
878
+ }
879
+
880
+ /* The same sheet, hinged on the other edge. Only the three properties that
881
+ name a side change: everything about how it looks and animates is shared,
882
+ so the two can never drift. */
883
+ .${DRAWER_CLASS}[data-side="right"] {
884
+ left: auto;
885
+ right: 0;
886
+ border-right: 0;
887
+ border-left: 1px solid
888
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 10%, transparent);
889
+ transform: translateX(101%);
890
+ }
891
+
892
+ .${DRAWER_CLASS}[data-open] {
893
+ transform: translateX(0);
894
+ visibility: visible;
895
+ transition: transform 220ms cubic-bezier(0.32, 0.72, 0, 1),
896
+ visibility 0s;
897
+ }
898
+
899
+ /* The rest of the panel, dimmed while the sheet is over it. Tapping it is
900
+ the same as pressing back, which is what every sheet on a phone does. */
901
+ .${SCRIM_CLASS} {
902
+ position: absolute;
903
+ inset: 0;
904
+ z-index: 1;
905
+ appearance: none;
906
+ border: 0;
907
+ padding: 0;
908
+ cursor: pointer;
909
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 22%, transparent);
910
+ opacity: 0;
911
+ visibility: hidden;
912
+ transition: opacity 220ms ease, visibility 0s linear 220ms;
913
+ }
914
+
915
+ .${SCRIM_CLASS}[data-open] {
916
+ opacity: 1;
917
+ visibility: visible;
918
+ transition: opacity 220ms ease, visibility 0s;
919
+ }
920
+
921
+ @media (prefers-reduced-motion: reduce) {
922
+ .${DRAWER_CLASS},
923
+ .${SCRIM_CLASS} {
924
+ transition: none;
925
+ }
926
+ }
927
+
928
+ .${HEADER_CLASS} {
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 8px;
932
+ padding: 10px 10px 10px 12px;
933
+ flex: 0 0 auto;
934
+ }
935
+
936
+ .${BACK_CLASS},
937
+ .${CLOSE_CLASS} {
938
+ appearance: none;
939
+ border: 0;
940
+ background: transparent;
941
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 70%, transparent);
942
+ cursor: pointer;
943
+ padding: 5px;
944
+ border-radius: 999px;
945
+ display: flex;
946
+ align-items: center;
947
+ flex: 0 0 auto;
948
+ -webkit-tap-highlight-color: transparent;
949
+ }
950
+
951
+ .${BACK_CLASS}:hover,
952
+ .${CLOSE_CLASS}:hover {
953
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
954
+ }
955
+
956
+ .${TITLE_CLASS} {
957
+ font-weight: 600;
958
+ font-size: 14px;
959
+ min-width: 0;
960
+ flex: 1 1 auto;
961
+ overflow: hidden;
962
+ text-overflow: ellipsis;
963
+ white-space: nowrap;
964
+ }
965
+
966
+ .${LIST_CLASS2} {
967
+ flex: 1 1 auto;
968
+ min-height: 0;
969
+ overflow: auto;
970
+ display: flex;
971
+ flex-direction: column;
972
+ gap: 4px;
973
+ padding: 0 8px 8px;
974
+ }
975
+
976
+ /* A row, laid out left to right: the optional mark, then the text column.
977
+ A drawer with no mark renders one child and reads exactly as it did when
978
+ the row was the text column itself. */
979
+ .${ITEM_CLASS} {
980
+ appearance: none;
981
+ border: 0;
982
+ cursor: pointer;
983
+ text-align: left;
984
+ font: inherit;
985
+ display: flex;
986
+ flex-direction: row;
987
+ align-items: center;
988
+ gap: 10px;
989
+ padding: 9px 10px;
990
+ border-radius: 10px;
991
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 4%, transparent);
992
+ color: inherit;
993
+ -webkit-tap-highlight-color: transparent;
994
+ }
995
+
996
+ .${ITEM_CLASS}:hover {
997
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 9%, transparent);
998
+ }
999
+
1000
+ /* The conversation the visitor is reading. Darker than hover so it still
1001
+ reads as selected while the pointer is over another row, and carrying an
1002
+ inset marker so the state is not colour alone. */
1003
+ .${ITEM_CLASS}[data-active] {
1004
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 12%, transparent);
1005
+ box-shadow: inset 2px 0 0 var(--_arcy-brand-bg, #101828);
1006
+ font-weight: 600;
1007
+ }
1008
+
1009
+ /* Dimmer than the title it sits beside: it labels the kind of row, and a
1010
+ mark at full strength competes with the name a visitor is reading. */
1011
+ .${ITEM_ICON_CLASS} {
1012
+ flex: 0 0 auto;
1013
+ display: flex;
1014
+ align-items: center;
1015
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 60%, transparent);
1016
+ }
1017
+
1018
+ /* The min-width: 0 is what keeps the ellipsis working: without it a flex
1019
+ item refuses to shrink below its content, and a long flow name pushes the
1020
+ row wider than the sheet instead of truncating. */
1021
+ .${ITEM_TEXT_CLASS} {
1022
+ display: flex;
1023
+ flex-direction: column;
1024
+ gap: 2px;
1025
+ min-width: 0;
1026
+ flex: 1 1 auto;
1027
+ }
1028
+
1029
+ .${ITEM_TITLE_CLASS} {
1030
+ font-size: 13px;
1031
+ overflow: hidden;
1032
+ text-overflow: ellipsis;
1033
+ white-space: nowrap;
1034
+ }
1035
+
1036
+ .${ITEM_SUB_CLASS} {
1037
+ font-size: 11.5px;
1038
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 55%, transparent);
1039
+ overflow: hidden;
1040
+ text-overflow: ellipsis;
1041
+ white-space: nowrap;
1042
+ }
1043
+
1044
+ /* The whole subtitle, for a row whose subtitle is a flow's summary and does
1045
+ not fit on one line (D1265). A single bubble reused by every row, and a
1046
+ child of the SHEET rather than of the row it describes: the list scrolls
1047
+ (overflow: auto above), and a bubble painted from inside a scrolling box
1048
+ is cut off at that box's edge instead of hanging past it.
1049
+
1050
+ Position is set in script, because it is the one thing CSS cannot do from
1051
+ here: the bubble sits beside the sheet, so it has to know how tall the row
1052
+ is and how much room the panel has left. Everything else is here. */
1053
+ .${ITEM_TIP_CLASS} {
1054
+ position: absolute;
1055
+ z-index: 3;
1056
+ /* Sized from its own text, not from the room left inside the sheet. The
1057
+ bubble is placed BESIDE the sheet, but the sheet is what it is
1058
+ positioned against, so the space between its left edge and the sheet's
1059
+ right edge is zero. Left to shrink to fit, the box takes that zero as
1060
+ its available width and falls back to the narrowest it can legally be,
1061
+ which with the overflow-wrap: anywhere below is one character: the
1062
+ summary comes out as a column of single letters. max-content sizes it
1063
+ to the sentence instead, and the max-width under it does the wrapping. */
1064
+ width: max-content;
1065
+ box-sizing: border-box;
1066
+ padding: 6px 9px;
1067
+ border-radius: 6px;
1068
+ background: var(--_arcy-main-text, #101828);
1069
+ color: var(--_arcy-main-bg, #ffffff);
1070
+ font-size: 11px;
1071
+ line-height: 1.35;
1072
+ /* Wraps, unlike the header tooltips: a summary is a sentence, and a
1073
+ sentence on one line is the truncation this bubble exists to undo. */
1074
+ white-space: normal;
1075
+ overflow-wrap: anywhere;
1076
+ pointer-events: none;
1077
+ opacity: 0;
1078
+ visibility: hidden;
1079
+ transition: opacity 120ms ease;
1080
+ }
1081
+
1082
+ .${ITEM_TIP_CLASS}[data-open] {
1083
+ opacity: 1;
1084
+ visibility: visible;
1085
+ }
1086
+
1087
+ /* Same reason as the header tooltips: a coarse pointer has no hover, so the
1088
+ bubble would latch open over the row the visitor just pressed. */
1089
+ @media (hover: none) {
1090
+ .${ITEM_TIP_CLASS} {
1091
+ display: none;
1092
+ }
1093
+ }
1094
+
1095
+ @media (prefers-reduced-motion: reduce) {
1096
+ .${ITEM_TIP_CLASS} {
1097
+ transition: none;
1098
+ }
1099
+ }
1100
+
1101
+ .${EMPTY_CLASS} {
1102
+ flex: 1 1 auto;
1103
+ display: flex;
1104
+ flex-direction: column;
1105
+ align-items: center;
1106
+ justify-content: center;
1107
+ gap: 10px;
1108
+ padding: 16px;
1109
+ text-align: center;
1110
+ font-size: 13px;
1111
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 60%, transparent);
1112
+ }
1113
+
1114
+ .${ACTION_CLASS} {
1115
+ appearance: none;
1116
+ border: 0;
1117
+ cursor: pointer;
1118
+ font: inherit;
1119
+ font-size: 13px;
1120
+ display: flex;
1121
+ align-items: center;
1122
+ justify-content: center;
1123
+ gap: 6px;
1124
+ padding: 9px 14px;
1125
+ border-radius: 999px;
1126
+ background: var(--_arcy-brand-bg, #101828);
1127
+ color: var(--_arcy-brand-text, #ffffff);
1128
+ -webkit-tap-highlight-color: transparent;
1129
+ }
1130
+
1131
+ /* The pinned "New chat" action, once the list is populated (D979): appended
1132
+ as a footer sibling of the scrollable list rather than its last row, so it
1133
+ never scrolls out of reach on a drawer with many conversations. */
1134
+ .${FOOTER_CLASS} {
1135
+ flex: 0 0 auto;
1136
+ padding: 10px;
1137
+ }
1138
+
1139
+ .${FOOTER_CLASS} .${ACTION_CLASS} {
1140
+ width: 100%;
1141
+ }
1142
+ `;
1143
+ var SVG_NS3 = "http://www.w3.org/2000/svg";
1144
+ var TIP_GAP = 8;
1145
+ var TIP_MAX_WIDTH = 240;
1146
+ var TIP_MIN_WIDTH = 120;
1147
+ function drawIcon(doc, paths, size, strokeWidth = 2) {
1148
+ const svg = doc.createElementNS(SVG_NS3, "svg");
1149
+ svg.setAttribute("viewBox", "0 0 24 24");
1150
+ svg.setAttribute("width", String(size));
1151
+ svg.setAttribute("height", String(size));
1152
+ svg.setAttribute("aria-hidden", "true");
1153
+ for (const d of paths) {
1154
+ const path = doc.createElementNS(SVG_NS3, "path");
1155
+ path.setAttribute("d", d);
1156
+ path.setAttribute("fill", "none");
1157
+ path.setAttribute("stroke", "currentColor");
1158
+ path.setAttribute("stroke-width", String(strokeWidth));
1159
+ path.setAttribute("stroke-linecap", "round");
1160
+ path.setAttribute("stroke-linejoin", "round");
1161
+ svg.appendChild(path);
1162
+ }
1163
+ return svg;
1164
+ }
1165
+ function backIcon(doc, side) {
1166
+ return drawIcon(doc, [side === "right" ? "M9 5l7 7-7 7" : "M15 5l-7 7 7 7"], 16);
1167
+ }
1168
+ function closeIcon(doc) {
1169
+ return drawIcon(doc, ["M18 6L6 18", "M6 6l12 12"], 16);
1170
+ }
1171
+ function plusIcon(doc) {
1172
+ return drawIcon(doc, ["M12 5v14M5 12h14"], 14, 2.5);
1173
+ }
1174
+ function mountDrawer(container, doc, options) {
1175
+ let emptyText = options.emptyText ?? "";
1176
+ let actionLabel = options.actionLabel ?? null;
1177
+ let items = [];
1178
+ let destroyed = false;
1179
+ const side = options.side ?? "left";
1180
+ const subtitleTips = options.subtitleTips !== false;
1181
+ const drawer = doc.createElement("div");
1182
+ drawer.className = DRAWER_CLASS;
1183
+ drawer.setAttribute("role", "group");
1184
+ drawer.setAttribute("data-side", side);
1185
+ const header = doc.createElement("div");
1186
+ header.className = HEADER_CLASS;
1187
+ const back = doc.createElement("button");
1188
+ back.type = "button";
1189
+ back.className = BACK_CLASS;
1190
+ back.setAttribute("aria-label", options.backLabel);
1191
+ back.setAttribute("data-tip", options.backLabel);
1192
+ back.setAttribute("data-tip-placement", "below");
1193
+ back.setAttribute("data-tip-align", "start");
1194
+ back.appendChild(backIcon(doc, side));
1195
+ const title = doc.createElement("span");
1196
+ title.className = TITLE_CLASS;
1197
+ title.textContent = options.title;
1198
+ const close = doc.createElement("button");
1199
+ close.type = "button";
1200
+ close.className = CLOSE_CLASS;
1201
+ close.setAttribute("aria-label", options.closeLabel);
1202
+ close.setAttribute("data-tip", options.closeLabel);
1203
+ close.setAttribute("data-tip-placement", "below");
1204
+ close.setAttribute("data-tip-align", "end");
1205
+ close.appendChild(closeIcon(doc));
1206
+ header.appendChild(back);
1207
+ header.appendChild(title);
1208
+ header.appendChild(close);
1209
+ const list = doc.createElement("div");
1210
+ list.className = LIST_CLASS2;
1211
+ drawer.appendChild(header);
1212
+ drawer.appendChild(list);
1213
+ const scrim = doc.createElement("button");
1214
+ scrim.type = "button";
1215
+ scrim.className = SCRIM_CLASS;
1216
+ scrim.setAttribute("aria-hidden", "true");
1217
+ scrim.tabIndex = -1;
1218
+ container.appendChild(scrim);
1219
+ container.appendChild(drawer);
1220
+ const tip = doc.createElement("div");
1221
+ tip.className = ITEM_TIP_CLASS;
1222
+ tip.setAttribute("aria-hidden", "true");
1223
+ drawer.appendChild(tip);
1224
+ function hideTip() {
1225
+ tip.removeAttribute("data-open");
1226
+ }
1227
+ function showTip(row, text) {
1228
+ try {
1229
+ const drawerRect = drawer.getBoundingClientRect();
1230
+ const containerRect = container.getBoundingClientRect();
1231
+ const rowRect = row.getBoundingClientRect();
1232
+ if (drawerRect.width === 0 || containerRect.width === 0) return;
1233
+ const room = (side === "right" ? drawerRect.left - containerRect.left : containerRect.right - drawerRect.right) - TIP_GAP * 2;
1234
+ if (room < TIP_MIN_WIDTH) {
1235
+ hideTip();
1236
+ return;
1237
+ }
1238
+ tip.textContent = text;
1239
+ tip.style.maxWidth = `${Math.min(TIP_MAX_WIDTH, room)}px`;
1240
+ const offset = `${drawerRect.width + TIP_GAP}px`;
1241
+ tip.style.left = side === "right" ? "auto" : offset;
1242
+ tip.style.right = side === "right" ? offset : "auto";
1243
+ tip.style.top = "0px";
1244
+ const height = tip.offsetHeight;
1245
+ const top = rowRect.top - drawerRect.top;
1246
+ const lowest = Math.max(TIP_GAP, drawerRect.height - height - TIP_GAP);
1247
+ tip.style.top = `${Math.min(Math.max(top, TIP_GAP), lowest)}px`;
1248
+ tip.setAttribute("data-open", "");
1249
+ } catch {
1250
+ hideTip();
1251
+ }
1252
+ }
1253
+ list.addEventListener("scroll", hideTip);
1254
+ const handleBack = () => {
1255
+ try {
1256
+ options.onBack();
1257
+ } catch {
1258
+ }
1259
+ };
1260
+ back.addEventListener("click", handleBack);
1261
+ close.addEventListener("click", handleBack);
1262
+ scrim.addEventListener("click", handleBack);
1263
+ let footer = null;
1264
+ function removeFooter() {
1265
+ footer?.remove();
1266
+ footer = null;
1267
+ }
1268
+ function render() {
1269
+ if (destroyed) return;
1270
+ hideTip();
1271
+ while (list.firstChild) list.removeChild(list.firstChild);
1272
+ removeFooter();
1273
+ if (items.length === 0) {
1274
+ const empty = doc.createElement("div");
1275
+ empty.className = EMPTY_CLASS;
1276
+ const text = doc.createElement("span");
1277
+ text.textContent = emptyText;
1278
+ empty.appendChild(text);
1279
+ if (actionLabel) empty.appendChild(actionButton(false));
1280
+ list.appendChild(empty);
1281
+ return;
1282
+ }
1283
+ for (const item of items) {
1284
+ const row = doc.createElement("button");
1285
+ row.type = "button";
1286
+ row.className = ITEM_CLASS;
1287
+ if (item.active) {
1288
+ row.setAttribute("data-active", "");
1289
+ row.setAttribute("aria-current", "true");
1290
+ }
1291
+ const drawMark = item.icon ?? options.itemIcon;
1292
+ if (drawMark) {
1293
+ try {
1294
+ const mark = drawMark(doc);
1295
+ if (mark) {
1296
+ const slot = doc.createElement("span");
1297
+ slot.className = ITEM_ICON_CLASS;
1298
+ slot.appendChild(mark);
1299
+ row.appendChild(slot);
1300
+ }
1301
+ } catch {
1302
+ }
1303
+ }
1304
+ const text = doc.createElement("span");
1305
+ text.className = ITEM_TEXT_CLASS;
1306
+ const rowTitle = doc.createElement("span");
1307
+ rowTitle.className = ITEM_TITLE_CLASS;
1308
+ rowTitle.textContent = item.title;
1309
+ text.appendChild(rowTitle);
1310
+ if (item.subtitle) {
1311
+ const sub = doc.createElement("span");
1312
+ sub.className = ITEM_SUB_CLASS;
1313
+ sub.textContent = item.subtitle;
1314
+ text.appendChild(sub);
1315
+ const summary = item.subtitle;
1316
+ if (subtitleTips) {
1317
+ row.addEventListener("mouseenter", () => showTip(row, summary));
1318
+ row.addEventListener("focus", () => showTip(row, summary));
1319
+ row.addEventListener("mouseleave", hideTip);
1320
+ row.addEventListener("blur", hideTip);
1321
+ }
1322
+ }
1323
+ row.appendChild(text);
1324
+ row.addEventListener("click", () => {
1325
+ try {
1326
+ options.onSelect(item.id);
1327
+ } catch {
1328
+ }
1329
+ });
1330
+ list.appendChild(row);
1331
+ }
1332
+ if (actionLabel) {
1333
+ footer = doc.createElement("div");
1334
+ footer.className = FOOTER_CLASS;
1335
+ footer.appendChild(actionButton(true));
1336
+ drawer.appendChild(footer);
1337
+ }
1338
+ }
1339
+ function actionButton(withIcon) {
1340
+ const action = doc.createElement("button");
1341
+ action.type = "button";
1342
+ action.className = ACTION_CLASS;
1343
+ if (withIcon) action.appendChild(plusIcon(doc));
1344
+ action.appendChild(doc.createTextNode(actionLabel ?? ""));
1345
+ action.addEventListener("click", () => {
1346
+ try {
1347
+ options.onAction?.();
1348
+ } catch {
1349
+ }
1350
+ });
1351
+ return action;
1352
+ }
1353
+ render();
1354
+ function setBehindInert(on) {
1355
+ try {
1356
+ for (const node of Array.from(container.children)) {
1357
+ if (node === drawer || node === scrim) continue;
1358
+ if (on) node.setAttribute("inert", "");
1359
+ else node.removeAttribute("inert");
1360
+ }
1361
+ } catch {
1362
+ }
1363
+ }
1364
+ return {
1365
+ open() {
1366
+ if (destroyed) return;
1367
+ drawer.setAttribute("data-open", "");
1368
+ scrim.setAttribute("data-open", "");
1369
+ setBehindInert(true);
1370
+ try {
1371
+ back.focus();
1372
+ } catch {
1373
+ }
1374
+ },
1375
+ close() {
1376
+ if (destroyed) return;
1377
+ hideTip();
1378
+ drawer.removeAttribute("data-open");
1379
+ scrim.removeAttribute("data-open");
1380
+ setBehindInert(false);
1381
+ },
1382
+ isOpen: () => drawer.hasAttribute("data-open"),
1383
+ setItems(next) {
1384
+ items = next;
1385
+ render();
1386
+ },
1387
+ setTitle(next) {
1388
+ title.textContent = next;
1389
+ },
1390
+ setTexts(texts) {
1391
+ if (texts.emptyText !== void 0) emptyText = texts.emptyText;
1392
+ if (texts.actionLabel !== void 0) actionLabel = texts.actionLabel;
1393
+ if (texts.backLabel !== void 0) {
1394
+ back.setAttribute("aria-label", texts.backLabel);
1395
+ back.setAttribute("data-tip", texts.backLabel);
1396
+ }
1397
+ if (texts.closeLabel !== void 0) {
1398
+ close.setAttribute("aria-label", texts.closeLabel);
1399
+ close.setAttribute("data-tip", texts.closeLabel);
1400
+ }
1401
+ render();
1402
+ },
1403
+ destroy() {
1404
+ if (destroyed) return;
1405
+ destroyed = true;
1406
+ try {
1407
+ setBehindInert(false);
1408
+ list.removeEventListener("scroll", hideTip);
1409
+ back.removeEventListener("click", handleBack);
1410
+ close.removeEventListener("click", handleBack);
1411
+ scrim.removeEventListener("click", handleBack);
1412
+ tip.remove();
1413
+ drawer.remove();
1414
+ scrim.remove();
1415
+ } catch {
1416
+ }
1417
+ }
1418
+ };
1419
+ }
1420
+
1421
+ // src/shell/fill-card.ts
1422
+ var CARD_CLASS = "arcy-fill-card";
1423
+ var FILL_CARD_CSS = `
1424
+ .${CARD_CLASS} {
1425
+ position: absolute;
1426
+ display: flex;
1427
+ flex-direction: column;
1428
+ gap: 10px;
1429
+ box-sizing: border-box;
1430
+ padding: 14px 16px;
1431
+ border-radius: var(--arcy-chat-radius, 16px);
1432
+ background: var(--_arcy-main-bg, #ffffff);
1433
+ color: var(--_arcy-main-text, #101828);
1434
+ border: var(--_arcy-border-width, 0px) solid
1435
+ var(--_arcy-border-color, color-mix(in srgb, var(--_arcy-main-text, #101828) 15%, transparent));
1436
+ box-shadow: var(--arcy-chat-shadow, 0 12px 32px rgba(16, 24, 40, 0.24));
1437
+ font-family: inherit;
1438
+ visibility: hidden;
1439
+ opacity: 0;
1440
+ transform-origin: bottom center;
1441
+ transition: opacity 200ms ease, visibility 200ms ease;
1442
+ }
1443
+
1444
+ .${CARD_CLASS}[data-open] {
1445
+ visibility: visible;
1446
+ opacity: 1;
1447
+ }
1448
+
1449
+ .${CARD_CLASS}-question {
1450
+ font-size: 14px;
1451
+ line-height: 1.45;
1452
+ overflow-wrap: anywhere;
1453
+ }
1454
+
1455
+ .${CARD_CLASS}-error {
1456
+ font-size: 12.5px;
1457
+ line-height: 1.4;
1458
+ color: var(--_arcy-error-text, #b42318);
1459
+ }
1460
+
1461
+ .${CARD_CLASS}-controls {
1462
+ display: flex;
1463
+ flex-wrap: wrap;
1464
+ align-items: center;
1465
+ gap: 8px;
1466
+ }
1467
+
1468
+ .${CARD_CLASS}-controls:empty {
1469
+ display: none;
1470
+ }
1471
+
1472
+ .${CARD_CLASS}-pill {
1473
+ appearance: none;
1474
+ border: 1px solid
1475
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 18%, transparent);
1476
+ background: transparent;
1477
+ color: inherit;
1478
+ font: inherit;
1479
+ font-size: 13px;
1480
+ padding: 7px 14px;
1481
+ border-radius: 999px;
1482
+ cursor: pointer;
1483
+ }
1484
+
1485
+ .${CARD_CLASS}-pill:hover {
1486
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1487
+ }
1488
+
1489
+ .${CARD_CLASS}-input {
1490
+ flex: 1 1 auto;
1491
+ min-width: 0;
1492
+ box-sizing: border-box;
1493
+ border: 1px solid
1494
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 18%, transparent);
1495
+ border-radius: 10px;
1496
+ background: transparent;
1497
+ color: inherit;
1498
+ font: inherit;
1499
+ font-size: 13px;
1500
+ padding: 8px 10px;
1501
+ outline: none;
1502
+ }
1503
+
1504
+ .${CARD_CLASS}-input:focus-visible {
1505
+ border-color: var(--_arcy-brand-bg, #101828);
1506
+ }
1507
+
1508
+ .${CARD_CLASS}-range {
1509
+ flex: 1 1 auto;
1510
+ min-width: 0;
1511
+ accent-color: var(--_arcy-brand-bg, #101828);
1512
+ }
1513
+
1514
+ .${CARD_CLASS}-range-value {
1515
+ flex: 0 0 auto;
1516
+ font-size: 13px;
1517
+ font-variant-numeric: tabular-nums;
1518
+ min-width: 2.5em;
1519
+ text-align: right;
1520
+ }
1521
+
1522
+ .${CARD_CLASS}-confirm {
1523
+ flex: 0 0 auto;
1524
+ appearance: none;
1525
+ border: 0;
1526
+ border-radius: 999px;
1527
+ background: var(--_arcy-brand-bg, #101828);
1528
+ color: var(--_arcy-brand-text, #ffffff);
1529
+ font: inherit;
1530
+ font-size: 13px;
1531
+ padding: 8px 16px;
1532
+ cursor: pointer;
1533
+ }
1534
+
1535
+ @media (prefers-reduced-motion: reduce) {
1536
+ .${CARD_CLASS} {
1537
+ transition: none;
1538
+ }
1539
+ }
1540
+ `;
1541
+ var ERROR_STRINGS = {
1542
+ required: "fillRequired",
1543
+ invalid_number: "fillInvalidNumber",
1544
+ number_too_small: "fillNumberTooSmall",
1545
+ number_too_large: "fillNumberTooLarge",
1546
+ invalid_email: "fillInvalidEmail",
1547
+ invalid_url: "fillInvalidUrl",
1548
+ invalid_date: "fillInvalidDate",
1549
+ invalid_time: "fillInvalidTime",
1550
+ date_too_early: "fillDateTooEarly",
1551
+ date_too_late: "fillDateTooLate",
1552
+ too_long: "fillTooLong",
1553
+ invalid_option: "fillInvalidOption",
1554
+ invalid: "fillInvalid"
1555
+ };
1556
+ var NATIVE_INPUT_TYPES = {
1557
+ date: "date",
1558
+ time: "time",
1559
+ datetime: "datetime-local",
1560
+ month: "month",
1561
+ week: "week"
1562
+ };
1563
+ function mountFillCard(context, getStrings) {
1564
+ const doc = context.host.ownerDocument;
1565
+ const card = doc.createElement("div");
1566
+ card.className = CARD_CLASS;
1567
+ card.setAttribute("role", "dialog");
1568
+ const question = doc.createElement("div");
1569
+ question.className = `${CARD_CLASS}-question`;
1570
+ const error = doc.createElement("div");
1571
+ error.className = `${CARD_CLASS}-error`;
1572
+ error.setAttribute("role", "alert");
1573
+ error.style.setProperty("display", "none");
1574
+ const controls = doc.createElement("div");
1575
+ controls.className = `${CARD_CLASS}-controls`;
1576
+ card.appendChild(question);
1577
+ card.appendChild(error);
1578
+ card.appendChild(controls);
1579
+ context.root.appendChild(card);
1580
+ let request = null;
1581
+ function errorText(code) {
1582
+ const strings = getStrings();
1583
+ const template = strings[ERROR_STRINGS[code]];
1584
+ const text = typeof template === "string" ? template : "";
1585
+ return text.replace("{min}", request?.min ?? "").replace("{max}", request?.max ?? String(request?.maxLength ?? ""));
1586
+ }
1587
+ function place2() {
1588
+ let metrics;
1589
+ try {
1590
+ metrics = context.bar.getMetrics();
1591
+ } catch {
1592
+ return;
1593
+ }
1594
+ const bottom = metrics.bottomOffset + metrics.keyboardOffset + metrics.height + PANEL_GAP;
1595
+ const style = card.style;
1596
+ style.setProperty("bottom", `${bottom}px`);
1597
+ style.setProperty("left", "50%");
1598
+ style.setProperty("translate", "-50%");
1599
+ if (metrics.isMobile) {
1600
+ style.setProperty("width", `calc(100% - ${MOBILE_MARGIN * 2}px)`);
1601
+ style.setProperty("max-width", "none");
1602
+ style.setProperty("transform", "none");
1603
+ return;
1604
+ }
1605
+ const width = surfaceWidth(metrics.widthPercent, {
1606
+ width: metrics.viewportWidth,
1607
+ height: metrics.viewportHeight
1608
+ });
1609
+ style.setProperty("width", `${width}px`);
1610
+ style.setProperty("max-width", `calc(100vw - ${PANEL_MARGIN * 2}px)`);
1611
+ style.setProperty(
1612
+ "transform",
1613
+ dragTransform({
1614
+ x: clampOffsetX(metrics.offset.x, width, metrics.viewportWidth),
1615
+ y: metrics.offset.y
1616
+ })
1617
+ );
1618
+ }
1619
+ function renderControls(active, hooks) {
1620
+ controls.textContent = "";
1621
+ const strings = getStrings();
1622
+ const pill = (label, value) => {
1623
+ const button = doc.createElement("button");
1624
+ button.type = "button";
1625
+ button.className = `${CARD_CLASS}-pill`;
1626
+ button.textContent = label;
1627
+ button.addEventListener("click", () => hooks.onAnswer(value));
1628
+ controls.appendChild(button);
1629
+ };
1630
+ const confirmRow = (input, read) => {
1631
+ const confirm = doc.createElement("button");
1632
+ confirm.type = "button";
1633
+ confirm.className = `${CARD_CLASS}-confirm`;
1634
+ confirm.textContent = strings.fillConfirm;
1635
+ confirm.addEventListener("click", () => hooks.onAnswer(read()));
1636
+ input.addEventListener("keydown", (event) => {
1637
+ if (event.key === "Enter") {
1638
+ event.preventDefault();
1639
+ hooks.onAnswer(read());
1640
+ }
1641
+ });
1642
+ controls.appendChild(input);
1643
+ controls.appendChild(confirm);
1644
+ };
1645
+ switch (active.kind) {
1646
+ case "select":
1647
+ case "radio": {
1648
+ for (const option of active.options ?? []) {
1649
+ pill(option.label, option.value);
1650
+ }
1651
+ return;
1652
+ }
1653
+ case "checkbox": {
1654
+ pill(strings.fillYes, "true");
1655
+ pill(strings.fillNo, "false");
1656
+ return;
1657
+ }
1658
+ case "range": {
1659
+ const input = doc.createElement("input");
1660
+ input.type = "range";
1661
+ input.className = `${CARD_CLASS}-range`;
1662
+ if (active.min !== null) input.min = active.min;
1663
+ if (active.max !== null) input.max = active.max;
1664
+ if (active.step !== null) input.step = active.step;
1665
+ const value = doc.createElement("span");
1666
+ value.className = `${CARD_CLASS}-range-value`;
1667
+ value.textContent = input.value;
1668
+ input.addEventListener("input", () => {
1669
+ value.textContent = input.value;
1670
+ });
1671
+ const confirm = doc.createElement("button");
1672
+ confirm.type = "button";
1673
+ confirm.className = `${CARD_CLASS}-confirm`;
1674
+ confirm.textContent = strings.fillConfirm;
1675
+ confirm.addEventListener("click", () => hooks.onAnswer(input.value));
1676
+ controls.appendChild(input);
1677
+ controls.appendChild(value);
1678
+ controls.appendChild(confirm);
1679
+ return;
1680
+ }
1681
+ case "password": {
1682
+ const input = doc.createElement("input");
1683
+ input.type = "password";
1684
+ input.className = `${CARD_CLASS}-input`;
1685
+ input.autocomplete = "off";
1686
+ confirmRow(input, () => input.value);
1687
+ try {
1688
+ input.focus();
1689
+ } catch {
1690
+ }
1691
+ return;
1692
+ }
1693
+ default: {
1694
+ const nativeType = NATIVE_INPUT_TYPES[active.kind];
1695
+ if (!nativeType) return;
1696
+ const input = doc.createElement("input");
1697
+ input.type = nativeType;
1698
+ input.className = `${CARD_CLASS}-input`;
1699
+ if (active.min !== null) input.min = active.min;
1700
+ if (active.max !== null) input.max = active.max;
1701
+ if (active.step !== null) input.step = active.step;
1702
+ confirmRow(input, () => input.value);
1703
+ }
1704
+ }
1705
+ }
1706
+ return {
1707
+ show(next, hooks) {
1708
+ request = next;
1709
+ question.textContent = next.question;
1710
+ renderControls(next, hooks);
1711
+ this.setError(next.errorCode);
1712
+ place2();
1713
+ card.setAttribute("data-open", "");
1714
+ },
1715
+ setError(code) {
1716
+ if (code === null) {
1717
+ error.textContent = "";
1718
+ error.style.setProperty("display", "none");
1719
+ } else {
1720
+ error.textContent = errorText(code);
1721
+ error.style.setProperty("display", "block");
1722
+ }
1723
+ if (card.hasAttribute("data-open")) place2();
1724
+ },
1725
+ hide() {
1726
+ request = null;
1727
+ card.removeAttribute("data-open");
1728
+ controls.textContent = "";
1729
+ error.textContent = "";
1730
+ error.style.setProperty("display", "none");
1731
+ },
1732
+ reposition() {
1733
+ if (card.hasAttribute("data-open")) place2();
1734
+ },
1735
+ isVisible: () => card.hasAttribute("data-open"),
1736
+ destroy() {
1737
+ try {
1738
+ card.remove();
1739
+ } catch {
1740
+ }
1741
+ }
1742
+ };
1743
+ }
1744
+
1745
+ // src/shell/renderable-flows.ts
1746
+ function renderableFlows(flows) {
1747
+ const renderable = [];
1748
+ for (const flow of flows) {
1749
+ const publicName = flow.publicName?.trim() ?? "";
1750
+ if (!publicName) continue;
1751
+ renderable.push({ ...flow, publicName });
1752
+ }
1753
+ return renderable;
1754
+ }
1755
+
1756
+ // src/shell/chat-api.ts
1757
+ var DEFAULT_API_BASE = "https://api.arcyai.com";
1758
+ var AGENT_QUERY_PATH = "/api/v1/sdk/agent/query";
1759
+ var AGENT_QUERY_TIMEOUT_MS = 2e4;
1760
+ function resolveFetch(fetchImpl) {
1761
+ if (fetchImpl !== void 0) return fetchImpl;
1762
+ return typeof fetch !== "undefined" ? fetch.bind(globalThis) : null;
1763
+ }
1764
+ function bounded(promise, timeoutMs, setTimeoutImpl, onTimeout) {
1765
+ return new Promise((resolve) => {
1766
+ let settled = false;
1767
+ const settle = (value) => {
1768
+ if (settled) return;
1769
+ settled = true;
1770
+ resolve(value);
1771
+ };
1772
+ promise.then(settle, () => settle(onTimeout));
1773
+ try {
1774
+ setTimeoutImpl(() => settle(onTimeout), timeoutMs);
1775
+ } catch {
1776
+ }
1777
+ });
1778
+ }
1779
+ var CAP_DENIAL_REASONS = /* @__PURE__ */ new Set([
1780
+ "credits_exhausted",
1781
+ "lapsed",
1782
+ "org_cap_exceeded",
1783
+ "user_cap_exceeded",
1784
+ "anon_org_cap_exceeded",
1785
+ "anon_user_cap_exceeded"
1786
+ ]);
1787
+ function isCapDenialReason(value) {
1788
+ return typeof value === "string" && CAP_DENIAL_REASONS.has(value);
1789
+ }
1790
+ function readChatResponse(body) {
1791
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
1792
+ const b = body;
1793
+ if (b.responseType !== "chat") return null;
1794
+ const chatResponse = b.chatResponse;
1795
+ if (!chatResponse || typeof chatResponse !== "object" || Array.isArray(chatResponse)) {
1796
+ return null;
1797
+ }
1798
+ const c = chatResponse;
1799
+ if (typeof c.message !== "string") return null;
1800
+ const flowsOffered = [];
1801
+ if (Array.isArray(c.flowsOffered)) {
1802
+ for (const entry of c.flowsOffered) {
1803
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1804
+ const e = entry;
1805
+ if (typeof e.flowCvid !== "string") continue;
1806
+ if (e.publicName !== null && typeof e.publicName !== "string") continue;
1807
+ flowsOffered.push({ flowCvid: e.flowCvid, publicName: e.publicName ?? null });
1808
+ }
1809
+ }
1810
+ const citedSources = [];
1811
+ if (Array.isArray(c.citedSources)) {
1812
+ for (const entry of c.citedSources) {
1813
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) continue;
1814
+ const e = entry;
1815
+ if (typeof e.documentId !== "string" || typeof e.title !== "string") continue;
1816
+ if (e.sourceUrl !== null && typeof e.sourceUrl !== "string") continue;
1817
+ citedSources.push({
1818
+ documentId: e.documentId,
1819
+ title: e.title,
1820
+ sourceUrl: e.sourceUrl ?? null
1821
+ });
1822
+ }
1823
+ }
1824
+ return {
1825
+ ok: true,
1826
+ message: c.message,
1827
+ messageId: typeof c.messageId === "string" && c.messageId.length > 0 ? c.messageId : null,
1828
+ isFallback: c.isFallback === true,
1829
+ flowsOffered,
1830
+ citedSources
1831
+ };
1832
+ }
1833
+ function headers(sessionToken) {
1834
+ return {
1835
+ "Content-Type": "application/json",
1836
+ Authorization: `Bearer ${sessionToken}`
1837
+ };
1838
+ }
1839
+ async function postAgentQuery(options) {
1840
+ const {
1841
+ apiBase = DEFAULT_API_BASE,
1842
+ publicKey,
1843
+ sessionId,
1844
+ sessionToken,
1845
+ query,
1846
+ route,
1847
+ conversationId,
1848
+ attachments = [],
1849
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
1850
+ timeoutMs = AGENT_QUERY_TIMEOUT_MS
1851
+ } = options;
1852
+ const fetchImpl = resolveFetch(options.fetchImpl);
1853
+ if (!fetchImpl) return { ok: false, reason: "unusable" };
1854
+ const attempt = (async () => {
1855
+ let response;
1856
+ try {
1857
+ response = await fetchImpl(`${apiBase}${AGENT_QUERY_PATH}`, {
1858
+ method: "POST",
1859
+ headers: headers(sessionToken),
1860
+ body: JSON.stringify({
1861
+ publicKey,
1862
+ sessionId,
1863
+ query,
1864
+ route,
1865
+ conversationId,
1866
+ // Omitted entirely when there are none, so an install with image
1867
+ // upload off sends exactly the body it sent before the feature
1868
+ // existed.
1869
+ ...attachments.length > 0 ? { attachments } : {}
1870
+ }),
1871
+ credentials: "omit"
1872
+ });
1873
+ } catch {
1874
+ return { ok: false, reason: "network" };
1875
+ }
1876
+ if (response.status === 402) {
1877
+ let parsed2;
1878
+ try {
1879
+ parsed2 = await response.json();
1880
+ } catch {
1881
+ return { ok: false, reason: "unusable" };
1882
+ }
1883
+ const capReason = parsed2 && typeof parsed2 === "object" && !Array.isArray(parsed2) ? parsed2.reason : void 0;
1884
+ return isCapDenialReason(capReason) ? { ok: false, reason: "cap_denied", capReason } : { ok: false, reason: "unusable" };
1885
+ }
1886
+ if (!response.ok) return { ok: false, reason: "rejected" };
1887
+ let parsed;
1888
+ try {
1889
+ parsed = await response.json();
1890
+ } catch {
1891
+ return { ok: false, reason: "unusable" };
1892
+ }
1893
+ const result = readChatResponse(parsed);
1894
+ return result ?? { ok: false, reason: "unusable" };
1895
+ })();
1896
+ return bounded(attempt, timeoutMs, setTimeoutImpl, { ok: false, reason: "network" });
1897
+ }
1898
+
1899
+ // src/shell/fonts.ts
1900
+ var SANS_FALLBACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
1901
+ function web(family, fallback = SANS_FALLBACK) {
1902
+ return { family, fallback, stack: `"${family}", ${fallback}` };
1903
+ }
1904
+ var CATALOG = {
1905
+ system: { stack: SANS_FALLBACK },
1906
+ serif: {
1907
+ stack: 'Charter, "Bitstream Charter", "Sitka Text", Cambria, Georgia, serif'
1908
+ },
1909
+ mono: {
1910
+ stack: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace'
1911
+ },
1912
+ humanist: {
1913
+ stack: 'Seravek, "Gill Sans Nova", Ubuntu, Calibri, "DejaVu Sans", sans-serif'
1914
+ },
1915
+ rounded: {
1916
+ stack: 'ui-rounded, "Hiragino Maru Gothic ProN", Quicksand, Comfortaa, "Arial Rounded MT Bold", Calibri, sans-serif'
1917
+ },
1918
+ inter: web("Inter"),
1919
+ roboto: web("Roboto"),
1920
+ "open-sans": web("Open Sans"),
1921
+ lato: web("Lato"),
1922
+ poppins: web("Poppins"),
1923
+ montserrat: web("Montserrat"),
1924
+ nunito: web("Nunito"),
1925
+ "source-sans-3": web("Source Sans 3"),
1926
+ "work-sans": web("Work Sans"),
1927
+ "dm-sans": web("DM Sans"),
1928
+ manrope: web("Manrope"),
1929
+ "playfair-display": web("Playfair Display", "Georgia, serif"),
1930
+ "ibm-plex-sans": web("IBM Plex Sans")
1931
+ };
1932
+ var SUBSETS = [
1933
+ [
1934
+ "latin",
1935
+ "U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD"
1936
+ ],
1937
+ [
1938
+ "latin-ext",
1939
+ "U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF"
1940
+ ]
1941
+ ];
1942
+ var FONT_PROPERTY = "--_arcy-font";
1943
+ var FONT_STYLE_MARKER = "data-arcy-font";
1944
+ function fontFaceCss(key, assetBase = DEFAULT_API_BASE) {
1945
+ const entry = CATALOG[key];
1946
+ if (!entry?.family) return "";
1947
+ return SUBSETS.map(
1948
+ ([subset, range]) => `@font-face{font-family:"${entry.family}";font-style:normal;font-weight:400;font-display:swap;src:url("${assetBase}/fonts/${key}-${subset}.woff2") format("woff2");unicode-range:${range};}`
1949
+ ).join("\n");
1950
+ }
1951
+ function applyFont(host, doc, assetBase = DEFAULT_API_BASE) {
1952
+ try {
1953
+ const key = host.style.getPropertyValue(FONT_KEY_PROPERTY).trim();
1954
+ const entry = key ? CATALOG[key] : void 0;
1955
+ if (!entry) {
1956
+ host.style.removeProperty(FONT_PROPERTY);
1957
+ return;
1958
+ }
1959
+ host.style.setProperty(FONT_PROPERTY, entry.stack);
1960
+ if (!entry.family) return;
1961
+ const head = doc.head || doc.documentElement;
1962
+ if (!head || head.querySelector(`style[${FONT_STYLE_MARKER}="${key}"]`)) {
1963
+ return;
1964
+ }
1965
+ const style = doc.createElement("style");
1966
+ applyNonce(style);
1967
+ style.setAttribute(FONT_STYLE_MARKER, key);
1968
+ style.textContent = fontFaceCss(key, assetBase);
1969
+ head.appendChild(style);
1970
+ } catch {
1971
+ }
1972
+ }
1973
+
1974
+ // src/shell/conversation-list.ts
1975
+ var CONVERSATIONS_PATH = "/api/v1/sdk/conversations";
1976
+ var CONVERSATIONS_TIMEOUT_MS = 15e3;
1977
+ var MAX_CONVERSATIONS = 20;
1978
+ var MAX_TITLE_LENGTH = 60;
1979
+ function resolveFetch2(fetchImpl) {
1980
+ if (fetchImpl !== void 0) return fetchImpl;
1981
+ return typeof fetch !== "undefined" ? fetch.bind(globalThis) : null;
1982
+ }
1983
+ function readConversations(body) {
1984
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
1985
+ const rows = body.conversations;
1986
+ if (!Array.isArray(rows)) return null;
1987
+ const out = [];
1988
+ for (const row of rows) {
1989
+ if (out.length >= MAX_CONVERSATIONS) break;
1990
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
1991
+ const r = row;
1992
+ if (typeof r.id !== "string" || r.id.length === 0) continue;
1993
+ const at = r.lastMessageAt;
1994
+ const lastMessageAt = typeof at === "number" && Number.isFinite(at) ? at : typeof at === "string" ? Date.parse(at) : NaN;
1995
+ out.push({
1996
+ id: r.id,
1997
+ firstMessage: typeof r.firstMessage === "string" && r.firstMessage.length > 0 ? r.firstMessage : null,
1998
+ hasImage: r.hasImage === true,
1999
+ lastMessageAt: Number.isFinite(lastMessageAt) ? lastMessageAt : 0
2000
+ });
2001
+ }
2002
+ return out;
2003
+ }
2004
+ function formatDate(timestamp) {
2005
+ if (!timestamp) return "";
2006
+ try {
2007
+ return new Date(timestamp).toLocaleDateString();
2008
+ } catch {
2009
+ return "";
2010
+ }
2011
+ }
2012
+ function deriveTitle(conversation, imageLabel) {
2013
+ const text = conversation.firstMessage?.trim() ?? "";
2014
+ if (text.length > 0) {
2015
+ return text.length > MAX_TITLE_LENGTH ? `${text.slice(0, MAX_TITLE_LENGTH - 1).trimEnd()}\u2026` : text;
2016
+ }
2017
+ const date = formatDate(conversation.lastMessageAt);
2018
+ if (conversation.hasImage) return date ? `${imageLabel} ${date}` : imageLabel;
2019
+ return date || imageLabel;
2020
+ }
2021
+ function toRows(conversations, imageLabel, currentConversationId) {
2022
+ return conversations.map((conversation) => ({
2023
+ id: conversation.id,
2024
+ title: deriveTitle(conversation, imageLabel),
2025
+ subtitle: formatDate(conversation.lastMessageAt),
2026
+ active: Boolean(
2027
+ currentConversationId && conversation.id === currentConversationId
2028
+ )
2029
+ }));
2030
+ }
2031
+ async function fetchConversations(options) {
2032
+ const {
2033
+ apiBase = DEFAULT_API_BASE,
2034
+ sessionToken,
2035
+ visitorId,
2036
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
2037
+ timeoutMs = CONVERSATIONS_TIMEOUT_MS
2038
+ } = options;
2039
+ const fetchImpl = resolveFetch2(options.fetchImpl);
2040
+ if (!fetchImpl || !sessionToken || !visitorId) return { ok: false };
2041
+ const attempt = (async () => {
2042
+ let response;
2043
+ try {
2044
+ response = await fetchImpl(
2045
+ `${apiBase}${CONVERSATIONS_PATH}?visitorId=${encodeURIComponent(visitorId)}`,
2046
+ {
2047
+ method: "GET",
2048
+ headers: { Authorization: `Bearer ${sessionToken}` },
2049
+ credentials: "omit"
2050
+ }
2051
+ );
2052
+ } catch {
2053
+ return { ok: false };
2054
+ }
2055
+ if (!response.ok) return { ok: false };
2056
+ let parsed;
2057
+ try {
2058
+ parsed = await response.json();
2059
+ } catch {
2060
+ return { ok: false };
2061
+ }
2062
+ const conversations = readConversations(parsed);
2063
+ return conversations ? { ok: true, conversations } : { ok: false };
2064
+ })();
2065
+ return new Promise((resolve) => {
2066
+ let settled = false;
2067
+ const settle = (value) => {
2068
+ if (settled) return;
2069
+ settled = true;
2070
+ resolve(value);
2071
+ };
2072
+ attempt.then(settle, () => settle({ ok: false }));
2073
+ try {
2074
+ setTimeoutImpl(() => settle({ ok: false }), timeoutMs);
2075
+ } catch {
2076
+ }
2077
+ });
2078
+ }
2079
+
2080
+ // src/shell/conversation-messages.ts
2081
+ var CONVERSATION_MESSAGES_PATH_TEMPLATE = "/api/v1/sdk/conversations/{conversationId}/messages";
2082
+ var CONVERSATION_MESSAGES_TIMEOUT_MS = 15e3;
2083
+ var MAX_REPLAYED_MESSAGES = 200;
2084
+ function resolveFetch3(fetchImpl) {
2085
+ if (fetchImpl !== void 0) return fetchImpl;
2086
+ return typeof fetch !== "undefined" ? fetch.bind(globalThis) : null;
2087
+ }
2088
+ function messagesPath(conversationId) {
2089
+ return CONVERSATION_MESSAGES_PATH_TEMPLATE.replace(
2090
+ "{conversationId}",
2091
+ encodeURIComponent(conversationId)
2092
+ );
2093
+ }
2094
+ function toTranscriptRole(role) {
2095
+ return role === "agent" ? "assistant" : "user";
2096
+ }
2097
+ function readMessages(body) {
2098
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
2099
+ const rows = body.messages;
2100
+ if (!Array.isArray(rows)) return null;
2101
+ const out = [];
2102
+ for (const row of rows) {
2103
+ if (out.length >= MAX_REPLAYED_MESSAGES) break;
2104
+ if (!row || typeof row !== "object" || Array.isArray(row)) continue;
2105
+ const r = row;
2106
+ if (typeof r.id !== "string" || r.id.length === 0) continue;
2107
+ if (r.role !== "user" && r.role !== "agent") continue;
2108
+ if (typeof r.text !== "string") continue;
2109
+ const at = r.occurredAt;
2110
+ const occurredAt = typeof at === "number" && Number.isFinite(at) ? at : typeof at === "string" ? Date.parse(at) : NaN;
2111
+ const images = typeof r.images === "number" && Number.isFinite(r.images) ? Math.max(0, Math.floor(r.images)) : 0;
2112
+ out.push({
2113
+ id: r.id,
2114
+ role: r.role,
2115
+ text: r.text,
2116
+ occurredAt: Number.isFinite(occurredAt) ? occurredAt : 0,
2117
+ images,
2118
+ // Only the two the backend can hold. Anything else reads as unrated,
2119
+ // which is the safe default: a thumb lit up for a rating nobody made
2120
+ // is worse than one that is not.
2121
+ feedback: r.feedback === "up" || r.feedback === "down" ? r.feedback : null
2122
+ });
2123
+ }
2124
+ return out;
2125
+ }
2126
+ async function fetchConversationMessages(options) {
2127
+ const {
2128
+ apiBase = DEFAULT_API_BASE,
2129
+ sessionToken,
2130
+ conversationId,
2131
+ visitorId,
2132
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
2133
+ timeoutMs = CONVERSATION_MESSAGES_TIMEOUT_MS
2134
+ } = options;
2135
+ const fetchImpl = resolveFetch3(options.fetchImpl);
2136
+ if (!fetchImpl || !sessionToken || !conversationId) return { ok: false };
2137
+ const attempt = (async () => {
2138
+ let response;
2139
+ try {
2140
+ const query = visitorId ? `?visitorId=${encodeURIComponent(visitorId)}` : "";
2141
+ response = await fetchImpl(`${apiBase}${messagesPath(conversationId)}${query}`, {
2142
+ method: "GET",
2143
+ headers: { Authorization: `Bearer ${sessionToken}` },
2144
+ credentials: "omit"
2145
+ });
2146
+ } catch {
2147
+ return { ok: false };
2148
+ }
2149
+ if (!response.ok) return { ok: false };
2150
+ let parsed;
2151
+ try {
2152
+ parsed = await response.json();
2153
+ } catch {
2154
+ return { ok: false };
2155
+ }
2156
+ const messages = readMessages(parsed);
2157
+ return messages ? { ok: true, messages } : { ok: false };
2158
+ })();
2159
+ return new Promise((resolve) => {
2160
+ let settled = false;
2161
+ const settle = (value) => {
2162
+ if (settled) return;
2163
+ settled = true;
2164
+ resolve(value);
2165
+ };
2166
+ attempt.then(settle, () => settle({ ok: false }));
2167
+ try {
2168
+ setTimeoutImpl(() => settle({ ok: false }), timeoutMs);
2169
+ } catch {
2170
+ }
2171
+ });
2172
+ }
2173
+
2174
+ // src/shell/chat-feedback.ts
2175
+ var FEEDBACK_PATH_TEMPLATE = "/api/v1/sdk/conversations/{conversationId}/messages/{messageId}/feedback";
2176
+ var FEEDBACK_TIMEOUT_MS = 1e4;
2177
+ function resolveFetch4(fetchImpl) {
2178
+ if (fetchImpl !== void 0) return fetchImpl;
2179
+ return typeof fetch !== "undefined" ? fetch.bind(globalThis) : null;
2180
+ }
2181
+ function feedbackPath(conversationId, messageId) {
2182
+ return FEEDBACK_PATH_TEMPLATE.replace(
2183
+ "{conversationId}",
2184
+ encodeURIComponent(conversationId)
2185
+ ).replace("{messageId}", encodeURIComponent(messageId));
2186
+ }
2187
+ async function postAnswerFeedback(options) {
2188
+ const {
2189
+ apiBase = DEFAULT_API_BASE,
2190
+ sessionToken,
2191
+ conversationId,
2192
+ messageId,
2193
+ value,
2194
+ visitorId,
2195
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
2196
+ timeoutMs = FEEDBACK_TIMEOUT_MS
2197
+ } = options;
2198
+ const fetchImpl = resolveFetch4(options.fetchImpl);
2199
+ if (!fetchImpl || !sessionToken || !conversationId || !messageId) {
2200
+ return { ok: false };
2201
+ }
2202
+ const attempt = (async () => {
2203
+ try {
2204
+ const query = visitorId ? `?visitorId=${encodeURIComponent(visitorId)}` : "";
2205
+ const response = await fetchImpl(
2206
+ `${apiBase}${feedbackPath(conversationId, messageId)}${query}`,
2207
+ {
2208
+ method: "POST",
2209
+ headers: {
2210
+ "Content-Type": "application/json",
2211
+ Authorization: `Bearer ${sessionToken}`
2212
+ },
2213
+ body: JSON.stringify({ value }),
2214
+ credentials: "omit"
2215
+ }
2216
+ );
2217
+ return { ok: response.ok };
2218
+ } catch {
2219
+ return { ok: false };
2220
+ }
2221
+ })();
2222
+ return new Promise((resolve) => {
2223
+ let settled = false;
2224
+ const settle = (result) => {
2225
+ if (settled) return;
2226
+ settled = true;
2227
+ resolve(result);
2228
+ };
2229
+ attempt.then(settle, () => settle({ ok: false }));
2230
+ try {
2231
+ setTimeoutImpl(() => settle({ ok: false }), timeoutMs);
2232
+ } catch {
2233
+ }
2234
+ });
2235
+ }
2236
+
2237
+ // src/shell/wordmark.ts
2238
+ var WORDMARK_SVG = "<svg width='476' height='139' viewBox='0 0 476 139' fill='none' xmlns='http://www.w3.org/2000/svg'><path d='M0 24.69C0 11.0541 11.0541 0 24.69 0H451.311C464.946 0 476 11.0537 476 24.6892V114.311C476 127.946 464.946 139 451.311 139H24.69C11.0541 139 0 127.946 0 114.31V24.69Z' fill='#12161A'/><path d='M132.047 84.6159L108.384 42.3014C101.224 29.4986 82.7988 29.502 75.6456 42.3116L52.0065 84.6362C43.8531 99.2299 63.7247 113.114 74.6251 100.436C83.7854 89.7891 100.271 89.7857 109.435 100.429C120.342 113.1 140.207 99.2096 132.047 84.6159Z' stroke='white' stroke-width='9.11833'/><path d='M232.67 100H221.885V85.398H183.969V100H173.183V70.2567C173.183 65.9148 173.93 61.9186 175.424 58.2681C176.917 54.6176 178.991 51.4787 181.646 48.8514C184.301 46.2242 187.44 44.1777 191.063 42.7119C194.685 41.2462 198.64 40.5133 202.927 40.5133H227.236C227.983 40.5133 228.688 40.6516 229.351 40.9282C230.015 41.2047 230.596 41.5919 231.094 42.0897C231.592 42.5875 231.979 43.1682 232.255 43.832C232.532 44.4957 232.67 45.2009 232.67 45.9476V100ZM183.969 74.6124H221.885V51.2989H202.927C202.595 51.2989 201.89 51.3542 200.811 51.4649C199.76 51.5478 198.53 51.7829 197.119 52.1701C195.736 52.5572 194.271 53.1518 192.722 53.9538C191.173 54.7558 189.749 55.8621 188.449 57.2725C187.149 58.6829 186.071 60.4528 185.214 62.5823C184.384 64.6841 183.969 67.2422 183.969 70.2567V74.6124ZM304.685 60.5082C304.685 62.9695 304.38 65.1681 303.772 67.104C303.164 69.0398 302.334 70.7545 301.283 72.2479C300.26 73.7136 299.071 74.9719 297.716 76.0228C296.36 77.0737 294.936 77.9449 293.443 78.6362C291.977 79.3 290.484 79.7839 288.963 80.0881C287.469 80.3924 286.059 80.5445 284.731 80.5445L307.257 100H290.58L268.097 80.5445H260.339V69.7589H284.731C286.086 69.6482 287.317 69.3717 288.423 68.9292C289.557 68.4591 290.525 67.8368 291.327 67.0625C292.157 66.2881 292.793 65.3617 293.235 64.2831C293.678 63.1769 293.899 61.9186 293.899 60.5082V53.7049C293.899 53.0965 293.816 52.6402 293.65 52.336C293.512 52.0041 293.318 51.7691 293.069 51.6308C292.848 51.4649 292.599 51.3681 292.323 51.3404C292.074 51.3128 291.839 51.2989 291.617 51.2989H256.025V100H245.239V45.9476C245.239 45.2009 245.378 44.4957 245.654 43.832C245.931 43.1682 246.304 42.5875 246.774 42.0897C247.272 41.5919 247.853 41.2047 248.517 40.9282C249.18 40.6516 249.899 40.5133 250.674 40.5133H291.617C294.024 40.5133 296.056 40.9558 297.716 41.8408C299.375 42.6981 300.716 43.7905 301.739 45.118C302.79 46.4178 303.537 47.8282 303.979 49.3492C304.45 50.8703 304.685 52.2945 304.685 53.622V60.5082ZM368.776 100H329.865C328.869 100 327.791 99.8894 326.629 99.6681C325.495 99.4192 324.361 99.0459 323.228 98.5481C322.121 98.0503 321.057 97.4142 320.033 96.6399C319.01 95.8379 318.098 94.8838 317.295 93.7775C316.521 92.6437 315.899 91.3439 315.429 89.8781C314.959 88.3848 314.724 86.7116 314.724 84.8587V55.6546C314.724 54.659 314.834 53.5943 315.055 52.4604C315.304 51.2989 315.678 50.1651 316.175 49.0588C316.673 47.925 317.323 46.8464 318.125 45.8232C318.927 44.7999 319.881 43.9011 320.987 43.1268C322.121 42.3248 323.421 41.6887 324.887 41.2185C326.353 40.7484 328.012 40.5133 329.865 40.5133H368.776V51.2989H329.865C328.454 51.2989 327.376 51.6723 326.629 52.419C325.882 53.1657 325.509 54.2719 325.509 55.7376V84.8587C325.509 86.2415 325.882 87.32 326.629 88.0944C327.404 88.8411 328.482 89.2144 329.865 89.2144H368.776V100ZM435.647 60.4252C435.647 62.8865 435.329 65.0851 434.692 67.021C434.084 68.9569 433.268 70.6715 432.245 72.1649C431.222 73.6306 430.019 74.8889 428.636 75.9398C427.281 76.9907 425.87 77.8619 424.405 78.5533C422.939 79.217 421.446 79.701 419.925 80.0052C418.431 80.3094 417.021 80.4615 415.693 80.4615H411.338V100H400.469V80.4615H396.155C394.827 80.4615 393.403 80.3094 391.882 80.0052C390.389 79.701 388.895 79.217 387.402 78.5533C385.936 77.8619 384.526 76.9907 383.171 75.9398C381.815 74.8889 380.612 73.6306 379.562 72.1649C378.538 70.6715 377.709 68.9569 377.073 67.021C376.464 65.0575 376.16 62.8589 376.16 60.4252V40.5133H386.946V60.4252C386.946 61.8356 387.167 63.1078 387.609 64.2416C388.052 65.3478 388.674 66.302 389.476 67.104C390.278 67.906 391.246 68.5282 392.38 68.9707C393.541 69.3855 394.827 69.5929 396.238 69.5929H415.693C416.716 69.427 417.767 69.2196 418.846 68.9707C419.925 68.6941 420.906 68.2378 421.791 67.6018C422.676 66.9657 423.409 66.0807 423.99 64.9468C424.571 63.813 424.861 62.3058 424.861 60.4252V40.5133H435.647V60.4252Z' fill='white'/></svg>";
2239
+ function createWordmark(doc) {
2240
+ try {
2241
+ const parsed = new DOMParser().parseFromString(WORDMARK_SVG, "image/svg+xml");
2242
+ const root = parsed.documentElement;
2243
+ if (!root || root.nodeName === "parsererror") return null;
2244
+ const node = doc.importNode(root, true);
2245
+ node.setAttribute("aria-hidden", "true");
2246
+ node.setAttribute("focusable", "false");
2247
+ return node;
2248
+ } catch {
2249
+ return null;
2250
+ }
2251
+ }
2252
+
2253
+ // src/shell/uploads.ts
2254
+ var UPLOAD_PATH = "/api/v1/sdk/uploads";
2255
+ var DISCARD_PATH = "/api/v1/sdk/uploads/discard";
2256
+ var MAX_IMAGES = 3;
2257
+ var MAX_BYTES = 5 * 1024 * 1024;
2258
+ var MAX_EDGE = 1568;
2259
+ var ALLOWED_TYPES = ATTACHABLE_TYPES;
2260
+ var UPLOAD_TIMEOUT_MS = 3e4;
2261
+ function validateFile(file, alreadyAttached) {
2262
+ if (!ALLOWED_TYPES.includes(file.type)) return { ok: false, reason: "type" };
2263
+ if (file.size > MAX_BYTES) return { ok: false, reason: "size" };
2264
+ if (alreadyAttached >= MAX_IMAGES) return { ok: false, reason: "count" };
2265
+ return { ok: true };
2266
+ }
2267
+ function scaledSize(width, height, maxEdge = MAX_EDGE) {
2268
+ const longest = Math.max(width, height);
2269
+ if (!(longest > maxEdge) || !(longest > 0)) {
2270
+ return { width: Math.round(width), height: Math.round(height) };
2271
+ }
2272
+ const ratio = maxEdge / longest;
2273
+ return {
2274
+ width: Math.max(1, Math.round(width * ratio)),
2275
+ height: Math.max(1, Math.round(height * ratio))
2276
+ };
2277
+ }
2278
+ function browserEnvironment(doc, fetchImpl = typeof fetch !== "undefined" ? fetch.bind(globalThis) : null) {
2279
+ return {
2280
+ createImageBitmap: (blob) => createImageBitmap(blob),
2281
+ createCanvas(width, height) {
2282
+ const canvas = doc.createElement("canvas");
2283
+ canvas.width = width;
2284
+ canvas.height = height;
2285
+ const context = canvas.getContext("2d");
2286
+ return {
2287
+ drawImage(source, w, h) {
2288
+ context?.drawImage(source, 0, 0, w, h);
2289
+ },
2290
+ toBlob(type, quality) {
2291
+ return new Promise((resolve) => {
2292
+ try {
2293
+ canvas.toBlob((blob) => resolve(blob), type, quality);
2294
+ } catch {
2295
+ resolve(null);
2296
+ }
2297
+ });
2298
+ }
2299
+ };
2300
+ },
2301
+ fetchImpl
2302
+ };
2303
+ }
2304
+ async function prepareImage(file, env) {
2305
+ const original = {
2306
+ blob: file,
2307
+ contentType: file.type,
2308
+ width: 0,
2309
+ height: 0
2310
+ };
2311
+ try {
2312
+ const bitmap = await env.createImageBitmap(file);
2313
+ const target = scaledSize(bitmap.width, bitmap.height);
2314
+ if (target.width === bitmap.width && target.height === bitmap.height) {
2315
+ bitmap.close?.();
2316
+ return { ...original, width: bitmap.width, height: bitmap.height };
2317
+ }
2318
+ const canvas = env.createCanvas(target.width, target.height);
2319
+ canvas.drawImage(bitmap, target.width, target.height);
2320
+ const blob = await canvas.toBlob("image/jpeg", 0.85);
2321
+ bitmap.close?.();
2322
+ if (!blob) return original;
2323
+ return {
2324
+ blob,
2325
+ contentType: "image/jpeg",
2326
+ width: target.width,
2327
+ height: target.height
2328
+ };
2329
+ } catch {
2330
+ return original;
2331
+ }
2332
+ }
2333
+ function readObjectKey(body) {
2334
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
2335
+ const key = body.objectKey;
2336
+ return typeof key === "string" && key.length > 0 ? key : null;
2337
+ }
2338
+ function bounded2(promise, timeoutMs, setTimeoutImpl, onTimeout) {
2339
+ return new Promise((resolve) => {
2340
+ let settled = false;
2341
+ const settle = (value) => {
2342
+ if (settled) return;
2343
+ settled = true;
2344
+ resolve(value);
2345
+ };
2346
+ promise.then(settle, () => settle(onTimeout));
2347
+ try {
2348
+ setTimeoutImpl(() => settle(onTimeout), timeoutMs);
2349
+ } catch {
2350
+ }
2351
+ });
2352
+ }
2353
+ async function uploadImage(file, alreadyAttached, options) {
2354
+ const validation = validateFile(file, alreadyAttached);
2355
+ if (!validation.ok) return { ok: false, reason: validation.reason };
2356
+ const {
2357
+ apiBase = DEFAULT_API_BASE,
2358
+ sessionToken,
2359
+ env,
2360
+ timeoutMs = UPLOAD_TIMEOUT_MS
2361
+ } = options;
2362
+ const setTimeoutImpl = env.setTimeoutImpl ?? ((fn, ms) => setTimeout(fn, ms));
2363
+ const fetchImpl = env.fetchImpl;
2364
+ if (!fetchImpl || !sessionToken) return { ok: false, reason: "failed" };
2365
+ const attempt = (async () => {
2366
+ const prepared = await prepareImage(file, env);
2367
+ let form;
2368
+ try {
2369
+ form = new FormData();
2370
+ form.append("file", prepared.blob, "image");
2371
+ } catch {
2372
+ return { ok: false, reason: "failed" };
2373
+ }
2374
+ let response;
2375
+ try {
2376
+ response = await fetchImpl(`${apiBase}${UPLOAD_PATH}`, {
2377
+ method: "POST",
2378
+ // No Content-Type: the browser writes it, with the boundary.
2379
+ headers: { Authorization: `Bearer ${sessionToken}` },
2380
+ body: form,
2381
+ credentials: "omit"
2382
+ });
2383
+ } catch {
2384
+ return { ok: false, reason: "failed" };
2385
+ }
2386
+ if (!response.ok) return { ok: false, reason: "failed" };
2387
+ let parsed;
2388
+ try {
2389
+ parsed = await response.json();
2390
+ } catch {
2391
+ return { ok: false, reason: "failed" };
2392
+ }
2393
+ const objectKey = readObjectKey(parsed);
2394
+ if (!objectKey) return { ok: false, reason: "failed" };
2395
+ return {
2396
+ ok: true,
2397
+ attachment: {
2398
+ objectKey,
2399
+ contentType: prepared.contentType,
2400
+ width: prepared.width,
2401
+ height: prepared.height
2402
+ }
2403
+ };
2404
+ })();
2405
+ return bounded2(attempt, timeoutMs, setTimeoutImpl, {
2406
+ ok: false,
2407
+ reason: "failed"
2408
+ });
2409
+ }
2410
+ async function discardUpload(objectKey, options) {
2411
+ const { apiBase = DEFAULT_API_BASE, sessionToken, fetchImpl } = options;
2412
+ if (!fetchImpl || !sessionToken || !objectKey) return false;
2413
+ try {
2414
+ const response = await fetchImpl(`${apiBase}${DISCARD_PATH}`, {
2415
+ method: "POST",
2416
+ headers: {
2417
+ "Content-Type": "application/json",
2418
+ Authorization: `Bearer ${sessionToken}`
2419
+ },
2420
+ body: JSON.stringify({ objectKey }),
2421
+ credentials: "omit"
2422
+ });
2423
+ return response.ok;
2424
+ } catch {
2425
+ return false;
2426
+ }
2427
+ }
2428
+
2429
+ // src/shell/chat.ts
2430
+ var PANEL_CLASS = "arcy-chat-panel";
2431
+ var HEADER_CLASS2 = "arcy-chat-header";
2432
+ var HEADER_ICON_CLASS = "arcy-chat-icon";
2433
+ var HEADER_NAME_CLASS = "arcy-chat-name";
2434
+ var HEADER_BUTTON_CLASS = "arcy-chat-hbtn";
2435
+ var BODY_CLASS = "arcy-chat-body";
2436
+ var STRIP_CLASS = "arcy-chat-strip";
2437
+ var MAIN_CLASS = "arcy-chat-main";
2438
+ var MARK_CLASS = "arcy-chat-mark";
2439
+ var MARK_LINK_CLASS = "arcy-chat-mark-link";
2440
+ var POLICY_CLASS = POLICY_LINK_CLASS;
2441
+ var ARCY_HOME_URL = "https://arcyai.com";
2442
+ var WELCOME_CLASS = "arcy-chat-welcome";
2443
+ var STARTER_CLASS = "arcy-chat-starter";
2444
+ var STARTER_ROW_CLASS = "arcy-chat-starters";
2445
+ var CHIPS_CLASS = "arcy-chat-chips";
2446
+ var CHIP_CLASS = "arcy-chat-chip";
2447
+ var ORG_CAP_REASONS = /* @__PURE__ */ new Set([
2448
+ "org_cap_exceeded",
2449
+ "anon_org_cap_exceeded",
2450
+ "credits_exhausted",
2451
+ "lapsed"
2452
+ ]);
2453
+ var DEFAULT_MAX_HEIGHT_PERCENT = 60;
2454
+ var CATALOG_RETRY_MS = 2500;
2455
+ var CHAT_CSS = TOOLTIP_CSS + `
2456
+ /* Stays mounted and animates open/shut in place (D929/D979) rather than
2457
+ toggling "display": the bar glides the panel the last few pixels rather
2458
+ than having it appear or vanish. "visibility" (not "pointer-events",
2459
+ which the panel forces "auto !important" inline to survive a hostile
2460
+ host page's own CSS) is what actually blocks interaction while closed: a
2461
+ "visibility: hidden" element is never hit-tested regardless of its
2462
+ "pointer-events" value. */
2463
+ .${PANEL_CLASS} {
2464
+ display: flex;
2465
+ flex-direction: column;
2466
+ overflow: hidden;
2467
+ visibility: hidden;
2468
+ opacity: 0;
2469
+ /* Always under the bar (.arcy-bar-wrap's own z-index in styles.ts): the
2470
+ panel mounts lazily, after the bar, and DOM order alone used to make it
2471
+ the one painted on top whenever the two overlapped. */
2472
+ z-index: 1;
2473
+ transform: translateY(8px) scale(0.98);
2474
+ transition: opacity 260ms cubic-bezier(0.32, 0.72, 0, 1),
2475
+ transform 260ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s linear 260ms;
2476
+ font: 14px/1.4 var(${FONT_PROPERTY}, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
2477
+ background: var(--_arcy-main-bg, #ffffff);
2478
+ color: var(--_arcy-main-text, #101828);
2479
+ /* The shared base.border (D832): one value, the bar's edge and this one. */
2480
+ border: var(--_arcy-border-width, 0px) solid
2481
+ var(--_arcy-border-color, color-mix(in srgb, var(--_arcy-main-text, #101828) 15%, transparent));
2482
+ border-radius: var(--_arcy-chat-radius, 16px);
2483
+ /* Offset 0 (was 8px down): the panel sits only PANEL_GAP (8px) above the
2484
+ bar, and a downward-pushed shadow painted across it. Centering the blur
2485
+ keeps the same softness without smearing onto the surface below. */
2486
+ box-shadow: var(--_arcy-chat-shadow, 0 0 24px rgba(16, 24, 40, 0.28));
2487
+ }
2488
+
2489
+ /* Glassiness (\xA75.4): a blur behind the panel plus an alpha on
2490
+ main.background. Only where the browser can blur; elsewhere the panel
2491
+ stays opaque, never transparent. */
2492
+ @supports (backdrop-filter: blur(1px)) {
2493
+ .${PANEL_CLASS} {
2494
+ background: color-mix(in srgb, var(--_arcy-main-bg, #ffffff) calc(100% - var(--_arcy-glass-alpha, 0%)), transparent);
2495
+ backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
2496
+ -webkit-backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
2497
+ }
2498
+ }
2499
+
2500
+ .${PANEL_CLASS}[data-open] {
2501
+ visibility: visible;
2502
+ opacity: 1;
2503
+ transform: translateY(0) scale(1);
2504
+ transition: opacity 260ms cubic-bezier(0.32, 0.72, 0, 1),
2505
+ transform 260ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s;
2506
+ }
2507
+
2508
+ /* Under the breakpoint the panel is a sheet: full width, rounded at the top
2509
+ only, so it reads as the screen rather than as a card floating on it.
2510
+ Set from JS as an attribute rather than a media query, because the
2511
+ breakpoint is one number and viewport.ts owns it (D865). */
2512
+ .${PANEL_CLASS}[data-sheet] {
2513
+ border-radius: var(--_arcy-chat-radius, 16px) var(--_arcy-chat-radius, 16px) 0 0;
2514
+ border-bottom: 0;
2515
+ }
2516
+
2517
+ .${HEADER_CLASS2} {
2518
+ display: flex;
2519
+ align-items: center;
2520
+ gap: 4px;
2521
+ padding: 8px 10px;
2522
+ flex: 0 0 auto;
2523
+ }
2524
+
2525
+ .${HEADER_BUTTON_CLASS} {
2526
+ appearance: none;
2527
+ border: 0;
2528
+ background: transparent;
2529
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 70%, transparent);
2530
+ cursor: pointer;
2531
+ padding: 6px;
2532
+ border-radius: 999px;
2533
+ display: flex;
2534
+ align-items: center;
2535
+ flex: 0 0 auto;
2536
+ -webkit-tap-highlight-color: transparent;
2537
+ }
2538
+
2539
+ .${HEADER_BUTTON_CLASS}:hover {
2540
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
2541
+ }
2542
+
2543
+ .${HEADER_BUTTON_CLASS}:focus-visible {
2544
+ outline: 2px solid currentColor;
2545
+ outline-offset: 2px;
2546
+ }
2547
+
2548
+ /* The identity sits in the middle, between the two controls on the left and
2549
+ the one on the right, so the panel reads as the operator's rather than as
2550
+ a third party's. */
2551
+ .${HEADER_CLASS2} .arcy-chat-identity {
2552
+ flex: 1 1 auto;
2553
+ display: flex;
2554
+ align-items: center;
2555
+ justify-content: center;
2556
+ gap: 6px;
2557
+ min-width: 0;
2558
+ }
2559
+
2560
+ /* Sized by its HEIGHT and keeps its own aspect ratio (D925/D930): it used to
2561
+ be a square box with "object-fit: cover", right for an app icon and wrong
2562
+ for everything else, since a wordmark got cropped to its middle letters.
2563
+ "chat.logoSize" means height here and the width follows. */
2564
+ .${HEADER_ICON_CLASS} {
2565
+ display: block;
2566
+ height: var(--_arcy-chat-logo-size, 24px);
2567
+ width: auto;
2568
+ max-width: 100%;
2569
+ border-radius: 4px;
2570
+ object-fit: contain;
2571
+ flex: 0 0 auto;
2572
+ }
2573
+
2574
+ .${HEADER_NAME_CLASS} {
2575
+ font-weight: 600;
2576
+ white-space: nowrap;
2577
+ overflow: hidden;
2578
+ text-overflow: ellipsis;
2579
+ min-width: 0;
2580
+ }
2581
+
2582
+ .${MAIN_CLASS} {
2583
+ position: relative;
2584
+ display: flex;
2585
+ flex: 1 1 auto;
2586
+ min-height: 0;
2587
+ overflow: hidden;
2588
+ }
2589
+
2590
+ .${BODY_CLASS} {
2591
+ position: relative;
2592
+ flex: 1 1 auto;
2593
+ overflow: auto;
2594
+ min-height: 0;
2595
+ scrollbar-width: thin;
2596
+ scrollbar-color: var(--_arcy-scroll-thumb, color-mix(in srgb, var(--_arcy-main-text, #101828) 25%, transparent))
2597
+ var(--_arcy-scroll-track, transparent);
2598
+ }
2599
+
2600
+ /* D896: a set scrollbar-color makes Chrome ignore every ::-webkit-scrollbar
2601
+ rule, which silently killed the width, track and hover tokens. */
2602
+ @supports selector(::-webkit-scrollbar) {
2603
+ .${BODY_CLASS} {
2604
+ scrollbar-width: auto;
2605
+ scrollbar-color: auto;
2606
+ }
2607
+ }
2608
+
2609
+ .${BODY_CLASS}::-webkit-scrollbar {
2610
+ width: var(--_arcy-scroll-width, 6px);
2611
+ }
2612
+
2613
+ .${BODY_CLASS}::-webkit-scrollbar-track {
2614
+ background: var(--_arcy-scroll-track, transparent);
2615
+ }
2616
+
2617
+ .${BODY_CLASS}::-webkit-scrollbar-thumb {
2618
+ background: var(--_arcy-scroll-thumb, color-mix(in srgb, var(--_arcy-main-text, #101828) 25%, transparent));
2619
+ border-radius: 999px;
2620
+ }
2621
+
2622
+ .${BODY_CLASS}::-webkit-scrollbar-thumb:hover {
2623
+ background: var(--_arcy-scroll-thumb-hover, color-mix(in srgb, var(--_arcy-main-text, #101828) 40%, transparent));
2624
+ }
2625
+
2626
+ /* \u2500\u2500 The welcome screen \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2627
+
2628
+ .${WELCOME_CLASS} {
2629
+ display: flex;
2630
+ flex-direction: column;
2631
+ align-items: center;
2632
+ text-align: center;
2633
+ gap: 10px;
2634
+ /* Room under the starter pills, so they do not sit on the bottom strip
2635
+ now that the panel takes its full height (D897), and room ABOVE the
2636
+ mark, which otherwise sits against the header's bottom edge and reads
2637
+ as part of the chrome rather than the start of the content. */
2638
+ padding: 26px 14px 20px;
2639
+ }
2640
+
2641
+ .${WELCOME_CLASS} .arcy-chat-welcome-icon {
2642
+ display: flex;
2643
+ align-items: center;
2644
+ justify-content: center;
2645
+ width: var(--_arcy-chat-welcome-icon-size, 34px);
2646
+ height: var(--_arcy-chat-welcome-icon-size, 34px);
2647
+ border-radius: 999px;
2648
+ background: var(--_arcy-brand-bg, #101828);
2649
+ color: var(--_arcy-brand-text, #ffffff);
2650
+ overflow: hidden;
2651
+ flex-shrink: 0;
2652
+ /* The uploaded mark is drawn full-bleed against a round clip: without an
2653
+ inset, any mark that is not a perfect circle (most logos aren't) reads
2654
+ as cropped at top/bottom the moment it stops being square.
2655
+ calc() against the size token itself, NOT a percentage: a percentage
2656
+ padding resolves against the *containing block's* width (the chat
2657
+ panel), not this element's own, so it would inflate a 34px badge to
2658
+ match the panel's width instead of insetting it. */
2659
+ box-sizing: border-box;
2660
+ padding: calc(var(--_arcy-chat-welcome-icon-size, 34px) * 0.15);
2661
+ }
2662
+
2663
+ .${WELCOME_CLASS} .arcy-chat-welcome-icon img {
2664
+ width: 100%;
2665
+ height: 100%;
2666
+ object-fit: contain;
2667
+ display: block;
2668
+ }
2669
+
2670
+ /* The default mark is drawn at a fixed 18px by icon(), so it would stay 18px
2671
+ inside a badge the operator resized. A share of the badge instead, which is
2672
+ what the 18-in-34 it shipped with already was. */
2673
+ .${WELCOME_CLASS} .arcy-chat-welcome-icon svg {
2674
+ width: 53%;
2675
+ height: 53%;
2676
+ }
2677
+
2678
+ .${WELCOME_CLASS} .arcy-chat-headline {
2679
+ font-size: 19px;
2680
+ font-weight: 600;
2681
+ }
2682
+
2683
+ .${WELCOME_CLASS} .arcy-chat-greeting {
2684
+ font-size: 13.5px;
2685
+ white-space: pre-wrap;
2686
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 70%, transparent);
2687
+ }
2688
+
2689
+ .${STARTER_ROW_CLASS} {
2690
+ display: flex;
2691
+ flex-wrap: wrap;
2692
+ justify-content: center;
2693
+ gap: 6px;
2694
+ }
2695
+
2696
+ /* The pills arrive a beat after the welcome screen paints, because they come
2697
+ from the flow catalog fetch rather than the bootstrap (D959). Holding one
2698
+ row of height until that answers is what stops the greeting sliding up the
2699
+ panel while somebody is reading it. Released either way: an operator who
2700
+ has featured nothing must not be left with a permanent gap. */
2701
+ .${STARTER_ROW_CLASS}[data-pending] {
2702
+ min-height: 34px;
2703
+ }
2704
+
2705
+ .${STARTER_CLASS} {
2706
+ appearance: none;
2707
+ text-align: left;
2708
+ font: inherit;
2709
+ font-size: 12.5px;
2710
+ cursor: pointer;
2711
+ padding: 7px 13px;
2712
+ border-radius: 999px;
2713
+ border: 1px solid color-mix(in srgb, var(--_arcy-main-text, #101828) 14%, transparent);
2714
+ background: transparent;
2715
+ color: inherit;
2716
+ max-width: 100%;
2717
+ transition: background 120ms ease, border-color 120ms ease;
2718
+ -webkit-tap-highlight-color: transparent;
2719
+ }
2720
+
2721
+ .${STARTER_CLASS}:hover {
2722
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 9%, transparent);
2723
+ border-color: color-mix(in srgb, var(--_arcy-main-text, #101828) 30%, transparent);
2724
+ }
2725
+
2726
+ @media (prefers-reduced-motion: reduce) {
2727
+ .${STARTER_CLASS} {
2728
+ transition: none;
2729
+ }
2730
+ }
2731
+
2732
+ /* \u2500\u2500 Attachments waiting to be sent \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2733
+
2734
+ .${CHIPS_CLASS} {
2735
+ display: flex;
2736
+ flex-wrap: wrap;
2737
+ gap: 6px;
2738
+ padding: 8px 12px 0;
2739
+ }
2740
+
2741
+ /* In the tray the bar owns the insets; the rule above is the D845 fallback
2742
+ for a core with no tray. */
2743
+ .${BAR_TRAY_CLASS} .${CHIPS_CLASS} {
2744
+ flex: 1 1 auto;
2745
+ min-width: 0;
2746
+ padding: 0;
2747
+ }
2748
+
2749
+ .${CHIP_CLASS} {
2750
+ position: relative;
2751
+ width: 56px;
2752
+ height: 56px;
2753
+ border-radius: 10px;
2754
+ overflow: hidden;
2755
+ border: 1px solid color-mix(in srgb, var(--_arcy-main-text, #101828) 16%, transparent);
2756
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 6%, transparent);
2757
+ }
2758
+
2759
+ .${CHIP_CLASS} img {
2760
+ display: block;
2761
+ width: 100%;
2762
+ height: 100%;
2763
+ object-fit: cover;
2764
+ }
2765
+
2766
+ /* While the bytes are still going up. The picture is already on screen, so
2767
+ the wait is a dimming rather than a placeholder that swaps for the real
2768
+ thing and moves the row. */
2769
+ .${CHIP_CLASS}[data-state="uploading"] img {
2770
+ opacity: 0.45;
2771
+ }
2772
+
2773
+ .${CHIP_CLASS}[data-state="uploading"]::after {
2774
+ content: "";
2775
+ position: absolute;
2776
+ inset: 50% auto auto 50%;
2777
+ width: 16px;
2778
+ height: 16px;
2779
+ margin: -8px 0 0 -8px;
2780
+ border-radius: 50%;
2781
+ border: 2px solid color-mix(in srgb, var(--_arcy-main-text, #101828) 25%, transparent);
2782
+ border-top-color: var(--_arcy-main-text, #101828);
2783
+ animation: arcy-chip-spin 700ms linear infinite;
2784
+ }
2785
+
2786
+ @keyframes arcy-chip-spin {
2787
+ to { transform: rotate(360deg); }
2788
+ }
2789
+
2790
+ @media (prefers-reduced-motion: reduce) {
2791
+ .${CHIP_CLASS}[data-state="uploading"]::after {
2792
+ animation: none;
2793
+ }
2794
+ }
2795
+
2796
+ /* The remove control. Over the picture rather than beside it, so three
2797
+ tiles still fit a phone screen, and 20px with a dark disc behind it so it
2798
+ is legible over a photograph of anything. */
2799
+ .${CHIP_CLASS} button {
2800
+ position: absolute;
2801
+ top: 3px;
2802
+ right: 3px;
2803
+ display: flex;
2804
+ align-items: center;
2805
+ justify-content: center;
2806
+ width: 18px;
2807
+ height: 18px;
2808
+ padding: 0;
2809
+ appearance: none;
2810
+ border: none;
2811
+ border-radius: 50%;
2812
+ background: rgba(16, 24, 40, 0.72);
2813
+ color: #ffffff;
2814
+ cursor: pointer;
2815
+ -webkit-tap-highlight-color: transparent;
2816
+ }
2817
+
2818
+ .${CHIP_CLASS} button:hover {
2819
+ background: rgba(16, 24, 40, 0.9);
2820
+ }
2821
+
2822
+ .${CHIP_CLASS} button svg {
2823
+ display: block;
2824
+ }
2825
+
2826
+ /* \u2500\u2500 The bottom strip \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
2827
+
2828
+ /* "Powered by ARCY" only, centred (D979): it is the widget's one free
2829
+ distribution channel, always rendered in beta (D839) and never a theme
2830
+ field. The AI disclosure and the consent sentence used to share this row
2831
+ and now sit under the bar instead, so the watermark is centred rather
2832
+ than pinned to a corner by a neighbour. */
2833
+ .${STRIP_CLASS} {
2834
+ flex: 0 0 auto;
2835
+ display: flex;
2836
+ align-items: center;
2837
+ justify-content: center;
2838
+ padding: 6px 14px 10px;
2839
+ font-size: 10.5px;
2840
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 55%, transparent);
2841
+ }
2842
+
2843
+ .${MARK_LINK_CLASS} {
2844
+ display: inline-flex;
2845
+ align-items: center;
2846
+ gap: 4px;
2847
+ max-width: 100%;
2848
+ color: inherit;
2849
+ text-decoration: none;
2850
+ }
2851
+
2852
+ .${MARK_LINK_CLASS} span {
2853
+ overflow: hidden;
2854
+ text-overflow: ellipsis;
2855
+ white-space: nowrap;
2856
+ }
2857
+
2858
+ .${MARK_CLASS} {
2859
+ height: 12px;
2860
+ width: auto;
2861
+ display: block;
2862
+ /* Dimmed to sit with the strip's own text, and lifted to full strength on
2863
+ hover so the link answers the pointer (D907). */
2864
+ opacity: 0.75;
2865
+ transition: opacity 120ms ease;
2866
+ }
2867
+
2868
+ .${MARK_LINK_CLASS}:hover .${MARK_CLASS},
2869
+ .${MARK_LINK_CLASS}:focus-visible .${MARK_CLASS} {
2870
+ opacity: 1;
2871
+ }
2872
+
2873
+ @media (prefers-reduced-motion: reduce) {
2874
+ .${MARK_CLASS} {
2875
+ transition: none;
2876
+ }
2877
+ }
2878
+
2879
+ @media (prefers-reduced-motion: reduce) {
2880
+ .${PANEL_CLASS} {
2881
+ transition: none;
2882
+ }
2883
+ }
2884
+ ` + TRANSCRIPT_CSS + FLOW_OFFER_CSS + DRAWER_CSS;
2885
+ var SVG_NS4 = "http://www.w3.org/2000/svg";
2886
+ function icon(doc, path, size = 17) {
2887
+ const svg = doc.createElementNS(SVG_NS4, "svg");
2888
+ svg.setAttribute("viewBox", "0 0 24 24");
2889
+ svg.setAttribute("aria-hidden", "true");
2890
+ svg.setAttribute("width", String(size));
2891
+ svg.setAttribute("height", String(size));
2892
+ for (const d of typeof path === "string" ? [path] : path) {
2893
+ const node = doc.createElementNS(SVG_NS4, "path");
2894
+ node.setAttribute("d", d);
2895
+ node.setAttribute("fill", "none");
2896
+ node.setAttribute("stroke", "currentColor");
2897
+ node.setAttribute("stroke-width", "2");
2898
+ node.setAttribute("stroke-linecap", "round");
2899
+ node.setAttribute("stroke-linejoin", "round");
2900
+ svg.appendChild(node);
2901
+ }
2902
+ return svg;
2903
+ }
2904
+ var CLOCK_PATH = "M12 21a9 9 0 100-18 9 9 0 000 18zM12 7v5l3 2";
2905
+ var FLOWS_PATH = "M13 3L5 14h6l-1 7 8-11h-6l1-7z";
2906
+ var CHEVRON_PATH = "M6 9l6 6 6-6";
2907
+ var CLOSE_PATH = "M18 6L6 18M6 6l12 12";
2908
+ var SETTINGS_PATHS = [
2909
+ "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z",
2910
+ // circle cx=12 cy=12 r=3
2911
+ "M9 12a3 3 0 1 0 6 0 3 3 0 1 0-6 0"
2912
+ ];
2913
+ var MOVE_DOWN_RIGHT_PATHS = ["M19 13V19H13", "M5 5L19 19"];
2914
+ var LANGUAGES_PATHS = [
2915
+ "M5 8h10",
2916
+ "M9 4v4",
2917
+ "M12 20l4-9 4 9",
2918
+ "M13.5 17h5",
2919
+ "M13 8a10 10 0 0 1-8 8",
2920
+ "M8 11a10 10 0 0 0 5 5"
2921
+ ];
2922
+ var MESSAGE_CIRCLE_PATH = "M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z";
2923
+ function place(panel, context) {
2924
+ const style = panel.style;
2925
+ const metrics = context.bar.getMetrics();
2926
+ const maxHeightPercent = context.getMaxHeightPercent?.() ?? DEFAULT_MAX_HEIGHT_PERCENT;
2927
+ const bottom = metrics.bottomOffset + metrics.keyboardOffset + metrics.height + PANEL_GAP;
2928
+ style.setProperty("bottom", `${bottom}px`);
2929
+ style.setProperty("left", "50%");
2930
+ style.setProperty("translate", "-50%");
2931
+ style.setProperty("transform", dragTransform(metrics.offset));
2932
+ if (metrics.isMobile) {
2933
+ panel.setAttribute("data-sheet", "");
2934
+ style.setProperty("width", `calc(100% - ${MOBILE_MARGIN * 2}px)`);
2935
+ style.setProperty("max-width", "none");
2936
+ style.setProperty("height", `calc(100% - ${bottom + MOBILE_MARGIN}px)`);
2937
+ style.setProperty("max-height", "none");
2938
+ return;
2939
+ }
2940
+ panel.removeAttribute("data-sheet");
2941
+ const width = surfaceWidth(metrics.widthPercent, {
2942
+ width: metrics.viewportWidth,
2943
+ height: metrics.viewportHeight
2944
+ });
2945
+ style.setProperty("width", `${width}px`);
2946
+ style.setProperty("max-width", `calc(100vw - ${PANEL_MARGIN * 2}px)`);
2947
+ style.setProperty(
2948
+ "transform",
2949
+ dragTransform({
2950
+ x: clampOffsetX(metrics.offset.x, width, metrics.viewportWidth),
2951
+ y: metrics.offset.y
2952
+ })
2953
+ );
2954
+ const effectiveBottom = bottom - metrics.offset.y;
2955
+ const height = panelHeight(maxHeightPercent, metrics.viewportHeight, effectiveBottom);
2956
+ style.setProperty("height", `${height}px`);
2957
+ style.setProperty("max-height", `${height}px`);
2958
+ }
2959
+ function mount(context) {
2960
+ try {
2961
+ let renderChips2 = function() {
2962
+ try {
2963
+ while (chips.firstChild) chips.removeChild(chips.firstChild);
2964
+ chips.style.setProperty("display", pending.length > 0 ? "flex" : "none");
2965
+ const label = strings().removeImage;
2966
+ for (const item of pending) {
2967
+ const tile = doc.createElement("div");
2968
+ tile.className = CHIP_CLASS;
2969
+ tile.setAttribute(
2970
+ "data-state",
2971
+ item.attachment ? "ready" : "uploading"
2972
+ );
2973
+ if (item.previewUrl) {
2974
+ const image = doc.createElement("img");
2975
+ image.src = item.previewUrl;
2976
+ image.alt = "";
2977
+ tile.appendChild(image);
2978
+ }
2979
+ const remove = doc.createElement("button");
2980
+ remove.type = "button";
2981
+ remove.setAttribute("aria-label", label);
2982
+ remove.setAttribute("data-tip", label);
2983
+ remove.appendChild(icon(doc, CLOSE_PATH, 12));
2984
+ remove.addEventListener("click", () => {
2985
+ pending = pending.filter((other) => other !== item);
2986
+ discard(item);
2987
+ renderChips2();
2988
+ });
2989
+ tile.appendChild(remove);
2990
+ chips.appendChild(tile);
2991
+ }
2992
+ context.bar.setAttachmentCount(pending.length);
2993
+ } catch {
2994
+ }
2995
+ }, startNewConversation2 = function() {
2996
+ context.setConversationId?.(null);
2997
+ transcript.clear();
2998
+ clearAttachments();
2999
+ renderWelcome();
3000
+ }, settleFill2 = function(value) {
3001
+ const pending2 = pendingFill;
3002
+ if (!pending2) return;
3003
+ pendingFill = null;
3004
+ fillCard.hide();
3005
+ context.bar.setAwaitingInput?.(false);
3006
+ context.bar.setDisabled(sending);
3007
+ context.bar.setPlaceholder(widget()?.inputPlaceholder ?? null);
3008
+ pending2.resolve(value);
3009
+ }, answerFill2 = function(raw) {
3010
+ const pending2 = pendingFill;
3011
+ if (!pending2) return;
3012
+ const request = pending2.request;
3013
+ const normalized = request.kind === "select" || request.kind === "radio" ? normalizeOptionReply(raw, request.options) : request.kind === "password" ? raw : raw.trim();
3014
+ const code = validateFillReply(normalized, request);
3015
+ if (code !== null) {
3016
+ fillCard.setError(code);
3017
+ return;
3018
+ }
3019
+ settleFill2(normalized);
3020
+ }, send2 = function(text) {
3021
+ if (sending) return;
3022
+ const sent = pending.filter(
3023
+ (item) => item.attachment !== null
3024
+ );
3025
+ pending = pending.filter((item) => item.attachment === null);
3026
+ renderChips2();
3027
+ transcript.addMessage("user", text, {
3028
+ images: sent.length,
3029
+ imageUrls: sent.map((item) => item.previewUrl).filter((url) => url !== null)
3030
+ });
3031
+ renderWelcome();
3032
+ sending = true;
3033
+ context.bar.setDisabled(true);
3034
+ transcript.setPending(true);
3035
+ void postAgentQuery({
3036
+ apiBase: context.apiBase ?? DEFAULT_API_BASE,
3037
+ publicKey: context.publicKey ?? "",
3038
+ sessionId: context.getSessionId?.() ?? "",
3039
+ sessionToken: context.getSessionToken?.() ?? "",
3040
+ query: text,
3041
+ route: context.getRoute?.() ?? win?.location?.pathname ?? "",
3042
+ conversationId: context.getConversationId?.() ?? "",
3043
+ attachments: sent.map((item) => ({
3044
+ objectKey: item.attachment.objectKey
3045
+ }))
3046
+ }).then((result) => {
3047
+ const chrome = strings();
3048
+ if (result.ok) {
3049
+ transcript.addMessage("assistant", result.message, {
3050
+ messageId: result.messageId,
3051
+ citedSources: result.citedSources,
3052
+ // ADR 0087: the offer belongs to this answer, so it is handed
3053
+ // to the bubble rather than to a strip that outlives the turn.
3054
+ flowOffers: result.flowsOffered
3055
+ });
3056
+ context.touchConversation?.();
3057
+ if (result.isFallback) {
3058
+ context.track?.("fallback_triggered", {}, { question: text });
3059
+ }
3060
+ for (const offer of result.flowsOffered) {
3061
+ context.track?.(
3062
+ "flow_suggested",
3063
+ {},
3064
+ { flowCvid: offer.flowCvid }
3065
+ );
3066
+ }
3067
+ return;
3068
+ }
3069
+ if (result.reason === "cap_denied") {
3070
+ if (ORG_CAP_REASONS.has(result.capReason)) {
3071
+ context.onOrgCapExceeded?.();
3072
+ } else {
3073
+ transcript.addMessage("system", chrome.userCapReached);
3074
+ }
3075
+ return;
3076
+ }
3077
+ transcript.addMessage("system", chrome.genericError);
3078
+ }).finally(() => {
3079
+ sending = false;
3080
+ transcript.setPending(false);
3081
+ context.bar.setDisabled(false);
3082
+ });
3083
+ }, openPanel2 = function() {
3084
+ if (panel.hasAttribute("data-open")) return;
3085
+ applyChrome2();
3086
+ applyFont(context.host, doc, context.apiBase);
3087
+ context.bar.setMode("open");
3088
+ place(panel, context);
3089
+ panel.setAttribute("data-open", "");
3090
+ try {
3091
+ const scope = context.isolated ? context.root : doc;
3092
+ const active = context.isolated ? scope.activeElement : doc.activeElement;
3093
+ const insideWidget = context.isolated ? active !== null : Boolean(active && context.host.contains(active));
3094
+ returnFocusTo = insideWidget ? null : doc.activeElement;
3095
+ context.bar.focusInput();
3096
+ } catch {
3097
+ }
3098
+ }, applyChrome2 = function() {
3099
+ const chrome = widget();
3100
+ const text = strings();
3101
+ renderIdentity();
3102
+ renderStrip();
3103
+ if (!pendingFill) {
3104
+ context.bar.setPlaceholder(chrome?.inputPlaceholder ?? null);
3105
+ }
3106
+ context.bar.setAttachEnabled(chrome?.allowImageUpload === true);
3107
+ context.bar.setDisclaimer(renderBarDisclaimer());
3108
+ for (const [button, label] of [
3109
+ [historyButton, text.historyTitle],
3110
+ [flowsButton, text.flowsTitle],
3111
+ [settingsButton, text.settingsTitle],
3112
+ [collapse, text.collapse]
3113
+ ]) {
3114
+ button.setAttribute("aria-label", label);
3115
+ button.setAttribute("data-tip", label);
3116
+ }
3117
+ for (const drawer of [historyDrawer, flowsDrawer, settingsDrawer]) {
3118
+ drawer.setTexts({ backLabel: text.back, closeLabel: text.closeSheet });
3119
+ }
3120
+ renderWelcome();
3121
+ }, closePanel2 = function() {
3122
+ if (!panel.hasAttribute("data-open")) return;
3123
+ panel.removeAttribute("data-open");
3124
+ historyDrawer.close();
3125
+ flowsDrawer.close();
3126
+ settingsDrawer.close();
3127
+ settingsView = "root";
3128
+ context.bar.setMode("idle");
3129
+ context.bar.setDisclaimer(null);
3130
+ try {
3131
+ const target = returnFocusTo;
3132
+ returnFocusTo = null;
3133
+ const root = panel.getRootNode();
3134
+ const inside = root instanceof ShadowRoot ? Boolean(root.activeElement) : document.activeElement !== null && document.activeElement !== document.body;
3135
+ if (inside && target && target.isConnected && typeof target.focus === "function") {
3136
+ target.focus();
3137
+ }
3138
+ } catch {
3139
+ }
3140
+ };
3141
+ var renderChips = renderChips2, startNewConversation = startNewConversation2, settleFill = settleFill2, answerFill = answerFill2, send = send2, openPanel = openPanel2, applyChrome = applyChrome2, closePanel = closePanel2;
3142
+ const shell = {
3143
+ host: context.host,
3144
+ root: context.root,
3145
+ isolated: context.isolated,
3146
+ destroy() {
3147
+ }
3148
+ };
3149
+ injectStyles(shell, CHAT_CSS);
3150
+ injectStyles(shell, FILL_CARD_CSS);
3151
+ const doc = context.host.ownerDocument;
3152
+ const win = doc.defaultView;
3153
+ const widget = () => {
3154
+ try {
3155
+ return context.getWidget?.() ?? null;
3156
+ } catch {
3157
+ return null;
3158
+ }
3159
+ };
3160
+ const strings = () => widget()?.chrome ?? FALLBACK_CHROME;
3161
+ const panel = doc.createElement("div");
3162
+ panel.className = PANEL_CLASS;
3163
+ panel.setAttribute("role", "dialog");
3164
+ panel.setAttribute("aria-label", "ARCY chat");
3165
+ panel.tabIndex = -1;
3166
+ panel.style.setProperty("position", "absolute", "important");
3167
+ panel.style.setProperty("pointer-events", "auto", "important");
3168
+ panel.style.setProperty("box-sizing", "border-box");
3169
+ const header = doc.createElement("div");
3170
+ header.className = HEADER_CLASS2;
3171
+ const headerButton = (label, path, align) => {
3172
+ const button = doc.createElement("button");
3173
+ button.type = "button";
3174
+ button.className = HEADER_BUTTON_CLASS;
3175
+ button.setAttribute("aria-label", label);
3176
+ button.setAttribute("data-tip", label);
3177
+ button.setAttribute("data-tip-placement", "below");
3178
+ button.setAttribute("data-tip-align", align);
3179
+ button.appendChild(icon(doc, path));
3180
+ return button;
3181
+ };
3182
+ const historyButton = headerButton(
3183
+ strings().historyTitle,
3184
+ CLOCK_PATH,
3185
+ "start"
3186
+ );
3187
+ const flowsButton = headerButton(strings().flowsTitle, FLOWS_PATH, "start");
3188
+ flowsButton.style.setProperty("display", "none");
3189
+ const identity = doc.createElement("div");
3190
+ identity.className = "arcy-chat-identity";
3191
+ let iconNode = doc.createElement("span");
3192
+ const name = doc.createElement("span");
3193
+ name.className = HEADER_NAME_CLASS;
3194
+ identity.appendChild(iconNode);
3195
+ identity.appendChild(name);
3196
+ const settingsButton = headerButton(
3197
+ strings().settingsTitle,
3198
+ SETTINGS_PATHS,
3199
+ "end"
3200
+ );
3201
+ if (!context.bar.dock) settingsButton.style.setProperty("display", "none");
3202
+ const collapse = headerButton(strings().collapse, CHEVRON_PATH, "end");
3203
+ header.appendChild(historyButton);
3204
+ header.appendChild(flowsButton);
3205
+ header.appendChild(identity);
3206
+ header.appendChild(settingsButton);
3207
+ header.appendChild(collapse);
3208
+ const renderIdentity = () => {
3209
+ const chrome = widget();
3210
+ const url = resolveImageUrl(chrome?.logoUrl, context.apiBase);
3211
+ name.textContent = url ? "" : chrome?.name ?? "";
3212
+ name.style.setProperty("display", url ? "none" : "block");
3213
+ if (url && iconNode.getAttribute("data-src") === url) return;
3214
+ const next = url ? doc.createElement("img") : doc.createElement("span");
3215
+ next.className = url ? HEADER_ICON_CLASS : "";
3216
+ if (url) {
3217
+ const img = next;
3218
+ img.alt = chrome?.name ?? "";
3219
+ img.decoding = "async";
3220
+ img.onerror = () => {
3221
+ try {
3222
+ const fallback = doc.createElement("span");
3223
+ img.replaceWith(fallback);
3224
+ iconNode = fallback;
3225
+ name.textContent = chrome?.name ?? "";
3226
+ name.style.setProperty("display", "block");
3227
+ } catch {
3228
+ }
3229
+ };
3230
+ img.setAttribute("data-src", url);
3231
+ img.src = url;
3232
+ }
3233
+ iconNode.replaceWith(next);
3234
+ iconNode = next;
3235
+ };
3236
+ const body = doc.createElement("div");
3237
+ body.className = BODY_CLASS;
3238
+ const strip = doc.createElement("div");
3239
+ strip.className = STRIP_CLASS;
3240
+ const poweredBy = doc.createElement("a");
3241
+ poweredBy.className = MARK_LINK_CLASS;
3242
+ poweredBy.href = ARCY_HOME_URL;
3243
+ poweredBy.target = "_blank";
3244
+ poweredBy.rel = "noopener noreferrer";
3245
+ const poweredByLabel = doc.createElement("span");
3246
+ const mark = createWordmark(doc);
3247
+ if (mark) mark.setAttribute("class", MARK_CLASS);
3248
+ poweredBy.appendChild(poweredByLabel);
3249
+ if (mark) poweredBy.appendChild(mark);
3250
+ strip.appendChild(poweredBy);
3251
+ const renderStrip = () => {
3252
+ const text = strings();
3253
+ poweredByLabel.textContent = text.poweredBy.replace(/\s*ARCY\s*$/i, "");
3254
+ poweredBy.setAttribute("aria-label", text.poweredBy);
3255
+ };
3256
+ const renderBarDisclaimer = () => {
3257
+ const chrome = widget();
3258
+ const text = strings();
3259
+ const url = chrome?.privacyPolicyUrl;
3260
+ const disclaimer = chrome?.name ? text.aiDisclaimer.replace("{app}", chrome.name) : text.aiDisclaimer;
3261
+ if (typeof url === "string" && /^https:\/\//i.test(url)) {
3262
+ const idx = text.consentWithPolicy.indexOf("{policy}");
3263
+ const before = idx >= 0 ? text.consentWithPolicy.slice(0, idx) : `${text.consentWithPolicy} `;
3264
+ const after = idx >= 0 ? text.consentWithPolicy.slice(idx + "{policy}".length) : "";
3265
+ return {
3266
+ before: `${disclaimer}${text.disclaimerJoin}${before}`,
3267
+ linkLabel: text.privacyPolicy,
3268
+ linkHref: url,
3269
+ after
3270
+ };
3271
+ }
3272
+ return disclaimer;
3273
+ };
3274
+ const main = doc.createElement("div");
3275
+ main.className = MAIN_CLASS;
3276
+ main.appendChild(body);
3277
+ panel.appendChild(header);
3278
+ panel.appendChild(main);
3279
+ panel.appendChild(strip);
3280
+ context.root.appendChild(panel);
3281
+ applyFont(context.host, doc, context.apiBase);
3282
+ place(panel, context);
3283
+ const transcript = mountTranscript(body, doc, {
3284
+ labels: {
3285
+ likeAnswer: strings().likeAnswer,
3286
+ dislikeAnswer: strings().dislikeAnswer
3287
+ },
3288
+ answeringLabel: () => strings().answering,
3289
+ searchedSourcesLabel: strings().searchedSourcesLabel,
3290
+ sourcesLabel: strings().sourcesLabel,
3291
+ flowOfferHint: () => strings().flowOfferHint,
3292
+ onStartFlow: (flowCvid) => {
3293
+ context.track?.("flow_pill_clicked", {}, { flowCvid });
3294
+ context.onStartFlow?.(flowCvid);
3295
+ },
3296
+ /**
3297
+ * A rating goes to two places, and both are deliberate (D1017).
3298
+ *
3299
+ * The telemetry event is the anonymous aggregate: it goes through the
3300
+ * core's own `track()`, so it passes the same consent gate and the
3301
+ * same batching every other event family does (D884), and
3302
+ * `answer_feedback` was already in the closed vocabulary.
3303
+ *
3304
+ * The write onto the message row is the per-answer record, and it is
3305
+ * what the operator actually reads: it renders next to the answer in
3306
+ * Analytics > conversation history. Until this existed the thumbs only
3307
+ * fired the event, which carried no link to a conversation or a
3308
+ * message and which nothing read, so from the operator's side the
3309
+ * controls did nothing at all.
3310
+ */
3311
+ onFeedback: (value, messageId) => {
3312
+ context.track?.("answer_feedback", {}, { value });
3313
+ const sessionToken = context.getSessionToken?.() ?? "";
3314
+ const conversationId = context.getConversationId?.() ?? "";
3315
+ if (!messageId || !sessionToken || !conversationId) return;
3316
+ void postAnswerFeedback({
3317
+ apiBase: context.apiBase,
3318
+ sessionToken,
3319
+ conversationId,
3320
+ messageId,
3321
+ value,
3322
+ visitorId: context.getVisitorId?.() ?? void 0
3323
+ });
3324
+ }
3325
+ });
3326
+ const welcome = doc.createElement("div");
3327
+ welcome.className = WELCOME_CLASS;
3328
+ body.insertBefore(welcome, body.firstChild);
3329
+ let starterFlows = [];
3330
+ let starterFlowsPending = true;
3331
+ let failedWelcomeIconUrl = null;
3332
+ const renderWelcome = () => {
3333
+ try {
3334
+ while (welcome.firstChild) welcome.removeChild(welcome.firstChild);
3335
+ if (!transcript.isEmpty()) {
3336
+ welcome.style.setProperty("display", "none");
3337
+ return;
3338
+ }
3339
+ welcome.style.setProperty("display", "flex");
3340
+ const chrome = widget();
3341
+ const iconEl = doc.createElement("div");
3342
+ iconEl.className = "arcy-chat-welcome-icon";
3343
+ const welcomeIconUrl = resolveImageUrl(
3344
+ chrome?.welcomeIconUrl,
3345
+ context.apiBase
3346
+ );
3347
+ if (welcomeIconUrl && welcomeIconUrl !== failedWelcomeIconUrl) {
3348
+ const img = doc.createElement("img");
3349
+ img.src = welcomeIconUrl;
3350
+ img.alt = "";
3351
+ img.addEventListener(
3352
+ "error",
3353
+ () => {
3354
+ failedWelcomeIconUrl = welcomeIconUrl;
3355
+ renderWelcome();
3356
+ },
3357
+ { once: true }
3358
+ );
3359
+ iconEl.appendChild(img);
3360
+ } else {
3361
+ iconEl.appendChild(icon(doc, MESSAGE_CIRCLE_PATH, 18));
3362
+ }
3363
+ welcome.appendChild(iconEl);
3364
+ const headline = chrome?.welcomeHeadline?.trim() ?? "";
3365
+ const greeting = chrome?.welcomeMessage?.trim() ?? "";
3366
+ if (headline) {
3367
+ const el = doc.createElement("div");
3368
+ el.className = "arcy-chat-headline";
3369
+ el.textContent = headline;
3370
+ welcome.appendChild(el);
3371
+ }
3372
+ if (greeting) {
3373
+ const el = doc.createElement("div");
3374
+ el.className = "arcy-chat-greeting";
3375
+ el.textContent = greeting;
3376
+ welcome.appendChild(el);
3377
+ }
3378
+ const row = doc.createElement("div");
3379
+ row.className = STARTER_ROW_CLASS;
3380
+ if (starterFlowsPending) row.setAttribute("data-pending", "");
3381
+ for (const flow of starterFlows) {
3382
+ const button = doc.createElement("button");
3383
+ button.type = "button";
3384
+ button.className = STARTER_CLASS;
3385
+ button.textContent = flow.publicName;
3386
+ button.addEventListener("click", () => {
3387
+ context.track?.("starter_flow_clicked", {}, { flowCvid: flow.cvid });
3388
+ context.onStartFlow?.(flow.cvid);
3389
+ });
3390
+ row.appendChild(button);
3391
+ }
3392
+ welcome.appendChild(row);
3393
+ } catch {
3394
+ }
3395
+ };
3396
+ const chips = doc.createElement("div");
3397
+ chips.className = CHIPS_CLASS;
3398
+ chips.style.setProperty("display", "none");
3399
+ const tray = context.bar.attachmentSlot?.() ?? null;
3400
+ if (tray) tray.appendChild(chips);
3401
+ else body.insertBefore(chips, body.firstChild);
3402
+ let pending = [];
3403
+ const uploadEnv = browserEnvironment(doc);
3404
+ const fetchImpl = uploadEnv.fetchImpl ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : null);
3405
+ const previewFor = (file) => {
3406
+ try {
3407
+ return win?.URL?.createObjectURL?.(file) ?? URL.createObjectURL(file);
3408
+ } catch {
3409
+ return null;
3410
+ }
3411
+ };
3412
+ const releasePreview = (item) => {
3413
+ if (!item.previewUrl) return;
3414
+ try {
3415
+ ;
3416
+ (win?.URL ?? URL).revokeObjectURL(item.previewUrl);
3417
+ } catch {
3418
+ }
3419
+ item.previewUrl = null;
3420
+ };
3421
+ const discard = (item) => {
3422
+ releasePreview(item);
3423
+ const objectKey = item.attachment?.objectKey;
3424
+ if (!objectKey) return;
3425
+ void discardUpload(objectKey, {
3426
+ apiBase: context.apiBase,
3427
+ sessionToken: context.getSessionToken?.() ?? "",
3428
+ fetchImpl
3429
+ });
3430
+ };
3431
+ const clearAttachments = () => {
3432
+ const dropped = pending;
3433
+ pending = [];
3434
+ for (const item of dropped) discard(item);
3435
+ renderChips2();
3436
+ };
3437
+ let ingestChain = Promise.resolve();
3438
+ const ingestFiles = (files) => {
3439
+ ingestChain = ingestChain.then(async () => {
3440
+ for (const file of files) {
3441
+ const sessionToken = context.getSessionToken?.() ?? "";
3442
+ const reject = (reason) => {
3443
+ const text = strings();
3444
+ transcript.addMessage(
3445
+ "system",
3446
+ reason === "type" ? text.imageWrongType : reason === "size" ? text.imageTooLarge : reason === "count" ? text.imageTooMany : text.imageFailed
3447
+ );
3448
+ };
3449
+ const check = validateFile(file, pending.length);
3450
+ if (!check.ok) {
3451
+ reject(check.reason);
3452
+ renderWelcome();
3453
+ continue;
3454
+ }
3455
+ const item = {
3456
+ previewUrl: previewFor(file),
3457
+ attachment: null
3458
+ };
3459
+ pending = [...pending, item];
3460
+ renderChips2();
3461
+ const result = await uploadImage(file, pending.length - 1, {
3462
+ apiBase: context.apiBase,
3463
+ sessionToken,
3464
+ env: uploadEnv
3465
+ });
3466
+ if (result.ok) {
3467
+ item.attachment = result.attachment;
3468
+ renderChips2();
3469
+ continue;
3470
+ }
3471
+ pending = pending.filter((other) => other !== item);
3472
+ releasePreview(item);
3473
+ renderChips2();
3474
+ reject(result.reason);
3475
+ renderWelcome();
3476
+ }
3477
+ });
3478
+ ingestChain = ingestChain.catch(() => {
3479
+ });
3480
+ };
3481
+ let conversations = [];
3482
+ const historyDrawer = mountDrawer(panel, doc, {
3483
+ title: strings().historyTitle,
3484
+ emptyText: strings().historyEmpty,
3485
+ actionLabel: strings().newChat,
3486
+ // A conversation's subtitle is its date. It is never clipped, so the
3487
+ // summary bubble had nothing to reveal and only repeated the line
3488
+ // under the pointer (D1268).
3489
+ subtitleTips: false,
3490
+ backLabel: strings().back,
3491
+ closeLabel: strings().closeSheet,
3492
+ onBack: () => historyDrawer.close(),
3493
+ onAction: () => {
3494
+ startNewConversation2();
3495
+ historyDrawer.close();
3496
+ },
3497
+ onSelect: (conversationId) => {
3498
+ if ((context.getConversationId?.() ?? "") === conversationId) {
3499
+ historyDrawer.close();
3500
+ return;
3501
+ }
3502
+ context.setConversationId?.(conversationId);
3503
+ transcript.clear();
3504
+ clearAttachments();
3505
+ renderWelcome();
3506
+ historyDrawer.close();
3507
+ void replayConversation(conversationId);
3508
+ }
3509
+ });
3510
+ async function replayConversation(conversationId) {
3511
+ const sessionToken = context.getSessionToken?.() ?? "";
3512
+ if (!sessionToken) return;
3513
+ const result = await fetchConversationMessages({
3514
+ apiBase: context.apiBase,
3515
+ sessionToken,
3516
+ conversationId,
3517
+ visitorId: context.getVisitorId?.() ?? void 0
3518
+ });
3519
+ if (!result.ok) return;
3520
+ if ((context.getConversationId?.() ?? "") !== conversationId) return;
3521
+ transcript.clear();
3522
+ for (const message of result.messages) {
3523
+ transcript.addMessage(
3524
+ toTranscriptRole(message.role),
3525
+ message.text,
3526
+ {
3527
+ images: message.images,
3528
+ at: message.occurredAt ? new Date(message.occurredAt) : void 0,
3529
+ messageId: message.id,
3530
+ feedback: message.feedback
3531
+ }
3532
+ );
3533
+ }
3534
+ renderWelcome();
3535
+ transcript.scrollToEnd();
3536
+ }
3537
+ const flowsDrawer = mountDrawer(panel, doc, {
3538
+ title: strings().flowsTitle,
3539
+ emptyText: strings().flowsEmpty,
3540
+ actionLabel: null,
3541
+ backLabel: strings().back,
3542
+ closeLabel: strings().closeSheet,
3543
+ // Every row here is a flow, so every row carries the flow mark: the
3544
+ // same bolt the Flows button in the header wears, so a visitor who
3545
+ // opened the sheet from that button sees the mark they tapped repeated
3546
+ // down the rows it opened.
3547
+ itemIcon: (d) => icon(d, FLOWS_PATH, 16),
3548
+ onBack: () => flowsDrawer.close(),
3549
+ onSelect: (flowCvid) => {
3550
+ flowsDrawer.close();
3551
+ context.onStartFlow?.(flowCvid);
3552
+ }
3553
+ });
3554
+ let settingsView = "root";
3555
+ const settingsDrawer = mountDrawer(panel, doc, {
3556
+ title: strings().settingsTitle,
3557
+ side: "right",
3558
+ actionLabel: null,
3559
+ backLabel: strings().back,
3560
+ closeLabel: strings().closeSheet,
3561
+ // Back pops a level before it closes, so the picker returns to the
3562
+ // settings list the visitor opened it from rather than dropping them
3563
+ // back in the conversation two steps from where they were.
3564
+ onBack: () => {
3565
+ if (settingsView === "language") {
3566
+ settingsView = "root";
3567
+ renderSettings();
3568
+ return;
3569
+ }
3570
+ settingsDrawer.close();
3571
+ },
3572
+ onSelect: (id) => {
3573
+ if (settingsView === "language") {
3574
+ settingsDrawer.close();
3575
+ settingsView = "root";
3576
+ context.onSelectLocale?.(id);
3577
+ return;
3578
+ }
3579
+ if (id === "language") {
3580
+ settingsView = "language";
3581
+ renderSettings();
3582
+ return;
3583
+ }
3584
+ if (id !== "dock") return;
3585
+ settingsDrawer.close();
3586
+ context.onRequestClose();
3587
+ context.bar.dock?.();
3588
+ }
3589
+ });
3590
+ const offeredLocales = () => {
3591
+ if (!context.onSelectLocale) return [];
3592
+ const locales = widget()?.locales ?? [];
3593
+ return locales.length >= 2 ? locales : [];
3594
+ };
3595
+ const renderSettings = () => {
3596
+ const text = strings();
3597
+ const locales = offeredLocales();
3598
+ if (settingsView === "language") {
3599
+ settingsDrawer.setTitle(text.language);
3600
+ settingsDrawer.setItems(
3601
+ locales.map((locale) => ({
3602
+ id: locale.code,
3603
+ // The language's own name, from the bootstrap. The widget never
3604
+ // spells one itself: see `WidgetChrome.locales`.
3605
+ title: locale.label,
3606
+ active: locale.code === widget()?.activeLocale,
3607
+ icon: (d) => icon(d, LANGUAGES_PATHS, 16)
3608
+ }))
3609
+ );
3610
+ return;
3611
+ }
3612
+ settingsDrawer.setTitle(text.settingsTitle);
3613
+ const active = locales.find(
3614
+ (locale) => locale.code === widget()?.activeLocale
3615
+ );
3616
+ settingsDrawer.setItems([
3617
+ ...locales.length ? [
3618
+ {
3619
+ id: "language",
3620
+ title: text.language,
3621
+ // The language it is in right now, so the row answers the
3622
+ // question before it is opened.
3623
+ subtitle: active?.label ?? null,
3624
+ icon: (d) => icon(d, LANGUAGES_PATHS, 16)
3625
+ }
3626
+ ] : [],
3627
+ {
3628
+ id: "dock",
3629
+ title: text.dockChat,
3630
+ subtitle: text.dockChatHint,
3631
+ icon: (d) => icon(d, MOVE_DOWN_RIGHT_PATHS, 16)
3632
+ }
3633
+ ]);
3634
+ };
3635
+ const openHistory = () => {
3636
+ const text = strings();
3637
+ historyDrawer.setTitle(text.historyTitle);
3638
+ historyDrawer.setTexts({
3639
+ emptyText: text.historyEmpty,
3640
+ actionLabel: text.newChat
3641
+ });
3642
+ historyDrawer.setItems(
3643
+ toRows(
3644
+ conversations,
3645
+ text.imageConversation,
3646
+ context.getConversationId?.()
3647
+ )
3648
+ );
3649
+ historyDrawer.open();
3650
+ void refreshConversations();
3651
+ };
3652
+ async function refreshConversations() {
3653
+ const sessionToken = context.getSessionToken?.() ?? "";
3654
+ const visitorId = context.getVisitorId?.() ?? "";
3655
+ if (!sessionToken || !visitorId) return;
3656
+ const result = await fetchConversations({
3657
+ apiBase: context.apiBase,
3658
+ sessionToken,
3659
+ visitorId
3660
+ });
3661
+ if (!result.ok) return;
3662
+ conversations = result.conversations;
3663
+ if (!historyDrawer.isOpen()) return;
3664
+ historyDrawer.setItems(
3665
+ toRows(
3666
+ conversations,
3667
+ strings().imageConversation,
3668
+ context.getConversationId?.()
3669
+ )
3670
+ );
3671
+ }
3672
+ historyButton.addEventListener("click", openHistory);
3673
+ flowsButton.addEventListener("click", () => {
3674
+ const text = strings();
3675
+ flowsDrawer.setTitle(text.flowsTitle);
3676
+ flowsDrawer.setTexts({ emptyText: text.flowsEmpty });
3677
+ flowsDrawer.open();
3678
+ });
3679
+ const openSettings = () => {
3680
+ settingsView = "root";
3681
+ renderSettings();
3682
+ settingsDrawer.open();
3683
+ };
3684
+ settingsButton.addEventListener("click", openSettings);
3685
+ const handleCollapse = () => context.onRequestClose();
3686
+ collapse.addEventListener("click", handleCollapse);
3687
+ const fillCard = mountFillCard(context, () => strings());
3688
+ let pendingFill = null;
3689
+ let sending = false;
3690
+ context.bar.onSubmit((text) => {
3691
+ if (pendingFill) {
3692
+ answerFill2(text);
3693
+ return;
3694
+ }
3695
+ openPanel2();
3696
+ send2(text);
3697
+ });
3698
+ context.bar.onFiles?.((files) => {
3699
+ openPanel2();
3700
+ ingestFiles(files);
3701
+ });
3702
+ const uploadsAllowed = () => widget()?.allowImageUpload === true;
3703
+ const onPanelDragOver = (event) => {
3704
+ if (!carriesFiles(event)) return;
3705
+ event.preventDefault();
3706
+ };
3707
+ const onPanelDrop = (event) => {
3708
+ if (!carriesFiles(event)) return;
3709
+ event.preventDefault();
3710
+ if (!uploadsAllowed()) return;
3711
+ const files = droppedFiles(event);
3712
+ if (files.length > 0) ingestFiles(files);
3713
+ };
3714
+ panel.addEventListener("dragover", onPanelDragOver);
3715
+ panel.addEventListener("drop", onPanelDrop);
3716
+ const settleStarterFlows = (flows) => {
3717
+ starterFlows = flows;
3718
+ starterFlowsPending = false;
3719
+ renderWelcome();
3720
+ };
3721
+ let catalogRequested = false;
3722
+ const requestFlowCatalog = () => {
3723
+ if (catalogRequested) return true;
3724
+ const sessionToken = context.getSessionToken?.();
3725
+ if (!sessionToken) return false;
3726
+ catalogRequested = true;
3727
+ void fetchFlowCatalog({
3728
+ apiBase: context.apiBase,
3729
+ sessionToken,
3730
+ sessionId: context.getSessionId?.()
3731
+ }).then((result) => {
3732
+ if (!result.ok) {
3733
+ settleStarterFlows([]);
3734
+ return;
3735
+ }
3736
+ const renderable = renderableFlows(result.flows);
3737
+ settleStarterFlows(result.starterFlows);
3738
+ if (renderable.length === 0) return;
3739
+ flowsButton.style.setProperty("display", "flex");
3740
+ flowsDrawer.setItems(
3741
+ renderable.map((flow) => ({
3742
+ id: flow.cvid,
3743
+ title: flow.publicName,
3744
+ // The operator's own summary, rendered as end-user copy. They
3745
+ // are told this in the dashboard, because a flow named "test
3746
+ // flow 3" now reaches their customers.
3747
+ subtitle: flow.summary
3748
+ }))
3749
+ );
3750
+ }).catch(() => {
3751
+ settleStarterFlows([]);
3752
+ });
3753
+ return true;
3754
+ };
3755
+ if (!requestFlowCatalog()) {
3756
+ try {
3757
+ win?.setTimeout(() => {
3758
+ if (!requestFlowCatalog() && starterFlowsPending) {
3759
+ settleStarterFlows([]);
3760
+ }
3761
+ }, CATALOG_RETRY_MS);
3762
+ } catch {
3763
+ }
3764
+ }
3765
+ const handleKeyDown = (event) => {
3766
+ if (event.key !== "Escape") return;
3767
+ event.stopPropagation();
3768
+ if (historyDrawer.isOpen()) {
3769
+ historyDrawer.close();
3770
+ return;
3771
+ }
3772
+ if (flowsDrawer.isOpen()) {
3773
+ flowsDrawer.close();
3774
+ return;
3775
+ }
3776
+ if (settingsDrawer.isOpen()) {
3777
+ settingsDrawer.close();
3778
+ return;
3779
+ }
3780
+ context.onRequestClose();
3781
+ };
3782
+ panel.addEventListener("keydown", handleKeyDown);
3783
+ const handleResize = () => {
3784
+ if (panel.hasAttribute("data-open")) place(panel, context);
3785
+ fillCard.reposition();
3786
+ };
3787
+ win?.addEventListener("resize", handleResize);
3788
+ let returnFocusTo = null;
3789
+ return {
3790
+ open: openPanel2,
3791
+ close: closePanel2,
3792
+ reposition() {
3793
+ if (panel.hasAttribute("data-open")) place(panel, context);
3794
+ fillCard.reposition();
3795
+ },
3796
+ refreshChrome() {
3797
+ if (!requestFlowCatalog() && starterFlowsPending) {
3798
+ settleStarterFlows([]);
3799
+ }
3800
+ if (!panel.hasAttribute("data-open")) return;
3801
+ applyChrome2();
3802
+ if (settingsDrawer.isOpen()) renderSettings();
3803
+ },
3804
+ destroy() {
3805
+ historyButton.removeEventListener("click", openHistory);
3806
+ settingsButton.removeEventListener("click", openSettings);
3807
+ collapse.removeEventListener("click", handleCollapse);
3808
+ panel.removeEventListener("keydown", handleKeyDown);
3809
+ win?.removeEventListener("resize", handleResize);
3810
+ returnFocusTo = null;
3811
+ settleFill2(null);
3812
+ fillCard.destroy();
3813
+ historyDrawer.destroy();
3814
+ flowsDrawer.destroy();
3815
+ settingsDrawer.destroy();
3816
+ transcript.destroy();
3817
+ for (const item of pending) releasePreview(item);
3818
+ pending = [];
3819
+ chips.remove();
3820
+ context.bar.setAttachmentCount(0);
3821
+ panel.remove();
3822
+ },
3823
+ promptFill(request) {
3824
+ return new Promise((resolve) => {
3825
+ settleFill2(null);
3826
+ context.onRequestClose();
3827
+ fillCard.show(request, { onAnswer: (value) => answerFill2(value) });
3828
+ context.bar.setAwaitingInput?.(true);
3829
+ if (request.kind === "password") {
3830
+ context.bar.setDisabled(true);
3831
+ } else {
3832
+ context.bar.setPlaceholder(request.question);
3833
+ context.bar.focusInput();
3834
+ }
3835
+ pendingFill = { resolve, request };
3836
+ });
3837
+ },
3838
+ cancelFill() {
3839
+ settleFill2(null);
3840
+ },
3841
+ showFlowFailure() {
3842
+ openPanel2();
3843
+ transcript.addMessage("assistant", strings().flowFailed);
3844
+ renderWelcome();
3845
+ }
3846
+ };
3847
+ } catch {
3848
+ return null;
3849
+ }
3850
+ }
3851
+ var registration = { contract: CHAT_CONTRACT, mount };
3852
+ if (typeof window !== "undefined") {
3853
+ try {
3854
+ ;
3855
+ window[CHAT_GLOBAL] = registration;
3856
+ } catch {
3857
+ }
3858
+ }
3859
+
3860
+ export { ARCY_HOME_URL, BODY_CLASS, CATALOG_RETRY_MS, CHAT_CSS, CHIPS_CLASS, CHIP_CLASS, HEADER_BUTTON_CLASS, HEADER_CLASS2 as HEADER_CLASS, HEADER_ICON_CLASS, HEADER_NAME_CLASS, MAIN_CLASS, MARK_CLASS, MARK_LINK_CLASS, PANEL_CLASS, POLICY_CLASS, STARTER_CLASS, STARTER_ROW_CLASS, STRIP_CLASS, WELCOME_CLASS, mount };