arcy.js 0.0.2 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +94 -3
  2. package/dist/arcy.chat.3968644db103.js +1271 -0
  3. package/dist/arcy.flow.fef131718e42.js +3 -0
  4. package/dist/arcy.legacy.b4005932a6cc.js +568 -0
  5. package/dist/arcy.loader.js +2 -0
  6. package/dist/arcy.modern.a51b9a32dd28.js +568 -0
  7. package/dist/arcy.picker.73f47221b73c.js +59 -0
  8. package/dist/chat-BK57WIAA.js +3867 -0
  9. package/dist/chunk-6HBSGVRN.js +219 -0
  10. package/dist/chunk-7VVKR3AO.js +122 -0
  11. package/dist/chunk-CQ2DESAJ.js +263 -0
  12. package/dist/chunk-DKYMIAYF.js +7 -0
  13. package/dist/chunk-FR6SJSDU.js +220 -0
  14. package/dist/chunk-LGWYJSYX.js +271 -0
  15. package/dist/chunk-NIHUDSWK.js +155 -0
  16. package/dist/chunk-NY3NXM2V.js +213 -0
  17. package/dist/design-mode.cjs +135 -0
  18. package/dist/design-mode.d.ts +197 -0
  19. package/dist/design-mode.js +2 -0
  20. package/dist/flow-M7W3H3C5.js +1422 -0
  21. package/dist/fonts/dm-sans-latin-ext.woff2 +0 -0
  22. package/dist/fonts/dm-sans-latin.woff2 +0 -0
  23. package/dist/fonts/ibm-plex-sans-latin-ext.woff2 +0 -0
  24. package/dist/fonts/ibm-plex-sans-latin.woff2 +0 -0
  25. package/dist/fonts/inter-latin-ext.woff2 +0 -0
  26. package/dist/fonts/inter-latin.woff2 +0 -0
  27. package/dist/fonts/lato-latin-ext.woff2 +0 -0
  28. package/dist/fonts/lato-latin.woff2 +0 -0
  29. package/dist/fonts/manrope-latin-ext.woff2 +0 -0
  30. package/dist/fonts/manrope-latin.woff2 +0 -0
  31. package/dist/fonts/montserrat-latin-ext.woff2 +0 -0
  32. package/dist/fonts/montserrat-latin.woff2 +0 -0
  33. package/dist/fonts/nunito-latin-ext.woff2 +0 -0
  34. package/dist/fonts/nunito-latin.woff2 +0 -0
  35. package/dist/fonts/open-sans-latin-ext.woff2 +0 -0
  36. package/dist/fonts/open-sans-latin.woff2 +0 -0
  37. package/dist/fonts/playfair-display-latin-ext.woff2 +0 -0
  38. package/dist/fonts/playfair-display-latin.woff2 +0 -0
  39. package/dist/fonts/poppins-latin-ext.woff2 +0 -0
  40. package/dist/fonts/poppins-latin.woff2 +0 -0
  41. package/dist/fonts/roboto-latin-ext.woff2 +0 -0
  42. package/dist/fonts/roboto-latin.woff2 +0 -0
  43. package/dist/fonts/source-sans-3-latin-ext.woff2 +0 -0
  44. package/dist/fonts/source-sans-3-latin.woff2 +0 -0
  45. package/dist/fonts/work-sans-latin-ext.woff2 +0 -0
  46. package/dist/fonts/work-sans-latin.woff2 +0 -0
  47. package/dist/index.cjs +13848 -0
  48. package/dist/index.d.ts +169 -0
  49. package/dist/index.js +4743 -0
  50. package/dist/picker-JPESE6JJ.js +2134 -0
  51. package/dist/snippet.cjs +46 -0
  52. package/dist/snippet.d.ts +39 -0
  53. package/dist/snippet.js +43 -0
  54. package/package.json +69 -5
  55. package/index.js +0 -5
package/dist/index.js ADDED
@@ -0,0 +1,4743 @@
1
+ import { DESIGN_MODE_PROTOCOL, DESIGN_MODE_EXIT, DESIGN_MODE_SCREENSHOT, DESIGN_MODE_PICK, DESIGN_MODE_READY } from './chunk-7VVKR3AO.js';
2
+ import { FONT_KEY_PROPERTY, FALLBACK_CHROME, readViewport, POLICY_LINK_CLASS, BAR_TRAY_CLASS, ATTACHABLE_TYPES, TOOLTIP_CSS, surfaceWidth, clampOffsetX, dragTransform, carriesFiles, droppedFiles, MOBILE_MARGIN, CHAT_CONTRACT, CHAT_GLOBAL } from './chunk-6HBSGVRN.js';
3
+ import { createShellState, safeStore, browserSessionStorage, writeJson, browserLocalStorage, readJson, hasPendingPreview, createFlowRunState, FLOW_CONTRACT, FLOW_GLOBAL } from './chunk-FR6SJSDU.js';
4
+ import { PICKER_CONTRACT, PICKER_GLOBAL } from './chunk-DKYMIAYF.js';
5
+ import { warn, reportCspViolations, releaseHostFocus, Z_INDEX, createShell, injectStyles } from './chunk-LGWYJSYX.js';
6
+ import { captureEventFingerprint, containsPii } from './chunk-NY3NXM2V.js';
7
+
8
+ /* arcy.js — https://arcyai.com */
9
+
10
+ // src/emitter.ts
11
+ function createEmitter() {
12
+ const handlers = /* @__PURE__ */ new Map();
13
+ function on(event, handler) {
14
+ if (typeof handler !== "function") {
15
+ warn(`on("${event}") needs a function. Ignoring.`);
16
+ return () => {
17
+ };
18
+ }
19
+ let set = handlers.get(event);
20
+ if (!set) {
21
+ set = /* @__PURE__ */ new Set();
22
+ handlers.set(event, set);
23
+ }
24
+ set.add(handler);
25
+ return () => {
26
+ set.delete(handler);
27
+ };
28
+ }
29
+ function emit(event) {
30
+ const set = handlers.get(event);
31
+ if (!set) return;
32
+ for (const handler of [...set]) {
33
+ try {
34
+ handler();
35
+ } catch (error) {
36
+ warn(`A "${event}" handler threw. ${String(error)}`);
37
+ }
38
+ }
39
+ }
40
+ return { on, emit };
41
+ }
42
+
43
+ // src/telemetry/route.ts
44
+ var MAX_ROUTE_LENGTH = 2048;
45
+ var OPAQUE = /^(?:\d+|[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[0-9a-f]{12,})$/i;
46
+ var EMAIL = /^[^@\s/]+@[^@\s/]+\.[^@\s/]+$/;
47
+ function decodeForTest(segment) {
48
+ if (segment.indexOf("%") === -1) return segment;
49
+ try {
50
+ return decodeURIComponent(segment);
51
+ } catch {
52
+ return segment;
53
+ }
54
+ }
55
+ function isIdentifierShaped(segment) {
56
+ if (segment.length === 0) return false;
57
+ if (OPAQUE.test(segment)) return true;
58
+ return EMAIL.test(decodeForTest(segment));
59
+ }
60
+ function scrubPath(path) {
61
+ const scrubbed = path.split("/").map((segment) => isIdentifierShaped(segment) ? ":id" : segment).join("/");
62
+ let trimmed = scrubbed;
63
+ while (trimmed.length > 1 && trimmed.charAt(trimmed.length - 1) === "/") {
64
+ trimmed = trimmed.slice(0, -1);
65
+ }
66
+ return trimmed.length === 0 ? "/" : trimmed;
67
+ }
68
+ function scrubQuery(query) {
69
+ if (query.length === 0) return "";
70
+ const keys = [];
71
+ for (const pair of query.split("&")) {
72
+ if (pair.length === 0) continue;
73
+ const eq = pair.indexOf("=");
74
+ const raw = eq === -1 ? pair : pair.slice(0, eq);
75
+ if (raw.length === 0) continue;
76
+ const key = isIdentifierShaped(raw) ? ":id" : raw;
77
+ if (keys.indexOf(key) === -1) keys.push(key);
78
+ }
79
+ if (keys.length === 0) return "";
80
+ keys.sort();
81
+ return `?${keys.join("&")}`;
82
+ }
83
+ function split(href, base) {
84
+ try {
85
+ const url = new URL(href, base ?? void 0);
86
+ return { path: url.pathname, query: url.search.slice(1) };
87
+ } catch {
88
+ const noHash = href.split("#")[0] ?? "";
89
+ if (!noHash.startsWith("/")) return null;
90
+ const q = noHash.indexOf("?");
91
+ return q === -1 ? { path: noHash, query: "" } : { path: noHash.slice(0, q), query: noHash.slice(q + 1) };
92
+ }
93
+ }
94
+ function normalizeRoute(href, base) {
95
+ if (typeof href !== "string" || href.length === 0) return null;
96
+ const parts = split(href, base);
97
+ if (parts === null) return null;
98
+ const path = scrubPath(parts.path);
99
+ if (!path) return null;
100
+ if (path.length >= MAX_ROUTE_LENGTH) return path.slice(0, MAX_ROUTE_LENGTH);
101
+ const query = scrubQuery(parts.query);
102
+ if (query.length === 0) return path;
103
+ return path.length + query.length > MAX_ROUTE_LENGTH ? path : path + query;
104
+ }
105
+
106
+ // src/picker/notice.ts
107
+ var NOTICE_HOST_ID = "arcy-picker-notice";
108
+ var CARD_OFFSET = [
109
+ ["left", "50%"],
110
+ ["bottom", "24px"]
111
+ ];
112
+ var CARD_STYLE = [
113
+ ["transform", "translateX(-50%)"],
114
+ ["box-sizing", "border-box"],
115
+ ["max-width", "min(520px, calc(100vw - 32px))"],
116
+ ["padding", "14px 16px"],
117
+ ["border-radius", "12px"],
118
+ ["background", "#101828"],
119
+ ["color", "#ffffff"],
120
+ [
121
+ "font",
122
+ '13px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
123
+ ],
124
+ ["box-shadow", "0 8px 24px rgba(16, 24, 40, 0.28)"]
125
+ ];
126
+ var TITLE_STYLE = [
127
+ ["font-weight", "600"],
128
+ ["margin-bottom", "4px"]
129
+ ];
130
+ var BODY_STYLE = [["color", "#d0d5dd"]];
131
+ var BUTTON_STYLE = [
132
+ ["appearance", "none"],
133
+ ["margin-top", "10px"],
134
+ ["border", "1px solid rgba(255, 255, 255, 0.25)"],
135
+ ["border-radius", "999px"],
136
+ ["background", "transparent"],
137
+ ["color", "#ffffff"],
138
+ ["font", "inherit"],
139
+ ["padding", "4px 12px"],
140
+ ["cursor", "pointer"]
141
+ ];
142
+ function style(el, declarations) {
143
+ for (const [property, value] of declarations) {
144
+ el.style.setProperty(property, value);
145
+ }
146
+ }
147
+ var shown = false;
148
+ function showDesignModeNotice(message) {
149
+ if (shown) return;
150
+ let shell = null;
151
+ try {
152
+ shell = createShell({ id: NOTICE_HOST_ID });
153
+ if (!shell) return;
154
+ shown = true;
155
+ const doc = shell.host.ownerDocument;
156
+ const card = doc.createElement("div");
157
+ card.style.setProperty("position", "absolute", "important");
158
+ card.style.setProperty("pointer-events", "auto", "important");
159
+ for (const [property, value] of CARD_OFFSET) {
160
+ card.style.setProperty(property, value, "important");
161
+ }
162
+ style(card, CARD_STYLE);
163
+ const title = doc.createElement("div");
164
+ style(title, TITLE_STYLE);
165
+ title.textContent = "ARCY element picker did not start";
166
+ const body = doc.createElement("div");
167
+ style(body, BODY_STYLE);
168
+ body.textContent = message;
169
+ const dismiss = doc.createElement("button");
170
+ dismiss.type = "button";
171
+ style(dismiss, BUTTON_STYLE);
172
+ dismiss.textContent = "Dismiss";
173
+ const held = shell;
174
+ dismiss.addEventListener("click", () => {
175
+ try {
176
+ held.destroy();
177
+ } catch {
178
+ }
179
+ });
180
+ card.append(title, body, dismiss);
181
+ shell.root.appendChild(card);
182
+ } catch {
183
+ try {
184
+ shell?.destroy();
185
+ } catch {
186
+ }
187
+ }
188
+ }
189
+
190
+ // src/picker/activate.ts
191
+ function refuse(message) {
192
+ warn(message);
193
+ showDesignModeNotice(message);
194
+ }
195
+ var PICKER_PARAM = "arcy_picker";
196
+ var PICKER_VERIFY_PATH = "/api/v1/sdk/tagging/verify";
197
+ var PICKER_LOAD_TIMEOUT_MS = 15e3;
198
+ var VERIFY_TIMEOUT_MS = 1e4;
199
+ function detectPickerNonce(search, hash) {
200
+ return readNonce(search) ?? readNonce(hash);
201
+ }
202
+ function readNonce(source) {
203
+ if (!source) return null;
204
+ try {
205
+ const nonce = new URLSearchParams(
206
+ source.charAt(0) === "#" ? source.slice(1) : source
207
+ ).get(PICKER_PARAM);
208
+ return nonce && nonce.length > 0 ? nonce : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ function stripPickerParam(win) {
214
+ try {
215
+ const url = new URL(win.location.href);
216
+ const inQuery = url.searchParams.has(PICKER_PARAM);
217
+ const inHash = readNonce(url.hash) !== null;
218
+ if (!inQuery && !inHash) return;
219
+ if (inQuery) url.searchParams.delete(PICKER_PARAM);
220
+ if (inHash) url.hash = "";
221
+ win.history.replaceState(win.history.state, "", url.toString());
222
+ } catch {
223
+ }
224
+ }
225
+ async function verifyPickerNonce(options) {
226
+ const {
227
+ apiBase,
228
+ token,
229
+ nonce,
230
+ fetchImpl,
231
+ timeoutMs = VERIFY_TIMEOUT_MS,
232
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms)
233
+ } = options;
234
+ if (!fetchImpl) return null;
235
+ const attempt = async () => {
236
+ try {
237
+ const response = await fetchImpl(`${apiBase}${PICKER_VERIFY_PATH}`, {
238
+ method: "POST",
239
+ headers: {
240
+ "Content-Type": "application/json",
241
+ Authorization: `Bearer ${token}`
242
+ },
243
+ body: JSON.stringify({ token: nonce }),
244
+ credentials: "omit"
245
+ });
246
+ if (!response.ok) return null;
247
+ const body = await response.json();
248
+ if (body?.ok !== true) return null;
249
+ const returnOrigin = normalizeOrigin(body.returnOrigin);
250
+ if (!returnOrigin) {
251
+ refuse(
252
+ "This ARCY server has not been told where its dashboard lives, so a pick would have nowhere to go. Set FRONTEND_DASHBOARD_URL and try again."
253
+ );
254
+ return null;
255
+ }
256
+ return {
257
+ returnOrigin,
258
+ screenshotsEnabled: body.elementScreenshotsEnabled !== false
259
+ };
260
+ } catch {
261
+ return null;
262
+ }
263
+ };
264
+ const deadline = new Promise((resolve) => {
265
+ try {
266
+ setTimeoutImpl(() => resolve(null), timeoutMs);
267
+ } catch {
268
+ }
269
+ });
270
+ return Promise.race([attempt(), deadline]);
271
+ }
272
+ function normalizeOrigin(value) {
273
+ if (typeof value !== "string" || value.length === 0) return null;
274
+ try {
275
+ const { origin } = new URL(value);
276
+ return origin && origin !== "null" ? origin : null;
277
+ } catch {
278
+ return null;
279
+ }
280
+ }
281
+ function currentPath(win) {
282
+ try {
283
+ const path = win.location.pathname;
284
+ if (typeof path !== "string" || !path.startsWith("/")) return "/";
285
+ return scrubPath(path);
286
+ } catch {
287
+ return "/";
288
+ }
289
+ }
290
+ function hasOpener(win) {
291
+ try {
292
+ const opener = win.opener;
293
+ return !!opener && !opener.closed;
294
+ } catch {
295
+ return false;
296
+ }
297
+ }
298
+ function postToOpener(win, returnOrigin, message) {
299
+ try {
300
+ const opener = win.opener;
301
+ if (!opener || opener.closed) {
302
+ warn("The ARCY dashboard window is gone, so that pick could not be sent.");
303
+ return;
304
+ }
305
+ opener.postMessage(message, returnOrigin);
306
+ } catch (error) {
307
+ warn(`That pick could not be sent to the ARCY dashboard. ${String(error)}`);
308
+ }
309
+ }
310
+ async function maybeActivateDesignMode(options) {
311
+ try {
312
+ const {
313
+ win,
314
+ token,
315
+ apiBase,
316
+ fetchImpl,
317
+ onVerified,
318
+ loadChunk,
319
+ loadTimeoutMs = PICKER_LOAD_TIMEOUT_MS,
320
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms)
321
+ } = options;
322
+ const nonce = detectPickerNonce(win.location.search, win.location.hash);
323
+ if (!nonce) return "no_nonce";
324
+ stripPickerParam(win);
325
+ if (!hasOpener(win)) {
326
+ refuse(
327
+ "This page was not opened by the ARCY dashboard, so a picked element would have nowhere to go. If you did start it from the flow builder, this site is severing the link back: a Cross-Origin-Opener-Policy header on this page (helmet sets one by default) does exactly that. Serve this page with Cross-Origin-Opener-Policy: unsafe-none while you author."
328
+ );
329
+ return "no_opener";
330
+ }
331
+ const verified = await verifyPickerNonce({
332
+ apiBase,
333
+ token,
334
+ nonce,
335
+ fetchImpl,
336
+ setTimeoutImpl
337
+ });
338
+ if (!verified) {
339
+ refuse(
340
+ "The design-mode link is invalid, expired, or could not be verified. It is single-use and short-lived, so a reload spends it. Check that this page carries the same environment Token as the flow you are editing, then open the picker again from the dashboard."
341
+ );
342
+ return "rejected";
343
+ }
344
+ try {
345
+ onVerified();
346
+ } catch {
347
+ }
348
+ const registration = await loadChunk(win, loadTimeoutMs, setTimeoutImpl);
349
+ if (!registration) return "load_failed";
350
+ const { returnOrigin, screenshotsEnabled } = verified;
351
+ const handle = registration.mount({
352
+ // 15.10's kill switch: decided before the chunk even loads.
353
+ screenshotsEnabled,
354
+ // Delivered as it happens, with no confirm step (D502): the dashboard
355
+ // field is the confirmation, and a second one would only ever disagree
356
+ // with the first.
357
+ onPick: (target, matchCount) => {
358
+ postToOpener(win, returnOrigin, {
359
+ type: DESIGN_MODE_PICK,
360
+ protocol: DESIGN_MODE_PROTOCOL,
361
+ target,
362
+ // Read now, not at mount: browse mode means the operator may have
363
+ // routed somewhere else since (D504).
364
+ route: currentPath(win),
365
+ // Scored by the chunk against this page's live document at the
366
+ // moment of the pick (ADR 0132). The core cannot compute it: doing
367
+ // so would pull `matchTarget` into the gated bundle.
368
+ matchCount
369
+ });
370
+ },
371
+ // Resolves after the pick above, and separately (15.7): a screenshot
372
+ // must never delay or gate the pick's own delivery (D502).
373
+ onScreenshot: (screenshot) => {
374
+ postToOpener(win, returnOrigin, {
375
+ type: DESIGN_MODE_SCREENSHOT,
376
+ protocol: DESIGN_MODE_PROTOCOL,
377
+ screenshot
378
+ });
379
+ },
380
+ onExit: () => {
381
+ postToOpener(win, returnOrigin, {
382
+ type: DESIGN_MODE_EXIT,
383
+ protocol: DESIGN_MODE_PROTOCOL
384
+ });
385
+ }
386
+ });
387
+ if (!handle) {
388
+ refuse("The element picker could not start. Design mode is off.");
389
+ return "mount_failed";
390
+ }
391
+ postToOpener(win, returnOrigin, {
392
+ type: DESIGN_MODE_READY,
393
+ protocol: DESIGN_MODE_PROTOCOL
394
+ });
395
+ return "active";
396
+ } catch (error) {
397
+ refuse(`Design mode could not start. ${String(error)}`);
398
+ return "load_failed";
399
+ }
400
+ }
401
+
402
+ // src/flow/preview.ts
403
+ var PREVIEW_PARAM = "arcy_preview";
404
+ var PREVIEW_PROTOCOL = 1;
405
+ var PREVIEW_VERIFY_PATH = "/api/v1/sdk/flows/preview/verify";
406
+ var VERIFY_TIMEOUT_MS2 = 1e4;
407
+ function detectPreviewNonce(search, hash) {
408
+ return readNonce2(search) ?? readNonce2(hash);
409
+ }
410
+ function readNonce2(source) {
411
+ if (!source) return null;
412
+ try {
413
+ const nonce = new URLSearchParams(
414
+ source.charAt(0) === "#" ? source.slice(1) : source
415
+ ).get(PREVIEW_PARAM);
416
+ return nonce && nonce.length > 0 ? nonce : null;
417
+ } catch {
418
+ return null;
419
+ }
420
+ }
421
+ function stripPreviewParam(win) {
422
+ try {
423
+ const url = new URL(win.location.href);
424
+ const inQuery = url.searchParams.has(PREVIEW_PARAM);
425
+ const inHash = readNonce2(url.hash) !== null;
426
+ if (!inQuery && !inHash) return;
427
+ if (inQuery) url.searchParams.delete(PREVIEW_PARAM);
428
+ if (inHash) url.hash = "";
429
+ win.history.replaceState(win.history.state, "", url.toString());
430
+ } catch {
431
+ }
432
+ }
433
+ function detectPreviewIntent(win, token) {
434
+ try {
435
+ const nonce = detectPreviewNonce(win.location.search, win.location.hash);
436
+ if (nonce) {
437
+ stripPreviewParam(win);
438
+ return { kind: "nonce", nonce };
439
+ }
440
+ if (hasPendingPreview(token)) return { kind: "pending" };
441
+ return null;
442
+ } catch {
443
+ return null;
444
+ }
445
+ }
446
+ async function verifyPreviewNonce(options) {
447
+ const {
448
+ apiBase,
449
+ token,
450
+ nonce,
451
+ sessionToken,
452
+ userTraits,
453
+ fetchImpl,
454
+ timeoutMs = VERIFY_TIMEOUT_MS2,
455
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms)
456
+ } = options;
457
+ if (!fetchImpl) return null;
458
+ const refuse2 = (message) => {
459
+ warn(message);
460
+ try {
461
+ showDesignModeNotice(message);
462
+ } catch {
463
+ }
464
+ return null;
465
+ };
466
+ const attempt = async () => {
467
+ try {
468
+ const response = await fetchImpl(`${apiBase}${PREVIEW_VERIFY_PATH}`, {
469
+ method: "POST",
470
+ headers: {
471
+ "Content-Type": "application/json",
472
+ Authorization: `Bearer ${token}`
473
+ },
474
+ body: JSON.stringify({
475
+ token: nonce,
476
+ ...sessionToken ? { sessionToken } : {},
477
+ ...userTraits && Object.keys(userTraits).length > 0 ? { userTraits } : {}
478
+ }),
479
+ credentials: "omit"
480
+ });
481
+ if (!response.ok) {
482
+ return refuse2(
483
+ "ARCY could not verify this preview link. Close this tab and press Preview in the dashboard again."
484
+ );
485
+ }
486
+ const body = await response.json();
487
+ if (body?.ok !== true || body.flow === void 0 || body.flow === null) {
488
+ return refuse2(
489
+ "ARCY could not verify this preview link. Close this tab and press Preview in the dashboard again."
490
+ );
491
+ }
492
+ if (body.protocol !== PREVIEW_PROTOCOL) {
493
+ return refuse2(
494
+ "This page runs an older version of arcy.js than the dashboard expects. The preview cannot run until the script updates."
495
+ );
496
+ }
497
+ return body.flow;
498
+ } catch {
499
+ return refuse2(
500
+ "ARCY could not reach its server to start this preview. Check the connection and press Preview again."
501
+ );
502
+ }
503
+ };
504
+ let settled = false;
505
+ let timer;
506
+ const deadline = new Promise((resolve) => {
507
+ try {
508
+ timer = setTimeoutImpl(() => {
509
+ if (settled) return;
510
+ settled = true;
511
+ resolve(
512
+ refuse2(
513
+ "ARCY took too long to verify this preview link. Close this tab and press Preview in the dashboard again."
514
+ )
515
+ );
516
+ }, timeoutMs);
517
+ } catch {
518
+ }
519
+ });
520
+ const result = await Promise.race([
521
+ attempt().then((value) => {
522
+ settled = true;
523
+ return value;
524
+ }),
525
+ deadline
526
+ ]);
527
+ try {
528
+ if (timer !== void 0) clearTimeout(timer);
529
+ } catch {
530
+ }
531
+ return result;
532
+ }
533
+
534
+ // src/session/bootstrap.ts
535
+ var DEFAULT_API_BASE = "https://api.arcyai.com";
536
+ var SESSION_INIT_PATH = "/api/v1/sdk/session/init";
537
+ var RETRY_DELAY_MS = 2e3;
538
+ var ATTEMPT_TIMEOUT_MS = 5e3;
539
+ var FAILURE_COOLDOWN_MS = 3e4;
540
+ var TELEMETRY_STATES = ["ON", "REQUIRE_CONSENT", "OFF"];
541
+ function readConfig(body) {
542
+ if (!body || typeof body !== "object" || Array.isArray(body)) return null;
543
+ const raw = body;
544
+ const state = raw.behavioralTelemetry;
545
+ const customization = raw.effectiveCustomization;
546
+ const widget = raw.widget;
547
+ const identity = raw.identity;
548
+ const identityAccepted = identity && typeof identity === "object" && !Array.isArray(identity) ? identity.accepted : void 0;
549
+ return {
550
+ sessionToken: typeof raw.sessionToken === "string" ? raw.sessionToken : null,
551
+ behavioralTelemetry: typeof state === "string" && TELEMETRY_STATES.indexOf(state) !== -1 ? state : null,
552
+ customization: customization && typeof customization === "object" && !Array.isArray(customization) ? customization : null,
553
+ widget: widget && typeof widget === "object" && !Array.isArray(widget) ? widget : null,
554
+ identityAccepted: typeof identityAccepted === "boolean" ? identityAccepted : null
555
+ };
556
+ }
557
+ function createSessionBootstrap(options) {
558
+ const {
559
+ token,
560
+ apiBase = DEFAULT_API_BASE,
561
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
562
+ now = () => Date.now(),
563
+ retryDelayMs = RETRY_DELAY_MS,
564
+ attemptTimeoutMs = ATTEMPT_TIMEOUT_MS,
565
+ failureCooldownMs = FAILURE_COOLDOWN_MS
566
+ } = options;
567
+ const fetchImpl = options.fetchImpl !== void 0 ? options.fetchImpl : typeof fetch !== "undefined" ? fetch.bind(globalThis) : null;
568
+ const url = `${apiBase}${SESSION_INIT_PATH}`;
569
+ let latest = null;
570
+ let sent = null;
571
+ let failed = null;
572
+ let inFlight = null;
573
+ let sequence = 0;
574
+ function body(payload) {
575
+ const out = {
576
+ publicKey: token,
577
+ sessionId: payload.sessionId
578
+ };
579
+ if (payload.visitorId !== void 0) out.visitorId = payload.visitorId;
580
+ if (payload.userId !== void 0) out.userId = payload.userId;
581
+ if (payload.userHash !== void 0) out.userHash = payload.userHash;
582
+ if (payload.userTraits !== void 0) out.userTraits = payload.userTraits;
583
+ if (payload.uiLanguage !== void 0) out.uiLanguage = payload.uiLanguage;
584
+ if (payload.contentLocale !== void 0)
585
+ out.contentLocale = payload.contentLocale;
586
+ if (payload.viewerLocale !== void 0)
587
+ out.viewerLocale = payload.viewerLocale;
588
+ if (payload.route !== void 0) out.route = payload.route;
589
+ if (payload.sessionContext !== void 0)
590
+ out.sessionContext = payload.sessionContext;
591
+ return JSON.stringify(out);
592
+ }
593
+ async function attempt(json) {
594
+ if (!fetchImpl) return { ok: false, reason: "unusable" };
595
+ let response;
596
+ try {
597
+ response = await fetchImpl(url, {
598
+ method: "POST",
599
+ headers: { "Content-Type": "application/json" },
600
+ body: json,
601
+ credentials: "omit"
602
+ });
603
+ } catch {
604
+ return null;
605
+ }
606
+ if (response.status >= 500) return null;
607
+ if (!response.ok) {
608
+ return { ok: false, reason: "rejected" };
609
+ }
610
+ let parsed;
611
+ try {
612
+ parsed = await response.json();
613
+ } catch {
614
+ return { ok: false, reason: "unusable" };
615
+ }
616
+ const config = readConfig(parsed);
617
+ if (!config) return { ok: false, reason: "unusable" };
618
+ return { ok: true, config };
619
+ }
620
+ function wait(ms) {
621
+ return new Promise((resolve) => {
622
+ try {
623
+ setTimeoutImpl(() => resolve(), ms);
624
+ } catch {
625
+ resolve();
626
+ }
627
+ });
628
+ }
629
+ function bounded(json) {
630
+ return Promise.race([
631
+ attempt(json),
632
+ wait(attemptTimeoutMs).then(() => null)
633
+ ]);
634
+ }
635
+ async function run(json) {
636
+ const first = await bounded(json);
637
+ if (first) return first;
638
+ await wait(retryDelayMs);
639
+ const second = await bounded(json);
640
+ return second ?? { ok: false, reason: "network" };
641
+ }
642
+ return {
643
+ async sync(payload) {
644
+ const json = body(payload);
645
+ if (inFlight && json === sent) return inFlight;
646
+ if (!inFlight && json === sent && latest) {
647
+ return { ok: true, config: latest };
648
+ }
649
+ if (failed && failed.json === json && now() - failed.at < failureCooldownMs) {
650
+ return { ok: false, reason: "network" };
651
+ }
652
+ sent = json;
653
+ const mySequence = ++sequence;
654
+ const request = run(json).then((result) => {
655
+ if (mySequence === sequence) {
656
+ if (result.ok) {
657
+ latest = result.config;
658
+ failed = null;
659
+ } else {
660
+ failed = { json, at: now() };
661
+ if (sent === json) sent = null;
662
+ }
663
+ }
664
+ if (inFlight === request) inFlight = null;
665
+ return result;
666
+ });
667
+ inFlight = request;
668
+ return request;
669
+ },
670
+ config: () => latest
671
+ };
672
+ }
673
+
674
+ // src/shell/chat-loader.ts
675
+ var CHAT_LOAD_TIMEOUT_MS = 15e3;
676
+ function createChatController(options) {
677
+ const {
678
+ root,
679
+ host,
680
+ win = typeof window !== "undefined" ? window : void 0,
681
+ loadChunk,
682
+ loadTimeoutMs = CHAT_LOAD_TIMEOUT_MS,
683
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
684
+ onRequestClose,
685
+ bar,
686
+ apiBase,
687
+ getSessionToken,
688
+ getSessionId,
689
+ getRoute,
690
+ getMaxHeightPercent,
691
+ onStartFlow,
692
+ publicKey,
693
+ getConversationId,
694
+ setConversationId,
695
+ touchConversation,
696
+ getVisitorId,
697
+ onOrgCapExceeded,
698
+ onStopFlow,
699
+ track,
700
+ getWidget,
701
+ onSelectLocale
702
+ } = options;
703
+ const isolated = root !== host;
704
+ let handle = null;
705
+ let loading = null;
706
+ let destroyed = false;
707
+ let wantOpen = false;
708
+ async function ensureMounted() {
709
+ if (handle || destroyed) return;
710
+ if (loading) return loading;
711
+ loading = (async () => {
712
+ if (!win) return;
713
+ try {
714
+ const registration = await loadChunk(win, loadTimeoutMs, setTimeoutImpl);
715
+ if (!registration || destroyed) return;
716
+ const context = {
717
+ root,
718
+ host,
719
+ isolated,
720
+ onRequestClose,
721
+ bar,
722
+ apiBase,
723
+ getSessionToken,
724
+ getSessionId,
725
+ getRoute,
726
+ getMaxHeightPercent,
727
+ onStartFlow,
728
+ publicKey,
729
+ getConversationId,
730
+ setConversationId,
731
+ touchConversation,
732
+ getVisitorId,
733
+ onOrgCapExceeded,
734
+ onStopFlow,
735
+ track,
736
+ getWidget,
737
+ onSelectLocale
738
+ };
739
+ handle = registration.mount(context);
740
+ if (!handle) warn("The chat panel could not start.");
741
+ } catch (error) {
742
+ warn(`The chat panel could not load. ${String(error)}`);
743
+ } finally {
744
+ loading = null;
745
+ }
746
+ })();
747
+ return loading;
748
+ }
749
+ return {
750
+ async open() {
751
+ if (destroyed) return;
752
+ wantOpen = true;
753
+ await ensureMounted();
754
+ if (!wantOpen || destroyed) return;
755
+ try {
756
+ handle?.open();
757
+ } catch (error) {
758
+ warn(`The chat panel could not open. ${String(error)}`);
759
+ }
760
+ },
761
+ close() {
762
+ wantOpen = false;
763
+ try {
764
+ handle?.close();
765
+ } catch (error) {
766
+ warn(`The chat panel could not close. ${String(error)}`);
767
+ }
768
+ },
769
+ destroy() {
770
+ destroyed = true;
771
+ wantOpen = false;
772
+ const current = handle;
773
+ handle = null;
774
+ try {
775
+ current?.destroy();
776
+ } catch (error) {
777
+ warn(`The chat panel could not be removed. ${String(error)}`);
778
+ }
779
+ },
780
+ reposition() {
781
+ try {
782
+ handle?.reposition?.();
783
+ } catch (error) {
784
+ warn(`The chat panel could not move. ${String(error)}`);
785
+ }
786
+ },
787
+ refreshChrome() {
788
+ try {
789
+ handle?.refreshChrome?.();
790
+ } catch (error) {
791
+ warn(`The chat panel could not apply its texts. ${String(error)}`);
792
+ }
793
+ },
794
+ async promptFill(request) {
795
+ if (destroyed) return null;
796
+ await ensureMounted();
797
+ if (destroyed || !handle?.promptFill) return null;
798
+ try {
799
+ return await handle.promptFill(request);
800
+ } catch (error) {
801
+ warn(`The flow's question could not be shown. ${String(error)}`);
802
+ return null;
803
+ }
804
+ },
805
+ cancelFill() {
806
+ try {
807
+ handle?.cancelFill?.();
808
+ } catch (error) {
809
+ warn(`The flow's question could not be withdrawn. ${String(error)}`);
810
+ }
811
+ },
812
+ async showFlowFailure() {
813
+ if (destroyed) return;
814
+ wantOpen = true;
815
+ await ensureMounted();
816
+ if (!wantOpen || destroyed || !handle?.showFlowFailure) return;
817
+ try {
818
+ handle.showFlowFailure();
819
+ } catch (error) {
820
+ warn(`The flow's failure notice could not be shown. ${String(error)}`);
821
+ }
822
+ }
823
+ };
824
+ }
825
+
826
+ // src/flow/flow-loader.ts
827
+ var FLOW_LOAD_TIMEOUT_MS = 15e3;
828
+ function createFlowController(options) {
829
+ const {
830
+ win = typeof window !== "undefined" ? window : void 0,
831
+ loadChunk,
832
+ loadTimeoutMs = FLOW_LOAD_TIMEOUT_MS,
833
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
834
+ context
835
+ } = options;
836
+ let handle = null;
837
+ let loading = null;
838
+ let destroyed = false;
839
+ async function ensureMounted() {
840
+ if (handle || destroyed) return;
841
+ if (loading) return loading;
842
+ loading = (async () => {
843
+ if (!win) return;
844
+ try {
845
+ const registration = await loadChunk(win, loadTimeoutMs, setTimeoutImpl);
846
+ if (!registration || destroyed) return;
847
+ handle = registration.mount({ ...context, doc: win.document, win });
848
+ if (!handle) warn("The flow engine could not start.");
849
+ } catch (error) {
850
+ warn(`The flow engine could not load. ${String(error)}`);
851
+ } finally {
852
+ loading = null;
853
+ }
854
+ })();
855
+ return loading;
856
+ }
857
+ return {
858
+ async start(flowCvid) {
859
+ if (destroyed) return;
860
+ await ensureMounted();
861
+ if (destroyed) return;
862
+ try {
863
+ await handle?.start(flowCvid);
864
+ } catch (error) {
865
+ warn(`The flow could not start. ${String(error)}`);
866
+ }
867
+ },
868
+ async resume() {
869
+ if (destroyed) return false;
870
+ const pending = createFlowRunState(context.token).read();
871
+ if (!pending) return false;
872
+ await ensureMounted();
873
+ if (destroyed || !handle) return false;
874
+ try {
875
+ return await handle.resume();
876
+ } catch (error) {
877
+ warn(`The flow could not resume. ${String(error)}`);
878
+ return false;
879
+ }
880
+ },
881
+ async startPreview(flow) {
882
+ if (destroyed) return;
883
+ await ensureMounted();
884
+ if (destroyed) return;
885
+ try {
886
+ await handle?.startPreview?.(flow);
887
+ } catch (error) {
888
+ warn(`The preview could not start. ${String(error)}`);
889
+ }
890
+ },
891
+ async resumePreview() {
892
+ if (destroyed) return false;
893
+ if (!hasPendingPreview(context.token)) return false;
894
+ await ensureMounted();
895
+ if (destroyed || !handle?.resumePreview) return false;
896
+ try {
897
+ return await handle.resumePreview();
898
+ } catch (error) {
899
+ warn(`The preview could not resume. ${String(error)}`);
900
+ return false;
901
+ }
902
+ },
903
+ stop() {
904
+ if (destroyed) return;
905
+ try {
906
+ handle?.stop?.();
907
+ } catch (error) {
908
+ warn(`The flow could not be stopped. ${String(error)}`);
909
+ }
910
+ },
911
+ destroy() {
912
+ destroyed = true;
913
+ const current = handle;
914
+ handle = null;
915
+ try {
916
+ current?.destroy();
917
+ } catch (error) {
918
+ warn(`The flow engine could not be removed. ${String(error)}`);
919
+ }
920
+ }
921
+ };
922
+ }
923
+
924
+ // src/shell/styles.ts
925
+ var BAR_CSS = TOOLTIP_CSS + `
926
+ .arcy-bar-wrap {
927
+ gap: 6px;
928
+ font: 14px/1.4 var(--_arcy-font, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif);
929
+ /* Only ever animates from a concrete pixel value: bar.ts's render()
930
+ freezes the wrap's current width before writing the target so this has
931
+ two real numbers to interpolate between, never the "max-content"
932
+ keyword most engines cannot transition. */
933
+ transition: width 220ms cubic-bezier(0.4, 0, 0.2, 1);
934
+ /* Above the panel, always (.arcy-chat-panel's own z-index in chat.ts):
935
+ both are position: absolute siblings inside the same shared root with
936
+ nothing else to order them, so without this the panel simply painted
937
+ over the bar whenever they overlapped, because the panel is always the
938
+ later of the two to mount (it loads lazily, on first open()) and DOM
939
+ order was the only tiebreaker. This is its own stacking context, so the
940
+ bar's own menu and tooltips (z-index 2 inside it, tooltip.ts) stay
941
+ above the panel too, not just the bar's own box. */
942
+ z-index: 2;
943
+ }
944
+
945
+ /* The corner launcher the widget becomes when a visitor docks it from the
946
+ panel's Settings sheet. Its size and placement are written inline from
947
+ bar.ts, for the reason at the top of this file: docked, this button is the
948
+ only way back into the widget, so losing this sheet has to leave it plain
949
+ rather than unfindable.
950
+
951
+ It is the WELCOME MARK: same size token, same brand colours, same
952
+ overflow-hidden circle an upload is inset into. The welcome screen's own
953
+ rule, in chat.ts, is its twin, and the two are meant to be
954
+ indistinguishable, because docked this button is that mark. */
955
+ .arcy-bar-dock {
956
+ overflow: hidden;
957
+ appearance: none;
958
+ border: 0;
959
+ /* box-sizing: border-box comes from bar.ts's SURVIVAL_STYLE. calc() against
960
+ the size token itself, NOT a percentage: a percentage padding resolves
961
+ against the containing block's width, not this button's own, and would
962
+ inflate a 34px button to match the page instead of insetting the icon. */
963
+ padding: calc(var(--_arcy-chat-welcome-icon-size, 34px) * 0.15);
964
+ cursor: pointer;
965
+ border-radius: 999px;
966
+ background: var(--_arcy-brand-bg, #101828);
967
+ color: var(--_arcy-brand-text, #ffffff);
968
+ -webkit-tap-highlight-color: transparent;
969
+ /* Its own stacking context, above the panel, same as the bar's wrap. */
970
+ z-index: 2;
971
+ transition: transform 160ms cubic-bezier(0.4, 0, 0.2, 1);
972
+ }
973
+
974
+ .arcy-bar-dock:hover {
975
+ transform: scale(1.06);
976
+ }
977
+
978
+ .arcy-bar-dock:focus-visible {
979
+ outline: 2px solid currentColor;
980
+ outline-offset: 3px;
981
+ }
982
+
983
+ .arcy-bar {
984
+ position: relative;
985
+ display: flex;
986
+ align-items: center;
987
+ box-sizing: border-box;
988
+ width: 100%;
989
+ min-height: 52px;
990
+ padding: 0 8px 0 8px;
991
+ border-radius: var(--arcy-bar-radius, 16px);
992
+ background: var(--_arcy-main-bg, #ffffff);
993
+ color: var(--_arcy-main-text, #101828);
994
+ /* The shared base.border (D832): one value, this edge and the panel's. */
995
+ border: var(--_arcy-border-width, 0px) solid
996
+ var(--_arcy-border-color, color-mix(in srgb, var(--_arcy-main-text, #101828) 15%, transparent));
997
+ /* The focus ring rides in front of the operator's own shadow rather than
998
+ replacing it (D908), so a focused bar keeps whatever depth they chose. */
999
+ box-shadow: var(--_arcy-bar-focus-ring, 0 0 #0000),
1000
+ var(--arcy-bar-shadow, 0 8px 24px rgba(16, 24, 40, 0.28));
1001
+ transition: box-shadow 160ms ease, background 160ms ease,
1002
+ border-color 160ms ease;
1003
+ }
1004
+
1005
+ /* Flow mode's status surface reads left to right (dot, name, step, stop),
1006
+ so the left edge sits closer to the composer's usual text inset while the
1007
+ right stays flush with the stop control (D930/D979). */
1008
+ .arcy-bar[data-mode="flow"] {
1009
+ padding: 0 8px 0 16px;
1010
+ }
1011
+
1012
+ /* Glassiness (\xA75.4): a blur behind the bar plus an alpha on
1013
+ main.background. Only where the browser can blur; elsewhere the bar stays
1014
+ opaque, never transparent. With no glass set the alpha is 0% and this is
1015
+ a no-op. */
1016
+ @supports (backdrop-filter: blur(1px)) {
1017
+ .arcy-bar {
1018
+ background: color-mix(in srgb, var(--_arcy-main-bg, #ffffff) calc(100% - var(--_arcy-glass-alpha, 0%)), transparent);
1019
+ backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
1020
+ -webkit-backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
1021
+ }
1022
+ }
1023
+
1024
+ .arcy-bar-form {
1025
+ display: flex;
1026
+ align-items: center;
1027
+ gap: 6px;
1028
+ flex: 1 1 auto;
1029
+ min-width: 0;
1030
+ margin: 0;
1031
+ }
1032
+
1033
+ .arcy-bar-input {
1034
+ flex: 1 1 auto;
1035
+ min-width: 0;
1036
+ appearance: none;
1037
+ border: 0;
1038
+ background: transparent;
1039
+ color: inherit;
1040
+ font: inherit;
1041
+ padding: 12px 0;
1042
+ outline: none;
1043
+ }
1044
+
1045
+ /* Collapsed, the whole pill is the target (ADR 0103): the input inside it is
1046
+ a few pixels wide, and a visitor aiming for it is a visitor who misses. */
1047
+ .arcy-bar[data-collapsed="true"] {
1048
+ cursor: pointer;
1049
+ }
1050
+
1051
+ .arcy-bar[data-collapsed="true"] .arcy-bar-input {
1052
+ /* No cursor override here, unlike the pill above: this is a real,
1053
+ focusable input, not a decoration, and hovering directly over it
1054
+ should give the same text-caret cue any input gives. The surrounding
1055
+ pill's own pointer cursor still covers the rest of the target. */
1056
+ /* Takes the row's spare width, exactly as it does open (ADR 0149): the
1057
+ pill is bar.collapsedWidth wide now rather than as wide as its own
1058
+ text, and an input that kept sizing itself to the placeholder would
1059
+ leave the operator's extra pixels stranded behind the send control. */
1060
+ padding-right: 8px;
1061
+ }
1062
+
1063
+ /* The left slot. As a mark it is not a control, so it takes no hover, no
1064
+ pointer and no tab stop; as the menu's plus it is an icon button. */
1065
+ .arcy-bar-grip {
1066
+ /* Outside the composer's own controls: what it moves is the whole bar, so
1067
+ it takes neither the left slot's hover treatment nor its padding
1068
+ (ADR 0111). Rendered on BOTH widths of the bar, resting and expanded,
1069
+ and only when the operator has allowed it: the data-grip rule below is
1070
+ driven by that setting alone. The handle used to vanish the moment the panel
1071
+ opened even though dragging the open bar already worked underneath,
1072
+ which is a control that is functional but invisible. */
1073
+ display: none;
1074
+ align-items: center;
1075
+ justify-content: center;
1076
+ flex: 0 0 auto;
1077
+ padding: 0 2px 0 4px;
1078
+ margin-right: -2px;
1079
+ border: 0;
1080
+ background: transparent;
1081
+ /* Matches the leading icon's own resting contrast (see .arcy-bar-leading
1082
+ below). The previous, much lower value read as barely-there against
1083
+ either theme, which made the handle hard to spot rather than subtle. */
1084
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 65%, transparent);
1085
+ cursor: grab;
1086
+ touch-action: none;
1087
+ -webkit-tap-highlight-color: transparent;
1088
+ }
1089
+
1090
+ .arcy-bar[data-grip="true"] .arcy-bar-grip {
1091
+ display: flex;
1092
+ }
1093
+
1094
+ .arcy-bar-grip:active {
1095
+ cursor: grabbing;
1096
+ }
1097
+
1098
+ .arcy-bar-grip:hover {
1099
+ color: var(--_arcy-main-text, #101828);
1100
+ }
1101
+
1102
+ .arcy-bar-leading {
1103
+ appearance: none;
1104
+ border: 0;
1105
+ padding: 8px;
1106
+ border-radius: 999px;
1107
+ background: transparent;
1108
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 65%, transparent);
1109
+ font: inherit;
1110
+ display: flex;
1111
+ align-items: center;
1112
+ justify-content: center;
1113
+ flex: 0 0 auto;
1114
+ -webkit-tap-highlight-color: transparent;
1115
+ }
1116
+
1117
+ .arcy-bar-leading[data-role="menu"] {
1118
+ cursor: pointer;
1119
+ }
1120
+
1121
+ .arcy-bar-leading[data-role="menu"]:hover {
1122
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1123
+ }
1124
+
1125
+ .arcy-bar-leading[data-role="mark"] {
1126
+ cursor: inherit;
1127
+ color: var(--_arcy-brand-bg, #101828);
1128
+ opacity: 1;
1129
+ }
1130
+
1131
+ .arcy-bar-menu {
1132
+ position: absolute;
1133
+ left: 0;
1134
+ bottom: calc(100% + 8px);
1135
+ z-index: 1;
1136
+ display: flex;
1137
+ flex-direction: column;
1138
+ gap: 2px;
1139
+ min-width: 200px;
1140
+ padding: 6px;
1141
+ border-radius: 12px;
1142
+ background: var(--_arcy-main-bg, #ffffff);
1143
+ color: var(--_arcy-main-text, #101828);
1144
+ border: 1px solid
1145
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 12%, transparent);
1146
+ box-shadow: 0 12px 32px rgba(16, 24, 40, 0.18);
1147
+ }
1148
+
1149
+ /* The menu is a surface the visitor sees, so it wears base.glassiness like
1150
+ the bar and the panel do (D832's one-value rule, one layer out). It gets
1151
+ its own @supports block rather than joining the bar's above, because this
1152
+ sheet declares the menu's opaque background AFTER that block: sharing it
1153
+ would put the two rules at equal specificity with the opaque one last,
1154
+ and the menu stayed white on a glassy bar.
1155
+
1156
+ Note this sheet is a template literal. No backticks in these comments. */
1157
+ @supports (backdrop-filter: blur(1px)) {
1158
+ .arcy-bar-menu {
1159
+ background: color-mix(in srgb, var(--_arcy-main-bg, #ffffff) calc(100% - var(--_arcy-glass-alpha, 0%)), transparent);
1160
+ backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
1161
+ -webkit-backdrop-filter: blur(var(--_arcy-glass-blur, 0px));
1162
+ }
1163
+ }
1164
+
1165
+ .arcy-bar-menu-item {
1166
+ appearance: none;
1167
+ border: 0;
1168
+ cursor: pointer;
1169
+ font: inherit;
1170
+ text-align: left;
1171
+ display: flex;
1172
+ align-items: center;
1173
+ gap: 10px;
1174
+ padding: 8px 10px;
1175
+ border-radius: 8px;
1176
+ background: transparent;
1177
+ color: inherit;
1178
+ -webkit-tap-highlight-color: transparent;
1179
+ }
1180
+
1181
+ .arcy-bar-menu-item:hover {
1182
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1183
+ }
1184
+
1185
+ .arcy-bar-input::placeholder {
1186
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 55%, transparent);
1187
+ /* Opera and older Firefox dim placeholders by default, which on a light
1188
+ bar reads as a disabled input. */
1189
+ opacity: 1;
1190
+ }
1191
+
1192
+ .arcy-bar-input:disabled {
1193
+ opacity: 0.6;
1194
+ }
1195
+
1196
+ .arcy-bar-icon,
1197
+ .arcy-bar-send,
1198
+ .arcy-bar-stop {
1199
+ appearance: none;
1200
+ border: 0;
1201
+ cursor: pointer;
1202
+ font: inherit;
1203
+ -webkit-tap-highlight-color: transparent;
1204
+ display: flex;
1205
+ align-items: center;
1206
+ justify-content: center;
1207
+ flex: 0 0 auto;
1208
+ }
1209
+
1210
+ .arcy-bar-icon {
1211
+ padding: 8px;
1212
+ border-radius: 999px;
1213
+ background: transparent;
1214
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 65%, transparent);
1215
+ }
1216
+
1217
+ .arcy-bar-icon:hover {
1218
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1219
+ }
1220
+
1221
+ .arcy-bar-send {
1222
+ padding: 10px;
1223
+ border-radius: 999px;
1224
+ background: var(--_arcy-brand-bg, #101828);
1225
+ color: var(--_arcy-brand-text, #ffffff);
1226
+ }
1227
+
1228
+ /* The send control answers the pointer whatever colour the operator chose
1229
+ (D888). The hover token defaults to the base colour, so a background swap
1230
+ alone is invisible on every theme that did not set one; a lift and a
1231
+ contact shadow read on any colour, including white on white. */
1232
+ .arcy-bar-send {
1233
+ transition: background 120ms ease, transform 120ms ease, box-shadow 120ms ease;
1234
+ }
1235
+
1236
+ .arcy-bar-send:hover {
1237
+ background: var(--_arcy-brand-bg-hover, var(--_arcy-brand-bg, #101828));
1238
+ transform: translateY(-1px);
1239
+ box-shadow: 0 2px 8px rgba(16, 24, 40, 0.18);
1240
+ }
1241
+
1242
+ .arcy-bar-send:active {
1243
+ background: var(--_arcy-brand-bg-active, var(--_arcy-brand-bg, #101828));
1244
+ transform: translateY(0);
1245
+ box-shadow: none;
1246
+ }
1247
+
1248
+ @media (prefers-reduced-motion: reduce) {
1249
+ .arcy-bar-send {
1250
+ transition: background 120ms ease;
1251
+ }
1252
+ .arcy-bar-send:hover,
1253
+ .arcy-bar-send:active {
1254
+ transform: none;
1255
+ }
1256
+ }
1257
+
1258
+ .arcy-bar-send:disabled,
1259
+ .arcy-bar-icon:disabled {
1260
+ cursor: default;
1261
+ opacity: 0.6;
1262
+ }
1263
+
1264
+ /* The input takes NO ring of its own (D887). It fills a bar that already
1265
+ looks like an input, so a rectangle drawn around the text reads as a
1266
+ browser artefact rather than as focus, and it was the first thing a
1267
+ visitor saw on clicking. The bar carries the state instead.
1268
+
1269
+ The border goes brand (D908). base.border.width defaults to 0, so a
1270
+ colour change alone would be invisible on most themes; the 1px ring is
1271
+ what actually paints the edge there, and on a theme that HAS a border the
1272
+ two land on the same line. */
1273
+ .arcy-bar:focus-within {
1274
+ outline: none;
1275
+ border-color: var(--_arcy-brand-bg, #101828);
1276
+ --_arcy-bar-focus-ring: 0 0 0 1px var(--_arcy-brand-bg, #101828);
1277
+ }
1278
+
1279
+ /* A control inside the bar still says which control it is: the bar's ring
1280
+ says "the widget has focus", this one says "and it is this button". */
1281
+ .arcy-bar-icon:focus-visible,
1282
+ .arcy-bar-send:focus-visible,
1283
+ .arcy-bar-stop:focus-visible,
1284
+ .arcy-bar-leading:focus-visible,
1285
+ .arcy-bar-menu-item:focus-visible {
1286
+ outline: 2px solid var(--_arcy-main-text, #101828);
1287
+ outline-offset: 1px;
1288
+ }
1289
+
1290
+ /* The attachment tray. See bar.ts's renderTray(). */
1291
+ .arcy-bar-tray {
1292
+ display: none;
1293
+ flex-wrap: wrap;
1294
+ align-items: center;
1295
+ gap: 6px;
1296
+ padding: 10px 4px 2px 4px;
1297
+ min-width: 0;
1298
+ }
1299
+
1300
+ /* A composer holding pictures is two rows, not one. */
1301
+ .arcy-bar[data-attachments="true"] {
1302
+ flex-direction: column;
1303
+ align-items: stretch;
1304
+ }
1305
+
1306
+ /* \u2500\u2500 Flow-running mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
1307
+
1308
+ .arcy-bar-status {
1309
+ display: flex;
1310
+ align-items: center;
1311
+ gap: 10px;
1312
+ flex: 1 1 auto;
1313
+ min-width: 0;
1314
+ }
1315
+
1316
+ .arcy-bar-dot {
1317
+ flex: 0 0 auto;
1318
+ width: 8px;
1319
+ height: 8px;
1320
+ border-radius: 999px;
1321
+ background: var(--_arcy-brand-bg, #101828);
1322
+ animation: arcy-bar-pulse 1.4s ease-in-out infinite;
1323
+ }
1324
+
1325
+ @keyframes arcy-bar-pulse {
1326
+ 0%, 100% { opacity: 1; }
1327
+ 50% { opacity: 0.35; }
1328
+ }
1329
+
1330
+ .arcy-bar-flow-name {
1331
+ flex: 1 1 auto;
1332
+ min-width: 0;
1333
+ overflow: hidden;
1334
+ text-overflow: ellipsis;
1335
+ white-space: nowrap;
1336
+ font-size: 13px;
1337
+ }
1338
+
1339
+ .arcy-bar-flow-step {
1340
+ flex: 0 0 auto;
1341
+ font-size: 12px;
1342
+ font-variant-numeric: tabular-nums;
1343
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 60%, transparent);
1344
+ }
1345
+
1346
+ .arcy-bar-stop {
1347
+ gap: 6px;
1348
+ padding: 6px 12px;
1349
+ border-radius: 999px;
1350
+ font-size: 12px;
1351
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1352
+ color: inherit;
1353
+ }
1354
+
1355
+ .arcy-bar-stop:hover {
1356
+ background: color-mix(in srgb, var(--_arcy-main-text, #101828) 14%, transparent);
1357
+ }
1358
+
1359
+ /* ADR 0169: the combined input rendering. The status row sits on a slim top
1360
+ row and the composer stays live beneath it, so a fill question can be
1361
+ answered without losing sight of the run. */
1362
+ .arcy-bar[data-mode="input"] {
1363
+ flex-direction: column;
1364
+ align-items: stretch;
1365
+ padding: 6px 8px 0 8px;
1366
+ }
1367
+
1368
+ .arcy-bar[data-mode="input"] .arcy-bar-status {
1369
+ flex: 0 0 auto;
1370
+ padding: 2px 8px 4px 8px;
1371
+ border-bottom: 1px solid
1372
+ color-mix(in srgb, var(--_arcy-main-text, #101828) 8%, transparent);
1373
+ }
1374
+
1375
+ .arcy-bar[data-mode="input"] .arcy-bar-form {
1376
+ padding-left: 8px;
1377
+ }
1378
+
1379
+ /* D1296: the done state. The run is over, so the dot stops pulsing and the
1380
+ check mark takes the counter's place. */
1381
+ .arcy-bar[data-phase="done"] .arcy-bar-dot {
1382
+ animation: none;
1383
+ opacity: 1;
1384
+ }
1385
+
1386
+ .arcy-bar[data-phase="done"] .arcy-bar-flow-step {
1387
+ color: var(--_arcy-main-text, #101828);
1388
+ font-size: 14px;
1389
+ }
1390
+
1391
+ /* The AI disclosure. Under the bar, and only while the panel is open: a
1392
+ quiet page stays quiet. */
1393
+ .arcy-bar-disclaimer {
1394
+ font-size: 10.5px;
1395
+ text-align: center;
1396
+ color: color-mix(in srgb, var(--_arcy-main-text, #101828) 55%, transparent);
1397
+ /* The wrap is centred on the bar, so this must not be able to widen it. */
1398
+ max-width: 100%;
1399
+ }
1400
+
1401
+ /* The privacy-policy link inline in the bar disclosure (D979/D981-followup).
1402
+ Underlined so it reads as clickable against text that otherwise is not. */
1403
+ .arcy-chat-policy {
1404
+ color: inherit;
1405
+ text-decoration: underline;
1406
+ text-underline-offset: 2px;
1407
+ cursor: pointer;
1408
+ }
1409
+
1410
+ .arcy-chat-policy:hover {
1411
+ color: var(--_arcy-main-text, #101828);
1412
+ }
1413
+
1414
+ @media (prefers-reduced-motion: reduce) {
1415
+ .arcy-bar,
1416
+ .arcy-bar-wrap,
1417
+ .arcy-bar-dock,
1418
+ .arcy-bar-dot {
1419
+ transition: none;
1420
+ animation: none;
1421
+ }
1422
+ }
1423
+ `;
1424
+
1425
+ // src/shell/bar.ts
1426
+ var MAX_INPUT_LENGTH = 1e4;
1427
+ var BAR_HEIGHT = 52;
1428
+ var DEFAULT_BOTTOM_OFFSET = 24;
1429
+ var DEFAULT_BAR_ICON_SIZE = 18;
1430
+ var DEFAULT_WIDTH_PERCENT = 30;
1431
+ var DEFAULT_COLLAPSED_WIDTH = 280;
1432
+ var DOCK_SIZE_CSS = "var(--_arcy-chat-welcome-icon-size, 34px)";
1433
+ var DOCK_GLYPH_SHARE = "53%";
1434
+ var DOCK_RIGHT_OFFSET = 24;
1435
+ var SVG_NS = "http://www.w3.org/2000/svg";
1436
+ var SURVIVAL_STYLE = [
1437
+ ["box-sizing", "border-box"],
1438
+ ["margin", "0"],
1439
+ ["display", "flex"]
1440
+ ];
1441
+ var SURVIVAL_IMPORTANT = [
1442
+ ["position", "absolute"],
1443
+ ["pointer-events", "auto"]
1444
+ ];
1445
+ var REVEAL_FADE_MS = 260;
1446
+ var REVEAL_AFTER_LOAD_MS = 300;
1447
+ var REVEAL_SETTLED_MS = 150;
1448
+ var REVEAL_MAX_MS = 3e3;
1449
+ function createIcon(doc, path, size2, strokeWidth = 2) {
1450
+ const svg = doc.createElementNS(SVG_NS, "svg");
1451
+ svg.setAttribute("viewBox", "0 0 24 24");
1452
+ svg.setAttribute("aria-hidden", "true");
1453
+ svg.setAttribute("width", String(size2));
1454
+ svg.setAttribute("height", String(size2));
1455
+ const node = doc.createElementNS(SVG_NS, "path");
1456
+ node.setAttribute("d", path);
1457
+ node.setAttribute("fill", "none");
1458
+ node.setAttribute("stroke", "currentColor");
1459
+ node.setAttribute("stroke-width", String(strokeWidth));
1460
+ node.setAttribute("stroke-linecap", "round");
1461
+ node.setAttribute("stroke-linejoin", "round");
1462
+ svg.appendChild(node);
1463
+ return svg;
1464
+ }
1465
+ function createImageIcon(doc, url, size2) {
1466
+ const img = doc.createElement("img");
1467
+ img.src = url;
1468
+ img.alt = "";
1469
+ img.setAttribute("aria-hidden", "true");
1470
+ img.width = size2;
1471
+ img.height = size2;
1472
+ img.style.setProperty("width", `${size2}px`);
1473
+ img.style.setProperty("height", `${size2}px`);
1474
+ img.style.setProperty("object-fit", "contain");
1475
+ return img;
1476
+ }
1477
+ var ATTACH_PATH = "M5 3H19A2 2 0 0 1 21 5V19A2 2 0 0 1 19 21H5A2 2 0 0 1 3 19V5A2 2 0 0 1 5 3ZM7 9a2 2 0 1 0 4 0a2 2 0 1 0-4 0M21 15l-3.086-3.086a2 2 0 0 0-2.828 0L6 21";
1478
+ var ZERO_OFFSET = { x: 0, y: 0 };
1479
+ var PLUS_PATH = "M12 5v14M5 12h14";
1480
+ var GRIP_PATH = "M8 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0";
1481
+ var SPARKLES_PATH = "M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594zM20 2v4M22 4h-4M4 20m-2 0a2 2 0 1 0 4 0a2 2 0 1 0-4 0";
1482
+ 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";
1483
+ var SEND_PATH = "M12 19V5M5 12l7-7 7 7";
1484
+ var STOP_PATH = "M6 6h12v12H6z";
1485
+ function safe(label, fn) {
1486
+ return (...args) => {
1487
+ try {
1488
+ fn(...args);
1489
+ } catch (error) {
1490
+ warn(`${label} failed. ${String(error)}`);
1491
+ }
1492
+ };
1493
+ }
1494
+ var DEFAULT_LABELS = {
1495
+ send: "Send",
1496
+ attachMenu: "Add",
1497
+ attachImage: "Attach an image",
1498
+ flowStop: "Stop",
1499
+ openChat: "Open chat",
1500
+ moveBar: "Move the chat bar",
1501
+ flowStep: "Step {step} of {total}"
1502
+ };
1503
+ function createBar(options = {}) {
1504
+ let labels = { ...DEFAULT_LABELS, ...options.labels ?? {} };
1505
+ let geometry = {
1506
+ widthPercent: options.geometry?.widthPercent ?? DEFAULT_WIDTH_PERCENT,
1507
+ bottomOffset: options.geometry?.bottomOffset ?? DEFAULT_BOTTOM_OFFSET,
1508
+ collapsedWidth: options.geometry?.collapsedWidth ?? DEFAULT_COLLAPSED_WIDTH
1509
+ };
1510
+ let shell = null;
1511
+ let revealed = false;
1512
+ let wrap = null;
1513
+ let bar = null;
1514
+ let form = null;
1515
+ let input = null;
1516
+ let leading = null;
1517
+ let leadingIcon = null;
1518
+ let iconUrl = options.iconUrl ?? null;
1519
+ let iconSize = options.iconSize ?? DEFAULT_BAR_ICON_SIZE;
1520
+ let welcomeIconUrl = options.welcomeIconUrl ?? null;
1521
+ let failedWelcomeIconUrl = null;
1522
+ let grip = null;
1523
+ let draggable = false;
1524
+ let dock = null;
1525
+ let dockIcon = null;
1526
+ let docked = false;
1527
+ let offset = { x: 0, y: 0 };
1528
+ let menu = null;
1529
+ let attach = null;
1530
+ let picker = null;
1531
+ let submit = null;
1532
+ let status = null;
1533
+ let statusName = null;
1534
+ let statusStep = null;
1535
+ let stop = null;
1536
+ let disclaimer = null;
1537
+ let tray = null;
1538
+ let mode = "idle";
1539
+ let flow = null;
1540
+ let awaitingInput = false;
1541
+ let attachEnabled = false;
1542
+ let attachmentCount = 0;
1543
+ let menuOpen = false;
1544
+ let gestureActivated = false;
1545
+ let vp = { width: 0, height: 0, isMobile: false, keyboardOffset: 0 };
1546
+ function isCollapsed() {
1547
+ return !flow && mode !== "open";
1548
+ }
1549
+ function clampOffset(next) {
1550
+ const width = wrap?.offsetWidth ?? 0;
1551
+ const height = wrap?.offsetHeight ?? 0;
1552
+ if (!vp.width || !vp.height) return next;
1553
+ const bottom = geometry.bottomOffset + vp.keyboardOffset;
1554
+ const maxUp = Math.max(0, vp.height - height - bottom);
1555
+ return {
1556
+ x: clampOffsetX(next.x, width, vp.width),
1557
+ // Negative is up. Down is bounded by the offset the operator set,
1558
+ // which is the floor the bar rests on.
1559
+ y: Math.min(bottom, Math.max(-maxUp, next.y))
1560
+ };
1561
+ }
1562
+ function handleGripDown(event) {
1563
+ if (!draggable || vp.isMobile) return;
1564
+ event.preventDefault();
1565
+ event.stopPropagation();
1566
+ const startX = event.clientX;
1567
+ const startY = event.clientY;
1568
+ const origin = offset;
1569
+ const handle = grip;
1570
+ const win = grip?.ownerDocument?.defaultView ?? (typeof window !== "undefined" ? window : null);
1571
+ const move = (moveEvent) => {
1572
+ if (moveEvent.pointerId !== event.pointerId) return;
1573
+ offset = clampOffset({
1574
+ x: origin.x + (moveEvent.clientX - startX),
1575
+ y: origin.y + (moveEvent.clientY - startY)
1576
+ });
1577
+ render();
1578
+ options.onGeometryChange?.();
1579
+ };
1580
+ const end = (endEvent) => {
1581
+ if (endEvent.pointerId !== event.pointerId) return;
1582
+ win?.removeEventListener("pointermove", move, true);
1583
+ win?.removeEventListener("pointerup", end, true);
1584
+ win?.removeEventListener("pointercancel", end, true);
1585
+ try {
1586
+ handle?.releasePointerCapture(endEvent.pointerId);
1587
+ } catch {
1588
+ }
1589
+ options.onPositionChange?.(offset);
1590
+ };
1591
+ try {
1592
+ handle?.setPointerCapture(event.pointerId);
1593
+ } catch {
1594
+ }
1595
+ win?.addEventListener("pointermove", move, true);
1596
+ win?.addEventListener("pointerup", end, true);
1597
+ win?.addEventListener("pointercancel", end, true);
1598
+ }
1599
+ function renderDock() {
1600
+ wrap?.style.setProperty("display", docked ? "none" : "flex", "important");
1601
+ if (!dock) return;
1602
+ dock.style.setProperty("display", docked ? "flex" : "none", "important");
1603
+ if (!docked) return;
1604
+ dock.style.setProperty(
1605
+ "bottom",
1606
+ `${geometry.bottomOffset + vp.keyboardOffset}px`,
1607
+ "important"
1608
+ );
1609
+ dock.style.setProperty("right", `${DOCK_RIGHT_OFFSET}px`, "important");
1610
+ }
1611
+ function render() {
1612
+ renderDock();
1613
+ if (!wrap) return;
1614
+ const collapsed = isCollapsed();
1615
+ const width = surfaceWidth(geometry.widthPercent, vp);
1616
+ const bottom = geometry.bottomOffset + vp.keyboardOffset;
1617
+ wrap.style.setProperty("bottom", `${bottom}px`, "important");
1618
+ wrap.style.setProperty("left", "50%", "important");
1619
+ wrap.style.setProperty("translate", "-50%", "important");
1620
+ const moved = draggable && !vp.isMobile ? clampOffset(offset) : ZERO_OFFSET;
1621
+ offset = moved;
1622
+ const applied = flow ? ZERO_OFFSET : {
1623
+ x: clampOffsetX(
1624
+ moved.x,
1625
+ collapsed ? geometry.collapsedWidth : width,
1626
+ vp.width
1627
+ ),
1628
+ y: moved.y
1629
+ };
1630
+ wrap.style.setProperty("transform", dragTransform(applied));
1631
+ bar?.setAttribute(
1632
+ "data-grip",
1633
+ draggable && !vp.isMobile && !flow ? "true" : "false"
1634
+ );
1635
+ wrap.style.setProperty(
1636
+ "width",
1637
+ collapsed ? `${geometry.collapsedWidth}px` : `${width}px`
1638
+ );
1639
+ const roomOnMobile = `calc(100% - ${MOBILE_MARGIN * 2}px)`;
1640
+ wrap.style.setProperty("max-width", vp.isMobile ? roomOnMobile : "none");
1641
+ wrap.setAttribute("data-mobile", vp.isMobile ? "" : "false");
1642
+ }
1643
+ function renderLeading() {
1644
+ if (!leading || !leadingIcon) return;
1645
+ const doc = leading.ownerDocument;
1646
+ const hasMenu = !isCollapsed() && attachEnabled;
1647
+ const next = hasMenu ? createIcon(doc, PLUS_PATH, 18) : iconUrl ? createImageIcon(doc, iconUrl, iconSize) : createIcon(doc, SPARKLES_PATH, iconSize);
1648
+ leading.replaceChild(next, leadingIcon);
1649
+ leadingIcon = next;
1650
+ leading.setAttribute("aria-label", labels.attachMenu);
1651
+ if (hasMenu) leading.setAttribute("data-tip", labels.attachMenu);
1652
+ else leading.removeAttribute("data-tip");
1653
+ leading.setAttribute("aria-haspopup", hasMenu ? "menu" : "false");
1654
+ leading.setAttribute("data-role", hasMenu ? "menu" : "mark");
1655
+ leading.disabled = !hasMenu;
1656
+ leading.setAttribute("aria-hidden", hasMenu ? "false" : "true");
1657
+ leading.tabIndex = hasMenu ? 0 : -1;
1658
+ if (!hasMenu) closeMenu();
1659
+ }
1660
+ function renderTray() {
1661
+ if (!tray || !bar) return;
1662
+ const visible = attachEnabled && attachmentCount > 0 && !isCollapsed();
1663
+ const was = bar.getAttribute("data-attachments") === "true";
1664
+ tray.style.setProperty("display", visible ? "flex" : "none");
1665
+ bar.setAttribute("data-attachments", visible ? "true" : "false");
1666
+ if (was === visible) return;
1667
+ safe("onGeometryChange", () => options.onGeometryChange?.())();
1668
+ }
1669
+ function setMenuOpen(open) {
1670
+ menuOpen = open;
1671
+ if (menu) menu.style.setProperty("display", open ? "flex" : "none");
1672
+ leading?.setAttribute("aria-expanded", open ? "true" : "false");
1673
+ if (open) attach?.focus();
1674
+ }
1675
+ function closeMenu() {
1676
+ if (!menuOpen) return;
1677
+ const active = menu?.getRootNode();
1678
+ const focusInMenu = Boolean(
1679
+ active && "activeElement" in active && active.activeElement && menu?.contains(active.activeElement)
1680
+ );
1681
+ setMenuOpen(false);
1682
+ if (focusInMenu) leading?.focus();
1683
+ }
1684
+ function renderDockIcon() {
1685
+ if (!dock || !dockIcon) return;
1686
+ const doc = dock.ownerDocument;
1687
+ const url = welcomeIconUrl && welcomeIconUrl !== failedWelcomeIconUrl ? welcomeIconUrl : null;
1688
+ let next;
1689
+ if (url) {
1690
+ const img = doc.createElement("img");
1691
+ img.src = url;
1692
+ img.alt = "";
1693
+ img.setAttribute("aria-hidden", "true");
1694
+ img.style.setProperty("width", "100%");
1695
+ img.style.setProperty("height", "100%");
1696
+ img.style.setProperty("object-fit", "contain");
1697
+ img.style.setProperty("display", "block");
1698
+ img.addEventListener(
1699
+ "error",
1700
+ () => {
1701
+ failedWelcomeIconUrl = url;
1702
+ renderDockIcon();
1703
+ },
1704
+ { once: true }
1705
+ );
1706
+ next = img;
1707
+ } else {
1708
+ const glyph = createIcon(doc, MESSAGE_CIRCLE_PATH, 18);
1709
+ glyph.setAttribute("width", DOCK_GLYPH_SHARE);
1710
+ glyph.setAttribute("height", DOCK_GLYPH_SHARE);
1711
+ next = glyph;
1712
+ }
1713
+ dock.replaceChild(next, dockIcon);
1714
+ dockIcon = next;
1715
+ }
1716
+ function renderMode() {
1717
+ if (!bar || !form || !status) return;
1718
+ const effective = flow ? awaitingInput ? "input" : "flow" : mode;
1719
+ const done = flow?.phase === "done";
1720
+ bar.setAttribute("data-mode", effective);
1721
+ bar.setAttribute(
1722
+ "data-phase",
1723
+ flow ? done ? "done" : "running" : "none"
1724
+ );
1725
+ bar.setAttribute("data-collapsed", isCollapsed() ? "true" : "false");
1726
+ renderLeading();
1727
+ renderTray();
1728
+ render();
1729
+ form.style.setProperty("display", effective === "flow" ? "none" : "flex");
1730
+ status.style.setProperty(
1731
+ "display",
1732
+ effective === "flow" || effective === "input" ? "flex" : "none"
1733
+ );
1734
+ submit?.style.setProperty("display", "flex");
1735
+ bar.setAttribute("aria-expanded", effective === "open" ? "true" : "false");
1736
+ if (flow && statusName && statusStep) {
1737
+ statusName.textContent = flow.name;
1738
+ statusStep.textContent = done ? "\u2713" : labels.flowStep.replace("{step}", String(flow.step)).replace("{total}", String(flow.total));
1739
+ stop?.style.setProperty("display", done ? "none" : "flex");
1740
+ }
1741
+ if (disclaimer) {
1742
+ disclaimer.style.setProperty(
1743
+ "display",
1744
+ effective === "open" && disclaimer.textContent ? "block" : "none"
1745
+ );
1746
+ }
1747
+ }
1748
+ function onResize() {
1749
+ vp = readViewport(typeof window !== "undefined" ? window : null);
1750
+ render();
1751
+ safe("onGeometryChange", () => options.onGeometryChange?.())();
1752
+ }
1753
+ function onSubmitEvent(event) {
1754
+ event.preventDefault();
1755
+ const value = input?.value.trim() ?? "";
1756
+ if (!value && attachmentCount === 0) {
1757
+ activate();
1758
+ return;
1759
+ }
1760
+ if (input) input.value = "";
1761
+ safe("onSubmit", () => options.onSubmit?.(value))();
1762
+ }
1763
+ function onFocus() {
1764
+ activate();
1765
+ }
1766
+ function activate() {
1767
+ if (flow || mode === "open" || gestureActivated) return;
1768
+ gestureActivated = true;
1769
+ safe("onActivate", () => options.onActivate?.())();
1770
+ }
1771
+ function onBarClick(event) {
1772
+ if (flow || mode === "open") return;
1773
+ if (menu && event.target instanceof Node && menu.contains(event.target)) {
1774
+ return;
1775
+ }
1776
+ activate();
1777
+ try {
1778
+ input?.focus();
1779
+ } catch {
1780
+ }
1781
+ }
1782
+ function onPointerDown() {
1783
+ gestureActivated = false;
1784
+ }
1785
+ function onLeadingClick(event) {
1786
+ event.preventDefault();
1787
+ event.stopPropagation();
1788
+ if (isCollapsed() || !attachEnabled) return;
1789
+ setMenuOpen(!menuOpen);
1790
+ }
1791
+ function openPicker() {
1792
+ try {
1793
+ picker?.click();
1794
+ } catch {
1795
+ }
1796
+ }
1797
+ function onAttachItem(event) {
1798
+ event.preventDefault();
1799
+ event.stopPropagation();
1800
+ closeMenu();
1801
+ openPicker();
1802
+ options.onAttach?.();
1803
+ }
1804
+ function onPicked() {
1805
+ const chosen = picker?.files;
1806
+ const files = chosen ? Array.from(chosen) : [];
1807
+ if (picker) picker.value = "";
1808
+ if (!attachEnabled || files.length === 0) return;
1809
+ options.onFiles?.(files);
1810
+ }
1811
+ function onRootKeyDown(event) {
1812
+ if (event.key === "Escape" && menuOpen) closeMenu();
1813
+ }
1814
+ function onShadowPointerDown(event) {
1815
+ if (!menuOpen) return;
1816
+ const target = event.target;
1817
+ const onMenu = target instanceof Node && (leading?.contains(target) === true || menu?.contains(target) === true);
1818
+ if (!onMenu) closeMenu();
1819
+ }
1820
+ function onRootPointerDown(event) {
1821
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
1822
+ const target = event.target;
1823
+ const host = shell?.host;
1824
+ const insideWidgetPath = Boolean(
1825
+ host && (path.some((node) => node === host) || path.length === 0 && target instanceof Node && host.contains(target))
1826
+ );
1827
+ if (menuOpen && !insideWidgetPath) closeMenu();
1828
+ if (flow || mode !== "open") return;
1829
+ if (!host || insideWidgetPath) return;
1830
+ safe("onDismiss", () => options.onDismiss?.())();
1831
+ }
1832
+ function onDragOver(event) {
1833
+ if (!carriesFiles(event)) return;
1834
+ event.preventDefault();
1835
+ }
1836
+ function onDrop(event) {
1837
+ if (!carriesFiles(event)) return;
1838
+ event.preventDefault();
1839
+ if (!attachEnabled) return;
1840
+ const files = droppedFiles(event);
1841
+ if (files.length === 0) return;
1842
+ options.onFiles?.(files);
1843
+ }
1844
+ const handleResize = safe("resize", onResize);
1845
+ const handleSubmit = safe("submit", onSubmitEvent);
1846
+ const handleFocus = safe("focus", onFocus);
1847
+ const handleBarClick = safe("barClick", onBarClick);
1848
+ const handlePointerDown = safe("pointerdown", onPointerDown);
1849
+ const handleLeading = safe("attachMenu", onLeadingClick);
1850
+ const handleAttach = safe("attach", onAttachItem);
1851
+ const handlePicked = safe("picked", onPicked);
1852
+ const handleStop = safe("stop", () => options.onStop?.());
1853
+ const handleRootKeyDown = safe("menuKey", onRootKeyDown);
1854
+ const handleRootPointerDown = safe("menuDismiss", onRootPointerDown);
1855
+ const handleShadowPointerDown = safe("menuDismissInside", onShadowPointerDown);
1856
+ const handleDragOver = safe("dragover", onDragOver);
1857
+ const handleDrop = safe("drop", onDrop);
1858
+ const handleDock = safe("undock", () => options.onUndock?.());
1859
+ function reveal(host) {
1860
+ if (revealed) return;
1861
+ revealed = true;
1862
+ try {
1863
+ host.style.setProperty("opacity", "1");
1864
+ } catch {
1865
+ }
1866
+ }
1867
+ function scheduleReveal(host, win) {
1868
+ if (revealed) return;
1869
+ if (!win || typeof win.setTimeout !== "function") {
1870
+ revealed = true;
1871
+ return;
1872
+ }
1873
+ try {
1874
+ host.style.setProperty("opacity", "0");
1875
+ host.style.setProperty("transition", `opacity ${REVEAL_FADE_MS}ms ease`);
1876
+ } catch {
1877
+ revealed = true;
1878
+ return;
1879
+ }
1880
+ const after = (ms) => {
1881
+ try {
1882
+ win.setTimeout(() => reveal(host), ms);
1883
+ } catch {
1884
+ reveal(host);
1885
+ }
1886
+ };
1887
+ if (host.ownerDocument.readyState === "complete") {
1888
+ after(REVEAL_SETTLED_MS);
1889
+ return;
1890
+ }
1891
+ try {
1892
+ win.addEventListener("load", () => after(REVEAL_AFTER_LOAD_MS), {
1893
+ once: true
1894
+ });
1895
+ } catch {
1896
+ }
1897
+ after(REVEAL_MAX_MS);
1898
+ }
1899
+ function mount() {
1900
+ if (shell?.host.isConnected) {
1901
+ warn("The chat bar is already mounted. Ignoring the second mount.");
1902
+ return;
1903
+ }
1904
+ shell = null;
1905
+ const created = createShell({ container: options.container });
1906
+ if (!created) return;
1907
+ shell = created;
1908
+ scheduleReveal(created.host, created.host.ownerDocument.defaultView);
1909
+ injectStyles(shell, BAR_CSS);
1910
+ const doc = shell.host.ownerDocument;
1911
+ wrap = doc.createElement("div");
1912
+ wrap.className = "arcy-bar-wrap";
1913
+ for (const [property, value] of SURVIVAL_STYLE) {
1914
+ wrap.style.setProperty(property, value);
1915
+ }
1916
+ for (const [property, value] of SURVIVAL_IMPORTANT) {
1917
+ wrap.style.setProperty(property, value, "important");
1918
+ }
1919
+ wrap.style.setProperty("flex-direction", "column");
1920
+ wrap.style.setProperty("align-items", "center");
1921
+ bar = doc.createElement("div");
1922
+ bar.className = "arcy-bar";
1923
+ bar.setAttribute("role", "group");
1924
+ bar.setAttribute("aria-label", labels.openChat);
1925
+ form = doc.createElement("form");
1926
+ form.className = "arcy-bar-form";
1927
+ input = doc.createElement("input");
1928
+ input.type = "text";
1929
+ input.className = "arcy-bar-input";
1930
+ input.maxLength = MAX_INPUT_LENGTH;
1931
+ input.setAttribute("aria-label", labels.openChat);
1932
+ input.autocomplete = "off";
1933
+ tray = doc.createElement("div");
1934
+ tray.className = BAR_TRAY_CLASS;
1935
+ tray.style.setProperty("display", "none");
1936
+ grip = doc.createElement("button");
1937
+ grip.type = "button";
1938
+ grip.className = "arcy-bar-grip";
1939
+ grip.setAttribute("aria-label", labels.moveBar);
1940
+ grip.setAttribute("data-tip", labels.moveBar);
1941
+ grip.appendChild(createIcon(doc, GRIP_PATH, 16));
1942
+ grip.addEventListener("pointerdown", handleGripDown);
1943
+ grip.addEventListener("click", (event) => event.stopPropagation());
1944
+ leading = doc.createElement("button");
1945
+ leading.type = "button";
1946
+ leading.className = "arcy-bar-leading";
1947
+ leadingIcon = iconUrl ? createImageIcon(doc, iconUrl, iconSize) : createIcon(doc, SPARKLES_PATH, iconSize);
1948
+ leading.appendChild(leadingIcon);
1949
+ menu = doc.createElement("div");
1950
+ menu.className = "arcy-bar-menu";
1951
+ menu.setAttribute("role", "menu");
1952
+ menu.style.setProperty("display", "none");
1953
+ attach = doc.createElement("button");
1954
+ attach.type = "button";
1955
+ attach.className = "arcy-bar-menu-item";
1956
+ attach.setAttribute("role", "menuitem");
1957
+ attach.appendChild(createIcon(doc, ATTACH_PATH, 16));
1958
+ attach.appendChild(doc.createTextNode(labels.attachImage));
1959
+ menu.appendChild(attach);
1960
+ picker = doc.createElement("input");
1961
+ picker.type = "file";
1962
+ picker.accept = ATTACHABLE_TYPES.join(",");
1963
+ picker.multiple = true;
1964
+ picker.tabIndex = -1;
1965
+ picker.setAttribute("aria-hidden", "true");
1966
+ picker.style.setProperty("display", "none");
1967
+ bar.appendChild(picker);
1968
+ submit = doc.createElement("button");
1969
+ submit.type = "submit";
1970
+ submit.className = "arcy-bar-send";
1971
+ submit.setAttribute("aria-label", labels.send);
1972
+ submit.appendChild(createIcon(doc, SEND_PATH, 18));
1973
+ form.appendChild(grip);
1974
+ form.appendChild(leading);
1975
+ form.appendChild(input);
1976
+ form.appendChild(submit);
1977
+ status = doc.createElement("div");
1978
+ status.className = "arcy-bar-status";
1979
+ status.style.setProperty("display", "none");
1980
+ const dot = doc.createElement("span");
1981
+ dot.className = "arcy-bar-dot";
1982
+ dot.setAttribute("aria-hidden", "true");
1983
+ statusName = doc.createElement("span");
1984
+ statusName.className = "arcy-bar-flow-name";
1985
+ statusStep = doc.createElement("span");
1986
+ statusStep.className = "arcy-bar-flow-step";
1987
+ stop = doc.createElement("button");
1988
+ stop.type = "button";
1989
+ stop.className = "arcy-bar-stop";
1990
+ stop.appendChild(createIcon(doc, STOP_PATH, 15, 2));
1991
+ stop.appendChild(doc.createTextNode(labels.flowStop));
1992
+ status.appendChild(dot);
1993
+ status.appendChild(statusName);
1994
+ status.appendChild(statusStep);
1995
+ status.appendChild(stop);
1996
+ bar.appendChild(tray);
1997
+ bar.appendChild(form);
1998
+ bar.appendChild(status);
1999
+ bar.appendChild(menu);
2000
+ disclaimer = doc.createElement("div");
2001
+ disclaimer.className = "arcy-bar-disclaimer";
2002
+ disclaimer.style.setProperty("display", "none");
2003
+ wrap.appendChild(bar);
2004
+ wrap.appendChild(disclaimer);
2005
+ dock = doc.createElement("button");
2006
+ dock.type = "button";
2007
+ dock.className = "arcy-bar-dock";
2008
+ dock.setAttribute("aria-label", labels.openChat);
2009
+ dock.setAttribute("data-tip", labels.openChat);
2010
+ dock.setAttribute("data-tip-placement", "above");
2011
+ dock.setAttribute("data-tip-align", "end");
2012
+ for (const [property, value] of SURVIVAL_STYLE) {
2013
+ dock.style.setProperty(property, value);
2014
+ }
2015
+ for (const [property, value] of SURVIVAL_IMPORTANT) {
2016
+ dock.style.setProperty(property, value, "important");
2017
+ }
2018
+ dock.style.setProperty("align-items", "center");
2019
+ dock.style.setProperty("justify-content", "center");
2020
+ dock.style.setProperty("width", DOCK_SIZE_CSS);
2021
+ dock.style.setProperty("height", DOCK_SIZE_CSS);
2022
+ dockIcon = createIcon(doc, MESSAGE_CIRCLE_PATH, 18);
2023
+ dock.appendChild(dockIcon);
2024
+ renderDockIcon();
2025
+ dock.addEventListener("click", handleDock);
2026
+ form.addEventListener("submit", handleSubmit);
2027
+ input.addEventListener("focus", handleFocus);
2028
+ bar.addEventListener("click", handleBarClick);
2029
+ bar.addEventListener("pointerdown", handlePointerDown, true);
2030
+ leading.addEventListener("click", handleLeading);
2031
+ attach.addEventListener("click", handleAttach);
2032
+ picker.addEventListener("change", handlePicked);
2033
+ stop.addEventListener("click", handleStop);
2034
+ bar.addEventListener("dragover", handleDragOver);
2035
+ bar.addEventListener("drop", handleDrop);
2036
+ doc.addEventListener("pointerdown", handleRootPointerDown, true);
2037
+ shell.root.addEventListener("pointerdown", handleShadowPointerDown, true);
2038
+ doc.addEventListener("keydown", handleRootKeyDown, true);
2039
+ const win = doc.defaultView;
2040
+ win?.addEventListener("resize", handleResize);
2041
+ win?.addEventListener("orientationchange", handleResize);
2042
+ win?.visualViewport?.addEventListener("resize", handleResize);
2043
+ win?.visualViewport?.addEventListener("scroll", handleResize);
2044
+ shell.root.appendChild(wrap);
2045
+ shell.root.appendChild(dock);
2046
+ vp = readViewport(win ?? null);
2047
+ docked = options.readDocked?.() === true;
2048
+ render();
2049
+ renderMode();
2050
+ }
2051
+ function unmount() {
2052
+ if (!shell) return;
2053
+ const win = shell.host.ownerDocument.defaultView;
2054
+ win?.removeEventListener("resize", handleResize);
2055
+ win?.removeEventListener("orientationchange", handleResize);
2056
+ win?.visualViewport?.removeEventListener("resize", handleResize);
2057
+ win?.visualViewport?.removeEventListener("scroll", handleResize);
2058
+ form?.removeEventListener("submit", handleSubmit);
2059
+ input?.removeEventListener("focus", handleFocus);
2060
+ bar?.removeEventListener("click", handleBarClick);
2061
+ bar?.removeEventListener("pointerdown", handlePointerDown, true);
2062
+ leading?.removeEventListener("click", handleLeading);
2063
+ attach?.removeEventListener("click", handleAttach);
2064
+ picker?.removeEventListener("change", handlePicked);
2065
+ stop?.removeEventListener("click", handleStop);
2066
+ bar?.removeEventListener("dragover", handleDragOver);
2067
+ bar?.removeEventListener("drop", handleDrop);
2068
+ dock?.removeEventListener("click", handleDock);
2069
+ const doc = shell.host.ownerDocument;
2070
+ doc.removeEventListener("pointerdown", handleRootPointerDown, true);
2071
+ shell.root.removeEventListener("pointerdown", handleShadowPointerDown, true);
2072
+ doc.removeEventListener("keydown", handleRootKeyDown, true);
2073
+ shell.destroy();
2074
+ shell = null;
2075
+ wrap = bar = form = status = statusName = statusStep = disclaimer = null;
2076
+ input = null;
2077
+ attach = submit = stop = null;
2078
+ picker = null;
2079
+ leading = null;
2080
+ leadingIcon = null;
2081
+ dock = null;
2082
+ dockIcon = null;
2083
+ menu = null;
2084
+ tray = null;
2085
+ menuOpen = false;
2086
+ flow = null;
2087
+ mode = "idle";
2088
+ }
2089
+ return {
2090
+ mount,
2091
+ unmount,
2092
+ isMounted: () => shell !== null && shell.host.isConnected,
2093
+ getElement: () => bar,
2094
+ getRoot: () => shell?.root ?? null,
2095
+ getHost: () => shell?.host ?? null,
2096
+ setMode(next) {
2097
+ if (mode === next) return;
2098
+ mode = next;
2099
+ gestureActivated = false;
2100
+ renderMode();
2101
+ },
2102
+ getMode: () => flow ? "flow" : mode,
2103
+ setGeometry(next) {
2104
+ geometry = {
2105
+ widthPercent: next.widthPercent ?? geometry.widthPercent,
2106
+ bottomOffset: next.bottomOffset ?? geometry.bottomOffset,
2107
+ collapsedWidth: next.collapsedWidth ?? geometry.collapsedWidth
2108
+ };
2109
+ render();
2110
+ },
2111
+ getMetrics: () => ({
2112
+ ...geometry,
2113
+ // What is actually rendered, not what is stored: a running flow is
2114
+ // pinned to the bottom centre (see `render`), and the panel and the
2115
+ // executor's scroll-into-view both place against this.
2116
+ offset: flow || !draggable || vp.isMobile ? ZERO_OFFSET : offset,
2117
+ height: wrap?.offsetHeight || bar?.offsetHeight || BAR_HEIGHT,
2118
+ keyboardOffset: vp.keyboardOffset,
2119
+ isMobile: vp.isMobile,
2120
+ viewportHeight: vp.height,
2121
+ viewportWidth: vp.width
2122
+ }),
2123
+ getHeight: () => wrap?.offsetHeight || bar?.offsetHeight || BAR_HEIGHT,
2124
+ setLabels(next) {
2125
+ labels = { ...labels, ...next };
2126
+ try {
2127
+ submit?.setAttribute("aria-label", labels.send);
2128
+ if (attach) {
2129
+ const text = attach.lastChild;
2130
+ if (text) text.textContent = labels.attachImage;
2131
+ }
2132
+ bar?.setAttribute("aria-label", labels.openChat);
2133
+ input?.setAttribute("aria-label", labels.openChat);
2134
+ if (dock) {
2135
+ dock.setAttribute("aria-label", labels.openChat);
2136
+ dock.setAttribute("data-tip", labels.openChat);
2137
+ }
2138
+ if (grip) {
2139
+ grip.setAttribute("aria-label", labels.moveBar);
2140
+ grip.setAttribute("data-tip", labels.moveBar);
2141
+ }
2142
+ if (stop) {
2143
+ const text = stop.lastChild;
2144
+ if (text) text.textContent = labels.flowStop;
2145
+ }
2146
+ } catch {
2147
+ }
2148
+ renderMode();
2149
+ },
2150
+ setPlaceholder(text) {
2151
+ try {
2152
+ if (input) input.placeholder = text ?? "";
2153
+ } catch {
2154
+ }
2155
+ },
2156
+ setDisabled(disabled) {
2157
+ try {
2158
+ if (input) input.disabled = disabled;
2159
+ if (submit) submit.disabled = disabled;
2160
+ if (attach) attach.disabled = disabled;
2161
+ if (disabled) closeMenu();
2162
+ } catch {
2163
+ }
2164
+ },
2165
+ setAttachEnabled(enabled) {
2166
+ if (attachEnabled === enabled) return;
2167
+ attachEnabled = enabled;
2168
+ renderMode();
2169
+ },
2170
+ setDraggable(enabled) {
2171
+ if (draggable === enabled) return;
2172
+ draggable = enabled;
2173
+ offset = enabled ? options.readPosition?.() ?? ZERO_OFFSET : ZERO_OFFSET;
2174
+ render();
2175
+ options.onGeometryChange?.();
2176
+ },
2177
+ setIcon(url) {
2178
+ if (iconUrl === url) return;
2179
+ iconUrl = url;
2180
+ renderLeading();
2181
+ },
2182
+ setIconSize(size2) {
2183
+ if (iconSize === size2) return;
2184
+ iconSize = size2;
2185
+ renderLeading();
2186
+ },
2187
+ setWelcomeIcon(url) {
2188
+ if (welcomeIconUrl === url) return;
2189
+ welcomeIconUrl = url;
2190
+ failedWelcomeIconUrl = null;
2191
+ renderDockIcon();
2192
+ },
2193
+ setDocked(next) {
2194
+ if (docked === next) return;
2195
+ docked = next;
2196
+ safe("onDockedChange", () => options.onDockedChange?.(next))();
2197
+ if (next) closeMenu();
2198
+ render();
2199
+ safe("onGeometryChange", () => options.onGeometryChange?.())();
2200
+ },
2201
+ isDocked: () => docked,
2202
+ setFlow(next) {
2203
+ flow = next;
2204
+ if (!next) awaitingInput = false;
2205
+ renderMode();
2206
+ },
2207
+ setAwaitingInput(next) {
2208
+ if (awaitingInput === next) return;
2209
+ awaitingInput = next;
2210
+ renderMode();
2211
+ safe("onGeometryChange", () => options.onGeometryChange?.())();
2212
+ },
2213
+ setDisclaimer(input2) {
2214
+ try {
2215
+ if (disclaimer) {
2216
+ disclaimer.textContent = "";
2217
+ if (input2 == null) {
2218
+ } else if (typeof input2 === "string") {
2219
+ disclaimer.textContent = input2;
2220
+ } else {
2221
+ const doc = disclaimer.ownerDocument;
2222
+ disclaimer.appendChild(doc.createTextNode(input2.before));
2223
+ const link = doc.createElement("a");
2224
+ link.className = POLICY_LINK_CLASS;
2225
+ link.href = input2.linkHref;
2226
+ link.target = "_blank";
2227
+ link.rel = "noopener noreferrer";
2228
+ link.textContent = input2.linkLabel;
2229
+ disclaimer.appendChild(link);
2230
+ if (input2.after) disclaimer.appendChild(doc.createTextNode(input2.after));
2231
+ }
2232
+ }
2233
+ } catch {
2234
+ }
2235
+ renderMode();
2236
+ },
2237
+ setAttachmentCount(count) {
2238
+ attachmentCount = count;
2239
+ try {
2240
+ renderTray();
2241
+ } catch {
2242
+ }
2243
+ },
2244
+ attachmentSlot: () => tray,
2245
+ focusInput() {
2246
+ try {
2247
+ if (shell) releaseHostFocus(shell.host.ownerDocument, shell.host);
2248
+ input?.focus();
2249
+ } catch {
2250
+ }
2251
+ },
2252
+ reposition: () => {
2253
+ vp = readViewport(typeof window !== "undefined" ? window : null);
2254
+ render();
2255
+ },
2256
+ readValue: () => input?.value ?? "",
2257
+ clearValue() {
2258
+ try {
2259
+ if (input) input.value = "";
2260
+ } catch {
2261
+ }
2262
+ }
2263
+ };
2264
+ }
2265
+
2266
+ // src/shell/tokens.ts
2267
+ var HEX = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
2268
+ var FONT_KEY = /^[a-z0-9-]{1,32}$/;
2269
+ var SHADOWS = {
2270
+ none: "none",
2271
+ soft: "0 0 2px rgba(16, 24, 40, 0.04), 0 0 8px rgba(16, 24, 40, 0.06)",
2272
+ medium: "0 0 4px rgba(16, 24, 40, 0.05), 0 0 20px rgba(16, 24, 40, 0.10)",
2273
+ strong: "0 0 8px rgba(16, 24, 40, 0.07), 0 0 36px rgba(16, 24, 40, 0.16)",
2274
+ bold: "0 0 12px rgba(16, 24, 40, 0.10), 0 0 48px rgba(16, 24, 40, 0.22)"
2275
+ };
2276
+ var MIN_WIDTH_PERCENT = 20;
2277
+ var MAX_WIDTH_PERCENT = 50;
2278
+ var MIN_MAX_HEIGHT_PERCENT = 30;
2279
+ var MAX_MAX_HEIGHT_PERCENT = 80;
2280
+ var MIN_BOTTOM_OFFSET = 0;
2281
+ var MAX_BOTTOM_OFFSET = 120;
2282
+ var MIN_BAR_ICON_SIZE = 14;
2283
+ var MAX_BAR_ICON_SIZE = 28;
2284
+ var MIN_COLLAPSED_WIDTH = 180;
2285
+ var MAX_COLLAPSED_WIDTH = 480;
2286
+ var MAX_Z_INDEX = 2147483647;
2287
+ var NO_TOKENS = {
2288
+ vars: {},
2289
+ widthPercent: null,
2290
+ bottomOffset: null,
2291
+ collapsedWidth: null,
2292
+ barIconSize: null,
2293
+ maxHeightPercent: null,
2294
+ zIndex: null
2295
+ };
2296
+ function object(value) {
2297
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
2298
+ }
2299
+ function size(value, min, max) {
2300
+ if (typeof value !== "number" || !Number.isFinite(value)) return null;
2301
+ return Math.min(max, Math.max(min, value));
2302
+ }
2303
+ function colors(vars, group, fields) {
2304
+ if (!group) return;
2305
+ for (const [field, property] of fields) {
2306
+ const raw = group[field];
2307
+ if (typeof raw === "string" && HEX.test(raw)) vars[property] = raw;
2308
+ }
2309
+ }
2310
+ function pixels(vars, property, value, min, max) {
2311
+ const n = size(value, min, max);
2312
+ if (n !== null) vars[property] = `${n}px`;
2313
+ }
2314
+ function shadow(vars, property, value) {
2315
+ if (typeof value === "string" && value in SHADOWS) {
2316
+ vars[property] = SHADOWS[value];
2317
+ }
2318
+ }
2319
+ function readTokens(value) {
2320
+ const tokens = object(value);
2321
+ if (!tokens) return NO_TOKENS;
2322
+ const vars = {};
2323
+ const base = object(tokens.base);
2324
+ colors(vars, object(base?.brand), [
2325
+ ["text", "--_arcy-brand-text"],
2326
+ ["background", "--_arcy-brand-bg"],
2327
+ ["hover", "--_arcy-brand-bg-hover"],
2328
+ ["active", "--_arcy-brand-bg-active"]
2329
+ ]);
2330
+ colors(vars, object(base?.main), [
2331
+ ["text", "--_arcy-main-text"],
2332
+ ["background", "--_arcy-main-bg"]
2333
+ ]);
2334
+ const border = object(base?.border);
2335
+ pixels(vars, "--_arcy-border-width", border?.width, 0, 8);
2336
+ colors(vars, border, [["color", "--_arcy-border-color"]]);
2337
+ const glass = size(base?.glassiness, 0, 100);
2338
+ if (glass !== null && glass > 0) {
2339
+ vars["--_arcy-glass-blur"] = `${Math.round(glass / 5)}px`;
2340
+ vars["--_arcy-glass-alpha"] = `${Math.round(glass / 2)}%`;
2341
+ }
2342
+ const font = base?.fontFamily;
2343
+ if (typeof font === "string" && FONT_KEY.test(font) && font !== "system") {
2344
+ vars[FONT_KEY_PROPERTY] = font;
2345
+ }
2346
+ const zIndex = size(base?.zIndex, 0, MAX_Z_INDEX);
2347
+ pixels(vars, "--arcy-bar-radius", base?.borderRadius, 0, 32);
2348
+ pixels(vars, "--_arcy-chat-radius", base?.borderRadius, 0, 32);
2349
+ shadow(vars, "--arcy-bar-shadow", base?.shadow);
2350
+ shadow(vars, "--_arcy-chat-shadow", base?.shadow);
2351
+ const bar = object(tokens.bar);
2352
+ const chat = object(tokens.chat);
2353
+ pixels(vars, "--_arcy-chat-logo-size", chat?.logoSize, 16, 48);
2354
+ pixels(vars, "--_arcy-chat-welcome-icon-size", chat?.welcomeIconSize, 16, 72);
2355
+ colors(vars, object(chat?.assistantBubble), [
2356
+ ["background", "--_arcy-assistant-bg"],
2357
+ ["text", "--_arcy-assistant-text"]
2358
+ ]);
2359
+ const scrollbar = object(chat?.scrollbar);
2360
+ pixels(vars, "--_arcy-scroll-width", scrollbar?.width, 2, 16);
2361
+ colors(vars, scrollbar, [
2362
+ ["track", "--_arcy-scroll-track"],
2363
+ ["thumb", "--_arcy-scroll-thumb"],
2364
+ ["thumbHover", "--_arcy-scroll-thumb-hover"]
2365
+ ]);
2366
+ return {
2367
+ vars,
2368
+ // Read as numbers rather than written as custom properties: `bar.ts`
2369
+ // (width) and `chat.ts`'s `place()` (max height) do their own viewport
2370
+ // maths with them, and one writer is what keeps the rendered bar and
2371
+ // the panel's placement from disagreeing.
2372
+ widthPercent: size(chat?.width, MIN_WIDTH_PERCENT, MAX_WIDTH_PERCENT),
2373
+ bottomOffset: size(bar?.bottomOffset, MIN_BOTTOM_OFFSET, MAX_BOTTOM_OFFSET),
2374
+ collapsedWidth: size(
2375
+ bar?.collapsedWidth,
2376
+ MIN_COLLAPSED_WIDTH,
2377
+ MAX_COLLAPSED_WIDTH
2378
+ ),
2379
+ barIconSize: size(bar?.iconSize, MIN_BAR_ICON_SIZE, MAX_BAR_ICON_SIZE),
2380
+ maxHeightPercent: size(
2381
+ chat?.maxHeight,
2382
+ MIN_MAX_HEIGHT_PERCENT,
2383
+ MAX_MAX_HEIGHT_PERCENT
2384
+ ),
2385
+ zIndex: zIndex !== null ? Math.round(zIndex) : null
2386
+ };
2387
+ }
2388
+ var MANAGED = [
2389
+ "--_arcy-brand-text",
2390
+ "--_arcy-brand-bg",
2391
+ "--_arcy-brand-bg-hover",
2392
+ "--_arcy-brand-bg-active",
2393
+ "--_arcy-main-text",
2394
+ "--_arcy-main-bg",
2395
+ "--_arcy-border-width",
2396
+ "--_arcy-border-color",
2397
+ "--_arcy-glass-blur",
2398
+ "--_arcy-glass-alpha",
2399
+ FONT_KEY_PROPERTY,
2400
+ "--arcy-bar-radius",
2401
+ "--arcy-bar-shadow",
2402
+ "--_arcy-chat-radius",
2403
+ "--_arcy-chat-logo-size",
2404
+ "--_arcy-chat-welcome-icon-size",
2405
+ "--_arcy-chat-shadow",
2406
+ "--_arcy-assistant-bg",
2407
+ "--_arcy-assistant-text",
2408
+ "--_arcy-scroll-width",
2409
+ "--_arcy-scroll-track",
2410
+ "--_arcy-scroll-thumb",
2411
+ "--_arcy-scroll-thumb-hover"
2412
+ ];
2413
+ function applyTokens(host, tokens) {
2414
+ try {
2415
+ for (const property of MANAGED) {
2416
+ const value = tokens.vars[property];
2417
+ if (value === void 0) host.style.removeProperty(property);
2418
+ else host.style.setProperty(property, value);
2419
+ }
2420
+ host.style.setProperty(
2421
+ "z-index",
2422
+ tokens.zIndex === null ? Z_INDEX : String(tokens.zIndex),
2423
+ "important"
2424
+ );
2425
+ } catch (error) {
2426
+ warn(`The widget could not apply its theme. ${String(error)}`);
2427
+ }
2428
+ }
2429
+ var TOKEN_KEY_PREFIX = "arcy.tokens.";
2430
+ var MAX_STORED = 4096;
2431
+ var MAX_NAME = 100;
2432
+ var MAX_URL = 2048;
2433
+ function readIdentity(value) {
2434
+ const raw = object(value);
2435
+ if (!raw) return null;
2436
+ const name = typeof raw.name === "string" ? raw.name.slice(0, MAX_NAME) : "";
2437
+ const url = raw.logoUrl;
2438
+ const logoUrl = typeof url === "string" && url.length <= MAX_URL && /^https:\/\//i.test(url) ? url : null;
2439
+ return { name, logoUrl };
2440
+ }
2441
+ function createTokenCache(token, raw = browserLocalStorage()) {
2442
+ const store = safeStore(raw);
2443
+ const key = TOKEN_KEY_PREFIX + token;
2444
+ return {
2445
+ read() {
2446
+ const blob = object(readJson(store, key));
2447
+ return {
2448
+ tokens: readTokens(blob?.t),
2449
+ identity: readIdentity(blob?.w)
2450
+ };
2451
+ },
2452
+ remember(tokens, identity) {
2453
+ const t = object(tokens);
2454
+ const w = readIdentity(identity);
2455
+ if (!t && !w) {
2456
+ store.removeItem(key);
2457
+ return;
2458
+ }
2459
+ try {
2460
+ const blob = { t: t ?? void 0, w: w ?? void 0 };
2461
+ if (JSON.stringify(blob).length > MAX_STORED) return;
2462
+ writeJson(store, key, blob);
2463
+ } catch {
2464
+ }
2465
+ }
2466
+ };
2467
+ }
2468
+
2469
+ // src/session/visit.ts
2470
+ var VISIT_KEY_PREFIX = "arcy.visit.";
2471
+ var FIRST_VISIT_VALUE = "first";
2472
+ function createVisitState(token, raw = browserSessionStorage()) {
2473
+ const store = safeStore(raw);
2474
+ const key = VISIT_KEY_PREFIX + token;
2475
+ return {
2476
+ markFirstVisit() {
2477
+ store.setItem(key, FIRST_VISIT_VALUE);
2478
+ },
2479
+ isFirstVisit() {
2480
+ return store.getItem(key) === FIRST_VISIT_VALUE;
2481
+ }
2482
+ };
2483
+ }
2484
+
2485
+ // src/telemetry/ulid.ts
2486
+ var ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
2487
+ var TIME_CHARS = 10;
2488
+ var RANDOM_CHARS = 16;
2489
+ var defaultRandomFill = (out) => {
2490
+ const source = typeof globalThis !== "undefined" ? globalThis.crypto : void 0;
2491
+ if (source && typeof source.getRandomValues === "function") {
2492
+ source.getRandomValues(out);
2493
+ return;
2494
+ }
2495
+ for (let i = 0; i < out.length; i++) {
2496
+ out[i] = Math.floor(Math.random() * 256);
2497
+ }
2498
+ };
2499
+ function ulid(now, fill = defaultRandomFill) {
2500
+ let time = "";
2501
+ let remaining = Math.floor(now);
2502
+ for (let i = 0; i < TIME_CHARS; i++) {
2503
+ time = (ALPHABET[remaining % 32] ?? "0") + time;
2504
+ remaining = Math.floor(remaining / 32);
2505
+ }
2506
+ const bytes = new Uint8Array(RANDOM_CHARS);
2507
+ fill(bytes);
2508
+ let random = "";
2509
+ for (let i = 0; i < RANDOM_CHARS; i++) {
2510
+ random += ALPHABET[(bytes[i] ?? 0) % 32];
2511
+ }
2512
+ return time + random;
2513
+ }
2514
+
2515
+ // src/shell/conversation.ts
2516
+ var CONVERSATION_KEY_PREFIX = "arcy.chat.";
2517
+ var CONVERSATION_TTL_MS = 24 * 60 * 60 * 1e3;
2518
+ function createConversationStore(token, { store: raw = browserLocalStorage(), fill = defaultRandomFill, now = Date.now } = {}) {
2519
+ const store = safeStore(raw);
2520
+ const key = CONVERSATION_KEY_PREFIX + token;
2521
+ function readRaw() {
2522
+ return readJson(store, key) ?? {};
2523
+ }
2524
+ function mint() {
2525
+ const id = ulid(now(), fill);
2526
+ writeJson(store, key, { id, lastActiveAt: now() });
2527
+ return id;
2528
+ }
2529
+ return {
2530
+ id() {
2531
+ const stored = readRaw();
2532
+ const id = typeof stored.id === "string" ? stored.id : null;
2533
+ const lastActiveAt = typeof stored.lastActiveAt === "number" ? stored.lastActiveAt : null;
2534
+ if (id && lastActiveAt !== null && now() - lastActiveAt < CONVERSATION_TTL_MS) {
2535
+ return id;
2536
+ }
2537
+ return mint();
2538
+ },
2539
+ touch() {
2540
+ const stored = readRaw();
2541
+ if (typeof stored.id !== "string") return;
2542
+ writeJson(store, key, { ...stored, lastActiveAt: now() });
2543
+ },
2544
+ reset() {
2545
+ mint();
2546
+ },
2547
+ adopt(conversationId) {
2548
+ if (typeof conversationId !== "string" || conversationId.length === 0) {
2549
+ return this.id();
2550
+ }
2551
+ writeJson(store, key, { id: conversationId, lastActiveAt: now() });
2552
+ return conversationId;
2553
+ },
2554
+ startNew() {
2555
+ return mint();
2556
+ }
2557
+ };
2558
+ }
2559
+
2560
+ // src/session/context.ts
2561
+ var CAMPAIGN_KEYS = [
2562
+ ["utm_source", "utmSource"],
2563
+ ["utm_medium", "utmMedium"],
2564
+ ["utm_campaign", "utmCampaign"],
2565
+ ["utm_content", "utmContent"],
2566
+ ["utm_term", "utmTerm"]
2567
+ ];
2568
+ var MAX_CAMPAIGN_LENGTH = 255;
2569
+ var MAX_REFERRER_LENGTH = 512;
2570
+ var CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
2571
+ function clean(value, max) {
2572
+ const trimmed = value.replace(CONTROL_CHARACTERS, "").trim();
2573
+ if (trimmed.length === 0) return void 0;
2574
+ return trimmed.slice(0, max);
2575
+ }
2576
+ function scrubReferrer(referrer) {
2577
+ if (typeof referrer !== "string" || referrer.length === 0) return void 0;
2578
+ try {
2579
+ const url = new URL(referrer);
2580
+ const host = url.host;
2581
+ if (!host) return void 0;
2582
+ const path = scrubPath(url.pathname);
2583
+ return clean(path === "/" ? host : host + path, MAX_REFERRER_LENGTH);
2584
+ } catch {
2585
+ return void 0;
2586
+ }
2587
+ }
2588
+ function readCampaign(search) {
2589
+ const out = {};
2590
+ if (typeof search !== "string" || search.length === 0) return out;
2591
+ const query = search.charAt(0) === "?" ? search.slice(1) : search;
2592
+ for (const pair of query.split("&")) {
2593
+ const eq = pair.indexOf("=");
2594
+ if (eq <= 0) continue;
2595
+ const rawKey = pair.slice(0, eq).toLowerCase();
2596
+ const match = CAMPAIGN_KEYS.find((entry) => entry[0] === rawKey);
2597
+ if (!match) continue;
2598
+ let value = pair.slice(eq + 1).replace(/\+/g, " ");
2599
+ try {
2600
+ value = decodeURIComponent(value);
2601
+ } catch {
2602
+ }
2603
+ const cleaned = clean(value, MAX_CAMPAIGN_LENGTH);
2604
+ if (cleaned) out[match[1]] = cleaned;
2605
+ }
2606
+ return out;
2607
+ }
2608
+ function readBrowser(userAgent) {
2609
+ const ua = typeof userAgent === "string" ? userAgent : "";
2610
+ if (ua.length === 0) return void 0;
2611
+ if (/Edg[A-Z]?\//.test(ua)) return "Edge";
2612
+ if (/OPR\/|Opera/.test(ua)) return "Opera";
2613
+ if (/SamsungBrowser\//.test(ua)) return "Samsung Internet";
2614
+ if (/Firefox\/|FxiOS\//.test(ua)) return "Firefox";
2615
+ if (/Chrome\/|CriOS\//.test(ua)) return "Chrome";
2616
+ if (/Safari\//.test(ua)) return "Safari";
2617
+ return void 0;
2618
+ }
2619
+ function readOs(userAgent) {
2620
+ const ua = typeof userAgent === "string" ? userAgent : "";
2621
+ if (ua.length === 0) return void 0;
2622
+ if (/iPhone|iPad|iPod/.test(ua)) return "iOS";
2623
+ if (/Android/.test(ua)) return "Android";
2624
+ if (/CrOS/.test(ua)) return "ChromeOS";
2625
+ if (/Windows/.test(ua)) return "Windows";
2626
+ if (/Mac OS X|Macintosh/.test(ua)) return "macOS";
2627
+ if (/Linux/.test(ua)) return "Linux";
2628
+ return void 0;
2629
+ }
2630
+ function viewportBucket(width) {
2631
+ if (typeof width !== "number" || !(width > 0)) return void 0;
2632
+ if (width < 480) return "xs";
2633
+ if (width < 768) return "sm";
2634
+ if (width < 1024) return "md";
2635
+ if (width < 1280) return "lg";
2636
+ return "xl";
2637
+ }
2638
+ function captureSessionContext(input) {
2639
+ const context = { ...readCampaign(input.search) };
2640
+ const referrer = scrubReferrer(input.referrer);
2641
+ if (referrer) context.referrer = referrer;
2642
+ const browser = readBrowser(input.userAgent);
2643
+ if (browser) context.browser = browser;
2644
+ const os = readOs(input.userAgent);
2645
+ if (os) context.os = os;
2646
+ const viewport = viewportBucket(input.viewportWidth);
2647
+ if (viewport) context.viewport = viewport;
2648
+ return Object.keys(context).length > 0 ? context : void 0;
2649
+ }
2650
+
2651
+ // src/telemetry/consent.ts
2652
+ var STATES = ["ON", "REQUIRE_CONSENT", "OFF"];
2653
+ function createConsentCache(store, key) {
2654
+ function read() {
2655
+ const raw = readJson(store, key);
2656
+ if (!raw) return {};
2657
+ const state = typeof raw.state === "string" && STATES.indexOf(raw.state) !== -1 ? raw.state : void 0;
2658
+ const consent = typeof raw.consent === "boolean" ? raw.consent : void 0;
2659
+ return { state, consent };
2660
+ }
2661
+ function write(next) {
2662
+ if (next.state === void 0 && next.consent === void 0) {
2663
+ store.removeItem(key);
2664
+ return;
2665
+ }
2666
+ writeJson(store, key, next);
2667
+ }
2668
+ return {
2669
+ state: () => read().state ?? null,
2670
+ consent: () => read().consent ?? null,
2671
+ setState(state) {
2672
+ write({ ...read(), state });
2673
+ },
2674
+ setConsent(granted) {
2675
+ const current = read();
2676
+ write({ state: current.state, consent: granted ? true : void 0 });
2677
+ },
2678
+ clearState() {
2679
+ write({ state: void 0, consent: read().consent });
2680
+ },
2681
+ shouldCapture() {
2682
+ const { state, consent } = read();
2683
+ if (state === void 0) return false;
2684
+ if (state === "OFF") return false;
2685
+ if (state === "REQUIRE_CONSENT") return consent === true;
2686
+ return true;
2687
+ }
2688
+ };
2689
+ }
2690
+
2691
+ // src/telemetry/device.ts
2692
+ var MOBILE = /iPhone|iPod|Android.*Mobile|Windows Phone|BlackBerry|Opera Mini|IEMobile/i;
2693
+ var TABLET = /iPad|Tablet|PlayBook|Silk|Kindle/i;
2694
+ function deviceClass(nav) {
2695
+ const ua = nav?.userAgent ?? "";
2696
+ if (TABLET.test(ua)) return "tablet";
2697
+ if (nav?.platform === "MacIntel" && typeof nav.maxTouchPoints === "number" && nav.maxTouchPoints > 1) {
2698
+ return "tablet";
2699
+ }
2700
+ if (MOBILE.test(ua)) return "mobile";
2701
+ if (/Android/i.test(ua)) return "tablet";
2702
+ return "desktop";
2703
+ }
2704
+
2705
+ // src/telemetry/describe.ts
2706
+ var MAX_TEXT_LENGTH = 100;
2707
+ var PII_SCAN_MAX = 4096;
2708
+ var VALUE_BEARING = ["INPUT", "TEXTAREA", "SELECT"];
2709
+ function readableCopy(raw) {
2710
+ if (raw.length === 0) return void 0;
2711
+ if (containsPii(raw.slice(0, PII_SCAN_MAX))) return void 0;
2712
+ return raw.slice(0, MAX_TEXT_LENGTH);
2713
+ }
2714
+ function describeElementPayload(element, describeElement) {
2715
+ const tag = (element.tagName ?? "").toUpperCase();
2716
+ const payload = { tag: tag.toLowerCase() };
2717
+ const role = element.getAttribute("role");
2718
+ if (role) payload.role = role.slice(0, 64);
2719
+ const type = element.getAttribute("type");
2720
+ if (type) payload.type = type.slice(0, 32);
2721
+ if (VALUE_BEARING.indexOf(tag) === -1) {
2722
+ const text = (element.textContent ?? "").replace(/\s+/g, " ").trim();
2723
+ const readable = readableCopy(text);
2724
+ if (readable) payload.text = readable;
2725
+ } else {
2726
+ const label = element.getAttribute("aria-label") ?? element.getAttribute("name");
2727
+ const readable = readableCopy(label ?? "");
2728
+ if (readable) payload.label = readable;
2729
+ }
2730
+ if (describeElement) {
2731
+ const fingerprint = describeElement(element);
2732
+ if (fingerprint && Object.keys(fingerprint).length > 0) {
2733
+ payload.fingerprint = fingerprint;
2734
+ }
2735
+ }
2736
+ return payload;
2737
+ }
2738
+ function describeClick(element, describeElement) {
2739
+ return describeElementPayload(element, describeElement);
2740
+ }
2741
+ function describeField(element, describeElement) {
2742
+ const payload = describeElementPayload(element, describeElement);
2743
+ const valid = element.validity?.valid;
2744
+ if (typeof valid === "boolean") payload.valid = valid;
2745
+ return payload;
2746
+ }
2747
+ function describeSubmit(form, describeElement) {
2748
+ const payload = describeElementPayload(form, describeElement);
2749
+ const name = form.getAttribute("name") ?? form.getAttribute("id");
2750
+ if (name) payload.form = name.slice(0, MAX_TEXT_LENGTH);
2751
+ const count = form.elements?.length;
2752
+ if (typeof count === "number") payload.fieldCount = count;
2753
+ return payload;
2754
+ }
2755
+
2756
+ // src/telemetry/errors.ts
2757
+ var REQUEST_METHODS = [
2758
+ "GET",
2759
+ "POST",
2760
+ "PUT",
2761
+ "PATCH",
2762
+ "DELETE",
2763
+ "HEAD",
2764
+ "OPTIONS"
2765
+ ];
2766
+ var OTHER_METHOD = "OTHER";
2767
+ var FAILED_STATUS_MIN = 400;
2768
+ var ERROR_MESSAGE_MAX = 200;
2769
+ var CONTROL_CHARACTERS2 = /[\u0000-\u001f\u007f]+/g;
2770
+ function readErrorMessage(raw) {
2771
+ let text = null;
2772
+ if (typeof raw === "string") text = raw;
2773
+ else if (raw && typeof raw === "object") {
2774
+ const message = raw.message;
2775
+ if (typeof message === "string") text = message;
2776
+ }
2777
+ if (text === null) return null;
2778
+ const flattened = text.replace(CONTROL_CHARACTERS2, " ").trim();
2779
+ if (flattened.length === 0) return null;
2780
+ return flattened.length > ERROR_MESSAGE_MAX ? flattened.slice(0, ERROR_MESSAGE_MAX) : flattened;
2781
+ }
2782
+ function normalizeMethod(raw) {
2783
+ if (typeof raw !== "string") return "GET";
2784
+ const upper = raw.toUpperCase();
2785
+ return REQUEST_METHODS.indexOf(upper) === -1 ? OTHER_METHOD : upper;
2786
+ }
2787
+ function isFailedStatus(status) {
2788
+ return typeof status === "number" && status >= FAILED_STATUS_MIN;
2789
+ }
2790
+ function installNetworkWatch(options) {
2791
+ const { target, report, ignore } = options;
2792
+ const restores = [];
2793
+ function emit(method, status, raw) {
2794
+ try {
2795
+ if (!isFailedStatus(status)) return;
2796
+ if (raw !== null && ignore?.(raw)) return;
2797
+ report({
2798
+ method,
2799
+ status,
2800
+ url: raw === null ? null : normalizeRoute(raw, target.location?.href)
2801
+ });
2802
+ } catch {
2803
+ }
2804
+ }
2805
+ function installFetch() {
2806
+ const original = target.fetch;
2807
+ if (typeof original !== "function") return;
2808
+ const wrapper = function(...args) {
2809
+ const result = original.apply(this, args);
2810
+ try {
2811
+ const promise = result;
2812
+ if (typeof promise?.then !== "function") return result;
2813
+ promise.then(
2814
+ (response) => {
2815
+ const status = response?.status;
2816
+ emit(fetchMethod(args), status, fetchUrl(args));
2817
+ },
2818
+ () => {
2819
+ }
2820
+ );
2821
+ } catch {
2822
+ }
2823
+ return result;
2824
+ };
2825
+ target.fetch = wrapper;
2826
+ restores.push(() => {
2827
+ if (target.fetch === wrapper) target.fetch = original;
2828
+ });
2829
+ }
2830
+ function installXhr() {
2831
+ const proto = target.XMLHttpRequest?.prototype;
2832
+ const originalOpen = proto?.open;
2833
+ const originalSend = proto?.send;
2834
+ if (!proto || typeof originalOpen !== "function" || typeof originalSend !== "function") {
2835
+ return;
2836
+ }
2837
+ if (typeof WeakMap !== "function") return;
2838
+ const pending = /* @__PURE__ */ new WeakMap();
2839
+ const open = function(...args) {
2840
+ const result = originalOpen.apply(this, args);
2841
+ try {
2842
+ pending.set(this, {
2843
+ method: normalizeMethod(args[0]),
2844
+ url: typeof args[1] === "string" ? args[1] : null,
2845
+ listening: pending.get(this)?.listening ?? false
2846
+ });
2847
+ } catch {
2848
+ }
2849
+ return result;
2850
+ };
2851
+ const send = function(...args) {
2852
+ try {
2853
+ const xhr = this;
2854
+ const record = pending.get(this);
2855
+ if (typeof xhr.addEventListener === "function" && record?.listening !== true) {
2856
+ if (record) record.listening = true;
2857
+ xhr.addEventListener("loadend", () => {
2858
+ const current = pending.get(this);
2859
+ emit(current?.method ?? "GET", xhr.status, current?.url ?? null);
2860
+ });
2861
+ }
2862
+ } catch {
2863
+ }
2864
+ return originalSend.apply(this, args);
2865
+ };
2866
+ proto.open = open;
2867
+ restores.push(() => {
2868
+ if (proto.open === open) proto.open = originalOpen;
2869
+ });
2870
+ proto.send = send;
2871
+ restores.push(() => {
2872
+ if (proto.send === send) proto.send = originalSend;
2873
+ });
2874
+ }
2875
+ try {
2876
+ installFetch();
2877
+ installXhr();
2878
+ } catch {
2879
+ for (const restore of restores) {
2880
+ try {
2881
+ restore();
2882
+ } catch {
2883
+ }
2884
+ }
2885
+ return null;
2886
+ }
2887
+ if (restores.length === 0) return null;
2888
+ return () => {
2889
+ for (const restore of restores) {
2890
+ try {
2891
+ restore();
2892
+ } catch {
2893
+ }
2894
+ }
2895
+ restores.length = 0;
2896
+ };
2897
+ }
2898
+ function fetchMethod(args) {
2899
+ const init = args[1];
2900
+ if (init && typeof init === "object" && typeof init.method === "string") {
2901
+ return normalizeMethod(init.method);
2902
+ }
2903
+ const input = args[0];
2904
+ if (input && typeof input === "object" && typeof input.method === "string") {
2905
+ return normalizeMethod(input.method);
2906
+ }
2907
+ return "GET";
2908
+ }
2909
+ function fetchUrl(args) {
2910
+ const input = args[0];
2911
+ if (typeof input === "string") return input;
2912
+ if (!input || typeof input !== "object") return null;
2913
+ const url = input.url;
2914
+ if (typeof url === "string") return url;
2915
+ const href = input.href;
2916
+ return typeof href === "string" ? href : null;
2917
+ }
2918
+
2919
+ // src/telemetry/events.ts
2920
+ function buildEvent(id, name, occurredAt, context, payload) {
2921
+ const event = {
2922
+ id,
2923
+ name,
2924
+ occurredAt,
2925
+ sessionId: context.sessionId
2926
+ };
2927
+ if (context.userId) event.userId = context.userId;
2928
+ if (context.visitorId) event.visitorId = context.visitorId;
2929
+ if (context.route) event.route = context.route;
2930
+ if (context.localeCode) event.localeCode = context.localeCode;
2931
+ if (context.deviceClass) event.deviceClass = context.deviceClass;
2932
+ if (context.flowSessionId) event.flowSessionId = context.flowSessionId;
2933
+ if (payload && Object.keys(payload).length > 0) event.payload = payload;
2934
+ return event;
2935
+ }
2936
+
2937
+ // src/telemetry/fields.ts
2938
+ var NON_FIELD_TYPES = [
2939
+ "button",
2940
+ "submit",
2941
+ "reset",
2942
+ "image",
2943
+ "hidden",
2944
+ "checkbox",
2945
+ "radio"
2946
+ ];
2947
+ var FIELD_TAGS = ["INPUT", "TEXTAREA", "SELECT"];
2948
+ function isFieldElement(element) {
2949
+ if (!element || typeof element.getAttribute !== "function") return false;
2950
+ const tag = (element.tagName ?? "").toUpperCase();
2951
+ if (FIELD_TAGS.indexOf(tag) === -1) return false;
2952
+ if (tag !== "INPUT") return true;
2953
+ const type = (element.getAttribute("type") ?? "text").toLowerCase();
2954
+ return NON_FIELD_TYPES.indexOf(type) === -1;
2955
+ }
2956
+ function isEmpty(element) {
2957
+ const value = element.value;
2958
+ return typeof value === "string" ? value.length === 0 : value == null;
2959
+ }
2960
+ function createFieldObserver(options) {
2961
+ let active = null;
2962
+ return {
2963
+ focus(element) {
2964
+ if (!isFieldElement(element)) return;
2965
+ active = {
2966
+ element,
2967
+ empty: isEmpty(element),
2968
+ filled: false,
2969
+ cleared: false
2970
+ };
2971
+ options.emit("field_focused", element);
2972
+ },
2973
+ change(element) {
2974
+ if (!active || active.element !== element) return;
2975
+ const empty = isEmpty(element);
2976
+ if (empty === active.empty) return;
2977
+ active.empty = empty;
2978
+ if (!empty) {
2979
+ if (active.filled) return;
2980
+ active.filled = true;
2981
+ options.emit("input_filled", element);
2982
+ return;
2983
+ }
2984
+ if (active.cleared) return;
2985
+ active.cleared = true;
2986
+ options.emit("input_cleared", element);
2987
+ },
2988
+ blur(element) {
2989
+ if (active && active.element === element) active = null;
2990
+ }
2991
+ };
2992
+ }
2993
+
2994
+ // src/telemetry/interactive.ts
2995
+ var INTERACTIVE_TAGS = [
2996
+ "A",
2997
+ "BUTTON",
2998
+ "INPUT",
2999
+ "SELECT",
3000
+ "TEXTAREA",
3001
+ "SUMMARY",
3002
+ "OPTION",
3003
+ "LABEL"
3004
+ ];
3005
+ var INTERACTIVE_ROLES = [
3006
+ "button",
3007
+ "link",
3008
+ "checkbox",
3009
+ "radio",
3010
+ "switch",
3011
+ "tab",
3012
+ "menuitem",
3013
+ "menuitemcheckbox",
3014
+ "menuitemradio",
3015
+ "option",
3016
+ "combobox",
3017
+ "textbox",
3018
+ "searchbox",
3019
+ "slider",
3020
+ "spinbutton",
3021
+ "treeitem"
3022
+ ];
3023
+ var MAX_ANCESTOR_DEPTH = 6;
3024
+ function isInteractiveSelf(element, cursorOf) {
3025
+ const tag = (element.tagName ?? "").toUpperCase();
3026
+ if (INTERACTIVE_TAGS.indexOf(tag) !== -1) return true;
3027
+ const role = element.getAttribute("role");
3028
+ if (role && INTERACTIVE_ROLES.indexOf(role.toLowerCase()) !== -1) return true;
3029
+ if (element.getAttribute("onclick") !== null) return true;
3030
+ const tabIndex = element.getAttribute("tabindex");
3031
+ if (tabIndex !== null && Number(tabIndex) >= 0) return true;
3032
+ if (element.getAttribute("contenteditable") === "true") return true;
3033
+ return cursorOf ? cursorOf(element) === "pointer" : false;
3034
+ }
3035
+ function findInteractive(target, cursorOf) {
3036
+ let node = target;
3037
+ for (let depth = 0; node && depth < MAX_ANCESTOR_DEPTH; depth++) {
3038
+ if (isInteractiveSelf(node, cursorOf)) return node;
3039
+ node = node.parentElement;
3040
+ }
3041
+ return null;
3042
+ }
3043
+
3044
+ // src/telemetry/scroll.ts
3045
+ var SCROLL_MILESTONES = [25, 50, 75, 100];
3046
+ var SCROLL_THROTTLE_MS = 250;
3047
+ var FINAL_MILESTONE = 100;
3048
+ function createScrollObserver(options) {
3049
+ const {
3050
+ readDepth,
3051
+ emit,
3052
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
3053
+ clearTimeoutImpl = (handle) => clearTimeout(handle),
3054
+ throttleMs = SCROLL_THROTTLE_MS
3055
+ } = options;
3056
+ let reported = 0;
3057
+ let pending = null;
3058
+ function evaluate() {
3059
+ pending = null;
3060
+ let depth = null;
3061
+ try {
3062
+ depth = readDepth();
3063
+ } catch {
3064
+ return;
3065
+ }
3066
+ if (depth === null) return;
3067
+ let reached = 0;
3068
+ for (const milestone of SCROLL_MILESTONES) {
3069
+ if (depth >= milestone) reached = milestone;
3070
+ }
3071
+ if (reached <= reported) return;
3072
+ reported = reached;
3073
+ emit(reached);
3074
+ }
3075
+ return {
3076
+ scrolled() {
3077
+ if (pending !== null) return;
3078
+ if (reported >= FINAL_MILESTONE) return;
3079
+ try {
3080
+ pending = setTimeoutImpl(evaluate, throttleMs);
3081
+ } catch {
3082
+ pending = null;
3083
+ }
3084
+ },
3085
+ reset() {
3086
+ reported = 0;
3087
+ if (pending !== null) {
3088
+ try {
3089
+ clearTimeoutImpl(pending);
3090
+ } catch {
3091
+ }
3092
+ pending = null;
3093
+ }
3094
+ },
3095
+ dispose() {
3096
+ if (pending === null) return;
3097
+ try {
3098
+ clearTimeoutImpl(pending);
3099
+ } catch {
3100
+ }
3101
+ pending = null;
3102
+ }
3103
+ };
3104
+ }
3105
+ function scrollDepthPercent(metrics) {
3106
+ const viewport = metrics.innerHeight ?? 0;
3107
+ const total = metrics.documentHeight ?? 0;
3108
+ const scrollable = total - viewport;
3109
+ if (!(scrollable > 0) || !(viewport > 0)) return null;
3110
+ const scrolled = (metrics.scrollY ?? 0) + viewport;
3111
+ const percent = scrolled / total * 100;
3112
+ if (!(percent > 0)) return 0;
3113
+ return percent > 100 ? 100 : percent;
3114
+ }
3115
+
3116
+ // src/telemetry/session.ts
3117
+ var SESSION_WINDOW_MS = 30 * 60 * 1e3;
3118
+ var TOUCH_WRITE_INTERVAL_MS = 60 * 1e3;
3119
+ function createSessionStore({
3120
+ store,
3121
+ key,
3122
+ now,
3123
+ fill,
3124
+ windowMs = SESSION_WINDOW_MS
3125
+ }) {
3126
+ function touch() {
3127
+ const at = now();
3128
+ const existing = readJson(store, key);
3129
+ const alive = existing && typeof existing.id === "string" && existing.id.length > 0 && typeof existing.seen === "number" && at - existing.seen < windowMs;
3130
+ if (alive) {
3131
+ if (at - existing.seen >= TOUCH_WRITE_INTERVAL_MS) {
3132
+ writeJson(store, key, { id: existing.id, seen: at });
3133
+ }
3134
+ return existing.id;
3135
+ }
3136
+ const id = ulid(at, fill);
3137
+ writeJson(store, key, { id, seen: at });
3138
+ return id;
3139
+ }
3140
+ return {
3141
+ touch,
3142
+ peek: () => readJson(store, key),
3143
+ clear() {
3144
+ store.removeItem(key);
3145
+ }
3146
+ };
3147
+ }
3148
+
3149
+ // src/telemetry/transport.ts
3150
+ var FLUSH_EVENT_THRESHOLD = 20;
3151
+ var FLUSH_INTERVAL_MS = 5e3;
3152
+ var MAX_EVENTS_PER_REQUEST = 100;
3153
+ var MAX_REQUEST_BYTES = 64 * 1024;
3154
+ var MAX_QUEUED_EVENTS = 1e3;
3155
+ var DEFAULT_API_BASE2 = "https://api.arcyai.com";
3156
+ var TELEMETRY_PATH = "/api/v1/sdk/telemetry";
3157
+ function utf8Length(value) {
3158
+ let bytes = 0;
3159
+ for (let i = 0; i < value.length; i++) {
3160
+ const code = value.charCodeAt(i);
3161
+ if (code < 128) bytes += 1;
3162
+ else if (code < 2048) bytes += 2;
3163
+ else if (code >= 55296 && code <= 56319) {
3164
+ bytes += 4;
3165
+ i++;
3166
+ } else bytes += 3;
3167
+ }
3168
+ return bytes;
3169
+ }
3170
+ function chunkEvents(events) {
3171
+ const chunks = [];
3172
+ let current = [];
3173
+ let bytes = 13;
3174
+ for (const event of events) {
3175
+ const size2 = utf8Length(JSON.stringify(event)) + 1;
3176
+ const wouldOverflow = current.length >= MAX_EVENTS_PER_REQUEST || current.length > 0 && bytes + size2 > MAX_REQUEST_BYTES;
3177
+ if (wouldOverflow) {
3178
+ chunks.push(current);
3179
+ current = [];
3180
+ bytes = 13;
3181
+ }
3182
+ current.push(event);
3183
+ bytes += size2;
3184
+ }
3185
+ if (current.length > 0) chunks.push(current);
3186
+ return chunks;
3187
+ }
3188
+ function createTransport(options) {
3189
+ const {
3190
+ token,
3191
+ apiBase = DEFAULT_API_BASE2,
3192
+ fetchImpl,
3193
+ sendBeacon,
3194
+ setTimeoutImpl = (fn, ms) => setTimeout(fn, ms),
3195
+ clearTimeoutImpl = (handle) => clearTimeout(handle),
3196
+ sessionStore,
3197
+ stopKey,
3198
+ onTelemetryOff
3199
+ } = options;
3200
+ const url = `${apiBase}${TELEMETRY_PATH}`;
3201
+ let queue = [];
3202
+ let timer = null;
3203
+ let stopReason = readPersistedStop();
3204
+ let inFlight = Promise.resolve();
3205
+ function readPersistedStop() {
3206
+ if (!sessionStore || !stopKey) return null;
3207
+ return sessionStore.getItem(stopKey) === "tracked_events_cap" ? "tracked_events_cap" : null;
3208
+ }
3209
+ function stop(reason) {
3210
+ stopReason = reason;
3211
+ queue = [];
3212
+ cancelTimer();
3213
+ if (reason === "tracked_events_cap") {
3214
+ if (sessionStore && stopKey) sessionStore.setItem(stopKey, reason);
3215
+ } else if (onTelemetryOff) {
3216
+ onTelemetryOff();
3217
+ }
3218
+ }
3219
+ function cancelTimer() {
3220
+ if (timer !== null) {
3221
+ clearTimeoutImpl(timer);
3222
+ timer = null;
3223
+ }
3224
+ }
3225
+ function scheduleFlush() {
3226
+ if (timer !== null) return;
3227
+ timer = setTimeoutImpl(() => {
3228
+ timer = null;
3229
+ void flush(false);
3230
+ }, FLUSH_INTERVAL_MS);
3231
+ }
3232
+ function enqueue(event) {
3233
+ if (stopReason !== null) return;
3234
+ queue.push(event);
3235
+ if (queue.length > MAX_QUEUED_EVENTS) {
3236
+ queue.splice(0, queue.length - MAX_QUEUED_EVENTS);
3237
+ }
3238
+ if (queue.length >= FLUSH_EVENT_THRESHOLD) {
3239
+ void flush(false);
3240
+ return;
3241
+ }
3242
+ scheduleFlush();
3243
+ }
3244
+ async function rejectionCode(response) {
3245
+ try {
3246
+ const body = await response.json();
3247
+ if (body && typeof body === "object") {
3248
+ const code = body.code;
3249
+ if (typeof code === "string") return code;
3250
+ }
3251
+ } catch {
3252
+ }
3253
+ return null;
3254
+ }
3255
+ async function send(chunk) {
3256
+ if (!fetchImpl) return chunk;
3257
+ let response;
3258
+ try {
3259
+ response = await fetchImpl(url, {
3260
+ method: "POST",
3261
+ headers: {
3262
+ "Content-Type": "application/json",
3263
+ Authorization: `Bearer ${token}`
3264
+ },
3265
+ body: JSON.stringify({ events: chunk }),
3266
+ // Not `keepalive` here: the ordinary path has no unload pressure, and
3267
+ // keepalive requests share a small per-page budget that the final
3268
+ // flush needs.
3269
+ credentials: "omit"
3270
+ });
3271
+ } catch {
3272
+ return chunk;
3273
+ }
3274
+ if (response.status === 403) {
3275
+ const code = await rejectionCode(response);
3276
+ if (code === "telemetry_off") {
3277
+ stop("telemetry_off");
3278
+ return [];
3279
+ }
3280
+ return [];
3281
+ }
3282
+ if (response.status === 429) {
3283
+ const code = await rejectionCode(response);
3284
+ if (code === "tracked_events_cap") {
3285
+ stop("tracked_events_cap");
3286
+ return [];
3287
+ }
3288
+ return chunk;
3289
+ }
3290
+ if (response.status >= 500) return chunk;
3291
+ return [];
3292
+ }
3293
+ function sendFinal(chunk) {
3294
+ const body = JSON.stringify({ events: chunk });
3295
+ const beaconUrl = `${url}?token=${encodeURIComponent(token)}`;
3296
+ if (sendBeacon && typeof Blob !== "undefined") {
3297
+ try {
3298
+ if (sendBeacon(beaconUrl, new Blob([body], { type: "application/json" }))) {
3299
+ return true;
3300
+ }
3301
+ } catch {
3302
+ }
3303
+ }
3304
+ if (fetchImpl) {
3305
+ try {
3306
+ void fetchImpl(url, {
3307
+ method: "POST",
3308
+ headers: {
3309
+ "Content-Type": "application/json",
3310
+ Authorization: `Bearer ${token}`
3311
+ },
3312
+ body,
3313
+ keepalive: true,
3314
+ credentials: "omit"
3315
+ }).catch(() => {
3316
+ });
3317
+ return true;
3318
+ } catch {
3319
+ return false;
3320
+ }
3321
+ }
3322
+ return false;
3323
+ }
3324
+ async function flush(final = false) {
3325
+ cancelTimer();
3326
+ if (stopReason !== null || queue.length === 0) {
3327
+ await inFlight;
3328
+ return;
3329
+ }
3330
+ const batch = queue;
3331
+ queue = [];
3332
+ const chunks = chunkEvents(batch);
3333
+ if (final) {
3334
+ for (const chunk of chunks) sendFinal(chunk);
3335
+ return;
3336
+ }
3337
+ inFlight = inFlight.then(async () => {
3338
+ const retained = [];
3339
+ for (const chunk of chunks) {
3340
+ if (stopReason !== null) return;
3341
+ retained.push(...await send(chunk));
3342
+ }
3343
+ if (retained.length > 0 && stopReason === null) {
3344
+ queue = retained.concat(queue);
3345
+ if (queue.length > MAX_QUEUED_EVENTS) {
3346
+ queue.splice(0, queue.length - MAX_QUEUED_EVENTS);
3347
+ }
3348
+ scheduleFlush();
3349
+ }
3350
+ });
3351
+ await inFlight;
3352
+ }
3353
+ return {
3354
+ enqueue,
3355
+ flush,
3356
+ stopped: () => stopReason,
3357
+ discard() {
3358
+ queue = [];
3359
+ cancelTimer();
3360
+ },
3361
+ dispose() {
3362
+ cancelTimer();
3363
+ }
3364
+ };
3365
+ }
3366
+
3367
+ // src/telemetry/collector.ts
3368
+ var ROUTE_CHANGE_EVENT = "arcy:routechange";
3369
+ var SESSION_KEY_PREFIX = "arcy.session.";
3370
+ var CONSENT_KEY_PREFIX = "arcy.telemetry.";
3371
+ var STOP_KEY_PREFIX = "arcy.telemetry.stop.";
3372
+ function isSynthetic(raw) {
3373
+ return raw.isTrusted === false;
3374
+ }
3375
+ function createTelemetry(options) {
3376
+ const {
3377
+ token,
3378
+ apiBase,
3379
+ describeElement,
3380
+ getUserId,
3381
+ getVisitorId,
3382
+ getFlowSessionId,
3383
+ now = () => Date.now(),
3384
+ fill
3385
+ } = options;
3386
+ const win = options.win !== void 0 ? options.win : typeof window !== "undefined" ? window : null;
3387
+ const doc = options.doc !== void 0 ? options.doc : typeof document !== "undefined" ? document : null;
3388
+ const localStore = options.localStore ?? browserLocalStorage();
3389
+ const consent = createConsentCache(
3390
+ localStore,
3391
+ CONSENT_KEY_PREFIX + token
3392
+ );
3393
+ const session = createSessionStore({
3394
+ store: localStore,
3395
+ key: SESSION_KEY_PREFIX + token,
3396
+ now,
3397
+ fill
3398
+ });
3399
+ const transport = createTransport({
3400
+ token,
3401
+ apiBase,
3402
+ fetchImpl: options.fetchImpl !== void 0 ? options.fetchImpl : typeof fetch !== "undefined" ? fetch.bind(globalThis) : null,
3403
+ sendBeacon: win?.navigator?.sendBeacon && typeof win.navigator.sendBeacon === "function" ? win.navigator.sendBeacon.bind(win.navigator) : null,
3404
+ setTimeoutImpl: options.setTimeoutImpl,
3405
+ clearTimeoutImpl: options.clearTimeoutImpl,
3406
+ sessionStore: options.sessionStore ?? browserSessionStorage(),
3407
+ stopKey: STOP_KEY_PREFIX + token,
3408
+ onTelemetryOff: () => consent.clearState()
3409
+ });
3410
+ const resolvedDeviceClass = deviceClass(win?.navigator);
3411
+ const localeCode = win?.navigator?.language ?? void 0;
3412
+ let started = false;
3413
+ let halted = false;
3414
+ let lastRoute = null;
3415
+ let loadNavigationPending = false;
3416
+ function currentRoute() {
3417
+ return normalizeRoute(win?.location?.href) ?? void 0;
3418
+ }
3419
+ function capture(name, payload, fields2) {
3420
+ if (halted) return false;
3421
+ if (!consent.shouldCapture()) return false;
3422
+ if (transport.stopped() !== null) return false;
3423
+ const at = now();
3424
+ const sessionId = session.touch();
3425
+ const event = buildEvent(
3426
+ ulid(at, fill),
3427
+ name,
3428
+ at,
3429
+ {
3430
+ sessionId,
3431
+ userId: getUserId?.() ?? void 0,
3432
+ visitorId: getVisitorId?.() ?? void 0,
3433
+ route: currentRoute(),
3434
+ localeCode,
3435
+ deviceClass: resolvedDeviceClass,
3436
+ flowSessionId: fields2?.flowSessionId ?? getFlowSessionId?.() ?? void 0
3437
+ },
3438
+ payload
3439
+ );
3440
+ if (fields2?.flowId) event.flowId = fields2.flowId;
3441
+ if (fields2?.flowVersion) event.flowVersion = fields2.flowVersion;
3442
+ if (fields2?.stepCvid) event.stepCvid = fields2.stepCvid;
3443
+ transport.enqueue(event);
3444
+ return true;
3445
+ }
3446
+ const onClick = (event) => {
3447
+ try {
3448
+ const raw = event;
3449
+ if (isSynthetic(raw)) return;
3450
+ const cursorOf = win?.getComputedStyle ? (element2) => {
3451
+ try {
3452
+ return win.getComputedStyle(element2).cursor ?? null;
3453
+ } catch {
3454
+ return null;
3455
+ }
3456
+ } : void 0;
3457
+ const element = findInteractive(raw.target, cursorOf);
3458
+ if (!element) return;
3459
+ capture("interaction_click", describeClick(element, describeElement));
3460
+ } catch {
3461
+ }
3462
+ };
3463
+ const onSubmit = (event) => {
3464
+ try {
3465
+ const raw = event;
3466
+ if (isSynthetic(raw)) return;
3467
+ if (!raw.target || typeof raw.target.getAttribute !== "function") return;
3468
+ capture("form_submit", describeSubmit(raw.target, describeElement));
3469
+ } catch {
3470
+ }
3471
+ };
3472
+ const onNavigate = () => {
3473
+ try {
3474
+ const route = currentRoute() ?? null;
3475
+ if (route === lastRoute) return;
3476
+ const from = lastRoute;
3477
+ lastRoute = route;
3478
+ scroll.reset();
3479
+ capture("navigation", from ? { from } : void 0);
3480
+ } catch {
3481
+ }
3482
+ };
3483
+ const fields = createFieldObserver({
3484
+ emit: (name, element) => {
3485
+ capture(name, describeField(element, describeElement));
3486
+ }
3487
+ });
3488
+ const scroll = createScrollObserver({
3489
+ readDepth: () => scrollDepthPercent({
3490
+ scrollY: win?.scrollY,
3491
+ innerHeight: win?.innerHeight,
3492
+ documentHeight: doc?.documentElement?.scrollHeight
3493
+ }),
3494
+ emit: (percent) => {
3495
+ capture("scroll_depth", { percent });
3496
+ },
3497
+ setTimeoutImpl: options.setTimeoutImpl,
3498
+ clearTimeoutImpl: options.clearTimeoutImpl
3499
+ });
3500
+ let sessionEnded = false;
3501
+ const onFocusIn = (event) => {
3502
+ try {
3503
+ const raw = event;
3504
+ if (isSynthetic(raw) || !raw.target) return;
3505
+ fields.focus(raw.target);
3506
+ } catch {
3507
+ }
3508
+ };
3509
+ const onFieldChange = (event) => {
3510
+ try {
3511
+ const raw = event;
3512
+ if (isSynthetic(raw) || !raw.target) return;
3513
+ fields.change(raw.target);
3514
+ } catch {
3515
+ }
3516
+ };
3517
+ const onFocusOut = (event) => {
3518
+ try {
3519
+ const raw = event;
3520
+ if (!raw.target) return;
3521
+ fields.blur(raw.target);
3522
+ } catch {
3523
+ }
3524
+ };
3525
+ const onScroll = () => {
3526
+ try {
3527
+ scroll.scrolled();
3528
+ } catch {
3529
+ }
3530
+ };
3531
+ const onVisibilityChange = () => {
3532
+ try {
3533
+ if (doc?.visibilityState === "visible") {
3534
+ capture("page_visible");
3535
+ return;
3536
+ }
3537
+ capture("page_hidden");
3538
+ void transport.flush(true);
3539
+ } catch {
3540
+ }
3541
+ };
3542
+ const onPageHide = () => {
3543
+ try {
3544
+ if (!sessionEnded) {
3545
+ sessionEnded = true;
3546
+ capture("session_end");
3547
+ }
3548
+ void transport.flush(true);
3549
+ } catch {
3550
+ }
3551
+ };
3552
+ const onError = (event) => {
3553
+ try {
3554
+ const raw = event;
3555
+ captureError({
3556
+ message: readErrorMessage(raw.message) ?? "Uncaught error",
3557
+ kind: "error",
3558
+ source: scrubSource(raw.filename)
3559
+ });
3560
+ } catch {
3561
+ }
3562
+ };
3563
+ const onRejection = (event) => {
3564
+ try {
3565
+ const raw = event;
3566
+ captureError({
3567
+ // The kind alone when the reason is neither a string nor an `Error`.
3568
+ // Serializing an unknown object from the customer's application is the
3569
+ // one thing this event must never do.
3570
+ message: readErrorMessage(raw.reason) ?? "Unhandled promise rejection",
3571
+ kind: "unhandledrejection"
3572
+ });
3573
+ } catch {
3574
+ }
3575
+ };
3576
+ function captureError(encounter) {
3577
+ const payload = {
3578
+ message: encounter.message,
3579
+ kind: encounter.kind
3580
+ };
3581
+ if (encounter.source) payload.source = encounter.source;
3582
+ capture("js_error", payload);
3583
+ }
3584
+ function scrubSource(raw) {
3585
+ if (typeof raw !== "string") return void 0;
3586
+ return normalizeRoute(raw, win?.location?.href) ?? void 0;
3587
+ }
3588
+ const ownOrigin = (apiBase ?? DEFAULT_API_BASE2).replace(/\/+$/, "");
3589
+ let uninstallNetworkWatch = null;
3590
+ return {
3591
+ start() {
3592
+ if (started || halted) return;
3593
+ started = true;
3594
+ lastRoute = currentRoute() ?? null;
3595
+ doc?.addEventListener("click", onClick, true);
3596
+ doc?.addEventListener("submit", onSubmit, true);
3597
+ doc?.addEventListener("focusin", onFocusIn, true);
3598
+ doc?.addEventListener("focusout", onFocusOut, true);
3599
+ doc?.addEventListener("input", onFieldChange, true);
3600
+ doc?.addEventListener("change", onFieldChange, true);
3601
+ win?.addEventListener(ROUTE_CHANGE_EVENT, onNavigate);
3602
+ win?.addEventListener("popstate", onNavigate);
3603
+ win?.addEventListener("scroll", onScroll, { passive: true });
3604
+ win?.addEventListener("pagehide", onPageHide);
3605
+ doc?.addEventListener("visibilitychange", onVisibilityChange);
3606
+ win?.addEventListener("error", onError);
3607
+ win?.addEventListener("unhandledrejection", onRejection);
3608
+ if (win && uninstallNetworkWatch === null) {
3609
+ uninstallNetworkWatch = installNetworkWatch({
3610
+ target: win,
3611
+ report: (failure) => {
3612
+ const payload = {
3613
+ method: failure.method,
3614
+ status: failure.status
3615
+ };
3616
+ if (failure.url) payload.url = failure.url;
3617
+ capture("request_failed", payload);
3618
+ },
3619
+ ignore: (url) => url.indexOf(ownOrigin) === 0
3620
+ });
3621
+ }
3622
+ loadNavigationPending = !capture("navigation") && consent.state() === null;
3623
+ },
3624
+ stop(options2) {
3625
+ halted = true;
3626
+ if (!started) return;
3627
+ started = false;
3628
+ doc?.removeEventListener("click", onClick, true);
3629
+ doc?.removeEventListener("submit", onSubmit, true);
3630
+ doc?.removeEventListener("focusin", onFocusIn, true);
3631
+ doc?.removeEventListener("focusout", onFocusOut, true);
3632
+ doc?.removeEventListener("input", onFieldChange, true);
3633
+ doc?.removeEventListener("change", onFieldChange, true);
3634
+ win?.removeEventListener(ROUTE_CHANGE_EVENT, onNavigate);
3635
+ win?.removeEventListener("popstate", onNavigate);
3636
+ win?.removeEventListener("scroll", onScroll);
3637
+ win?.removeEventListener("pagehide", onPageHide);
3638
+ doc?.removeEventListener("visibilitychange", onVisibilityChange);
3639
+ win?.removeEventListener("error", onError);
3640
+ win?.removeEventListener("unhandledrejection", onRejection);
3641
+ if (uninstallNetworkWatch) {
3642
+ uninstallNetworkWatch();
3643
+ uninstallNetworkWatch = null;
3644
+ }
3645
+ scroll.dispose();
3646
+ if (options2?.discard) transport.discard();
3647
+ else void transport.flush(false);
3648
+ transport.dispose();
3649
+ },
3650
+ setTelemetryState(state) {
3651
+ consent.setState(state);
3652
+ if (loadNavigationPending) {
3653
+ loadNavigationPending = false;
3654
+ capture("navigation");
3655
+ }
3656
+ },
3657
+ setTelemetryConsent(granted) {
3658
+ consent.setConsent(granted);
3659
+ if (!granted) transport.discard();
3660
+ },
3661
+ track(name, payload, fields2) {
3662
+ capture(name, payload, fields2);
3663
+ },
3664
+ flush: () => transport.flush(false),
3665
+ sessionId: () => session.touch(),
3666
+ resetSession: () => session.clear(),
3667
+ stopped: () => transport.stopped(),
3668
+ state: () => consent.state()
3669
+ };
3670
+ }
3671
+
3672
+ // src/version.ts
3673
+ var VERSION = "0.1.1";
3674
+
3675
+ // src/api.ts
3676
+ var OPTION_KEYS = [
3677
+ "telemetry",
3678
+ "locale",
3679
+ "defaultOpen",
3680
+ "contentLocale",
3681
+ "apiBase"
3682
+ ];
3683
+ function emptyIdentity() {
3684
+ return { userId: null, isAnonymous: false, attributes: {} };
3685
+ }
3686
+ function readUserHash(value, caller) {
3687
+ if (value === void 0 || value === null)
3688
+ return { ok: true, hash: void 0 };
3689
+ if (typeof value !== "string" || value.length === 0) {
3690
+ warn(`${caller}() was given a userHash that is not a string. Ignoring it.`);
3691
+ return { ok: false };
3692
+ }
3693
+ return { ok: true, hash: value };
3694
+ }
3695
+ function sanitizeAttributes(attributes, method) {
3696
+ if (attributes === void 0) return {};
3697
+ if (typeof attributes !== "object" || attributes === null || Array.isArray(attributes)) {
3698
+ warn(`${method}() attributes must be an object. Ignoring the call.`);
3699
+ return null;
3700
+ }
3701
+ const out = {};
3702
+ for (const [key, value] of Object.entries(attributes)) {
3703
+ if (value === void 0) continue;
3704
+ if (value === null) {
3705
+ out[key] = null;
3706
+ continue;
3707
+ }
3708
+ const type = typeof value;
3709
+ if (type === "string" || type === "number" || type === "boolean") {
3710
+ out[key] = value;
3711
+ continue;
3712
+ }
3713
+ if (Array.isArray(value) && value.every((v) => typeof v === "string")) {
3714
+ out[key] = value;
3715
+ continue;
3716
+ }
3717
+ warn(
3718
+ `${method}() dropped "${key}": an attribute must be a string, number, boolean, null, or an array of strings.`
3719
+ );
3720
+ }
3721
+ return out;
3722
+ }
3723
+ var unavailableChunks = {
3724
+ cdnBase: null,
3725
+ loadChat: () => Promise.resolve(null),
3726
+ loadFlow: () => Promise.resolve(null),
3727
+ loadPicker: () => Promise.resolve(null)
3728
+ };
3729
+ function createArcy(chunks) {
3730
+ return createArcyInternals({ chunks }).api;
3731
+ }
3732
+ function createArcyInternals(internalOptions = {}) {
3733
+ const chunks = internalOptions.chunks ?? unavailableChunks;
3734
+ const emitter = createEmitter();
3735
+ function resolveApiBase() {
3736
+ const fromOptions = options.apiBase;
3737
+ if (typeof fromOptions === "string" && fromOptions.length > 0) {
3738
+ return fromOptions.replace(/\/+$/, "");
3739
+ }
3740
+ return internalOptions.apiBase ?? DEFAULT_API_BASE;
3741
+ }
3742
+ let token = null;
3743
+ let options = {};
3744
+ let identity = emptyIdentity();
3745
+ let isOpen = false;
3746
+ let bar = null;
3747
+ let chatController = null;
3748
+ let flowController = null;
3749
+ let flowResumeAttempted = false;
3750
+ let previewIntent = null;
3751
+ let telemetry = null;
3752
+ let bootstrap = null;
3753
+ let sendSessionSequence = 0;
3754
+ let entryRouteValue;
3755
+ let sessionContextValue;
3756
+ let sessionContextResolved = false;
3757
+ let shellState = null;
3758
+ let visitState = null;
3759
+ let conversationStore = null;
3760
+ let visitorId = null;
3761
+ let tokenCache = null;
3762
+ let tokens = NO_TOKENS;
3763
+ let widgetIdentity = null;
3764
+ let barSubmit = null;
3765
+ let barFiles = null;
3766
+ let pendingFiles = null;
3767
+ let pendingMessage = null;
3768
+ let fillPending = false;
3769
+ let openPending = false;
3770
+ function requireInit(method) {
3771
+ if (token === null) {
3772
+ warn(`${method}() was called before init(). Ignoring.`);
3773
+ return false;
3774
+ }
3775
+ return true;
3776
+ }
3777
+ async function init(nextToken, nextOptions) {
3778
+ if (typeof nextToken !== "string" || nextToken.length === 0) {
3779
+ warn("init() needs an environment token. Ignoring the call.");
3780
+ return;
3781
+ }
3782
+ if (token !== null) {
3783
+ warn("init() has already been called. Ignoring the second call.");
3784
+ return;
3785
+ }
3786
+ token = nextToken;
3787
+ options = {};
3788
+ if (nextOptions !== void 0) {
3789
+ if (typeof nextOptions !== "object" || nextOptions === null) {
3790
+ warn("init() options must be an object. Continuing without them.");
3791
+ } else {
3792
+ for (const [key, value] of Object.entries(nextOptions)) {
3793
+ if (!OPTION_KEYS.includes(key)) {
3794
+ warn(
3795
+ `init() ignored unknown option "${key}". Only telemetry, locale, defaultOpen, contentLocale, and apiBase can be set in code; everything else is configured in the dashboard.`
3796
+ );
3797
+ continue;
3798
+ }
3799
+ options[key] = value;
3800
+ }
3801
+ }
3802
+ }
3803
+ if (typeof window !== "undefined") {
3804
+ reportCspViolations(window, {
3805
+ apiBase: resolveApiBase(),
3806
+ cdnBase: chunks.cdnBase
3807
+ });
3808
+ }
3809
+ start(nextToken);
3810
+ startDesignMode(nextToken);
3811
+ startPreviewMode(nextToken);
3812
+ await syncSession();
3813
+ emitter.emit("ready");
3814
+ if (options.defaultOpen) {
3815
+ open();
3816
+ }
3817
+ }
3818
+ function start(activeToken) {
3819
+ try {
3820
+ shellState = createShellState(activeToken);
3821
+ const hadAnonymousId = shellState.read().anonymousId !== null;
3822
+ visitorId = shellState.anonymousId();
3823
+ visitState = createVisitState(activeToken);
3824
+ if (!hadAnonymousId) visitState.markFirstVisit();
3825
+ } catch (error) {
3826
+ shellState = null;
3827
+ visitorId = null;
3828
+ warn(`The ARCY session could not be started. ${String(error)}`);
3829
+ }
3830
+ try {
3831
+ conversationStore = createConversationStore(activeToken);
3832
+ } catch (error) {
3833
+ conversationStore = null;
3834
+ warn(`The ARCY session could not be started. ${String(error)}`);
3835
+ }
3836
+ try {
3837
+ tokenCache = createTokenCache(activeToken);
3838
+ const cached = tokenCache.read();
3839
+ tokens = cached.tokens;
3840
+ widgetIdentity = cached.identity;
3841
+ } catch (error) {
3842
+ tokenCache = null;
3843
+ tokens = NO_TOKENS;
3844
+ warn(`The ARCY session could not be started. ${String(error)}`);
3845
+ }
3846
+ try {
3847
+ telemetry = createTelemetry({
3848
+ token: activeToken,
3849
+ // The same seam the bootstrap gets. Without it the collector's batches
3850
+ // leave through the real `fetch` even under test, so nothing in this
3851
+ // package could ever assert what a captured event actually carries.
3852
+ apiBase: resolveApiBase(),
3853
+ fetchImpl: internalOptions.fetchImpl,
3854
+ // 15.1 plugs in here. This is what makes a click on the customer's
3855
+ // page resolvable to the element the dashboard picked: both sides
3856
+ // produce byte-identical JSON for the same node, and the server hashes
3857
+ // it into one key (ADR 0059).
3858
+ describeElement: (element) => captureEventFingerprint(
3859
+ element
3860
+ ),
3861
+ getUserId: () => identity.isAnonymous ? null : identity.userId,
3862
+ // Slice 1.8. Read per event rather than captured, so the id a
3863
+ // `reset()` cleared stops being attached from that moment on rather
3864
+ // than at the next page load.
3865
+ getVisitorId: () => visitorId
3866
+ });
3867
+ if (options.telemetry !== void 0) {
3868
+ telemetry.setTelemetryConsent(options.telemetry === true);
3869
+ }
3870
+ telemetry.start();
3871
+ } catch (error) {
3872
+ warn(`Telemetry could not start. ${String(error)}`);
3873
+ }
3874
+ try {
3875
+ bootstrap = createSessionBootstrap({
3876
+ token: activeToken,
3877
+ apiBase: resolveApiBase(),
3878
+ fetchImpl: internalOptions.fetchImpl,
3879
+ setTimeoutImpl: internalOptions.setTimeoutImpl
3880
+ });
3881
+ } catch (error) {
3882
+ warn(`The ARCY session could not be started. ${String(error)}`);
3883
+ }
3884
+ mountWidget();
3885
+ }
3886
+ function startDesignMode(activeToken) {
3887
+ if (typeof window === "undefined") return;
3888
+ try {
3889
+ void maybeActivateDesignMode({
3890
+ win: window,
3891
+ token: activeToken,
3892
+ loadChunk: chunks.loadPicker,
3893
+ apiBase: resolveApiBase(),
3894
+ fetchImpl: internalOptions.fetchImpl !== void 0 ? internalOptions.fetchImpl : typeof fetch !== "undefined" ? fetch.bind(globalThis) : null,
3895
+ setTimeoutImpl: internalOptions.setTimeoutImpl,
3896
+ // Discard, not flush (D490, D492): the queue at this moment is the
3897
+ // operator's own page load, which is exactly what must not be sent.
3898
+ onVerified: () => telemetry?.stop({ discard: true })
3899
+ });
3900
+ } catch (error) {
3901
+ warn(`Design mode could not start. ${String(error)}`);
3902
+ }
3903
+ }
3904
+ function startPreviewMode(activeToken) {
3905
+ if (typeof window === "undefined") return;
3906
+ try {
3907
+ const intent = detectPreviewIntent(window, activeToken);
3908
+ if (!intent) return;
3909
+ telemetry?.stop({ discard: true });
3910
+ flowResumeAttempted = true;
3911
+ previewIntent = intent;
3912
+ } catch (error) {
3913
+ warn(`The preview could not start. ${String(error)}`);
3914
+ }
3915
+ }
3916
+ function verifyPreview(nonce) {
3917
+ return verifyPreviewNonce({
3918
+ apiBase: resolveApiBase(),
3919
+ token: token ?? "",
3920
+ nonce,
3921
+ sessionToken: bootstrap?.config()?.sessionToken ?? null,
3922
+ userTraits: identity.attributes,
3923
+ fetchImpl: internalOptions.fetchImpl !== void 0 ? internalOptions.fetchImpl : typeof fetch !== "undefined" ? fetch.bind(globalThis) : null,
3924
+ setTimeoutImpl: internalOptions.setTimeoutImpl
3925
+ });
3926
+ }
3927
+ function hideWidget() {
3928
+ openPending = false;
3929
+ bar?.unmount();
3930
+ bar = null;
3931
+ chatController?.destroy();
3932
+ chatController = null;
3933
+ flowController?.destroy();
3934
+ flowController = null;
3935
+ barSubmit = null;
3936
+ barFiles = null;
3937
+ pendingFiles = null;
3938
+ close();
3939
+ }
3940
+ function mountWidget() {
3941
+ try {
3942
+ if (!bar) mountBar();
3943
+ applyTheme();
3944
+ applyChrome();
3945
+ chatController?.refreshChrome();
3946
+ if (openPending) {
3947
+ openPending = false;
3948
+ open();
3949
+ }
3950
+ } catch (error) {
3951
+ warn(`The chat bar could not render. ${String(error)}`);
3952
+ }
3953
+ }
3954
+ function applyTheme() {
3955
+ try {
3956
+ const host = bar?.getHost();
3957
+ if (!bar || !host) return;
3958
+ applyTokens(host, tokens);
3959
+ bar.setGeometry({
3960
+ // One value, two readers (ADR 0101): the bar's width IS the panel's,
3961
+ // so they can never be configured out of line with each other.
3962
+ widthPercent: tokens.widthPercent ?? DEFAULT_WIDTH_PERCENT,
3963
+ bottomOffset: tokens.bottomOffset ?? DEFAULT_BOTTOM_OFFSET,
3964
+ // The resting width is the bar's alone (ADR 0149): collapsed, there
3965
+ // is no panel above it to share an edge with.
3966
+ collapsedWidth: tokens.collapsedWidth ?? DEFAULT_COLLAPSED_WIDTH
3967
+ });
3968
+ bar.setIconSize(tokens.barIconSize ?? DEFAULT_BAR_ICON_SIZE);
3969
+ } catch (error) {
3970
+ warn(`The widget could not apply its theme. ${String(error)}`);
3971
+ }
3972
+ }
3973
+ function readWidgetChrome() {
3974
+ const raw = bootstrap?.config()?.widget;
3975
+ const identity2 = readIdentity(raw) ?? widgetIdentity;
3976
+ if (!identity2) return null;
3977
+ const text = (value) => typeof value === "string" ? value : "";
3978
+ const chrome = raw?.chrome && typeof raw.chrome === "object" && !Array.isArray(raw.chrome) ? { ...FALLBACK_CHROME, ...raw.chrome } : FALLBACK_CHROME;
3979
+ const welcomeIconUrl = raw?.welcomeIconUrl;
3980
+ const chatBarIconUrl = raw?.chatBarIconUrl;
3981
+ return {
3982
+ name: identity2.name,
3983
+ logoUrl: identity2.logoUrl,
3984
+ // Re-checked here rather than trusted, same as privacyPolicyUrl below:
3985
+ // a rewritten bootstrap must not be able to point this at a
3986
+ // non-`https` URL (ADR 0050).
3987
+ welcomeIconUrl: typeof welcomeIconUrl === "string" && /^https:\/\//i.test(welcomeIconUrl) ? welcomeIconUrl : null,
3988
+ // Same re-check, same reason: this renders into an `<img src>` on a
3989
+ // page ARCY does not control (ADR 0050).
3990
+ chatBarIconUrl: typeof chatBarIconUrl === "string" && /^https:\/\//i.test(chatBarIconUrl) ? chatBarIconUrl : null,
3991
+ welcomeMessage: text(raw?.welcomeMessage),
3992
+ inputPlaceholder: text(raw?.inputPlaceholder),
3993
+ welcomeHeadline: text(raw?.welcomeHeadline),
3994
+ // Re-checked here rather than trusted: a rewritten bootstrap must not
3995
+ // be able to turn a camera prompt on for an operator who never did
3996
+ // (ADR 0050, ADR 0102).
3997
+ allowImageUpload: raw?.allowImageUpload === true,
3998
+ // Re-checked rather than trusted, for the reason above it: a rewritten
3999
+ // bootstrap must not be able to make the bar movable on an install
4000
+ // whose operator never allowed it (ADR 0050, ADR 0111).
4001
+ barDraggable: raw?.barDraggable === true,
4002
+ privacyPolicyUrl: typeof raw?.privacyPolicyUrl === "string" && /^https:\/\//i.test(raw.privacyPolicyUrl) ? raw.privacyPolicyUrl : null,
4003
+ chrome,
4004
+ // D1058: which locales the operator enabled, and which one this
4005
+ // session landed on. Shape-checked rather than trusted, the same
4006
+ // posture as everything else read out of the bootstrap (ADR 0050): a
4007
+ // row missing either half would render an unpressable picker row.
4008
+ locales: readLocales(raw?.locales),
4009
+ activeLocale: typeof raw?.activeLocale === "string" ? raw.activeLocale : ""
4010
+ };
4011
+ }
4012
+ function readLocales(value) {
4013
+ if (!Array.isArray(value)) return [];
4014
+ const out = [];
4015
+ for (const entry of value) {
4016
+ if (!entry || typeof entry !== "object") continue;
4017
+ const { code, label } = entry;
4018
+ if (typeof code !== "string" || code.length === 0) continue;
4019
+ if (typeof label !== "string" || label.length === 0) continue;
4020
+ out.push({ code, label });
4021
+ }
4022
+ return out;
4023
+ }
4024
+ function mountBar() {
4025
+ try {
4026
+ bar = createBar({
4027
+ geometry: {
4028
+ widthPercent: tokens.widthPercent ?? DEFAULT_WIDTH_PERCENT,
4029
+ bottomOffset: tokens.bottomOffset ?? DEFAULT_BOTTOM_OFFSET,
4030
+ collapsedWidth: tokens.collapsedWidth ?? DEFAULT_COLLAPSED_WIDTH
4031
+ },
4032
+ // The strings ARCY owns, from the last bootstrap or the shipped
4033
+ // English. Re-applied by `applyChrome()` once the bootstrap answers.
4034
+ labels: barLabels(),
4035
+ // Whatever the last bootstrap said, re-applied by `applyChrome()`
4036
+ // once the fresh one answers, same as the labels above.
4037
+ iconUrl: readWidgetChrome()?.chatBarIconUrl ?? null,
4038
+ // The docked launcher wears the WELCOME mark, at the size the
4039
+ // welcome screen draws it, so the two are one shape. Re-applied by
4040
+ // `applyChrome()` on every bootstrap like the icon above it.
4041
+ welcomeIconUrl: readWidgetChrome()?.welcomeIconUrl ?? null,
4042
+ // Whatever the theme currently in force says, re-applied by
4043
+ // `applyTheme()` on every theme change, same as bottomOffset below.
4044
+ iconSize: tokens.barIconSize ?? DEFAULT_BAR_ICON_SIZE,
4045
+ // A tap, or a focus, on a collapsed bar. Both mean "open": the bar
4046
+ // is an input, and an input a visitor is typing into with no surface
4047
+ // above it is the launcher's old problem in a new shape.
4048
+ onActivate: () => open(),
4049
+ // The submit handler proper belongs to the panel, which owns the
4050
+ // transcript the message lands in. Until the chunk registers one, a
4051
+ // submit opens the panel and the text is re-sent by the panel's own
4052
+ // handler on the next tick.
4053
+ onSubmit: (text) => {
4054
+ if (!fillPending) open();
4055
+ if (barSubmit) {
4056
+ barSubmit(text);
4057
+ return;
4058
+ }
4059
+ pendingMessage = text;
4060
+ },
4061
+ // The bar has already opened the file dialog by the time this runs,
4062
+ // inside the visitor's own click. All that is left is to bring the
4063
+ // panel up behind it, so the chips and any rejection have somewhere
4064
+ // to land.
4065
+ onAttach: () => {
4066
+ open();
4067
+ },
4068
+ onFiles: (files) => {
4069
+ open();
4070
+ if (barFiles) {
4071
+ barFiles(files);
4072
+ return;
4073
+ }
4074
+ pendingFiles = files;
4075
+ },
4076
+ // A pointer on the customer's own page collapses the widget back to
4077
+ // its resting pill (D890). `close()` is idempotent and emits the
4078
+ // public `close` event exactly as the collapse control does, so a
4079
+ // page listening for it cannot tell the two apart, which is right:
4080
+ // the visitor closed the chat either way.
4081
+ onDismiss: () => {
4082
+ close();
4083
+ },
4084
+ onStop: () => {
4085
+ flowController?.stop();
4086
+ flushFlowStatus();
4087
+ },
4088
+ // A resize, a rotation, or an opening keyboard all move the bar, and
4089
+ // the panel sits on the bar.
4090
+ onGeometryChange: () => chatController?.reposition(),
4091
+ // ADR 0111. The position belongs to this browser, so it is read from
4092
+ // and written to the shell key rather than sent anywhere: where a
4093
+ // visitor wants the bar is a property of the window they are in.
4094
+ readPosition: () => shellState?.read().position ?? null,
4095
+ onPositionChange: (position) => shellState?.rememberPosition(position),
4096
+ // The dock, same posture as the position above it: a visitor who
4097
+ // shrank ARCY to a corner button said so about this browser, so it
4098
+ // is remembered here and sent nowhere.
4099
+ readDocked: () => shellState?.read().docked === true,
4100
+ onDockedChange: (value) => shellState?.rememberDocked(value),
4101
+ // One press gets the visitor all the way back: `open()` undocks and
4102
+ // opens the panel, rather than leaving them with a bar they then
4103
+ // have to press again.
4104
+ onUndock: () => open()
4105
+ });
4106
+ bar.mount();
4107
+ mountChatController();
4108
+ } catch (error) {
4109
+ warn(`The chat bar could not render. ${String(error)}`);
4110
+ }
4111
+ }
4112
+ function applyChrome() {
4113
+ try {
4114
+ if (!bar) return;
4115
+ const chrome = readWidgetChrome();
4116
+ bar.setLabels(barLabels());
4117
+ if (bar.getMode() !== "flow") {
4118
+ bar.setPlaceholder(chrome?.inputPlaceholder ?? null);
4119
+ }
4120
+ bar.setAttachEnabled(chrome?.allowImageUpload === true);
4121
+ bar.setDraggable(chrome?.barDraggable === true);
4122
+ bar.setIcon(chrome?.chatBarIconUrl ?? null);
4123
+ bar.setWelcomeIcon(chrome?.welcomeIconUrl ?? null);
4124
+ } catch (error) {
4125
+ warn(`The widget could not apply its texts. ${String(error)}`);
4126
+ }
4127
+ }
4128
+ function barLabels() {
4129
+ const chrome = readWidgetChrome()?.chrome ?? FALLBACK_CHROME;
4130
+ return {
4131
+ send: chrome.send,
4132
+ attachMenu: chrome.attachMenu,
4133
+ attachImage: chrome.attachImage,
4134
+ flowStop: chrome.flowStop,
4135
+ openChat: chrome.openChat,
4136
+ moveBar: chrome.moveBar,
4137
+ flowStep: chrome.flowStep
4138
+ };
4139
+ }
4140
+ const FLOW_STEP_HOLD_MS = 600;
4141
+ const FLOW_DONE_HOLD_MS = 4e3;
4142
+ let flowStatusTimer = null;
4143
+ let flowStatusQueue = [];
4144
+ let shownFlowStatus = null;
4145
+ function startFlowStatusHold(status) {
4146
+ const hold = status.phase === "done" ? FLOW_DONE_HOLD_MS : FLOW_STEP_HOLD_MS;
4147
+ try {
4148
+ flowStatusTimer = setTimeout(() => {
4149
+ flowStatusTimer = null;
4150
+ while (flowStatusQueue.length > 0) {
4151
+ const next = flowStatusQueue.shift();
4152
+ applyFlowStatus(next);
4153
+ if (next !== null) {
4154
+ startFlowStatusHold(next);
4155
+ return;
4156
+ }
4157
+ }
4158
+ }, hold);
4159
+ } catch {
4160
+ }
4161
+ }
4162
+ function setFlowStatus(status) {
4163
+ if (status === null) {
4164
+ const endedWell = shownFlowStatus?.phase === "done" || flowStatusQueue.some((queued) => queued?.phase === "done");
4165
+ if (flowStatusTimer !== null && endedWell) {
4166
+ if (flowStatusQueue[flowStatusQueue.length - 1] !== null) {
4167
+ flowStatusQueue.push(null);
4168
+ }
4169
+ return;
4170
+ }
4171
+ flushFlowStatus();
4172
+ return;
4173
+ }
4174
+ if (flowStatusTimer !== null) {
4175
+ const last = flowStatusQueue[flowStatusQueue.length - 1];
4176
+ if (status.phase !== "done" && last !== void 0 && last !== null && last.phase !== "done") {
4177
+ flowStatusQueue[flowStatusQueue.length - 1] = status;
4178
+ return;
4179
+ }
4180
+ flowStatusQueue.push(status);
4181
+ return;
4182
+ }
4183
+ applyFlowStatus(status);
4184
+ startFlowStatusHold(status);
4185
+ }
4186
+ function flushFlowStatus() {
4187
+ if (flowStatusTimer !== null) {
4188
+ try {
4189
+ clearTimeout(flowStatusTimer);
4190
+ } catch {
4191
+ }
4192
+ flowStatusTimer = null;
4193
+ }
4194
+ flowStatusQueue = [];
4195
+ applyFlowStatus(null);
4196
+ }
4197
+ function applyFlowStatus(status) {
4198
+ shownFlowStatus = status;
4199
+ try {
4200
+ if (status) bar?.setDocked(false);
4201
+ bar?.setFlow(status);
4202
+ if (status) {
4203
+ close();
4204
+ return;
4205
+ }
4206
+ bar?.setPlaceholder(readWidgetChrome()?.inputPlaceholder ?? null);
4207
+ } catch (error) {
4208
+ warn(`The flow status could not be shown. ${String(error)}`);
4209
+ }
4210
+ }
4211
+ function mountChatController() {
4212
+ try {
4213
+ const root = bar?.getRoot();
4214
+ const host = bar?.getHost();
4215
+ const mountedBar = bar;
4216
+ if (!root || !host || !mountedBar) return;
4217
+ chatController = createChatController({
4218
+ root,
4219
+ host,
4220
+ loadChunk: chunks.loadChat,
4221
+ // The panel's collapse control reaches the public close() through
4222
+ // here, so isOpen() has exactly one writer. There is no close, only
4223
+ // collapse: the bar never leaves.
4224
+ onRequestClose: () => close(),
4225
+ // The bar is the widget's only composer, and it lives in the core
4226
+ // while the transcript lives in the chunk. This is the whole of the
4227
+ // bridge between them (26.3).
4228
+ bar: {
4229
+ onSubmit: (handler) => {
4230
+ barSubmit = handler;
4231
+ if (pendingMessage !== null) {
4232
+ const text = pendingMessage;
4233
+ pendingMessage = null;
4234
+ handler(text);
4235
+ }
4236
+ },
4237
+ onFiles: (handler) => {
4238
+ barFiles = handler;
4239
+ if (pendingFiles !== null) {
4240
+ const files = pendingFiles;
4241
+ pendingFiles = null;
4242
+ handler(files);
4243
+ }
4244
+ },
4245
+ setPlaceholder: (text) => mountedBar.setPlaceholder(text),
4246
+ setDisabled: (disabled) => mountedBar.setDisabled(disabled),
4247
+ setMode: (mode) => mountedBar.setMode(mode),
4248
+ setFlow: (status) => mountedBar.setFlow(status),
4249
+ setAwaitingInput: (awaiting) => mountedBar.setAwaitingInput(awaiting),
4250
+ setDisclaimer: (text) => mountedBar.setDisclaimer(text),
4251
+ setAttachEnabled: (enabled) => mountedBar.setAttachEnabled(enabled),
4252
+ setAttachmentCount: (count) => mountedBar.setAttachmentCount(count),
4253
+ // Where the panel's image tiles render: inside the composer, above
4254
+ // its input. The tiles stay the panel's (it owns the upload they
4255
+ // stand for); only the place is the bar's.
4256
+ attachmentSlot: () => mountedBar.attachmentSlot(),
4257
+ focusInput: () => mountedBar.focusInput(),
4258
+ getMetrics: () => mountedBar.getMetrics(),
4259
+ // The panel's Settings sheet asked for the widget to become a
4260
+ // corner button. The panel has already collapsed itself by the
4261
+ // time this runs, so this is only the bar's half.
4262
+ dock: () => mountedBar.setDocked(true)
4263
+ },
4264
+ // The flows catalog fetch inputs; read live rather than captured,
4265
+ // same reasoning as everything else in this options object, since
4266
+ // the panel can mount before the bootstrap resolves.
4267
+ apiBase: resolveApiBase(),
4268
+ getSessionToken: () => bootstrap?.config()?.sessionToken ?? null,
4269
+ getSessionId: () => telemetry?.sessionId(),
4270
+ getRoute: () => typeof window !== "undefined" ? window.location.pathname : "",
4271
+ getMaxHeightPercent: () => tokens.maxHeightPercent ?? void 0,
4272
+ onStartFlow: (flowCvid) => {
4273
+ ensureFlowController();
4274
+ void flowController?.start(flowCvid);
4275
+ },
4276
+ onStopFlow: () => {
4277
+ flowController?.stop();
4278
+ flushFlowStatus();
4279
+ },
4280
+ // D774: the widget's basic chat query loop.
4281
+ publicKey: token ?? void 0,
4282
+ getConversationId: () => conversationStore?.id() ?? null,
4283
+ setConversationId: (conversationId) => conversationId === null ? conversationStore?.startNew() ?? null : conversationStore?.adopt(conversationId) ?? null,
4284
+ touchConversation: () => conversationStore?.touch(),
4285
+ // D858: Recent chats are scoped to this, never to an identify()
4286
+ // userId. Identity verification is optional per environment, so an
4287
+ // unverified identify() from a console would otherwise hand an
4288
+ // attacker another person's transcripts.
4289
+ getVisitorId: () => visitorId,
4290
+ // #714/closes 13.5: the one teardown path (`hideWidget`).
4291
+ onOrgCapExceeded: () => hideWidget(),
4292
+ // Same bridge buildFlowContext() already wires for the flow engine.
4293
+ track: (name, fields, payload) => {
4294
+ telemetry?.track(name, payload, fields);
4295
+ },
4296
+ getWidget: () => readWidgetChrome(),
4297
+ onSelectLocale: (localeCode) => void selectLocale(localeCode)
4298
+ });
4299
+ } catch (error) {
4300
+ warn(`The chat panel could not be prepared. ${String(error)}`);
4301
+ }
4302
+ }
4303
+ async function selectLocale(localeCode) {
4304
+ try {
4305
+ if (typeof localeCode !== "string" || localeCode.length === 0) return;
4306
+ if (shellState?.read().locale === localeCode) return;
4307
+ shellState?.rememberLocale(localeCode);
4308
+ await syncSession();
4309
+ applyChrome();
4310
+ chatController?.refreshChrome();
4311
+ } catch (error) {
4312
+ warn(`The widget could not change its language. ${String(error)}`);
4313
+ }
4314
+ }
4315
+ function buildFlowContext() {
4316
+ if (!token) return null;
4317
+ const sessionToken = bootstrap?.config()?.sessionToken;
4318
+ if (!sessionToken) return null;
4319
+ const visits = visitState;
4320
+ return {
4321
+ apiBase: resolveApiBase(),
4322
+ token,
4323
+ sessionToken,
4324
+ sessionId: telemetry?.sessionId(),
4325
+ // Slice 18.1's trigger conditions read traits set via identify();
4326
+ // `Attributes` also allows null and string[], neither of which
4327
+ // `ConditionContext.getTrait` accepts, so both degrade to
4328
+ // "unresolvable" rather than being coerced into something wrong.
4329
+ getTrait: (codeName) => {
4330
+ const value = identity.attributes[codeName];
4331
+ if (value === void 0 || value === null || Array.isArray(value)) {
4332
+ return void 0;
4333
+ }
4334
+ return value;
4335
+ },
4336
+ // D1299: absent entirely (rather than answering false) when storage
4337
+ // failed at start(), so a `visitor` condition reads as unresolvable
4338
+ // instead of every visitor counting as returning.
4339
+ ...visits ? { isFirstVisit: () => visits.isFirstVisit() } : {},
4340
+ // The fill-step bridge (ADR 0168/0169): the chat chunk renders the
4341
+ // question in the compact card beside the bar; the panel is never
4342
+ // opened for it. Absent a mounted chunk (a failed mount, or an org
4343
+ // cap denial), a fill step waits rather than acting on a value nobody
4344
+ // asked for. A visitor who docked the widget gets it back: the card
4345
+ // and the composer both live on the bar.
4346
+ promptFill: (request) => {
4347
+ bar?.setDocked(false);
4348
+ fillPending = true;
4349
+ const prompt = chatController?.promptFill(request) ?? Promise.resolve(null);
4350
+ return prompt.finally(() => {
4351
+ fillPending = false;
4352
+ });
4353
+ },
4354
+ // ADR 0166/D1294: the run ended, so a question still waiting is
4355
+ // withdrawn wherever it is showing.
4356
+ cancelFill: () => {
4357
+ fillPending = false;
4358
+ chatController?.cancelFill();
4359
+ },
4360
+ // D1085's clean failure stop: the run already ended and the bar is
4361
+ // back to normal by the time this fires; the panel shows the
4362
+ // localized apology and the composer is ready for a question.
4363
+ // Routed through open() first for the same `isOpen` bookkeeping
4364
+ // reason `promptFill` documents above.
4365
+ onFailure: () => {
4366
+ if (!isOpen) open();
4367
+ void chatController?.showFlowFailure();
4368
+ },
4369
+ // 18.5's telemetry bridge: forwards straight into the one `track()`
4370
+ // pipeline this bundle already owns (`telemetry/collector.ts`), so a
4371
+ // flow's lifecycle events go through the exact same consent gate and
4372
+ // batching every other event family does. Absent telemetry (it failed
4373
+ // to initialize, or this session opted out), the flow runs and simply
4374
+ // emits nothing, same posture as `promptFill` above.
4375
+ track: (name, fields, payload) => {
4376
+ telemetry?.track(name, payload, fields);
4377
+ },
4378
+ // 26.4: the bar is the running flow's status surface, and the panel
4379
+ // gets out of the way so the user can watch their own product being
4380
+ // driven.
4381
+ onStatus: (status) => setFlowStatus(status),
4382
+ // The bar covers the bottom of the screen, so a target under it is
4383
+ // scrolled into view before it is clicked or filled. Without this a
4384
+ // flow "successfully" clicks a button nobody can see.
4385
+ getBottomOcclusion: () => {
4386
+ const metrics = bar?.getMetrics();
4387
+ if (!metrics) return 0;
4388
+ return metrics.bottomOffset + metrics.keyboardOffset + metrics.height;
4389
+ }
4390
+ };
4391
+ }
4392
+ function ensureFlowController() {
4393
+ if (flowController) return;
4394
+ try {
4395
+ const context = buildFlowContext();
4396
+ if (!context) return;
4397
+ flowController = createFlowController({ context, loadChunk: chunks.loadFlow });
4398
+ } catch (error) {
4399
+ warn(`The flow engine could not be prepared. ${String(error)}`);
4400
+ }
4401
+ }
4402
+ async function syncSession() {
4403
+ try {
4404
+ await sendSession();
4405
+ } catch (error) {
4406
+ warn(`The ARCY session could not be started. ${String(error)}`);
4407
+ }
4408
+ try {
4409
+ const config = bootstrap?.config();
4410
+ if (config) {
4411
+ const next = (config.customization ?? {}).tokens;
4412
+ tokenCache?.remember(next, config.widget);
4413
+ tokens = readTokens(next);
4414
+ widgetIdentity = readIdentity(config.widget) ?? widgetIdentity;
4415
+ }
4416
+ } catch (error) {
4417
+ warn(`The widget could not read its theme. ${String(error)}`);
4418
+ }
4419
+ if (!flowResumeAttempted) {
4420
+ ensureFlowController();
4421
+ if (flowController) {
4422
+ flowResumeAttempted = true;
4423
+ void flowController.resume();
4424
+ }
4425
+ }
4426
+ if (previewIntent) {
4427
+ const intent = previewIntent;
4428
+ previewIntent = null;
4429
+ void (async () => {
4430
+ ensureFlowController();
4431
+ if (!flowController) return;
4432
+ if (intent.kind === "pending") {
4433
+ await flowController.resumePreview();
4434
+ return;
4435
+ }
4436
+ const flow = await verifyPreview(intent.nonce);
4437
+ if (!flow) return;
4438
+ await flowController.startPreview(flow);
4439
+ })().catch((error) => warn(`The preview could not run. ${String(error)}`));
4440
+ }
4441
+ mountWidget();
4442
+ }
4443
+ function entryRoute() {
4444
+ if (entryRouteValue !== void 0) return entryRouteValue;
4445
+ try {
4446
+ const path = typeof window !== "undefined" ? window.location.pathname : null;
4447
+ if (typeof path !== "string" || !path.startsWith("/")) return void 0;
4448
+ entryRouteValue = scrubPath(path);
4449
+ return entryRouteValue;
4450
+ } catch {
4451
+ return void 0;
4452
+ }
4453
+ }
4454
+ function sessionContext() {
4455
+ if (sessionContextResolved) return sessionContextValue;
4456
+ sessionContextResolved = true;
4457
+ try {
4458
+ if (typeof window === "undefined") return void 0;
4459
+ sessionContextValue = captureSessionContext({
4460
+ referrer: typeof document !== "undefined" ? document.referrer : null,
4461
+ search: window.location?.search ?? null,
4462
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : null,
4463
+ viewportWidth: window.innerWidth
4464
+ });
4465
+ return sessionContextValue;
4466
+ } catch {
4467
+ return void 0;
4468
+ }
4469
+ }
4470
+ async function sendSession() {
4471
+ if (!bootstrap || !telemetry) return;
4472
+ const mySequence = ++sendSessionSequence;
4473
+ const result = await bootstrap.sync({
4474
+ sessionId: telemetry.sessionId(),
4475
+ // The **entry** route, frozen at the first send of this page load, not
4476
+ // the route the user is standing on now (D338). The Users and
4477
+ // Organizations session tables read it to answer "where did this
4478
+ // session start", and freezing it is what keeps an SPA calling
4479
+ // identify() per route change from making every navigation a new
4480
+ // payload, which is the re-send storm D338's skip exists to prevent.
4481
+ route: entryRoute(),
4482
+ // Frozen with the entry route and for the same reason. Segmentation
4483
+ // dimensions, not behavior: they land on the session row rather than on
4484
+ // four hundred events that would each repeat them (#790).
4485
+ sessionContext: sessionContext(),
4486
+ // Sent whether or not this browser has identified (D517). It is the
4487
+ // Lift Subject for anyone who never will, and the anonymous half of the
4488
+ // pairing ADR 0055 discovers for everyone who does.
4489
+ visitorId: visitorId ?? void 0,
4490
+ userId: identity.isAnonymous ? void 0 : identity.userId ?? void 0,
4491
+ // Sent with every payload that asserts the id, not only the call that
4492
+ // supplied it (D158). Bootstrap is the only browser-reachable assertion
4493
+ // of a user id until 6.5 lands the session-token identify route (D168),
4494
+ // so this is the whole of the widget's half of PRD 6.
4495
+ userHash: identity.isAnonymous ? void 0 : identity.userHash,
4496
+ userTraits: Object.keys(identity.attributes).length ? identity.attributes : void 0,
4497
+ // Only the two locales the DTO's enum accepts. Passing a third would
4498
+ // fail validation and take the whole bootstrap down, so an unsupported
4499
+ // locale falls back to the environment default rather than 400ing.
4500
+ uiLanguage: options.locale === "en" || options.locale === "tr" ? options.locale : void 0,
4501
+ // `options.contentLocale`'s bootstrap-time seed for `SdkSession.localeCode`
4502
+ // (D733). Sent as-is, unlike `uiLanguage` above: the DTO's own
4503
+ // `normalizeLocaleCode` (not an enum, unlike `uiLanguage`'s two-entry
4504
+ // one) is the single point that validates it and falls through the
4505
+ // rest of the chain on a miss, so a typo here degrades gracefully
4506
+ // server-side rather than needing a second whitelist here that could
4507
+ // drift from the backend's catalogue.
4508
+ contentLocale: typeof options.contentLocale === "string" ? options.contentLocale : void 0,
4509
+ // D1058/ADR 0130: the language the visitor picked for themselves in
4510
+ // the panel's Settings sheet, which outranks every tier above. Read
4511
+ // from storage on every send rather than held in a variable: the pick
4512
+ // is written before the re-bootstrap it triggers, and reading it here
4513
+ // is what makes the choice govern the NEXT page load too, from the
4514
+ // first payload, with no round trip to learn it.
4515
+ viewerLocale: shellState?.read().locale ?? void 0
4516
+ });
4517
+ if (!result.ok) {
4518
+ warn(
4519
+ "The ARCY session could not be started. The widget will keep working, and telemetry stays off until configuration arrives."
4520
+ );
4521
+ return;
4522
+ }
4523
+ if (mySequence !== sendSessionSequence) return;
4524
+ if (result.config.identityAccepted === false) {
4525
+ warn(
4526
+ "This environment enforces identity verification and no valid userHash was supplied, so this user was not identified and none of their attributes were stored. Pass a signed userHash: arcy.identify(userId, attributes, { userHash }). See Settings > Identity verification in the ARCY dashboard."
4527
+ );
4528
+ }
4529
+ if (result.config.behavioralTelemetry) {
4530
+ telemetry.setTelemetryState(result.config.behavioralTelemetry);
4531
+ }
4532
+ }
4533
+ async function identify(userId, attributes, options2) {
4534
+ if (!requireInit("identify")) return;
4535
+ if (typeof userId !== "string" || userId.length === 0) {
4536
+ warn("identify() needs a user id. Ignoring the call.");
4537
+ return;
4538
+ }
4539
+ const clean2 = sanitizeAttributes(attributes, "identify");
4540
+ if (clean2 === null) return;
4541
+ const hash = readUserHash((options2 ?? {}).userHash, "identify");
4542
+ if (!hash.ok) return;
4543
+ const sameUser = identity.userId === userId && !identity.isAnonymous;
4544
+ identity = {
4545
+ userId,
4546
+ isAnonymous: false,
4547
+ attributes: sameUser ? { ...identity.attributes, ...clean2 } : clean2,
4548
+ // **The proof carries over for the same id and never across ids.** An
4549
+ // SPA re-identifying the same user on every route change passes the hash
4550
+ // once at login and nothing afterwards, and dropping it there would
4551
+ // unprove an identity that was proven. Carrying it to a *different* id
4552
+ // is the identity takeover D170 caught in 6.2 from the other side: a
4553
+ // proof is a proof of one id, so a new user starts from whatever this
4554
+ // call actually carried, which is usually nothing.
4555
+ userHash: sameUser ? hash.hash ?? identity.userHash : hash.hash
4556
+ };
4557
+ await syncSession();
4558
+ }
4559
+ async function identifyAnonymous(attributes) {
4560
+ if (!requireInit("identifyAnonymous")) return;
4561
+ const clean2 = sanitizeAttributes(attributes, "identifyAnonymous");
4562
+ if (clean2 === null) return;
4563
+ identity = { userId: null, isAnonymous: true, attributes: clean2 };
4564
+ await syncSession();
4565
+ }
4566
+ async function updateUser(attributes) {
4567
+ if (!requireInit("updateUser")) return;
4568
+ if (!identity.userId && !identity.isAnonymous) {
4569
+ warn(
4570
+ "updateUser() was called before identify() or identifyAnonymous(). Ignoring the call."
4571
+ );
4572
+ return;
4573
+ }
4574
+ const clean2 = sanitizeAttributes(attributes, "updateUser");
4575
+ if (clean2 === null) return;
4576
+ identity = { ...identity, attributes: { ...identity.attributes, ...clean2 } };
4577
+ await syncSession();
4578
+ }
4579
+ function reset() {
4580
+ identity = emptyIdentity();
4581
+ shellState?.forgetAnonymousId();
4582
+ visitorId = shellState?.anonymousId() ?? null;
4583
+ telemetry?.resetSession();
4584
+ conversationStore?.reset();
4585
+ close();
4586
+ }
4587
+ function open() {
4588
+ if (!requireInit("open")) return;
4589
+ if (!bar) {
4590
+ openPending = true;
4591
+ return;
4592
+ }
4593
+ if (isOpen) return;
4594
+ bar?.setDocked(false);
4595
+ isOpen = true;
4596
+ bar?.setMode("open");
4597
+ void chatController?.open();
4598
+ emitter.emit("open");
4599
+ }
4600
+ function close() {
4601
+ if (!isOpen) return;
4602
+ isOpen = false;
4603
+ bar?.setMode("idle");
4604
+ chatController?.close();
4605
+ emitter.emit("close");
4606
+ }
4607
+ function on(event, handler) {
4608
+ return emitter.on(event, handler);
4609
+ }
4610
+ return {
4611
+ api: {
4612
+ init,
4613
+ identify,
4614
+ identifyAnonymous,
4615
+ updateUser,
4616
+ reset,
4617
+ open,
4618
+ close,
4619
+ on,
4620
+ VERSION
4621
+ },
4622
+ getToken: () => token,
4623
+ getOptions: () => options,
4624
+ getIdentity: () => identity,
4625
+ isOpen: () => isOpen,
4626
+ getBar: () => bar,
4627
+ getChatController: () => chatController,
4628
+ getFlowController: () => flowController,
4629
+ getTelemetry: () => telemetry,
4630
+ getConfig: () => bootstrap?.config() ?? null,
4631
+ selectLocale,
4632
+ getShellState: () => shellState,
4633
+ getTokens: () => tokens
4634
+ };
4635
+ }
4636
+
4637
+ // src/chunks/bundled.ts
4638
+ async function importChunk(win, importer, globalKey, contract, surface, timeoutMs, setTimeoutImpl) {
4639
+ const holder = win;
4640
+ let warned = false;
4641
+ const complain = (message) => {
4642
+ if (warned) return;
4643
+ warned = true;
4644
+ warn(message);
4645
+ };
4646
+ const read = () => {
4647
+ let registration2;
4648
+ try {
4649
+ registration2 = holder[globalKey];
4650
+ } catch {
4651
+ complain(`The ${surface} registration could not be read. It will not start.`);
4652
+ return null;
4653
+ }
4654
+ if (!registration2) return null;
4655
+ if (typeof registration2.mount !== "function") {
4656
+ complain(`The ${surface} registered something unusable. It will not start.`);
4657
+ return null;
4658
+ }
4659
+ let declared;
4660
+ try {
4661
+ declared = registration2.contract;
4662
+ } catch {
4663
+ declared = void 0;
4664
+ }
4665
+ if (declared !== contract) {
4666
+ complain(
4667
+ `The bundled ${surface} speaks contract ${typeof declared === "number" ? declared : "unknown"}, this arcy.js speaks ${contract}. Reinstall arcy.js so both halves come from one version.`
4668
+ );
4669
+ return null;
4670
+ }
4671
+ return registration2;
4672
+ };
4673
+ let present = false;
4674
+ try {
4675
+ present = holder[globalKey] !== void 0;
4676
+ } catch {
4677
+ present = true;
4678
+ }
4679
+ if (present) return read();
4680
+ const arrived = await Promise.race([
4681
+ importer().then(
4682
+ () => "loaded",
4683
+ (error) => {
4684
+ warn(
4685
+ `The ${surface} chunk could not be loaded from your own build. ${String(error)}`
4686
+ );
4687
+ return "failed";
4688
+ }
4689
+ ),
4690
+ new Promise((resolve) => {
4691
+ try {
4692
+ setTimeoutImpl(() => resolve("timeout"), timeoutMs);
4693
+ } catch {
4694
+ }
4695
+ })
4696
+ ]);
4697
+ if (arrived === "failed") return null;
4698
+ if (arrived === "timeout") {
4699
+ warn(`The ${surface} did not load in time.`);
4700
+ return null;
4701
+ }
4702
+ const registration = read();
4703
+ if (!registration && !warned) {
4704
+ complain(`The ${surface} loaded but did not register. It will not start.`);
4705
+ }
4706
+ return registration;
4707
+ }
4708
+ var bundledChunks = {
4709
+ cdnBase: null,
4710
+ loadChat: (win, timeoutMs, setTimeoutImpl) => importChunk(
4711
+ win,
4712
+ () => import('./chat-BK57WIAA.js'),
4713
+ CHAT_GLOBAL,
4714
+ CHAT_CONTRACT,
4715
+ "chat panel",
4716
+ timeoutMs,
4717
+ setTimeoutImpl
4718
+ ),
4719
+ loadFlow: (win, timeoutMs, setTimeoutImpl) => importChunk(
4720
+ win,
4721
+ () => import('./flow-M7W3H3C5.js'),
4722
+ FLOW_GLOBAL,
4723
+ FLOW_CONTRACT,
4724
+ "flow engine",
4725
+ timeoutMs,
4726
+ setTimeoutImpl
4727
+ ),
4728
+ loadPicker: (win, timeoutMs, setTimeoutImpl) => importChunk(
4729
+ win,
4730
+ () => import('./picker-JPESE6JJ.js'),
4731
+ PICKER_GLOBAL,
4732
+ PICKER_CONTRACT,
4733
+ "design-mode picker",
4734
+ timeoutMs,
4735
+ setTimeoutImpl
4736
+ )
4737
+ };
4738
+
4739
+ // src/index.ts
4740
+ var arcy = createArcy(bundledChunks);
4741
+ var src_default = arcy;
4742
+
4743
+ export { VERSION, src_default as default };