@zerocost/sdk 0.11.0 → 0.13.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/consent-ui.d.ts +27 -0
- package/dist/core/consent.d.ts +35 -5
- package/dist/core/widget-render.d.ts +8 -0
- package/dist/index.cjs +1494 -156
- package/dist/index.d.ts +18 -19
- package/dist/index.js +1493 -156
- package/dist/modules/llm-data.d.ts +39 -12
- package/dist/modules/widget.d.ts +2 -1
- package/dist/types/index.d.ts +20 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
+
ConsentManager: () => ConsentManager,
|
|
23
24
|
LLMDataModule: () => LLMDataModule,
|
|
24
25
|
RecordingModule: () => RecordingModule,
|
|
25
26
|
ZerocostClient: () => ZerocostClient,
|
|
@@ -138,6 +139,142 @@ var TrackModule = class {
|
|
|
138
139
|
}
|
|
139
140
|
};
|
|
140
141
|
|
|
142
|
+
// src/core/widget-render.ts
|
|
143
|
+
var SDK_WIDGET_REFRESH_MS = 2e4;
|
|
144
|
+
function escapeHtml(value) {
|
|
145
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
146
|
+
}
|
|
147
|
+
function resolveTheme(theme) {
|
|
148
|
+
if (theme === "light" || theme === "dark") {
|
|
149
|
+
return theme;
|
|
150
|
+
}
|
|
151
|
+
if (typeof window !== "undefined" && window.matchMedia) {
|
|
152
|
+
return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
|
|
153
|
+
}
|
|
154
|
+
return "dark";
|
|
155
|
+
}
|
|
156
|
+
function getPalette(theme) {
|
|
157
|
+
const mode = resolveTheme(theme);
|
|
158
|
+
if (mode === "light") {
|
|
159
|
+
return {
|
|
160
|
+
bg: "#ffffff",
|
|
161
|
+
surface: "#f8f8f8",
|
|
162
|
+
surfaceStrong: "#efefef",
|
|
163
|
+
border: "#dddddd",
|
|
164
|
+
text: "#111111",
|
|
165
|
+
textMuted: "#666666",
|
|
166
|
+
textFaint: "#8a8a8a",
|
|
167
|
+
accentBg: "#111111",
|
|
168
|
+
accentText: "#ffffff",
|
|
169
|
+
badgeBg: "#f1f1f1"
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
return {
|
|
173
|
+
bg: "#0a0a0a",
|
|
174
|
+
surface: "#111111",
|
|
175
|
+
surfaceStrong: "#181818",
|
|
176
|
+
border: "#2b2b2b",
|
|
177
|
+
text: "#ffffff",
|
|
178
|
+
textMuted: "#a3a3a3",
|
|
179
|
+
textFaint: "#737373",
|
|
180
|
+
accentBg: "#ffffff",
|
|
181
|
+
accentText: "#111111",
|
|
182
|
+
badgeBg: "#171717"
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function normalizeFormat(format) {
|
|
186
|
+
if (format === "floating-video") return "video-widget";
|
|
187
|
+
if (format === "sidebar-display") return "sponsored-card";
|
|
188
|
+
return format;
|
|
189
|
+
}
|
|
190
|
+
function renderVideoWidget(ad, theme) {
|
|
191
|
+
const palette = getPalette(theme);
|
|
192
|
+
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>`;
|
|
193
|
+
return `
|
|
194
|
+
<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;">
|
|
195
|
+
${media}
|
|
196
|
+
<div style="position:absolute;top:10px;right:10px;display:flex;gap:8px;z-index:3;">
|
|
197
|
+
<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>
|
|
198
|
+
</div>
|
|
199
|
+
<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>
|
|
200
|
+
<div style="position:absolute;left:0;right:0;bottom:0;padding:14px;z-index:2;">
|
|
201
|
+
<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>
|
|
202
|
+
<div style="margin-top:10px;color:#fff;font-size:14px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
203
|
+
${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>` : ""}
|
|
204
|
+
<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>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
`;
|
|
208
|
+
}
|
|
209
|
+
function renderTooltipWidget(ad, theme) {
|
|
210
|
+
const palette = getPalette(theme);
|
|
211
|
+
return `
|
|
212
|
+
<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;">
|
|
213
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;">
|
|
214
|
+
<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;">
|
|
215
|
+
<span style="width:6px;height:6px;border-radius:999px;background:${palette.text};display:inline-block;"></span>
|
|
216
|
+
Sponsored
|
|
217
|
+
</div>
|
|
218
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
219
|
+
</div>
|
|
220
|
+
<div style="margin-top:10px;color:${palette.text};font-size:13px;line-height:1.55;">
|
|
221
|
+
${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>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
`;
|
|
225
|
+
}
|
|
226
|
+
function renderSponsoredCard(ad, theme) {
|
|
227
|
+
const palette = getPalette(theme);
|
|
228
|
+
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>`;
|
|
229
|
+
return `
|
|
230
|
+
<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;">
|
|
231
|
+
${media}
|
|
232
|
+
<div style="padding:12px;">
|
|
233
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
|
234
|
+
<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>
|
|
235
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
236
|
+
</div>
|
|
237
|
+
<div style="margin-top:10px;color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
238
|
+
${ad.description ? `<div style="margin-top:6px;color:${palette.textMuted};font-size:11px;line-height:1.35;">${escapeHtml(ad.description)}</div>` : ""}
|
|
239
|
+
<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>
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
`;
|
|
243
|
+
}
|
|
244
|
+
function renderInlineText(ad, theme) {
|
|
245
|
+
const palette = getPalette(theme);
|
|
246
|
+
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>`;
|
|
247
|
+
return `
|
|
248
|
+
<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;">
|
|
249
|
+
<div style="display:flex;align-items:center;justify-content:space-between;padding:9px 12px;border-bottom:1px solid ${palette.border};background:${palette.surfaceStrong};">
|
|
250
|
+
<span style="color:${palette.textFaint};font-size:9px;font-weight:700;letter-spacing:0.12em;text-transform:uppercase;">Sponsored</span>
|
|
251
|
+
<button type="button" data-zc-close style="background:none;border:none;color:${palette.textFaint};font-size:12px;cursor:pointer;padding:0;">x</button>
|
|
252
|
+
</div>
|
|
253
|
+
<div style="padding:12px;display:flex;gap:10px;align-items:flex-start;">
|
|
254
|
+
${media}
|
|
255
|
+
<div style="min-width:0;flex:1;">
|
|
256
|
+
<div style="color:${palette.text};font-size:13px;font-weight:700;line-height:1.2;">${escapeHtml(ad.title)}</div>
|
|
257
|
+
${ad.description ? `<div style="margin-top:5px;color:${palette.textMuted};font-size:11px;line-height:1.45;">${escapeHtml(ad.description)}</div>` : ""}
|
|
258
|
+
<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>
|
|
259
|
+
</div>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
function renderWidgetMarkup(ad, options) {
|
|
265
|
+
const format = normalizeFormat(options.format);
|
|
266
|
+
if (format === "tooltip-ad") {
|
|
267
|
+
return renderTooltipWidget(ad, options.theme);
|
|
268
|
+
}
|
|
269
|
+
if (format === "sponsored-card") {
|
|
270
|
+
return renderSponsoredCard(ad, options.theme);
|
|
271
|
+
}
|
|
272
|
+
if (format === "inline-text") {
|
|
273
|
+
return renderInlineText(ad, options.theme);
|
|
274
|
+
}
|
|
275
|
+
return renderVideoWidget(ad, options.theme);
|
|
276
|
+
}
|
|
277
|
+
|
|
141
278
|
// src/modules/widget.ts
|
|
142
279
|
var POSITION_STYLES = {
|
|
143
280
|
"bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
|
|
@@ -152,6 +289,17 @@ var POSITION_STYLES = {
|
|
|
152
289
|
};
|
|
153
290
|
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
154
291
|
var AUTO_SLOT_ID = "zerocost-auto-slot";
|
|
292
|
+
var CHAT_CONTAINER_SELECTORS = [
|
|
293
|
+
"[data-zerocost-chat]",
|
|
294
|
+
"[data-zc-chat]",
|
|
295
|
+
"[data-chat]",
|
|
296
|
+
"[data-chat-stream]",
|
|
297
|
+
"[data-conversation]",
|
|
298
|
+
"[data-ai-chat]",
|
|
299
|
+
'[role="log"]',
|
|
300
|
+
'[aria-label*="chat" i]',
|
|
301
|
+
'[aria-label*="conversation" i]'
|
|
302
|
+
];
|
|
155
303
|
var WidgetModule = class {
|
|
156
304
|
constructor(client) {
|
|
157
305
|
this.client = client;
|
|
@@ -159,15 +307,17 @@ var WidgetModule = class {
|
|
|
159
307
|
mounted = /* @__PURE__ */ new Map();
|
|
160
308
|
async autoInjectWithConfig(display, widget) {
|
|
161
309
|
try {
|
|
162
|
-
const selected = this.
|
|
310
|
+
const selected = this.resolveSelectedWidgets(display, widget);
|
|
163
311
|
this.clearAutoInjectedSlots();
|
|
164
|
-
if (
|
|
312
|
+
if (selected.length === 0) {
|
|
165
313
|
this.client.log("No enabled widget format found. Skipping injection.");
|
|
166
314
|
return;
|
|
167
315
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
316
|
+
for (const config of selected) {
|
|
317
|
+
this.client.log(`Auto-inject: rendering configured format "${config.format}".`);
|
|
318
|
+
await this.mountSingleFormat(config);
|
|
319
|
+
}
|
|
320
|
+
this.client.log("Auto-inject completed.");
|
|
171
321
|
} catch (err) {
|
|
172
322
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
173
323
|
}
|
|
@@ -181,34 +331,65 @@ var WidgetModule = class {
|
|
|
181
331
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
182
332
|
}
|
|
183
333
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
334
|
+
resolveSelectedWidgets(display, widget) {
|
|
335
|
+
const configs = this.normalizeConfigs(display);
|
|
336
|
+
const selected = [];
|
|
337
|
+
if (widget?.enabled && widget?.format && widget.format !== "inline-text") {
|
|
338
|
+
selected.push({
|
|
187
339
|
format: widget.format,
|
|
188
340
|
position: widget.position || "bottom-right",
|
|
189
341
|
theme: widget.theme || "dark",
|
|
190
342
|
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
191
343
|
enabled: true
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
344
|
+
});
|
|
345
|
+
} else {
|
|
346
|
+
for (const format of FORMAT_PRIORITY) {
|
|
347
|
+
if (format === "inline-text") continue;
|
|
348
|
+
const config = configs[format];
|
|
349
|
+
if (config?.enabled) {
|
|
350
|
+
selected.push({
|
|
351
|
+
format,
|
|
352
|
+
position: config.position,
|
|
353
|
+
theme: config.theme,
|
|
354
|
+
autoplay: config.autoplay,
|
|
355
|
+
enabled: true
|
|
356
|
+
});
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
205
359
|
}
|
|
206
360
|
}
|
|
207
|
-
|
|
361
|
+
const inlineConfig = configs["inline-text"];
|
|
362
|
+
if (widget?.enabled && widget?.format === "inline-text") {
|
|
363
|
+
selected.push({
|
|
364
|
+
format: "inline-text",
|
|
365
|
+
position: widget.position || inlineConfig?.position || "after-paragraph-1",
|
|
366
|
+
theme: widget.theme || inlineConfig?.theme || "dark",
|
|
367
|
+
autoplay: false,
|
|
368
|
+
enabled: true
|
|
369
|
+
});
|
|
370
|
+
} else if (inlineConfig?.enabled) {
|
|
371
|
+
selected.push({
|
|
372
|
+
format: "inline-text",
|
|
373
|
+
position: inlineConfig.position,
|
|
374
|
+
theme: inlineConfig.theme,
|
|
375
|
+
autoplay: false,
|
|
376
|
+
enabled: true
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
return selected;
|
|
208
380
|
}
|
|
209
381
|
normalizeConfigs(display) {
|
|
210
|
-
if (display && typeof display === "object" && "video-widget" in display) {
|
|
211
|
-
|
|
382
|
+
if (display && typeof display === "object" && ("video-widget" in display || "floating-video" in display)) {
|
|
383
|
+
const source = display;
|
|
384
|
+
const videoConfig = source["video-widget"] || source["floating-video"];
|
|
385
|
+
const sponsoredCardConfig = source["sponsored-card"] || source["sidebar-display"];
|
|
386
|
+
return {
|
|
387
|
+
"video-widget": videoConfig || { position: "bottom-right", theme: "dark", autoplay: true, enabled: true },
|
|
388
|
+
"tooltip-ad": source["tooltip-ad"] || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
389
|
+
"sponsored-card": sponsoredCardConfig || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
390
|
+
"sidebar-display": sponsoredCardConfig || { position: "bottom-right", theme: "dark", autoplay: false, enabled: false },
|
|
391
|
+
"inline-text": source["inline-text"] || { position: "after-paragraph-1", theme: "dark", autoplay: false, enabled: false }
|
|
392
|
+
};
|
|
212
393
|
}
|
|
213
394
|
const pos = display?.position || "bottom-right";
|
|
214
395
|
const theme = display?.theme || "dark";
|
|
@@ -228,31 +409,32 @@ var WidgetModule = class {
|
|
|
228
409
|
return;
|
|
229
410
|
}
|
|
230
411
|
if (!isInline) {
|
|
231
|
-
let
|
|
232
|
-
if (!
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
document.body.appendChild(
|
|
412
|
+
let element = document.getElementById(AUTO_SLOT_ID);
|
|
413
|
+
if (!element) {
|
|
414
|
+
element = document.createElement("div");
|
|
415
|
+
element.id = AUTO_SLOT_ID;
|
|
416
|
+
document.body.appendChild(element);
|
|
236
417
|
}
|
|
237
418
|
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
238
|
-
const maxW = config.format === "video-widget" ? "max-width:200px;" :
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
419
|
+
const maxW = config.format === "video-widget" ? "max-width:200px;" : "max-width:176px;";
|
|
420
|
+
element.setAttribute("style", `${posStyle}${maxW}`);
|
|
421
|
+
element.setAttribute("data-zerocost", "");
|
|
422
|
+
element.setAttribute("data-format", config.format);
|
|
242
423
|
}
|
|
243
424
|
await this.mount(targetElementId, {
|
|
244
425
|
format: config.format,
|
|
245
|
-
refreshInterval:
|
|
246
|
-
// Config polling handles re-rendering
|
|
426
|
+
refreshInterval: SDK_WIDGET_REFRESH_MS / 1e3,
|
|
247
427
|
theme: config.theme,
|
|
248
428
|
autoplay: config.autoplay,
|
|
249
429
|
position: config.position
|
|
250
430
|
});
|
|
251
431
|
}
|
|
252
432
|
ensureInlineTarget(position) {
|
|
433
|
+
const chatContainer = this.findChatContainer();
|
|
434
|
+
if (!chatContainer) return null;
|
|
253
435
|
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
254
436
|
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
255
|
-
const paragraphs = Array.from(
|
|
437
|
+
const paragraphs = Array.from(chatContainer.querySelectorAll("p"));
|
|
256
438
|
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
257
439
|
if (!anchor) return null;
|
|
258
440
|
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
@@ -269,11 +451,22 @@ var WidgetModule = class {
|
|
|
269
451
|
}
|
|
270
452
|
return inlineId;
|
|
271
453
|
}
|
|
454
|
+
findChatContainer() {
|
|
455
|
+
for (const selector of CHAT_CONTAINER_SELECTORS) {
|
|
456
|
+
const container = document.querySelector(selector);
|
|
457
|
+
if (container) return container;
|
|
458
|
+
}
|
|
459
|
+
const semanticContainers = Array.from(document.querySelectorAll("section, main, div, article"));
|
|
460
|
+
return semanticContainers.find((node) => {
|
|
461
|
+
const marker = `${node.id} ${node.className || ""}`.toLowerCase();
|
|
462
|
+
return /chat|conversation|assistant|messages|thread/.test(marker);
|
|
463
|
+
}) || null;
|
|
464
|
+
}
|
|
272
465
|
async mount(targetElementId, options = {}) {
|
|
273
|
-
const
|
|
274
|
-
if (!
|
|
466
|
+
const element = document.getElementById(targetElementId);
|
|
467
|
+
if (!element) return;
|
|
275
468
|
if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
|
|
276
|
-
const refreshMs = (options.refreshInterval ??
|
|
469
|
+
const refreshMs = (options.refreshInterval ?? SDK_WIDGET_REFRESH_MS / 1e3) * 1e3;
|
|
277
470
|
const theme = options.theme || "dark";
|
|
278
471
|
const format = options.format || "video-widget";
|
|
279
472
|
const autoplay = options.autoplay ?? format === "video-widget";
|
|
@@ -287,15 +480,15 @@ var WidgetModule = class {
|
|
|
287
480
|
};
|
|
288
481
|
const data = await this.client.request("/serve-widget", body);
|
|
289
482
|
const ad = data.ad;
|
|
290
|
-
if (!ad
|
|
483
|
+
if (!ad) {
|
|
291
484
|
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
292
|
-
|
|
485
|
+
element.innerHTML = "";
|
|
293
486
|
return;
|
|
294
487
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.ensureVideoPlayback(
|
|
298
|
-
const ctas =
|
|
488
|
+
element.innerHTML = renderWidgetMarkup(ad, { format, theme });
|
|
489
|
+
element.setAttribute("data-zerocost-ad-id", ad.id);
|
|
490
|
+
this.ensureVideoPlayback(element);
|
|
491
|
+
const ctas = element.querySelectorAll("[data-zc-cta]");
|
|
299
492
|
ctas.forEach((cta) => {
|
|
300
493
|
cta.addEventListener("click", () => {
|
|
301
494
|
this.client.request("/track-event", {
|
|
@@ -305,11 +498,11 @@ var WidgetModule = class {
|
|
|
305
498
|
});
|
|
306
499
|
});
|
|
307
500
|
});
|
|
308
|
-
const closeBtn =
|
|
501
|
+
const closeBtn = element.querySelector("[data-zc-close]");
|
|
309
502
|
if (closeBtn) {
|
|
310
|
-
closeBtn.addEventListener("click", (
|
|
311
|
-
|
|
312
|
-
|
|
503
|
+
closeBtn.addEventListener("click", (event) => {
|
|
504
|
+
event.preventDefault();
|
|
505
|
+
event.stopPropagation();
|
|
313
506
|
this.unmount(targetElementId);
|
|
314
507
|
});
|
|
315
508
|
}
|
|
@@ -346,8 +539,8 @@ var WidgetModule = class {
|
|
|
346
539
|
unmount(targetElementId) {
|
|
347
540
|
const slot = this.mounted.get(targetElementId);
|
|
348
541
|
if (slot?.interval) clearInterval(slot.interval);
|
|
349
|
-
const
|
|
350
|
-
if (
|
|
542
|
+
const element = document.getElementById(targetElementId);
|
|
543
|
+
if (element) element.remove();
|
|
351
544
|
this.mounted.delete(targetElementId);
|
|
352
545
|
}
|
|
353
546
|
unmountAll() {
|
|
@@ -366,59 +559,99 @@ var LLMDataModule = class {
|
|
|
366
559
|
clickHandler = null;
|
|
367
560
|
errorHandler = null;
|
|
368
561
|
fetchOriginal = null;
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
562
|
+
mutationObserver = null;
|
|
563
|
+
conversationScanTimer = null;
|
|
564
|
+
isSampledSession = false;
|
|
565
|
+
observedConversations = /* @__PURE__ */ new Map();
|
|
566
|
+
conversationCounter = 0;
|
|
372
567
|
start(config) {
|
|
568
|
+
this.stop();
|
|
373
569
|
this.config = config;
|
|
374
570
|
this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
|
|
375
|
-
|
|
571
|
+
this.isSampledSession = Math.random() * 100 <= config.sampleRate;
|
|
572
|
+
if (!this.isSampledSession) {
|
|
376
573
|
this.client.log("LLMData: session not sampled, skipping");
|
|
377
574
|
return;
|
|
378
575
|
}
|
|
379
576
|
if (config.uiInteractions) this.captureUIInteractions();
|
|
380
|
-
if (config.textPrompts)
|
|
577
|
+
if (config.textPrompts) {
|
|
578
|
+
this.interceptPrompts();
|
|
579
|
+
this.captureConversationSurfaces();
|
|
580
|
+
this.scheduleConversationScan();
|
|
581
|
+
}
|
|
381
582
|
if (config.apiErrors) this.captureAPIErrors();
|
|
382
583
|
this.flushInterval = setInterval(() => this.flush(), 1e4);
|
|
383
584
|
}
|
|
384
585
|
stop() {
|
|
385
|
-
if (this.flushInterval)
|
|
586
|
+
if (this.flushInterval) {
|
|
587
|
+
clearInterval(this.flushInterval);
|
|
588
|
+
this.flushInterval = null;
|
|
589
|
+
}
|
|
386
590
|
if (this.clickHandler) {
|
|
387
591
|
document.removeEventListener("click", this.clickHandler, true);
|
|
592
|
+
this.clickHandler = null;
|
|
388
593
|
}
|
|
389
594
|
if (this.errorHandler) {
|
|
390
595
|
window.removeEventListener("error", this.errorHandler);
|
|
596
|
+
this.errorHandler = null;
|
|
391
597
|
}
|
|
392
598
|
if (this.fetchOriginal) {
|
|
393
599
|
window.fetch = this.fetchOriginal;
|
|
600
|
+
this.fetchOriginal = null;
|
|
394
601
|
}
|
|
395
|
-
this.
|
|
602
|
+
if (this.mutationObserver) {
|
|
603
|
+
this.mutationObserver.disconnect();
|
|
604
|
+
this.mutationObserver = null;
|
|
605
|
+
}
|
|
606
|
+
if (this.conversationScanTimer) {
|
|
607
|
+
clearTimeout(this.conversationScanTimer);
|
|
608
|
+
this.conversationScanTimer = null;
|
|
609
|
+
}
|
|
610
|
+
this.observedConversations.clear();
|
|
611
|
+
this.isSampledSession = false;
|
|
612
|
+
void this.flush();
|
|
396
613
|
this.config = null;
|
|
397
614
|
}
|
|
398
|
-
/**
|
|
399
|
-
* Manually track an LLM prompt/response pair (for startups that want to
|
|
400
|
-
* explicitly send their AI interactions).
|
|
401
|
-
*/
|
|
402
615
|
trackPrompt(prompt, response, meta) {
|
|
403
|
-
if (!this.
|
|
616
|
+
if (!this.canCapture("textPrompts")) return;
|
|
404
617
|
this.pushEvent("llm_prompt", {
|
|
405
618
|
prompt: this.scrub(prompt),
|
|
406
619
|
response: response ? this.scrub(response) : void 0,
|
|
407
|
-
|
|
620
|
+
source: "manual",
|
|
621
|
+
...this.sanitizeMeta(meta)
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
trackConversation(messages, meta) {
|
|
625
|
+
if (!this.canCapture("textPrompts")) return;
|
|
626
|
+
const cleanedMessages = messages.map((message) => ({
|
|
627
|
+
role: this.normalizeRole(message.role),
|
|
628
|
+
content: this.scrub(message.content || ""),
|
|
629
|
+
...message.name ? { name: this.scrub(message.name) } : {}
|
|
630
|
+
})).filter((message) => message.content);
|
|
631
|
+
if (cleanedMessages.length === 0) return;
|
|
632
|
+
const sanitizedMeta = this.sanitizeMeta(meta);
|
|
633
|
+
const conversationId = this.getConversationId(sanitizedMeta);
|
|
634
|
+
cleanedMessages.forEach((message, index) => {
|
|
635
|
+
this.pushConversationMessage(message, {
|
|
636
|
+
conversationId,
|
|
637
|
+
turnIndex: index,
|
|
638
|
+
source: "manual",
|
|
639
|
+
...sanitizedMeta
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
this.pushConversationSummary(conversationId, cleanedMessages, {
|
|
643
|
+
source: "manual",
|
|
644
|
+
...sanitizedMeta
|
|
408
645
|
});
|
|
409
646
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Manually track an API error.
|
|
412
|
-
*/
|
|
413
647
|
trackError(endpoint, status, message) {
|
|
414
|
-
if (!this.
|
|
648
|
+
if (!this.canCapture("apiErrors")) return;
|
|
415
649
|
this.pushEvent("api_error", {
|
|
416
650
|
endpoint,
|
|
417
651
|
status,
|
|
418
652
|
message: message ? this.scrub(message) : void 0
|
|
419
653
|
});
|
|
420
654
|
}
|
|
421
|
-
// ── Private ──
|
|
422
655
|
captureUIInteractions() {
|
|
423
656
|
this.clickHandler = (e) => {
|
|
424
657
|
const target = e.target;
|
|
@@ -442,44 +675,136 @@ var LLMDataModule = class {
|
|
|
442
675
|
}
|
|
443
676
|
interceptPrompts() {
|
|
444
677
|
this.fetchOriginal = window.fetch;
|
|
445
|
-
const self = this;
|
|
446
678
|
const origFetch = window.fetch;
|
|
679
|
+
const self = this;
|
|
447
680
|
window.fetch = async function(input, init) {
|
|
448
681
|
const fetchInput = input instanceof URL ? input.toString() : input;
|
|
449
682
|
const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
|
|
450
683
|
const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
|
|
451
|
-
if (!isLLM)
|
|
684
|
+
if (!isLLM) {
|
|
685
|
+
return origFetch.call(window, fetchInput, init);
|
|
686
|
+
}
|
|
687
|
+
const requestMeta = self.extractRequestMeta(url, init);
|
|
688
|
+
if (requestMeta.messages.length > 0) {
|
|
689
|
+
requestMeta.messages.forEach((message, index) => {
|
|
690
|
+
self.pushConversationMessage(message, {
|
|
691
|
+
conversationId: requestMeta.conversationId,
|
|
692
|
+
turnIndex: index,
|
|
693
|
+
source: "network-request",
|
|
694
|
+
endpoint: requestMeta.endpoint,
|
|
695
|
+
requestId: requestMeta.requestId,
|
|
696
|
+
...requestMeta.meta
|
|
697
|
+
});
|
|
698
|
+
});
|
|
699
|
+
}
|
|
452
700
|
try {
|
|
453
701
|
const res = await origFetch.call(window, fetchInput, init);
|
|
454
702
|
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
703
|
clone.text().then((text) => {
|
|
704
|
+
const responseMessages = self.extractResponseMessages(text);
|
|
705
|
+
if (responseMessages.length > 0) {
|
|
706
|
+
responseMessages.forEach((message, index) => {
|
|
707
|
+
self.pushConversationMessage(message, {
|
|
708
|
+
conversationId: requestMeta.conversationId,
|
|
709
|
+
turnIndex: requestMeta.messages.length + index,
|
|
710
|
+
source: "network-response",
|
|
711
|
+
endpoint: requestMeta.endpoint,
|
|
712
|
+
requestId: requestMeta.requestId,
|
|
713
|
+
status: res.status,
|
|
714
|
+
...requestMeta.meta
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
self.pushConversationSummary(
|
|
718
|
+
requestMeta.conversationId,
|
|
719
|
+
[...requestMeta.messages, ...responseMessages],
|
|
720
|
+
{
|
|
721
|
+
source: "network",
|
|
722
|
+
endpoint: requestMeta.endpoint,
|
|
723
|
+
requestId: requestMeta.requestId,
|
|
724
|
+
status: res.status,
|
|
725
|
+
...requestMeta.meta
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
}
|
|
465
729
|
self.pushEvent("llm_prompt", {
|
|
466
|
-
endpoint:
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
730
|
+
endpoint: requestMeta.endpoint,
|
|
731
|
+
conversationId: requestMeta.conversationId,
|
|
732
|
+
request: requestMeta.promptPreview,
|
|
733
|
+
response: self.scrub(text.slice(0, 1200)),
|
|
734
|
+
status: res.status,
|
|
735
|
+
source: "network",
|
|
736
|
+
requestId: requestMeta.requestId,
|
|
737
|
+
...requestMeta.meta
|
|
470
738
|
});
|
|
471
739
|
}).catch(() => {
|
|
472
740
|
});
|
|
473
741
|
return res;
|
|
474
|
-
} catch (
|
|
742
|
+
} catch (error) {
|
|
743
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
475
744
|
self.pushEvent("api_error", {
|
|
476
|
-
endpoint:
|
|
477
|
-
message: self.scrub(
|
|
745
|
+
endpoint: requestMeta.endpoint,
|
|
746
|
+
message: self.scrub(message),
|
|
747
|
+
requestId: requestMeta.requestId
|
|
478
748
|
});
|
|
479
|
-
throw
|
|
749
|
+
throw error;
|
|
480
750
|
}
|
|
481
751
|
};
|
|
482
752
|
}
|
|
753
|
+
captureConversationSurfaces() {
|
|
754
|
+
if (typeof MutationObserver === "undefined" || typeof document === "undefined") return;
|
|
755
|
+
this.mutationObserver = new MutationObserver(() => this.scheduleConversationScan());
|
|
756
|
+
this.mutationObserver.observe(document.body, {
|
|
757
|
+
childList: true,
|
|
758
|
+
subtree: true,
|
|
759
|
+
characterData: true
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
scheduleConversationScan() {
|
|
763
|
+
if (this.conversationScanTimer) {
|
|
764
|
+
clearTimeout(this.conversationScanTimer);
|
|
765
|
+
}
|
|
766
|
+
this.conversationScanTimer = setTimeout(() => {
|
|
767
|
+
this.conversationScanTimer = null;
|
|
768
|
+
this.scanConversationSurfaces();
|
|
769
|
+
}, 300);
|
|
770
|
+
}
|
|
771
|
+
scanConversationSurfaces() {
|
|
772
|
+
const containers = this.findConversationContainers();
|
|
773
|
+
if (containers.length === 0) return;
|
|
774
|
+
containers.forEach((container) => {
|
|
775
|
+
const snapshot = this.buildConversationSnapshot(container);
|
|
776
|
+
if (!snapshot || snapshot.messages.length === 0) return;
|
|
777
|
+
const existing = this.observedConversations.get(snapshot.conversationId);
|
|
778
|
+
const newMessages = snapshot.messages.map((message, index) => ({
|
|
779
|
+
message,
|
|
780
|
+
index,
|
|
781
|
+
fingerprint: this.getMessageFingerprint(snapshot.conversationId, message, index)
|
|
782
|
+
})).filter(({ fingerprint }) => !existing?.messageFingerprints.has(fingerprint));
|
|
783
|
+
if (newMessages.length === 0 && existing?.signature === snapshot.signature) {
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
newMessages.forEach(({ message, index, fingerprint }) => {
|
|
787
|
+
this.pushConversationMessage(message, {
|
|
788
|
+
conversationId: snapshot.conversationId,
|
|
789
|
+
turnIndex: index,
|
|
790
|
+
source: "dom",
|
|
791
|
+
conversationTitle: snapshot.title,
|
|
792
|
+
pageTitle: document.title,
|
|
793
|
+
path: location.pathname
|
|
794
|
+
});
|
|
795
|
+
snapshot.messageFingerprints.add(fingerprint);
|
|
796
|
+
});
|
|
797
|
+
if (!existing || existing.signature !== snapshot.signature) {
|
|
798
|
+
this.pushConversationSummary(snapshot.conversationId, snapshot.messages, {
|
|
799
|
+
source: "dom",
|
|
800
|
+
conversationTitle: snapshot.title,
|
|
801
|
+
pageTitle: document.title,
|
|
802
|
+
path: location.pathname
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
this.observedConversations.set(snapshot.conversationId, snapshot);
|
|
806
|
+
});
|
|
807
|
+
}
|
|
483
808
|
captureAPIErrors() {
|
|
484
809
|
this.errorHandler = (e) => {
|
|
485
810
|
this.pushEvent("api_error", {
|
|
@@ -491,9 +816,37 @@ var LLMDataModule = class {
|
|
|
491
816
|
};
|
|
492
817
|
window.addEventListener("error", this.errorHandler);
|
|
493
818
|
}
|
|
819
|
+
pushConversationMessage(message, meta) {
|
|
820
|
+
const content = this.scrub(message.content || "");
|
|
821
|
+
if (!content) return;
|
|
822
|
+
this.pushEvent("llm_message", {
|
|
823
|
+
role: this.normalizeRole(message.role),
|
|
824
|
+
content,
|
|
825
|
+
...message.name ? { name: this.scrub(message.name) } : {},
|
|
826
|
+
...meta
|
|
827
|
+
});
|
|
828
|
+
}
|
|
829
|
+
pushConversationSummary(conversationId, messages, meta) {
|
|
830
|
+
const preview = messages.map((message) => ({
|
|
831
|
+
role: this.normalizeRole(message.role),
|
|
832
|
+
content: this.scrub(message.content || ""),
|
|
833
|
+
...message.name ? { name: this.scrub(message.name) } : {}
|
|
834
|
+
})).filter((message) => message.content).slice(0, 12);
|
|
835
|
+
if (preview.length === 0) return;
|
|
836
|
+
this.pushEvent("llm_conversation", {
|
|
837
|
+
conversationId,
|
|
838
|
+
messageCount: preview.length,
|
|
839
|
+
roles: Array.from(new Set(preview.map((message) => message.role))),
|
|
840
|
+
preview,
|
|
841
|
+
...meta
|
|
842
|
+
});
|
|
843
|
+
}
|
|
494
844
|
pushEvent(type, data) {
|
|
845
|
+
if (!this.isSampledSession) return;
|
|
495
846
|
this.buffer.push({ type, data, timestamp: Date.now() });
|
|
496
|
-
if (this.buffer.length >= 50)
|
|
847
|
+
if (this.buffer.length >= 50) {
|
|
848
|
+
void this.flush();
|
|
849
|
+
}
|
|
497
850
|
}
|
|
498
851
|
async flush() {
|
|
499
852
|
if (this.buffer.length === 0) return;
|
|
@@ -506,25 +859,286 @@ var LLMDataModule = class {
|
|
|
506
859
|
this.buffer.unshift(...events);
|
|
507
860
|
}
|
|
508
861
|
}
|
|
509
|
-
/** Basic PII scrubbing */
|
|
510
862
|
scrub(text) {
|
|
511
|
-
return text.replace(
|
|
863
|
+
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
864
|
}
|
|
513
865
|
getPath(el) {
|
|
514
866
|
const parts = [];
|
|
515
|
-
let
|
|
516
|
-
while (
|
|
517
|
-
let
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
867
|
+
let current = el;
|
|
868
|
+
while (current && parts.length < 5) {
|
|
869
|
+
let segment = current.tagName?.toLowerCase() || "";
|
|
870
|
+
if (current.id) {
|
|
871
|
+
segment += `#${current.id}`;
|
|
872
|
+
} else if (typeof current.className === "string" && current.className) {
|
|
873
|
+
const cls = current.className.split(" ")[0];
|
|
874
|
+
if (cls) segment += `.${cls}`;
|
|
522
875
|
}
|
|
523
|
-
parts.unshift(
|
|
524
|
-
|
|
876
|
+
parts.unshift(segment);
|
|
877
|
+
current = current.parentElement;
|
|
525
878
|
}
|
|
526
879
|
return parts.join(" > ");
|
|
527
880
|
}
|
|
881
|
+
canCapture(setting) {
|
|
882
|
+
return Boolean(this.config?.[setting] && this.isSampledSession);
|
|
883
|
+
}
|
|
884
|
+
sanitizeMeta(meta) {
|
|
885
|
+
if (!meta) return {};
|
|
886
|
+
return Object.fromEntries(
|
|
887
|
+
Object.entries(meta).map(([key, value]) => {
|
|
888
|
+
if (typeof value === "string") return [key, this.scrub(value)];
|
|
889
|
+
if (Array.isArray(value)) {
|
|
890
|
+
return [key, value.map((item) => typeof item === "string" ? this.scrub(item) : item)];
|
|
891
|
+
}
|
|
892
|
+
return [key, value];
|
|
893
|
+
})
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
extractRequestMeta(url, init) {
|
|
897
|
+
const endpoint = this.safePathname(url);
|
|
898
|
+
const requestId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
899
|
+
const parsed = this.parseRequestBody(init?.body);
|
|
900
|
+
const conversationId = this.getConversationId(parsed.meta);
|
|
901
|
+
return {
|
|
902
|
+
endpoint,
|
|
903
|
+
requestId,
|
|
904
|
+
conversationId,
|
|
905
|
+
messages: parsed.messages,
|
|
906
|
+
promptPreview: parsed.messages.map((message) => `[${message.role}] ${message.content}`).join("\n").slice(0, 1200),
|
|
907
|
+
meta: parsed.meta
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
parseRequestBody(body) {
|
|
911
|
+
if (!body || typeof body !== "string") {
|
|
912
|
+
return { messages: [], meta: {} };
|
|
913
|
+
}
|
|
914
|
+
try {
|
|
915
|
+
const parsed = JSON.parse(body);
|
|
916
|
+
const messages = this.extractMessagesFromUnknown(parsed);
|
|
917
|
+
const meta = {};
|
|
918
|
+
const keys = ["model", "conversationId", "conversation_id", "threadId", "thread_id", "sessionId", "session_id", "topic", "domain", "workflow"];
|
|
919
|
+
keys.forEach((key) => {
|
|
920
|
+
const value = parsed[key];
|
|
921
|
+
if (typeof value === "string" && value.trim()) {
|
|
922
|
+
meta[key] = this.scrub(value.trim().slice(0, 120));
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
return { messages, meta };
|
|
926
|
+
} catch {
|
|
927
|
+
return {
|
|
928
|
+
messages: [{ role: "user", content: this.scrub(body.slice(0, 2e3)) }],
|
|
929
|
+
meta: {}
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
extractMessagesFromUnknown(value) {
|
|
934
|
+
if (!value || typeof value !== "object") return [];
|
|
935
|
+
const record = value;
|
|
936
|
+
if (Array.isArray(record.messages)) {
|
|
937
|
+
return record.messages.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item?.content));
|
|
938
|
+
}
|
|
939
|
+
if (typeof record.prompt === "string") {
|
|
940
|
+
return [{ role: "user", content: this.scrub(record.prompt.slice(0, 2e3)) }];
|
|
941
|
+
}
|
|
942
|
+
if (Array.isArray(record.input)) {
|
|
943
|
+
return record.input.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item?.content));
|
|
944
|
+
}
|
|
945
|
+
if (typeof record.input === "string") {
|
|
946
|
+
return [{ role: "user", content: this.scrub(record.input.slice(0, 2e3)) }];
|
|
947
|
+
}
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
normalizeMessage(input) {
|
|
951
|
+
if (typeof input === "string") {
|
|
952
|
+
const content2 = this.scrub(input.slice(0, 2e3));
|
|
953
|
+
return content2 ? { role: "user", content: content2 } : null;
|
|
954
|
+
}
|
|
955
|
+
if (!input || typeof input !== "object") return null;
|
|
956
|
+
const record = input;
|
|
957
|
+
const role = this.normalizeRole(record.role);
|
|
958
|
+
const content = this.extractContent(record.content ?? record.text ?? record.message);
|
|
959
|
+
const name = typeof record.name === "string" ? this.scrub(record.name.slice(0, 120)) : void 0;
|
|
960
|
+
if (!content) return null;
|
|
961
|
+
return { role, content, ...name ? { name } : {} };
|
|
962
|
+
}
|
|
963
|
+
extractContent(value) {
|
|
964
|
+
if (typeof value === "string") return this.scrub(value.slice(0, 4e3));
|
|
965
|
+
if (Array.isArray(value)) {
|
|
966
|
+
return value.map((item) => {
|
|
967
|
+
if (typeof item === "string") return this.scrub(item);
|
|
968
|
+
if (item && typeof item === "object") {
|
|
969
|
+
const record = item;
|
|
970
|
+
if (typeof record.text === "string") return this.scrub(record.text);
|
|
971
|
+
}
|
|
972
|
+
return "";
|
|
973
|
+
}).filter(Boolean).join("\n").slice(0, 4e3);
|
|
974
|
+
}
|
|
975
|
+
return "";
|
|
976
|
+
}
|
|
977
|
+
extractResponseMessages(text) {
|
|
978
|
+
const trimmed = text.trim();
|
|
979
|
+
if (!trimmed) return [];
|
|
980
|
+
try {
|
|
981
|
+
const parsed = JSON.parse(trimmed);
|
|
982
|
+
const messages = this.extractAssistantMessages(parsed);
|
|
983
|
+
if (messages.length > 0) return messages;
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
return [{ role: "assistant", content: this.scrub(trimmed.slice(0, 4e3)) }];
|
|
987
|
+
}
|
|
988
|
+
extractAssistantMessages(value) {
|
|
989
|
+
if (!value || typeof value !== "object") return [];
|
|
990
|
+
const record = value;
|
|
991
|
+
const direct = this.normalizeMessage(record.message ?? record.output ?? record.response ?? record.answer);
|
|
992
|
+
if (direct) {
|
|
993
|
+
return [{ ...direct, role: direct.role === "user" ? "assistant" : direct.role }];
|
|
994
|
+
}
|
|
995
|
+
if (Array.isArray(record.choices)) {
|
|
996
|
+
return record.choices.map((choice) => {
|
|
997
|
+
if (!choice || typeof choice !== "object") return null;
|
|
998
|
+
const payload = choice;
|
|
999
|
+
const normalized = this.normalizeMessage(payload.message ?? payload.delta ?? payload.text);
|
|
1000
|
+
if (!normalized) return null;
|
|
1001
|
+
return {
|
|
1002
|
+
...normalized,
|
|
1003
|
+
role: normalized.role === "user" ? "assistant" : normalized.role
|
|
1004
|
+
};
|
|
1005
|
+
}).filter((item) => Boolean(item && item.content));
|
|
1006
|
+
}
|
|
1007
|
+
if (Array.isArray(record.messages)) {
|
|
1008
|
+
return record.messages.map((item) => this.normalizeMessage(item)).filter((item) => Boolean(item && item.content)).map((item) => ({
|
|
1009
|
+
...item,
|
|
1010
|
+
role: item.role === "user" ? "assistant" : item.role
|
|
1011
|
+
}));
|
|
1012
|
+
}
|
|
1013
|
+
return [];
|
|
1014
|
+
}
|
|
1015
|
+
findConversationContainers() {
|
|
1016
|
+
const selectors = [
|
|
1017
|
+
"[data-chat-thread]",
|
|
1018
|
+
"[data-conversation]",
|
|
1019
|
+
'[data-testid*="chat" i]',
|
|
1020
|
+
'[data-testid*="conversation" i]',
|
|
1021
|
+
'[aria-label*="chat" i]',
|
|
1022
|
+
'[aria-label*="conversation" i]',
|
|
1023
|
+
'[role="log"]',
|
|
1024
|
+
'[role="feed"]',
|
|
1025
|
+
"main"
|
|
1026
|
+
];
|
|
1027
|
+
const found = selectors.flatMap((selector) => Array.from(document.querySelectorAll(selector))).filter((element) => this.looksLikeConversationContainer(element));
|
|
1028
|
+
return Array.from(new Set(found)).slice(0, 4);
|
|
1029
|
+
}
|
|
1030
|
+
looksLikeConversationContainer(element) {
|
|
1031
|
+
const marker = [
|
|
1032
|
+
element.dataset.chatThread,
|
|
1033
|
+
element.dataset.conversation,
|
|
1034
|
+
element.getAttribute("aria-label"),
|
|
1035
|
+
element.getAttribute("data-testid"),
|
|
1036
|
+
typeof element.className === "string" ? element.className : "",
|
|
1037
|
+
element.id
|
|
1038
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1039
|
+
if (/chat|conversation|assistant|thread|messages/.test(marker)) {
|
|
1040
|
+
return true;
|
|
1041
|
+
}
|
|
1042
|
+
const messageNodes = element.querySelectorAll('[data-message-author-role], [data-role], [role="article"], article, [data-testid*="message" i]');
|
|
1043
|
+
return messageNodes.length >= 2;
|
|
1044
|
+
}
|
|
1045
|
+
buildConversationSnapshot(container) {
|
|
1046
|
+
const messageNodes = Array.from(
|
|
1047
|
+
container.querySelectorAll('[data-message-author-role], [data-role], [role="article"], article, [data-testid*="message" i], [class*="message"], [class*="chat"]')
|
|
1048
|
+
).filter((node) => this.isUsableMessageNode(node)).slice(0, 80);
|
|
1049
|
+
const messages = messageNodes.map((node) => this.messageFromNode(node)).filter((message) => Boolean(message?.content));
|
|
1050
|
+
if (messages.length < 2) return null;
|
|
1051
|
+
const title = this.extractConversationTitle(container);
|
|
1052
|
+
const conversationId = this.getConversationId({ conversationTitle: title, path: location.pathname }, container);
|
|
1053
|
+
const signature = messages.map((message) => `${message.role}:${message.content}`).join("|").slice(0, 6e3);
|
|
1054
|
+
return {
|
|
1055
|
+
conversationId,
|
|
1056
|
+
signature,
|
|
1057
|
+
messageFingerprints: new Set(messages.map((message, index) => this.getMessageFingerprint(conversationId, message, index))),
|
|
1058
|
+
messages,
|
|
1059
|
+
title
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
isUsableMessageNode(node) {
|
|
1063
|
+
const text = node.innerText?.trim() || "";
|
|
1064
|
+
if (!text || text.length < 2) return false;
|
|
1065
|
+
const marker = `${typeof node.className === "string" ? node.className : ""} ${node.getAttribute("data-testid") || ""} ${node.getAttribute("aria-label") || ""}`.toLowerCase();
|
|
1066
|
+
if (/input|textarea|composer|toolbar|button|copy code/.test(marker)) {
|
|
1067
|
+
return false;
|
|
1068
|
+
}
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
messageFromNode(node) {
|
|
1072
|
+
const text = this.scrub((node.innerText || "").trim().slice(0, 4e3));
|
|
1073
|
+
if (!text) return null;
|
|
1074
|
+
return {
|
|
1075
|
+
role: this.detectRole(node, text),
|
|
1076
|
+
content: text
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
detectRole(node, text) {
|
|
1080
|
+
const marker = [
|
|
1081
|
+
node.dataset.messageAuthorRole,
|
|
1082
|
+
node.dataset.role,
|
|
1083
|
+
node.getAttribute("data-role"),
|
|
1084
|
+
node.getAttribute("aria-label"),
|
|
1085
|
+
typeof node.className === "string" ? node.className : "",
|
|
1086
|
+
node.id,
|
|
1087
|
+
node.closest("[data-message-author-role]")?.getAttribute("data-message-author-role")
|
|
1088
|
+
].filter(Boolean).join(" ").toLowerCase();
|
|
1089
|
+
if (/assistant|bot|ai|model|gpt|claude|copilot/.test(marker)) return "assistant";
|
|
1090
|
+
if (/user|human|prompt|customer|visitor/.test(marker)) return "user";
|
|
1091
|
+
if (/system/.test(marker)) return "system";
|
|
1092
|
+
if (/tool|function/.test(marker)) return "tool";
|
|
1093
|
+
const alignment = window.getComputedStyle(node).textAlign;
|
|
1094
|
+
if (alignment === "right") return "user";
|
|
1095
|
+
const lower = text.toLowerCase();
|
|
1096
|
+
if (lower.startsWith("you:")) return "user";
|
|
1097
|
+
if (lower.startsWith("assistant:") || lower.startsWith("ai:")) return "assistant";
|
|
1098
|
+
return "unknown";
|
|
1099
|
+
}
|
|
1100
|
+
extractConversationTitle(container) {
|
|
1101
|
+
const heading = container.querySelector('h1, h2, h3, [data-testid*="title" i], [class*="title"]');
|
|
1102
|
+
return this.scrub(heading?.innerText?.trim().slice(0, 160) || document.title || "conversation");
|
|
1103
|
+
}
|
|
1104
|
+
getConversationId(meta, container) {
|
|
1105
|
+
const explicit = [
|
|
1106
|
+
meta?.conversationId,
|
|
1107
|
+
meta?.conversation_id,
|
|
1108
|
+
meta?.threadId,
|
|
1109
|
+
meta?.thread_id,
|
|
1110
|
+
meta?.sessionId,
|
|
1111
|
+
meta?.session_id
|
|
1112
|
+
].find((value) => typeof value === "string" && value.trim().length > 0);
|
|
1113
|
+
if (explicit) return this.scrub(explicit).slice(0, 120);
|
|
1114
|
+
if (container) {
|
|
1115
|
+
const existing = container.dataset.zcConversationId;
|
|
1116
|
+
if (existing) return existing;
|
|
1117
|
+
const generated = `conv-${location.pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "root"}-${++this.conversationCounter}`;
|
|
1118
|
+
container.dataset.zcConversationId = generated;
|
|
1119
|
+
return generated;
|
|
1120
|
+
}
|
|
1121
|
+
return `conv-${location.pathname.replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "") || "root"}-${Date.now()}`;
|
|
1122
|
+
}
|
|
1123
|
+
getMessageFingerprint(conversationId, message, index) {
|
|
1124
|
+
return `${conversationId}:${index}:${message.role}:${message.content.slice(0, 240)}`;
|
|
1125
|
+
}
|
|
1126
|
+
safePathname(url) {
|
|
1127
|
+
try {
|
|
1128
|
+
return new URL(url, window.location.origin).pathname;
|
|
1129
|
+
} catch {
|
|
1130
|
+
return url;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
normalizeRole(value) {
|
|
1134
|
+
if (typeof value !== "string") return "unknown";
|
|
1135
|
+
const normalized = value.toLowerCase();
|
|
1136
|
+
if (normalized.includes("assistant") || normalized.includes("bot") || normalized.includes("ai") || normalized.includes("model")) return "assistant";
|
|
1137
|
+
if (normalized.includes("user") || normalized.includes("human")) return "user";
|
|
1138
|
+
if (normalized.includes("system")) return "system";
|
|
1139
|
+
if (normalized.includes("tool") || normalized.includes("function")) return "tool";
|
|
1140
|
+
return "unknown";
|
|
1141
|
+
}
|
|
528
1142
|
};
|
|
529
1143
|
|
|
530
1144
|
// src/modules/recording.ts
|
|
@@ -690,8 +1304,611 @@ var RecordingModule = class {
|
|
|
690
1304
|
}
|
|
691
1305
|
};
|
|
692
1306
|
|
|
1307
|
+
// src/core/consent-ui.ts
|
|
1308
|
+
var STYLE_ID = "zerocost-consent-styles";
|
|
1309
|
+
function injectStyles(theme) {
|
|
1310
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
1311
|
+
const darkVars = `
|
|
1312
|
+
--zc-bg: #111111;
|
|
1313
|
+
--zc-surface: #1a1a1a;
|
|
1314
|
+
--zc-border: #2a2a2a;
|
|
1315
|
+
--zc-text: #ffffff;
|
|
1316
|
+
--zc-text-secondary: #999999;
|
|
1317
|
+
--zc-accent: #ffffff;
|
|
1318
|
+
--zc-accent-bg: #ffffff;
|
|
1319
|
+
--zc-accent-fg: #000000;
|
|
1320
|
+
--zc-toggle-off-bg: #333333;
|
|
1321
|
+
--zc-toggle-on-bg: #00e599;
|
|
1322
|
+
--zc-toggle-knob: #ffffff;
|
|
1323
|
+
--zc-backdrop: rgba(0,0,0,0.65);
|
|
1324
|
+
--zc-link: #888888;
|
|
1325
|
+
--zc-link-hover: #cccccc;
|
|
1326
|
+
`;
|
|
1327
|
+
const lightVars = `
|
|
1328
|
+
--zc-bg: #ffffff;
|
|
1329
|
+
--zc-surface: #f5f5f5;
|
|
1330
|
+
--zc-border: #e0e0e0;
|
|
1331
|
+
--zc-text: #111111;
|
|
1332
|
+
--zc-text-secondary: #666666;
|
|
1333
|
+
--zc-accent: #111111;
|
|
1334
|
+
--zc-accent-bg: #111111;
|
|
1335
|
+
--zc-accent-fg: #ffffff;
|
|
1336
|
+
--zc-toggle-off-bg: #cccccc;
|
|
1337
|
+
--zc-toggle-on-bg: #00c77d;
|
|
1338
|
+
--zc-toggle-knob: #ffffff;
|
|
1339
|
+
--zc-backdrop: rgba(0,0,0,0.45);
|
|
1340
|
+
--zc-link: #666666;
|
|
1341
|
+
--zc-link-hover: #111111;
|
|
1342
|
+
`;
|
|
1343
|
+
let themeRule;
|
|
1344
|
+
if (theme === "dark") {
|
|
1345
|
+
themeRule = `.zc-consent-root { ${darkVars} }`;
|
|
1346
|
+
} else if (theme === "light") {
|
|
1347
|
+
themeRule = `.zc-consent-root { ${lightVars} }`;
|
|
1348
|
+
} else {
|
|
1349
|
+
themeRule = `
|
|
1350
|
+
.zc-consent-root { ${lightVars} }
|
|
1351
|
+
@media (prefers-color-scheme: dark) {
|
|
1352
|
+
.zc-consent-root { ${darkVars} }
|
|
1353
|
+
}
|
|
1354
|
+
`;
|
|
1355
|
+
}
|
|
1356
|
+
const css = `
|
|
1357
|
+
${themeRule}
|
|
1358
|
+
|
|
1359
|
+
.zc-consent-root * {
|
|
1360
|
+
box-sizing: border-box;
|
|
1361
|
+
margin: 0;
|
|
1362
|
+
padding: 0;
|
|
1363
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
.zc-consent-backdrop {
|
|
1367
|
+
position: fixed;
|
|
1368
|
+
inset: 0;
|
|
1369
|
+
z-index: 999999;
|
|
1370
|
+
background: var(--zc-backdrop);
|
|
1371
|
+
display: flex;
|
|
1372
|
+
align-items: center;
|
|
1373
|
+
justify-content: center;
|
|
1374
|
+
animation: zc-fade-in 200ms ease;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
@keyframes zc-fade-in {
|
|
1378
|
+
from { opacity: 0; }
|
|
1379
|
+
to { opacity: 1; }
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
@keyframes zc-slide-up {
|
|
1383
|
+
from { transform: translateY(100%); }
|
|
1384
|
+
to { transform: translateY(0); }
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
.zc-consent-card {
|
|
1388
|
+
background: var(--zc-bg);
|
|
1389
|
+
border: 1px solid var(--zc-border);
|
|
1390
|
+
border-radius: 16px;
|
|
1391
|
+
width: 100%;
|
|
1392
|
+
max-width: 460px;
|
|
1393
|
+
max-height: 90vh;
|
|
1394
|
+
overflow-y: auto;
|
|
1395
|
+
padding: 28px 24px 24px;
|
|
1396
|
+
animation: zc-fade-in 200ms ease;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
/* Mobile: bottom-sheet style */
|
|
1400
|
+
@media (max-width: 640px) {
|
|
1401
|
+
.zc-consent-backdrop {
|
|
1402
|
+
align-items: flex-end;
|
|
1403
|
+
}
|
|
1404
|
+
.zc-consent-card {
|
|
1405
|
+
border-radius: 20px 20px 0 0;
|
|
1406
|
+
max-width: 100%;
|
|
1407
|
+
animation: zc-slide-up 200ms ease;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/* Scrollbar */
|
|
1412
|
+
.zc-consent-card::-webkit-scrollbar { width: 4px; }
|
|
1413
|
+
.zc-consent-card::-webkit-scrollbar-thumb { background: var(--zc-border); border-radius: 4px; }
|
|
1414
|
+
|
|
1415
|
+
.zc-consent-header {
|
|
1416
|
+
display: flex;
|
|
1417
|
+
align-items: center;
|
|
1418
|
+
gap: 10px;
|
|
1419
|
+
margin-bottom: 6px;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
.zc-consent-logo {
|
|
1423
|
+
width: 28px;
|
|
1424
|
+
height: 28px;
|
|
1425
|
+
border-radius: 6px;
|
|
1426
|
+
background: var(--zc-accent-bg);
|
|
1427
|
+
display: flex;
|
|
1428
|
+
align-items: center;
|
|
1429
|
+
justify-content: center;
|
|
1430
|
+
flex-shrink: 0;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
.zc-consent-logo svg {
|
|
1434
|
+
width: 16px;
|
|
1435
|
+
height: 16px;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
.zc-consent-title {
|
|
1439
|
+
font-size: 16px;
|
|
1440
|
+
font-weight: 700;
|
|
1441
|
+
color: var(--zc-text);
|
|
1442
|
+
letter-spacing: -0.02em;
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
.zc-consent-subtitle {
|
|
1446
|
+
font-size: 13px;
|
|
1447
|
+
color: var(--zc-text-secondary);
|
|
1448
|
+
line-height: 1.5;
|
|
1449
|
+
margin-bottom: 20px;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
.zc-consent-toggles {
|
|
1453
|
+
display: flex;
|
|
1454
|
+
flex-direction: column;
|
|
1455
|
+
gap: 10px;
|
|
1456
|
+
margin-bottom: 20px;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
.zc-consent-toggle-card {
|
|
1460
|
+
background: var(--zc-surface);
|
|
1461
|
+
border: 1px solid var(--zc-border);
|
|
1462
|
+
border-radius: 12px;
|
|
1463
|
+
padding: 14px 16px;
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
.zc-consent-toggle-row {
|
|
1467
|
+
display: flex;
|
|
1468
|
+
align-items: center;
|
|
1469
|
+
justify-content: space-between;
|
|
1470
|
+
margin-bottom: 6px;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
.zc-consent-toggle-label {
|
|
1474
|
+
font-size: 14px;
|
|
1475
|
+
font-weight: 600;
|
|
1476
|
+
color: var(--zc-text);
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
.zc-consent-toggle-desc {
|
|
1480
|
+
font-size: 12px;
|
|
1481
|
+
color: var(--zc-text-secondary);
|
|
1482
|
+
line-height: 1.5;
|
|
1483
|
+
margin-bottom: 4px;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
.zc-consent-learn-more {
|
|
1487
|
+
font-size: 11px;
|
|
1488
|
+
color: var(--zc-link);
|
|
1489
|
+
text-decoration: none;
|
|
1490
|
+
cursor: pointer;
|
|
1491
|
+
transition: color 150ms;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
.zc-consent-learn-more:hover {
|
|
1495
|
+
color: var(--zc-link-hover);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
/* Toggle switch */
|
|
1499
|
+
.zc-toggle {
|
|
1500
|
+
position: relative;
|
|
1501
|
+
width: 40px;
|
|
1502
|
+
height: 22px;
|
|
1503
|
+
flex-shrink: 0;
|
|
1504
|
+
cursor: pointer;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
.zc-toggle input {
|
|
1508
|
+
opacity: 0;
|
|
1509
|
+
width: 0;
|
|
1510
|
+
height: 0;
|
|
1511
|
+
position: absolute;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
.zc-toggle-track {
|
|
1515
|
+
position: absolute;
|
|
1516
|
+
inset: 0;
|
|
1517
|
+
background: var(--zc-toggle-off-bg);
|
|
1518
|
+
border-radius: 11px;
|
|
1519
|
+
transition: background 200ms ease;
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
.zc-toggle input:checked + .zc-toggle-track {
|
|
1523
|
+
background: var(--zc-toggle-on-bg);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
.zc-toggle-knob {
|
|
1527
|
+
position: absolute;
|
|
1528
|
+
top: 2px;
|
|
1529
|
+
left: 2px;
|
|
1530
|
+
width: 18px;
|
|
1531
|
+
height: 18px;
|
|
1532
|
+
background: var(--zc-toggle-knob);
|
|
1533
|
+
border-radius: 50%;
|
|
1534
|
+
transition: transform 200ms ease;
|
|
1535
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
.zc-toggle input:checked ~ .zc-toggle-knob {
|
|
1539
|
+
transform: translateX(18px);
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/* Footer */
|
|
1543
|
+
.zc-consent-footer {
|
|
1544
|
+
display: flex;
|
|
1545
|
+
flex-wrap: wrap;
|
|
1546
|
+
gap: 4px 12px;
|
|
1547
|
+
justify-content: center;
|
|
1548
|
+
margin-bottom: 16px;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
.zc-consent-footer a {
|
|
1552
|
+
font-size: 11px;
|
|
1553
|
+
color: var(--zc-link);
|
|
1554
|
+
text-decoration: none;
|
|
1555
|
+
transition: color 150ms;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.zc-consent-footer a:hover {
|
|
1559
|
+
color: var(--zc-link-hover);
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
.zc-consent-footer-sep {
|
|
1563
|
+
font-size: 11px;
|
|
1564
|
+
color: var(--zc-link);
|
|
1565
|
+
opacity: 0.5;
|
|
1566
|
+
}
|
|
1567
|
+
|
|
1568
|
+
/* Confirm button */
|
|
1569
|
+
.zc-consent-confirm {
|
|
1570
|
+
display: block;
|
|
1571
|
+
width: 100%;
|
|
1572
|
+
padding: 12px;
|
|
1573
|
+
font-size: 14px;
|
|
1574
|
+
font-weight: 600;
|
|
1575
|
+
border: none;
|
|
1576
|
+
border-radius: 10px;
|
|
1577
|
+
cursor: pointer;
|
|
1578
|
+
background: var(--zc-accent-bg);
|
|
1579
|
+
color: var(--zc-accent-fg);
|
|
1580
|
+
letter-spacing: -0.01em;
|
|
1581
|
+
transition: opacity 150ms;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
.zc-consent-confirm:hover {
|
|
1585
|
+
opacity: 0.88;
|
|
1586
|
+
}
|
|
1587
|
+
|
|
1588
|
+
.zc-consent-confirm:active {
|
|
1589
|
+
opacity: 0.75;
|
|
1590
|
+
}
|
|
1591
|
+
`;
|
|
1592
|
+
const style = document.createElement("style");
|
|
1593
|
+
style.id = STYLE_ID;
|
|
1594
|
+
style.textContent = css;
|
|
1595
|
+
document.head.appendChild(style);
|
|
1596
|
+
}
|
|
1597
|
+
function createToggle(id, checked) {
|
|
1598
|
+
const label = document.createElement("label");
|
|
1599
|
+
label.className = "zc-toggle";
|
|
1600
|
+
const input = document.createElement("input");
|
|
1601
|
+
input.type = "checkbox";
|
|
1602
|
+
input.checked = checked;
|
|
1603
|
+
input.id = id;
|
|
1604
|
+
const track = document.createElement("span");
|
|
1605
|
+
track.className = "zc-toggle-track";
|
|
1606
|
+
const knob = document.createElement("span");
|
|
1607
|
+
knob.className = "zc-toggle-knob";
|
|
1608
|
+
label.appendChild(input);
|
|
1609
|
+
label.appendChild(track);
|
|
1610
|
+
label.appendChild(knob);
|
|
1611
|
+
return label;
|
|
1612
|
+
}
|
|
1613
|
+
function createToggleCard(toggleId, title, description, learnMoreUrl, defaultOn) {
|
|
1614
|
+
const card = document.createElement("div");
|
|
1615
|
+
card.className = "zc-consent-toggle-card";
|
|
1616
|
+
const row = document.createElement("div");
|
|
1617
|
+
row.className = "zc-consent-toggle-row";
|
|
1618
|
+
const labelSpan = document.createElement("span");
|
|
1619
|
+
labelSpan.className = "zc-consent-toggle-label";
|
|
1620
|
+
labelSpan.textContent = title;
|
|
1621
|
+
const toggle = createToggle(toggleId, defaultOn);
|
|
1622
|
+
row.appendChild(labelSpan);
|
|
1623
|
+
row.appendChild(toggle);
|
|
1624
|
+
card.appendChild(row);
|
|
1625
|
+
const desc = document.createElement("div");
|
|
1626
|
+
desc.className = "zc-consent-toggle-desc";
|
|
1627
|
+
desc.textContent = description;
|
|
1628
|
+
card.appendChild(desc);
|
|
1629
|
+
const link = document.createElement("a");
|
|
1630
|
+
link.className = "zc-consent-learn-more";
|
|
1631
|
+
link.href = learnMoreUrl;
|
|
1632
|
+
link.target = "_blank";
|
|
1633
|
+
link.rel = "noopener noreferrer";
|
|
1634
|
+
link.textContent = "Learn more \u2197";
|
|
1635
|
+
card.appendChild(link);
|
|
1636
|
+
return card;
|
|
1637
|
+
}
|
|
1638
|
+
function showConsentUI(options) {
|
|
1639
|
+
return new Promise((resolve) => {
|
|
1640
|
+
const { appName, theme, privacyPolicyUrl } = options;
|
|
1641
|
+
const defaults = options.defaults ?? { ads: true, usageData: false, aiInteractions: false };
|
|
1642
|
+
injectStyles(theme);
|
|
1643
|
+
const root = document.createElement("div");
|
|
1644
|
+
root.className = "zc-consent-root";
|
|
1645
|
+
const backdrop = document.createElement("div");
|
|
1646
|
+
backdrop.className = "zc-consent-backdrop";
|
|
1647
|
+
const blockEscape = (e) => {
|
|
1648
|
+
if (e.key === "Escape") {
|
|
1649
|
+
e.preventDefault();
|
|
1650
|
+
e.stopPropagation();
|
|
1651
|
+
}
|
|
1652
|
+
};
|
|
1653
|
+
document.addEventListener("keydown", blockEscape, true);
|
|
1654
|
+
const card = document.createElement("div");
|
|
1655
|
+
card.className = "zc-consent-card";
|
|
1656
|
+
const header = document.createElement("div");
|
|
1657
|
+
header.className = "zc-consent-header";
|
|
1658
|
+
const logo = document.createElement("div");
|
|
1659
|
+
logo.className = "zc-consent-logo";
|
|
1660
|
+
logo.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" style="color:var(--zc-accent-fg)"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>`;
|
|
1661
|
+
const title = document.createElement("div");
|
|
1662
|
+
title.className = "zc-consent-title";
|
|
1663
|
+
title.textContent = `${appName || "This app"} uses Zerocost`;
|
|
1664
|
+
header.appendChild(logo);
|
|
1665
|
+
header.appendChild(title);
|
|
1666
|
+
card.appendChild(header);
|
|
1667
|
+
const subtitle = document.createElement("div");
|
|
1668
|
+
subtitle.className = "zc-consent-subtitle";
|
|
1669
|
+
subtitle.textContent = "Manage your preferences below. You can update these anytime.";
|
|
1670
|
+
card.appendChild(subtitle);
|
|
1671
|
+
const toggles = document.createElement("div");
|
|
1672
|
+
toggles.className = "zc-consent-toggles";
|
|
1673
|
+
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
1674
|
+
toggles.appendChild(createToggleCard(
|
|
1675
|
+
"zc-toggle-ads",
|
|
1676
|
+
"Ads",
|
|
1677
|
+
"Contextual, non-intrusive ads. No cookies or browsing history used.",
|
|
1678
|
+
`${baseUrl}/consent/ads`,
|
|
1679
|
+
defaults.ads
|
|
1680
|
+
));
|
|
1681
|
+
toggles.appendChild(createToggleCard(
|
|
1682
|
+
"zc-toggle-usage",
|
|
1683
|
+
"Usage data",
|
|
1684
|
+
"Anonymized usage patterns. No personal information is shared.",
|
|
1685
|
+
`${baseUrl}/consent/usage-data`,
|
|
1686
|
+
defaults.usageData
|
|
1687
|
+
));
|
|
1688
|
+
toggles.appendChild(createToggleCard(
|
|
1689
|
+
"zc-toggle-ai",
|
|
1690
|
+
"AI interactions",
|
|
1691
|
+
"Anonymized conversation data used for AI research.",
|
|
1692
|
+
`${baseUrl}/consent/ai-interactions`,
|
|
1693
|
+
defaults.aiInteractions
|
|
1694
|
+
));
|
|
1695
|
+
card.appendChild(toggles);
|
|
1696
|
+
const footer = document.createElement("div");
|
|
1697
|
+
footer.className = "zc-consent-footer";
|
|
1698
|
+
const ppLink = document.createElement("a");
|
|
1699
|
+
ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
|
|
1700
|
+
ppLink.target = "_blank";
|
|
1701
|
+
ppLink.rel = "noopener noreferrer";
|
|
1702
|
+
ppLink.textContent = "Privacy Policy";
|
|
1703
|
+
footer.appendChild(ppLink);
|
|
1704
|
+
const sep1 = document.createElement("span");
|
|
1705
|
+
sep1.className = "zc-consent-footer-sep";
|
|
1706
|
+
sep1.textContent = "\xB7";
|
|
1707
|
+
footer.appendChild(sep1);
|
|
1708
|
+
const termsLink = document.createElement("a");
|
|
1709
|
+
termsLink.href = `${baseUrl}/terms`;
|
|
1710
|
+
termsLink.target = "_blank";
|
|
1711
|
+
termsLink.rel = "noopener noreferrer";
|
|
1712
|
+
termsLink.textContent = "Terms";
|
|
1713
|
+
footer.appendChild(termsLink);
|
|
1714
|
+
const sep2 = document.createElement("span");
|
|
1715
|
+
sep2.className = "zc-consent-footer-sep";
|
|
1716
|
+
sep2.textContent = "\xB7";
|
|
1717
|
+
footer.appendChild(sep2);
|
|
1718
|
+
const dnsLink = document.createElement("a");
|
|
1719
|
+
dnsLink.href = `${baseUrl}/do-not-sell`;
|
|
1720
|
+
dnsLink.target = "_blank";
|
|
1721
|
+
dnsLink.rel = "noopener noreferrer";
|
|
1722
|
+
dnsLink.textContent = "Do Not Sell My Data";
|
|
1723
|
+
footer.appendChild(dnsLink);
|
|
1724
|
+
card.appendChild(footer);
|
|
1725
|
+
const confirmBtn = document.createElement("button");
|
|
1726
|
+
confirmBtn.className = "zc-consent-confirm";
|
|
1727
|
+
confirmBtn.textContent = "Confirm";
|
|
1728
|
+
confirmBtn.addEventListener("click", () => {
|
|
1729
|
+
const ads = document.getElementById("zc-toggle-ads")?.checked ?? defaults.ads;
|
|
1730
|
+
const usageData = document.getElementById("zc-toggle-usage")?.checked ?? defaults.usageData;
|
|
1731
|
+
const aiInteractions = document.getElementById("zc-toggle-ai")?.checked ?? defaults.aiInteractions;
|
|
1732
|
+
document.removeEventListener("keydown", blockEscape, true);
|
|
1733
|
+
root.remove();
|
|
1734
|
+
resolve({ ads, usageData, aiInteractions });
|
|
1735
|
+
});
|
|
1736
|
+
card.appendChild(confirmBtn);
|
|
1737
|
+
backdrop.appendChild(card);
|
|
1738
|
+
root.appendChild(backdrop);
|
|
1739
|
+
document.body.appendChild(root);
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
function removeConsentUI() {
|
|
1743
|
+
document.querySelector(".zc-consent-root")?.remove();
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// src/core/consent.ts
|
|
1747
|
+
var CONSENT_VERSION = "1.1";
|
|
1748
|
+
var CONSENT_STORAGE_PREFIX = "zerocost-consent:";
|
|
1749
|
+
var TWELVE_MONTHS_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1750
|
+
var ConsentManager = class {
|
|
1751
|
+
record = null;
|
|
1752
|
+
needsReset = false;
|
|
1753
|
+
client;
|
|
1754
|
+
consentConfig;
|
|
1755
|
+
appName;
|
|
1756
|
+
theme;
|
|
1757
|
+
constructor(client, opts) {
|
|
1758
|
+
this.client = client;
|
|
1759
|
+
this.consentConfig = opts.consent ?? {};
|
|
1760
|
+
this.appName = opts.appName ?? "";
|
|
1761
|
+
this.theme = opts.theme ?? "dark";
|
|
1762
|
+
this.hydrateFromStorage();
|
|
1763
|
+
}
|
|
1764
|
+
// ── Public API (per spec §6.3) ───────────────────────────────────
|
|
1765
|
+
/** Returns the current consent record, or null if none exists. */
|
|
1766
|
+
get() {
|
|
1767
|
+
return this.record;
|
|
1768
|
+
}
|
|
1769
|
+
/** Programmatically open the consent popup (e.g. from app settings). */
|
|
1770
|
+
async open() {
|
|
1771
|
+
removeConsentUI();
|
|
1772
|
+
await this.promptAndWait();
|
|
1773
|
+
}
|
|
1774
|
+
/** Clear consent — prompt will re-fire on next init(). */
|
|
1775
|
+
reset() {
|
|
1776
|
+
this.record = null;
|
|
1777
|
+
this.needsReset = true;
|
|
1778
|
+
this.clearStorage();
|
|
1779
|
+
this.client.log("Consent reset. Prompt will re-fire on next init().");
|
|
1780
|
+
}
|
|
1781
|
+
/** Restore a previously saved record (skip re-prompting if valid). */
|
|
1782
|
+
restore(record) {
|
|
1783
|
+
if (this.isValid(record)) {
|
|
1784
|
+
this.record = record;
|
|
1785
|
+
this.writeStorage(record);
|
|
1786
|
+
this.client.log("Consent restored from saved record.");
|
|
1787
|
+
} else {
|
|
1788
|
+
this.client.log("Restored record invalid (version/expiry). Will re-prompt.");
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
/** Check whether a specific feature is consented. */
|
|
1792
|
+
has(feature) {
|
|
1793
|
+
if (!this.record) return false;
|
|
1794
|
+
return !!this.record[feature];
|
|
1795
|
+
}
|
|
1796
|
+
// ── Internal (used by ZerocostSDK.init) ──────────────────────────
|
|
1797
|
+
/** Should the consent prompt be shown? */
|
|
1798
|
+
shouldPrompt() {
|
|
1799
|
+
if (this.needsReset) return true;
|
|
1800
|
+
if (!this.record) return true;
|
|
1801
|
+
if (!this.isValid(this.record)) return true;
|
|
1802
|
+
return false;
|
|
1803
|
+
}
|
|
1804
|
+
/** Show the consent popup, wait for confirmation, store record. */
|
|
1805
|
+
async promptAndWait() {
|
|
1806
|
+
this.needsReset = false;
|
|
1807
|
+
const result = await showConsentUI({
|
|
1808
|
+
appName: this.appName,
|
|
1809
|
+
theme: this.theme,
|
|
1810
|
+
privacyPolicyUrl: this.consentConfig.privacyPolicyUrl,
|
|
1811
|
+
defaults: this.record ? { ads: this.record.ads, usageData: this.record.usageData, aiInteractions: this.record.aiInteractions } : void 0
|
|
1812
|
+
});
|
|
1813
|
+
const userId = this.getOrCreateUserId();
|
|
1814
|
+
const record = {
|
|
1815
|
+
userId,
|
|
1816
|
+
appId: this.client.getConfig().appId,
|
|
1817
|
+
ads: result.ads,
|
|
1818
|
+
usageData: result.usageData,
|
|
1819
|
+
aiInteractions: result.aiInteractions,
|
|
1820
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1821
|
+
version: CONSENT_VERSION,
|
|
1822
|
+
method: "confirmed",
|
|
1823
|
+
ipRegion: "OTHER"
|
|
1824
|
+
// server can enrich via IP
|
|
1825
|
+
};
|
|
1826
|
+
this.record = record;
|
|
1827
|
+
this.writeStorage(record);
|
|
1828
|
+
if (this.consentConfig.onConsentChange) {
|
|
1829
|
+
try {
|
|
1830
|
+
this.consentConfig.onConsentChange(record);
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
this.client.log(`onConsentChange callback error: ${err}`);
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
1835
|
+
this.submitToServer(record);
|
|
1836
|
+
this.client.log("Consent confirmed.", record);
|
|
1837
|
+
}
|
|
1838
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
1839
|
+
isValid(record) {
|
|
1840
|
+
if (record.version !== CONSENT_VERSION) return false;
|
|
1841
|
+
const age = Date.now() - new Date(record.timestamp).getTime();
|
|
1842
|
+
if (age > TWELVE_MONTHS_MS) return false;
|
|
1843
|
+
return true;
|
|
1844
|
+
}
|
|
1845
|
+
storageKey() {
|
|
1846
|
+
return `${CONSENT_STORAGE_PREFIX}${this.client.getConfig().appId}`;
|
|
1847
|
+
}
|
|
1848
|
+
hydrateFromStorage() {
|
|
1849
|
+
if (typeof window === "undefined") return;
|
|
1850
|
+
try {
|
|
1851
|
+
const raw = localStorage.getItem(this.storageKey());
|
|
1852
|
+
if (!raw) return;
|
|
1853
|
+
const parsed = JSON.parse(raw);
|
|
1854
|
+
if (this.isValid(parsed)) {
|
|
1855
|
+
this.record = parsed;
|
|
1856
|
+
}
|
|
1857
|
+
} catch {
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
writeStorage(record) {
|
|
1861
|
+
if (typeof window === "undefined") return;
|
|
1862
|
+
try {
|
|
1863
|
+
localStorage.setItem(this.storageKey(), JSON.stringify(record));
|
|
1864
|
+
} catch {
|
|
1865
|
+
this.client.log("Failed to write consent to localStorage.");
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
clearStorage() {
|
|
1869
|
+
if (typeof window === "undefined") return;
|
|
1870
|
+
try {
|
|
1871
|
+
localStorage.removeItem(this.storageKey());
|
|
1872
|
+
} catch {
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
getOrCreateUserId() {
|
|
1876
|
+
const key = "zerocost-user-id";
|
|
1877
|
+
if (typeof window === "undefined") return this.generateUUID();
|
|
1878
|
+
let id = localStorage.getItem(key);
|
|
1879
|
+
if (!id) {
|
|
1880
|
+
id = this.generateUUID();
|
|
1881
|
+
try {
|
|
1882
|
+
localStorage.setItem(key, id);
|
|
1883
|
+
} catch {
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
return id;
|
|
1887
|
+
}
|
|
1888
|
+
generateUUID() {
|
|
1889
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1890
|
+
return crypto.randomUUID();
|
|
1891
|
+
}
|
|
1892
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1893
|
+
const r = Math.random() * 16 | 0;
|
|
1894
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1895
|
+
return v.toString(16);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
async submitToServer(record) {
|
|
1899
|
+
try {
|
|
1900
|
+
await this.client.request("/consent/submit", record);
|
|
1901
|
+
this.client.log("Consent record submitted to server.");
|
|
1902
|
+
} catch (err) {
|
|
1903
|
+
this.client.log(`Failed to submit consent to server: ${err}`);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
693
1908
|
// src/index.ts
|
|
694
|
-
var
|
|
1909
|
+
var CONFIG_CACHE_PREFIX = "zerocost-sdk-config:";
|
|
1910
|
+
var CONFIG_SYNC_DEBOUNCE_MS = 750;
|
|
1911
|
+
var CONFIG_STALE_AFTER_MS = 3e4;
|
|
695
1912
|
var ZerocostSDK = class {
|
|
696
1913
|
core;
|
|
697
1914
|
ads;
|
|
@@ -699,8 +1916,12 @@ var ZerocostSDK = class {
|
|
|
699
1916
|
widget;
|
|
700
1917
|
data;
|
|
701
1918
|
recording;
|
|
702
|
-
|
|
1919
|
+
consent;
|
|
703
1920
|
lastConfigHash = "";
|
|
1921
|
+
lastDataCollectionHash = "";
|
|
1922
|
+
configSyncInFlight = null;
|
|
1923
|
+
lastConfigSyncAt = 0;
|
|
1924
|
+
cleanupConfigListeners = [];
|
|
704
1925
|
constructor(config) {
|
|
705
1926
|
this.core = new ZerocostClient(config);
|
|
706
1927
|
this.ads = new AdsModule(this.core);
|
|
@@ -708,90 +1929,206 @@ var ZerocostSDK = class {
|
|
|
708
1929
|
this.widget = new WidgetModule(this.core);
|
|
709
1930
|
this.data = new LLMDataModule(this.core);
|
|
710
1931
|
this.recording = new RecordingModule(this.core);
|
|
1932
|
+
this.consent = new ConsentManager(this.core, {
|
|
1933
|
+
appName: config.appName,
|
|
1934
|
+
theme: config.theme,
|
|
1935
|
+
consent: config.consent
|
|
1936
|
+
});
|
|
711
1937
|
}
|
|
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
1938
|
async init() {
|
|
723
1939
|
this.core.init();
|
|
724
1940
|
if (typeof document === "undefined") {
|
|
725
|
-
this.core.log("Running in non-browser environment
|
|
1941
|
+
this.core.log("Running in non-browser environment; skipping DOM injection.");
|
|
1942
|
+
return;
|
|
1943
|
+
}
|
|
1944
|
+
if (window !== window.top) {
|
|
1945
|
+
this.core.log("Running inside an iframe. Ads render if permissions allow.");
|
|
1946
|
+
}
|
|
1947
|
+
this.core.log("Initializing Zerocost SDK.");
|
|
1948
|
+
if (this.consent.shouldPrompt()) {
|
|
1949
|
+
this.core.log("Consent required \u2014 showing prompt.");
|
|
1950
|
+
await this.consent.promptAndWait();
|
|
1951
|
+
}
|
|
1952
|
+
if (!this.consent.has("ads")) {
|
|
1953
|
+
this.core.log("Ads consent not granted \u2014 skipping ad injection.");
|
|
726
1954
|
return;
|
|
727
1955
|
}
|
|
728
|
-
const
|
|
729
|
-
if (
|
|
730
|
-
this.
|
|
1956
|
+
const cachedConfig = this.readCachedConfig();
|
|
1957
|
+
if (cachedConfig) {
|
|
1958
|
+
this.lastConfigHash = this.configToHash(cachedConfig);
|
|
1959
|
+
this.syncDataCollection(cachedConfig.dataCollection);
|
|
1960
|
+
await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
|
|
1961
|
+
this.core.log("Applied cached config immediately.");
|
|
731
1962
|
}
|
|
732
|
-
this.
|
|
1963
|
+
this.startConfigSync();
|
|
733
1964
|
try {
|
|
734
|
-
|
|
735
|
-
this.
|
|
736
|
-
this.applyConfig(config);
|
|
737
|
-
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
738
|
-
this.startConfigPolling();
|
|
1965
|
+
await this.refreshConfig({ force: true, reason: "init" });
|
|
1966
|
+
this.core.log("SDK fully initialized. Ads are rendering automatically.");
|
|
739
1967
|
} catch (err) {
|
|
740
|
-
this.core.log(`Init error: ${err}. Attempting fallback ad injection
|
|
741
|
-
this.widget.autoInject();
|
|
1968
|
+
this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
|
|
1969
|
+
await this.widget.autoInject();
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
async refreshConfig(options = {}) {
|
|
1973
|
+
const now = Date.now();
|
|
1974
|
+
if (!options.force && now - this.lastConfigSyncAt < CONFIG_SYNC_DEBOUNCE_MS) {
|
|
1975
|
+
return this.configSyncInFlight ?? Promise.resolve();
|
|
742
1976
|
}
|
|
1977
|
+
if (this.configSyncInFlight) {
|
|
1978
|
+
return this.configSyncInFlight;
|
|
1979
|
+
}
|
|
1980
|
+
this.configSyncInFlight = (async () => {
|
|
1981
|
+
try {
|
|
1982
|
+
const config = await this.fetchConfig();
|
|
1983
|
+
const nextHash = this.configToHash(config);
|
|
1984
|
+
const hasDisplayChanged = nextHash !== this.lastConfigHash;
|
|
1985
|
+
this.lastConfigHash = nextHash;
|
|
1986
|
+
this.lastConfigSyncAt = Date.now();
|
|
1987
|
+
this.writeCachedConfig(config);
|
|
1988
|
+
if (hasDisplayChanged) {
|
|
1989
|
+
this.core.log(`Config change detected${options.reason ? ` via ${options.reason}` : ""}. Updating ad formats immediately.`);
|
|
1990
|
+
await this.widget.autoInjectWithConfig(config.display, config.widget);
|
|
1991
|
+
}
|
|
1992
|
+
this.syncDataCollection(config.dataCollection);
|
|
1993
|
+
} catch (err) {
|
|
1994
|
+
this.core.log(`Config sync failed${options.reason ? ` during ${options.reason}` : ""}; keeping current placements.`);
|
|
1995
|
+
throw err;
|
|
1996
|
+
} finally {
|
|
1997
|
+
this.configSyncInFlight = null;
|
|
1998
|
+
}
|
|
1999
|
+
})();
|
|
2000
|
+
return this.configSyncInFlight;
|
|
743
2001
|
}
|
|
744
2002
|
async fetchConfig() {
|
|
745
2003
|
return this.core.request("/get-placements");
|
|
746
2004
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
2005
|
+
configToHash(config) {
|
|
2006
|
+
try {
|
|
2007
|
+
return JSON.stringify({ display: config.display, widget: config.widget });
|
|
2008
|
+
} catch {
|
|
2009
|
+
return "";
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
dataCollectionToHash(dataCollection) {
|
|
2013
|
+
try {
|
|
2014
|
+
return JSON.stringify(dataCollection || {});
|
|
2015
|
+
} catch {
|
|
2016
|
+
return "";
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
syncDataCollection(dataCollection) {
|
|
2020
|
+
const nextHash = this.dataCollectionToHash(dataCollection);
|
|
2021
|
+
if (nextHash === this.lastDataCollectionHash) return;
|
|
2022
|
+
this.data.stop();
|
|
2023
|
+
this.recording.stop();
|
|
2024
|
+
if (dataCollection?.llm && this.consent.has("usageData")) {
|
|
751
2025
|
this.data.start(dataCollection.llm);
|
|
752
2026
|
}
|
|
753
|
-
if (dataCollection?.recording) {
|
|
2027
|
+
if (dataCollection?.recording && this.consent.has("aiInteractions")) {
|
|
754
2028
|
this.recording.start(dataCollection.recording);
|
|
755
2029
|
}
|
|
2030
|
+
this.lastDataCollectionHash = nextHash;
|
|
756
2031
|
}
|
|
757
|
-
|
|
2032
|
+
getConfigCacheKey() {
|
|
2033
|
+
return `${CONFIG_CACHE_PREFIX}${this.core.getConfig().appId}`;
|
|
2034
|
+
}
|
|
2035
|
+
readCachedConfig() {
|
|
2036
|
+
if (typeof window === "undefined") return null;
|
|
758
2037
|
try {
|
|
759
|
-
|
|
2038
|
+
const raw = window.localStorage.getItem(this.getConfigCacheKey());
|
|
2039
|
+
return raw ? JSON.parse(raw) : null;
|
|
760
2040
|
} catch {
|
|
761
|
-
return
|
|
2041
|
+
return null;
|
|
762
2042
|
}
|
|
763
2043
|
}
|
|
764
|
-
|
|
765
|
-
if (
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
2044
|
+
writeCachedConfig(config) {
|
|
2045
|
+
if (typeof window === "undefined") return;
|
|
2046
|
+
try {
|
|
2047
|
+
window.localStorage.setItem(this.getConfigCacheKey(), JSON.stringify(config));
|
|
2048
|
+
} catch {
|
|
2049
|
+
this.core.log("Failed to persist cached config.");
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
startConfigSync() {
|
|
2053
|
+
if (typeof window === "undefined") return;
|
|
2054
|
+
const syncIfVisible = () => {
|
|
2055
|
+
if (document.visibilityState === "visible") {
|
|
2056
|
+
this.refreshConfig({ reason: "visibility" }).catch(() => {
|
|
2057
|
+
});
|
|
777
2058
|
}
|
|
778
|
-
}
|
|
2059
|
+
};
|
|
2060
|
+
const syncIfStale = (reason) => {
|
|
2061
|
+
if (Date.now() - this.lastConfigSyncAt >= CONFIG_STALE_AFTER_MS) {
|
|
2062
|
+
this.refreshConfig({ reason }).catch(() => {
|
|
2063
|
+
});
|
|
2064
|
+
}
|
|
2065
|
+
};
|
|
2066
|
+
const onVisibilityChange = () => syncIfVisible();
|
|
2067
|
+
const onFocus = () => syncIfStale("focus");
|
|
2068
|
+
const onPageShow = () => syncIfStale("pageshow");
|
|
2069
|
+
const onOnline = () => this.refreshConfig({ force: true, reason: "online" }).catch(() => {
|
|
2070
|
+
});
|
|
2071
|
+
const onNavigation = () => syncIfStale("navigation");
|
|
2072
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2073
|
+
window.addEventListener("focus", onFocus);
|
|
2074
|
+
window.addEventListener("pageshow", onPageShow);
|
|
2075
|
+
window.addEventListener("online", onOnline);
|
|
2076
|
+
window.addEventListener("popstate", onNavigation);
|
|
2077
|
+
window.addEventListener("hashchange", onNavigation);
|
|
2078
|
+
window.addEventListener("zerocost:navigation", onNavigation);
|
|
2079
|
+
const restoreHistoryPatch = this.patchHistory(onNavigation);
|
|
2080
|
+
this.cleanupConfigListeners.push(
|
|
2081
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange),
|
|
2082
|
+
() => window.removeEventListener("focus", onFocus),
|
|
2083
|
+
() => window.removeEventListener("pageshow", onPageShow),
|
|
2084
|
+
() => window.removeEventListener("online", onOnline),
|
|
2085
|
+
() => window.removeEventListener("popstate", onNavigation),
|
|
2086
|
+
() => window.removeEventListener("hashchange", onNavigation),
|
|
2087
|
+
() => window.removeEventListener("zerocost:navigation", onNavigation),
|
|
2088
|
+
restoreHistoryPatch
|
|
2089
|
+
);
|
|
779
2090
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
2091
|
+
patchHistory(onNavigation) {
|
|
2092
|
+
if (typeof window === "undefined") return () => {
|
|
2093
|
+
};
|
|
2094
|
+
const historyRef = window.history;
|
|
2095
|
+
if (historyRef.__zerocostPatched) {
|
|
2096
|
+
return () => {
|
|
2097
|
+
};
|
|
787
2098
|
}
|
|
2099
|
+
const originalPushState = historyRef.pushState.bind(window.history);
|
|
2100
|
+
const originalReplaceState = historyRef.replaceState.bind(window.history);
|
|
2101
|
+
historyRef.__zerocostPatched = true;
|
|
2102
|
+
historyRef.__zerocostPushState = originalPushState;
|
|
2103
|
+
historyRef.__zerocostReplaceState = originalReplaceState;
|
|
2104
|
+
historyRef.pushState = ((...args) => {
|
|
2105
|
+
const result = originalPushState(...args);
|
|
2106
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
2107
|
+
onNavigation();
|
|
2108
|
+
return result;
|
|
2109
|
+
});
|
|
2110
|
+
historyRef.replaceState = ((...args) => {
|
|
2111
|
+
const result = originalReplaceState(...args);
|
|
2112
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
2113
|
+
onNavigation();
|
|
2114
|
+
return result;
|
|
2115
|
+
});
|
|
2116
|
+
return () => {
|
|
2117
|
+
if (!historyRef.__zerocostPatched) return;
|
|
2118
|
+
historyRef.pushState = historyRef.__zerocostPushState || historyRef.pushState;
|
|
2119
|
+
historyRef.replaceState = historyRef.__zerocostReplaceState || historyRef.replaceState;
|
|
2120
|
+
delete historyRef.__zerocostPatched;
|
|
2121
|
+
delete historyRef.__zerocostPushState;
|
|
2122
|
+
delete historyRef.__zerocostReplaceState;
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
destroy() {
|
|
2126
|
+
this.cleanupConfigListeners.forEach((cleanup) => cleanup());
|
|
2127
|
+
this.cleanupConfigListeners = [];
|
|
788
2128
|
this.widget.unmountAll();
|
|
789
2129
|
this.data.stop();
|
|
790
2130
|
this.recording.stop();
|
|
791
2131
|
}
|
|
792
|
-
/**
|
|
793
|
-
* Validate the configured API key against the server.
|
|
794
|
-
*/
|
|
795
2132
|
async validateKey() {
|
|
796
2133
|
try {
|
|
797
2134
|
const result = await this.core.request("/validate-key");
|
|
@@ -803,6 +2140,7 @@ var ZerocostSDK = class {
|
|
|
803
2140
|
};
|
|
804
2141
|
// Annotate the CommonJS export names for ESM import in node:
|
|
805
2142
|
0 && (module.exports = {
|
|
2143
|
+
ConsentManager,
|
|
806
2144
|
LLMDataModule,
|
|
807
2145
|
RecordingModule,
|
|
808
2146
|
ZerocostClient,
|