@zerocost/sdk 0.10.0 → 0.12.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/dist/core/widget-render.d.ts +8 -0
- package/dist/index.cjs +881 -156
- package/dist/index.d.ts +15 -19
- package/dist/index.js +881 -156
- package/dist/modules/llm-data.d.ts +39 -12
- package/dist/modules/widget.d.ts +2 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65,14 +65,18 @@ var ZerocostClient = class {
|
|
|
65
65
|
}
|
|
66
66
|
async request(path, body) {
|
|
67
67
|
const url = `${this.baseUrl}${path}`;
|
|
68
|
-
|
|
68
|
+
const payload = {
|
|
69
|
+
...body || {},
|
|
70
|
+
app_id: this.config.appId
|
|
71
|
+
};
|
|
72
|
+
this.log(`\u2192 ${url}`, payload);
|
|
69
73
|
const res = await fetch(url, {
|
|
70
74
|
method: "POST",
|
|
71
75
|
headers: {
|
|
72
76
|
"Content-Type": "application/json",
|
|
73
77
|
"x-api-key": this.config.apiKey
|
|
74
78
|
},
|
|
75
|
-
body:
|
|
79
|
+
body: JSON.stringify(payload)
|
|
76
80
|
});
|
|
77
81
|
const data = await res.json();
|
|
78
82
|
if (!res.ok) {
|
|
@@ -134,6 +138,142 @@ var TrackModule = class {
|
|
|
134
138
|
}
|
|
135
139
|
};
|
|
136
140
|
|
|
141
|
+
// src/core/widget-render.ts
|
|
142
|
+
var SDK_WIDGET_REFRESH_MS = 2e4;
|
|
143
|
+
function escapeHtml(value) {
|
|
144
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
145
|
+
}
|
|
146
|
+
function resolveTheme(theme) {
|
|
147
|
+
if (theme === "light" || theme === "dark") {
|
|
148
|
+
return theme;
|
|
149
|
+
}
|
|
150
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
151
|
+
return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
152
|
+
}
|
|
153
|
+
return "dark";
|
|
154
|
+
}
|
|
155
|
+
function getPalette(theme) {
|
|
156
|
+
const mode = resolveTheme(theme);
|
|
157
|
+
if (mode === "light") {
|
|
158
|
+
return {
|
|
159
|
+
bg: "#ffffff",
|
|
160
|
+
surface: "#f8f8f8",
|
|
161
|
+
surfaceStrong: "#efefef",
|
|
162
|
+
border: "#dddddd",
|
|
163
|
+
text: "#111111",
|
|
164
|
+
textMuted: "#666666",
|
|
165
|
+
textFaint: "#8a8a8a",
|
|
166
|
+
accentBg: "#111111",
|
|
167
|
+
accentText: "#ffffff",
|
|
168
|
+
badgeBg: "#f1f1f1"
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
bg: "#0a0a0a",
|
|
173
|
+
surface: "#111111",
|
|
174
|
+
surfaceStrong: "#181818",
|
|
175
|
+
border: "#2b2b2b",
|
|
176
|
+
text: "#ffffff",
|
|
177
|
+
textMuted: "#a3a3a3",
|
|
178
|
+
textFaint: "#737373",
|
|
179
|
+
accentBg: "#ffffff",
|
|
180
|
+
accentText: "#111111",
|
|
181
|
+
badgeBg: "#171717"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
function normalizeFormat(format) {
|
|
185
|
+
if (format === "floating-video") return "video-widget";
|
|
186
|
+
if (format === "sidebar-display") return "sponsored-card";
|
|
187
|
+
return format;
|
|
188
|
+
}
|
|
189
|
+
function renderVideoWidget(ad, theme) {
|
|
190
|
+
const palette = getPalette(theme);
|
|
191
|
+
const media = ad.video_url ? `<video src="${escapeHtml(ad.video_url)}" autoplay muted loop playsinline preload="auto" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : ad.image_url ? `<img src="${escapeHtml(ad.image_url)}" alt="${escapeHtml(ad.title)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />` : `<div style="position:absolute;inset:0;background:linear-gradient(135deg,#1f2937 0%,#111827 40%,#030712 100%);"></div>`;
|
|
192
|
+
return `
|
|
193
|
+
<div style="position:relative;width:200px;aspect-ratio:9/16;border-radius:16px;overflow:hidden;background:${palette.bg};border:1px solid rgba(255,255,255,0.12);box-shadow:0 20px 60px rgba(0,0,0,0.45);font-family:Space Grotesk,system-ui,sans-serif;">
|
|
194
|
+
${media}
|
|
195
|
+
<div style="position:absolute;top:10px;right:10px;display:flex;gap:8px;z-index:3;">
|
|
196
|
+
<button type="button" data-zc-close style="width:22px;height:22px;border:none;border-radius:999px;background:rgba(0,0,0,0.52);color:#fff;font-size:12px;cursor:pointer;">x</button>
|
|
197
|
+
</div>
|
|
198
|
+
<div style="position:absolute;inset:0;background:linear-gradient(180deg,rgba(0,0,0,0.04) 0%,rgba(0,0,0,0.12) 35%,rgba(0,0,0,0.85) 100%);"></div>
|
|
199
|
+
<div style="position:absolute;left:0;right:0;bottom:0;padding:14px;z-index:2;">
|
|
200
|
+
<div style="display:inline-flex;align-items:center;padding:4px 7px;border-radius:999px;background:rgba(17,17,17,0.72);border:1px solid rgba(255,255,255,0.08);color:#d4d4d4;font-size:9px;font-weight:700;letter-spacing:0.14em;text-transform:uppercase;">Sponsored</div>
|
|
201
|
+
<div style="margin-top:10px;color:#fff;font-size:14px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
202
|
+
${ad.description ? `<div style="margin-top:6px;color:rgba(255,255,255,0.78);font-size:11px;line-height:1.35;">${escapeHtml(ad.description)}</div>` : ""}
|
|
203
|
+
<a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="margin-top:12px;display:block;width:100%;padding:9px 10px;border-radius:10px;background:#ffffff;color:#111111;text-align:center;text-decoration:none;font-size:11px;font-weight:700;">${escapeHtml(ad.cta_text || "Learn More")}</a>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
`;
|
|
207
|
+
}
|
|
208
|
+
function renderTooltipWidget(ad, theme) {
|
|
209
|
+
const palette = getPalette(theme);
|
|
210
|
+
return `
|
|
211
|
+
<div style="width:320px;max-width:100%;padding:14px 15px;border-radius:14px;background:${palette.surface};border:1px solid ${palette.border};box-shadow:0 18px 48px rgba(0,0,0,0.28);font-family:Space Grotesk,system-ui,sans-serif;">
|
|
212
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
|
|
213
|
+
<div style="display:inline-flex;align-items:center;gap:6px;color:${palette.textFaint};font-size:10px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">
|
|
214
|
+
<span style="width:6px;height:6px;border-radius:999px;background:${palette.text};display:inline-block;"></span>
|
|
215
|
+
Sponsored
|
|
216
|
+
</div>
|
|
217
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
218
|
+
</div>
|
|
219
|
+
<div style="margin-top:10px;color:${palette.text};font-size:13px;line-height:1.55;">
|
|
220
|
+
${escapeHtml(ad.description || ad.title)} <a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="color:${palette.text};font-weight:700;text-decoration:underline;text-underline-offset:2px;">${escapeHtml(ad.cta_text || "Learn More")}</a>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
function renderSponsoredCard(ad, theme) {
|
|
226
|
+
const palette = getPalette(theme);
|
|
227
|
+
const media = ad.image_url ? `<img src="${escapeHtml(ad.image_url)}" alt="${escapeHtml(ad.title)}" style="display:block;width:100%;height:92px;object-fit:cover;" />` : `<div style="width:100%;height:92px;background:linear-gradient(135deg,#1f2937 0%,#111827 40%,#030712 100%);"></div>`;
|
|
228
|
+
return `
|
|
229
|
+
<div style="width:176px;border-radius:16px;overflow:hidden;background:${palette.surface};border:1px solid ${palette.border};box-shadow:0 18px 52px rgba(0,0,0,0.32);font-family:Space Grotesk,system-ui,sans-serif;">
|
|
230
|
+
${media}
|
|
231
|
+
<div style="padding:12px;">
|
|
232
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
|
233
|
+
<div style="display:inline-flex;align-items:center;padding:4px 6px;border-radius:999px;background:${palette.badgeBg};color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</div>
|
|
234
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
235
|
+
</div>
|
|
236
|
+
<div style="margin-top:10px;color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
237
|
+
${ad.description ? `<div style="margin-top:6px;color:${palette.textMuted};font-size:11px;line-height:1.35;">${escapeHtml(ad.description)}</div>` : ""}
|
|
238
|
+
<a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="margin-top:12px;display:block;width:100%;padding:8px 10px;border-radius:10px;border:1px solid ${palette.border};background:${palette.surfaceStrong};color:${palette.text};text-align:center;text-decoration:none;font-size:11px;font-weight:700;">${escapeHtml(ad.cta_text || "Learn More")}</a>
|
|
239
|
+
</div>
|
|
240
|
+
</div>
|
|
241
|
+
`;
|
|
242
|
+
}
|
|
243
|
+
function renderInlineText(ad, theme) {
|
|
244
|
+
const palette = getPalette(theme);
|
|
245
|
+
const media = ad.image_url ? `<img src="${escapeHtml(ad.image_url)}" alt="${escapeHtml(ad.title)}" style="width:34px;height:34px;border-radius:10px;object-fit:cover;flex-shrink:0;" />` : `<div style="width:34px;height:34px;border-radius:10px;background:${palette.accentBg};color:${palette.accentText};display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex-shrink:0;">Ad</div>`;
|
|
246
|
+
return `
|
|
247
|
+
<div style="margin:10px 0;border-radius:14px;overflow:hidden;background:${palette.surface};border:1px solid ${palette.border};font-family:Space Grotesk,system-ui,sans-serif;">
|
|
248
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-bottom:1px solid ${palette.border};background:${palette.surfaceStrong};">
|
|
249
|
+
<span style="color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</span>
|
|
250
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
251
|
+
</div>
|
|
252
|
+
<div style="padding:12px;display:flex;gap:10px;align-items:flex-start;">
|
|
253
|
+
${media}
|
|
254
|
+
<div style="min-width:0;flex:1;">
|
|
255
|
+
<div style="color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
256
|
+
${ad.description ? `<div style="margin-top:5px;color:${palette.textMuted};font-size:11px;line-height:1.45;">${escapeHtml(ad.description)}</div>` : ""}
|
|
257
|
+
<a href="${escapeHtml(ad.landing_url)}" target="_blank" rel="noreferrer noopener" data-zc-cta style="margin-top:9px;display:inline-block;padding:7px 10px;border-radius:9px;background:${palette.accentBg};color:${palette.accentText};text-decoration:none;font-size:10px;font-weight:700;">${escapeHtml(ad.cta_text || "Learn More")}</a>
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
`;
|
|
262
|
+
}
|
|
263
|
+
function renderWidgetMarkup(ad, options) {
|
|
264
|
+
const format = normalizeFormat(options.format);
|
|
265
|
+
if (format === "tooltip-ad") {
|
|
266
|
+
return renderTooltipWidget(ad, options.theme);
|
|
267
|
+
}
|
|
268
|
+
if (format === "sponsored-card") {
|
|
269
|
+
return renderSponsoredCard(ad, options.theme);
|
|
270
|
+
}
|
|
271
|
+
if (format === "inline-text") {
|
|
272
|
+
return renderInlineText(ad, options.theme);
|
|
273
|
+
}
|
|
274
|
+
return renderVideoWidget(ad, options.theme);
|
|
275
|
+
}
|
|
276
|
+
|
|
137
277
|
// src/modules/widget.ts
|
|
138
278
|
var POSITION_STYLES = {
|
|
139
279
|
"bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
|
|
@@ -148,6 +288,17 @@ var POSITION_STYLES = {
|
|
|
148
288
|
};
|
|
149
289
|
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
150
290
|
var AUTO_SLOT_ID = "zerocost-auto-slot";
|
|
291
|
+
var CHAT_CONTAINER_SELECTORS = [
|
|
292
|
+
"[data-zerocost-chat]",
|
|
293
|
+
"[data-zc-chat]",
|
|
294
|
+
"[data-chat]",
|
|
295
|
+
"[data-chat-stream]",
|
|
296
|
+
"[data-conversation]",
|
|
297
|
+
"[data-ai-chat]",
|
|
298
|
+
'[role="log"]',
|
|
299
|
+
'[aria-label*="chat" i]',
|
|
300
|
+
'[aria-label*="conversation" i]'
|
|
301
|
+
];
|
|
151
302
|
var WidgetModule = class {
|
|
152
303
|
constructor(client) {
|
|
153
304
|
this.client = client;
|
|
@@ -155,15 +306,17 @@ var WidgetModule = class {
|
|
|
155
306
|
mounted = /* @__PURE__ */ new Map();
|
|
156
307
|
async autoInjectWithConfig(display, widget) {
|
|
157
308
|
try {
|
|
158
|
-
const selected = this.
|
|
309
|
+
const selected = this.resolveSelectedWidgets(display, widget);
|
|
159
310
|
this.clearAutoInjectedSlots();
|
|
160
|
-
if (
|
|
311
|
+
if (selected.length === 0) {
|
|
161
312
|
this.client.log("No enabled widget format found. Skipping injection.");
|
|
162
313
|
return;
|
|
163
314
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
315
|
+
for (const config of selected) {
|
|
316
|
+
this.client.log(`Auto-inject: rendering configured format "${config.format}".`);
|
|
317
|
+
await this.mountSingleFormat(config);
|
|
318
|
+
}
|
|
319
|
+
this.client.log("Auto-inject completed.");
|
|
167
320
|
} catch (err) {
|
|
168
321
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
169
322
|
}
|
|
@@ -177,34 +330,65 @@ var WidgetModule = class {
|
|
|
177
330
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
178
331
|
}
|
|
179
332
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
333
|
+
resolveSelectedWidgets(display, widget) {
|
|
334
|
+
const configs = this.normalizeConfigs(display);
|
|
335
|
+
const selected = [];
|
|
336
|
+
if (widget?.enabled && widget?.format && widget.format !== "inline-text") {
|
|
337
|
+
selected.push({
|
|
183
338
|
format: widget.format,
|
|
184
339
|
position: widget.position || "bottom-right",
|
|
185
340
|
theme: widget.theme || "dark",
|
|
186
341
|
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
187
342
|
enabled: true
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
343
|
+
});
|
|
344
|
+
} else {
|
|
345
|
+
for (const format of FORMAT_PRIORITY) {
|
|
346
|
+
if (format === "inline-text") continue;
|
|
347
|
+
const config = configs[format];
|
|
348
|
+
if (config?.enabled) {
|
|
349
|
+
selected.push({
|
|
350
|
+
format,
|
|
351
|
+
position: config.position,
|
|
352
|
+
theme: config.theme,
|
|
353
|
+
autoplay: config.autoplay,
|
|
354
|
+
enabled: true
|
|
355
|
+
});
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
201
358
|
}
|
|
202
359
|
}
|
|
203
|
-
|
|
360
|
+
const inlineConfig = configs["inline-text"];
|
|
361
|
+
if (widget?.enabled && widget?.format === "inline-text") {
|
|
362
|
+
selected.push({
|
|
363
|
+
format: "inline-text",
|
|
364
|
+
position: widget.position || inlineConfig?.position || "after-paragraph-1",
|
|
365
|
+
theme: widget.theme || inlineConfig?.theme || "dark",
|
|
366
|
+
autoplay: false,
|
|
367
|
+
enabled: true
|
|
368
|
+
});
|
|
369
|
+
} else if (inlineConfig?.enabled) {
|
|
370
|
+
selected.push({
|
|
371
|
+
format: "inline-text",
|
|
372
|
+
position: inlineConfig.position,
|
|
373
|
+
theme: inlineConfig.theme,
|
|
374
|
+
autoplay: false,
|
|
375
|
+
enabled: true
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return selected;
|
|
204
379
|
}
|
|
205
380
|
normalizeConfigs(display) {
|
|
206
|
-
if (display && typeof display === "object" && "video-widget" in display) {
|
|
207
|
-
|
|
381
|
+
if (display && typeof display === "object" && ("video-widget" in display || "floating-video" in display)) {
|
|
382
|
+
const source = display;
|
|
383
|
+
const videoConfig = source["video-widget"] || source["floating-video"];
|
|
384
|
+
const sponsoredCardConfig = source["sponsored-card"] || source["sidebar-display"];
|
|
385
|
+
return {
|
|
386
|
+
"video-widget": videoConfig || { position: "bottom-right", theme: "dark", autoplay: true, enabled: true },
|
|
387
|
+
"tooltip-ad": source["tooltip-ad"] || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
388
|
+
"sponsored-card": sponsoredCardConfig || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
389
|
+
"sidebar-display": sponsoredCardConfig || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
390
|
+
"inline-text": source["inline-text"] || { position: "after-paragraph-1", theme: "dark", autoplay: false, enabled: false }
|
|
391
|
+
};
|
|
208
392
|
}
|
|
209
393
|
const pos = display?.position || "bottom-right";
|
|
210
394
|
const theme = display?.theme || "dark";
|
|
@@ -224,31 +408,32 @@ var WidgetModule = class {
|
|
|
224
408
|
return;
|
|
225
409
|
}
|
|
226
410
|
if (!isInline) {
|
|
227
|
-
let
|
|
228
|
-
if (!
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
document.body.appendChild(
|
|
411
|
+
let element = document.getElementById(AUTO_SLOT_ID);
|
|
412
|
+
if (!element) {
|
|
413
|
+
element = document.createElement("div");
|
|
414
|
+
element.id = AUTO_SLOT_ID;
|
|
415
|
+
document.body.appendChild(element);
|
|
232
416
|
}
|
|
233
417
|
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
234
|
-
const maxW = config.format === "video-widget" ? "max-width:200px;" :
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
418
|
+
const maxW = config.format === "video-widget" ? "max-width:200px;" : "max-width:176px;";
|
|
419
|
+
element.setAttribute("style", `${posStyle}${maxW}`);
|
|
420
|
+
element.setAttribute("data-zerocost", "");
|
|
421
|
+
element.setAttribute("data-format", config.format);
|
|
238
422
|
}
|
|
239
423
|
await this.mount(targetElementId, {
|
|
240
424
|
format: config.format,
|
|
241
|
-
refreshInterval:
|
|
242
|
-
// Config polling handles re-rendering
|
|
425
|
+
refreshInterval: SDK_WIDGET_REFRESH_MS / 1e3,
|
|
243
426
|
theme: config.theme,
|
|
244
427
|
autoplay: config.autoplay,
|
|
245
428
|
position: config.position
|
|
246
429
|
});
|
|
247
430
|
}
|
|
248
431
|
ensureInlineTarget(position) {
|
|
432
|
+
const chatContainer = this.findChatContainer();
|
|
433
|
+
if (!chatContainer) return null;
|
|
249
434
|
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
250
435
|
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
251
|
-
const paragraphs = Array.from(
|
|
436
|
+
const paragraphs = Array.from(chatContainer.querySelectorAll("p"));
|
|
252
437
|
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
253
438
|
if (!anchor) return null;
|
|
254
439
|
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
@@ -265,11 +450,22 @@ var WidgetModule = class {
|
|
|
265
450
|
}
|
|
266
451
|
return inlineId;
|
|
267
452
|
}
|
|
453
|
+
findChatContainer() {
|
|
454
|
+
for (const selector of CHAT_CONTAINER_SELECTORS) {
|
|
455
|
+
const container = document.querySelector(selector);
|
|
456
|
+
if (container) return container;
|
|
457
|
+
}
|
|
458
|
+
const semanticContainers = Array.from(document.querySelectorAll("section, main, div, article"));
|
|
459
|
+
return semanticContainers.find((node) => {
|
|
460
|
+
const marker = `${node.id} ${node.className || ""}`.toLowerCase();
|
|
461
|
+
return /chat|conversation|assistant|messages|thread/.test(marker);
|
|
462
|
+
}) || null;
|
|
463
|
+
}
|
|
268
464
|
async mount(targetElementId, options = {}) {
|
|
269
|
-
const
|
|
270
|
-
if (!
|
|
465
|
+
const element = document.getElementById(targetElementId);
|
|
466
|
+
if (!element) return;
|
|
271
467
|
if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
|
|
272
|
-
const refreshMs = (options.refreshInterval ??
|
|
468
|
+
const refreshMs = (options.refreshInterval ?? SDK_WIDGET_REFRESH_MS / 1e3) * 1e3;
|
|
273
469
|
const theme = options.theme || "dark";
|
|
274
470
|
const format = options.format || "video-widget";
|
|
275
471
|
const autoplay = options.autoplay ?? format === "video-widget";
|
|
@@ -283,15 +479,15 @@ var WidgetModule = class {
|
|
|
283
479
|
};
|
|
284
480
|
const data = await this.client.request("/serve-widget", body);
|
|
285
481
|
const ad = data.ad;
|
|
286
|
-
if (!ad
|
|
482
|
+
if (!ad) {
|
|
287
483
|
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
288
|
-
|
|
484
|
+
element.innerHTML = "";
|
|
289
485
|
return;
|
|
290
486
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
this.ensureVideoPlayback(
|
|
294
|
-
const ctas =
|
|
487
|
+
element.innerHTML = renderWidgetMarkup(ad, { format, theme });
|
|
488
|
+
element.setAttribute("data-zerocost-ad-id", ad.id);
|
|
489
|
+
this.ensureVideoPlayback(element);
|
|
490
|
+
const ctas = element.querySelectorAll("[data-zc-cta]");
|
|
295
491
|
ctas.forEach((cta) => {
|
|
296
492
|
cta.addEventListener("click", () => {
|
|
297
493
|
this.client.request("/track-event", {
|
|
@@ -301,11 +497,11 @@ var WidgetModule = class {
|
|
|
301
497
|
});
|
|
302
498
|
});
|
|
303
499
|
});
|
|
304
|
-
const closeBtn =
|
|
500
|
+
const closeBtn = element.querySelector("[data-zc-close]");
|
|
305
501
|
if (closeBtn) {
|
|
306
|
-
closeBtn.addEventListener("click", (
|
|
307
|
-
|
|
308
|
-
|
|
502
|
+
closeBtn.addEventListener("click", (event) => {
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
event.stopPropagation();
|
|
309
505
|
this.unmount(targetElementId);
|
|
310
506
|
});
|
|
311
507
|
}
|
|
@@ -342,8 +538,8 @@ var WidgetModule = class {
|
|
|
342
538
|
unmount(targetElementId) {
|
|
343
539
|
const slot = this.mounted.get(targetElementId);
|
|
344
540
|
if (slot?.interval) clearInterval(slot.interval);
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
541
|
+
const element = document.getElementById(targetElementId);
|
|
542
|
+
if (element) element.remove();
|
|
347
543
|
this.mounted.delete(targetElementId);
|
|
348
544
|
}
|
|
349
545
|
unmountAll() {
|
|
@@ -362,59 +558,99 @@ var LLMDataModule = class {
|
|
|
362
558
|
clickHandler = null;
|
|
363
559
|
errorHandler = null;
|
|
364
560
|
fetchOriginal = null;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
561
|
+
mutationObserver = null;
|
|
562
|
+
conversationScanTimer = null;
|
|
563
|
+
isSampledSession = false;
|
|
564
|
+
observedConversations = /* @__PURE__ */ new Map();
|
|
565
|
+
conversationCounter = 0;
|
|
368
566
|
start(config) {
|
|
567
|
+
this.stop();
|
|
369
568
|
this.config = config;
|
|
370
569
|
this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
|
|
371
|
-
|
|
570
|
+
this.isSampledSession = Math.random() * 100 <= config.sampleRate;
|
|
571
|
+
if (!this.isSampledSession) {
|
|
372
572
|
this.client.log("LLMData: session not sampled, skipping");
|
|
373
573
|
return;
|
|
374
574
|
}
|
|
375
575
|
if (config.uiInteractions) this.captureUIInteractions();
|
|
376
|
-
if (config.textPrompts)
|
|
576
|
+
if (config.textPrompts) {
|
|
577
|
+
this.interceptPrompts();
|
|
578
|
+
this.captureConversationSurfaces();
|
|
579
|
+
this.scheduleConversationScan();
|
|
580
|
+
}
|
|
377
581
|
if (config.apiErrors) this.captureAPIErrors();
|
|
378
582
|
this.flushInterval = setInterval(() => this.flush(), 1e4);
|
|
379
583
|
}
|
|
380
584
|
stop() {
|
|
381
|
-
if (this.flushInterval)
|
|
585
|
+
if (this.flushInterval) {
|
|
586
|
+
clearInterval(this.flushInterval);
|
|
587
|
+
this.flushInterval = null;
|
|
588
|
+
}
|
|
382
589
|
if (this.clickHandler) {
|
|
383
590
|
document.removeEventListener("click", this.clickHandler, true);
|
|
591
|
+
this.clickHandler = null;
|
|
384
592
|
}
|
|
385
593
|
if (this.errorHandler) {
|
|
386
594
|
window.removeEventListener("error", this.errorHandler);
|
|
595
|
+
this.errorHandler = null;
|
|
387
596
|
}
|
|
388
597
|
if (this.fetchOriginal) {
|
|
389
598
|
window.fetch = this.fetchOriginal;
|
|
599
|
+
this.fetchOriginal = null;
|
|
390
600
|
}
|
|
391
|
-
this.
|
|
601
|
+
if (this.mutationObserver) {
|
|
602
|
+
this.mutationObserver.disconnect();
|
|
603
|
+
this.mutationObserver = null;
|
|
604
|
+
}
|
|
605
|
+
if (this.conversationScanTimer) {
|
|
606
|
+
clearTimeout(this.conversationScanTimer);
|
|
607
|
+
this.conversationScanTimer = null;
|
|
608
|
+
}
|
|
609
|
+
this.observedConversations.clear();
|
|
610
|
+
this.isSampledSession = false;
|
|
611
|
+
void this.flush();
|
|
392
612
|
this.config = null;
|
|
393
613
|
}
|
|
394
|
-
/**
|
|
395
|
-
* Manually track an LLM prompt/response pair (for startups that want to
|
|
396
|
-
* explicitly send their AI interactions).
|
|
397
|
-
*/
|
|
398
614
|
trackPrompt(prompt, response, meta) {
|
|
399
|
-
if (!this.
|
|
615
|
+
if (!this.canCapture("textPrompts")) return;
|
|
400
616
|
this.pushEvent("llm_prompt", {
|
|
401
617
|
prompt: this.scrub(prompt),
|
|
402
618
|
response: response ? this.scrub(response) : void 0,
|
|
403
|
-
|
|
619
|
+
source: "manual",
|
|
620
|
+
...this.sanitizeMeta(meta)
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
trackConversation(messages, meta) {
|
|
624
|
+
if (!this.canCapture("textPrompts")) return;
|
|
625
|
+
const cleanedMessages = messages.map((message) => ({
|
|
626
|
+
role: this.normalizeRole(message.role),
|
|
627
|
+
content: this.scrub(message.content || ""),
|
|
628
|
+
...message.name ? { name: this.scrub(message.name) } : {}
|
|
629
|
+
})).filter((message) => message.content);
|
|
630
|
+
if (cleanedMessages.length === 0) return;
|
|
631
|
+
const sanitizedMeta = this.sanitizeMeta(meta);
|
|
632
|
+
const conversationId = this.getConversationId(sanitizedMeta);
|
|
633
|
+
cleanedMessages.forEach((message, index) => {
|
|
634
|
+
this.pushConversationMessage(message, {
|
|
635
|
+
conversationId,
|
|
636
|
+
turnIndex: index,
|
|
637
|
+
source: "manual",
|
|
638
|
+
...sanitizedMeta
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
this.pushConversationSummary(conversationId, cleanedMessages, {
|
|
642
|
+
source: "manual",
|
|
643
|
+
...sanitizedMeta
|
|
404
644
|
});
|
|
405
645
|
}
|
|
406
|
-
/**
|
|
407
|
-
* Manually track an API error.
|
|
408
|
-
*/
|
|
409
646
|
trackError(endpoint, status, message) {
|
|
410
|
-
if (!this.
|
|
647
|
+
if (!this.canCapture("apiErrors")) return;
|
|
411
648
|
this.pushEvent("api_error", {
|
|
412
649
|
endpoint,
|
|
413
650
|
status,
|
|
414
651
|
message: message ? this.scrub(message) : void 0
|
|
415
652
|
});
|
|
416
653
|
}
|
|
417
|
-
// ── Private ──
|
|
418
654
|
captureUIInteractions() {
|
|
419
655
|
this.clickHandler = (e) => {
|
|
420
656
|
const target = e.target;
|
|
@@ -438,44 +674,136 @@ var LLMDataModule = class {
|
|
|
438
674
|
}
|
|
439
675
|
interceptPrompts() {
|
|
440
676
|
this.fetchOriginal = window.fetch;
|
|
441
|
-
const self = this;
|
|
442
677
|
const origFetch = window.fetch;
|
|
678
|
+
const self = this;
|
|
443
679
|
window.fetch = async function(input, init) {
|
|
444
680
|
const fetchInput = input instanceof URL ? input.toString() : input;
|
|
445
681
|
const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
|
|
446
682
|
const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
|
|
447
|
-
if (!isLLM)
|
|
683
|
+
if (!isLLM) {
|
|
684
|
+
return origFetch.call(window, fetchInput, init);
|
|
685
|
+
}
|
|
686
|
+
const requestMeta = self.extractRequestMeta(url, init);
|
|
687
|
+
if (requestMeta.messages.length > 0) {
|
|
688
|
+
requestMeta.messages.forEach((message, index) => {
|
|
689
|
+
self.pushConversationMessage(message, {
|
|
690
|
+
conversationId: requestMeta.conversationId,
|
|
691
|
+
turnIndex: index,
|
|
692
|
+
source: "network-request",
|
|
693
|
+
endpoint: requestMeta.endpoint,
|
|
694
|
+
requestId: requestMeta.requestId,
|
|
695
|
+
...requestMeta.meta
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
448
699
|
try {
|
|
449
700
|
const res = await origFetch.call(window, fetchInput, init);
|
|
450
701
|
const clone = res.clone();
|
|
451
|
-
let reqBody;
|
|
452
|
-
if (init?.body && typeof init.body === "string") {
|
|
453
|
-
try {
|
|
454
|
-
const parsed = JSON.parse(init.body);
|
|
455
|
-
reqBody = JSON.stringify(parsed.messages || parsed.prompt || "").slice(0, 500);
|
|
456
|
-
} catch {
|
|
457
|
-
reqBody = void 0;
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
702
|
clone.text().then((text) => {
|
|
703
|
+
const responseMessages = self.extractResponseMessages(text);
|
|
704
|
+
if (responseMessages.length > 0) {
|
|
705
|
+
responseMessages.forEach((message, index) => {
|
|
706
|
+
self.pushConversationMessage(message, {
|
|
707
|
+
conversationId: requestMeta.conversationId,
|
|
708
|
+
turnIndex: requestMeta.messages.length + index,
|
|
709
|
+
source: "network-response",
|
|
710
|
+
endpoint: requestMeta.endpoint,
|
|
711
|
+
requestId: requestMeta.requestId,
|
|
712
|
+
status: res.status,
|
|
713
|
+
...requestMeta.meta
|
|
714
|
+
});
|
|
715
|
+
});
|
|
716
|
+
self.pushConversationSummary(
|
|
717
|
+
requestMeta.conversationId,
|
|
718
|
+
[...requestMeta.messages, ...responseMessages],
|
|
719
|
+
{
|
|
720
|
+
source: "network",
|
|
721
|
+
endpoint: requestMeta.endpoint,
|
|
722
|
+
requestId: requestMeta.requestId,
|
|
723
|
+
status: res.status,
|
|
724
|
+
...requestMeta.meta
|
|
725
|
+
}
|
|
726
|
+
);
|
|
727
|
+
}
|
|
461
728
|
self.pushEvent("llm_prompt", {
|
|
462
|
-
endpoint:
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
729
|
+
endpoint: requestMeta.endpoint,
|
|
730
|
+
conversationId: requestMeta.conversationId,
|
|
731
|
+
request: requestMeta.promptPreview,
|
|
732
|
+
response: self.scrub(text.slice(0, 1200)),
|
|
733
|
+
status: res.status,
|
|
734
|
+
source: "network",
|
|
735
|
+
requestId: requestMeta.requestId,
|
|
736
|
+
...requestMeta.meta
|
|
466
737
|
});
|
|
467
738
|
}).catch(() => {
|
|
468
739
|
});
|
|
469
740
|
return res;
|
|
470
|
-
} catch (
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
471
743
|
self.pushEvent("api_error", {
|
|
472
|
-
endpoint:
|
|
473
|
-
message: self.scrub(
|
|
744
|
+
endpoint: requestMeta.endpoint,
|
|
745
|
+
message: self.scrub(message),
|
|
746
|
+
requestId: requestMeta.requestId
|
|
474
747
|
});
|
|
475
|
-
throw
|
|
748
|
+
throw error;
|
|
476
749
|
}
|
|
477
750
|
};
|
|
478
751
|
}
|
|
752
|
+
captureConversationSurfaces() {
|
|
753
|
+
if (typeof MutationObserver === "undefined" || typeof document === "undefined") return;
|
|
754
|
+
this.mutationObserver = new MutationObserver(() => this.scheduleConversationScan());
|
|
755
|
+
this.mutationObserver.observe(document.body, {
|
|
756
|
+
childList: true,
|
|
757
|
+
subtree: true,
|
|
758
|
+
characterData: true
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
scheduleConversationScan() {
|
|
762
|
+
if (this.conversationScanTimer) {
|
|
763
|
+
clearTimeout(this.conversationScanTimer);
|
|
764
|
+
}
|
|
765
|
+
this.conversationScanTimer = setTimeout(() => {
|
|
766
|
+
this.conversationScanTimer = null;
|
|
767
|
+
this.scanConversationSurfaces();
|
|
768
|
+
}, 300);
|
|
769
|
+
}
|
|
770
|
+
scanConversationSurfaces() {
|
|
771
|
+
const containers = this.findConversationContainers();
|
|
772
|
+
if (containers.length === 0) return;
|
|
773
|
+
containers.forEach((container) => {
|
|
774
|
+
const snapshot = this.buildConversationSnapshot(container);
|
|
775
|
+
if (!snapshot || snapshot.messages.length === 0) return;
|
|
776
|
+
const existing = this.observedConversations.get(snapshot.conversationId);
|
|
777
|
+
const newMessages = snapshot.messages.map((message, index) => ({
|
|
778
|
+
message,
|
|
779
|
+
index,
|
|
780
|
+
fingerprint: this.getMessageFingerprint(snapshot.conversationId, message, index)
|
|
781
|
+
})).filter(({ fingerprint }) => !existing?.messageFingerprints.has(fingerprint));
|
|
782
|
+
if (newMessages.length === 0 && existing?.signature === snapshot.signature) {
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
newMessages.forEach(({ message, index, fingerprint }) => {
|
|
786
|
+
this.pushConversationMessage(message, {
|
|
787
|
+
conversationId: snapshot.conversationId,
|
|
788
|
+
turnIndex: index,
|
|
789
|
+
source: "dom",
|
|
790
|
+
conversationTitle: snapshot.title,
|
|
791
|
+
pageTitle: document.title,
|
|
792
|
+
path: location.pathname
|
|
793
|
+
});
|
|
794
|
+
snapshot.messageFingerprints.add(fingerprint);
|
|
795
|
+
});
|
|
796
|
+
if (!existing || existing.signature !== snapshot.signature) {
|
|
797
|
+
this.pushConversationSummary(snapshot.conversationId, snapshot.messages, {
|
|
798
|
+
source: "dom",
|
|
799
|
+
conversationTitle: snapshot.title,
|
|
800
|
+
pageTitle: document.title,
|
|
801
|
+
path: location.pathname
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
this.observedConversations.set(snapshot.conversationId, snapshot);
|
|
805
|
+
});
|
|
806
|
+
}
|
|
479
807
|
captureAPIErrors() {
|
|
480
808
|
this.errorHandler = (e) => {
|
|
481
809
|
this.pushEvent("api_error", {
|
|
@@ -487,9 +815,37 @@ var LLMDataModule = class {
|
|
|
487
815
|
};
|
|
488
816
|
window.addEventListener("error", this.errorHandler);
|
|
489
817
|
}
|
|
818
|
+
pushConversationMessage(message, meta) {
|
|
819
|
+
const content = this.scrub(message.content || "");
|
|
820
|
+
if (!content) return;
|
|
821
|
+
this.pushEvent("llm_message", {
|
|
822
|
+
role: this.normalizeRole(message.role),
|
|
823
|
+
content,
|
|
824
|
+
...message.name ? { name: this.scrub(message.name) } : {},
|
|
825
|
+
...meta
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
pushConversationSummary(conversationId, messages, meta) {
|
|
829
|
+
const preview = messages.map((message) => ({
|
|
830
|
+
role: this.normalizeRole(message.role),
|
|
831
|
+
content: this.scrub(message.content || ""),
|
|
832
|
+
...message.name ? { name: this.scrub(message.name) } : {}
|
|
833
|
+
})).filter((message) => message.content).slice(0, 12);
|
|
834
|
+
if (preview.length === 0) return;
|
|
835
|
+
this.pushEvent("llm_conversation", {
|
|
836
|
+
conversationId,
|
|
837
|
+
messageCount: preview.length,
|
|
838
|
+
roles: Array.from(new Set(preview.map((message) => message.role))),
|
|
839
|
+
preview,
|
|
840
|
+
...meta
|
|
841
|
+
});
|
|
842
|
+
}
|
|
490
843
|
pushEvent(type, data) {
|
|
844
|
+
if (!this.isSampledSession) return;
|
|
491
845
|
this.buffer.push({ type, data, timestamp: Date.now() });
|
|
492
|
-
if (this.buffer.length >= 50)
|
|
846
|
+
if (this.buffer.length >= 50) {
|
|
847
|
+
void this.flush();
|
|
848
|
+
}
|
|
493
849
|
}
|
|
494
850
|
async flush() {
|
|
495
851
|
if (this.buffer.length === 0) return;
|
|
@@ -502,25 +858,286 @@ var LLMDataModule = class {
|
|
|
502
858
|
this.buffer.unshift(...events);
|
|
503
859
|
}
|
|
504
860
|
}
|
|
505
|
-
/** Basic PII scrubbing */
|
|
506
861
|
scrub(text) {
|
|
507
|
-
return text.replace(
|
|
862
|
+
return text.replace(/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "[EMAIL]").replace(/\+?\d[\d\s().-]{7,}\d/g, "[PHONE]").replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]").replace(/\b(?:\d[ -]*?){13,19}\b/g, "[CARD]").replace(/\b(sk|pk|api|key|secret|token)[-_]?[a-zA-Z0-9]{16,}\b/gi, "[API_KEY]").replace(/\b[A-F0-9]{32,}\b/gi, "[TOKEN]").replace(/\b(?:\d{1,3}\.){3}\d{1,3}\b/g, "[IP]");
|
|
508
863
|
}
|
|
509
864
|
getPath(el) {
|
|
510
865
|
const parts = [];
|
|
511
|
-
let
|
|
512
|
-
while (
|
|
513
|
-
let
|
|
514
|
-
if (
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
866
|
+
let current = el;
|
|
867
|
+
while (current && parts.length < 5) {
|
|
868
|
+
let segment = current.tagName?.toLowerCase() || "";
|
|
869
|
+
if (current.id) {
|
|
870
|
+
segment += `#${current.id}`;
|
|
871
|
+
} else if (typeof current.className === "string" && current.className) {
|
|
872
|
+
const cls = current.className.split(" ")[0];
|
|
873
|
+
if (cls) segment += `.${cls}`;
|
|
518
874
|
}
|
|
519
|
-
parts.unshift(
|
|
520
|
-
|
|
875
|
+
parts.unshift(segment);
|
|
876
|
+
current = current.parentElement;
|
|
521
877
|
}
|
|
522
878
|
return parts.join(" > ");
|
|
523
879
|
}
|
|
880
|
+
canCapture(setting) {
|
|
881
|
+
return Boolean(this.config?.[setting] && this.isSampledSession);
|
|
882
|
+
}
|
|
883
|
+
sanitizeMeta(meta) {
|
|
884
|
+
if (!meta) return {};
|
|
885
|
+
return Object.fromEntries(
|
|
886
|
+
Object.entries(meta).map(([key, value]) => {
|
|
887
|
+
if (typeof value === "string") return [key, this.scrub(value)];
|
|
888
|
+
if (Array.isArray(value)) {
|
|
889
|
+
return [key, value.map((item) => typeof item === "string" ? this.scrub(item) : item)];
|
|
890
|
+
}
|
|
891
|
+
return [key, value];
|
|
892
|
+
})
|
|
893
|
+
);
|
|
894
|
+
}
|
|
895
|
+
extractRequestMeta(url, init) {
|
|
896
|
+
const endpoint = this.safePathname(url);
|
|
897
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
898
|
+
const parsed = this.parseRequestBody(init?.body);
|
|
899
|
+
const conversationId = this.getConversationId(parsed.meta);
|
|
900
|
+
return {
|
|
901
|
+
endpoint,
|
|
902
|
+
requestId,
|
|
903
|
+
conversationId,
|
|
904
|
+
messages: parsed.messages,
|
|
905
|
+
promptPreview: parsed.messages.map((message) => `[${message.role}] ${message.content}`).join("\n").slice(0, 1200),
|
|
906
|
+
meta: parsed.meta
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
parseRequestBody(body) {
|
|
910
|
+
if (!body || typeof body !== "string") {
|
|
911
|
+
return { messages: [], meta: {} };
|
|
912
|
+
}
|
|
913
|
+
try {
|
|
914
|
+
const parsed = JSON.parse(body);
|
|
915
|
+
const messages = this.extractMessagesFromUnknown(parsed);
|
|
916
|
+
const meta = {};
|
|
917
|
+
const keys = ["model", "conversationId", "conversation_id", "threadId", "thread_id", "sessionId", "session_id", "topic", "domain", "workflow"];
|
|
918
|
+
keys.forEach((key) => {
|
|
919
|
+
const value = parsed[key];
|
|
920
|
+
if (typeof value === "string" && value.trim()) {
|
|
921
|
+
meta[key] = this.scrub(value.trim().slice(0, 120));
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
return { messages, meta };
|
|
925
|
+
} catch {
|
|
926
|
+
return {
|
|
927
|
+
messages: [{ role: "user", content: this.scrub(body.slice(0, 2e3)) }],
|
|
928
|
+
meta: {}
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
extractMessagesFromUnknown(value) {
|
|
933
|
+
if (!value || typeof value !== "object") return [];
|
|
934
|
+
const record = value;
|
|
935
|
+
if (Array.isArray(record.messages)) {
|
|
936
|
+
return record.messages.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item?.content));
|
|
937
|
+
}
|
|
938
|
+
if (typeof record.prompt === "string") {
|
|
939
|
+
return [{ role: "user", content: this.scrub(record.prompt.slice(0, 2e3)) }];
|
|
940
|
+
}
|
|
941
|
+
if (Array.isArray(record.input)) {
|
|
942
|
+
return record.input.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item?.content));
|
|
943
|
+
}
|
|
944
|
+
if (typeof record.input === "string") {
|
|
945
|
+
return [{ role: "user", content: this.scrub(record.input.slice(0, 2e3)) }];
|
|
946
|
+
}
|
|
947
|
+
return [];
|
|
948
|
+
}
|
|
949
|
+
normalizeMessage(input) {
|
|
950
|
+
if (typeof input === "string") {
|
|
951
|
+
const content2 = this.scrub(input.slice(0, 2e3));
|
|
952
|
+
return content2 ? { role: "user", content: content2 } : null;
|
|
953
|
+
}
|
|
954
|
+
if (!input || typeof input !== "object") return null;
|
|
955
|
+
const record = input;
|
|
956
|
+
const role = this.normalizeRole(record.role);
|
|
957
|
+
const content = this.extractContent(record.content ?? record.text ?? record.message);
|
|
958
|
+
const name = typeof record.name === "string" ? this.scrub(record.name.slice(0, 120)) : void 0;
|
|
959
|
+
if (!content) return null;
|
|
960
|
+
return { role, content, ...name ? { name } : {} };
|
|
961
|
+
}
|
|
962
|
+
extractContent(value) {
|
|
963
|
+
if (typeof value === "string") return this.scrub(value.slice(0, 4e3));
|
|
964
|
+
if (Array.isArray(value)) {
|
|
965
|
+
return value.map((item) => {
|
|
966
|
+
if (typeof item === "string") return this.scrub(item);
|
|
967
|
+
if (item && typeof item === "object") {
|
|
968
|
+
const record = item;
|
|
969
|
+
if (typeof record.text === "string") return this.scrub(record.text);
|
|
970
|
+
}
|
|
971
|
+
return "";
|
|
972
|
+
}).filter(Boolean).join("\n").slice(0, 4e3);
|
|
973
|
+
}
|
|
974
|
+
return "";
|
|
975
|
+
}
|
|
976
|
+
extractResponseMessages(text) {
|
|
977
|
+
const trimmed = text.trim();
|
|
978
|
+
if (!trimmed) return [];
|
|
979
|
+
try {
|
|
980
|
+
const parsed = JSON.parse(trimmed);
|
|
981
|
+
const messages = this.extractAssistantMessages(parsed);
|
|
982
|
+
if (messages.length > 0) return messages;
|
|
983
|
+
} catch {
|
|
984
|
+
}
|
|
985
|
+
return [{ role: "assistant", content: this.scrub(trimmed.slice(0, 4e3)) }];
|
|
986
|
+
}
|
|
987
|
+
extractAssistantMessages(value) {
|
|
988
|
+
if (!value || typeof value !== "object") return [];
|
|
989
|
+
const record = value;
|
|
990
|
+
const direct = this.normalizeMessage(record.message ?? record.output ?? record.response ?? record.answer);
|
|
991
|
+
if (direct) {
|
|
992
|
+
return [{ ...direct, role: direct.role === "user" ? "assistant" : direct.role }];
|
|
993
|
+
}
|
|
994
|
+
if (Array.isArray(record.choices)) {
|
|
995
|
+
return record.choices.map((choice) => {
|
|
996
|
+
if (!choice || typeof choice !== "object") return null;
|
|
997
|
+
const payload = choice;
|
|
998
|
+
const normalized = this.normalizeMessage(payload.message ?? payload.delta ?? payload.text);
|
|
999
|
+
if (!normalized) return null;
|
|
1000
|
+
return {
|
|
1001
|
+
...normalized,
|
|
1002
|
+
role: normalized.role === "user" ? "assistant" : normalized.role
|
|
1003
|
+
};
|
|
1004
|
+
}).filter((item) => Boolean(item && item.content));
|
|
1005
|
+
}
|
|
1006
|
+
if (Array.isArray(record.messages)) {
|
|
1007
|
+
return record.messages.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item && item.content)).map((item) => ({
|
|
1008
|
+
...item,
|
|
1009
|
+
role: item.role === "user" ? "assistant" : item.role
|
|
1010
|
+
}));
|
|
1011
|
+
}
|
|
1012
|
+
return [];
|
|
1013
|
+
}
|
|
1014
|
+
findConversationContainers() {
|
|
1015
|
+
const selectors = [
|
|
1016
|
+
"[data-chat-thread]",
|
|
1017
|
+
"[data-conversation]",
|
|
1018
|
+
'[data-testid*="chat" i]',
|
|
1019
|
+
'[data-testid*="conversation" i]',
|
|
1020
|
+
'[aria-label*="chat" i]',
|
|
1021
|
+
'[aria-label*="conversation" i]',
|
|
1022
|
+
'[role="log"]',
|
|
1023
|
+
'[role="feed"]',
|
|
1024
|
+
"main"
|
|
1025
|
+
];
|
|
1026
|
+
const found = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector))).filter((element) => this.looksLikeConversationContainer(element));
|
|
1027
|
+
return Array.from(new Set(found)).slice(0, 4);
|
|
1028
|
+
}
|
|
1029
|
+
looksLikeConversationContainer(element) {
|
|
1030
|
+
const marker = [
|
|
1031
|
+
element.dataset.chatThread,
|
|
1032
|
+
element.dataset.conversation,
|
|
1033
|
+
element.getAttribute("aria-label"),
|
|
1034
|
+
element.getAttribute("data-testid"),
|
|
1035
|
+
typeof element.className === "string" ? element.className : "",
|
|
1036
|
+
element.id
|
|
1037
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1038
|
+
if (/chat|conversation|assistant|thread|messages/.test(marker)) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
const messageNodes = element.querySelectorAll('[data-message-author-role], [data-role], [role="article"], article, [data-testid*="message" i]');
|
|
1042
|
+
return messageNodes.length >= 2;
|
|
1043
|
+
}
|
|
1044
|
+
buildConversationSnapshot(container) {
|
|
1045
|
+
const messageNodes = Array.from(
|
|
1046
|
+
container.querySelectorAll('[data-message-author-role], [data-role], [role="article"], article, [data-testid*="message" i], [class*="message"], [class*="chat"]')
|
|
1047
|
+
).filter((node) => this.isUsableMessageNode(node)).slice(0, 80);
|
|
1048
|
+
const messages = messageNodes.map((node) => this.messageFromNode(node)).filter((message) => Boolean(message?.content));
|
|
1049
|
+
if (messages.length < 2) return null;
|
|
1050
|
+
const title = this.extractConversationTitle(container);
|
|
1051
|
+
const conversationId = this.getConversationId({ conversationTitle: title, path: location.pathname }, container);
|
|
1052
|
+
const signature = messages.map((message) => `${message.role}:${message.content}`).join("|").slice(0, 6e3);
|
|
1053
|
+
return {
|
|
1054
|
+
conversationId,
|
|
1055
|
+
signature,
|
|
1056
|
+
messageFingerprints: new Set(messages.map((message, index) => this.getMessageFingerprint(conversationId, message, index))),
|
|
1057
|
+
messages,
|
|
1058
|
+
title
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
isUsableMessageNode(node) {
|
|
1062
|
+
const text = node.innerText?.trim() || "";
|
|
1063
|
+
if (!text || text.length < 2) return false;
|
|
1064
|
+
const marker = `${typeof node.className === "string" ? node.className : ""} ${node.getAttribute("data-testid") || ""} ${node.getAttribute("aria-label") || ""}`.toLowerCase();
|
|
1065
|
+
if (/input|textarea|composer|toolbar|button|copy code/.test(marker)) {
|
|
1066
|
+
return false;
|
|
1067
|
+
}
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
messageFromNode(node) {
|
|
1071
|
+
const text = this.scrub((node.innerText || "").trim().slice(0, 4e3));
|
|
1072
|
+
if (!text) return null;
|
|
1073
|
+
return {
|
|
1074
|
+
role: this.detectRole(node, text),
|
|
1075
|
+
content: text
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
detectRole(node, text) {
|
|
1079
|
+
const marker = [
|
|
1080
|
+
node.dataset.messageAuthorRole,
|
|
1081
|
+
node.dataset.role,
|
|
1082
|
+
node.getAttribute("data-role"),
|
|
1083
|
+
node.getAttribute("aria-label"),
|
|
1084
|
+
typeof node.className === "string" ? node.className : "",
|
|
1085
|
+
node.id,
|
|
1086
|
+
node.closest("[data-message-author-role]")?.getAttribute("data-message-author-role")
|
|
1087
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1088
|
+
if (/assistant|bot|ai|model|gpt|claude|copilot/.test(marker)) return "assistant";
|
|
1089
|
+
if (/user|human|prompt|customer|visitor/.test(marker)) return "user";
|
|
1090
|
+
if (/system/.test(marker)) return "system";
|
|
1091
|
+
if (/tool|function/.test(marker)) return "tool";
|
|
1092
|
+
const alignment = window.getComputedStyle(node).textAlign;
|
|
1093
|
+
if (alignment === "right") return "user";
|
|
1094
|
+
const lower = text.toLowerCase();
|
|
1095
|
+
if (lower.startsWith("you:")) return "user";
|
|
1096
|
+
if (lower.startsWith("assistant:") || lower.startsWith("ai:")) return "assistant";
|
|
1097
|
+
return "unknown";
|
|
1098
|
+
}
|
|
1099
|
+
extractConversationTitle(container) {
|
|
1100
|
+
const heading = container.querySelector('h1, h2, h3, [data-testid*="title" i], [class*="title"]');
|
|
1101
|
+
return this.scrub(heading?.innerText?.trim().slice(0, 160) || document.title || "conversation");
|
|
1102
|
+
}
|
|
1103
|
+
getConversationId(meta, container) {
|
|
1104
|
+
const explicit = [
|
|
1105
|
+
meta?.conversationId,
|
|
1106
|
+
meta?.conversation_id,
|
|
1107
|
+
meta?.threadId,
|
|
1108
|
+
meta?.thread_id,
|
|
1109
|
+
meta?.sessionId,
|
|
1110
|
+
meta?.session_id
|
|
1111
|
+
].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
1112
|
+
if (explicit) return this.scrub(explicit).slice(0, 120);
|
|
1113
|
+
if (container) {
|
|
1114
|
+
const existing = container.dataset.zcConversationId;
|
|
1115
|
+
if (existing) return existing;
|
|
1116
|
+
const generated = `conv-${location.pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "root"}-${++this.conversationCounter}`;
|
|
1117
|
+
container.dataset.zcConversationId = generated;
|
|
1118
|
+
return generated;
|
|
1119
|
+
}
|
|
1120
|
+
return `conv-${location.pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "root"}-${Date.now()}`;
|
|
1121
|
+
}
|
|
1122
|
+
getMessageFingerprint(conversationId, message, index) {
|
|
1123
|
+
return `${conversationId}:${index}:${message.role}:${message.content.slice(0, 240)}`;
|
|
1124
|
+
}
|
|
1125
|
+
safePathname(url) {
|
|
1126
|
+
try {
|
|
1127
|
+
return new URL(url, window.location.origin).pathname;
|
|
1128
|
+
} catch {
|
|
1129
|
+
return url;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
normalizeRole(value) {
|
|
1133
|
+
if (typeof value !== "string") return "unknown";
|
|
1134
|
+
const normalized = value.toLowerCase();
|
|
1135
|
+
if (normalized.includes("assistant") || normalized.includes("bot") || normalized.includes("ai") || normalized.includes("model")) return "assistant";
|
|
1136
|
+
if (normalized.includes("user") || normalized.includes("human")) return "user";
|
|
1137
|
+
if (normalized.includes("system")) return "system";
|
|
1138
|
+
if (normalized.includes("tool") || normalized.includes("function")) return "tool";
|
|
1139
|
+
return "unknown";
|
|
1140
|
+
}
|
|
524
1141
|
};
|
|
525
1142
|
|
|
526
1143
|
// src/modules/recording.ts
|
|
@@ -687,7 +1304,9 @@ var RecordingModule = class {
|
|
|
687
1304
|
};
|
|
688
1305
|
|
|
689
1306
|
// src/index.ts
|
|
690
|
-
var
|
|
1307
|
+
var CONFIG_CACHE_PREFIX = "zerocost-sdk-config:";
|
|
1308
|
+
var CONFIG_SYNC_DEBOUNCE_MS = 750;
|
|
1309
|
+
var CONFIG_STALE_AFTER_MS = 3e4;
|
|
691
1310
|
var ZerocostSDK = class {
|
|
692
1311
|
core;
|
|
693
1312
|
ads;
|
|
@@ -695,8 +1314,11 @@ var ZerocostSDK = class {
|
|
|
695
1314
|
widget;
|
|
696
1315
|
data;
|
|
697
1316
|
recording;
|
|
698
|
-
configPollTimer = null;
|
|
699
1317
|
lastConfigHash = "";
|
|
1318
|
+
lastDataCollectionHash = "";
|
|
1319
|
+
configSyncInFlight = null;
|
|
1320
|
+
lastConfigSyncAt = 0;
|
|
1321
|
+
cleanupConfigListeners = [];
|
|
700
1322
|
constructor(config) {
|
|
701
1323
|
this.core = new ZerocostClient(config);
|
|
702
1324
|
this.ads = new AdsModule(this.core);
|
|
@@ -705,89 +1327,192 @@ var ZerocostSDK = class {
|
|
|
705
1327
|
this.data = new LLMDataModule(this.core);
|
|
706
1328
|
this.recording = new RecordingModule(this.core);
|
|
707
1329
|
}
|
|
708
|
-
/**
|
|
709
|
-
* Initialize the SDK. Automatically:
|
|
710
|
-
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
711
|
-
* 2. Starts LLM data collection if enabled
|
|
712
|
-
* 3. Starts UX session recording if enabled
|
|
713
|
-
* 4. Polls for config changes every 5s — instant ad format switching
|
|
714
|
-
*
|
|
715
|
-
* No custom components needed — ads render automatically.
|
|
716
|
-
* Enable `debug: true` in config to see detailed logs.
|
|
717
|
-
*/
|
|
718
1330
|
async init() {
|
|
719
1331
|
this.core.init();
|
|
720
1332
|
if (typeof document === "undefined") {
|
|
721
|
-
this.core.log("Running in non-browser environment
|
|
1333
|
+
this.core.log("Running in non-browser environment; skipping DOM injection.");
|
|
722
1334
|
return;
|
|
723
1335
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
this.core.log("Running inside an iframe. Ads will still render if permissions allow.");
|
|
1336
|
+
if (window !== window.top) {
|
|
1337
|
+
this.core.log("Running inside an iframe. Ads render if permissions allow.");
|
|
727
1338
|
}
|
|
728
|
-
this.core.log("Initializing
|
|
1339
|
+
this.core.log("Initializing Zerocost SDK.");
|
|
1340
|
+
const cachedConfig = this.readCachedConfig();
|
|
1341
|
+
if (cachedConfig) {
|
|
1342
|
+
this.lastConfigHash = this.configToHash(cachedConfig);
|
|
1343
|
+
this.syncDataCollection(cachedConfig.dataCollection);
|
|
1344
|
+
await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
|
|
1345
|
+
this.core.log("Applied cached config immediately.");
|
|
1346
|
+
}
|
|
1347
|
+
this.startConfigSync();
|
|
729
1348
|
try {
|
|
730
|
-
|
|
731
|
-
this.
|
|
732
|
-
this.applyConfig(config);
|
|
733
|
-
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
734
|
-
this.startConfigPolling();
|
|
1349
|
+
await this.refreshConfig({ force: true, reason: "init" });
|
|
1350
|
+
this.core.log("SDK fully initialized. Ads are rendering automatically.");
|
|
735
1351
|
} catch (err) {
|
|
736
|
-
this.core.log(`Init error: ${err}. Attempting fallback ad injection
|
|
737
|
-
this.widget.autoInject();
|
|
1352
|
+
this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
|
|
1353
|
+
await this.widget.autoInject();
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
async refreshConfig(options = {}) {
|
|
1357
|
+
const now = Date.now();
|
|
1358
|
+
if (!options.force && now - this.lastConfigSyncAt < CONFIG_SYNC_DEBOUNCE_MS) {
|
|
1359
|
+
return this.configSyncInFlight ?? Promise.resolve();
|
|
738
1360
|
}
|
|
1361
|
+
if (this.configSyncInFlight) {
|
|
1362
|
+
return this.configSyncInFlight;
|
|
1363
|
+
}
|
|
1364
|
+
this.configSyncInFlight = (async () => {
|
|
1365
|
+
try {
|
|
1366
|
+
const config = await this.fetchConfig();
|
|
1367
|
+
const nextHash = this.configToHash(config);
|
|
1368
|
+
const hasDisplayChanged = nextHash !== this.lastConfigHash;
|
|
1369
|
+
this.lastConfigHash = nextHash;
|
|
1370
|
+
this.lastConfigSyncAt = Date.now();
|
|
1371
|
+
this.writeCachedConfig(config);
|
|
1372
|
+
if (hasDisplayChanged) {
|
|
1373
|
+
this.core.log(`Config change detected${options.reason ? ` via ${options.reason}` : ""}. Updating ad formats immediately.`);
|
|
1374
|
+
await this.widget.autoInjectWithConfig(config.display, config.widget);
|
|
1375
|
+
}
|
|
1376
|
+
this.syncDataCollection(config.dataCollection);
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
this.core.log(`Config sync failed${options.reason ? ` during ${options.reason}` : ""}; keeping current placements.`);
|
|
1379
|
+
throw err;
|
|
1380
|
+
} finally {
|
|
1381
|
+
this.configSyncInFlight = null;
|
|
1382
|
+
}
|
|
1383
|
+
})();
|
|
1384
|
+
return this.configSyncInFlight;
|
|
739
1385
|
}
|
|
740
1386
|
async fetchConfig() {
|
|
741
1387
|
return this.core.request("/get-placements");
|
|
742
1388
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
1389
|
+
configToHash(config) {
|
|
1390
|
+
try {
|
|
1391
|
+
return JSON.stringify({ display: config.display, widget: config.widget });
|
|
1392
|
+
} catch {
|
|
1393
|
+
return "";
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
dataCollectionToHash(dataCollection) {
|
|
1397
|
+
try {
|
|
1398
|
+
return JSON.stringify(dataCollection || {});
|
|
1399
|
+
} catch {
|
|
1400
|
+
return "";
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
syncDataCollection(dataCollection) {
|
|
1404
|
+
const nextHash = this.dataCollectionToHash(dataCollection);
|
|
1405
|
+
if (nextHash === this.lastDataCollectionHash) return;
|
|
1406
|
+
this.data.stop();
|
|
1407
|
+
this.recording.stop();
|
|
746
1408
|
if (dataCollection?.llm) {
|
|
747
1409
|
this.data.start(dataCollection.llm);
|
|
748
1410
|
}
|
|
749
1411
|
if (dataCollection?.recording) {
|
|
750
1412
|
this.recording.start(dataCollection.recording);
|
|
751
1413
|
}
|
|
1414
|
+
this.lastDataCollectionHash = nextHash;
|
|
752
1415
|
}
|
|
753
|
-
|
|
1416
|
+
getConfigCacheKey() {
|
|
1417
|
+
return `${CONFIG_CACHE_PREFIX}${this.core.getConfig().appId}`;
|
|
1418
|
+
}
|
|
1419
|
+
readCachedConfig() {
|
|
1420
|
+
if (typeof window === "undefined") return null;
|
|
754
1421
|
try {
|
|
755
|
-
|
|
1422
|
+
const raw = window.localStorage.getItem(this.getConfigCacheKey());
|
|
1423
|
+
return raw ? JSON.parse(raw) : null;
|
|
756
1424
|
} catch {
|
|
757
|
-
return
|
|
1425
|
+
return null;
|
|
758
1426
|
}
|
|
759
1427
|
}
|
|
760
|
-
|
|
761
|
-
if (
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1428
|
+
writeCachedConfig(config) {
|
|
1429
|
+
if (typeof window === "undefined") return;
|
|
1430
|
+
try {
|
|
1431
|
+
window.localStorage.setItem(this.getConfigCacheKey(), JSON.stringify(config));
|
|
1432
|
+
} catch {
|
|
1433
|
+
this.core.log("Failed to persist cached config.");
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
startConfigSync() {
|
|
1437
|
+
if (typeof window === "undefined") return;
|
|
1438
|
+
const syncIfVisible = () => {
|
|
1439
|
+
if (document.visibilityState === "visible") {
|
|
1440
|
+
this.refreshConfig({ reason: "visibility" }).catch(() => {
|
|
1441
|
+
});
|
|
773
1442
|
}
|
|
774
|
-
}
|
|
1443
|
+
};
|
|
1444
|
+
const syncIfStale = (reason) => {
|
|
1445
|
+
if (Date.now() - this.lastConfigSyncAt >= CONFIG_STALE_AFTER_MS) {
|
|
1446
|
+
this.refreshConfig({ reason }).catch(() => {
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
const onVisibilityChange = () => syncIfVisible();
|
|
1451
|
+
const onFocus = () => syncIfStale("focus");
|
|
1452
|
+
const onPageShow = () => syncIfStale("pageshow");
|
|
1453
|
+
const onOnline = () => this.refreshConfig({ force: true, reason: "online" }).catch(() => {
|
|
1454
|
+
});
|
|
1455
|
+
const onNavigation = () => syncIfStale("navigation");
|
|
1456
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
1457
|
+
window.addEventListener("focus", onFocus);
|
|
1458
|
+
window.addEventListener("pageshow", onPageShow);
|
|
1459
|
+
window.addEventListener("online", onOnline);
|
|
1460
|
+
window.addEventListener("popstate", onNavigation);
|
|
1461
|
+
window.addEventListener("hashchange", onNavigation);
|
|
1462
|
+
window.addEventListener("zerocost:navigation", onNavigation);
|
|
1463
|
+
const restoreHistoryPatch = this.patchHistory(onNavigation);
|
|
1464
|
+
this.cleanupConfigListeners.push(
|
|
1465
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange),
|
|
1466
|
+
() => window.removeEventListener("focus", onFocus),
|
|
1467
|
+
() => window.removeEventListener("pageshow", onPageShow),
|
|
1468
|
+
() => window.removeEventListener("online", onOnline),
|
|
1469
|
+
() => window.removeEventListener("popstate", onNavigation),
|
|
1470
|
+
() => window.removeEventListener("hashchange", onNavigation),
|
|
1471
|
+
() => window.removeEventListener("zerocost:navigation", onNavigation),
|
|
1472
|
+
restoreHistoryPatch
|
|
1473
|
+
);
|
|
775
1474
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
if (
|
|
781
|
-
|
|
782
|
-
|
|
1475
|
+
patchHistory(onNavigation) {
|
|
1476
|
+
if (typeof window === "undefined") return () => {
|
|
1477
|
+
};
|
|
1478
|
+
const historyRef = window.history;
|
|
1479
|
+
if (historyRef.__zerocostPatched) {
|
|
1480
|
+
return () => {
|
|
1481
|
+
};
|
|
783
1482
|
}
|
|
1483
|
+
const originalPushState = historyRef.pushState.bind(window.history);
|
|
1484
|
+
const originalReplaceState = historyRef.replaceState.bind(window.history);
|
|
1485
|
+
historyRef.__zerocostPatched = true;
|
|
1486
|
+
historyRef.__zerocostPushState = originalPushState;
|
|
1487
|
+
historyRef.__zerocostReplaceState = originalReplaceState;
|
|
1488
|
+
historyRef.pushState = ((...args) => {
|
|
1489
|
+
const result = originalPushState(...args);
|
|
1490
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
1491
|
+
onNavigation();
|
|
1492
|
+
return result;
|
|
1493
|
+
});
|
|
1494
|
+
historyRef.replaceState = ((...args) => {
|
|
1495
|
+
const result = originalReplaceState(...args);
|
|
1496
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
1497
|
+
onNavigation();
|
|
1498
|
+
return result;
|
|
1499
|
+
});
|
|
1500
|
+
return () => {
|
|
1501
|
+
if (!historyRef.__zerocostPatched) return;
|
|
1502
|
+
historyRef.pushState = historyRef.__zerocostPushState || historyRef.pushState;
|
|
1503
|
+
historyRef.replaceState = historyRef.__zerocostReplaceState || historyRef.replaceState;
|
|
1504
|
+
delete historyRef.__zerocostPatched;
|
|
1505
|
+
delete historyRef.__zerocostPushState;
|
|
1506
|
+
delete historyRef.__zerocostReplaceState;
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
destroy() {
|
|
1510
|
+
this.cleanupConfigListeners.forEach((cleanup) => cleanup());
|
|
1511
|
+
this.cleanupConfigListeners = [];
|
|
784
1512
|
this.widget.unmountAll();
|
|
785
1513
|
this.data.stop();
|
|
786
1514
|
this.recording.stop();
|
|
787
1515
|
}
|
|
788
|
-
/**
|
|
789
|
-
* Validate the configured API key against the server.
|
|
790
|
-
*/
|
|
791
1516
|
async validateKey() {
|
|
792
1517
|
try {
|
|
793
1518
|
const result = await this.core.request("/validate-key");
|