@guidelytech/sdk-core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/{capture-DVJyQ2JZ.d.cts → capture-CgN69Lxq.d.cts} +2 -2
  2. package/dist/{capture-369s4YYT.d.ts → capture-Csz721-A.d.ts} +2 -2
  3. package/dist/capture.cjs +2 -0
  4. package/dist/capture.cjs.map +1 -1
  5. package/dist/capture.d.cts +3 -3
  6. package/dist/capture.d.ts +3 -3
  7. package/dist/capture.js +1 -1
  8. package/dist/{chunk-C4WWSF67.js → chunk-JUSM7J5N.js} +3 -1
  9. package/dist/{chunk-C4WWSF67.js.map → chunk-JUSM7J5N.js.map} +1 -1
  10. package/dist/{chunk-GDJB466J.js → chunk-ZAF2KRJA.js} +29 -6
  11. package/dist/chunk-ZAF2KRJA.js.map +1 -0
  12. package/dist/headless.cjs +32 -4
  13. package/dist/headless.cjs.map +1 -1
  14. package/dist/headless.d.cts +6 -5
  15. package/dist/headless.d.ts +6 -5
  16. package/dist/headless.js +9 -3
  17. package/dist/{health-DFHUstWd.d.cts → health-sbP9JwTE.d.cts} +2 -0
  18. package/dist/{health-DFHUstWd.d.ts → health-sbP9JwTE.d.ts} +2 -0
  19. package/dist/{http-D0dCcpLN.d.cts → http-Cb6KYYFv.d.cts} +1 -1
  20. package/dist/{http-DJd8jRhK.d.ts → http-D047Ngxk.d.ts} +1 -1
  21. package/dist/index.cjs +1632 -1045
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +77 -9
  24. package/dist/index.d.ts +77 -9
  25. package/dist/index.js +760 -201
  26. package/dist/index.js.map +1 -1
  27. package/dist/recorder.cjs.map +1 -1
  28. package/dist/recorder.d.cts +1 -1
  29. package/dist/recorder.d.ts +1 -1
  30. package/dist/recorder.js +1 -1
  31. package/dist/{types-B5ecgHUV.d.cts → types-BDlebXrm.d.ts} +59 -5
  32. package/dist/{types-BtPovimB.d.ts → types-Mtlmlj5C.d.cts} +59 -5
  33. package/package.json +4 -4
  34. package/dist/chunk-GDJB466J.js.map +0 -1
package/dist/index.js CHANGED
@@ -3,11 +3,14 @@ import {
3
3
  EventQueue,
4
4
  Outbox,
5
5
  areMutuallyExclusive,
6
+ countsTowardThrottle,
6
7
  dismissedAfterMerge,
7
8
  isExclusiveGuide,
8
9
  isFrequencyEligible,
9
- isWithinSchedule
10
- } from "./chunk-GDJB466J.js";
10
+ isThrottleEligible,
11
+ isWithinSchedule,
12
+ pruneLaunches
13
+ } from "./chunk-ZAF2KRJA.js";
11
14
  import {
12
15
  Api,
13
16
  KEYS,
@@ -16,12 +19,12 @@ import {
16
19
  postWithRetry,
17
20
  readJson,
18
21
  writeJson
19
- } from "./chunk-C4WWSF67.js";
22
+ } from "./chunk-JUSM7J5N.js";
20
23
 
21
24
  // src/client.ts
22
- import { evaluateRules, randomId, rolloutBucket, sanitizeProperties, supportsPlatform } from "@guidelytech/shared/runtime";
25
+ import { evaluateRules, localizeGuide, localizeResourceCenter, normalizeLocale, randomId as randomId2, rolloutBucket, sanitizeProperties, supportsPlatform, visibleAnnouncements, visitorLocales } from "@guidelytech/shared/runtime";
23
26
  import {
24
- closestComposed as closestComposed3,
27
+ closestComposed as closestComposed4,
25
28
  composedTarget,
26
29
  destroyHost,
27
30
  isChecklistComplete,
@@ -35,8 +38,82 @@ import {
35
38
  import { createScope } from "@guidelytech/guide-runtime";
36
39
  import { createFlagsClient } from "@guidelytech/flags";
37
40
 
41
+ // src/agent-turns.ts
42
+ import { AGENT_TOOL_CALLS_PER_TURN, AGENT_TURN_MAX_TEXT, AGENT_TURNS_PER_BATCH, randomId } from "@guidelytech/shared/runtime";
43
+ var PENDING_MAX = 500;
44
+ function toolCalls(calls) {
45
+ if (!Array.isArray(calls)) return void 0;
46
+ return calls.filter((c) => !!c && typeof c.name === "string" && !!c.name).slice(0, AGENT_TOOL_CALLS_PER_TURN).map((c) => ({
47
+ name: c.name.slice(0, 100),
48
+ ...c.status === "ok" || c.status === "error" ? { status: c.status } : {},
49
+ ...typeof c.latencyMs === "number" && Number.isFinite(c.latencyMs) && c.latencyMs >= 0 ? { latencyMs: Math.round(c.latencyMs) } : {}
50
+ }));
51
+ }
52
+ function prepareAgentTurn(turn, identity, redact, onError) {
53
+ if (!turn || typeof turn !== "object" || !turn.conversationId || !turn.agentId || !turn.role) return null;
54
+ let out = {
55
+ ...turn,
56
+ type: "agent.conversation",
57
+ turnId: turn.turnId || `t_${randomId(20)}`,
58
+ occurredAt: turn.occurredAt || (/* @__PURE__ */ new Date()).toISOString(),
59
+ anonymousId: turn.anonymousId ?? identity.anonymousId,
60
+ userId: turn.userId !== void 0 ? turn.userId : identity.userId,
61
+ accountId: turn.accountId !== void 0 ? turn.accountId : identity.accountId,
62
+ sessionId: turn.sessionId ?? identity.sessionId
63
+ };
64
+ if (redact) {
65
+ try {
66
+ const r = redact({ ...out, toolCalls: out.toolCalls ? out.toolCalls.map((c) => ({ ...c })) : void 0, metadata: out.metadata ? { ...out.metadata } : void 0 });
67
+ if (!r) return null;
68
+ out = { ...r, type: "agent.conversation", conversationId: out.conversationId, agentId: out.agentId, turnId: out.turnId, role: out.role };
69
+ } catch (err) {
70
+ onError?.(err);
71
+ return null;
72
+ }
73
+ }
74
+ if (typeof out.text === "string" && out.text.length > AGENT_TURN_MAX_TEXT) out.text = out.text.slice(0, AGENT_TURN_MAX_TEXT);
75
+ if (out.text !== void 0 && typeof out.text !== "string") out.text = String(out.text);
76
+ const calls = toolCalls(out.toolCalls);
77
+ if (calls) out.toolCalls = calls;
78
+ else delete out.toolCalls;
79
+ return out;
80
+ }
81
+ var AgentTurns = class {
82
+ constructor(opts) {
83
+ __publicField(this, "opts", opts);
84
+ __publicField(this, "outbox");
85
+ __publicField(this, "pending", []);
86
+ this.outbox = new Outbox({ api: opts.api, store: opts.store, storageKey: opts.storageKey, health: opts.health, maxItems: 20, maxAgeMs: 24 * 60 * 60 * 1e3 });
87
+ }
88
+ get queued() {
89
+ return this.outbox.size + this.pending.length;
90
+ }
91
+ /** Queues turns to send now, or (`hold`) until consent is granted. */
92
+ add(turns, hold = false) {
93
+ if (!turns.length) return;
94
+ if (hold) {
95
+ this.pending.push(...turns);
96
+ if (this.pending.length > PENDING_MAX) this.pending.splice(0, this.pending.length - PENDING_MAX);
97
+ return;
98
+ }
99
+ for (let i = 0; i < turns.length; i += AGENT_TURNS_PER_BATCH) {
100
+ const batch = turns.slice(i, i + AGENT_TURNS_PER_BATCH);
101
+ this.outbox.add(`agent:${batch[0].turnId}`, "/agent-events", { turns: batch, sdk: this.opts.sdk });
102
+ }
103
+ }
104
+ /** Consent changed: granted sends what was held, anything else drops it. */
105
+ consentChanged(granted) {
106
+ const held = this.pending.splice(0);
107
+ if (granted) this.add(held);
108
+ }
109
+ destroy() {
110
+ this.pending = [];
111
+ this.outbox.destroy();
112
+ }
113
+ };
114
+
38
115
  // src/authoring.ts
39
- import { closestComposed, highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
116
+ import { closestComposed as closestComposed3, highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
40
117
 
41
118
  // src/snapshot.ts
42
119
  var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
@@ -114,6 +191,276 @@ ${clone.outerHTML}`;
114
191
  return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
115
192
  }
116
193
 
194
+ // src/tagging.ts
195
+ import { NOISY_DATA_ATTR as NOISY_DATA_ATTR2, SNAPSHOT_LIMITS, TAG_PROPERTY_LIMITS, featureRuleMatchesClick, pageRuleMatches, pageRulesMatch } from "@guidelytech/shared/runtime";
196
+ import { closestComposed as closestComposed2, isStableClass, isStableId as isStableId2, querySelectorStrategy } from "@guidelytech/guide-runtime";
197
+
198
+ // src/autocapture.ts
199
+ import { closestComposed, isGuidelyElement, isStableId, SHADOW_SEPARATOR, shadowHostChain } from "@guidelytech/guide-runtime";
200
+ var INTERACTIVE_SELECTOR = "a,button,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio],select,summary,label,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option],[onclick],[data-guidely-track]";
201
+ var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
202
+ var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
203
+ var NOISY_DATA_ATTR = /^data-(state|v-|react|radix|headlessui|svelte|orientation|highlighted|focus|key|index|side|align|disabled|selected|active|open|guidely-(mask|ignore|block))/i;
204
+ function validSelectors(list) {
205
+ const out = [];
206
+ const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
207
+ for (const raw of list) {
208
+ const s = (raw || "").trim();
209
+ if (!s || out.includes(s)) continue;
210
+ try {
211
+ probe?.querySelector(s);
212
+ out.push(s);
213
+ } catch {
214
+ }
215
+ }
216
+ return out;
217
+ }
218
+ function isIgnoredElement(el) {
219
+ return isGuidelyElement(el) || !!closestComposed(el, "[data-guidely-ignore]");
220
+ }
221
+ function interactiveElement(target) {
222
+ try {
223
+ return closestComposed(target, INTERACTIVE_SELECTOR);
224
+ } catch {
225
+ return null;
226
+ }
227
+ }
228
+ function esc(value) {
229
+ const g = globalThis;
230
+ return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
231
+ }
232
+ var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
233
+ function elementSelector(el) {
234
+ const own = ownSelector(el);
235
+ const chain = shadowHostChain(el);
236
+ return (chain ? chain + SHADOW_SEPARATOR + own : own).slice(0, 300);
237
+ }
238
+ function ownSelector(el) {
239
+ const tag = el.tagName.toLowerCase();
240
+ if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
241
+ for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
242
+ for (const a of Array.from(el.attributes)) {
243
+ if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
244
+ }
245
+ const label = el.getAttribute("aria-label");
246
+ if (label && label.length <= 80) return attr(tag, "aria-label", label);
247
+ const parts = [];
248
+ let cur = el;
249
+ while (cur && parts.length < 4) {
250
+ const t = cur.tagName.toLowerCase();
251
+ if (t === "html" || t === "body") break;
252
+ if (cur !== el && cur.id && isStableId(cur.id)) {
253
+ parts.unshift(`#${esc(cur.id)}`);
254
+ break;
255
+ }
256
+ const parent = cur.parentElement;
257
+ const node = cur;
258
+ const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
259
+ parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
260
+ cur = parent;
261
+ }
262
+ return parts.join(" > ") || tag;
263
+ }
264
+ function matches(el, selector, deep) {
265
+ if (!selector) return false;
266
+ try {
267
+ return !!closestComposed(el, selector) || deep && !!el.querySelector(selector);
268
+ } catch {
269
+ return false;
270
+ }
271
+ }
272
+ function elementText(el, mask, maskAll) {
273
+ if (maskAll || matches(el, mask, true)) return void 0;
274
+ const tag = el.tagName.toLowerCase();
275
+ let text = "";
276
+ if (tag === "input") {
277
+ if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
278
+ text = el.value;
279
+ } else if (tag === "select" || tag === "textarea") return void 0;
280
+ else text = el.innerText ?? el.textContent ?? "";
281
+ text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
282
+ return text ? text.slice(0, 100) : void 0;
283
+ }
284
+ function linkHref(el) {
285
+ const a = closestComposed(el, "a[href]");
286
+ if (!a) return void 0;
287
+ try {
288
+ const u = new URL(a.getAttribute("href") || "", location.href);
289
+ if (!/^https?:$/.test(u.protocol)) return void 0;
290
+ return u.origin === location.origin ? u.pathname : u.origin;
291
+ } catch {
292
+ return void 0;
293
+ }
294
+ }
295
+ function describeClick(el, e, opts) {
296
+ const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
297
+ const text = elementText(el, opts.mask, opts.maskAll);
298
+ if (text) props.text = text;
299
+ const href = opts.collectUrls ? linkHref(el) : void 0;
300
+ if (href) props.href = href;
301
+ return props;
302
+ }
303
+ var RageClickDetector = class {
304
+ constructor(threshold = 3, windowMs = 1e3, radius = 30) {
305
+ __publicField(this, "threshold", threshold);
306
+ __publicField(this, "windowMs", windowMs);
307
+ __publicField(this, "radius", radius);
308
+ __publicField(this, "clicks", []);
309
+ __publicField(this, "fired", false);
310
+ }
311
+ /** Clicks in the current burst once it has fired (rage), otherwise 0. */
312
+ get burst() {
313
+ return this.fired ? this.clicks.length : 0;
314
+ }
315
+ /** Records a click; returns the click count when it completes a burst, otherwise 0. */
316
+ add(x, y, t) {
317
+ const first = this.clicks[0];
318
+ const last = this.clicks[this.clicks.length - 1];
319
+ if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
320
+ this.clicks = [];
321
+ this.fired = false;
322
+ }
323
+ this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
324
+ this.clicks.push({ x, y, t });
325
+ if (this.fired || this.clicks.length < this.threshold) return 0;
326
+ this.fired = true;
327
+ return this.clicks.length;
328
+ }
329
+ };
330
+
331
+ // src/tagging.ts
332
+ var TEXT_ATTRS = /* @__PURE__ */ new Set(["aria-label", "title", "alt"]);
333
+ var SAFE_ATTRS = ["role", "name", "type", "aria-label", "title", "alt", "for"];
334
+ var STATE_CLASS = /^(is-|has-)?(active|hover|hovered|focus|focused|focus-visible|selected|open|opened|closed|disabled|visible|hidden|show|shown|collapsed|expanded|checked|pressed|loading|current)$/i;
335
+ function nthOfType(el) {
336
+ const parent = el.parentElement;
337
+ if (!parent) return void 0;
338
+ let n = 0;
339
+ for (const c of Array.from(parent.children)) {
340
+ if (c.tagName === el.tagName) n++;
341
+ if (c === el) return n;
342
+ }
343
+ return void 0;
344
+ }
345
+ function nodeOf(el, masked, featureAttribute) {
346
+ const node = { t: el.tagName.toLowerCase().slice(0, SNAPSHOT_LIMITS.tag) };
347
+ if (el.id && isStableId2(el.id) && el.id.length <= SNAPSHOT_LIMITS.id) node.id = el.id;
348
+ const classes = Array.from(el.classList).filter((c) => isStableClass(c) && !STATE_CLASS.test(c) && c.length <= SNAPSHOT_LIMITS.className);
349
+ if (classes.length) node.c = classes.slice(0, SNAPSHOT_LIMITS.classes);
350
+ const a = {};
351
+ const put = (name, value) => {
352
+ if (value === null || Object.keys(a).length >= SNAPSHOT_LIMITS.attrs || name in a) return;
353
+ if (masked && TEXT_ATTRS.has(name)) return;
354
+ a[name] = value.slice(0, SNAPSHOT_LIMITS.attrValue);
355
+ };
356
+ if (featureAttribute && el.hasAttribute(featureAttribute)) put(featureAttribute, el.getAttribute(featureAttribute));
357
+ for (const attr2 of Array.from(el.attributes)) {
358
+ if (attr2.name.startsWith("data-") && !NOISY_DATA_ATTR2.test(attr2.name) && attr2.value.length <= SNAPSHOT_LIMITS.attrValue) put(attr2.name, attr2.value);
359
+ }
360
+ for (const name of SAFE_ATTRS) if (el.hasAttribute(name)) put(name, el.getAttribute(name));
361
+ if (Object.keys(a).length) node.a = a;
362
+ const n = nthOfType(el);
363
+ if (n) node.n = n;
364
+ return node;
365
+ }
366
+ function elementSnapshot(el, opts) {
367
+ const masked = opts.maskAll || isMasked(el, opts.mask);
368
+ const snap = nodeOf(el, masked, opts.featureAttribute);
369
+ const p = [];
370
+ for (let cur = el.parentElement; cur && p.length < SNAPSHOT_LIMITS.ancestors; cur = cur.parentElement) {
371
+ if (cur.tagName === "BODY" || cur.tagName === "HTML") break;
372
+ p.push(nodeOf(cur, masked, opts.featureAttribute));
373
+ }
374
+ if (p.length) snap.p = p;
375
+ const h = [];
376
+ let root = el.getRootNode?.();
377
+ while (root && root.nodeType === 11 && root.host && h.length < SNAPSHOT_LIMITS.hosts) {
378
+ h.push(nodeOf(root.host, masked, opts.featureAttribute));
379
+ root = root.host.getRootNode?.();
380
+ }
381
+ if (h.length) snap.h = h;
382
+ return snap;
383
+ }
384
+ function isMasked(el, mask) {
385
+ if (!mask) return false;
386
+ try {
387
+ return !!closestComposed2(el, mask);
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+ function featureAttributeOf(target, attribute) {
393
+ if (!attribute) return null;
394
+ try {
395
+ const el = closestComposed2(target, `[${attribute}]`);
396
+ const value = el?.getAttribute(attribute)?.replace(/\s+/g, " ").trim();
397
+ return el && value ? { el, value: value.slice(0, 100) } : null;
398
+ } catch {
399
+ return null;
400
+ }
401
+ }
402
+ function readProperty(def, base, opts) {
403
+ let el = base;
404
+ if (def.selector) {
405
+ try {
406
+ el = querySelectorStrategy({ kind: "css", value: def.selector }, document)[0] ?? null;
407
+ } catch {
408
+ el = null;
409
+ }
410
+ }
411
+ if (!el) return void 0;
412
+ if (def.source === "attribute") {
413
+ const name = (def.attribute ?? "").toLowerCase();
414
+ if (!name || name === "value" || name.startsWith("on")) return void 0;
415
+ if (opts.maskAll || isMasked(el, opts.mask)) return void 0;
416
+ const v = el.getAttribute(name);
417
+ return v ? v.replace(/\s+/g, " ").trim().slice(0, TAG_PROPERTY_LIMITS.value) || void 0 : void 0;
418
+ }
419
+ return elementText(el, opts.mask, opts.maskAll);
420
+ }
421
+ function clickPropertyValues(config, el, click, opts) {
422
+ if (!config?.properties.length) return void 0;
423
+ const out = {};
424
+ const css = (selector) => {
425
+ try {
426
+ return !!closestComposed2(el, selector);
427
+ } catch {
428
+ return false;
429
+ }
430
+ };
431
+ const here = { url: location.href, path: location.pathname, title: document.title };
432
+ for (const tag of config.properties) {
433
+ if (tag.kind !== "feature" || !tag.featureRules) continue;
434
+ const rules = tag.featureRules;
435
+ const matches2 = (r) => featureRuleMatchesClick(r, click, css) && (!r.page?.pattern || pageRuleMatches({ type: "url", value: r.page.pattern }, here));
436
+ if (!rules.some((r) => !r.exclude && matches2(r)) || rules.some((r) => r.exclude && matches2(r))) continue;
437
+ for (const def of tag.properties) {
438
+ const v = readProperty(def, el, opts);
439
+ if (v) out[def.id] = v;
440
+ }
441
+ }
442
+ return Object.keys(out).length ? out : void 0;
443
+ }
444
+ function pagePropertyValues(config, url, opts) {
445
+ if (!config?.properties.length) return void 0;
446
+ const out = {};
447
+ let path = "/";
448
+ try {
449
+ path = new URL(url, location.href).pathname;
450
+ } catch {
451
+ }
452
+ const ctx = { url, path, title: document.title };
453
+ for (const tag of config.properties) {
454
+ if (tag.kind !== "page" || !tag.pageRules || !pageRulesMatch(tag.pageRules, ctx)) continue;
455
+ for (const def of tag.properties) {
456
+ if (!def.selector) continue;
457
+ const v = readProperty(def, null, opts);
458
+ if (v) out[def.id] = v;
459
+ }
460
+ }
461
+ return Object.keys(out).length ? out : void 0;
462
+ }
463
+
117
464
  // src/authoring.ts
118
465
  async function loadSchemas() {
119
466
  const m = await import("@guidelytech/shared");
@@ -184,6 +531,8 @@ var AuthoringBridge = class {
184
531
  this.opts.onActive(true);
185
532
  this.session = startAuthoring({
186
533
  dashboardOrigin: this.info.dashboardOrigin,
534
+ // Tag mode (Pages & Features): picks become feature rules; the page itself is tagged from the dashboard.
535
+ message: this.info.purpose === "tag" ? "Guidely tagging \u2014 click an element to tag it as a feature \xB7 tag the page from the dashboard \xB7 Esc to finish" : void 0,
187
536
  onPick: (pick) => this.sendPick(pick),
188
537
  onExit: () => this.stop()
189
538
  });
@@ -200,7 +549,7 @@ var AuthoringBridge = class {
200
549
  masked(el) {
201
550
  return this.opts.maskSelectors.some((sel) => {
202
551
  try {
203
- return !!closestComposed(el, sel);
552
+ return !!closestComposed3(el, sel);
204
553
  } catch {
205
554
  return false;
206
555
  }
@@ -211,6 +560,7 @@ var AuthoringBridge = class {
211
560
  const { element, ...rest } = raw;
212
561
  const pick = this.masked(element) ? { ...rest, textSnapshot: "", selectors: rest.selectors.filter((s) => s.kind !== "text") } : rest;
213
562
  pick.url = window.location.href;
563
+ if (this.info.purpose === "tag") pick.elementSnapshot = elementSnapshot(element, { mask: this.opts.maskSelectors.join(","), maskAll: false, featureAttribute: null });
214
564
  this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
215
565
  void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
216
566
  }
@@ -380,139 +730,6 @@ function takeCaptureToken(sessionStore) {
380
730
  // src/load-capture.ts
381
731
  var loadCapture = async () => (await import("./capture.js")).capture;
382
732
 
383
- // src/autocapture.ts
384
- import { closestComposed as closestComposed2, isGuidelyElement, isStableId, SHADOW_SEPARATOR, shadowHostChain } from "@guidelytech/guide-runtime";
385
- var INTERACTIVE_SELECTOR = "a,button,input[type=button],input[type=submit],input[type=reset],input[type=checkbox],input[type=radio],select,summary,label,[role=button],[role=link],[role=tab],[role=menuitem],[role=checkbox],[role=switch],[role=option],[onclick],[data-guidely-track]";
386
- var DEFAULT_MASK_SELECTORS = ["[data-guidely-mask]", ".guidely-mask"];
387
- var PREFERRED_ATTRS = ["data-testid", "data-test-id", "data-test", "data-cy", "data-qa", "data-guidely-track", "data-tour"];
388
- var NOISY_DATA_ATTR = /^data-(state|v-|react|radix|headlessui|svelte|orientation|highlighted|focus|key|index|side|align|disabled|selected|active|open|guidely-(mask|ignore|block))/i;
389
- function validSelectors(list) {
390
- const out = [];
391
- const probe = typeof document !== "undefined" ? document.createDocumentFragment() : null;
392
- for (const raw of list) {
393
- const s = (raw || "").trim();
394
- if (!s || out.includes(s)) continue;
395
- try {
396
- probe?.querySelector(s);
397
- out.push(s);
398
- } catch {
399
- }
400
- }
401
- return out;
402
- }
403
- function isIgnoredElement(el) {
404
- return isGuidelyElement(el) || !!closestComposed2(el, "[data-guidely-ignore]");
405
- }
406
- function interactiveElement(target) {
407
- try {
408
- return closestComposed2(target, INTERACTIVE_SELECTOR);
409
- } catch {
410
- return null;
411
- }
412
- }
413
- function esc(value) {
414
- const g = globalThis;
415
- return g.CSS?.escape ? g.CSS.escape(value) : value.replace(/[^\w-]/g, (c) => `\\${c}`);
416
- }
417
- var attr = (tag, name, value) => value ? `${tag}[${name}="${value.replace(/["\\]/g, "\\$&")}"]` : `${tag}[${name}]`;
418
- function elementSelector(el) {
419
- const own = ownSelector(el);
420
- const chain = shadowHostChain(el);
421
- return (chain ? chain + SHADOW_SEPARATOR + own : own).slice(0, 300);
422
- }
423
- function ownSelector(el) {
424
- const tag = el.tagName.toLowerCase();
425
- if (el.id && isStableId(el.id)) return `#${esc(el.id)}`;
426
- for (const name of PREFERRED_ATTRS) if (el.hasAttribute(name)) return attr(tag, name, el.getAttribute(name) || "");
427
- for (const a of Array.from(el.attributes)) {
428
- if (a.name.startsWith("data-") && !NOISY_DATA_ATTR.test(a.name) && a.value && a.value.length <= 60) return attr(tag, a.name, a.value);
429
- }
430
- const label = el.getAttribute("aria-label");
431
- if (label && label.length <= 80) return attr(tag, "aria-label", label);
432
- const parts = [];
433
- let cur = el;
434
- while (cur && parts.length < 4) {
435
- const t = cur.tagName.toLowerCase();
436
- if (t === "html" || t === "body") break;
437
- if (cur !== el && cur.id && isStableId(cur.id)) {
438
- parts.unshift(`#${esc(cur.id)}`);
439
- break;
440
- }
441
- const parent = cur.parentElement;
442
- const node = cur;
443
- const same = parent ? Array.from(parent.children).filter((c) => c.tagName === node.tagName) : [];
444
- parts.unshift(same.length > 1 ? `${t}:nth-of-type(${same.indexOf(node) + 1})` : t);
445
- cur = parent;
446
- }
447
- return parts.join(" > ") || tag;
448
- }
449
- function matches(el, selector, deep) {
450
- if (!selector) return false;
451
- try {
452
- return !!closestComposed2(el, selector) || deep && !!el.querySelector(selector);
453
- } catch {
454
- return false;
455
- }
456
- }
457
- function elementText(el, mask, maskAll) {
458
- if (maskAll || matches(el, mask, true)) return void 0;
459
- const tag = el.tagName.toLowerCase();
460
- let text = "";
461
- if (tag === "input") {
462
- if (!/^(button|submit|reset)$/i.test(el.type || "")) return void 0;
463
- text = el.value;
464
- } else if (tag === "select" || tag === "textarea") return void 0;
465
- else text = el.innerText ?? el.textContent ?? "";
466
- text = (text || el.getAttribute("aria-label") || el.getAttribute("title") || "").replace(/\s+/g, " ").trim();
467
- return text ? text.slice(0, 100) : void 0;
468
- }
469
- function linkHref(el) {
470
- const a = closestComposed2(el, "a[href]");
471
- if (!a) return void 0;
472
- try {
473
- const u = new URL(a.getAttribute("href") || "", location.href);
474
- if (!/^https?:$/.test(u.protocol)) return void 0;
475
- return u.origin === location.origin ? u.pathname : u.origin;
476
- } catch {
477
- return void 0;
478
- }
479
- }
480
- function describeClick(el, e, opts) {
481
- const props = { tag: el.tagName.toLowerCase(), selector: elementSelector(el), x: Math.round(e.pageX ?? 0) || 0, y: Math.round(e.pageY ?? 0) || 0 };
482
- const text = elementText(el, opts.mask, opts.maskAll);
483
- if (text) props.text = text;
484
- const href = opts.collectUrls ? linkHref(el) : void 0;
485
- if (href) props.href = href;
486
- return props;
487
- }
488
- var RageClickDetector = class {
489
- constructor(threshold = 3, windowMs = 1e3, radius = 30) {
490
- __publicField(this, "threshold", threshold);
491
- __publicField(this, "windowMs", windowMs);
492
- __publicField(this, "radius", radius);
493
- __publicField(this, "clicks", []);
494
- __publicField(this, "fired", false);
495
- }
496
- /** Clicks in the current burst once it has fired (rage), otherwise 0. */
497
- get burst() {
498
- return this.fired ? this.clicks.length : 0;
499
- }
500
- /** Records a click; returns the click count when it completes a burst, otherwise 0. */
501
- add(x, y, t) {
502
- const first = this.clicks[0];
503
- const last = this.clicks[this.clicks.length - 1];
504
- if (!first || !last || t - last.t > this.windowMs || Math.hypot(x - first.x, y - first.y) > this.radius) {
505
- this.clicks = [];
506
- this.fired = false;
507
- }
508
- this.clicks = this.clicks.filter((c) => t - c.t <= this.windowMs);
509
- this.clicks.push({ x, y, t });
510
- if (this.fired || this.clicks.length < this.threshold) return 0;
511
- this.fired = true;
512
- return this.clicks.length;
513
- }
514
- };
515
-
516
733
  // src/context.ts
517
734
  function detectDevice(ua, width) {
518
735
  const u = ua || "";
@@ -522,7 +739,8 @@ function detectDevice(ua, width) {
522
739
  else if (width > 0 && width < 768) type = "mobile";
523
740
  else if (width > 0 && width < 1024) type = "tablet";
524
741
  let browser;
525
- if (/Edg\//i.test(u)) browser = "edge";
742
+ if (/Electron\//i.test(u)) browser = "electron";
743
+ else if (/Edg\//i.test(u)) browser = "edge";
526
744
  else if (/OPR\/|Opera/i.test(u)) browser = "opera";
527
745
  else if (/SamsungBrowser/i.test(u)) browser = "samsung";
528
746
  else if (/Chrome\/|CriOS/i.test(u)) browser = "chrome";
@@ -788,6 +1006,14 @@ function createMessengerTransport(opts) {
788
1006
  h.onDone(d.messages.at(-1) ?? null, d.conversation);
789
1007
  } else if (!finished) h.onError("The reply was interrupted");
790
1008
  }
1009
+ async function feedback(path, body, fallback) {
1010
+ const res = await api.post(`/feedback${path}`, { ...body, anonymousId: opts.anonymousId(), userId: opts.userId() });
1011
+ if (!res.ok || !res.data) {
1012
+ if (res.status === 429) throw new HttpError(429, "You're doing that a lot. Please try again in a minute.");
1013
+ throw new HttpError(res.status, serverMessage(res.data) ?? fallback);
1014
+ }
1015
+ return res.data;
1016
+ }
791
1017
  return {
792
1018
  async listConversations() {
793
1019
  const tokens = storedConversations(store, projectId).map((c) => c.token);
@@ -824,7 +1050,8 @@ function createMessengerTransport(opts) {
824
1050
  return res.data;
825
1051
  },
826
1052
  async getArticle(id) {
827
- const res = await api.get(`/articles/${encodeURIComponent(id)}`);
1053
+ const locale = opts.locale?.();
1054
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
828
1055
  return res.ok && res.data ? res.data.article : null;
829
1056
  },
830
1057
  async fileComplaint(input) {
@@ -854,7 +1081,19 @@ function createMessengerTransport(opts) {
854
1081
  async getHowTo(id) {
855
1082
  const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
856
1083
  return res.ok && res.data ? res.data.howTo : null;
857
- }
1084
+ },
1085
+ // Ideas (feature requests). The visitor's ids go in every body; the server decides what they may do.
1086
+ listIdeas: (query) => feedback("/requests/query", query, "Ideas couldn't be loaded."),
1087
+ getIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/view`, {}, "This idea couldn't be loaded."),
1088
+ submitIdea: (input) => feedback("/requests", { ...input, pageUrl: opts.pageUrl() }, "Your idea couldn't be sent. Please try again."),
1089
+ voteIdea: (id, vote) => feedback(`/requests/${encodeURIComponent(id)}/vote`, vote, "Your vote couldn't be saved."),
1090
+ unvoteIdea: (id) => feedback(`/requests/${encodeURIComponent(id)}/unvote`, {}, "Your vote couldn't be removed."),
1091
+ followIdea: (id, follow) => feedback(`/requests/${encodeURIComponent(id)}/follow`, { follow }, "That didn't work. Please try again."),
1092
+ commentIdea: (id, body) => feedback(`/requests/${encodeURIComponent(id)}/comments`, { body }, "Your comment couldn't be posted."),
1093
+ similarIdeas: async (title) => (await feedback("/similar", { title }, "Similar ideas couldn't be loaded.")).requests ?? [],
1094
+ ideaUpdates: () => feedback("/updates", {}, "Updates couldn't be loaded."),
1095
+ markIdeaUpdatesRead: (ids) => feedback("/updates/read", ids ? { ids } : {}, "Updates couldn't be marked read."),
1096
+ ideaPortalLink: async () => (await feedback("/portal-link", {}, "The feedback portal couldn't be opened.")).url
858
1097
  };
859
1098
  }
860
1099
 
@@ -984,6 +1223,48 @@ function watchNavigation(listener, debounceMs = 50) {
984
1223
  };
985
1224
  }
986
1225
 
1226
+ // src/page-url.ts
1227
+ var WEB_SCHEMES = /* @__PURE__ */ new Set(["http", "https", "about", "data", "blob", "javascript"]);
1228
+ function appScheme(href) {
1229
+ let u;
1230
+ try {
1231
+ u = new URL(href);
1232
+ } catch {
1233
+ return null;
1234
+ }
1235
+ const scheme = u.protocol.replace(/:$/, "").toLowerCase();
1236
+ return WEB_SCHEMES.has(scheme) ? null : { u, scheme };
1237
+ }
1238
+ function normalizeAppUrl(href) {
1239
+ const app = appScheme(href);
1240
+ if (!app) return href;
1241
+ const { u, scheme } = app;
1242
+ const host = scheme === "file" ? "app" : u.host || "app";
1243
+ const hash = /^#!?(\/.*)$/.exec(u.hash);
1244
+ if (hash) {
1245
+ const route = hash[1];
1246
+ return `${scheme}://${host}${route.startsWith("/") ? route : `/${route}`}`;
1247
+ }
1248
+ let path = safeDecode(u.pathname);
1249
+ if (scheme === "file" || /\.html?$/i.test(path)) {
1250
+ const file = path.split("/").filter(Boolean).pop() ?? "";
1251
+ const name = file.replace(/\.html?$/i, "");
1252
+ path = !name || name.toLowerCase() === "index" ? "/" : `/${name}`;
1253
+ }
1254
+ if (!path.startsWith("/")) path = `/${path}`;
1255
+ return `${scheme}://${host}${path}${u.search}`;
1256
+ }
1257
+ function safeDecode(s) {
1258
+ try {
1259
+ return decodeURI(s);
1260
+ } catch {
1261
+ return s;
1262
+ }
1263
+ }
1264
+ function isAppUrl(href) {
1265
+ return appScheme(href) !== null;
1266
+ }
1267
+
987
1268
  // src/page-tracking.ts
988
1269
  function scrollDepth() {
989
1270
  const de = document.documentElement;
@@ -1029,12 +1310,112 @@ var PageTracker = class {
1029
1310
  }
1030
1311
  };
1031
1312
 
1313
+ // src/prototype-overlay.ts
1314
+ var PROTOTYPE_CLOSE_MESSAGE = "guidely:prototype:close";
1315
+ var TOKEN = /^[A-Za-z0-9_-]{16,64}$/;
1316
+ var OVERLAY_ATTR = "data-guidely-prototype-overlay";
1317
+ function matchPrototypeTestUrl(urlOrToken, apiUrl) {
1318
+ const value = (urlOrToken || "").trim();
1319
+ let api;
1320
+ try {
1321
+ api = new URL(apiUrl);
1322
+ } catch {
1323
+ return null;
1324
+ }
1325
+ if (TOKEN.test(value)) return new URL(`/validate/${value}`, api.origin);
1326
+ let url;
1327
+ try {
1328
+ url = new URL(value, typeof location !== "undefined" ? location.href : api.origin);
1329
+ } catch {
1330
+ return null;
1331
+ }
1332
+ if (url.origin !== api.origin) return null;
1333
+ const m = /^\/validate\/([^/]+)\/?$/.exec(url.pathname);
1334
+ if (!m || !TOKEN.test(m[1])) return null;
1335
+ return new URL(`/validate/${m[1]}`, api.origin);
1336
+ }
1337
+ var current = null;
1338
+ function openPrototypeOverlay(testUrl, opts) {
1339
+ current?.close();
1340
+ const doc = document;
1341
+ const previous = doc.activeElement;
1342
+ const src = new URL(testUrl.href);
1343
+ const hash = new URLSearchParams();
1344
+ hash.set("key", opts.key);
1345
+ if (opts.anonymousId) hash.set("anon", opts.anonymousId);
1346
+ if (opts.userId) hash.set("uid", opts.userId);
1347
+ src.hash = hash.toString();
1348
+ const root = doc.createElement("div");
1349
+ root.setAttribute(OVERLAY_ATTR, "");
1350
+ root.setAttribute("role", "dialog");
1351
+ root.setAttribute("aria-modal", "true");
1352
+ root.setAttribute("aria-label", "Prototype test");
1353
+ root.style.cssText = `position:fixed;inset:0;z-index:${opts.zIndex ?? 2147483646};background:rgba(15,23,42,.55);display:flex;align-items:stretch;justify-content:center;padding:0;margin:0;`;
1354
+ const frame = doc.createElement("iframe");
1355
+ frame.src = src.toString();
1356
+ frame.title = "Prototype test";
1357
+ frame.setAttribute("allow", "fullscreen");
1358
+ frame.setAttribute("referrerpolicy", "no-referrer");
1359
+ frame.style.cssText = "border:0;width:100%;height:100%;background:#fff;display:block;";
1360
+ const close = doc.createElement("button");
1361
+ close.type = "button";
1362
+ close.setAttribute("aria-label", "Close prototype test");
1363
+ close.textContent = "\xD7";
1364
+ close.style.cssText = "position:absolute;top:10px;right:10px;width:36px;height:36px;border-radius:999px;border:0;background:rgba(15,23,42,.75);color:#fff;font:600 22px/36px system-ui,sans-serif;text-align:center;cursor:pointer;padding:0;z-index:1;";
1365
+ root.append(frame, close);
1366
+ const overflow = doc.documentElement.style.overflow;
1367
+ doc.documentElement.style.overflow = "hidden";
1368
+ let closed = false;
1369
+ const shut = () => {
1370
+ if (closed) return;
1371
+ closed = true;
1372
+ window.removeEventListener("message", onMessage);
1373
+ doc.removeEventListener("keydown", onKey, true);
1374
+ doc.removeEventListener("focusin", onFocus, true);
1375
+ root.remove();
1376
+ doc.documentElement.style.overflow = overflow;
1377
+ if (current === overlay) current = null;
1378
+ try {
1379
+ previous?.focus?.();
1380
+ } catch {
1381
+ }
1382
+ opts.onClose?.();
1383
+ };
1384
+ const onMessage = (e) => {
1385
+ if (e.origin !== testUrl.origin || e.source !== frame.contentWindow) return;
1386
+ const data = e.data;
1387
+ if (data && typeof data === "object" && data.type === PROTOTYPE_CLOSE_MESSAGE) shut();
1388
+ };
1389
+ const onKey = (e) => {
1390
+ if (e.key === "Escape") {
1391
+ e.preventDefault();
1392
+ e.stopPropagation();
1393
+ shut();
1394
+ } else if (e.key === "Tab") {
1395
+ e.preventDefault();
1396
+ (doc.activeElement === close ? frame : close).focus();
1397
+ }
1398
+ };
1399
+ const onFocus = (e) => {
1400
+ if (!root.contains(e.target)) close.focus();
1401
+ };
1402
+ close.addEventListener("click", shut);
1403
+ window.addEventListener("message", onMessage);
1404
+ doc.addEventListener("keydown", onKey, true);
1405
+ doc.addEventListener("focusin", onFocus, true);
1406
+ doc.body.appendChild(root);
1407
+ frame.focus();
1408
+ const overlay = { element: root, close: shut };
1409
+ current = overlay;
1410
+ return overlay;
1411
+ }
1412
+
1032
1413
  // src/load-recorder.ts
1033
1414
  var loadRecorder = async () => (await import("./recorder.js")).recorder;
1034
1415
 
1035
1416
  // src/client.ts
1036
1417
  var SDK_NAME = "@guidelytech/sdk-core";
1037
- var SDK_VERSION = "0.10.0";
1418
+ var SDK_VERSION = "0.11.0";
1038
1419
  var DEFAULT_API_URL = "https://api.guidely.app";
1039
1420
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
1040
1421
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -1060,7 +1441,7 @@ function emptyState() {
1060
1441
  return { guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] };
1061
1442
  }
1062
1443
  function newSession(now = Date.now()) {
1063
- return { id: `s_${randomId(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1444
+ return { id: `s_${randomId2(16)}`, startedAt: new Date(now).toISOString(), shownGuides: [], events: [], lastActivityAt: now };
1064
1445
  }
1065
1446
  var GuidelyClient = class _GuidelyClient {
1066
1447
  constructor(options) {
@@ -1072,6 +1453,8 @@ var GuidelyClient = class _GuidelyClient {
1072
1453
  __publicField(this, "queue");
1073
1454
  /** Survey answers and guide/checklist progress, kept until the server accepts them. */
1074
1455
  __publicField(this, "outbox");
1456
+ /** Agent Analytics turns (agentConversation), in their own outbox. */
1457
+ __publicField(this, "agentTurns");
1075
1458
  __publicField(this, "config", null);
1076
1459
  __publicField(this, "etag", null);
1077
1460
  __publicField(this, "anonymousId");
@@ -1121,6 +1504,10 @@ var GuidelyClient = class _GuidelyClient {
1121
1504
  __publicField(this, "flags");
1122
1505
  /** Experiments already reported this session (`experiment_viewed` is sent once per session). */
1123
1506
  __publicField(this, "exposures", /* @__PURE__ */ new Set());
1507
+ /** Language set with `setLocale()`. */
1508
+ __publicField(this, "locale", null);
1509
+ /** Language each shown guide was rendered in; added to its events as `locale`. */
1510
+ __publicField(this, "guideLocales", /* @__PURE__ */ new Map());
1124
1511
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
1125
1512
  __publicField(this, "eventBus", {
1126
1513
  on: (name, cb) => {
@@ -1155,7 +1542,7 @@ var GuidelyClient = class _GuidelyClient {
1155
1542
  this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
1156
1543
  this.anonymousId = this.store.get(KEYS.anon) || "";
1157
1544
  if (!this.anonymousId) {
1158
- this.anonymousId = `anon_${randomId(20)}`;
1545
+ this.anonymousId = `anon_${randomId2(20)}`;
1159
1546
  this.store.set(KEYS.anon, this.anonymousId);
1160
1547
  }
1161
1548
  this.firstVisit = !this.store.get(KEYS.seen);
@@ -1165,7 +1552,7 @@ var GuidelyClient = class _GuidelyClient {
1165
1552
  const session = readJson(this.sessionStore, KEYS.session, null);
1166
1553
  this.session = session && session.id ? session : newSession();
1167
1554
  this.persistSession();
1168
- this.lastUrl = location.href;
1555
+ this.lastUrl = this.pageHref();
1169
1556
  this.lastAudiences = new Set(this.state.audienceIds);
1170
1557
  this.queue = new EventQueue({
1171
1558
  store: this.store,
@@ -1180,6 +1567,7 @@ var GuidelyClient = class _GuidelyClient {
1180
1567
  onError: (err) => this.reportError(err)
1181
1568
  });
1182
1569
  this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(this.options.projectId), health: this.api.health });
1570
+ this.agentTurns = new AgentTurns({ api: this.api, store: this.store, storageKey: KEYS.agentOutbox(this.options.projectId), health: this.api.health, sdk: { name: SDK_NAME, version: SDK_VERSION } });
1183
1571
  if (options.user) this.user = { ...options.user };
1184
1572
  this.flags = this.createFlags(options.flags);
1185
1573
  }
@@ -1256,7 +1644,7 @@ var GuidelyClient = class _GuidelyClient {
1256
1644
  this.resumeActiveGuide();
1257
1645
  this.evaluate("ready");
1258
1646
  if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1259
- this.pageTracker.start(location.href);
1647
+ this.pageTracker.start(this.pageHref());
1260
1648
  this.touchSession();
1261
1649
  this.syncCapture();
1262
1650
  } catch (err) {
@@ -1289,7 +1677,7 @@ var GuidelyClient = class _GuidelyClient {
1289
1677
  wireListeners() {
1290
1678
  const doc = document;
1291
1679
  const win = window;
1292
- this.cleanups.push(watchNavigation((url) => this.handleNavigation(url)));
1680
+ this.cleanups.push(watchNavigation((url) => this.handleNavigation(this.pageHref(url))));
1293
1681
  const onClick = (e) => this.handleDocumentClick(e);
1294
1682
  doc.addEventListener("click", onClick, true);
1295
1683
  this.cleanups.push(() => doc.removeEventListener("click", onClick, true));
@@ -1310,7 +1698,7 @@ var GuidelyClient = class _GuidelyClient {
1310
1698
  const onPageShow = (e) => {
1311
1699
  if (!e.persisted || !this.pageLeft) return;
1312
1700
  this.pageLeft = false;
1313
- this.pageTracker.start(location.href);
1701
+ this.pageTracker.start(this.pageHref());
1314
1702
  };
1315
1703
  let lastScroll = 0;
1316
1704
  const onScroll = () => {
@@ -1359,7 +1747,7 @@ var GuidelyClient = class _GuidelyClient {
1359
1747
  this.cleanups.push(() => clearInterval(refresh));
1360
1748
  const poll = setInterval(() => {
1361
1749
  if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
1362
- const url = location.href;
1750
+ const url = this.pageHref();
1363
1751
  const ctx = this.getContext();
1364
1752
  for (const guide of this.config.guides) {
1365
1753
  if (guide.config.trigger.type === "element_visible" && guide.config.trigger.value) this.armVisibility(guide, ctx, url);
@@ -1428,10 +1816,11 @@ var GuidelyClient = class _GuidelyClient {
1428
1816
  this.queue.flushSync();
1429
1817
  this.queue.destroy();
1430
1818
  this.outbox.destroy();
1819
+ this.agentTurns.destroy();
1431
1820
  this.flags.destroy();
1432
1821
  this.emitter.clear();
1433
1822
  destroyHost();
1434
- if (current === this) current = null;
1823
+ if (current2 === this) current2 = null;
1435
1824
  }
1436
1825
  // ---------------------------------------------------------------------------
1437
1826
  // Identity
@@ -1449,7 +1838,7 @@ var GuidelyClient = class _GuidelyClient {
1449
1838
  if (this.isReady) this.evaluate("identify");
1450
1839
  }
1451
1840
  async syncIdentify() {
1452
- const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? location.href : void 0 };
1841
+ const body = { anonymousId: this.anonymousId, userId: this.user?.id ?? null, traits: this.user?.traits, account: this.user?.account ? { id: this.user.account.id, traits: this.user.account.traits } : null, sessionId: this.session.id, url: this.collectUrls() ? this.pageHref() : void 0 };
1453
1842
  const res = await postWithRetry(this.api, "/identify", body, 2);
1454
1843
  if (res?.ok && res.data) this.mergeServerState(res.data);
1455
1844
  }
@@ -1480,7 +1869,10 @@ var GuidelyClient = class _GuidelyClient {
1480
1869
  checklists,
1481
1870
  surveysSubmitted: Array.from(/* @__PURE__ */ new Set([...this.state.surveysSubmitted, ...data.surveysSubmitted || []])),
1482
1871
  audienceIds: data.audienceIds || [],
1483
- firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt
1872
+ firstSeenAt: this.state.firstSeenAt ?? data.firstSeenAt,
1873
+ // Device-local only (the server doesn't hold them).
1874
+ ...this.state.throttle ? { throttle: this.state.throttle } : {},
1875
+ ...this.state.announcementsSeenAt ? { announcementsSeenAt: this.state.announcementsSeenAt } : {}
1484
1876
  };
1485
1877
  if (data.firstSeenAt && Date.now() - Date.parse(data.firstSeenAt) > 6e4) this.firstVisit = false;
1486
1878
  this.persistState();
@@ -1516,7 +1908,7 @@ var GuidelyClient = class _GuidelyClient {
1516
1908
  this.stopGuide();
1517
1909
  this.user = null;
1518
1910
  this.store.remove(KEYS.user(this.options.projectId));
1519
- this.anonymousId = `anon_${randomId(20)}`;
1911
+ this.anonymousId = `anon_${randomId2(20)}`;
1520
1912
  this.store.set(KEYS.anon, this.anonymousId);
1521
1913
  this.state = emptyState();
1522
1914
  this.persistState();
@@ -1548,6 +1940,7 @@ var GuidelyClient = class _GuidelyClient {
1548
1940
  if (consent === "denied") this.queue.clear();
1549
1941
  else if (consent === "granted") void this.queue.flush();
1550
1942
  if (consent !== "granted") this.replay?.stop({ discard: true });
1943
+ if (consent !== "pending") this.agentTurns.consentChanged(consent === "granted");
1551
1944
  this.syncCapture();
1552
1945
  }
1553
1946
  analyticsEnabled() {
@@ -1560,17 +1953,19 @@ var GuidelyClient = class _GuidelyClient {
1560
1953
  if (this.consent === "denied" || !this.options.analytics) return null;
1561
1954
  if (name !== "page_leave" && name !== "heatmap") this.touchSession();
1562
1955
  const event = {
1563
- eventId: randomId(24),
1956
+ eventId: randomId2(24),
1564
1957
  name,
1565
1958
  occurredAt: nowIso(),
1566
1959
  anonymousId: this.anonymousId,
1567
1960
  userId: this.user?.id || null,
1568
1961
  accountId: this.user?.account?.id || null,
1569
1962
  sessionId: this.session.id,
1570
- url: this.collectUrls() ? location.href : void 0,
1963
+ url: this.collectUrls() ? this.pageHref() : void 0,
1571
1964
  ...extra
1572
1965
  };
1573
1966
  if (extra.properties) event.properties = sanitizeProperties(extra.properties);
1967
+ const locale = extra.guideId ? this.guideLocales.get(extra.guideId) : void 0;
1968
+ if (locale) event.properties = { ...event.properties ?? {}, locale };
1574
1969
  this.queue.enqueue(event);
1575
1970
  this.replay?.addCustomEvent("guidely", { name, customName: event.customName, guideId: event.guideId });
1576
1971
  this.emitter.emit("track", event);
@@ -1594,9 +1989,30 @@ var GuidelyClient = class _GuidelyClient {
1594
1989
  this.completeChecklistTasksBy("event", name);
1595
1990
  if (this.isReady) this.evaluate("event", { eventName: name });
1596
1991
  }
1992
+ /**
1993
+ * Agent Analytics: records one or more turns of a conversation with your own AI agent or chatbot
1994
+ * (`{ conversationId, agentId, role, text, toolCalls?, latencyMs?, outcome?, … }`). The visitor's
1995
+ * anonymous id, user id, account id and session id are filled in, `agents.redact` from the init
1996
+ * options runs on each turn, and turns are retried until Guidely accepts them. Returns false when
1997
+ * nothing was queued (analytics off or consent denied, or every turn was invalid or dropped).
1998
+ */
1999
+ agentConversation(turns) {
2000
+ if (this.destroyed || !this.options.analytics || this.consent === "denied" || this.config?.settings?.analyticsEnabled === false) return false;
2001
+ const identity = { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, sessionId: this.session.id };
2002
+ const prepared = (Array.isArray(turns) ? turns : [turns]).map((t) => prepareAgentTurn(t, identity, this.options.agents?.redact, (err) => this.reportError(err))).filter((t) => !!t);
2003
+ if (!prepared.length) return false;
2004
+ this.touchSession();
2005
+ this.agentTurns.add(prepared, this.consent === "pending");
2006
+ return true;
2007
+ }
2008
+ /** Alias of {@link agentConversation}. */
2009
+ trackAgentTurn(turns) {
2010
+ return this.agentConversation(turns);
2011
+ }
1597
2012
  /** Records a page view and re-evaluates triggers. Called automatically on SPA navigation. */
1598
- page(url = location.href) {
2013
+ page(href) {
1599
2014
  if (this.destroyed) return;
2015
+ const url = this.pageHref(href);
1600
2016
  this.leavePage(url);
1601
2017
  this.trackPageView(url);
1602
2018
  this.handleNavigation(url, true);
@@ -1688,6 +2104,8 @@ var GuidelyClient = class _GuidelyClient {
1688
2104
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
1689
2105
  if (ref) props.referrer = ref;
1690
2106
  }
2107
+ const ep = this.captureAllowed() ? pagePropertyValues(this.config?.tagging, url, { mask: this.maskSelector(), maskAll: this.maskAllText() }) : void 0;
2108
+ if (ep) props.ep = ep;
1691
2109
  this.session.pageViews = (this.session.pageViews ?? 0) + 1;
1692
2110
  this.persistSession();
1693
2111
  this.enqueue("page_view", { url: collect ? url : void 0, properties: props });
@@ -1713,13 +2131,20 @@ var GuidelyClient = class _GuidelyClient {
1713
2131
  const heat = this.autocaptureOn("heatmaps");
1714
2132
  if (!clicks && !rage && !heat || isIgnoredElement(target)) return;
1715
2133
  this.touchSession();
1716
- const el = interactiveElement(target);
2134
+ const featureAttribute = this.config?.tagging?.featureAttribute ?? null;
2135
+ const feature = featureAttributeOf(target, featureAttribute);
2136
+ const el = interactiveElement(target) ?? feature?.el ?? null;
1717
2137
  const opts = { mask: this.maskSelector(), maskAll: this.maskAllText(), collectUrls: this.collectUrls() };
1718
- if (clicks && el) this.enqueue("click", { properties: { ...describeClick(el, e, opts) } });
2138
+ const clickProps = (on) => {
2139
+ const p = describeClick(on, e, opts);
2140
+ const ep = clickPropertyValues(this.config?.tagging, on, p, opts);
2141
+ return { ...p, el: elementSnapshot(on, { ...opts, featureAttribute }), ...feature ? { fa: feature.value } : {}, ...ep ? { ep } : {} };
2142
+ };
2143
+ if (clicks && el) this.enqueue("click", { properties: clickProps(el) });
1719
2144
  const count = this.rage.add(e.pageX ?? 0, e.pageY ?? 0, Date.now());
1720
- if (rage && count) this.enqueue("rage_click", { properties: { ...describeClick(el ?? target, e, opts), count } });
2145
+ if (rage && count) this.enqueue("rage_click", { properties: { ...clickProps(el ?? target), count } });
1721
2146
  if (!heat) return;
1722
- const url = location.href;
2147
+ const url = this.pageHref();
1723
2148
  if (this.heatmap.url !== url) this.flushHeatmap();
1724
2149
  if (this.heatmap.add(url, heatmapPoint(el ?? target, e, this.rage.burst > 0), this.rage.burst)) this.flushHeatmap();
1725
2150
  }
@@ -1727,7 +2152,7 @@ var GuidelyClient = class _GuidelyClient {
1727
2152
  sampleMove(e) {
1728
2153
  const target = composedTarget(e);
1729
2154
  if (document.visibilityState === "hidden" || !target?.closest || !this.autocaptureOn("mouseMovement") || isIgnoredElement(target)) return;
1730
- const url = location.href;
2155
+ const url = this.pageHref();
1731
2156
  if (this.heatmap.url !== url) this.flushHeatmap();
1732
2157
  if (this.heatmap.move(url, movementBin(interactiveElement(target) ?? target, e))) this.flushHeatmap();
1733
2158
  }
@@ -1803,7 +2228,7 @@ var GuidelyClient = class _GuidelyClient {
1803
2228
  if (!this.isReady || this.destroyed) return;
1804
2229
  if (this.config && !this.initialPageViewSent && this.config.settings?.autocapture) {
1805
2230
  this.initialPageViewSent = true;
1806
- this.trackPageView(location.href);
2231
+ this.trackPageView(this.pageHref());
1807
2232
  }
1808
2233
  if (this.config) {
1809
2234
  for (const [props, extra] of this.earlyExceptions.splice(0)) {
@@ -1838,7 +2263,7 @@ var GuidelyClient = class _GuidelyClient {
1838
2263
  return {
1839
2264
  anonymousId: this.anonymousId,
1840
2265
  userId: this.user?.id || null,
1841
- url: this.collectUrls() ? location.href : void 0,
2266
+ url: this.collectUrls() ? this.pageHref() : void 0,
1842
2267
  device: { type: device.type, browser: device.browser, os: device.os },
1843
2268
  viewport: { width: window.innerWidth, height: window.innerHeight },
1844
2269
  sdk: { name: SDK_NAME, version: SDK_VERSION }
@@ -1889,6 +2314,14 @@ var GuidelyClient = class _GuidelyClient {
1889
2314
  // ---------------------------------------------------------------------------
1890
2315
  // Targeting + triggers
1891
2316
  // ---------------------------------------------------------------------------
2317
+ /** The page URL Guidely works with: `options.pageUrl`, else app-relative for `file:` / custom protocols (`normalizeAppUrl`). */
2318
+ pageHref(href = location.href) {
2319
+ try {
2320
+ return this.options.pageUrl ? this.options.pageUrl(href) || href : normalizeAppUrl(href);
2321
+ } catch {
2322
+ return href;
2323
+ }
2324
+ }
1892
2325
  /** Builds the targeting context for the current moment. */
1893
2326
  getContext() {
1894
2327
  const guides = this.state.guides;
@@ -1899,7 +2332,7 @@ var GuidelyClient = class _GuidelyClient {
1899
2332
  userId: this.user?.id || null,
1900
2333
  userAttributes: this.user?.traits ?? {},
1901
2334
  accountAttributes: this.user?.account?.traits ?? {},
1902
- url: location.href,
2335
+ url: this.pageHref(),
1903
2336
  firstVisit: this.firstVisit,
1904
2337
  eventsSeen: this.session.events,
1905
2338
  completedGuides: keysOf(Object.keys(guides).filter((id) => guides[id]?.completedAt)),
@@ -1908,7 +2341,8 @@ var GuidelyClient = class _GuidelyClient {
1908
2341
  completedChecklists: keysOf(completedChecklists),
1909
2342
  audienceIds: this.state.audienceIds,
1910
2343
  environment: this.options.environment,
1911
- platform: "web"
2344
+ platform: "web",
2345
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {}
1912
2346
  });
1913
2347
  }
1914
2348
  isTargeted(guide, ctx, url) {
@@ -1920,6 +2354,52 @@ var GuidelyClient = class _GuidelyClient {
1920
2354
  ctx.rolloutSalt = guide.id;
1921
2355
  return evaluateRules(c.targeting, ctx);
1922
2356
  }
2357
+ /** Guide throttling: may this guide launch automatically now? */
2358
+ throttleAllows(guide) {
2359
+ if (!countsTowardThrottle(guide)) return true;
2360
+ return isThrottleEligible(this.config?.settings.throttling, this.state.throttle ?? []);
2361
+ }
2362
+ recordLaunch(guide, manual) {
2363
+ if (manual || !countsTowardThrottle(guide) || !this.config?.settings.throttling?.enabled) return;
2364
+ this.state.throttle = pruneLaunches([...this.state.throttle ?? [], nowIso()], this.config.settings.throttling);
2365
+ this.persistState();
2366
+ }
2367
+ /** Guides and A/B test control groups, which are evaluated the same way but never render. */
2368
+ candidates() {
2369
+ const guides = this.config?.guides ?? [];
2370
+ const holdouts = this.config?.guideHoldouts ?? [];
2371
+ return holdouts.length ? [...guides, ...holdouts.map((h) => h.guide)] : guides;
2372
+ }
2373
+ holdoutFor(guide) {
2374
+ return this.config?.guideHoldouts?.find((h) => h.guide.id === guide.id) ?? null;
2375
+ }
2376
+ /** The visitor's language candidates, most preferred first. */
2377
+ locales() {
2378
+ const nav = typeof navigator !== "undefined" ? [...navigator.languages ?? [], navigator.language] : [];
2379
+ return visitorLocales(this.locale ?? this.options.locale, this.user?.traits, nav);
2380
+ }
2381
+ defaultLocale() {
2382
+ return this.config?.settings.localization?.defaultLocale || "en";
2383
+ }
2384
+ /** The guide in the visitor's language (remembered so its events carry `locale`). */
2385
+ localize(guide) {
2386
+ const localized = localizeGuide(guide, this.locales(), this.defaultLocale());
2387
+ this.guideLocales.set(guide.id, localized.locale);
2388
+ return localized;
2389
+ }
2390
+ /**
2391
+ * Sets the language for guides and the resource center (`null` = back to the traits / browser).
2392
+ * Guides already on screen keep their language; the resource center re-renders.
2393
+ */
2394
+ setLocale(locale) {
2395
+ this.locale = locale ? normalizeLocale(locale) || null : null;
2396
+ if (this.resourceCenter) {
2397
+ this.resourceCenter.destroy();
2398
+ this.resourceCenterScope?.destroy();
2399
+ this.resourceCenter = null;
2400
+ this.renderPersistentWidgets();
2401
+ }
2402
+ }
1923
2403
  isEligible(guide, ctx, url) {
1924
2404
  if (!this.isTargeted(guide, ctx, url)) return false;
1925
2405
  const shownThisSession = this.session.shownGuides.includes(guide.id);
@@ -1936,13 +2416,13 @@ var GuidelyClient = class _GuidelyClient {
1936
2416
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
1937
2417
  return reason === "ready" || reason === "navigation" || reason === "identify";
1938
2418
  case "url_match":
1939
- return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches2(t.value, location.href);
2419
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches2(t.value, this.pageHref());
1940
2420
  case "event":
1941
2421
  return reason === "event" && !!t.value && detail.eventName === t.value;
1942
2422
  case "element_click":
1943
2423
  if (reason !== "click" || !t.value || !detail.target) return false;
1944
2424
  try {
1945
- return !!closestComposed3(detail.target, t.value);
2425
+ return !!closestComposed4(detail.target, t.value);
1946
2426
  } catch {
1947
2427
  return false;
1948
2428
  }
@@ -1961,15 +2441,15 @@ var GuidelyClient = class _GuidelyClient {
1961
2441
  }
1962
2442
  evaluate(reason, detail = {}) {
1963
2443
  if (!this.config || this.destroyed || this.editing()) return;
1964
- const url = location.href;
2444
+ const url = this.pageHref();
1965
2445
  const ctx = this.getContext();
1966
2446
  if (reason === "identify") {
1967
- const current2 = new Set(this.state.audienceIds);
1968
- detail.newAudiences = new Set(Array.from(current2).filter((a) => !this.lastAudiences.has(a)));
1969
- this.lastAudiences = current2;
2447
+ const current3 = new Set(this.state.audienceIds);
2448
+ detail.newAudiences = new Set(Array.from(current3).filter((a) => !this.lastAudiences.has(a)));
2449
+ this.lastAudiences = current3;
1970
2450
  }
1971
2451
  const candidates = [];
1972
- for (const guide of this.config.guides) {
2452
+ for (const guide of this.candidates()) {
1973
2453
  const t = guide.config.trigger;
1974
2454
  if (detail.endedGuide && detail.endedGuide.id === guide.id) continue;
1975
2455
  if (reason === "ready" || reason === "navigation") {
@@ -1985,6 +2465,10 @@ var GuidelyClient = class _GuidelyClient {
1985
2465
  if (t.type === "page_load" && t.delayMs > 0 && reason === "timer" && detail.guideId !== guide.id) continue;
1986
2466
  if (!this.triggerFires(guide, reason, detail)) continue;
1987
2467
  if (!this.isEligible(guide, ctx, url)) continue;
2468
+ if (!this.throttleAllows(guide)) {
2469
+ this.log("throttled", guide.key);
2470
+ continue;
2471
+ }
1988
2472
  candidates.push(guide);
1989
2473
  }
1990
2474
  if (reason === "after_guide" || reason === "navigation") this.drainQueue(url, ctx);
@@ -2035,6 +2519,13 @@ var GuidelyClient = class _GuidelyClient {
2035
2519
  /** Decides whether a guide can show now, otherwise queues it. */
2036
2520
  tryShow(guide, url, manual) {
2037
2521
  if (this.destroyed) return false;
2522
+ if (!manual && !this.throttleAllows(guide)) return false;
2523
+ const holdout = this.holdoutFor(guide);
2524
+ if (holdout) {
2525
+ if (this.active && !manual) return false;
2526
+ this.recordImpression(guide, manual);
2527
+ return true;
2528
+ }
2038
2529
  if (!isExclusiveGuide(guide)) {
2039
2530
  if (this.widgets.has(guide.id)) return false;
2040
2531
  this.show(guide, manual);
@@ -2075,14 +2566,18 @@ var GuidelyClient = class _GuidelyClient {
2075
2566
  guideState(id) {
2076
2567
  return this.state.guides[id] ?? (this.state.guides[id] = { impressions: 0 });
2077
2568
  }
2078
- recordImpression(guide) {
2569
+ recordImpression(guide, manual = false) {
2079
2570
  const s = this.guideState(guide.id);
2080
2571
  s.impressions += 1;
2081
2572
  s.lastShownAt = nowIso();
2082
2573
  if (!this.session.shownGuides.includes(guide.id)) this.session.shownGuides.push(guide.id);
2574
+ this.recordLaunch(guide, manual);
2083
2575
  this.persistState();
2084
2576
  this.persistSession();
2085
- this.syncGuideState(guide.id, { impression: true });
2577
+ const holdout = this.holdoutFor(guide);
2578
+ const experiment = holdout ? { id: holdout.experimentId, variantId: holdout.variantId } : guide.experiment;
2579
+ if (experiment) this.enqueue("experiment_viewed", { properties: { guideExperimentId: experiment.id, variantId: experiment.variantId, ...holdout ? {} : { guideId: guide.id } } });
2580
+ if (!holdout) this.syncGuideState(guide.id, { impression: true });
2086
2581
  }
2087
2582
  recordEnd(guide, reason) {
2088
2583
  const s = this.guideState(guide.id);
@@ -2106,13 +2601,17 @@ var GuidelyClient = class _GuidelyClient {
2106
2601
  this.reportError(err);
2107
2602
  }
2108
2603
  }
2109
- showFlow(guide, manual, stepId) {
2604
+ showFlow(raw, manual, stepId) {
2110
2605
  if (this.editing()) return;
2606
+ const guide = this.localize(raw);
2111
2607
  const exclusive = isExclusiveGuide(guide);
2112
- const entry = { guide, manual, startUrl: location.href };
2608
+ const entry = { guide, manual, startUrl: this.pageHref() };
2113
2609
  const controller = runGuide2(guide, {
2114
2610
  theme: this.theme(),
2611
+ // Code blocks only arrive when a workspace admin allowed them (the server strips them otherwise).
2612
+ runCode: true,
2115
2613
  ctx: () => this.getContext(),
2614
+ currentUrl: () => this.pageHref(),
2116
2615
  startAtStep: stepId,
2117
2616
  eventBus: this.eventBus,
2118
2617
  // Started from Help, Scout or startGuide(): steps on other pages offer to take the user there.
@@ -2126,7 +2625,7 @@ var GuidelyClient = class _GuidelyClient {
2126
2625
  const step = e.stepId ? guide.config.steps.find((s) => s.id === e.stepId) : void 0;
2127
2626
  this.enqueue(e.name, { guideId: guide.id, guideVersion: guide.version, stepId: e.stepId, stepIndex: e.stepIndex, properties: e.properties });
2128
2627
  if (e.name === "guide_started") {
2129
- this.recordImpression(guide);
2628
+ this.recordImpression(guide, manual);
2130
2629
  this.emitter.emit("guide:start", guide);
2131
2630
  }
2132
2631
  if (e.name === "step_viewed" && step) {
@@ -2173,8 +2672,9 @@ var GuidelyClient = class _GuidelyClient {
2173
2672
  this.widgets.delete(guide.id);
2174
2673
  }
2175
2674
  }
2176
- showChecklist(guide) {
2675
+ showChecklist(raw) {
2177
2676
  if (this.editing()) return;
2677
+ const guide = this.localize(raw);
2178
2678
  const config = guide.config.checklist;
2179
2679
  if (!config || this.widgets.has(guide.id)) return;
2180
2680
  const scope = createScope(this.theme(), guide.config.appearance);
@@ -2195,12 +2695,13 @@ var GuidelyClient = class _GuidelyClient {
2195
2695
  }, checklist: widget, scope });
2196
2696
  if (first) this.recordImpression(guide);
2197
2697
  }
2198
- showSurvey(guide, manual) {
2698
+ showSurvey(raw, manual) {
2199
2699
  if (this.editing()) return;
2700
+ const guide = this.localize(raw);
2200
2701
  const config = guide.config.survey;
2201
2702
  if (!config) return;
2202
2703
  const scope = createScope(this.theme(), guide.config.appearance);
2203
- const entry = { guide, manual, startUrl: location.href, scope };
2704
+ const entry = { guide, manual, startUrl: this.pageHref(), scope };
2204
2705
  const finish = (reason) => {
2205
2706
  widget.destroy();
2206
2707
  scope.destroy();
@@ -2218,7 +2719,7 @@ var GuidelyClient = class _GuidelyClient {
2218
2719
  title: guide.name,
2219
2720
  onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
2220
2721
  onSubmit: (answers) => {
2221
- const responseId = randomId(24);
2722
+ const responseId = randomId2(24);
2222
2723
  this.state.surveysSubmitted.push(guide.id);
2223
2724
  this.persistState();
2224
2725
  this.recordEnd(guide, "completed");
@@ -2231,7 +2732,7 @@ var GuidelyClient = class _GuidelyClient {
2231
2732
  });
2232
2733
  entry.survey = widget;
2233
2734
  this.active = entry;
2234
- this.recordImpression(guide);
2735
+ this.recordImpression(guide, manual);
2235
2736
  this.enqueue("survey_displayed", { guideId: guide.id, guideVersion: guide.version });
2236
2737
  this.emitter.emit("guide:start", guide);
2237
2738
  }
@@ -2240,6 +2741,7 @@ var GuidelyClient = class _GuidelyClient {
2240
2741
  }
2241
2742
  navigate(url, newTab) {
2242
2743
  try {
2744
+ if (this.openPrototypeTest(url)) return;
2243
2745
  if (newTab) window.open(url, "_blank", "noopener");
2244
2746
  else location.assign(url);
2245
2747
  } catch (err) {
@@ -2264,7 +2766,13 @@ var GuidelyClient = class _GuidelyClient {
2264
2766
  this.resourceCenterScope = scope;
2265
2767
  this.resourceCenter = renderResourceCenter({
2266
2768
  scope: scope.el,
2267
- config: rc,
2769
+ config: localizeResourceCenter(rc, this.locales(), this.defaultLocale()),
2770
+ evaluate: (rules) => evaluateRules(rules, this.getContext()),
2771
+ announcementsSeenAt: () => this.state.announcementsSeenAt ?? null,
2772
+ onAnnouncementsSeen: () => {
2773
+ this.state.announcementsSeenAt = nowIso();
2774
+ this.persistState();
2775
+ },
2268
2776
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2269
2777
  search: (q) => this.searchResourceCenter(q),
2270
2778
  audienceIds: () => this.state.audienceIds,
@@ -2309,7 +2817,8 @@ var GuidelyClient = class _GuidelyClient {
2309
2817
  projectId: this.options.projectId,
2310
2818
  anonymousId: () => this.anonymousId,
2311
2819
  userId: () => this.user?.id || null,
2312
- pageUrl: () => this.collectUrls() ? location.href : void 0
2820
+ pageUrl: () => this.collectUrls() ? this.pageHref() : void 0,
2821
+ locale: () => this.rcLocale()
2313
2822
  }),
2314
2823
  visitor: () => this.messengerVisitor(),
2315
2824
  brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
@@ -2318,7 +2827,8 @@ var GuidelyClient = class _GuidelyClient {
2318
2827
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2319
2828
  writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2320
2829
  },
2321
- resourceCenter: this.config.resourceCenter,
2830
+ resourceCenter: this.config.resourceCenter ? localizeResourceCenter(this.config.resourceCenter, this.locales(), this.defaultLocale()) : null,
2831
+ evaluate: (rules) => evaluateRules(rules, this.getContext()),
2322
2832
  audienceIds: () => this.state.audienceIds,
2323
2833
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
2324
2834
  onStartGuide: (idOrKey) => this.startGuide(idOrKey),
@@ -2342,7 +2852,7 @@ var GuidelyClient = class _GuidelyClient {
2342
2852
  /** What the messenger personalises from, computed fresh on every render. */
2343
2853
  messengerVisitor() {
2344
2854
  const ctx = this.getContext();
2345
- const url = location.href;
2855
+ const url = this.pageHref();
2346
2856
  const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2347
2857
  const visitor = buildMessengerVisitor({
2348
2858
  guides: this.config?.guides ?? [],
@@ -2357,7 +2867,20 @@ var GuidelyClient = class _GuidelyClient {
2357
2867
  },
2358
2868
  newsSeen: Array.isArray(seen) ? seen : []
2359
2869
  });
2360
- return { ...visitor, identified: !!this.user?.id };
2870
+ const rc = this.config?.resourceCenter;
2871
+ const rcNews = rc?.announcements?.length ? visibleAnnouncements(localizeResourceCenter(rc, this.locales(), this.defaultLocale()).announcements, this.state.audienceIds).map((a) => ({
2872
+ id: `rc_${a.id}`,
2873
+ key: a.guideId ?? "",
2874
+ title: a.title,
2875
+ body: plainText(a.body),
2876
+ imageUrl: null,
2877
+ publishedAt: a.publishedAt,
2878
+ unread: !(Array.isArray(seen) && seen.includes(`rc_${a.id}`)),
2879
+ articleId: a.articleId ?? null,
2880
+ url: a.url ?? null
2881
+ })) : [];
2882
+ const news = rcNews.length ? [...visitor.news, ...rcNews].sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, 10) : visitor.news;
2883
+ return { ...visitor, news, identified: !!this.user?.id };
2361
2884
  }
2362
2885
  destroyMessenger() {
2363
2886
  this.messenger?.destroy();
@@ -2365,9 +2888,17 @@ var GuidelyClient = class _GuidelyClient {
2365
2888
  this.messenger = null;
2366
2889
  this.messengerScope = null;
2367
2890
  }
2891
+ /** The resource center's language for this visitor, or null for the default language. */
2892
+ rcLocale() {
2893
+ const rc = this.config?.resourceCenter;
2894
+ if (!rc) return null;
2895
+ const locale = localizeResourceCenter(rc, this.locales(), this.defaultLocale()).locale;
2896
+ return locale.toLowerCase() === this.defaultLocale().toLowerCase() ? null : locale;
2897
+ }
2368
2898
  async loadArticle(id) {
2369
2899
  try {
2370
- const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
2900
+ const locale = this.rcLocale();
2901
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`, locale ? { params: { locale } } : void 0);
2371
2902
  return res.ok && res.data ? res.data.article : null;
2372
2903
  } catch {
2373
2904
  return null;
@@ -2432,7 +2963,7 @@ var GuidelyClient = class _GuidelyClient {
2432
2963
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2433
2964
  else if (kind === "click") {
2434
2965
  try {
2435
- hit = !!closestComposed3(value, task.completionValue);
2966
+ hit = !!closestComposed4(value, task.completionValue);
2436
2967
  } catch {
2437
2968
  hit = false;
2438
2969
  }
@@ -2543,6 +3074,18 @@ var GuidelyClient = class _GuidelyClient {
2543
3074
  closeMessenger() {
2544
3075
  this.messenger?.close();
2545
3076
  }
3077
+ /**
3078
+ * Opens a Guidely prototype test (its `/validate/<token>` URL on this SDK's API origin, or the bare
3079
+ * token) in a full-screen overlay, attributed to the current user. Returns false, doing nothing, for
3080
+ * any other URL. Guide buttons that open a test URL use this automatically.
3081
+ */
3082
+ openPrototypeTest(urlOrToken) {
3083
+ if (typeof document === "undefined") return false;
3084
+ const url = matchPrototypeTestUrl(urlOrToken, this.options.apiUrl);
3085
+ if (!url) return false;
3086
+ openPrototypeOverlay(url, { key: this.options.projectId, anonymousId: this.anonymousId, userId: this.user?.id ?? null });
3087
+ return true;
3088
+ }
2546
3089
  /**
2547
3090
  * Opens the complaint form in the messenger, optionally with a category picked (its id from
2548
3091
  * Complaints → Categories). Does nothing when the project doesn't take complaints.
@@ -2551,6 +3094,14 @@ var GuidelyClient = class _GuidelyClient {
2551
3094
  if (!this.config?.messenger?.enabled || !this.config.messenger.complaints?.categories.length) return;
2552
3095
  void this.loadMessenger().then((m) => m?.openComplaint(categoryId));
2553
3096
  }
3097
+ /**
3098
+ * Opens the Ideas tab in the messenger (feature requests and voting), or one idea by its id.
3099
+ * Does nothing when the project hasn't turned on Ideas.
3100
+ */
3101
+ openIdeas(requestId) {
3102
+ if (!this.config?.messenger?.enabled || !this.config.messenger.ideas) return;
3103
+ void this.loadMessenger().then((m) => m?.openIdeas(requestId));
3104
+ }
2554
3105
  /** Opens a new messenger conversation, optionally pre-filling the message. */
2555
3106
  showNewMessage(text) {
2556
3107
  if (!this.config?.messenger?.enabled) return;
@@ -2580,14 +3131,15 @@ var GuidelyClient = class _GuidelyClient {
2580
3131
  const device = detectDevice(navigator.userAgent, window.innerWidth);
2581
3132
  const account = this.user?.account;
2582
3133
  return {
2583
- url: location.href,
2584
- path: location.pathname,
3134
+ url: this.pageHref(),
3135
+ path: pathOf2(this.pageHref()),
2585
3136
  deviceType: device.type,
2586
3137
  browser: device.browser,
2587
3138
  os: device.os,
2588
3139
  locale: navigator.language || void 0,
2589
3140
  environment: this.options.environment,
2590
3141
  platform: "web",
3142
+ ...this.options.appVersion ? { appVersion: this.options.appVersion } : {},
2591
3143
  ...this.user?.traits ?? {},
2592
3144
  id: this.user?.id || this.anonymousId,
2593
3145
  anonymousId: this.anonymousId,
@@ -2747,18 +3299,18 @@ var GuidelyClient = class _GuidelyClient {
2747
3299
  // ---------------------------------------------------------------------------
2748
3300
  /** Creates (or returns the existing) client and starts initialisation. */
2749
3301
  static init(options) {
2750
- if (current && !current.destroyed && current.options.projectId === options.projectId) return current;
2751
- current?.destroy();
2752
- current = new _GuidelyClient(options);
2753
- void current.init();
2754
- return current;
3302
+ if (current2 && !current2.destroyed && current2.options.projectId === options.projectId) return current2;
3303
+ current2?.destroy();
3304
+ current2 = new _GuidelyClient(options);
3305
+ void current2.init();
3306
+ return current2;
2755
3307
  }
2756
3308
  /** The most recently created client, if any. */
2757
3309
  static get() {
2758
- return current && !current.destroyed ? current : null;
3310
+ return current2 && !current2.destroyed ? current2 : null;
2759
3311
  }
2760
3312
  };
2761
- var current = null;
3313
+ var current2 = null;
2762
3314
  function webOnly(config) {
2763
3315
  return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
2764
3316
  }
@@ -2780,7 +3332,7 @@ function externalReferrer() {
2780
3332
  }
2781
3333
  function createGuidely(options) {
2782
3334
  const client = new GuidelyClient(options);
2783
- current = client;
3335
+ current2 = client;
2784
3336
  void client.init();
2785
3337
  return client;
2786
3338
  }
@@ -2802,18 +3354,25 @@ export {
2802
3354
  GuidelyClient,
2803
3355
  HEARTBEAT_MS,
2804
3356
  KEYS,
3357
+ PROTOTYPE_CLOSE_MESSAGE,
2805
3358
  SDK_NAME,
2806
3359
  SDK_VERSION,
2807
3360
  areMutuallyExclusive,
2808
3361
  buildContext,
2809
3362
  captureSnapshot,
3363
+ countsTowardThrottle,
2810
3364
  createGuidely,
2811
3365
  createStorage,
2812
3366
  index_default as default,
2813
3367
  detectDevice,
3368
+ isAppUrl,
2814
3369
  isExclusiveGuide,
2815
3370
  isFrequencyEligible,
3371
+ isThrottleEligible,
2816
3372
  isWithinSchedule,
3373
+ matchPrototypeTestUrl,
3374
+ normalizeAppUrl,
3375
+ pruneLaunches,
2817
3376
  watchNavigation
2818
3377
  };
2819
3378
  //# sourceMappingURL=index.js.map