@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.js
CHANGED
|
@@ -109,6 +109,142 @@ var TrackModule = class {
|
|
|
109
109
|
}
|
|
110
110
|
};
|
|
111
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
|
+
|
|
112
248
|
// src/modules/widget.ts
|
|
113
249
|
var POSITION_STYLES = {
|
|
114
250
|
"bottom-right": "position:fixed;bottom:24px;right:24px;z-index:9999;",
|
|
@@ -123,6 +259,17 @@ var POSITION_STYLES = {
|
|
|
123
259
|
};
|
|
124
260
|
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
125
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
|
+
];
|
|
126
273
|
var WidgetModule = class {
|
|
127
274
|
constructor(client) {
|
|
128
275
|
this.client = client;
|
|
@@ -130,15 +277,17 @@ var WidgetModule = class {
|
|
|
130
277
|
mounted = /* @__PURE__ */ new Map();
|
|
131
278
|
async autoInjectWithConfig(display, widget) {
|
|
132
279
|
try {
|
|
133
|
-
const selected = this.
|
|
280
|
+
const selected = this.resolveSelectedWidgets(display, widget);
|
|
134
281
|
this.clearAutoInjectedSlots();
|
|
135
|
-
if (
|
|
282
|
+
if (selected.length === 0) {
|
|
136
283
|
this.client.log("No enabled widget format found. Skipping injection.");
|
|
137
284
|
return;
|
|
138
285
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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.");
|
|
142
291
|
} catch (err) {
|
|
143
292
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
144
293
|
}
|
|
@@ -152,34 +301,65 @@ var WidgetModule = class {
|
|
|
152
301
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
153
302
|
}
|
|
154
303
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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({
|
|
158
309
|
format: widget.format,
|
|
159
310
|
position: widget.position || "bottom-right",
|
|
160
311
|
theme: widget.theme || "dark",
|
|
161
312
|
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
162
313
|
enabled: true
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
329
|
}
|
|
177
330
|
}
|
|
178
|
-
|
|
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;
|
|
179
350
|
}
|
|
180
351
|
normalizeConfigs(display) {
|
|
181
|
-
if (display && typeof display === "object" && "video-widget" in display) {
|
|
182
|
-
|
|
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
|
+
};
|
|
183
363
|
}
|
|
184
364
|
const pos = display?.position || "bottom-right";
|
|
185
365
|
const theme = display?.theme || "dark";
|
|
@@ -199,31 +379,32 @@ var WidgetModule = class {
|
|
|
199
379
|
return;
|
|
200
380
|
}
|
|
201
381
|
if (!isInline) {
|
|
202
|
-
let
|
|
203
|
-
if (!
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
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);
|
|
207
387
|
}
|
|
208
388
|
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
209
|
-
const maxW = config.format === "video-widget" ? "max-width:200px;" :
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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);
|
|
213
393
|
}
|
|
214
394
|
await this.mount(targetElementId, {
|
|
215
395
|
format: config.format,
|
|
216
|
-
refreshInterval:
|
|
217
|
-
// Config polling handles re-rendering
|
|
396
|
+
refreshInterval: SDK_WIDGET_REFRESH_MS / 1e3,
|
|
218
397
|
theme: config.theme,
|
|
219
398
|
autoplay: config.autoplay,
|
|
220
399
|
position: config.position
|
|
221
400
|
});
|
|
222
401
|
}
|
|
223
402
|
ensureInlineTarget(position) {
|
|
403
|
+
const chatContainer = this.findChatContainer();
|
|
404
|
+
if (!chatContainer) return null;
|
|
224
405
|
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
225
406
|
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
226
|
-
const paragraphs = Array.from(
|
|
407
|
+
const paragraphs = Array.from(chatContainer.querySelectorAll("p"));
|
|
227
408
|
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
228
409
|
if (!anchor) return null;
|
|
229
410
|
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
@@ -240,11 +421,22 @@ var WidgetModule = class {
|
|
|
240
421
|
}
|
|
241
422
|
return inlineId;
|
|
242
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
|
+
}
|
|
243
435
|
async mount(targetElementId, options = {}) {
|
|
244
|
-
const
|
|
245
|
-
if (!
|
|
436
|
+
const element = document.getElementById(targetElementId);
|
|
437
|
+
if (!element) return;
|
|
246
438
|
if (this.mounted.has(targetElementId)) this.unmount(targetElementId);
|
|
247
|
-
const refreshMs = (options.refreshInterval ??
|
|
439
|
+
const refreshMs = (options.refreshInterval ?? SDK_WIDGET_REFRESH_MS / 1e3) * 1e3;
|
|
248
440
|
const theme = options.theme || "dark";
|
|
249
441
|
const format = options.format || "video-widget";
|
|
250
442
|
const autoplay = options.autoplay ?? format === "video-widget";
|
|
@@ -258,15 +450,15 @@ var WidgetModule = class {
|
|
|
258
450
|
};
|
|
259
451
|
const data = await this.client.request("/serve-widget", body);
|
|
260
452
|
const ad = data.ad;
|
|
261
|
-
if (!ad
|
|
453
|
+
if (!ad) {
|
|
262
454
|
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
263
|
-
|
|
455
|
+
element.innerHTML = "";
|
|
264
456
|
return;
|
|
265
457
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
this.ensureVideoPlayback(
|
|
269
|
-
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]");
|
|
270
462
|
ctas.forEach((cta) => {
|
|
271
463
|
cta.addEventListener("click", () => {
|
|
272
464
|
this.client.request("/track-event", {
|
|
@@ -276,11 +468,11 @@ var WidgetModule = class {
|
|
|
276
468
|
});
|
|
277
469
|
});
|
|
278
470
|
});
|
|
279
|
-
const closeBtn =
|
|
471
|
+
const closeBtn = element.querySelector("[data-zc-close]");
|
|
280
472
|
if (closeBtn) {
|
|
281
|
-
closeBtn.addEventListener("click", (
|
|
282
|
-
|
|
283
|
-
|
|
473
|
+
closeBtn.addEventListener("click", (event) => {
|
|
474
|
+
event.preventDefault();
|
|
475
|
+
event.stopPropagation();
|
|
284
476
|
this.unmount(targetElementId);
|
|
285
477
|
});
|
|
286
478
|
}
|
|
@@ -317,8 +509,8 @@ var WidgetModule = class {
|
|
|
317
509
|
unmount(targetElementId) {
|
|
318
510
|
const slot = this.mounted.get(targetElementId);
|
|
319
511
|
if (slot?.interval) clearInterval(slot.interval);
|
|
320
|
-
const
|
|
321
|
-
if (
|
|
512
|
+
const element = document.getElementById(targetElementId);
|
|
513
|
+
if (element) element.remove();
|
|
322
514
|
this.mounted.delete(targetElementId);
|
|
323
515
|
}
|
|
324
516
|
unmountAll() {
|
|
@@ -337,59 +529,99 @@ var LLMDataModule = class {
|
|
|
337
529
|
clickHandler = null;
|
|
338
530
|
errorHandler = null;
|
|
339
531
|
fetchOriginal = null;
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
532
|
+
mutationObserver = null;
|
|
533
|
+
conversationScanTimer = null;
|
|
534
|
+
isSampledSession = false;
|
|
535
|
+
observedConversations = /* @__PURE__ */ new Map();
|
|
536
|
+
conversationCounter = 0;
|
|
343
537
|
start(config) {
|
|
538
|
+
this.stop();
|
|
344
539
|
this.config = config;
|
|
345
540
|
this.client.log(`LLMData: started (sample=${config.sampleRate}%)`);
|
|
346
|
-
|
|
541
|
+
this.isSampledSession = Math.random() * 100 <= config.sampleRate;
|
|
542
|
+
if (!this.isSampledSession) {
|
|
347
543
|
this.client.log("LLMData: session not sampled, skipping");
|
|
348
544
|
return;
|
|
349
545
|
}
|
|
350
546
|
if (config.uiInteractions) this.captureUIInteractions();
|
|
351
|
-
if (config.textPrompts)
|
|
547
|
+
if (config.textPrompts) {
|
|
548
|
+
this.interceptPrompts();
|
|
549
|
+
this.captureConversationSurfaces();
|
|
550
|
+
this.scheduleConversationScan();
|
|
551
|
+
}
|
|
352
552
|
if (config.apiErrors) this.captureAPIErrors();
|
|
353
553
|
this.flushInterval = setInterval(() => this.flush(), 1e4);
|
|
354
554
|
}
|
|
355
555
|
stop() {
|
|
356
|
-
if (this.flushInterval)
|
|
556
|
+
if (this.flushInterval) {
|
|
557
|
+
clearInterval(this.flushInterval);
|
|
558
|
+
this.flushInterval = null;
|
|
559
|
+
}
|
|
357
560
|
if (this.clickHandler) {
|
|
358
561
|
document.removeEventListener("click", this.clickHandler, true);
|
|
562
|
+
this.clickHandler = null;
|
|
359
563
|
}
|
|
360
564
|
if (this.errorHandler) {
|
|
361
565
|
window.removeEventListener("error", this.errorHandler);
|
|
566
|
+
this.errorHandler = null;
|
|
362
567
|
}
|
|
363
568
|
if (this.fetchOriginal) {
|
|
364
569
|
window.fetch = this.fetchOriginal;
|
|
570
|
+
this.fetchOriginal = null;
|
|
365
571
|
}
|
|
366
|
-
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();
|
|
367
583
|
this.config = null;
|
|
368
584
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Manually track an LLM prompt/response pair (for startups that want to
|
|
371
|
-
* explicitly send their AI interactions).
|
|
372
|
-
*/
|
|
373
585
|
trackPrompt(prompt, response, meta) {
|
|
374
|
-
if (!this.
|
|
586
|
+
if (!this.canCapture("textPrompts")) return;
|
|
375
587
|
this.pushEvent("llm_prompt", {
|
|
376
588
|
prompt: this.scrub(prompt),
|
|
377
589
|
response: response ? this.scrub(response) : void 0,
|
|
378
|
-
|
|
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
|
|
379
615
|
});
|
|
380
616
|
}
|
|
381
|
-
/**
|
|
382
|
-
* Manually track an API error.
|
|
383
|
-
*/
|
|
384
617
|
trackError(endpoint, status, message) {
|
|
385
|
-
if (!this.
|
|
618
|
+
if (!this.canCapture("apiErrors")) return;
|
|
386
619
|
this.pushEvent("api_error", {
|
|
387
620
|
endpoint,
|
|
388
621
|
status,
|
|
389
622
|
message: message ? this.scrub(message) : void 0
|
|
390
623
|
});
|
|
391
624
|
}
|
|
392
|
-
// ── Private ──
|
|
393
625
|
captureUIInteractions() {
|
|
394
626
|
this.clickHandler = (e) => {
|
|
395
627
|
const target = e.target;
|
|
@@ -413,44 +645,136 @@ var LLMDataModule = class {
|
|
|
413
645
|
}
|
|
414
646
|
interceptPrompts() {
|
|
415
647
|
this.fetchOriginal = window.fetch;
|
|
416
|
-
const self = this;
|
|
417
648
|
const origFetch = window.fetch;
|
|
649
|
+
const self = this;
|
|
418
650
|
window.fetch = async function(input, init) {
|
|
419
651
|
const fetchInput = input instanceof URL ? input.toString() : input;
|
|
420
652
|
const url = typeof fetchInput === "string" ? fetchInput : fetchInput.url;
|
|
421
653
|
const isLLM = /\/(chat|completions|generate|predict|inference|ask)/i.test(url);
|
|
422
|
-
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
|
+
}
|
|
423
670
|
try {
|
|
424
671
|
const res = await origFetch.call(window, fetchInput, init);
|
|
425
672
|
const clone = res.clone();
|
|
426
|
-
let reqBody;
|
|
427
|
-
if (init?.body && typeof init.body === "string") {
|
|
428
|
-
try {
|
|
429
|
-
const parsed = JSON.parse(init.body);
|
|
430
|
-
reqBody = JSON.stringify(parsed.messages || parsed.prompt || "").slice(0, 500);
|
|
431
|
-
} catch {
|
|
432
|
-
reqBody = void 0;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
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
|
+
}
|
|
436
699
|
self.pushEvent("llm_prompt", {
|
|
437
|
-
endpoint:
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
|
441
708
|
});
|
|
442
709
|
}).catch(() => {
|
|
443
710
|
});
|
|
444
711
|
return res;
|
|
445
|
-
} catch (
|
|
712
|
+
} catch (error) {
|
|
713
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
446
714
|
self.pushEvent("api_error", {
|
|
447
|
-
endpoint:
|
|
448
|
-
message: self.scrub(
|
|
715
|
+
endpoint: requestMeta.endpoint,
|
|
716
|
+
message: self.scrub(message),
|
|
717
|
+
requestId: requestMeta.requestId
|
|
449
718
|
});
|
|
450
|
-
throw
|
|
719
|
+
throw error;
|
|
451
720
|
}
|
|
452
721
|
};
|
|
453
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
|
+
}
|
|
454
778
|
captureAPIErrors() {
|
|
455
779
|
this.errorHandler = (e) => {
|
|
456
780
|
this.pushEvent("api_error", {
|
|
@@ -462,9 +786,37 @@ var LLMDataModule = class {
|
|
|
462
786
|
};
|
|
463
787
|
window.addEventListener("error", this.errorHandler);
|
|
464
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
|
+
}
|
|
465
814
|
pushEvent(type, data) {
|
|
815
|
+
if (!this.isSampledSession) return;
|
|
466
816
|
this.buffer.push({ type, data, timestamp: Date.now() });
|
|
467
|
-
if (this.buffer.length >= 50)
|
|
817
|
+
if (this.buffer.length >= 50) {
|
|
818
|
+
void this.flush();
|
|
819
|
+
}
|
|
468
820
|
}
|
|
469
821
|
async flush() {
|
|
470
822
|
if (this.buffer.length === 0) return;
|
|
@@ -477,25 +829,286 @@ var LLMDataModule = class {
|
|
|
477
829
|
this.buffer.unshift(...events);
|
|
478
830
|
}
|
|
479
831
|
}
|
|
480
|
-
/** Basic PII scrubbing */
|
|
481
832
|
scrub(text) {
|
|
482
|
-
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]");
|
|
483
834
|
}
|
|
484
835
|
getPath(el) {
|
|
485
836
|
const parts = [];
|
|
486
|
-
let
|
|
487
|
-
while (
|
|
488
|
-
let
|
|
489
|
-
if (
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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}`;
|
|
493
845
|
}
|
|
494
|
-
parts.unshift(
|
|
495
|
-
|
|
846
|
+
parts.unshift(segment);
|
|
847
|
+
current = current.parentElement;
|
|
496
848
|
}
|
|
497
849
|
return parts.join(" > ");
|
|
498
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
|
+
}
|
|
499
1112
|
};
|
|
500
1113
|
|
|
501
1114
|
// src/modules/recording.ts
|
|
@@ -661,8 +1274,611 @@ var RecordingModule = class {
|
|
|
661
1274
|
}
|
|
662
1275
|
};
|
|
663
1276
|
|
|
1277
|
+
// src/core/consent-ui.ts
|
|
1278
|
+
var STYLE_ID = "zerocost-consent-styles";
|
|
1279
|
+
function injectStyles(theme) {
|
|
1280
|
+
if (document.getElementById(STYLE_ID)) return;
|
|
1281
|
+
const darkVars = `
|
|
1282
|
+
--zc-bg: #111111;
|
|
1283
|
+
--zc-surface: #1a1a1a;
|
|
1284
|
+
--zc-border: #2a2a2a;
|
|
1285
|
+
--zc-text: #ffffff;
|
|
1286
|
+
--zc-text-secondary: #999999;
|
|
1287
|
+
--zc-accent: #ffffff;
|
|
1288
|
+
--zc-accent-bg: #ffffff;
|
|
1289
|
+
--zc-accent-fg: #000000;
|
|
1290
|
+
--zc-toggle-off-bg: #333333;
|
|
1291
|
+
--zc-toggle-on-bg: #00e599;
|
|
1292
|
+
--zc-toggle-knob: #ffffff;
|
|
1293
|
+
--zc-backdrop: rgba(0,0,0,0.65);
|
|
1294
|
+
--zc-link: #888888;
|
|
1295
|
+
--zc-link-hover: #cccccc;
|
|
1296
|
+
`;
|
|
1297
|
+
const lightVars = `
|
|
1298
|
+
--zc-bg: #ffffff;
|
|
1299
|
+
--zc-surface: #f5f5f5;
|
|
1300
|
+
--zc-border: #e0e0e0;
|
|
1301
|
+
--zc-text: #111111;
|
|
1302
|
+
--zc-text-secondary: #666666;
|
|
1303
|
+
--zc-accent: #111111;
|
|
1304
|
+
--zc-accent-bg: #111111;
|
|
1305
|
+
--zc-accent-fg: #ffffff;
|
|
1306
|
+
--zc-toggle-off-bg: #cccccc;
|
|
1307
|
+
--zc-toggle-on-bg: #00c77d;
|
|
1308
|
+
--zc-toggle-knob: #ffffff;
|
|
1309
|
+
--zc-backdrop: rgba(0,0,0,0.45);
|
|
1310
|
+
--zc-link: #666666;
|
|
1311
|
+
--zc-link-hover: #111111;
|
|
1312
|
+
`;
|
|
1313
|
+
let themeRule;
|
|
1314
|
+
if (theme === "dark") {
|
|
1315
|
+
themeRule = `.zc-consent-root { ${darkVars} }`;
|
|
1316
|
+
} else if (theme === "light") {
|
|
1317
|
+
themeRule = `.zc-consent-root { ${lightVars} }`;
|
|
1318
|
+
} else {
|
|
1319
|
+
themeRule = `
|
|
1320
|
+
.zc-consent-root { ${lightVars} }
|
|
1321
|
+
@media (prefers-color-scheme: dark) {
|
|
1322
|
+
.zc-consent-root { ${darkVars} }
|
|
1323
|
+
}
|
|
1324
|
+
`;
|
|
1325
|
+
}
|
|
1326
|
+
const css = `
|
|
1327
|
+
${themeRule}
|
|
1328
|
+
|
|
1329
|
+
.zc-consent-root * {
|
|
1330
|
+
box-sizing: border-box;
|
|
1331
|
+
margin: 0;
|
|
1332
|
+
padding: 0;
|
|
1333
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', Roboto, sans-serif;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
.zc-consent-backdrop {
|
|
1337
|
+
position: fixed;
|
|
1338
|
+
inset: 0;
|
|
1339
|
+
z-index: 999999;
|
|
1340
|
+
background: var(--zc-backdrop);
|
|
1341
|
+
display: flex;
|
|
1342
|
+
align-items: center;
|
|
1343
|
+
justify-content: center;
|
|
1344
|
+
animation: zc-fade-in 200ms ease;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
@keyframes zc-fade-in {
|
|
1348
|
+
from { opacity: 0; }
|
|
1349
|
+
to { opacity: 1; }
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
@keyframes zc-slide-up {
|
|
1353
|
+
from { transform: translateY(100%); }
|
|
1354
|
+
to { transform: translateY(0); }
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
.zc-consent-card {
|
|
1358
|
+
background: var(--zc-bg);
|
|
1359
|
+
border: 1px solid var(--zc-border);
|
|
1360
|
+
border-radius: 16px;
|
|
1361
|
+
width: 100%;
|
|
1362
|
+
max-width: 460px;
|
|
1363
|
+
max-height: 90vh;
|
|
1364
|
+
overflow-y: auto;
|
|
1365
|
+
padding: 28px 24px 24px;
|
|
1366
|
+
animation: zc-fade-in 200ms ease;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
/* Mobile: bottom-sheet style */
|
|
1370
|
+
@media (max-width: 640px) {
|
|
1371
|
+
.zc-consent-backdrop {
|
|
1372
|
+
align-items: flex-end;
|
|
1373
|
+
}
|
|
1374
|
+
.zc-consent-card {
|
|
1375
|
+
border-radius: 20px 20px 0 0;
|
|
1376
|
+
max-width: 100%;
|
|
1377
|
+
animation: zc-slide-up 200ms ease;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/* Scrollbar */
|
|
1382
|
+
.zc-consent-card::-webkit-scrollbar { width: 4px; }
|
|
1383
|
+
.zc-consent-card::-webkit-scrollbar-thumb { background: var(--zc-border); border-radius: 4px; }
|
|
1384
|
+
|
|
1385
|
+
.zc-consent-header {
|
|
1386
|
+
display: flex;
|
|
1387
|
+
align-items: center;
|
|
1388
|
+
gap: 10px;
|
|
1389
|
+
margin-bottom: 6px;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
.zc-consent-logo {
|
|
1393
|
+
width: 28px;
|
|
1394
|
+
height: 28px;
|
|
1395
|
+
border-radius: 6px;
|
|
1396
|
+
background: var(--zc-accent-bg);
|
|
1397
|
+
display: flex;
|
|
1398
|
+
align-items: center;
|
|
1399
|
+
justify-content: center;
|
|
1400
|
+
flex-shrink: 0;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
.zc-consent-logo svg {
|
|
1404
|
+
width: 16px;
|
|
1405
|
+
height: 16px;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
.zc-consent-title {
|
|
1409
|
+
font-size: 16px;
|
|
1410
|
+
font-weight: 700;
|
|
1411
|
+
color: var(--zc-text);
|
|
1412
|
+
letter-spacing: -0.02em;
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
.zc-consent-subtitle {
|
|
1416
|
+
font-size: 13px;
|
|
1417
|
+
color: var(--zc-text-secondary);
|
|
1418
|
+
line-height: 1.5;
|
|
1419
|
+
margin-bottom: 20px;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
.zc-consent-toggles {
|
|
1423
|
+
display: flex;
|
|
1424
|
+
flex-direction: column;
|
|
1425
|
+
gap: 10px;
|
|
1426
|
+
margin-bottom: 20px;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
.zc-consent-toggle-card {
|
|
1430
|
+
background: var(--zc-surface);
|
|
1431
|
+
border: 1px solid var(--zc-border);
|
|
1432
|
+
border-radius: 12px;
|
|
1433
|
+
padding: 14px 16px;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
.zc-consent-toggle-row {
|
|
1437
|
+
display: flex;
|
|
1438
|
+
align-items: center;
|
|
1439
|
+
justify-content: space-between;
|
|
1440
|
+
margin-bottom: 6px;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
.zc-consent-toggle-label {
|
|
1444
|
+
font-size: 14px;
|
|
1445
|
+
font-weight: 600;
|
|
1446
|
+
color: var(--zc-text);
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
.zc-consent-toggle-desc {
|
|
1450
|
+
font-size: 12px;
|
|
1451
|
+
color: var(--zc-text-secondary);
|
|
1452
|
+
line-height: 1.5;
|
|
1453
|
+
margin-bottom: 4px;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
.zc-consent-learn-more {
|
|
1457
|
+
font-size: 11px;
|
|
1458
|
+
color: var(--zc-link);
|
|
1459
|
+
text-decoration: none;
|
|
1460
|
+
cursor: pointer;
|
|
1461
|
+
transition: color 150ms;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
.zc-consent-learn-more:hover {
|
|
1465
|
+
color: var(--zc-link-hover);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/* Toggle switch */
|
|
1469
|
+
.zc-toggle {
|
|
1470
|
+
position: relative;
|
|
1471
|
+
width: 40px;
|
|
1472
|
+
height: 22px;
|
|
1473
|
+
flex-shrink: 0;
|
|
1474
|
+
cursor: pointer;
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
.zc-toggle input {
|
|
1478
|
+
opacity: 0;
|
|
1479
|
+
width: 0;
|
|
1480
|
+
height: 0;
|
|
1481
|
+
position: absolute;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
.zc-toggle-track {
|
|
1485
|
+
position: absolute;
|
|
1486
|
+
inset: 0;
|
|
1487
|
+
background: var(--zc-toggle-off-bg);
|
|
1488
|
+
border-radius: 11px;
|
|
1489
|
+
transition: background 200ms ease;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
.zc-toggle input:checked + .zc-toggle-track {
|
|
1493
|
+
background: var(--zc-toggle-on-bg);
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
.zc-toggle-knob {
|
|
1497
|
+
position: absolute;
|
|
1498
|
+
top: 2px;
|
|
1499
|
+
left: 2px;
|
|
1500
|
+
width: 18px;
|
|
1501
|
+
height: 18px;
|
|
1502
|
+
background: var(--zc-toggle-knob);
|
|
1503
|
+
border-radius: 50%;
|
|
1504
|
+
transition: transform 200ms ease;
|
|
1505
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.2);
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
.zc-toggle input:checked ~ .zc-toggle-knob {
|
|
1509
|
+
transform: translateX(18px);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/* Footer */
|
|
1513
|
+
.zc-consent-footer {
|
|
1514
|
+
display: flex;
|
|
1515
|
+
flex-wrap: wrap;
|
|
1516
|
+
gap: 4px 12px;
|
|
1517
|
+
justify-content: center;
|
|
1518
|
+
margin-bottom: 16px;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
.zc-consent-footer a {
|
|
1522
|
+
font-size: 11px;
|
|
1523
|
+
color: var(--zc-link);
|
|
1524
|
+
text-decoration: none;
|
|
1525
|
+
transition: color 150ms;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
.zc-consent-footer a:hover {
|
|
1529
|
+
color: var(--zc-link-hover);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
.zc-consent-footer-sep {
|
|
1533
|
+
font-size: 11px;
|
|
1534
|
+
color: var(--zc-link);
|
|
1535
|
+
opacity: 0.5;
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
/* Confirm button */
|
|
1539
|
+
.zc-consent-confirm {
|
|
1540
|
+
display: block;
|
|
1541
|
+
width: 100%;
|
|
1542
|
+
padding: 12px;
|
|
1543
|
+
font-size: 14px;
|
|
1544
|
+
font-weight: 600;
|
|
1545
|
+
border: none;
|
|
1546
|
+
border-radius: 10px;
|
|
1547
|
+
cursor: pointer;
|
|
1548
|
+
background: var(--zc-accent-bg);
|
|
1549
|
+
color: var(--zc-accent-fg);
|
|
1550
|
+
letter-spacing: -0.01em;
|
|
1551
|
+
transition: opacity 150ms;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
.zc-consent-confirm:hover {
|
|
1555
|
+
opacity: 0.88;
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
.zc-consent-confirm:active {
|
|
1559
|
+
opacity: 0.75;
|
|
1560
|
+
}
|
|
1561
|
+
`;
|
|
1562
|
+
const style = document.createElement("style");
|
|
1563
|
+
style.id = STYLE_ID;
|
|
1564
|
+
style.textContent = css;
|
|
1565
|
+
document.head.appendChild(style);
|
|
1566
|
+
}
|
|
1567
|
+
function createToggle(id, checked) {
|
|
1568
|
+
const label = document.createElement("label");
|
|
1569
|
+
label.className = "zc-toggle";
|
|
1570
|
+
const input = document.createElement("input");
|
|
1571
|
+
input.type = "checkbox";
|
|
1572
|
+
input.checked = checked;
|
|
1573
|
+
input.id = id;
|
|
1574
|
+
const track = document.createElement("span");
|
|
1575
|
+
track.className = "zc-toggle-track";
|
|
1576
|
+
const knob = document.createElement("span");
|
|
1577
|
+
knob.className = "zc-toggle-knob";
|
|
1578
|
+
label.appendChild(input);
|
|
1579
|
+
label.appendChild(track);
|
|
1580
|
+
label.appendChild(knob);
|
|
1581
|
+
return label;
|
|
1582
|
+
}
|
|
1583
|
+
function createToggleCard(toggleId, title, description, learnMoreUrl, defaultOn) {
|
|
1584
|
+
const card = document.createElement("div");
|
|
1585
|
+
card.className = "zc-consent-toggle-card";
|
|
1586
|
+
const row = document.createElement("div");
|
|
1587
|
+
row.className = "zc-consent-toggle-row";
|
|
1588
|
+
const labelSpan = document.createElement("span");
|
|
1589
|
+
labelSpan.className = "zc-consent-toggle-label";
|
|
1590
|
+
labelSpan.textContent = title;
|
|
1591
|
+
const toggle = createToggle(toggleId, defaultOn);
|
|
1592
|
+
row.appendChild(labelSpan);
|
|
1593
|
+
row.appendChild(toggle);
|
|
1594
|
+
card.appendChild(row);
|
|
1595
|
+
const desc = document.createElement("div");
|
|
1596
|
+
desc.className = "zc-consent-toggle-desc";
|
|
1597
|
+
desc.textContent = description;
|
|
1598
|
+
card.appendChild(desc);
|
|
1599
|
+
const link = document.createElement("a");
|
|
1600
|
+
link.className = "zc-consent-learn-more";
|
|
1601
|
+
link.href = learnMoreUrl;
|
|
1602
|
+
link.target = "_blank";
|
|
1603
|
+
link.rel = "noopener noreferrer";
|
|
1604
|
+
link.textContent = "Learn more \u2197";
|
|
1605
|
+
card.appendChild(link);
|
|
1606
|
+
return card;
|
|
1607
|
+
}
|
|
1608
|
+
function showConsentUI(options) {
|
|
1609
|
+
return new Promise((resolve) => {
|
|
1610
|
+
const { appName, theme, privacyPolicyUrl } = options;
|
|
1611
|
+
const defaults = options.defaults ?? { ads: true, usageData: false, aiInteractions: false };
|
|
1612
|
+
injectStyles(theme);
|
|
1613
|
+
const root = document.createElement("div");
|
|
1614
|
+
root.className = "zc-consent-root";
|
|
1615
|
+
const backdrop = document.createElement("div");
|
|
1616
|
+
backdrop.className = "zc-consent-backdrop";
|
|
1617
|
+
const blockEscape = (e) => {
|
|
1618
|
+
if (e.key === "Escape") {
|
|
1619
|
+
e.preventDefault();
|
|
1620
|
+
e.stopPropagation();
|
|
1621
|
+
}
|
|
1622
|
+
};
|
|
1623
|
+
document.addEventListener("keydown", blockEscape, true);
|
|
1624
|
+
const card = document.createElement("div");
|
|
1625
|
+
card.className = "zc-consent-card";
|
|
1626
|
+
const header = document.createElement("div");
|
|
1627
|
+
header.className = "zc-consent-header";
|
|
1628
|
+
const logo = document.createElement("div");
|
|
1629
|
+
logo.className = "zc-consent-logo";
|
|
1630
|
+
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>`;
|
|
1631
|
+
const title = document.createElement("div");
|
|
1632
|
+
title.className = "zc-consent-title";
|
|
1633
|
+
title.textContent = `${appName || "This app"} uses Zerocost`;
|
|
1634
|
+
header.appendChild(logo);
|
|
1635
|
+
header.appendChild(title);
|
|
1636
|
+
card.appendChild(header);
|
|
1637
|
+
const subtitle = document.createElement("div");
|
|
1638
|
+
subtitle.className = "zc-consent-subtitle";
|
|
1639
|
+
subtitle.textContent = "Manage your preferences below. You can update these anytime.";
|
|
1640
|
+
card.appendChild(subtitle);
|
|
1641
|
+
const toggles = document.createElement("div");
|
|
1642
|
+
toggles.className = "zc-consent-toggles";
|
|
1643
|
+
const baseUrl = typeof window !== "undefined" ? window.location.origin : "";
|
|
1644
|
+
toggles.appendChild(createToggleCard(
|
|
1645
|
+
"zc-toggle-ads",
|
|
1646
|
+
"Ads",
|
|
1647
|
+
"Contextual, non-intrusive ads. No cookies or browsing history used.",
|
|
1648
|
+
`${baseUrl}/consent/ads`,
|
|
1649
|
+
defaults.ads
|
|
1650
|
+
));
|
|
1651
|
+
toggles.appendChild(createToggleCard(
|
|
1652
|
+
"zc-toggle-usage",
|
|
1653
|
+
"Usage data",
|
|
1654
|
+
"Anonymized usage patterns. No personal information is shared.",
|
|
1655
|
+
`${baseUrl}/consent/usage-data`,
|
|
1656
|
+
defaults.usageData
|
|
1657
|
+
));
|
|
1658
|
+
toggles.appendChild(createToggleCard(
|
|
1659
|
+
"zc-toggle-ai",
|
|
1660
|
+
"AI interactions",
|
|
1661
|
+
"Anonymized conversation data used for AI research.",
|
|
1662
|
+
`${baseUrl}/consent/ai-interactions`,
|
|
1663
|
+
defaults.aiInteractions
|
|
1664
|
+
));
|
|
1665
|
+
card.appendChild(toggles);
|
|
1666
|
+
const footer = document.createElement("div");
|
|
1667
|
+
footer.className = "zc-consent-footer";
|
|
1668
|
+
const ppLink = document.createElement("a");
|
|
1669
|
+
ppLink.href = privacyPolicyUrl || `${baseUrl}/privacy`;
|
|
1670
|
+
ppLink.target = "_blank";
|
|
1671
|
+
ppLink.rel = "noopener noreferrer";
|
|
1672
|
+
ppLink.textContent = "Privacy Policy";
|
|
1673
|
+
footer.appendChild(ppLink);
|
|
1674
|
+
const sep1 = document.createElement("span");
|
|
1675
|
+
sep1.className = "zc-consent-footer-sep";
|
|
1676
|
+
sep1.textContent = "\xB7";
|
|
1677
|
+
footer.appendChild(sep1);
|
|
1678
|
+
const termsLink = document.createElement("a");
|
|
1679
|
+
termsLink.href = `${baseUrl}/terms`;
|
|
1680
|
+
termsLink.target = "_blank";
|
|
1681
|
+
termsLink.rel = "noopener noreferrer";
|
|
1682
|
+
termsLink.textContent = "Terms";
|
|
1683
|
+
footer.appendChild(termsLink);
|
|
1684
|
+
const sep2 = document.createElement("span");
|
|
1685
|
+
sep2.className = "zc-consent-footer-sep";
|
|
1686
|
+
sep2.textContent = "\xB7";
|
|
1687
|
+
footer.appendChild(sep2);
|
|
1688
|
+
const dnsLink = document.createElement("a");
|
|
1689
|
+
dnsLink.href = `${baseUrl}/do-not-sell`;
|
|
1690
|
+
dnsLink.target = "_blank";
|
|
1691
|
+
dnsLink.rel = "noopener noreferrer";
|
|
1692
|
+
dnsLink.textContent = "Do Not Sell My Data";
|
|
1693
|
+
footer.appendChild(dnsLink);
|
|
1694
|
+
card.appendChild(footer);
|
|
1695
|
+
const confirmBtn = document.createElement("button");
|
|
1696
|
+
confirmBtn.className = "zc-consent-confirm";
|
|
1697
|
+
confirmBtn.textContent = "Confirm";
|
|
1698
|
+
confirmBtn.addEventListener("click", () => {
|
|
1699
|
+
const ads = document.getElementById("zc-toggle-ads")?.checked ?? defaults.ads;
|
|
1700
|
+
const usageData = document.getElementById("zc-toggle-usage")?.checked ?? defaults.usageData;
|
|
1701
|
+
const aiInteractions = document.getElementById("zc-toggle-ai")?.checked ?? defaults.aiInteractions;
|
|
1702
|
+
document.removeEventListener("keydown", blockEscape, true);
|
|
1703
|
+
root.remove();
|
|
1704
|
+
resolve({ ads, usageData, aiInteractions });
|
|
1705
|
+
});
|
|
1706
|
+
card.appendChild(confirmBtn);
|
|
1707
|
+
backdrop.appendChild(card);
|
|
1708
|
+
root.appendChild(backdrop);
|
|
1709
|
+
document.body.appendChild(root);
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
function removeConsentUI() {
|
|
1713
|
+
document.querySelector(".zc-consent-root")?.remove();
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
// src/core/consent.ts
|
|
1717
|
+
var CONSENT_VERSION = "1.1";
|
|
1718
|
+
var CONSENT_STORAGE_PREFIX = "zerocost-consent:";
|
|
1719
|
+
var TWELVE_MONTHS_MS = 365 * 24 * 60 * 60 * 1e3;
|
|
1720
|
+
var ConsentManager = class {
|
|
1721
|
+
record = null;
|
|
1722
|
+
needsReset = false;
|
|
1723
|
+
client;
|
|
1724
|
+
consentConfig;
|
|
1725
|
+
appName;
|
|
1726
|
+
theme;
|
|
1727
|
+
constructor(client, opts) {
|
|
1728
|
+
this.client = client;
|
|
1729
|
+
this.consentConfig = opts.consent ?? {};
|
|
1730
|
+
this.appName = opts.appName ?? "";
|
|
1731
|
+
this.theme = opts.theme ?? "dark";
|
|
1732
|
+
this.hydrateFromStorage();
|
|
1733
|
+
}
|
|
1734
|
+
// ── Public API (per spec §6.3) ───────────────────────────────────
|
|
1735
|
+
/** Returns the current consent record, or null if none exists. */
|
|
1736
|
+
get() {
|
|
1737
|
+
return this.record;
|
|
1738
|
+
}
|
|
1739
|
+
/** Programmatically open the consent popup (e.g. from app settings). */
|
|
1740
|
+
async open() {
|
|
1741
|
+
removeConsentUI();
|
|
1742
|
+
await this.promptAndWait();
|
|
1743
|
+
}
|
|
1744
|
+
/** Clear consent — prompt will re-fire on next init(). */
|
|
1745
|
+
reset() {
|
|
1746
|
+
this.record = null;
|
|
1747
|
+
this.needsReset = true;
|
|
1748
|
+
this.clearStorage();
|
|
1749
|
+
this.client.log("Consent reset. Prompt will re-fire on next init().");
|
|
1750
|
+
}
|
|
1751
|
+
/** Restore a previously saved record (skip re-prompting if valid). */
|
|
1752
|
+
restore(record) {
|
|
1753
|
+
if (this.isValid(record)) {
|
|
1754
|
+
this.record = record;
|
|
1755
|
+
this.writeStorage(record);
|
|
1756
|
+
this.client.log("Consent restored from saved record.");
|
|
1757
|
+
} else {
|
|
1758
|
+
this.client.log("Restored record invalid (version/expiry). Will re-prompt.");
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
/** Check whether a specific feature is consented. */
|
|
1762
|
+
has(feature) {
|
|
1763
|
+
if (!this.record) return false;
|
|
1764
|
+
return !!this.record[feature];
|
|
1765
|
+
}
|
|
1766
|
+
// ── Internal (used by ZerocostSDK.init) ──────────────────────────
|
|
1767
|
+
/** Should the consent prompt be shown? */
|
|
1768
|
+
shouldPrompt() {
|
|
1769
|
+
if (this.needsReset) return true;
|
|
1770
|
+
if (!this.record) return true;
|
|
1771
|
+
if (!this.isValid(this.record)) return true;
|
|
1772
|
+
return false;
|
|
1773
|
+
}
|
|
1774
|
+
/** Show the consent popup, wait for confirmation, store record. */
|
|
1775
|
+
async promptAndWait() {
|
|
1776
|
+
this.needsReset = false;
|
|
1777
|
+
const result = await showConsentUI({
|
|
1778
|
+
appName: this.appName,
|
|
1779
|
+
theme: this.theme,
|
|
1780
|
+
privacyPolicyUrl: this.consentConfig.privacyPolicyUrl,
|
|
1781
|
+
defaults: this.record ? { ads: this.record.ads, usageData: this.record.usageData, aiInteractions: this.record.aiInteractions } : void 0
|
|
1782
|
+
});
|
|
1783
|
+
const userId = this.getOrCreateUserId();
|
|
1784
|
+
const record = {
|
|
1785
|
+
userId,
|
|
1786
|
+
appId: this.client.getConfig().appId,
|
|
1787
|
+
ads: result.ads,
|
|
1788
|
+
usageData: result.usageData,
|
|
1789
|
+
aiInteractions: result.aiInteractions,
|
|
1790
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1791
|
+
version: CONSENT_VERSION,
|
|
1792
|
+
method: "confirmed",
|
|
1793
|
+
ipRegion: "OTHER"
|
|
1794
|
+
// server can enrich via IP
|
|
1795
|
+
};
|
|
1796
|
+
this.record = record;
|
|
1797
|
+
this.writeStorage(record);
|
|
1798
|
+
if (this.consentConfig.onConsentChange) {
|
|
1799
|
+
try {
|
|
1800
|
+
this.consentConfig.onConsentChange(record);
|
|
1801
|
+
} catch (err) {
|
|
1802
|
+
this.client.log(`onConsentChange callback error: ${err}`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
this.submitToServer(record);
|
|
1806
|
+
this.client.log("Consent confirmed.", record);
|
|
1807
|
+
}
|
|
1808
|
+
// ── Helpers ──────────────────────────────────────────────────────
|
|
1809
|
+
isValid(record) {
|
|
1810
|
+
if (record.version !== CONSENT_VERSION) return false;
|
|
1811
|
+
const age = Date.now() - new Date(record.timestamp).getTime();
|
|
1812
|
+
if (age > TWELVE_MONTHS_MS) return false;
|
|
1813
|
+
return true;
|
|
1814
|
+
}
|
|
1815
|
+
storageKey() {
|
|
1816
|
+
return `${CONSENT_STORAGE_PREFIX}${this.client.getConfig().appId}`;
|
|
1817
|
+
}
|
|
1818
|
+
hydrateFromStorage() {
|
|
1819
|
+
if (typeof window === "undefined") return;
|
|
1820
|
+
try {
|
|
1821
|
+
const raw = localStorage.getItem(this.storageKey());
|
|
1822
|
+
if (!raw) return;
|
|
1823
|
+
const parsed = JSON.parse(raw);
|
|
1824
|
+
if (this.isValid(parsed)) {
|
|
1825
|
+
this.record = parsed;
|
|
1826
|
+
}
|
|
1827
|
+
} catch {
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
writeStorage(record) {
|
|
1831
|
+
if (typeof window === "undefined") return;
|
|
1832
|
+
try {
|
|
1833
|
+
localStorage.setItem(this.storageKey(), JSON.stringify(record));
|
|
1834
|
+
} catch {
|
|
1835
|
+
this.client.log("Failed to write consent to localStorage.");
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
clearStorage() {
|
|
1839
|
+
if (typeof window === "undefined") return;
|
|
1840
|
+
try {
|
|
1841
|
+
localStorage.removeItem(this.storageKey());
|
|
1842
|
+
} catch {
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
getOrCreateUserId() {
|
|
1846
|
+
const key = "zerocost-user-id";
|
|
1847
|
+
if (typeof window === "undefined") return this.generateUUID();
|
|
1848
|
+
let id = localStorage.getItem(key);
|
|
1849
|
+
if (!id) {
|
|
1850
|
+
id = this.generateUUID();
|
|
1851
|
+
try {
|
|
1852
|
+
localStorage.setItem(key, id);
|
|
1853
|
+
} catch {
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
return id;
|
|
1857
|
+
}
|
|
1858
|
+
generateUUID() {
|
|
1859
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
1860
|
+
return crypto.randomUUID();
|
|
1861
|
+
}
|
|
1862
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
1863
|
+
const r = Math.random() * 16 | 0;
|
|
1864
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
1865
|
+
return v.toString(16);
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
async submitToServer(record) {
|
|
1869
|
+
try {
|
|
1870
|
+
await this.client.request("/consent/submit", record);
|
|
1871
|
+
this.client.log("Consent record submitted to server.");
|
|
1872
|
+
} catch (err) {
|
|
1873
|
+
this.client.log(`Failed to submit consent to server: ${err}`);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
};
|
|
1877
|
+
|
|
664
1878
|
// src/index.ts
|
|
665
|
-
var
|
|
1879
|
+
var CONFIG_CACHE_PREFIX = "zerocost-sdk-config:";
|
|
1880
|
+
var CONFIG_SYNC_DEBOUNCE_MS = 750;
|
|
1881
|
+
var CONFIG_STALE_AFTER_MS = 3e4;
|
|
666
1882
|
var ZerocostSDK = class {
|
|
667
1883
|
core;
|
|
668
1884
|
ads;
|
|
@@ -670,8 +1886,12 @@ var ZerocostSDK = class {
|
|
|
670
1886
|
widget;
|
|
671
1887
|
data;
|
|
672
1888
|
recording;
|
|
673
|
-
|
|
1889
|
+
consent;
|
|
674
1890
|
lastConfigHash = "";
|
|
1891
|
+
lastDataCollectionHash = "";
|
|
1892
|
+
configSyncInFlight = null;
|
|
1893
|
+
lastConfigSyncAt = 0;
|
|
1894
|
+
cleanupConfigListeners = [];
|
|
675
1895
|
constructor(config) {
|
|
676
1896
|
this.core = new ZerocostClient(config);
|
|
677
1897
|
this.ads = new AdsModule(this.core);
|
|
@@ -679,90 +1899,206 @@ var ZerocostSDK = class {
|
|
|
679
1899
|
this.widget = new WidgetModule(this.core);
|
|
680
1900
|
this.data = new LLMDataModule(this.core);
|
|
681
1901
|
this.recording = new RecordingModule(this.core);
|
|
1902
|
+
this.consent = new ConsentManager(this.core, {
|
|
1903
|
+
appName: config.appName,
|
|
1904
|
+
theme: config.theme,
|
|
1905
|
+
consent: config.consent
|
|
1906
|
+
});
|
|
682
1907
|
}
|
|
683
|
-
/**
|
|
684
|
-
* Initialize the SDK. Automatically:
|
|
685
|
-
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
686
|
-
* 2. Starts LLM data collection if enabled
|
|
687
|
-
* 3. Starts UX session recording if enabled
|
|
688
|
-
* 4. Polls for config changes every 5s — instant ad format switching
|
|
689
|
-
*
|
|
690
|
-
* No custom components needed — ads render automatically.
|
|
691
|
-
* Enable `debug: true` in config to see detailed logs.
|
|
692
|
-
*/
|
|
693
1908
|
async init() {
|
|
694
1909
|
this.core.init();
|
|
695
1910
|
if (typeof document === "undefined") {
|
|
696
|
-
this.core.log("Running in non-browser environment
|
|
1911
|
+
this.core.log("Running in non-browser environment; skipping DOM injection.");
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (window !== window.top) {
|
|
1915
|
+
this.core.log("Running inside an iframe. Ads render if permissions allow.");
|
|
1916
|
+
}
|
|
1917
|
+
this.core.log("Initializing Zerocost SDK.");
|
|
1918
|
+
if (this.consent.shouldPrompt()) {
|
|
1919
|
+
this.core.log("Consent required \u2014 showing prompt.");
|
|
1920
|
+
await this.consent.promptAndWait();
|
|
1921
|
+
}
|
|
1922
|
+
if (!this.consent.has("ads")) {
|
|
1923
|
+
this.core.log("Ads consent not granted \u2014 skipping ad injection.");
|
|
697
1924
|
return;
|
|
698
1925
|
}
|
|
699
|
-
const
|
|
700
|
-
if (
|
|
701
|
-
this.
|
|
1926
|
+
const cachedConfig = this.readCachedConfig();
|
|
1927
|
+
if (cachedConfig) {
|
|
1928
|
+
this.lastConfigHash = this.configToHash(cachedConfig);
|
|
1929
|
+
this.syncDataCollection(cachedConfig.dataCollection);
|
|
1930
|
+
await this.widget.autoInjectWithConfig(cachedConfig.display, cachedConfig.widget);
|
|
1931
|
+
this.core.log("Applied cached config immediately.");
|
|
702
1932
|
}
|
|
703
|
-
this.
|
|
1933
|
+
this.startConfigSync();
|
|
704
1934
|
try {
|
|
705
|
-
|
|
706
|
-
this.
|
|
707
|
-
this.applyConfig(config);
|
|
708
|
-
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
709
|
-
this.startConfigPolling();
|
|
1935
|
+
await this.refreshConfig({ force: true, reason: "init" });
|
|
1936
|
+
this.core.log("SDK fully initialized. Ads are rendering automatically.");
|
|
710
1937
|
} catch (err) {
|
|
711
|
-
this.core.log(`Init error: ${err}. Attempting fallback ad injection
|
|
712
|
-
this.widget.autoInject();
|
|
1938
|
+
this.core.log(`Init error: ${err}. Attempting fallback ad injection.`);
|
|
1939
|
+
await this.widget.autoInject();
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
async refreshConfig(options = {}) {
|
|
1943
|
+
const now = Date.now();
|
|
1944
|
+
if (!options.force && now - this.lastConfigSyncAt < CONFIG_SYNC_DEBOUNCE_MS) {
|
|
1945
|
+
return this.configSyncInFlight ?? Promise.resolve();
|
|
713
1946
|
}
|
|
1947
|
+
if (this.configSyncInFlight) {
|
|
1948
|
+
return this.configSyncInFlight;
|
|
1949
|
+
}
|
|
1950
|
+
this.configSyncInFlight = (async () => {
|
|
1951
|
+
try {
|
|
1952
|
+
const config = await this.fetchConfig();
|
|
1953
|
+
const nextHash = this.configToHash(config);
|
|
1954
|
+
const hasDisplayChanged = nextHash !== this.lastConfigHash;
|
|
1955
|
+
this.lastConfigHash = nextHash;
|
|
1956
|
+
this.lastConfigSyncAt = Date.now();
|
|
1957
|
+
this.writeCachedConfig(config);
|
|
1958
|
+
if (hasDisplayChanged) {
|
|
1959
|
+
this.core.log(`Config change detected${options.reason ? ` via ${options.reason}` : ""}. Updating ad formats immediately.`);
|
|
1960
|
+
await this.widget.autoInjectWithConfig(config.display, config.widget);
|
|
1961
|
+
}
|
|
1962
|
+
this.syncDataCollection(config.dataCollection);
|
|
1963
|
+
} catch (err) {
|
|
1964
|
+
this.core.log(`Config sync failed${options.reason ? ` during ${options.reason}` : ""}; keeping current placements.`);
|
|
1965
|
+
throw err;
|
|
1966
|
+
} finally {
|
|
1967
|
+
this.configSyncInFlight = null;
|
|
1968
|
+
}
|
|
1969
|
+
})();
|
|
1970
|
+
return this.configSyncInFlight;
|
|
714
1971
|
}
|
|
715
1972
|
async fetchConfig() {
|
|
716
1973
|
return this.core.request("/get-placements");
|
|
717
1974
|
}
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
1975
|
+
configToHash(config) {
|
|
1976
|
+
try {
|
|
1977
|
+
return JSON.stringify({ display: config.display, widget: config.widget });
|
|
1978
|
+
} catch {
|
|
1979
|
+
return "";
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
dataCollectionToHash(dataCollection) {
|
|
1983
|
+
try {
|
|
1984
|
+
return JSON.stringify(dataCollection || {});
|
|
1985
|
+
} catch {
|
|
1986
|
+
return "";
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
syncDataCollection(dataCollection) {
|
|
1990
|
+
const nextHash = this.dataCollectionToHash(dataCollection);
|
|
1991
|
+
if (nextHash === this.lastDataCollectionHash) return;
|
|
1992
|
+
this.data.stop();
|
|
1993
|
+
this.recording.stop();
|
|
1994
|
+
if (dataCollection?.llm && this.consent.has("usageData")) {
|
|
722
1995
|
this.data.start(dataCollection.llm);
|
|
723
1996
|
}
|
|
724
|
-
if (dataCollection?.recording) {
|
|
1997
|
+
if (dataCollection?.recording && this.consent.has("aiInteractions")) {
|
|
725
1998
|
this.recording.start(dataCollection.recording);
|
|
726
1999
|
}
|
|
2000
|
+
this.lastDataCollectionHash = nextHash;
|
|
727
2001
|
}
|
|
728
|
-
|
|
2002
|
+
getConfigCacheKey() {
|
|
2003
|
+
return `${CONFIG_CACHE_PREFIX}${this.core.getConfig().appId}`;
|
|
2004
|
+
}
|
|
2005
|
+
readCachedConfig() {
|
|
2006
|
+
if (typeof window === "undefined") return null;
|
|
729
2007
|
try {
|
|
730
|
-
|
|
2008
|
+
const raw = window.localStorage.getItem(this.getConfigCacheKey());
|
|
2009
|
+
return raw ? JSON.parse(raw) : null;
|
|
731
2010
|
} catch {
|
|
732
|
-
return
|
|
2011
|
+
return null;
|
|
733
2012
|
}
|
|
734
2013
|
}
|
|
735
|
-
|
|
736
|
-
if (
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
2014
|
+
writeCachedConfig(config) {
|
|
2015
|
+
if (typeof window === "undefined") return;
|
|
2016
|
+
try {
|
|
2017
|
+
window.localStorage.setItem(this.getConfigCacheKey(), JSON.stringify(config));
|
|
2018
|
+
} catch {
|
|
2019
|
+
this.core.log("Failed to persist cached config.");
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
startConfigSync() {
|
|
2023
|
+
if (typeof window === "undefined") return;
|
|
2024
|
+
const syncIfVisible = () => {
|
|
2025
|
+
if (document.visibilityState === "visible") {
|
|
2026
|
+
this.refreshConfig({ reason: "visibility" }).catch(() => {
|
|
2027
|
+
});
|
|
748
2028
|
}
|
|
749
|
-
}
|
|
2029
|
+
};
|
|
2030
|
+
const syncIfStale = (reason) => {
|
|
2031
|
+
if (Date.now() - this.lastConfigSyncAt >= CONFIG_STALE_AFTER_MS) {
|
|
2032
|
+
this.refreshConfig({ reason }).catch(() => {
|
|
2033
|
+
});
|
|
2034
|
+
}
|
|
2035
|
+
};
|
|
2036
|
+
const onVisibilityChange = () => syncIfVisible();
|
|
2037
|
+
const onFocus = () => syncIfStale("focus");
|
|
2038
|
+
const onPageShow = () => syncIfStale("pageshow");
|
|
2039
|
+
const onOnline = () => this.refreshConfig({ force: true, reason: "online" }).catch(() => {
|
|
2040
|
+
});
|
|
2041
|
+
const onNavigation = () => syncIfStale("navigation");
|
|
2042
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
2043
|
+
window.addEventListener("focus", onFocus);
|
|
2044
|
+
window.addEventListener("pageshow", onPageShow);
|
|
2045
|
+
window.addEventListener("online", onOnline);
|
|
2046
|
+
window.addEventListener("popstate", onNavigation);
|
|
2047
|
+
window.addEventListener("hashchange", onNavigation);
|
|
2048
|
+
window.addEventListener("zerocost:navigation", onNavigation);
|
|
2049
|
+
const restoreHistoryPatch = this.patchHistory(onNavigation);
|
|
2050
|
+
this.cleanupConfigListeners.push(
|
|
2051
|
+
() => document.removeEventListener("visibilitychange", onVisibilityChange),
|
|
2052
|
+
() => window.removeEventListener("focus", onFocus),
|
|
2053
|
+
() => window.removeEventListener("pageshow", onPageShow),
|
|
2054
|
+
() => window.removeEventListener("online", onOnline),
|
|
2055
|
+
() => window.removeEventListener("popstate", onNavigation),
|
|
2056
|
+
() => window.removeEventListener("hashchange", onNavigation),
|
|
2057
|
+
() => window.removeEventListener("zerocost:navigation", onNavigation),
|
|
2058
|
+
restoreHistoryPatch
|
|
2059
|
+
);
|
|
750
2060
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
if (
|
|
756
|
-
|
|
757
|
-
|
|
2061
|
+
patchHistory(onNavigation) {
|
|
2062
|
+
if (typeof window === "undefined") return () => {
|
|
2063
|
+
};
|
|
2064
|
+
const historyRef = window.history;
|
|
2065
|
+
if (historyRef.__zerocostPatched) {
|
|
2066
|
+
return () => {
|
|
2067
|
+
};
|
|
758
2068
|
}
|
|
2069
|
+
const originalPushState = historyRef.pushState.bind(window.history);
|
|
2070
|
+
const originalReplaceState = historyRef.replaceState.bind(window.history);
|
|
2071
|
+
historyRef.__zerocostPatched = true;
|
|
2072
|
+
historyRef.__zerocostPushState = originalPushState;
|
|
2073
|
+
historyRef.__zerocostReplaceState = originalReplaceState;
|
|
2074
|
+
historyRef.pushState = ((...args) => {
|
|
2075
|
+
const result = originalPushState(...args);
|
|
2076
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
2077
|
+
onNavigation();
|
|
2078
|
+
return result;
|
|
2079
|
+
});
|
|
2080
|
+
historyRef.replaceState = ((...args) => {
|
|
2081
|
+
const result = originalReplaceState(...args);
|
|
2082
|
+
window.dispatchEvent(new Event("zerocost:navigation"));
|
|
2083
|
+
onNavigation();
|
|
2084
|
+
return result;
|
|
2085
|
+
});
|
|
2086
|
+
return () => {
|
|
2087
|
+
if (!historyRef.__zerocostPatched) return;
|
|
2088
|
+
historyRef.pushState = historyRef.__zerocostPushState || historyRef.pushState;
|
|
2089
|
+
historyRef.replaceState = historyRef.__zerocostReplaceState || historyRef.replaceState;
|
|
2090
|
+
delete historyRef.__zerocostPatched;
|
|
2091
|
+
delete historyRef.__zerocostPushState;
|
|
2092
|
+
delete historyRef.__zerocostReplaceState;
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
2095
|
+
destroy() {
|
|
2096
|
+
this.cleanupConfigListeners.forEach((cleanup) => cleanup());
|
|
2097
|
+
this.cleanupConfigListeners = [];
|
|
759
2098
|
this.widget.unmountAll();
|
|
760
2099
|
this.data.stop();
|
|
761
2100
|
this.recording.stop();
|
|
762
2101
|
}
|
|
763
|
-
/**
|
|
764
|
-
* Validate the configured API key against the server.
|
|
765
|
-
*/
|
|
766
2102
|
async validateKey() {
|
|
767
2103
|
try {
|
|
768
2104
|
const result = await this.core.request("/validate-key");
|
|
@@ -773,6 +2109,7 @@ var ZerocostSDK = class {
|
|
|
773
2109
|
}
|
|
774
2110
|
};
|
|
775
2111
|
export {
|
|
2112
|
+
ConsentManager,
|
|
776
2113
|
LLMDataModule,
|
|
777
2114
|
RecordingModule,
|
|
778
2115
|
ZerocostClient,
|