@guidelytech/sdk-core 0.1.0 → 0.2.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
@@ -19,6 +19,82 @@ import { createScope } from "@guidelytech/guide-runtime";
19
19
  // src/authoring.ts
20
20
  import { highlightSelectors, previewStep, runGuide, startAuthoring } from "@guidelytech/guide-runtime";
21
21
 
22
+ // src/snapshot.ts
23
+ var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
24
+ var STRIP = "script, noscript, template, #guidely-root, [data-guidely], meta[http-equiv], link[rel~='preload'], link[rel~='modulepreload'], link[rel~='prefetch'], link[rel~='manifest'], link[rel~='serviceworker']";
25
+ function rulesText(sheet) {
26
+ if (!sheet) return null;
27
+ try {
28
+ return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function maskText(root) {
34
+ const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
35
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
36
+ n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
37
+ }
38
+ }
39
+ function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
40
+ const win = doc.defaultView;
41
+ if (!win || !doc.documentElement) return null;
42
+ const clone = doc.documentElement.cloneNode(true);
43
+ const origStyles = doc.documentElement.querySelectorAll("style");
44
+ clone.querySelectorAll("style").forEach((el, i) => {
45
+ const css = rulesText(origStyles[i]?.sheet);
46
+ if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
47
+ });
48
+ const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
49
+ clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
50
+ const r = origFrames[i]?.getBoundingClientRect();
51
+ const box = doc.createElement("div");
52
+ box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
53
+ if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
54
+ el.replaceWith(box);
55
+ });
56
+ clone.querySelectorAll(STRIP).forEach((el) => el.remove());
57
+ for (const sel of maskSelectors) {
58
+ try {
59
+ clone.querySelectorAll(sel).forEach(maskText);
60
+ } catch {
61
+ }
62
+ }
63
+ clone.querySelectorAll("*").forEach((el) => {
64
+ for (const attr of Array.from(el.attributes)) {
65
+ const name = attr.name.toLowerCase();
66
+ if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr.value)) el.removeAttribute(attr.name);
67
+ }
68
+ if (el instanceof HTMLInputElement) {
69
+ el.removeAttribute("value");
70
+ el.removeAttribute("checked");
71
+ } else if (el instanceof HTMLTextAreaElement) {
72
+ el.textContent = "";
73
+ }
74
+ el.removeAttribute("autofocus");
75
+ });
76
+ const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
77
+ let head = clone.querySelector("head");
78
+ if (!head) {
79
+ head = doc.createElement("head");
80
+ clone.prepend(head);
81
+ }
82
+ if (adopted) {
83
+ const style = doc.createElement("style");
84
+ style.textContent = adopted;
85
+ head.append(style);
86
+ }
87
+ clone.querySelectorAll("base").forEach((el) => el.remove());
88
+ const base = doc.createElement("base");
89
+ base.setAttribute("href", doc.baseURI || win.location.href);
90
+ base.setAttribute("target", "_blank");
91
+ head.prepend(base);
92
+ const html = `<!DOCTYPE html>
93
+ ${clone.outerHTML}`;
94
+ if (html.length > maxBytes) return null;
95
+ return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
96
+ }
97
+
22
98
  // src/storage.ts
23
99
  function memoryStore() {
24
100
  const map = /* @__PURE__ */ new Map();
@@ -86,7 +162,9 @@ var KEYS = {
86
162
  queue: (key) => `guidely:queue:${key}`,
87
163
  session: "guidely:session",
88
164
  active: "guidely:active",
89
- authoring: "guidely:authoring"
165
+ authoring: "guidely:authoring",
166
+ /** Messenger conversations this browser can read: [{ id, token }]. */
167
+ conversations: (key) => `guidely:conversations:${key}`
90
168
  };
91
169
 
92
170
  // src/authoring.ts
@@ -96,6 +174,8 @@ async function loadSchemas() {
96
174
  }
97
175
  var AUTHORING_PARAM = "guidely_authoring";
98
176
  var HEARTBEAT_MS = 15e3;
177
+ var SNAPSHOT_DEBOUNCE_MS = 800;
178
+ var SNAPSHOT_MAX_WAIT_MS = 4e3;
99
179
  var AuthoringBridge = class {
100
180
  constructor(opts) {
101
181
  __publicField(this, "opts", opts);
@@ -106,6 +186,10 @@ var AuthoringBridge = class {
106
186
  __publicField(this, "preview", null);
107
187
  __publicField(this, "previewGuide", null);
108
188
  __publicField(this, "active", false);
189
+ __publicField(this, "observer", null);
190
+ __publicField(this, "snapshotTimer", null);
191
+ __publicField(this, "snapshotDue", 0);
192
+ __publicField(this, "lastSnapshot", "");
109
193
  }
110
194
  isActive() {
111
195
  return this.active;
@@ -161,6 +245,7 @@ var AuthoringBridge = class {
161
245
  });
162
246
  }, HEARTBEAT_MS);
163
247
  this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
248
+ this.watchPage();
164
249
  } catch (err) {
165
250
  this.opts.onError(err);
166
251
  }
@@ -182,6 +267,32 @@ var AuthoringBridge = class {
182
267
  this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
183
268
  void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
184
269
  }
270
+ /** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
271
+ watchPage() {
272
+ this.scheduleSnapshot();
273
+ if (typeof MutationObserver === "undefined") return;
274
+ this.observer = new MutationObserver(() => this.scheduleSnapshot());
275
+ this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
276
+ }
277
+ scheduleSnapshot() {
278
+ const now = Date.now();
279
+ if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
280
+ else clearTimeout(this.snapshotTimer);
281
+ this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
282
+ }
283
+ sendSnapshot() {
284
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
285
+ this.snapshotTimer = null;
286
+ if (!this.info || !this.active) return;
287
+ try {
288
+ const snapshot = captureSnapshot(document, this.opts.maskSelectors);
289
+ if (!snapshot || snapshot.html === this.lastSnapshot) return;
290
+ this.lastSnapshot = snapshot.html;
291
+ this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
292
+ } catch (err) {
293
+ this.opts.onError(err);
294
+ }
295
+ }
185
296
  post(message) {
186
297
  if (!this.info) return;
187
298
  try {
@@ -249,6 +360,10 @@ var AuthoringBridge = class {
249
360
  case "guidely:stop-preview":
250
361
  this.stopPreview();
251
362
  break;
363
+ case "guidely:request-snapshot":
364
+ this.lastSnapshot = "";
365
+ this.sendSnapshot();
366
+ break;
252
367
  case "guidely:ping":
253
368
  this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
254
369
  break;
@@ -267,6 +382,10 @@ var AuthoringBridge = class {
267
382
  if (!this.active) return;
268
383
  this.active = false;
269
384
  this.stopPreview();
385
+ this.observer?.disconnect();
386
+ this.observer = null;
387
+ if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
388
+ this.snapshotTimer = null;
270
389
  if (this.heartbeat) clearInterval(this.heartbeat);
271
390
  this.heartbeat = null;
272
391
  this.session?.stop();
@@ -435,7 +554,7 @@ var Api = class {
435
554
  async request(method, path, options = {}) {
436
555
  const f = this.fetcher;
437
556
  if (!f) throw new Error("fetch is not available");
438
- const headers = { "X-Guidely-Key": this.key, Accept: "application/json" };
557
+ const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
439
558
  if (options.body !== void 0) headers["Content-Type"] = "application/json";
440
559
  if (options.etag) headers["If-None-Match"] = options.etag;
441
560
  const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
@@ -469,6 +588,33 @@ var Api = class {
469
588
  post(path, body, options) {
470
589
  return this.request("POST", path, { body, ...options });
471
590
  }
591
+ /**
592
+ * POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
593
+ * returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
594
+ */
595
+ async stream(path, body, onEvent, headers = {}) {
596
+ const f = this.fetcher;
597
+ if (!f) throw new Error("fetch is not available");
598
+ const res = await f(this.url(path), {
599
+ method: "POST",
600
+ headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
601
+ body: JSON.stringify(body),
602
+ credentials: "omit",
603
+ mode: "cors"
604
+ });
605
+ const type = res.headers?.get?.("content-type") ?? "";
606
+ if (!type.includes("text/event-stream") || !res.body) {
607
+ let data = null;
608
+ try {
609
+ data = await res.json();
610
+ } catch {
611
+ data = null;
612
+ }
613
+ return { status: res.status, ok: res.ok, data, etag: null };
614
+ }
615
+ await readEventStream(res.body, onEvent);
616
+ return { status: res.status, ok: res.ok, data: null, etag: null };
617
+ }
472
618
  /** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
473
619
  beacon(path, body) {
474
620
  try {
@@ -495,6 +641,143 @@ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
495
641
  }
496
642
  return null;
497
643
  }
644
+ async function readEventStream(body, onEvent) {
645
+ const reader = body.getReader();
646
+ const decoder = new TextDecoder();
647
+ let buffer = "";
648
+ const dispatch = (raw) => {
649
+ let event = "message";
650
+ const data = [];
651
+ for (const line of raw.split("\n")) {
652
+ if (!line || line.startsWith(":")) continue;
653
+ const i = line.indexOf(":");
654
+ const field = i < 0 ? line : line.slice(0, i);
655
+ const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
656
+ if (field === "event") event = value;
657
+ else if (field === "data") data.push(value);
658
+ }
659
+ if (!data.length) return;
660
+ try {
661
+ onEvent(event, JSON.parse(data.join("\n")));
662
+ } catch {
663
+ }
664
+ };
665
+ for (; ; ) {
666
+ const { done, value } = await reader.read();
667
+ if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
668
+ let sep;
669
+ while ((sep = buffer.indexOf("\n\n")) >= 0) {
670
+ dispatch(buffer.slice(0, sep));
671
+ buffer = buffer.slice(sep + 2);
672
+ }
673
+ if (done) break;
674
+ }
675
+ if (buffer.trim()) dispatch(buffer);
676
+ }
677
+
678
+ // src/messenger-api.ts
679
+ var MAX_STORED = 50;
680
+ var TOKEN_HEADER = "X-Guidely-Conversation";
681
+ function storedConversations(store, projectId) {
682
+ const list = readJson(store, KEYS.conversations(projectId), []);
683
+ return Array.isArray(list) ? list.filter((c) => c && typeof c.id === "string" && typeof c.token === "string") : [];
684
+ }
685
+ function forgetConversations(store, projectId) {
686
+ store.remove(KEYS.conversations(projectId));
687
+ }
688
+ var HttpError = class extends Error {
689
+ constructor(status) {
690
+ super(`Request failed (${status})`);
691
+ __publicField(this, "status", status);
692
+ }
693
+ };
694
+ function createMessengerTransport(opts) {
695
+ const { api, store, projectId } = opts;
696
+ const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
697
+ const remember = (id, token) => {
698
+ const rest = storedConversations(store, projectId).filter((c) => c.id !== id);
699
+ writeJson(store, KEYS.conversations(projectId), [{ id, token }, ...rest].slice(0, MAX_STORED));
700
+ };
701
+ const forget = (id) => writeJson(store, KEYS.conversations(projectId), storedConversations(store, projectId).filter((c) => c.id !== id));
702
+ async function exchange(path, body, headers, h, onStarted) {
703
+ let finished = false;
704
+ const res = await api.stream(
705
+ path,
706
+ body,
707
+ (event, data) => {
708
+ if (event === "conversation") {
709
+ const d = data;
710
+ onStarted?.(d);
711
+ h.onConversation?.(d.conversation);
712
+ d.messages.forEach((m) => h.onMessage(m));
713
+ } else if (event === "message") h.onMessage(data);
714
+ else if (event === "delta") {
715
+ const d = data;
716
+ h.onDelta(d.messageId, d.text);
717
+ } else if (event === "done") {
718
+ const d = data;
719
+ finished = true;
720
+ h.onDone(d.message, d.conversation);
721
+ } else if (event === "error") h.onError(data?.message ?? "Something went wrong");
722
+ },
723
+ headers
724
+ );
725
+ if (!res.ok) throw new HttpError(res.status);
726
+ if (res.data) {
727
+ const d = res.data;
728
+ if ("token" in d) {
729
+ onStarted?.(d);
730
+ h.onConversation?.(d.conversation);
731
+ }
732
+ d.messages.forEach((m) => h.onMessage(m));
733
+ h.onDone(d.messages.at(-1) ?? null, d.conversation);
734
+ } else if (!finished) h.onError("The reply was interrupted");
735
+ }
736
+ return {
737
+ async listConversations() {
738
+ const tokens = storedConversations(store, projectId).map((c) => c.token);
739
+ if (!tokens.length) return [];
740
+ const res = await api.post("/conversations/lookup", { tokens });
741
+ if (!res.ok || !res.data) throw new HttpError(res.status);
742
+ const known = new Set(res.data.conversations.map((c) => c.id));
743
+ for (const c of storedConversations(store, projectId)) if (!known.has(c.id)) forget(c.id);
744
+ return res.data.conversations.map(({ token: _token, ...c }) => c);
745
+ },
746
+ async loadMessages(id, since) {
747
+ const res = await api.get(`/conversations/${encodeURIComponent(id)}/messages`, { params: { since: since ?? void 0 }, headers: { [TOKEN_HEADER]: tokenFor(id) } });
748
+ if (!res.ok || !res.data) throw new HttpError(res.status);
749
+ return res.data;
750
+ },
751
+ start(text, h) {
752
+ return exchange("/conversations", { anonymousId: opts.anonymousId(), userId: opts.userId(), message: text, pageUrl: opts.pageUrl() }, {}, h, (r) => remember(r.conversation.id, r.token));
753
+ },
754
+ send(id, text, h) {
755
+ return exchange(`/conversations/${encodeURIComponent(id)}/messages`, { message: text }, { [TOKEN_HEADER]: tokenFor(id) }, h);
756
+ },
757
+ async handoff(id) {
758
+ const res = await api.post(`/conversations/${encodeURIComponent(id)}/handoff`, {}, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
759
+ if (!res.ok || !res.data) throw new HttpError(res.status);
760
+ return res.data;
761
+ },
762
+ async rate(id, rating) {
763
+ const res = await api.post(`/conversations/${encodeURIComponent(id)}/rating`, { rating }, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
764
+ if (!res.ok) throw new HttpError(res.status);
765
+ },
766
+ async listArticles() {
767
+ const res = await api.get("/articles");
768
+ if (!res.ok || !res.data) throw new HttpError(res.status);
769
+ return res.data;
770
+ },
771
+ async getArticle(id) {
772
+ const res = await api.get(`/articles/${encodeURIComponent(id)}`);
773
+ return res.ok && res.data ? res.data.article : null;
774
+ },
775
+ async searchArticles(q) {
776
+ const res = await api.get("/resource-center/search", { params: { q } });
777
+ return res.ok && res.data ? res.data.articles : [];
778
+ }
779
+ };
780
+ }
498
781
 
499
782
  // src/navigation.ts
500
783
  function watchNavigation(listener, debounceMs = 50) {
@@ -666,7 +949,7 @@ var EventQueue = class {
666
949
 
667
950
  // src/client.ts
668
951
  var SDK_NAME = "@guidelytech/sdk-core";
669
- var SDK_VERSION = "0.1.0";
952
+ var SDK_VERSION = "0.2.1";
670
953
  var DEFAULT_API_URL = "https://api.guidely.app";
671
954
  var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
672
955
  function isBrowser() {
@@ -702,6 +985,9 @@ var GuidelyClient = class _GuidelyClient {
702
985
  __publicField(this, "widgets", /* @__PURE__ */ new Map());
703
986
  __publicField(this, "resourceCenter", null);
704
987
  __publicField(this, "resourceCenterScope", null);
988
+ __publicField(this, "messenger", null);
989
+ __publicField(this, "messengerScope", null);
990
+ __publicField(this, "messengerLoading", null);
705
991
  __publicField(this, "cleanups", []);
706
992
  __publicField(this, "timers", /* @__PURE__ */ new Set());
707
993
  __publicField(this, "visibilityObservers", /* @__PURE__ */ new Map());
@@ -710,6 +996,8 @@ var GuidelyClient = class _GuidelyClient {
710
996
  __publicField(this, "lastUrl");
711
997
  __publicField(this, "authoring", null);
712
998
  __publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
999
+ /** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
1000
+ __publicField(this, "identitySettled", false);
713
1001
  __publicField(this, "customListeners", /* @__PURE__ */ new Map());
714
1002
  __publicField(this, "eventBus", {
715
1003
  on: (name, cb) => {
@@ -793,8 +1081,10 @@ var GuidelyClient = class _GuidelyClient {
793
1081
  await this.fetchConfig();
794
1082
  if (this.destroyed) return;
795
1083
  this.wireListeners();
796
- if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
797
- else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1084
+ if (!this.identitySettled) {
1085
+ if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
1086
+ else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
1087
+ }
798
1088
  if (this.destroyed) return;
799
1089
  this.isReady = true;
800
1090
  if (this.config) this.emitter.emit("ready", this.config);
@@ -886,6 +1176,7 @@ var GuidelyClient = class _GuidelyClient {
886
1176
  this.resourceCenter?.destroy();
887
1177
  this.resourceCenterScope?.destroy();
888
1178
  this.resourceCenter = null;
1179
+ this.destroyMessenger();
889
1180
  this.visibilityObservers.forEach((stop) => stop());
890
1181
  this.visibilityObservers.clear();
891
1182
  this.timers.forEach((t) => clearTimeout(t));
@@ -905,6 +1196,7 @@ var GuidelyClient = class _GuidelyClient {
905
1196
  /** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
906
1197
  async identify(userId, traits, account) {
907
1198
  if (this.destroyed) return;
1199
+ this.identitySettled = true;
908
1200
  this.user = { id: String(userId), traits: { ...this.user?.id === String(userId) ? this.user.traits : {}, ...traits || {} }, account: account ?? (this.user?.id === String(userId) ? this.user.account : void 0) };
909
1201
  writeJson(this.store, KEYS.user(this.options.projectId), this.user);
910
1202
  this.enqueue("identify", { properties: { traits: sanitizeProperties(this.user.traits) } });
@@ -970,6 +1262,7 @@ var GuidelyClient = class _GuidelyClient {
970
1262
  }
971
1263
  /** Forgets the user: new anonymous id, cleared local guide/checklist state, triggers re-evaluated. */
972
1264
  reset() {
1265
+ this.identitySettled = true;
973
1266
  this.stopGuide();
974
1267
  this.user = null;
975
1268
  this.store.remove(KEYS.user(this.options.projectId));
@@ -984,6 +1277,8 @@ var GuidelyClient = class _GuidelyClient {
984
1277
  this.firedDelays.clear();
985
1278
  this.widgets.forEach((w) => w.destroy());
986
1279
  this.widgets.clear();
1280
+ forgetConversations(this.store, this.options.projectId);
1281
+ this.destroyMessenger();
987
1282
  this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
988
1283
  if (this.isReady) {
989
1284
  this.renderPersistentWidgets();
@@ -1420,6 +1715,16 @@ var GuidelyClient = class _GuidelyClient {
1420
1715
  renderPersistentWidgets() {
1421
1716
  if (!this.config) return;
1422
1717
  const rc = this.config.resourceCenter;
1718
+ if (this.config.messenger?.enabled) {
1719
+ if (this.resourceCenter) {
1720
+ this.resourceCenter.destroy();
1721
+ this.resourceCenterScope?.destroy();
1722
+ this.resourceCenter = null;
1723
+ }
1724
+ if (!this.messenger) void this.loadMessenger();
1725
+ return;
1726
+ }
1727
+ this.destroyMessenger();
1423
1728
  if (rc && rc.enabled && !this.resourceCenter) {
1424
1729
  const scope = createScope(this.theme());
1425
1730
  this.resourceCenterScope = scope;
@@ -1428,6 +1733,7 @@ var GuidelyClient = class _GuidelyClient {
1428
1733
  config: rc,
1429
1734
  guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
1430
1735
  search: (q) => this.searchResourceCenter(q),
1736
+ loadArticle: (id) => this.loadArticle(id),
1431
1737
  onStartGuide: (id) => this.startGuide(id),
1432
1738
  onOpenChecklist: (id) => this.openChecklist(id),
1433
1739
  onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
@@ -1450,6 +1756,60 @@ var GuidelyClient = class _GuidelyClient {
1450
1756
  if (!guide || !guide.config.steps.some((s) => s.id === saved.stepId)) return;
1451
1757
  this.showFlow(guide, saved.manual ?? false, saved.stepId);
1452
1758
  }
1759
+ /** Loads the messenger widget on demand (it is a separate chunk) and renders it. */
1760
+ loadMessenger() {
1761
+ if (this.messenger) return Promise.resolve(this.messenger);
1762
+ if (this.messengerLoading) return this.messengerLoading;
1763
+ this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
1764
+ const cfg = this.config?.messenger;
1765
+ if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
1766
+ const scope = createScope(this.theme());
1767
+ this.messengerScope = scope;
1768
+ this.messenger = renderMessenger({
1769
+ scope: scope.el,
1770
+ config: cfg,
1771
+ api: createMessengerTransport({
1772
+ api: this.api,
1773
+ store: this.store,
1774
+ projectId: this.options.projectId,
1775
+ anonymousId: () => this.anonymousId,
1776
+ userId: () => this.user?.id || null,
1777
+ pageUrl: () => this.collectUrls() ? location.href : void 0
1778
+ }),
1779
+ resourceCenter: this.config.resourceCenter,
1780
+ guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
1781
+ onStartGuide: (idOrKey) => this.startGuide(idOrKey),
1782
+ onOpenChecklist: (id) => this.openChecklist(id),
1783
+ onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
1784
+ onOpen: () => {
1785
+ this.enqueue("messenger_opened");
1786
+ this.emitter.emit("messenger:open");
1787
+ },
1788
+ onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation })
1789
+ });
1790
+ return this.messenger;
1791
+ }).catch((err) => {
1792
+ this.reportError(err);
1793
+ return null;
1794
+ }).finally(() => {
1795
+ this.messengerLoading = null;
1796
+ });
1797
+ return this.messengerLoading;
1798
+ }
1799
+ destroyMessenger() {
1800
+ this.messenger?.destroy();
1801
+ this.messengerScope?.destroy();
1802
+ this.messenger = null;
1803
+ this.messengerScope = null;
1804
+ }
1805
+ async loadArticle(id) {
1806
+ try {
1807
+ const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
1808
+ return res.ok && res.data ? res.data.article : null;
1809
+ } catch {
1810
+ return null;
1811
+ }
1812
+ }
1453
1813
  async searchResourceCenter(q) {
1454
1814
  try {
1455
1815
  const res = await this.api.get("/resource-center/search", { params: { q } });
@@ -1604,12 +1964,27 @@ var GuidelyClient = class _GuidelyClient {
1604
1964
  return !!this.state.guides[guide?.id ?? idOrKey]?.completedAt;
1605
1965
  }
1606
1966
  openResourceCenter() {
1967
+ if (this.config?.messenger?.enabled) return this.openMessenger("help");
1607
1968
  if (!this.resourceCenter) this.renderPersistentWidgets();
1608
1969
  this.resourceCenter?.open();
1609
1970
  }
1610
1971
  closeResourceCenter() {
1972
+ if (this.messenger) return this.closeMessenger();
1611
1973
  this.resourceCenter?.close();
1612
1974
  }
1975
+ /** Opens the Scout messenger (when enabled for the project), optionally on a tab. */
1976
+ openMessenger(tab) {
1977
+ if (!this.config?.messenger?.enabled) return;
1978
+ void this.loadMessenger().then((m) => m?.open(tab));
1979
+ }
1980
+ closeMessenger() {
1981
+ this.messenger?.close();
1982
+ }
1983
+ /** Opens a new messenger conversation, optionally pre-filling the message. */
1984
+ showNewMessage(text) {
1985
+ if (!this.config?.messenger?.enabled) return;
1986
+ void this.loadMessenger().then((m) => m?.showNewMessage(text));
1987
+ }
1613
1988
  on(event, cb) {
1614
1989
  return this.emitter.on(event, cb);
1615
1990
  }
@@ -1721,7 +2096,7 @@ var Guidely = {
1721
2096
  init: (options) => GuidelyClient.init(options),
1722
2097
  get: () => GuidelyClient.get(),
1723
2098
  createGuidely,
1724
- version: "0.1.0"
2099
+ version: SDK_VERSION
1725
2100
  };
1726
2101
  var index_default = Guidely;
1727
2102
  export {
@@ -1736,6 +2111,7 @@ export {
1736
2111
  SDK_VERSION,
1737
2112
  areMutuallyExclusive,
1738
2113
  buildContext,
2114
+ captureSnapshot,
1739
2115
  createGuidely,
1740
2116
  createStorage,
1741
2117
  index_default as default,