@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/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/README.md
CHANGED
|
@@ -149,8 +149,10 @@ Messages posted to the opener: `guidely:authoring-ready { sessionId, url }`, `gu
|
|
|
149
149
|
|
|
150
150
|
| Bundle | minified | min+gz |
|
|
151
151
|
|---|---|---|
|
|
152
|
-
| sdk-core + guide-runtime + floating-ui + `@guidelytech/shared/runtime` (everything an app ships) |
|
|
153
|
-
|
|
|
152
|
+
| sdk-core + guide-runtime + floating-ui + `@guidelytech/shared/runtime` (everything an app ships) | 127 kB | **39.4 kB** |
|
|
153
|
+
| Scout messenger chunk (`@guidelytech/guide-runtime/messenger`, loaded on demand) | 26 kB | 8.8 kB |
|
|
154
|
+
| script-tag build `guidely.js` (authoring schemas and messenger inlined) | 244 kB | ~71 kB |
|
|
154
155
|
|
|
155
156
|
The SDK imports only the zod-free `@guidelytech/shared/runtime` entry; the Zod schemas are loaded
|
|
156
|
-
lazily and only when the page enters authoring mode. Budget: 45 kB min+gz for the npm runtime
|
|
157
|
+
lazily and only when the page enters authoring mode. Budget: 45 kB min+gz for the npm runtime and
|
|
158
|
+
15 kB for the messenger chunk.
|
package/dist/index.cjs
CHANGED
|
@@ -43,6 +43,7 @@ __export(index_exports, {
|
|
|
43
43
|
SDK_VERSION: () => SDK_VERSION,
|
|
44
44
|
areMutuallyExclusive: () => areMutuallyExclusive,
|
|
45
45
|
buildContext: () => buildContext,
|
|
46
|
+
captureSnapshot: () => captureSnapshot,
|
|
46
47
|
createGuidely: () => createGuidely,
|
|
47
48
|
createStorage: () => createStorage,
|
|
48
49
|
default: () => index_default,
|
|
@@ -55,13 +56,89 @@ __export(index_exports, {
|
|
|
55
56
|
module.exports = __toCommonJS(index_exports);
|
|
56
57
|
|
|
57
58
|
// src/client.ts
|
|
58
|
-
var
|
|
59
|
+
var import_runtime6 = require("@guidelytech/shared/runtime");
|
|
59
60
|
var import_guide_runtime2 = require("@guidelytech/guide-runtime");
|
|
60
61
|
var import_guide_runtime3 = require("@guidelytech/guide-runtime");
|
|
61
62
|
|
|
62
63
|
// src/authoring.ts
|
|
63
64
|
var import_guide_runtime = require("@guidelytech/guide-runtime");
|
|
64
65
|
|
|
66
|
+
// src/snapshot.ts
|
|
67
|
+
var MAX_SNAPSHOT_BYTES = 6 * 1024 * 1024;
|
|
68
|
+
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']";
|
|
69
|
+
function rulesText(sheet) {
|
|
70
|
+
if (!sheet) return null;
|
|
71
|
+
try {
|
|
72
|
+
return Array.from(sheet.cssRules, (r) => r.cssText).join("\n");
|
|
73
|
+
} catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function maskText(root) {
|
|
78
|
+
const walker = root.ownerDocument.createTreeWalker(root, NodeFilter.SHOW_TEXT);
|
|
79
|
+
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
|
|
80
|
+
n.textContent = (n.textContent ?? "").replace(/\S/g, "\u2022");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function captureSnapshot(doc, maskSelectors, maxBytes = MAX_SNAPSHOT_BYTES) {
|
|
84
|
+
const win = doc.defaultView;
|
|
85
|
+
if (!win || !doc.documentElement) return null;
|
|
86
|
+
const clone = doc.documentElement.cloneNode(true);
|
|
87
|
+
const origStyles = doc.documentElement.querySelectorAll("style");
|
|
88
|
+
clone.querySelectorAll("style").forEach((el, i) => {
|
|
89
|
+
const css = rulesText(origStyles[i]?.sheet);
|
|
90
|
+
if (css !== null && css.length > (el.textContent ?? "").length) el.textContent = css;
|
|
91
|
+
});
|
|
92
|
+
const origFrames = doc.documentElement.querySelectorAll("iframe, canvas, video, embed, object");
|
|
93
|
+
clone.querySelectorAll("iframe, canvas, video, embed, object").forEach((el, i) => {
|
|
94
|
+
const r = origFrames[i]?.getBoundingClientRect();
|
|
95
|
+
const box = doc.createElement("div");
|
|
96
|
+
box.setAttribute("style", `${el.getAttribute("style") ?? ""};width:${r?.width ?? 0}px;height:${r?.height ?? 0}px;background:#e2e8f0;border-radius:4px`);
|
|
97
|
+
if (el.getAttribute("class")) box.setAttribute("class", el.getAttribute("class"));
|
|
98
|
+
el.replaceWith(box);
|
|
99
|
+
});
|
|
100
|
+
clone.querySelectorAll(STRIP).forEach((el) => el.remove());
|
|
101
|
+
for (const sel of maskSelectors) {
|
|
102
|
+
try {
|
|
103
|
+
clone.querySelectorAll(sel).forEach(maskText);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
clone.querySelectorAll("*").forEach((el) => {
|
|
108
|
+
for (const attr of Array.from(el.attributes)) {
|
|
109
|
+
const name = attr.name.toLowerCase();
|
|
110
|
+
if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && /^\s*javascript:/i.test(attr.value)) el.removeAttribute(attr.name);
|
|
111
|
+
}
|
|
112
|
+
if (el instanceof HTMLInputElement) {
|
|
113
|
+
el.removeAttribute("value");
|
|
114
|
+
el.removeAttribute("checked");
|
|
115
|
+
} else if (el instanceof HTMLTextAreaElement) {
|
|
116
|
+
el.textContent = "";
|
|
117
|
+
}
|
|
118
|
+
el.removeAttribute("autofocus");
|
|
119
|
+
});
|
|
120
|
+
const adopted = Array.from(doc.adoptedStyleSheets ?? []).map(rulesText).filter(Boolean).join("\n");
|
|
121
|
+
let head = clone.querySelector("head");
|
|
122
|
+
if (!head) {
|
|
123
|
+
head = doc.createElement("head");
|
|
124
|
+
clone.prepend(head);
|
|
125
|
+
}
|
|
126
|
+
if (adopted) {
|
|
127
|
+
const style = doc.createElement("style");
|
|
128
|
+
style.textContent = adopted;
|
|
129
|
+
head.append(style);
|
|
130
|
+
}
|
|
131
|
+
clone.querySelectorAll("base").forEach((el) => el.remove());
|
|
132
|
+
const base = doc.createElement("base");
|
|
133
|
+
base.setAttribute("href", doc.baseURI || win.location.href);
|
|
134
|
+
base.setAttribute("target", "_blank");
|
|
135
|
+
head.prepend(base);
|
|
136
|
+
const html = `<!DOCTYPE html>
|
|
137
|
+
${clone.outerHTML}`;
|
|
138
|
+
if (html.length > maxBytes) return null;
|
|
139
|
+
return { html, url: win.location.href, title: doc.title, width: win.innerWidth, height: win.innerHeight, capturedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
140
|
+
}
|
|
141
|
+
|
|
65
142
|
// src/storage.ts
|
|
66
143
|
function memoryStore() {
|
|
67
144
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -129,7 +206,9 @@ var KEYS = {
|
|
|
129
206
|
queue: (key) => `guidely:queue:${key}`,
|
|
130
207
|
session: "guidely:session",
|
|
131
208
|
active: "guidely:active",
|
|
132
|
-
authoring: "guidely:authoring"
|
|
209
|
+
authoring: "guidely:authoring",
|
|
210
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
211
|
+
conversations: (key) => `guidely:conversations:${key}`
|
|
133
212
|
};
|
|
134
213
|
|
|
135
214
|
// src/authoring.ts
|
|
@@ -139,6 +218,8 @@ async function loadSchemas() {
|
|
|
139
218
|
}
|
|
140
219
|
var AUTHORING_PARAM = "guidely_authoring";
|
|
141
220
|
var HEARTBEAT_MS = 15e3;
|
|
221
|
+
var SNAPSHOT_DEBOUNCE_MS = 800;
|
|
222
|
+
var SNAPSHOT_MAX_WAIT_MS = 4e3;
|
|
142
223
|
var AuthoringBridge = class {
|
|
143
224
|
constructor(opts) {
|
|
144
225
|
__publicField(this, "opts", opts);
|
|
@@ -149,6 +230,10 @@ var AuthoringBridge = class {
|
|
|
149
230
|
__publicField(this, "preview", null);
|
|
150
231
|
__publicField(this, "previewGuide", null);
|
|
151
232
|
__publicField(this, "active", false);
|
|
233
|
+
__publicField(this, "observer", null);
|
|
234
|
+
__publicField(this, "snapshotTimer", null);
|
|
235
|
+
__publicField(this, "snapshotDue", 0);
|
|
236
|
+
__publicField(this, "lastSnapshot", "");
|
|
152
237
|
}
|
|
153
238
|
isActive() {
|
|
154
239
|
return this.active;
|
|
@@ -204,6 +289,7 @@ var AuthoringBridge = class {
|
|
|
204
289
|
});
|
|
205
290
|
}, HEARTBEAT_MS);
|
|
206
291
|
this.post({ type: "guidely:authoring-ready", sessionId: this.info.id, url: window.location.href });
|
|
292
|
+
this.watchPage();
|
|
207
293
|
} catch (err) {
|
|
208
294
|
this.opts.onError(err);
|
|
209
295
|
}
|
|
@@ -225,6 +311,32 @@ var AuthoringBridge = class {
|
|
|
225
311
|
this.post({ type: "guidely:pick", sessionId: this.info.id, pick });
|
|
226
312
|
void this.opts.api.post("/authoring/session/picks", pick, { params: { token: this.token } }).catch((err) => this.opts.onError(err));
|
|
227
313
|
}
|
|
314
|
+
/** Sends a snapshot now and again whenever the page settles after DOM changes or navigation. */
|
|
315
|
+
watchPage() {
|
|
316
|
+
this.scheduleSnapshot();
|
|
317
|
+
if (typeof MutationObserver === "undefined") return;
|
|
318
|
+
this.observer = new MutationObserver(() => this.scheduleSnapshot());
|
|
319
|
+
this.observer.observe(document.documentElement, { subtree: true, childList: true, attributes: true, characterData: true });
|
|
320
|
+
}
|
|
321
|
+
scheduleSnapshot() {
|
|
322
|
+
const now = Date.now();
|
|
323
|
+
if (!this.snapshotTimer) this.snapshotDue = now + SNAPSHOT_MAX_WAIT_MS;
|
|
324
|
+
else clearTimeout(this.snapshotTimer);
|
|
325
|
+
this.snapshotTimer = setTimeout(() => this.sendSnapshot(), Math.max(0, Math.min(SNAPSHOT_DEBOUNCE_MS, this.snapshotDue - now)));
|
|
326
|
+
}
|
|
327
|
+
sendSnapshot() {
|
|
328
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
329
|
+
this.snapshotTimer = null;
|
|
330
|
+
if (!this.info || !this.active) return;
|
|
331
|
+
try {
|
|
332
|
+
const snapshot = captureSnapshot(document, this.opts.maskSelectors);
|
|
333
|
+
if (!snapshot || snapshot.html === this.lastSnapshot) return;
|
|
334
|
+
this.lastSnapshot = snapshot.html;
|
|
335
|
+
this.post({ type: "guidely:snapshot", sessionId: this.info.id, snapshot });
|
|
336
|
+
} catch (err) {
|
|
337
|
+
this.opts.onError(err);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
228
340
|
post(message) {
|
|
229
341
|
if (!this.info) return;
|
|
230
342
|
try {
|
|
@@ -292,6 +404,10 @@ var AuthoringBridge = class {
|
|
|
292
404
|
case "guidely:stop-preview":
|
|
293
405
|
this.stopPreview();
|
|
294
406
|
break;
|
|
407
|
+
case "guidely:request-snapshot":
|
|
408
|
+
this.lastSnapshot = "";
|
|
409
|
+
this.sendSnapshot();
|
|
410
|
+
break;
|
|
295
411
|
case "guidely:ping":
|
|
296
412
|
this.post({ type: "guidely:pong", sessionId: this.info.id, url: window.location.href });
|
|
297
413
|
break;
|
|
@@ -310,6 +426,10 @@ var AuthoringBridge = class {
|
|
|
310
426
|
if (!this.active) return;
|
|
311
427
|
this.active = false;
|
|
312
428
|
this.stopPreview();
|
|
429
|
+
this.observer?.disconnect();
|
|
430
|
+
this.observer = null;
|
|
431
|
+
if (this.snapshotTimer) clearTimeout(this.snapshotTimer);
|
|
432
|
+
this.snapshotTimer = null;
|
|
313
433
|
if (this.heartbeat) clearInterval(this.heartbeat);
|
|
314
434
|
this.heartbeat = null;
|
|
315
435
|
this.session?.stop();
|
|
@@ -478,7 +598,7 @@ var Api = class {
|
|
|
478
598
|
async request(method, path, options = {}) {
|
|
479
599
|
const f = this.fetcher;
|
|
480
600
|
if (!f) throw new Error("fetch is not available");
|
|
481
|
-
const headers = { "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
601
|
+
const headers = { ...options.headers, "X-Guidely-Key": this.key, Accept: "application/json" };
|
|
482
602
|
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
483
603
|
if (options.etag) headers["If-None-Match"] = options.etag;
|
|
484
604
|
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
@@ -512,6 +632,33 @@ var Api = class {
|
|
|
512
632
|
post(path, body, options) {
|
|
513
633
|
return this.request("POST", path, { body, ...options });
|
|
514
634
|
}
|
|
635
|
+
/**
|
|
636
|
+
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
637
|
+
* returned instead. No timeout: Scout replies can take a while and the server keeps the stream alive.
|
|
638
|
+
*/
|
|
639
|
+
async stream(path, body, onEvent, headers = {}) {
|
|
640
|
+
const f = this.fetcher;
|
|
641
|
+
if (!f) throw new Error("fetch is not available");
|
|
642
|
+
const res = await f(this.url(path), {
|
|
643
|
+
method: "POST",
|
|
644
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
645
|
+
body: JSON.stringify(body),
|
|
646
|
+
credentials: "omit",
|
|
647
|
+
mode: "cors"
|
|
648
|
+
});
|
|
649
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
650
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
651
|
+
let data = null;
|
|
652
|
+
try {
|
|
653
|
+
data = await res.json();
|
|
654
|
+
} catch {
|
|
655
|
+
data = null;
|
|
656
|
+
}
|
|
657
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
658
|
+
}
|
|
659
|
+
await readEventStream(res.body, onEvent);
|
|
660
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
661
|
+
}
|
|
515
662
|
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
516
663
|
beacon(path, body) {
|
|
517
664
|
try {
|
|
@@ -538,6 +685,143 @@ async function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
|
538
685
|
}
|
|
539
686
|
return null;
|
|
540
687
|
}
|
|
688
|
+
async function readEventStream(body, onEvent) {
|
|
689
|
+
const reader = body.getReader();
|
|
690
|
+
const decoder = new TextDecoder();
|
|
691
|
+
let buffer = "";
|
|
692
|
+
const dispatch = (raw) => {
|
|
693
|
+
let event = "message";
|
|
694
|
+
const data = [];
|
|
695
|
+
for (const line of raw.split("\n")) {
|
|
696
|
+
if (!line || line.startsWith(":")) continue;
|
|
697
|
+
const i = line.indexOf(":");
|
|
698
|
+
const field = i < 0 ? line : line.slice(0, i);
|
|
699
|
+
const value = i < 0 ? "" : line.slice(i + 1).replace(/^ /, "");
|
|
700
|
+
if (field === "event") event = value;
|
|
701
|
+
else if (field === "data") data.push(value);
|
|
702
|
+
}
|
|
703
|
+
if (!data.length) return;
|
|
704
|
+
try {
|
|
705
|
+
onEvent(event, JSON.parse(data.join("\n")));
|
|
706
|
+
} catch {
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
for (; ; ) {
|
|
710
|
+
const { done, value } = await reader.read();
|
|
711
|
+
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
712
|
+
let sep;
|
|
713
|
+
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
714
|
+
dispatch(buffer.slice(0, sep));
|
|
715
|
+
buffer = buffer.slice(sep + 2);
|
|
716
|
+
}
|
|
717
|
+
if (done) break;
|
|
718
|
+
}
|
|
719
|
+
if (buffer.trim()) dispatch(buffer);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/messenger-api.ts
|
|
723
|
+
var MAX_STORED = 50;
|
|
724
|
+
var TOKEN_HEADER = "X-Guidely-Conversation";
|
|
725
|
+
function storedConversations(store, projectId) {
|
|
726
|
+
const list = readJson(store, KEYS.conversations(projectId), []);
|
|
727
|
+
return Array.isArray(list) ? list.filter((c) => c && typeof c.id === "string" && typeof c.token === "string") : [];
|
|
728
|
+
}
|
|
729
|
+
function forgetConversations(store, projectId) {
|
|
730
|
+
store.remove(KEYS.conversations(projectId));
|
|
731
|
+
}
|
|
732
|
+
var HttpError = class extends Error {
|
|
733
|
+
constructor(status) {
|
|
734
|
+
super(`Request failed (${status})`);
|
|
735
|
+
__publicField(this, "status", status);
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
function createMessengerTransport(opts) {
|
|
739
|
+
const { api, store, projectId } = opts;
|
|
740
|
+
const tokenFor = (id) => storedConversations(store, projectId).find((c) => c.id === id)?.token ?? "";
|
|
741
|
+
const remember = (id, token) => {
|
|
742
|
+
const rest = storedConversations(store, projectId).filter((c) => c.id !== id);
|
|
743
|
+
writeJson(store, KEYS.conversations(projectId), [{ id, token }, ...rest].slice(0, MAX_STORED));
|
|
744
|
+
};
|
|
745
|
+
const forget = (id) => writeJson(store, KEYS.conversations(projectId), storedConversations(store, projectId).filter((c) => c.id !== id));
|
|
746
|
+
async function exchange(path, body, headers, h, onStarted) {
|
|
747
|
+
let finished = false;
|
|
748
|
+
const res = await api.stream(
|
|
749
|
+
path,
|
|
750
|
+
body,
|
|
751
|
+
(event, data) => {
|
|
752
|
+
if (event === "conversation") {
|
|
753
|
+
const d = data;
|
|
754
|
+
onStarted?.(d);
|
|
755
|
+
h.onConversation?.(d.conversation);
|
|
756
|
+
d.messages.forEach((m) => h.onMessage(m));
|
|
757
|
+
} else if (event === "message") h.onMessage(data);
|
|
758
|
+
else if (event === "delta") {
|
|
759
|
+
const d = data;
|
|
760
|
+
h.onDelta(d.messageId, d.text);
|
|
761
|
+
} else if (event === "done") {
|
|
762
|
+
const d = data;
|
|
763
|
+
finished = true;
|
|
764
|
+
h.onDone(d.message, d.conversation);
|
|
765
|
+
} else if (event === "error") h.onError(data?.message ?? "Something went wrong");
|
|
766
|
+
},
|
|
767
|
+
headers
|
|
768
|
+
);
|
|
769
|
+
if (!res.ok) throw new HttpError(res.status);
|
|
770
|
+
if (res.data) {
|
|
771
|
+
const d = res.data;
|
|
772
|
+
if ("token" in d) {
|
|
773
|
+
onStarted?.(d);
|
|
774
|
+
h.onConversation?.(d.conversation);
|
|
775
|
+
}
|
|
776
|
+
d.messages.forEach((m) => h.onMessage(m));
|
|
777
|
+
h.onDone(d.messages.at(-1) ?? null, d.conversation);
|
|
778
|
+
} else if (!finished) h.onError("The reply was interrupted");
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
async listConversations() {
|
|
782
|
+
const tokens = storedConversations(store, projectId).map((c) => c.token);
|
|
783
|
+
if (!tokens.length) return [];
|
|
784
|
+
const res = await api.post("/conversations/lookup", { tokens });
|
|
785
|
+
if (!res.ok || !res.data) throw new HttpError(res.status);
|
|
786
|
+
const known = new Set(res.data.conversations.map((c) => c.id));
|
|
787
|
+
for (const c of storedConversations(store, projectId)) if (!known.has(c.id)) forget(c.id);
|
|
788
|
+
return res.data.conversations.map(({ token: _token, ...c }) => c);
|
|
789
|
+
},
|
|
790
|
+
async loadMessages(id, since) {
|
|
791
|
+
const res = await api.get(`/conversations/${encodeURIComponent(id)}/messages`, { params: { since: since ?? void 0 }, headers: { [TOKEN_HEADER]: tokenFor(id) } });
|
|
792
|
+
if (!res.ok || !res.data) throw new HttpError(res.status);
|
|
793
|
+
return res.data;
|
|
794
|
+
},
|
|
795
|
+
start(text, h) {
|
|
796
|
+
return exchange("/conversations", { anonymousId: opts.anonymousId(), userId: opts.userId(), message: text, pageUrl: opts.pageUrl() }, {}, h, (r) => remember(r.conversation.id, r.token));
|
|
797
|
+
},
|
|
798
|
+
send(id, text, h) {
|
|
799
|
+
return exchange(`/conversations/${encodeURIComponent(id)}/messages`, { message: text }, { [TOKEN_HEADER]: tokenFor(id) }, h);
|
|
800
|
+
},
|
|
801
|
+
async handoff(id) {
|
|
802
|
+
const res = await api.post(`/conversations/${encodeURIComponent(id)}/handoff`, {}, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
|
|
803
|
+
if (!res.ok || !res.data) throw new HttpError(res.status);
|
|
804
|
+
return res.data;
|
|
805
|
+
},
|
|
806
|
+
async rate(id, rating) {
|
|
807
|
+
const res = await api.post(`/conversations/${encodeURIComponent(id)}/rating`, { rating }, { headers: { [TOKEN_HEADER]: tokenFor(id) } });
|
|
808
|
+
if (!res.ok) throw new HttpError(res.status);
|
|
809
|
+
},
|
|
810
|
+
async listArticles() {
|
|
811
|
+
const res = await api.get("/articles");
|
|
812
|
+
if (!res.ok || !res.data) throw new HttpError(res.status);
|
|
813
|
+
return res.data;
|
|
814
|
+
},
|
|
815
|
+
async getArticle(id) {
|
|
816
|
+
const res = await api.get(`/articles/${encodeURIComponent(id)}`);
|
|
817
|
+
return res.ok && res.data ? res.data.article : null;
|
|
818
|
+
},
|
|
819
|
+
async searchArticles(q) {
|
|
820
|
+
const res = await api.get("/resource-center/search", { params: { q } });
|
|
821
|
+
return res.ok && res.data ? res.data.articles : [];
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
}
|
|
541
825
|
|
|
542
826
|
// src/navigation.ts
|
|
543
827
|
function watchNavigation(listener, debounceMs = 50) {
|
|
@@ -709,7 +993,7 @@ var EventQueue = class {
|
|
|
709
993
|
|
|
710
994
|
// src/client.ts
|
|
711
995
|
var SDK_NAME = "@guidelytech/sdk-core";
|
|
712
|
-
var SDK_VERSION = "0.1
|
|
996
|
+
var SDK_VERSION = "0.2.1";
|
|
713
997
|
var DEFAULT_API_URL = "https://api.guidely.app";
|
|
714
998
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
715
999
|
function isBrowser() {
|
|
@@ -745,6 +1029,9 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
745
1029
|
__publicField(this, "widgets", /* @__PURE__ */ new Map());
|
|
746
1030
|
__publicField(this, "resourceCenter", null);
|
|
747
1031
|
__publicField(this, "resourceCenterScope", null);
|
|
1032
|
+
__publicField(this, "messenger", null);
|
|
1033
|
+
__publicField(this, "messengerScope", null);
|
|
1034
|
+
__publicField(this, "messengerLoading", null);
|
|
748
1035
|
__publicField(this, "cleanups", []);
|
|
749
1036
|
__publicField(this, "timers", /* @__PURE__ */ new Set());
|
|
750
1037
|
__publicField(this, "visibilityObservers", /* @__PURE__ */ new Map());
|
|
@@ -753,6 +1040,8 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
753
1040
|
__publicField(this, "lastUrl");
|
|
754
1041
|
__publicField(this, "authoring", null);
|
|
755
1042
|
__publicField(this, "lastAudiences", /* @__PURE__ */ new Set());
|
|
1043
|
+
/** Set once the app calls identify()/reset(); bootstrap then leaves identity alone instead of re-identifying. */
|
|
1044
|
+
__publicField(this, "identitySettled", false);
|
|
756
1045
|
__publicField(this, "customListeners", /* @__PURE__ */ new Map());
|
|
757
1046
|
__publicField(this, "eventBus", {
|
|
758
1047
|
on: (name, cb) => {
|
|
@@ -787,7 +1076,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
787
1076
|
this.sessionStore = this.options.storage === "memory" ? this.store : createStorage("session");
|
|
788
1077
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
789
1078
|
if (!this.anonymousId) {
|
|
790
|
-
this.anonymousId = `anon_${(0,
|
|
1079
|
+
this.anonymousId = `anon_${(0, import_runtime6.randomId)(20)}`;
|
|
791
1080
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
792
1081
|
}
|
|
793
1082
|
this.firstVisit = !this.store.get(KEYS.seen);
|
|
@@ -795,7 +1084,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
795
1084
|
this.state = { ...emptyState(), ...readJson(this.store, KEYS.state(this.options.projectId), {}) };
|
|
796
1085
|
this.user = readJson(this.store, KEYS.user(this.options.projectId), null);
|
|
797
1086
|
const session = readJson(this.sessionStore, KEYS.session, null);
|
|
798
|
-
this.session = session && session.id ? session : { id: `s_${(0,
|
|
1087
|
+
this.session = session && session.id ? session : { id: `s_${(0, import_runtime6.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
|
|
799
1088
|
this.persistSession();
|
|
800
1089
|
this.lastUrl = location.href;
|
|
801
1090
|
this.lastAudiences = new Set(this.state.audienceIds);
|
|
@@ -836,8 +1125,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
836
1125
|
await this.fetchConfig();
|
|
837
1126
|
if (this.destroyed) return;
|
|
838
1127
|
this.wireListeners();
|
|
839
|
-
if (this.
|
|
840
|
-
|
|
1128
|
+
if (!this.identitySettled) {
|
|
1129
|
+
if (this.options.user) await this.identify(this.options.user.id, this.options.user.traits, this.options.user.account);
|
|
1130
|
+
else if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
|
|
1131
|
+
}
|
|
841
1132
|
if (this.destroyed) return;
|
|
842
1133
|
this.isReady = true;
|
|
843
1134
|
if (this.config) this.emitter.emit("ready", this.config);
|
|
@@ -929,6 +1220,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
929
1220
|
this.resourceCenter?.destroy();
|
|
930
1221
|
this.resourceCenterScope?.destroy();
|
|
931
1222
|
this.resourceCenter = null;
|
|
1223
|
+
this.destroyMessenger();
|
|
932
1224
|
this.visibilityObservers.forEach((stop) => stop());
|
|
933
1225
|
this.visibilityObservers.clear();
|
|
934
1226
|
this.timers.forEach((t) => clearTimeout(t));
|
|
@@ -948,9 +1240,10 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
948
1240
|
/** Identifies the end user and loads server-held state (audiences, guide history, checklist progress). */
|
|
949
1241
|
async identify(userId, traits, account) {
|
|
950
1242
|
if (this.destroyed) return;
|
|
1243
|
+
this.identitySettled = true;
|
|
951
1244
|
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) };
|
|
952
1245
|
writeJson(this.store, KEYS.user(this.options.projectId), this.user);
|
|
953
|
-
this.enqueue("identify", { properties: { traits: (0,
|
|
1246
|
+
this.enqueue("identify", { properties: { traits: (0, import_runtime6.sanitizeProperties)(this.user.traits) } });
|
|
954
1247
|
await this.syncIdentify();
|
|
955
1248
|
this.emitter.emit("identify", { userId: this.user.id, anonymousId: this.anonymousId });
|
|
956
1249
|
if (this.isReady) this.evaluate("identify");
|
|
@@ -1013,20 +1306,23 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1013
1306
|
}
|
|
1014
1307
|
/** Forgets the user: new anonymous id, cleared local guide/checklist state, triggers re-evaluated. */
|
|
1015
1308
|
reset() {
|
|
1309
|
+
this.identitySettled = true;
|
|
1016
1310
|
this.stopGuide();
|
|
1017
1311
|
this.user = null;
|
|
1018
1312
|
this.store.remove(KEYS.user(this.options.projectId));
|
|
1019
|
-
this.anonymousId = `anon_${(0,
|
|
1313
|
+
this.anonymousId = `anon_${(0, import_runtime6.randomId)(20)}`;
|
|
1020
1314
|
this.store.set(KEYS.anon, this.anonymousId);
|
|
1021
1315
|
this.state = emptyState();
|
|
1022
1316
|
this.persistState();
|
|
1023
|
-
this.session = { id: `s_${(0,
|
|
1317
|
+
this.session = { id: `s_${(0, import_runtime6.randomId)(16)}`, startedAt: nowIso(), shownGuides: [], events: [] };
|
|
1024
1318
|
this.persistSession();
|
|
1025
1319
|
this.sessionStore.remove(KEYS.active);
|
|
1026
1320
|
this.lastAudiences = /* @__PURE__ */ new Set();
|
|
1027
1321
|
this.firedDelays.clear();
|
|
1028
1322
|
this.widgets.forEach((w) => w.destroy());
|
|
1029
1323
|
this.widgets.clear();
|
|
1324
|
+
forgetConversations(this.store, this.options.projectId);
|
|
1325
|
+
this.destroyMessenger();
|
|
1030
1326
|
this.emitter.emit("identify", { userId: null, anonymousId: this.anonymousId });
|
|
1031
1327
|
if (this.isReady) {
|
|
1032
1328
|
this.renderPersistentWidgets();
|
|
@@ -1051,7 +1347,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1051
1347
|
enqueue(name, extra = {}) {
|
|
1052
1348
|
if (this.consent === "denied" || !this.options.analytics) return null;
|
|
1053
1349
|
const event = {
|
|
1054
|
-
eventId: (0,
|
|
1350
|
+
eventId: (0, import_runtime6.randomId)(24),
|
|
1055
1351
|
name,
|
|
1056
1352
|
occurredAt: nowIso(),
|
|
1057
1353
|
anonymousId: this.anonymousId,
|
|
@@ -1061,7 +1357,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1061
1357
|
url: this.collectUrls() ? location.href : void 0,
|
|
1062
1358
|
...extra
|
|
1063
1359
|
};
|
|
1064
|
-
if (extra.properties) event.properties = (0,
|
|
1360
|
+
if (extra.properties) event.properties = (0, import_runtime6.sanitizeProperties)(extra.properties);
|
|
1065
1361
|
this.queue.enqueue(event);
|
|
1066
1362
|
this.emitter.emit("track", event);
|
|
1067
1363
|
return event;
|
|
@@ -1145,7 +1441,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1145
1441
|
if (!isWithinSchedule(guide)) return false;
|
|
1146
1442
|
if (guide.kind === "survey" && (this.state.surveysSubmitted.includes(guide.id) || this.state.surveysSubmitted.includes(guide.key))) return false;
|
|
1147
1443
|
ctx.rolloutSalt = guide.id;
|
|
1148
|
-
return (0,
|
|
1444
|
+
return (0, import_runtime6.evaluateRules)(c.targeting, ctx);
|
|
1149
1445
|
}
|
|
1150
1446
|
isEligible(guide, ctx, url) {
|
|
1151
1447
|
if (!this.isTargeted(guide, ctx, url)) return false;
|
|
@@ -1432,7 +1728,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1432
1728
|
title: guide.name,
|
|
1433
1729
|
onDismiss: () => finish(this.state.surveysSubmitted.includes(guide.id) ? "submitted" : "dismissed"),
|
|
1434
1730
|
onSubmit: (answers) => {
|
|
1435
|
-
const responseId = (0,
|
|
1731
|
+
const responseId = (0, import_runtime6.randomId)(24);
|
|
1436
1732
|
this.state.surveysSubmitted.push(guide.id);
|
|
1437
1733
|
this.persistState();
|
|
1438
1734
|
this.recordEnd(guide, "completed");
|
|
@@ -1463,6 +1759,16 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1463
1759
|
renderPersistentWidgets() {
|
|
1464
1760
|
if (!this.config) return;
|
|
1465
1761
|
const rc = this.config.resourceCenter;
|
|
1762
|
+
if (this.config.messenger?.enabled) {
|
|
1763
|
+
if (this.resourceCenter) {
|
|
1764
|
+
this.resourceCenter.destroy();
|
|
1765
|
+
this.resourceCenterScope?.destroy();
|
|
1766
|
+
this.resourceCenter = null;
|
|
1767
|
+
}
|
|
1768
|
+
if (!this.messenger) void this.loadMessenger();
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
this.destroyMessenger();
|
|
1466
1772
|
if (rc && rc.enabled && !this.resourceCenter) {
|
|
1467
1773
|
const scope = (0, import_guide_runtime3.createScope)(this.theme());
|
|
1468
1774
|
this.resourceCenterScope = scope;
|
|
@@ -1471,6 +1777,7 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1471
1777
|
config: rc,
|
|
1472
1778
|
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
1473
1779
|
search: (q) => this.searchResourceCenter(q),
|
|
1780
|
+
loadArticle: (id) => this.loadArticle(id),
|
|
1474
1781
|
onStartGuide: (id) => this.startGuide(id),
|
|
1475
1782
|
onOpenChecklist: (id) => this.openChecklist(id),
|
|
1476
1783
|
onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
|
|
@@ -1493,6 +1800,60 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1493
1800
|
if (!guide || !guide.config.steps.some((s) => s.id === saved.stepId)) return;
|
|
1494
1801
|
this.showFlow(guide, saved.manual ?? false, saved.stepId);
|
|
1495
1802
|
}
|
|
1803
|
+
/** Loads the messenger widget on demand (it is a separate chunk) and renders it. */
|
|
1804
|
+
loadMessenger() {
|
|
1805
|
+
if (this.messenger) return Promise.resolve(this.messenger);
|
|
1806
|
+
if (this.messengerLoading) return this.messengerLoading;
|
|
1807
|
+
this.messengerLoading = import("@guidelytech/guide-runtime/messenger").then(({ renderMessenger }) => {
|
|
1808
|
+
const cfg = this.config?.messenger;
|
|
1809
|
+
if (this.destroyed || !this.config || !cfg?.enabled || this.messenger) return this.messenger;
|
|
1810
|
+
const scope = (0, import_guide_runtime3.createScope)(this.theme());
|
|
1811
|
+
this.messengerScope = scope;
|
|
1812
|
+
this.messenger = renderMessenger({
|
|
1813
|
+
scope: scope.el,
|
|
1814
|
+
config: cfg,
|
|
1815
|
+
api: createMessengerTransport({
|
|
1816
|
+
api: this.api,
|
|
1817
|
+
store: this.store,
|
|
1818
|
+
projectId: this.options.projectId,
|
|
1819
|
+
anonymousId: () => this.anonymousId,
|
|
1820
|
+
userId: () => this.user?.id || null,
|
|
1821
|
+
pageUrl: () => this.collectUrls() ? location.href : void 0
|
|
1822
|
+
}),
|
|
1823
|
+
resourceCenter: this.config.resourceCenter,
|
|
1824
|
+
guides: this.config.guides.map((g) => ({ id: g.id, key: g.key, name: g.name, kind: g.kind })),
|
|
1825
|
+
onStartGuide: (idOrKey) => this.startGuide(idOrKey),
|
|
1826
|
+
onOpenChecklist: (id) => this.openChecklist(id),
|
|
1827
|
+
onArticleView: (a) => this.enqueue("article_viewed", { properties: { articleId: a.id, slug: a.slug } }),
|
|
1828
|
+
onOpen: () => {
|
|
1829
|
+
this.enqueue("messenger_opened");
|
|
1830
|
+
this.emitter.emit("messenger:open");
|
|
1831
|
+
},
|
|
1832
|
+
onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation })
|
|
1833
|
+
});
|
|
1834
|
+
return this.messenger;
|
|
1835
|
+
}).catch((err) => {
|
|
1836
|
+
this.reportError(err);
|
|
1837
|
+
return null;
|
|
1838
|
+
}).finally(() => {
|
|
1839
|
+
this.messengerLoading = null;
|
|
1840
|
+
});
|
|
1841
|
+
return this.messengerLoading;
|
|
1842
|
+
}
|
|
1843
|
+
destroyMessenger() {
|
|
1844
|
+
this.messenger?.destroy();
|
|
1845
|
+
this.messengerScope?.destroy();
|
|
1846
|
+
this.messenger = null;
|
|
1847
|
+
this.messengerScope = null;
|
|
1848
|
+
}
|
|
1849
|
+
async loadArticle(id) {
|
|
1850
|
+
try {
|
|
1851
|
+
const res = await this.api.get(`/articles/${encodeURIComponent(id)}`);
|
|
1852
|
+
return res.ok && res.data ? res.data.article : null;
|
|
1853
|
+
} catch {
|
|
1854
|
+
return null;
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1496
1857
|
async searchResourceCenter(q) {
|
|
1497
1858
|
try {
|
|
1498
1859
|
const res = await this.api.get("/resource-center/search", { params: { q } });
|
|
@@ -1647,12 +2008,27 @@ var GuidelyClient = class _GuidelyClient {
|
|
|
1647
2008
|
return !!this.state.guides[guide?.id ?? idOrKey]?.completedAt;
|
|
1648
2009
|
}
|
|
1649
2010
|
openResourceCenter() {
|
|
2011
|
+
if (this.config?.messenger?.enabled) return this.openMessenger("help");
|
|
1650
2012
|
if (!this.resourceCenter) this.renderPersistentWidgets();
|
|
1651
2013
|
this.resourceCenter?.open();
|
|
1652
2014
|
}
|
|
1653
2015
|
closeResourceCenter() {
|
|
2016
|
+
if (this.messenger) return this.closeMessenger();
|
|
1654
2017
|
this.resourceCenter?.close();
|
|
1655
2018
|
}
|
|
2019
|
+
/** Opens the Scout messenger (when enabled for the project), optionally on a tab. */
|
|
2020
|
+
openMessenger(tab) {
|
|
2021
|
+
if (!this.config?.messenger?.enabled) return;
|
|
2022
|
+
void this.loadMessenger().then((m) => m?.open(tab));
|
|
2023
|
+
}
|
|
2024
|
+
closeMessenger() {
|
|
2025
|
+
this.messenger?.close();
|
|
2026
|
+
}
|
|
2027
|
+
/** Opens a new messenger conversation, optionally pre-filling the message. */
|
|
2028
|
+
showNewMessage(text) {
|
|
2029
|
+
if (!this.config?.messenger?.enabled) return;
|
|
2030
|
+
void this.loadMessenger().then((m) => m?.showNewMessage(text));
|
|
2031
|
+
}
|
|
1656
2032
|
on(event, cb) {
|
|
1657
2033
|
return this.emitter.on(event, cb);
|
|
1658
2034
|
}
|
|
@@ -1764,7 +2140,7 @@ var Guidely = {
|
|
|
1764
2140
|
init: (options) => GuidelyClient.init(options),
|
|
1765
2141
|
get: () => GuidelyClient.get(),
|
|
1766
2142
|
createGuidely,
|
|
1767
|
-
version:
|
|
2143
|
+
version: SDK_VERSION
|
|
1768
2144
|
};
|
|
1769
2145
|
var index_default = Guidely;
|
|
1770
2146
|
// Annotate the CommonJS export names for ESM import in node:
|
|
@@ -1780,6 +2156,7 @@ var index_default = Guidely;
|
|
|
1780
2156
|
SDK_VERSION,
|
|
1781
2157
|
areMutuallyExclusive,
|
|
1782
2158
|
buildContext,
|
|
2159
|
+
captureSnapshot,
|
|
1783
2160
|
createGuidely,
|
|
1784
2161
|
createStorage,
|
|
1785
2162
|
detectDevice,
|