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