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.
- package/README.md +94 -3
- package/dist/arcy.chat.3968644db103.js +1271 -0
- package/dist/arcy.flow.fef131718e42.js +3 -0
- package/dist/arcy.legacy.b4005932a6cc.js +568 -0
- package/dist/arcy.loader.js +2 -0
- package/dist/arcy.modern.a51b9a32dd28.js +568 -0
- package/dist/arcy.picker.73f47221b73c.js +59 -0
- package/dist/chat-BK57WIAA.js +3867 -0
- package/dist/chunk-6HBSGVRN.js +219 -0
- package/dist/chunk-7VVKR3AO.js +122 -0
- package/dist/chunk-CQ2DESAJ.js +263 -0
- package/dist/chunk-DKYMIAYF.js +7 -0
- package/dist/chunk-FR6SJSDU.js +220 -0
- package/dist/chunk-LGWYJSYX.js +271 -0
- package/dist/chunk-NIHUDSWK.js +155 -0
- package/dist/chunk-NY3NXM2V.js +213 -0
- package/dist/design-mode.cjs +135 -0
- package/dist/design-mode.d.ts +197 -0
- package/dist/design-mode.js +2 -0
- package/dist/flow-M7W3H3C5.js +1422 -0
- package/dist/fonts/dm-sans-latin-ext.woff2 +0 -0
- package/dist/fonts/dm-sans-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-latin-ext.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-latin.woff2 +0 -0
- package/dist/fonts/inter-latin-ext.woff2 +0 -0
- package/dist/fonts/inter-latin.woff2 +0 -0
- package/dist/fonts/lato-latin-ext.woff2 +0 -0
- package/dist/fonts/lato-latin.woff2 +0 -0
- package/dist/fonts/manrope-latin-ext.woff2 +0 -0
- package/dist/fonts/manrope-latin.woff2 +0 -0
- package/dist/fonts/montserrat-latin-ext.woff2 +0 -0
- package/dist/fonts/montserrat-latin.woff2 +0 -0
- package/dist/fonts/nunito-latin-ext.woff2 +0 -0
- package/dist/fonts/nunito-latin.woff2 +0 -0
- package/dist/fonts/open-sans-latin-ext.woff2 +0 -0
- package/dist/fonts/open-sans-latin.woff2 +0 -0
- package/dist/fonts/playfair-display-latin-ext.woff2 +0 -0
- package/dist/fonts/playfair-display-latin.woff2 +0 -0
- package/dist/fonts/poppins-latin-ext.woff2 +0 -0
- package/dist/fonts/poppins-latin.woff2 +0 -0
- package/dist/fonts/roboto-latin-ext.woff2 +0 -0
- package/dist/fonts/roboto-latin.woff2 +0 -0
- package/dist/fonts/source-sans-3-latin-ext.woff2 +0 -0
- package/dist/fonts/source-sans-3-latin.woff2 +0 -0
- package/dist/fonts/work-sans-latin-ext.woff2 +0 -0
- package/dist/fonts/work-sans-latin.woff2 +0 -0
- package/dist/index.cjs +13848 -0
- package/dist/index.d.ts +169 -0
- package/dist/index.js +4743 -0
- package/dist/picker-JPESE6JJ.js +2134 -0
- package/dist/snippet.cjs +46 -0
- package/dist/snippet.d.ts +39 -0
- package/dist/snippet.js +43 -0
- package/package.json +69 -5
- 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 };
|