@guidelytech/sdk-core 0.5.0 → 0.6.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/dist/index.js CHANGED
@@ -1,20 +1,22 @@
1
1
  import {
2
2
  Emitter,
3
3
  EventQueue,
4
- KEYS,
5
4
  areMutuallyExclusive,
6
- createStorage,
7
5
  isExclusiveGuide,
8
6
  isFrequencyEligible,
9
- isWithinSchedule,
7
+ isWithinSchedule
8
+ } from "./chunk-GDF6RCHQ.js";
9
+ import {
10
+ KEYS,
11
+ createStorage,
10
12
  readJson,
11
13
  writeJson
12
- } from "./chunk-HLR3W63Z.js";
14
+ } from "./chunk-CYAG6ZYX.js";
13
15
  import {
14
16
  Api,
15
17
  __publicField,
16
18
  postWithRetry
17
- } from "./chunk-GJT7CFAC.js";
19
+ } from "./chunk-AKNE7D5M.js";
18
20
 
19
21
  // src/client.ts
20
22
  import { evaluateRules, randomId, rolloutBucket, sanitizeProperties, supportsPlatform } from "@guidelytech/shared/runtime";
@@ -25,7 +27,7 @@ import {
25
27
  renderResourceCenter,
26
28
  renderSurvey,
27
29
  runGuide as runGuide2,
28
- urlMatches,
30
+ urlMatches as urlMatches2,
29
31
  waitForTarget
30
32
  } from "@guidelytech/guide-runtime";
31
33
  import { createScope } from "@guidelytech/guide-runtime";
@@ -338,6 +340,43 @@ var AuthoringBridge = class {
338
340
  }
339
341
  };
340
342
 
343
+ // src/capture-token.ts
344
+ import { CAPTURE_PARAM } from "@guidelytech/shared/runtime";
345
+ function takeCaptureToken(sessionStore) {
346
+ if (typeof window === "undefined") return null;
347
+ let token = null;
348
+ let authoringLink = false;
349
+ try {
350
+ const url = new URL(window.location.href);
351
+ token = url.searchParams.get(CAPTURE_PARAM);
352
+ authoringLink = url.searchParams.has(AUTHORING_PARAM);
353
+ if (token) {
354
+ url.searchParams.delete(CAPTURE_PARAM);
355
+ window.history.replaceState(window.history.state, "", url.toString());
356
+ }
357
+ } catch {
358
+ token = null;
359
+ }
360
+ const saved = readJson(sessionStore, KEYS.capture, null);
361
+ let marker = null;
362
+ if (token) marker = saved?.token === token ? saved : { token };
363
+ else if (saved?.token && !authoringLink) marker = saved;
364
+ if (!marker) {
365
+ if (saved) {
366
+ sessionStore.remove(KEYS.capture);
367
+ sessionStore.remove(KEYS.capturePending);
368
+ }
369
+ return null;
370
+ }
371
+ if (token && saved?.token !== token) sessionStore.remove(KEYS.capturePending);
372
+ writeJson(sessionStore, KEYS.capture, marker);
373
+ sessionStore.remove(KEYS.authoring);
374
+ return marker;
375
+ }
376
+
377
+ // src/load-capture.ts
378
+ var loadCapture = async () => (await import("./capture.js")).capture;
379
+
341
380
  // src/autocapture.ts
342
381
  import { isGuidelyElement, isStableId } from "@guidelytech/guide-runtime";
343
382
  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]";
@@ -673,6 +712,15 @@ var ExceptionThrottle = class {
673
712
  // src/messenger-api.ts
674
713
  var MAX_STORED = 50;
675
714
  var TOKEN_HEADER = "X-Guidely-Conversation";
715
+ function pagePath(url) {
716
+ if (!url) return void 0;
717
+ try {
718
+ const u = new URL(url);
719
+ return (u.pathname + (/^#!?\//.test(u.hash) ? u.hash.split("?")[0] : "")).slice(0, 2e3);
720
+ } catch {
721
+ return void 0;
722
+ }
723
+ }
676
724
  function storedConversations(store, projectId) {
677
725
  const list = readJson(store, KEYS.conversations(projectId), []);
678
726
  return Array.isArray(list) ? list.filter((c) => c && typeof c.id === "string" && typeof c.token === "string") : [];
@@ -747,7 +795,7 @@ function createMessengerTransport(opts) {
747
795
  return exchange("/conversations", { anonymousId: opts.anonymousId(), userId: opts.userId(), message: text, pageUrl: opts.pageUrl() }, {}, h, (r) => remember(r.conversation.id, r.token));
748
796
  },
749
797
  send(id, text, h) {
750
- return exchange(`/conversations/${encodeURIComponent(id)}/messages`, { message: text }, { [TOKEN_HEADER]: tokenFor(id) }, h);
798
+ return exchange(`/conversations/${encodeURIComponent(id)}/messages`, { message: text, pageUrl: opts.pageUrl() }, { [TOKEN_HEADER]: tokenFor(id) }, h);
751
799
  },
752
800
  async handoff(id) {
753
801
  const res = await api.post(`/conversations/${encodeURIComponent(id)}/handoff`, {}, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
@@ -767,13 +815,98 @@ function createMessengerTransport(opts) {
767
815
  const res = await api.get(`/articles/${encodeURIComponent(id)}`);
768
816
  return res.ok && res.data ? res.data.article : null;
769
817
  },
818
+ async pageContent(path) {
819
+ const res = await api.post("/messenger/page", { anonymousId: opts.anonymousId(), userId: opts.userId(), path: path.split(/[?#]/)[0] || "/" });
820
+ if (!res.ok || !res.data) throw new HttpError(res.status);
821
+ return res.data;
822
+ },
770
823
  async searchArticles(q) {
771
824
  const res = await api.get("/resource-center/search", { params: { q } });
772
825
  return res.ok && res.data ? res.data.articles : [];
826
+ },
827
+ async listHowTos() {
828
+ const res = await api.get("/how-tos", { params: { url: pagePath(opts.pageUrl()) } });
829
+ if (!res.ok || !res.data) throw new HttpError(res.status);
830
+ return res.data;
831
+ },
832
+ async getHowTo(id) {
833
+ const res = await api.get(`/how-tos/${encodeURIComponent(id)}`);
834
+ return res.ok && res.data ? res.data.howTo : null;
773
835
  }
774
836
  };
775
837
  }
776
838
 
839
+ // src/messenger-visitor.ts
840
+ import { firstNameFromTraits } from "@guidelytech/shared/runtime";
841
+ import { urlMatches } from "@guidelytech/guide-runtime";
842
+ var PAGE_GUIDE_KINDS = /* @__PURE__ */ new Set(["flow", "tooltip", "modal"]);
843
+ var CATCH_ALL = /* @__PURE__ */ new Set(["", "*", "**", "/*", "/**", "/"]);
844
+ var RESUME_WITHIN_MS = 14 * 864e5;
845
+ var NEWS_WITHIN_MS = 90 * 864e5;
846
+ var MAX_NEWS = 10;
847
+ function pathOf(url) {
848
+ try {
849
+ return new URL(url, "http://x.invalid").pathname || "/";
850
+ } catch {
851
+ return null;
852
+ }
853
+ }
854
+ function targetsPage(guide, url) {
855
+ const c = guide.config;
856
+ const checks = [...c.urlPatterns, ...c.trigger.urlPattern ? [c.trigger.urlPattern] : [], ...c.trigger.type === "url_match" && c.trigger.value ? [c.trigger.value] : [], ...c.steps[0]?.url ? [c.steps[0].url] : []];
857
+ const specific = checks.filter((p) => !CATCH_ALL.has(p.trim()));
858
+ return specific.length > 0 && checks.every((p) => urlMatches(p, url));
859
+ }
860
+ function plainText(html, max = 280) {
861
+ const text = html.replace(/<(br|\/p|\/div|\/li|\/h[1-6])\s*\/?>/gi, " ").replace(/<[^>]*>/g, "").replace(/&nbsp;/g, " ").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&amp;/g, "&").replace(/\s+/g, " ").trim();
862
+ return text.length > max ? `${text.slice(0, max - 1).trimEnd()}\u2026` : text;
863
+ }
864
+ function buildMessengerVisitor(input) {
865
+ const now = input.now ?? Date.now();
866
+ const { guides, state } = input;
867
+ const done = (g) => !!state.guides[g.id]?.completedAt;
868
+ const pageGuides = guides.filter((g) => PAGE_GUIDE_KINDS.has(g.kind) && g.config.steps.length > 0 && !done(g) && targetsPage(g, input.url) && input.isTargeted(g)).slice(0, 3).map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind }));
869
+ let resumeGuide = null;
870
+ let resumeAt = 0;
871
+ for (const g of guides) {
872
+ const s = state.guides[g.id];
873
+ if (g.kind !== "flow" || !s?.lastStepId || s.completedAt || g.config.steps.length < 2) continue;
874
+ const at = Date.parse(s.lastShownAt ?? "");
875
+ if (!Number.isFinite(at) || now - at > RESUME_WITHIN_MS || at <= resumeAt) continue;
876
+ const stepIndex = g.config.steps.findIndex((st) => st.id === s.lastStepId);
877
+ if (stepIndex < 1 || !isWithinSchedule(g)) continue;
878
+ resumeGuide = { id: g.id, name: g.name, stepId: s.lastStepId, stepIndex, stepCount: g.config.steps.length };
879
+ resumeAt = at;
880
+ }
881
+ let checklist = null;
882
+ for (const g of guides) {
883
+ const tasks = g.config.checklist?.tasks;
884
+ if (g.kind !== "checklist" || !tasks?.length) continue;
885
+ const p = state.checklists[g.id];
886
+ if (p?.completedAt || p?.dismissedAt || !input.isAudienceTargeted(g)) continue;
887
+ const completed = new Set(p?.completedTaskIds ?? []);
888
+ const doneCount = tasks.filter((t) => completed.has(t.id)).length;
889
+ if (doneCount >= tasks.length) continue;
890
+ checklist = { id: g.id, name: g.name, done: doneCount, total: tasks.length, nextTask: tasks.find((t) => !completed.has(t.id))?.title ?? null };
891
+ break;
892
+ }
893
+ const seen = new Set(input.newsSeen);
894
+ const news = guides.filter((g) => g.kind === "announcement" && g.config.steps.length > 0 && now - Date.parse(g.publishedAt) <= NEWS_WITHIN_MS && input.isAudienceTargeted(g)).sort((a, b) => b.publishedAt.localeCompare(a.publishedAt)).slice(0, MAX_NEWS).map((g) => {
895
+ const step = g.config.steps[0];
896
+ return {
897
+ id: g.id,
898
+ key: g.key,
899
+ title: step.title || g.name,
900
+ body: plainText(step.body ?? ""),
901
+ imageUrl: step.imageUrl || null,
902
+ publishedAt: g.publishedAt,
903
+ // Read once opened in the News tab, or once the announcement itself was shown.
904
+ unread: !seen.has(g.id) && !(state.guides[g.id]?.impressions ?? 0)
905
+ };
906
+ });
907
+ return { firstName: firstNameFromTraits(input.traits), path: pathOf(input.url), pageGuides, resumeGuide, checklist, news };
908
+ }
909
+
777
910
  // src/navigation.ts
778
911
  function watchNavigation(listener, debounceMs = 50) {
779
912
  if (typeof window === "undefined") return () => {
@@ -879,7 +1012,7 @@ var loadRecorder = async () => (await import("./recorder.js")).recorder;
879
1012
 
880
1013
  // src/client.ts
881
1014
  var SDK_NAME = "@guidelytech/sdk-core";
882
- var SDK_VERSION = "0.5.0";
1015
+ var SDK_VERSION = "0.6.1";
883
1016
  var DEFAULT_API_URL = "https://api.guidely.app";
884
1017
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
885
1018
  var DAY_MS = 24 * 60 * 60 * 1e3;
@@ -930,6 +1063,9 @@ var GuidelyClient = class _GuidelyClient {
930
1063
  __publicField(this, "firstVisit");
931
1064
  __publicField(this, "lastUrl");
932
1065
  __publicField(this, "authoring", null);
1066
+ __publicField(this, "capture", null);
1067
+ /** A capture token was found and the (lazily loaded) capture bridge is starting. */
1068
+ __publicField(this, "captureStarting", false);
933
1069
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
934
1070
  /** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
935
1071
  __publicField(this, "identitySettled", false);
@@ -1038,10 +1174,11 @@ var GuidelyClient = class _GuidelyClient {
1038
1174
  this.isReady = true;
1039
1175
  if (this.config) this.emitter.emit("ready", this.config);
1040
1176
  this.resolveReady?.();
1177
+ const capturing = !this.options.disableAuthoring && this.maybeStartCapture();
1041
1178
  this.renderPersistentWidgets();
1042
1179
  this.resumeActiveGuide();
1043
1180
  this.evaluate("ready");
1044
- if (!this.options.disableAuthoring) this.maybeStartAuthoring();
1181
+ if (!this.options.disableAuthoring && !capturing) this.maybeStartAuthoring();
1045
1182
  this.pageTracker.start(location.href);
1046
1183
  this.touchSession();
1047
1184
  this.syncCapture();
@@ -1139,7 +1276,7 @@ var GuidelyClient = class _GuidelyClient {
1139
1276
  const refresh = setInterval(() => void this.refresh(), this.options.configRefreshMs);
1140
1277
  this.cleanups.push(() => clearInterval(refresh));
1141
1278
  const poll = setInterval(() => {
1142
- if (!this.isReady || !this.config || this.authoring?.isActive() || doc.visibilityState === "hidden") return;
1279
+ if (!this.isReady || !this.config || this.editing() || doc.visibilityState === "hidden") return;
1143
1280
  const url = location.href;
1144
1281
  const ctx = this.getContext();
1145
1282
  for (const guide of this.config.guides) {
@@ -1197,6 +1334,8 @@ var GuidelyClient = class _GuidelyClient {
1197
1334
  this.cleanups.splice(0).forEach((fn) => fn());
1198
1335
  this.authoring?.stop();
1199
1336
  this.authoring = null;
1337
+ this.capture?.destroy();
1338
+ this.capture = null;
1200
1339
  this.replay?.destroy();
1201
1340
  this.replay = null;
1202
1341
  this.queue.flushSync();
@@ -1229,6 +1368,7 @@ var GuidelyClient = class _GuidelyClient {
1229
1368
  for (const [id, s] of Object.entries(data.guideState || {})) {
1230
1369
  const local = guides[id];
1231
1370
  guides[id] = {
1371
+ ...local,
1232
1372
  impressions: Math.max(local?.impressions ?? 0, s.impressions ?? 0),
1233
1373
  completedAt: local?.completedAt ?? s.completedAt ?? null,
1234
1374
  dismissedAt: local?.dismissedAt ?? s.dismissedAt ?? null,
@@ -1257,6 +1397,7 @@ var GuidelyClient = class _GuidelyClient {
1257
1397
  const g = this.findGuide(id);
1258
1398
  if (w.checklist && g) w.checklist.setProgress(this.getChecklistProgress(g.id));
1259
1399
  });
1400
+ this.messenger?.refresh();
1260
1401
  }
1261
1402
  /** Sets or replaces the account (company/organisation) for the current user. */
1262
1403
  async setAccount(account) {
@@ -1378,16 +1519,17 @@ var GuidelyClient = class _GuidelyClient {
1378
1519
  const st = this.active.controller.getState();
1379
1520
  if (st.status === "running" && !this.active.manual) {
1380
1521
  const g = this.active.guide;
1381
- if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches(p, url))) this.active.controller.destroy();
1522
+ if (g.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches2(p, url))) this.active.controller.destroy();
1382
1523
  }
1383
1524
  }
1384
1525
  this.widgets.forEach((w, id) => {
1385
1526
  if (!w.controller || w.manual) return;
1386
1527
  const g = this.findGuide(id);
1387
- if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches(p, url))) w.controller.destroy();
1528
+ if (g?.config.urlPatterns.length && !g.config.urlPatterns.some((p) => urlMatches2(p, url))) w.controller.destroy();
1388
1529
  });
1389
1530
  this.firedDelays.clear();
1390
1531
  if (this.isReady) this.evaluate("navigation");
1532
+ if (changed) this.messenger?.refresh();
1391
1533
  if (changed) void this.queue.flush();
1392
1534
  }
1393
1535
  handleDocumentClick(e) {
@@ -1418,9 +1560,13 @@ var GuidelyClient = class _GuidelyClient {
1418
1560
  if (this.replay?.activeSessionId) this.replay.stop();
1419
1561
  this.syncReplay();
1420
1562
  }
1421
- /** Autocapture, error tracking and replay need analytics on, consent granted and no authoring session. */
1563
+ /** Autocapture, error tracking and replay need analytics on, consent granted and no authoring or how-to capture session. */
1422
1564
  captureAllowed() {
1423
- return !this.destroyed && this.analyticsEnabled() && !this.authoring?.isActive();
1565
+ return !this.destroyed && this.analyticsEnabled() && !this.editing();
1566
+ }
1567
+ /** Authoring or how-to capture is running: no guides, widgets, replay or autocapture. */
1568
+ editing() {
1569
+ return !!this.authoring?.isActive() || this.captureStarting || !!this.capture?.isActive();
1424
1570
  }
1425
1571
  /** A capability is on when the project enables it (absent on old servers → off) and no option turns it off. */
1426
1572
  autocaptureOn(key) {
@@ -1430,7 +1576,7 @@ var GuidelyClient = class _GuidelyClient {
1430
1576
  /** Page views on SPA navigation predate autocapture: only an explicit opt-out stops them. */
1431
1577
  pageviewsOn() {
1432
1578
  const o = this.options.autocapture;
1433
- return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o.pageviews === false) && !this.authoring?.isActive();
1579
+ return this.config?.settings?.autocapture?.pageviews !== false && o !== false && !(typeof o === "object" && o.pageviews === false) && !this.editing();
1434
1580
  }
1435
1581
  trackPageView(url) {
1436
1582
  if (url === this.pageViewUrl || !this.pageviewsOn()) return;
@@ -1440,7 +1586,7 @@ var GuidelyClient = class _GuidelyClient {
1440
1586
  const device = detectDevice(navigator.userAgent, window.innerWidth);
1441
1587
  const props = { deviceType: device.type, browser: device.browser, os: device.os, screenWidth: window.screen?.width, viewportWidth: window.innerWidth };
1442
1588
  if (collect) {
1443
- props.path = pathOf(url);
1589
+ props.path = pathOf2(url);
1444
1590
  props.title = (document.title || "").slice(0, 200);
1445
1591
  const ref = !this.session.pageViews ? externalReferrer() : void 0;
1446
1592
  if (ref) props.referrer = ref;
@@ -1457,7 +1603,7 @@ var GuidelyClient = class _GuidelyClient {
1457
1603
  if (!this.pageLeft && this.autocaptureOn("pageLeave")) {
1458
1604
  const info = t.leave();
1459
1605
  const collect = this.collectUrls();
1460
- this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
1606
+ this.enqueue("page_leave", { url: collect ? info.url : void 0, properties: { ...collect ? { path: pathOf2(info.url) } : {}, durationMs: info.durationMs, scrollDepth: info.scrollDepth } });
1461
1607
  }
1462
1608
  if (nextUrl !== null) {
1463
1609
  t.start(nextUrl);
@@ -1494,7 +1640,7 @@ var GuidelyClient = class _GuidelyClient {
1494
1640
  if (!batch || !this.captureAllowed()) return;
1495
1641
  const collect = this.collectUrls();
1496
1642
  const de = document.documentElement;
1497
- const properties = { path: collect ? pathOf(batch.url) : void 0, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, pageHeight: Math.max(de.scrollHeight, document.body?.scrollHeight || 0), points: batch.points };
1643
+ const properties = { path: collect ? pathOf2(batch.url) : void 0, viewportWidth: window.innerWidth, viewportHeight: window.innerHeight, pageHeight: Math.max(de.scrollHeight, document.body?.scrollHeight || 0), points: batch.points };
1498
1644
  if (!collect) delete properties.path;
1499
1645
  this.enqueue("heatmap", { url: collect ? batch.url : void 0, properties });
1500
1646
  }
@@ -1668,8 +1814,8 @@ var GuidelyClient = class _GuidelyClient {
1668
1814
  }
1669
1815
  isTargeted(guide, ctx, url) {
1670
1816
  const c = guide.config;
1671
- if (c.urlPatterns.length && !c.urlPatterns.some((p) => urlMatches(p, url))) return false;
1672
- if (c.trigger.urlPattern && !urlMatches(c.trigger.urlPattern, url)) return false;
1817
+ if (c.urlPatterns.length && !c.urlPatterns.some((p) => urlMatches2(p, url))) return false;
1818
+ if (c.trigger.urlPattern && !urlMatches2(c.trigger.urlPattern, url)) return false;
1673
1819
  if (!isWithinSchedule(guide)) return false;
1674
1820
  if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
1675
1821
  ctx.rolloutSalt = guide.id;
@@ -1691,7 +1837,7 @@ var GuidelyClient = class _GuidelyClient {
1691
1837
  if (t.delayMs > 0) return reason === "timer" && detail.guideId === guide.id;
1692
1838
  return reason === "ready" || reason === "navigation" || reason === "identify";
1693
1839
  case "url_match":
1694
- return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches(t.value, location.href);
1840
+ return (reason === "ready" || reason === "navigation" || reason === "identify") && urlMatches2(t.value, location.href);
1695
1841
  case "event":
1696
1842
  return reason === "event" && !!t.value && detail.eventName === t.value;
1697
1843
  case "element_click":
@@ -1715,7 +1861,7 @@ var GuidelyClient = class _GuidelyClient {
1715
1861
  }
1716
1862
  }
1717
1863
  evaluate(reason, detail = {}) {
1718
- if (!this.config || this.destroyed || this.authoring?.isActive()) return;
1864
+ if (!this.config || this.destroyed || this.editing()) return;
1719
1865
  const url = location.href;
1720
1866
  const ctx = this.getContext();
1721
1867
  if (reason === "identify") {
@@ -1860,7 +2006,7 @@ var GuidelyClient = class _GuidelyClient {
1860
2006
  }
1861
2007
  }
1862
2008
  showFlow(guide, manual, stepId) {
1863
- if (this.authoring?.isActive()) return;
2009
+ if (this.editing()) return;
1864
2010
  const exclusive = isExclusiveGuide(guide);
1865
2011
  const entry = { guide, manual, startUrl: location.href };
1866
2012
  const controller = runGuide2(guide, {
@@ -1878,6 +2024,10 @@ var GuidelyClient = class _GuidelyClient {
1878
2024
  }
1879
2025
  if (e.name === "step_viewed" && step) {
1880
2026
  if (exclusive) writeJson(this.sessionStore, KEYS.active, { guideId: guide.id, stepId: step.id, manual });
2027
+ if (guide.kind === "flow" && guide.config.steps.length > 1) {
2028
+ this.guideState(guide.id).lastStepId = step.id;
2029
+ this.persistState();
2030
+ }
1881
2031
  this.emitter.emit("guide:step", { guide, stepId: step.id, stepIndex: e.stepIndex ?? 0 });
1882
2032
  }
1883
2033
  if (e.name === "guide_completed") {
@@ -1917,7 +2067,7 @@ var GuidelyClient = class _GuidelyClient {
1917
2067
  }
1918
2068
  }
1919
2069
  showChecklist(guide) {
1920
- if (this.authoring?.isActive()) return;
2070
+ if (this.editing()) return;
1921
2071
  const config = guide.config.checklist;
1922
2072
  if (!config || this.widgets.has(guide.id)) return;
1923
2073
  const scope = createScope(this.theme(), guide.config.appearance);
@@ -1939,7 +2089,7 @@ var GuidelyClient = class _GuidelyClient {
1939
2089
  if (first) this.recordImpression(guide);
1940
2090
  }
1941
2091
  showSurvey(guide, manual) {
1942
- if (this.authoring?.isActive()) return;
2092
+ if (this.editing()) return;
1943
2093
  const config = guide.config.survey;
1944
2094
  if (!config) return;
1945
2095
  const scope = createScope(this.theme(), guide.config.appearance);
@@ -1990,7 +2140,7 @@ var GuidelyClient = class _GuidelyClient {
1990
2140
  }
1991
2141
  }
1992
2142
  renderPersistentWidgets() {
1993
- if (!this.config) return;
2143
+ if (!this.config || this.captureStarting || this.capture?.isActive()) return;
1994
2144
  const rc = this.config.resourceCenter;
1995
2145
  if (this.config.messenger?.enabled) {
1996
2146
  if (this.resourceCenter) {
@@ -2054,6 +2204,13 @@ var GuidelyClient = class _GuidelyClient {
2054
2204
  userId: () => this.user?.id || null,
2055
2205
  pageUrl: () => this.collectUrls() ? location.href : void 0
2056
2206
  }),
2207
+ visitor: () => this.messengerVisitor(),
2208
+ brand: { logoUrl: this.theme().logoUrl ?? null, name: this.theme().brandName ?? null },
2209
+ onResumeGuide: (guideId, stepId) => this.startGuide(guideId, { stepId }),
2210
+ onNewsSeen: (ids) => {
2211
+ const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2212
+ writeJson(this.store, KEYS.newsSeen(this.options.projectId), Array.from(/* @__PURE__ */ new Set([...ids, ...Array.isArray(seen) ? seen : []])).slice(0, 200));
2213
+ },
2057
2214
  resourceCenter: this.config.resourceCenter,
2058
2215
  audienceIds: () => this.state.audienceIds,
2059
2216
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
@@ -2075,6 +2232,25 @@ var GuidelyClient = class _GuidelyClient {
2075
2232
  });
2076
2233
  return this.messengerLoading;
2077
2234
  }
2235
+ /** What the messenger personalises from, computed fresh on every render. */
2236
+ messengerVisitor() {
2237
+ const ctx = this.getContext();
2238
+ const url = location.href;
2239
+ const seen = readJson(this.store, KEYS.newsSeen(this.options.projectId), []);
2240
+ return buildMessengerVisitor({
2241
+ guides: this.config?.guides ?? [],
2242
+ state: this.state,
2243
+ traits: this.user?.traits,
2244
+ url,
2245
+ isTargeted: (g) => this.isTargeted(g, ctx, url),
2246
+ isAudienceTargeted: (g) => {
2247
+ if (!isWithinSchedule(g)) return false;
2248
+ ctx.rolloutSalt = g.id;
2249
+ return evaluateRules(g.config.targeting, ctx);
2250
+ },
2251
+ newsSeen: Array.isArray(seen) ? seen : []
2252
+ });
2253
+ }
2078
2254
  destroyMessenger() {
2079
2255
  this.messenger?.destroy();
2080
2256
  this.messengerScope?.destroy();
@@ -2144,7 +2320,7 @@ var GuidelyClient = class _GuidelyClient {
2144
2320
  if (task.completion !== kind || !task.completionValue) continue;
2145
2321
  let hit = false;
2146
2322
  if (kind === "event") hit = value === task.completionValue;
2147
- else if (kind === "url_visited") hit = urlMatches(task.completionValue, value);
2323
+ else if (kind === "url_visited") hit = urlMatches2(task.completionValue, value);
2148
2324
  else if (kind === "guide_completed") hit = value.id === task.completionValue || value.key === task.completionValue;
2149
2325
  else if (kind === "click") {
2150
2326
  try {
@@ -2288,7 +2464,8 @@ var GuidelyClient = class _GuidelyClient {
2288
2464
  audienceIds: this.state.audienceIds,
2289
2465
  session: this.session,
2290
2466
  pendingEvents: this.queue.size,
2291
- authoring: !!this.authoring?.isActive()
2467
+ authoring: !!this.authoring?.isActive(),
2468
+ capture: !!this.capture?.isActive()
2292
2469
  };
2293
2470
  }
2294
2471
  // ---------------------------------------------------------------------------
@@ -2303,22 +2480,78 @@ var GuidelyClient = class _GuidelyClient {
2303
2480
  ctx: () => this.getContext(),
2304
2481
  maskSelectors: [...this.options.maskSelectors || [], ...this.config?.settings?.maskSelectors || []],
2305
2482
  onError: (err) => this.reportError(err),
2306
- onActive: (active) => {
2307
- this.emitter.emit("authoring", { active });
2308
- if (active) this.replay?.stop({ discard: true });
2309
- else this.syncCapture();
2310
- if (active) {
2311
- this.stopGuide();
2312
- this.widgets.forEach((w) => w.destroy());
2313
- this.widgets.clear();
2314
- } else this.evaluate("ready");
2315
- }
2483
+ onActive: (active) => this.setEditing("authoring", active)
2316
2484
  });
2317
2485
  if (bridge.detect()) this.authoring = bridge;
2318
2486
  } catch (err) {
2319
2487
  this.reportError(err);
2320
2488
  }
2321
2489
  }
2490
+ /** Authoring or capture started / ended: pause everything a visitor would see or be recorded by. */
2491
+ setEditing(kind, active) {
2492
+ this.emitter.emit(kind, { active });
2493
+ if (active) {
2494
+ this.replay?.stop({ discard: true });
2495
+ this.stopGuide();
2496
+ this.widgets.forEach((w) => w.destroy());
2497
+ this.widgets.clear();
2498
+ if (kind === "capture") this.hidePersistentWidgets();
2499
+ } else {
2500
+ this.syncCapture();
2501
+ this.renderPersistentWidgets();
2502
+ this.evaluate("ready");
2503
+ }
2504
+ }
2505
+ hidePersistentWidgets() {
2506
+ this.resourceCenter?.destroy();
2507
+ this.resourceCenterScope?.destroy();
2508
+ this.resourceCenter = null;
2509
+ this.resourceCenterScope = null;
2510
+ this.destroyMessenger();
2511
+ }
2512
+ // ---------------------------------------------------------------------------
2513
+ // How-to capture
2514
+ // ---------------------------------------------------------------------------
2515
+ /**
2516
+ * Starts how-to capture when the URL carries `guidely_capture` (or this tab is mid-recording).
2517
+ * The bridge, toolbar and screenshot code are a separate chunk loaded only then.
2518
+ */
2519
+ maybeStartCapture() {
2520
+ let marker = null;
2521
+ try {
2522
+ marker = takeCaptureToken(this.sessionStore);
2523
+ } catch (err) {
2524
+ this.reportError(err);
2525
+ }
2526
+ if (!marker) return false;
2527
+ const found = marker;
2528
+ this.captureStarting = true;
2529
+ this.replay?.stop({ discard: true });
2530
+ loadCapture().then(async (capture) => {
2531
+ if (this.destroyed) return;
2532
+ const bridge = capture.createBridge({
2533
+ api: this.api,
2534
+ sessionStore: this.sessionStore,
2535
+ marker: found,
2536
+ maskSelector: this.maskSelector(),
2537
+ onError: (err) => this.reportError(err),
2538
+ onActive: (active) => {
2539
+ this.captureStarting = false;
2540
+ this.setEditing("capture", active);
2541
+ }
2542
+ });
2543
+ this.capture = bridge;
2544
+ await bridge.start();
2545
+ }).catch((err) => this.reportError(err)).finally(() => {
2546
+ if (!this.captureStarting) return;
2547
+ this.captureStarting = false;
2548
+ if (this.destroyed) return;
2549
+ this.syncCapture();
2550
+ this.renderPersistentWidgets();
2551
+ this.evaluate("ready");
2552
+ });
2553
+ return true;
2554
+ }
2322
2555
  // ---------------------------------------------------------------------------
2323
2556
  // Internals
2324
2557
  // ---------------------------------------------------------------------------
@@ -2360,7 +2593,7 @@ var current = null;
2360
2593
  function webOnly(config) {
2361
2594
  return config.guides.every((g) => supportsPlatform(g.config, "web")) ? config : { ...config, guides: config.guides.filter((g) => supportsPlatform(g.config, "web")) };
2362
2595
  }
2363
- function pathOf(url) {
2596
+ function pathOf2(url) {
2364
2597
  try {
2365
2598
  return new URL(url, "http://localhost").pathname;
2366
2599
  } catch {
@@ -2393,6 +2626,7 @@ var Guidely = {
2393
2626
  var index_default = Guidely;
2394
2627
  export {
2395
2628
  AUTHORING_PARAM,
2629
+ CAPTURE_PARAM,
2396
2630
  DEFAULT_API_URL,
2397
2631
  EventQueue,
2398
2632
  Guidely,