@guidelytech/sdk-core 0.1.0 → 0.2.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.
- package/README.md +5 -3
- package/dist/index.cjs +393 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +41 -4
- package/dist/index.d.ts +41 -4
- package/dist/index.js +382 -6
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
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.
|
|
952
|
+
var SDK_VERSION = "0.2.0";
|
|
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.
|
|
797
|
-
|
|
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:
|
|
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,
|