@zerocost/sdk 0.11.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 +875 -154
- package/dist/index.d.ts +15 -19
- package/dist/index.js +875 -154
- 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
|
@@ -138,6 +138,142 @@ var TrackModule = class {
|
|
|
138
138
|
}
|
|
139
139
|
};
|
|
140
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
|
+
|
|
141
277
|
// src/modules/widget.ts
|
|
142
278
|
var POSITION_STYLES = {
|
|
143
279
|
"bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
|
|
@@ -152,6 +288,17 @@ var POSITION_STYLES = {
|
|
|
152
288
|
};
|
|
153
289
|
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
154
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
|
+
];
|
|
155
302
|
var WidgetModule = class {
|
|
156
303
|
constructor(client) {
|
|
157
304
|
this.client = client;
|
|
@@ -159,15 +306,17 @@ var WidgetModule = class {
|
|
|
159
306
|
mounted = /* @__PURE__ */ new Map();
|
|
160
307
|
async autoInjectWithConfig(display, widget) {
|
|
161
308
|
try {
|
|
162
|
-
const selected = this.
|
|
309
|
+
const selected = this.resolveSelectedWidgets(display, widget);
|
|
163
310
|
this.clearAutoInjectedSlots();
|
|
164
|
-
if (
|
|
311
|
+
if (selected.length === 0) {
|
|
165
312
|
this.client.log("No enabled widget format found. Skipping injection.");
|
|
166
313
|
return;
|
|
167
314
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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.");
|
|
171
320
|
} catch (err) {
|
|
172
321
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
173
322
|
}
|
|
@@ -181,34 +330,65 @@ var WidgetModule = class {
|
|
|
181
330
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
182
331
|
}
|
|
183
332
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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({
|
|
187
338
|
format: widget.format,
|
|
188
339
|
position: widget.position || "bottom-right",
|
|
189
340
|
theme: widget.theme || "dark",
|
|
190
341
|
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
191
342
|
enabled: true
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
+
}
|
|
205
358
|
}
|
|
206
359
|
}
|
|
207
|
-
|
|
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;
|
|
208
379
|
}
|
|
209
380
|
normalizeConfigs(display) {
|
|
210
|
-
if (display && typeof display === "object" && "video-widget" in display) {
|
|
211
|
-
|
|
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
|
+
};
|
|
212
392
|
}
|
|
213
393
|
const pos = display?.position || "bottom-right";
|
|
214
394
|
const theme = display?.theme || "dark";
|
|
@@ -228,31 +408,32 @@ var WidgetModule = class {
|
|
|
228
408
|
return;
|
|
229
409
|
}
|
|
230
410
|
if (!isInline) {
|
|
231
|
-
let
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
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);
|
|
236
416
|
}
|
|
237
417
|
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
238
|
-
const maxW = config.format === "video-widget" ? "max-width:200px;" :
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
242
422
|
}
|
|
243
423
|
await this.mount(targetElementId, {
|
|
244
424
|
format: config.format,
|
|
245
|
-
refreshInterval:
|
|
246
|
-
// Config polling handles re-rendering
|
|
425
|
+
refreshInterval: SDK_WIDGET_REFRESH_MS / 1e3,
|
|
247
426
|
theme: config.theme,
|
|
248
427
|
autoplay: config.autoplay,
|
|
249
428
|
position: config.position
|
|
250
429
|
});
|
|
251
430
|
}
|
|
252
431
|
ensureInlineTarget(position) {
|
|
432
|
+
const chatContainer = this.findChatContainer();
|
|
433
|
+
if (!chatContainer) return null;
|
|
253
434
|
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
254
435
|
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
255
|
-
const paragraphs = Array.from(
|
|
436
|
+
const paragraphs = Array.from(chatContainer.querySelectorAll("p"));
|
|
256
437
|
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
257
438
|
if (!anchor) return null;
|
|
258
439
|
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
@@ -269,11 +450,22 @@ var WidgetModule = class {
|
|
|
269
450
|
}
|
|
270
451
|
return inlineId;
|
|
271
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
|
+
}
|
|
272
464
|
async mount(targetElementId, options = {}) {
|
|
273
|
-
const
|
|
274
|
-
if (!
|
|
465
|
+
const element = document.getElementById(targetElementId);
|
|
466
|
+
if (!element) return;
|
|
275
467
|
if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
|
|
276
|
-
const refreshMs = (options.refreshInterval ??
|
|
468
|
+
const refreshMs = (options.refreshInterval ?? SDK_WIDGET_REFRESH_MS / 1e3) * 1e3;
|
|
277
469
|
const theme = options.theme || "dark";
|
|
278
470
|
const format = options.format || "video-widget";
|
|
279
471
|
const autoplay = options.autoplay ?? format === "video-widget";
|
|
@@ -287,15 +479,15 @@ var WidgetModule = class {
|
|
|
287
479
|
};
|
|
288
480
|
const data = await this.client.request("/serve-widget", body);
|
|
289
481
|
const ad = data.ad;
|
|
290
|
-
if (!ad
|
|
482
|
+
if (!ad) {
|
|
291
483
|
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
292
|
-
|
|
484
|
+
element.innerHTML = "";
|
|
293
485
|
return;
|
|
294
486
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.ensureVideoPlayback(
|
|
298
|
-
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]");
|
|
299
491
|
ctas.forEach((cta) => {
|
|
300
492
|
cta.addEventListener("click", () => {
|
|
301
493
|
this.client.request("/track-event", {
|
|
@@ -305,11 +497,11 @@ var WidgetModule = class {
|
|
|
305
497
|
});
|
|
306
498
|
});
|
|
307
499
|
});
|
|
308
|
-
const closeBtn =
|
|
500
|
+
const closeBtn = element.querySelector("[data-zc-close]");
|
|
309
501
|
if (closeBtn) {
|
|
310
|
-
closeBtn.addEventListener("click", (
|
|
311
|
-
|
|
312
|
-
|
|
502
|
+
closeBtn.addEventListener("click", (event) => {
|
|
503
|
+
event.preventDefault();
|
|
504
|
+
event.stopPropagation();
|
|
313
505
|
this.unmount(targetElementId);
|
|
314
506
|
});
|
|
315
507
|
}
|
|
@@ -346,8 +538,8 @@ var WidgetModule = class {
|
|
|
346
538
|
unmount(targetElementId) {
|
|
347
539
|
const slot = this.mounted.get(targetElementId);
|
|
348
540
|
if (slot?.interval) clearInterval(slot.interval);
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
541
|
+
const element = document.getElementById(targetElementId);
|
|
542
|
+
if (element) element.remove();
|
|
351
543
|
this.mounted.delete(targetElementId);
|
|
352
544
|
}
|
|
353
545
|
unmountAll() {
|
|
@@ -366,59 +558,99 @@ var LLMDataModule = class {
|
|
|
366
558
|
clickHandler = null;
|
|
367
559
|
errorHandler = null;
|
|
368
560
|
fetchOriginal = null;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
561
|
+
mutationObserver = null;
|
|
562
|
+
conversationScanTimer = null;
|
|
563
|
+
isSampledSession = false;
|
|
564
|
+
observedConversations = /* @__PURE__ */ new Map();
|
|
565
|
+
conversationCounter = 0;
|
|
372
566
|
start(config) {
|
|
567
|
+
this.stop();
|
|
373
568
|
this.config = config;
|
|
374
569
|
this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
|
|
375
|
-
|
|
570
|
+
this.isSampledSession = Math.random() * 100 <= config.sampleRate;
|
|
571
|
+
if (!this.isSampledSession) {
|
|
376
572
|
this.client.log("LLMData: session not sampled, skipping");
|
|
377
573
|
return;
|
|
378
574
|
}
|
|
379
575
|
if (config.uiInteractions) this.captureUIInteractions();
|
|
380
|
-
if (config.textPrompts)
|
|
576
|
+
if (config.textPrompts) {
|
|
577
|
+
this.interceptPrompts();
|
|
578
|
+
this.captureConversationSurfaces();
|
|
579
|
+
this.scheduleConversationScan();
|
|
580
|
+
}
|
|
381
581
|
if (config.apiErrors) this.captureAPIErrors();
|
|
382
582
|
this.flushInterval = setInterval(() => this.flush(), 1e4);
|
|
383
583
|
}
|
|
384
584
|
stop() {
|
|
385
|
-
if (this.flushInterval)
|
|
585
|
+
if (this.flushInterval) {
|
|
586
|
+
clearInterval(this.flushInterval);
|
|
587
|
+
this.flushInterval = null;
|
|
588
|
+
}
|
|
386
589
|
if (this.clickHandler) {
|
|
387
590
|
document.removeEventListener("click", this.clickHandler, true);
|
|
591
|
+
this.clickHandler = null;
|
|
388
592
|
}
|
|
389
593
|
if (this.errorHandler) {
|
|
390
594
|
window.removeEventListener("error", this.errorHandler);
|
|
595
|
+
this.errorHandler = null;
|
|
391
596
|
}
|
|
392
597
|
if (this.fetchOriginal) {
|
|
393
598
|
window.fetch = this.fetchOriginal;
|
|
599
|
+
this.fetchOriginal = null;
|
|
394
600
|
}
|
|
395
|
-
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();
|
|
396
612
|
this.config = null;
|
|
397
613
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Manually track an LLM prompt/response pair (for startups that want to
|
|
400
|
-
* explicitly send their AI interactions).
|
|
401
|
-
*/
|
|
402
614
|
trackPrompt(prompt, response, meta) {
|
|
403
|
-
if (!this.
|
|
615
|
+
if (!this.canCapture("textPrompts")) return;
|
|
404
616
|
this.pushEvent("llm_prompt", {
|
|
405
617
|
prompt: this.scrub(prompt),
|
|
406
618
|
response: response ? this.scrub(response) : void 0,
|
|
407
|
-
|
|
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
|
|
408
644
|
});
|
|
409
645
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Manually track an API error.
|
|
412
|
-
*/
|
|
413
646
|
trackError(endpoint, status, message) {
|
|
414
|
-
if (!this.
|
|
647
|
+
if (!this.canCapture("apiErrors")) return;
|
|
415
648
|
this.pushEvent("api_error", {
|
|
416
649
|
endpoint,
|
|
417
650
|
status,
|
|
418
651
|
message: message ? this.scrub(message) : void 0
|
|
419
652
|
});
|
|
420
653
|
}
|
|
421
|
-
// ── Private ──
|
|
422
654
|
captureUIInteractions() {
|
|
423
655
|
this.clickHandler = (e) => {
|
|
424
656
|
const target = e.target;
|
|
@@ -442,44 +674,136 @@ var LLMDataModule = class {
|
|
|
442
674
|
}
|
|
443
675
|
interceptPrompts() {
|
|
444
676
|
this.fetchOriginal = window.fetch;
|
|
445
|
-
const self = this;
|
|
446
677
|
const origFetch = window.fetch;
|
|
678
|
+
const self = this;
|
|
447
679
|
window.fetch = async function(input, init) {
|
|
448
680
|
const fetchInput = input instanceof URL ? input.toString() : input;
|
|
449
681
|
const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
|
|
450
682
|
const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
|
|
451
|
-
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
|
+
}
|
|
452
699
|
try {
|
|
453
700
|
const res = await origFetch.call(window, fetchInput, init);
|
|
454
701
|
const clone = res.clone();
|
|
455
|
-
let reqBody;
|
|
456
|
-
if (init?.body && typeof init.body === "string") {
|
|
457
|
-
try {
|
|
458
|
-
const parsed = JSON.parse(init.body);
|
|
459
|
-
reqBody = JSON.stringify(parsed.messages || parsed.prompt || "").slice(0, 500);
|
|
460
|
-
} catch {
|
|
461
|
-
reqBody = void 0;
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
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
|
+
}
|
|
465
728
|
self.pushEvent("llm_prompt", {
|
|
466
|
-
endpoint:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
|
470
737
|
});
|
|
471
738
|
}).catch(() => {
|
|
472
739
|
});
|
|
473
740
|
return res;
|
|
474
|
-
} catch (
|
|
741
|
+
} catch (error) {
|
|
742
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
475
743
|
self.pushEvent("api_error", {
|
|
476
|
-
endpoint:
|
|
477
|
-
message: self.scrub(
|
|
744
|
+
endpoint: requestMeta.endpoint,
|
|
745
|
+
message: self.scrub(message),
|
|
746
|
+
requestId: requestMeta.requestId
|
|
478
747
|
});
|
|
479
|
-
throw
|
|
748
|
+
throw error;
|
|
480
749
|
}
|
|
481
750
|
};
|
|
482
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
|
+
}
|
|
483
807
|
captureAPIErrors() {
|
|
484
808
|
this.errorHandler = (e) => {
|
|
485
809
|
this.pushEvent("api_error", {
|
|
@@ -491,9 +815,37 @@ var LLMDataModule = class {
|
|
|
491
815
|
};
|
|
492
816
|
window.addEventListener("error", this.errorHandler);
|
|
493
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
|
+
}
|
|
494
843
|
pushEvent(type, data) {
|
|
844
|
+
if (!this.isSampledSession) return;
|
|
495
845
|
this.buffer.push({ type, data, timestamp: Date.now() });
|
|
496
|
-
if (this.buffer.length >= 50)
|
|
846
|
+
if (this.buffer.length >= 50) {
|
|
847
|
+
void this.flush();
|
|
848
|
+
}
|
|
497
849
|
}
|
|
498
850
|
async flush() {
|
|
499
851
|
if (this.buffer.length === 0) return;
|
|
@@ -506,25 +858,286 @@ var LLMDataModule = class {
|
|
|
506
858
|
this.buffer.unshift(...events);
|
|
507
859
|
}
|
|
508
860
|
}
|
|
509
|
-
/** Basic PII scrubbing */
|
|
510
861
|
scrub(text) {
|
|
511
|
-
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]");
|
|
512
863
|
}
|
|
513
864
|
getPath(el) {
|
|
514
865
|
const parts = [];
|
|
515
|
-
let
|
|
516
|
-
while (
|
|
517
|
-
let
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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}`;
|
|
522
874
|
}
|
|
523
|
-
parts.unshift(
|
|
524
|
-
|
|
875
|
+
parts.unshift(segment);
|
|
876
|
+
current = current.parentElement;
|
|
525
877
|
}
|
|
526
878
|
return parts.join(" > ");
|
|
527
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
|
+
}
|
|
528
1141
|
};
|
|
529
1142
|
|
|
530
1143
|
// src/modules/recording.ts
|
|
@@ -691,7 +1304,9 @@ var RecordingModule = class {
|
|
|
691
1304
|
};
|
|
692
1305
|
|
|
693
1306
|
// src/index.ts
|
|
694
|
-
var
|
|
1307
|
+
var CONFIG_CACHE_PREFIX = "zerocost-sdk-config:";
|
|
1308
|
+
var CONFIG_SYNC_DEBOUNCE_MS = 750;
|
|
1309
|
+
var CONFIG_STALE_AFTER_MS = 3e4;
|
|
695
1310
|
var ZerocostSDK = class {
|
|
696
1311
|
core;
|
|
697
1312
|
ads;
|
|
@@ -699,8 +1314,11 @@ var ZerocostSDK = class {
|
|
|
699
1314
|
widget;
|
|
700
1315
|
data;
|
|
701
1316
|
recording;
|
|
702
|
-
configPollTimer = null;
|
|
703
1317
|
lastConfigHash = "";
|
|
1318
|
+
lastDataCollectionHash = "";
|
|
1319
|
+
configSyncInFlight = null;
|
|
1320
|
+
lastConfigSyncAt = 0;
|
|
1321
|
+
cleanupConfigListeners = [];
|
|
704
1322
|
constructor(config) {
|
|
705
1323
|
this.core = new ZerocostClient(config);
|
|
706
1324
|
this.ads = new AdsModule(this.core);
|
|
@@ -709,89 +1327,192 @@ var ZerocostSDK = class {
|
|
|
709
1327
|
this.data = new LLMDataModule(this.core);
|
|
710
1328
|
this.recording = new RecordingModule(this.core);
|
|
711
1329
|
}
|
|
712
|
-
/**
|
|
713
|
-
* Initialize the SDK. Automatically:
|
|
714
|
-
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
715
|
-
* 2. Starts LLM data collection if enabled
|
|
716
|
-
* 3. Starts UX session recording if enabled
|
|
717
|
-
* 4. Polls for config changes every 5s — instant ad format switching
|
|
718
|
-
*
|
|
719
|
-
* No custom components needed — ads render automatically.
|
|
720
|
-
* Enable `debug: true` in config to see detailed logs.
|
|
721
|
-
*/
|
|
722
1330
|
async init() {
|
|
723
1331
|
this.core.init();
|
|
724
1332
|
if (typeof document === "undefined") {
|
|
725
|
-
this.core.log("Running in non-browser environment
|
|
1333
|
+
this.core.log("Running in non-browser environment; skipping DOM injection.");
|
|
726
1334
|
return;
|
|
727
1335
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1336
|
+
if (window !== window.top) {
|
|
1337
|
+
this.core.log("Running inside an iframe. Ads render if permissions allow.");
|
|
1338
|
+
}
|
|
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.");
|
|
731
1346
|
}
|
|
732
|
-
this.
|
|
1347
|
+
this.startConfigSync();
|
|
733
1348
|
try {
|
|
734
|
-
|
|
735
|
-
this.
|
|
736
|
-
this.applyConfig(config);
|
|
737
|
-
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
738
|
-
this.startConfigPolling();
|
|
1349
|
+
await this.refreshConfig({ force: true, reason: "init" });
|
|
1350
|
+
this.core.log("SDK fully initialized. Ads are rendering automatically.");
|
|
739
1351
|
} catch (err) {
|
|
740
|
-
this.core.log(`Init error: ${err}. Attempting fallback ad injection
|
|
741
|
-
this.widget.autoInject();
|
|
1352
|
+
this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
|
|
1353
|
+
await this.widget.autoInject();
|
|
742
1354
|
}
|
|
743
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();
|
|
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;
|
|
1385
|
+
}
|
|
744
1386
|
async fetchConfig() {
|
|
745
1387
|
return this.core.request("/get-placements");
|
|
746
1388
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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();
|
|
750
1408
|
if (dataCollection?.llm) {
|
|
751
1409
|
this.data.start(dataCollection.llm);
|
|
752
1410
|
}
|
|
753
1411
|
if (dataCollection?.recording) {
|
|
754
1412
|
this.recording.start(dataCollection.recording);
|
|
755
1413
|
}
|
|
1414
|
+
this.lastDataCollectionHash = nextHash;
|
|
756
1415
|
}
|
|
757
|
-
|
|
1416
|
+
getConfigCacheKey() {
|
|
1417
|
+
return `${CONFIG_CACHE_PREFIX}${this.core.getConfig().appId}`;
|
|
1418
|
+
}
|
|
1419
|
+
readCachedConfig() {
|
|
1420
|
+
if (typeof window === "undefined") return null;
|
|
758
1421
|
try {
|
|
759
|
-
|
|
1422
|
+
const raw = window.localStorage.getItem(this.getConfigCacheKey());
|
|
1423
|
+
return raw ? JSON.parse(raw) : null;
|
|
760
1424
|
} catch {
|
|
761
|
-
return
|
|
1425
|
+
return null;
|
|
762
1426
|
}
|
|
763
1427
|
}
|
|
764
|
-
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
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
|
+
});
|
|
777
1442
|
}
|
|
778
|
-
}
|
|
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
|
+
);
|
|
779
1474
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
1475
|
+
patchHistory(onNavigation) {
|
|
1476
|
+
if (typeof window === "undefined") return () => {
|
|
1477
|
+
};
|
|
1478
|
+
const historyRef = window.history;
|
|
1479
|
+
if (historyRef.__zerocostPatched) {
|
|
1480
|
+
return () => {
|
|
1481
|
+
};
|
|
787
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 = [];
|
|
788
1512
|
this.widget.unmountAll();
|
|
789
1513
|
this.data.stop();
|
|
790
1514
|
this.recording.stop();
|
|
791
1515
|
}
|
|
792
|
-
/**
|
|
793
|
-
* Validate the configured API key against the server.
|
|
794
|
-
*/
|
|
795
1516
|
async validateKey() {
|
|
796
1517
|
try {
|
|
797
1518
|
const result = await this.core.request("/validate-key");
|