@zerocost/sdk 0.7.0 → 0.9.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/index.cjs +164 -184
- package/dist/index.d.ts +7 -0
- package/dist/index.js +164 -184
- package/dist/modules/widget.d.ts +9 -25
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -146,23 +146,24 @@ var POSITION_STYLES = {
|
|
|
146
146
|
"sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
|
|
147
147
|
"sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
|
|
148
148
|
};
|
|
149
|
-
var FORMAT_PRIORITY = ["video-widget", "
|
|
149
|
+
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
150
|
+
var AUTO_SLOT_ID = "zerocost-auto-slot";
|
|
150
151
|
var WidgetModule = class {
|
|
151
152
|
constructor(client) {
|
|
152
153
|
this.client = client;
|
|
153
154
|
}
|
|
154
155
|
mounted = /* @__PURE__ */ new Map();
|
|
155
|
-
async autoInjectWithConfig(display) {
|
|
156
|
+
async autoInjectWithConfig(display, widget) {
|
|
156
157
|
try {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
if (!
|
|
160
|
-
this.client.log("No
|
|
158
|
+
const selected = this.resolveSelectedWidget(display, widget);
|
|
159
|
+
this.clearAutoInjectedSlots();
|
|
160
|
+
if (!selected || !selected.enabled) {
|
|
161
|
+
this.client.log("No enabled widget format found. Skipping injection.");
|
|
161
162
|
return;
|
|
162
163
|
}
|
|
163
|
-
this.client.log(`Auto-inject: rendering "${
|
|
164
|
-
await this.mountSingleFormat(
|
|
165
|
-
this.client.log("\u2713
|
|
164
|
+
this.client.log(`Auto-inject: rendering configured format "${selected.format}" only.`);
|
|
165
|
+
await this.mountSingleFormat(selected);
|
|
166
|
+
this.client.log("\u2713 Single ad slot injected.");
|
|
166
167
|
} catch (err) {
|
|
167
168
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
168
169
|
}
|
|
@@ -170,21 +171,39 @@ var WidgetModule = class {
|
|
|
170
171
|
async autoInject() {
|
|
171
172
|
try {
|
|
172
173
|
this.client.log("Fetching ad placements from server...");
|
|
173
|
-
const { display } = await this.client.request("/get-placements");
|
|
174
|
-
await this.autoInjectWithConfig(display);
|
|
174
|
+
const { display, widget } = await this.client.request("/get-placements");
|
|
175
|
+
await this.autoInjectWithConfig(display, widget);
|
|
175
176
|
} catch (err) {
|
|
176
177
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
177
178
|
}
|
|
178
179
|
}
|
|
179
|
-
|
|
180
|
+
resolveSelectedWidget(display, widget) {
|
|
181
|
+
if (widget?.enabled && widget?.format) {
|
|
182
|
+
return {
|
|
183
|
+
format: widget.format,
|
|
184
|
+
position: widget.position || "bottom-right",
|
|
185
|
+
theme: widget.theme || "dark",
|
|
186
|
+
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
187
|
+
enabled: true
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const configs = this.normalizeConfigs(display);
|
|
180
191
|
for (const fmt of FORMAT_PRIORITY) {
|
|
181
192
|
const cfg = configs[fmt];
|
|
182
|
-
if (cfg?.enabled)
|
|
193
|
+
if (cfg?.enabled) {
|
|
194
|
+
return {
|
|
195
|
+
format: fmt,
|
|
196
|
+
position: cfg.position,
|
|
197
|
+
theme: cfg.theme,
|
|
198
|
+
autoplay: cfg.autoplay,
|
|
199
|
+
enabled: true
|
|
200
|
+
};
|
|
201
|
+
}
|
|
183
202
|
}
|
|
184
203
|
return null;
|
|
185
204
|
}
|
|
186
205
|
normalizeConfigs(display) {
|
|
187
|
-
if (display && "video-widget" in display) {
|
|
206
|
+
if (display && typeof display === "object" && "video-widget" in display) {
|
|
188
207
|
return display;
|
|
189
208
|
}
|
|
190
209
|
const pos = display?.position || "bottom-right";
|
|
@@ -193,22 +212,58 @@ var WidgetModule = class {
|
|
|
193
212
|
"video-widget": { position: pos, theme, autoplay: true, enabled: true },
|
|
194
213
|
"tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
|
|
195
214
|
"sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
|
|
215
|
+
"sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
|
|
196
216
|
"inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
|
|
197
217
|
};
|
|
198
218
|
}
|
|
199
|
-
async mountSingleFormat(
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
219
|
+
async mountSingleFormat(config) {
|
|
220
|
+
const isInline = config.format === "inline-text";
|
|
221
|
+
const targetElementId = isInline ? this.ensureInlineTarget(config.position) : AUTO_SLOT_ID;
|
|
222
|
+
if (!targetElementId) {
|
|
223
|
+
this.client.log("Inline target not found. Skipping inline ad render.");
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (!isInline) {
|
|
227
|
+
let el = document.getElementById(AUTO_SLOT_ID);
|
|
228
|
+
if (!el) {
|
|
229
|
+
el = document.createElement("div");
|
|
230
|
+
el.id = AUTO_SLOT_ID;
|
|
231
|
+
document.body.appendChild(el);
|
|
232
|
+
}
|
|
233
|
+
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
234
|
+
const maxW = config.format === "video-widget" ? "max-width:200px;" : config.format === "sponsored-card" || config.format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
|
|
235
|
+
el.setAttribute("style", `${posStyle}${maxW}`);
|
|
236
|
+
el.setAttribute("data-zerocost", "");
|
|
237
|
+
el.setAttribute("data-format", config.format);
|
|
238
|
+
}
|
|
239
|
+
await this.mount(targetElementId, {
|
|
240
|
+
format: config.format,
|
|
241
|
+
refreshInterval: 0,
|
|
242
|
+
// Config polling handles re-rendering
|
|
243
|
+
theme: config.theme,
|
|
244
|
+
autoplay: config.autoplay,
|
|
245
|
+
position: config.position
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
ensureInlineTarget(position) {
|
|
249
|
+
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
250
|
+
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
251
|
+
const paragraphs = Array.from(document.querySelectorAll("p"));
|
|
252
|
+
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
253
|
+
if (!anchor) return null;
|
|
254
|
+
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
255
|
+
if (existing) existing.remove();
|
|
256
|
+
const inlineId = `${AUTO_SLOT_ID}-inline`;
|
|
257
|
+
let target = document.getElementById(inlineId);
|
|
258
|
+
if (!target) {
|
|
259
|
+
target = document.createElement("div");
|
|
260
|
+
target.id = inlineId;
|
|
261
|
+
target.setAttribute("data-zerocost", "");
|
|
262
|
+
target.setAttribute("data-format", "inline-text");
|
|
263
|
+
target.style.margin = "12px 0";
|
|
264
|
+
anchor.insertAdjacentElement("afterend", target);
|
|
265
|
+
}
|
|
266
|
+
return inlineId;
|
|
212
267
|
}
|
|
213
268
|
async mount(targetElementId, options = {}) {
|
|
214
269
|
const el = document.getElementById(targetElementId);
|
|
@@ -217,36 +272,25 @@ var WidgetModule = class {
|
|
|
217
272
|
const refreshMs = (options.refreshInterval ?? 30) * 1e3;
|
|
218
273
|
const theme = options.theme || "dark";
|
|
219
274
|
const format = options.format || "video-widget";
|
|
220
|
-
const autoplay = options.autoplay ??
|
|
275
|
+
const autoplay = options.autoplay ?? format === "video-widget";
|
|
221
276
|
const render = async () => {
|
|
222
277
|
try {
|
|
223
|
-
const formatMap = {
|
|
224
|
-
"video-widget": "video",
|
|
225
|
-
"sponsored-card": "display",
|
|
226
|
-
"sidebar-display": "display",
|
|
227
|
-
"tooltip-ad": "native",
|
|
228
|
-
"inline-text": "native"
|
|
229
|
-
};
|
|
230
278
|
const body = {
|
|
231
|
-
|
|
279
|
+
widget_style: format,
|
|
232
280
|
theme,
|
|
233
|
-
autoplay
|
|
281
|
+
autoplay,
|
|
282
|
+
position: options.position
|
|
234
283
|
};
|
|
235
284
|
const data = await this.client.request("/serve-widget", body);
|
|
236
285
|
const ad = data.ad;
|
|
237
286
|
if (!ad || !data.html) {
|
|
238
|
-
this.client.log(`No ad inventory available for format "${format}"
|
|
287
|
+
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
239
288
|
el.innerHTML = "";
|
|
240
289
|
return;
|
|
241
290
|
}
|
|
242
291
|
el.innerHTML = data.html;
|
|
243
292
|
el.setAttribute("data-zerocost-ad-id", ad.id);
|
|
244
|
-
|
|
245
|
-
if (video) {
|
|
246
|
-
video.muted = true;
|
|
247
|
-
video.play().catch(() => {
|
|
248
|
-
});
|
|
249
|
-
}
|
|
293
|
+
this.ensureVideoPlayback(el);
|
|
250
294
|
const ctas = el.querySelectorAll("[data-zc-cta]");
|
|
251
295
|
ctas.forEach((cta) => {
|
|
252
296
|
cta.addEventListener("click", () => {
|
|
@@ -262,10 +306,7 @@ var WidgetModule = class {
|
|
|
262
306
|
closeBtn.addEventListener("click", (e) => {
|
|
263
307
|
e.preventDefault();
|
|
264
308
|
e.stopPropagation();
|
|
265
|
-
|
|
266
|
-
if (mountInfo?.interval) clearInterval(mountInfo.interval);
|
|
267
|
-
this.mounted.delete(targetElementId);
|
|
268
|
-
el.remove();
|
|
309
|
+
this.unmount(targetElementId);
|
|
269
310
|
});
|
|
270
311
|
}
|
|
271
312
|
} catch (err) {
|
|
@@ -276,138 +317,37 @@ var WidgetModule = class {
|
|
|
276
317
|
const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
|
|
277
318
|
this.mounted.set(targetElementId, { elementId: targetElementId, interval });
|
|
278
319
|
}
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
if (
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
buildFormatHtml(format, ad, theme, autoplay) {
|
|
293
|
-
switch (format) {
|
|
294
|
-
case "video-widget":
|
|
295
|
-
return this.buildVideoWidget(ad, theme, autoplay);
|
|
296
|
-
case "tooltip-ad":
|
|
297
|
-
return this.buildTooltipAd(ad, theme);
|
|
298
|
-
case "sponsored-card":
|
|
299
|
-
return this.buildSponsoredCard(ad, theme);
|
|
300
|
-
case "inline-text":
|
|
301
|
-
return this.buildInlineText(ad, theme);
|
|
302
|
-
default:
|
|
303
|
-
return this.buildSponsoredCard(ad, theme);
|
|
320
|
+
ensureVideoPlayback(root) {
|
|
321
|
+
const video = root.querySelector("video");
|
|
322
|
+
if (!video) return;
|
|
323
|
+
video.muted = true;
|
|
324
|
+
video.autoplay = true;
|
|
325
|
+
video.loop = true;
|
|
326
|
+
video.playsInline = true;
|
|
327
|
+
video.preload = "auto";
|
|
328
|
+
const tryPlay = () => video.play().catch(() => {
|
|
329
|
+
});
|
|
330
|
+
if (video.readyState >= 2) {
|
|
331
|
+
tryPlay();
|
|
332
|
+
return;
|
|
304
333
|
}
|
|
334
|
+
video.addEventListener("loadeddata", tryPlay, { once: true });
|
|
335
|
+
video.addEventListener("canplay", tryPlay, { once: true });
|
|
305
336
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
buildVideoWidget(ad, theme, autoplay) {
|
|
311
|
-
const isDark = theme === "dark";
|
|
312
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
313
|
-
const videoSrc = ad.video_url || ad.image_url || "";
|
|
314
|
-
const hasVideo = !!ad.video_url;
|
|
315
|
-
const sponsor = ad.title || "Sponsor";
|
|
316
|
-
const cta = ad.cta_text || "Learn More";
|
|
317
|
-
return `
|
|
318
|
-
<div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
|
|
319
|
-
<button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
|
|
320
|
-
${hasVideo ? `<video src="${this.esc(videoSrc)}" autoplay muted loop playsinline preload="auto" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
|
|
321
|
-
<div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
|
|
322
|
-
<div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
|
|
323
|
-
<div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
|
|
324
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
|
|
325
|
-
</div>
|
|
326
|
-
</div>`;
|
|
327
|
-
}
|
|
328
|
-
/**
|
|
329
|
-
* Tooltip Ad — matches playground ContextualTextPreview
|
|
330
|
-
* Small floating card with sponsored label, inline text, and link
|
|
331
|
-
*/
|
|
332
|
-
buildTooltipAd(ad, theme) {
|
|
333
|
-
const isDark = theme === "dark";
|
|
334
|
-
const bg = isDark ? "#111" : "#ffffff";
|
|
335
|
-
const fg = isDark ? "#fff" : "#111";
|
|
336
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
337
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
338
|
-
return `
|
|
339
|
-
<div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;position:relative;">
|
|
340
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
341
|
-
<div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
|
|
342
|
-
<span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
|
|
343
|
-
Sponsored
|
|
344
|
-
</div>
|
|
345
|
-
<p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
|
|
346
|
-
${this.esc(ad.description || ad.title)}
|
|
347
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
348
|
-
</p>
|
|
349
|
-
</div>`;
|
|
337
|
+
clearAutoInjectedSlots() {
|
|
338
|
+
this.unmountAll();
|
|
339
|
+
const existing = document.querySelectorAll("[data-zerocost], #zerocost-auto-slot, #zerocost-auto-slot-inline");
|
|
340
|
+
existing.forEach((node) => node.remove());
|
|
350
341
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const bg = isDark ? "#111" : "#ffffff";
|
|
358
|
-
const fg = isDark ? "#fff" : "#111";
|
|
359
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
360
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
361
|
-
return `
|
|
362
|
-
<div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;position:relative;">
|
|
363
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
364
|
-
${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
|
|
365
|
-
<div style="padding:12px;">
|
|
366
|
-
<div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
|
|
367
|
-
<div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
|
|
368
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
369
|
-
</div>
|
|
370
|
-
</div>`;
|
|
342
|
+
unmount(targetElementId) {
|
|
343
|
+
const slot = this.mounted.get(targetElementId);
|
|
344
|
+
if (slot?.interval) clearInterval(slot.interval);
|
|
345
|
+
const el = document.getElementById(targetElementId);
|
|
346
|
+
if (el) el.remove();
|
|
347
|
+
this.mounted.delete(targetElementId);
|
|
371
348
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
* Card with sponsored header, icon, headline, body, CTA button
|
|
375
|
-
* Designed to be injected inside content/chat streams
|
|
376
|
-
*/
|
|
377
|
-
buildInlineText(ad, theme) {
|
|
378
|
-
const isDark = theme === "dark";
|
|
379
|
-
const bg = isDark ? "#141414" : "#fafafa";
|
|
380
|
-
const fg = isDark ? "#fff" : "#111";
|
|
381
|
-
const fgMuted = isDark ? "#aaa" : "#666";
|
|
382
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
383
|
-
const border = isDark ? "#2a2a2a" : "#d0d0d0";
|
|
384
|
-
const headerBorder = isDark ? "#222" : "#e8e8e8";
|
|
385
|
-
const btnBg = isDark ? "#fff" : "#111";
|
|
386
|
-
const btnFg = isDark ? "#111" : "#fff";
|
|
387
|
-
const initial = "\u25B2";
|
|
388
|
-
const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
|
|
389
|
-
return `
|
|
390
|
-
<div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;position:relative;">
|
|
391
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
392
|
-
<div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
|
|
393
|
-
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
|
|
394
|
-
<span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
|
|
395
|
-
</div>
|
|
396
|
-
<div style="padding:12px;">
|
|
397
|
-
<div style="display:flex;gap:10px;">
|
|
398
|
-
<div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
|
|
399
|
-
<div style="flex:1;min-width:0;">
|
|
400
|
-
<div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
|
|
401
|
-
<div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
|
|
402
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
|
|
403
|
-
</div>
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
</div>`;
|
|
407
|
-
}
|
|
408
|
-
esc(str) {
|
|
409
|
-
if (!str) return "";
|
|
410
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
349
|
+
unmountAll() {
|
|
350
|
+
for (const id of Array.from(this.mounted.keys())) this.unmount(id);
|
|
411
351
|
}
|
|
412
352
|
};
|
|
413
353
|
|
|
@@ -747,6 +687,7 @@ var RecordingModule = class {
|
|
|
747
687
|
};
|
|
748
688
|
|
|
749
689
|
// src/index.ts
|
|
690
|
+
var CONFIG_POLL_INTERVAL = 5e3;
|
|
750
691
|
var ZerocostSDK = class {
|
|
751
692
|
core;
|
|
752
693
|
ads;
|
|
@@ -754,6 +695,8 @@ var ZerocostSDK = class {
|
|
|
754
695
|
widget;
|
|
755
696
|
data;
|
|
756
697
|
recording;
|
|
698
|
+
configPollTimer = null;
|
|
699
|
+
lastConfigHash = "";
|
|
757
700
|
constructor(config) {
|
|
758
701
|
this.core = new ZerocostClient(config);
|
|
759
702
|
this.ads = new AdsModule(this.core);
|
|
@@ -767,6 +710,7 @@ var ZerocostSDK = class {
|
|
|
767
710
|
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
768
711
|
* 2. Starts LLM data collection if enabled
|
|
769
712
|
* 3. Starts UX session recording if enabled
|
|
713
|
+
* 4. Polls for config changes every 5s — instant ad format switching
|
|
770
714
|
*
|
|
771
715
|
* No custom components needed — ads render automatically.
|
|
772
716
|
* Enable `debug: true` in config to see detailed logs.
|
|
@@ -783,24 +727,60 @@ var ZerocostSDK = class {
|
|
|
783
727
|
}
|
|
784
728
|
this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
|
|
785
729
|
try {
|
|
786
|
-
const
|
|
787
|
-
this.
|
|
788
|
-
|
|
789
|
-
this.data.start(dataCollection.llm);
|
|
790
|
-
}
|
|
791
|
-
if (dataCollection?.recording) {
|
|
792
|
-
this.recording.start(dataCollection.recording);
|
|
793
|
-
}
|
|
730
|
+
const config = await this.fetchConfig();
|
|
731
|
+
this.lastConfigHash = this.configToHash(config);
|
|
732
|
+
this.applyConfig(config);
|
|
794
733
|
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
734
|
+
this.startConfigPolling();
|
|
795
735
|
} catch (err) {
|
|
796
736
|
this.core.log(`Init error: ${err}. Attempting fallback ad injection...`);
|
|
797
737
|
this.widget.autoInject();
|
|
798
738
|
}
|
|
799
739
|
}
|
|
740
|
+
async fetchConfig() {
|
|
741
|
+
return this.core.request("/get-placements");
|
|
742
|
+
}
|
|
743
|
+
applyConfig(config) {
|
|
744
|
+
const { display, widget, dataCollection } = config;
|
|
745
|
+
this.widget.autoInjectWithConfig(display, widget);
|
|
746
|
+
if (dataCollection?.llm) {
|
|
747
|
+
this.data.start(dataCollection.llm);
|
|
748
|
+
}
|
|
749
|
+
if (dataCollection?.recording) {
|
|
750
|
+
this.recording.start(dataCollection.recording);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
configToHash(config) {
|
|
754
|
+
try {
|
|
755
|
+
return JSON.stringify({ d: config.display, w: config.widget });
|
|
756
|
+
} catch {
|
|
757
|
+
return "";
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
startConfigPolling() {
|
|
761
|
+
if (this.configPollTimer) return;
|
|
762
|
+
this.configPollTimer = setInterval(async () => {
|
|
763
|
+
try {
|
|
764
|
+
const config = await this.fetchConfig();
|
|
765
|
+
const hash = this.configToHash(config);
|
|
766
|
+
if (this.lastConfigHash && hash !== this.lastConfigHash) {
|
|
767
|
+
this.core.log("\u26A1 Config change detected \u2014 switching ad format instantly.");
|
|
768
|
+
this.widget.unmountAll();
|
|
769
|
+
this.applyConfig(config);
|
|
770
|
+
}
|
|
771
|
+
this.lastConfigHash = hash;
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
}, CONFIG_POLL_INTERVAL);
|
|
775
|
+
}
|
|
800
776
|
/**
|
|
801
777
|
* Tear down all modules.
|
|
802
778
|
*/
|
|
803
779
|
destroy() {
|
|
780
|
+
if (this.configPollTimer) {
|
|
781
|
+
clearInterval(this.configPollTimer);
|
|
782
|
+
this.configPollTimer = null;
|
|
783
|
+
}
|
|
804
784
|
this.widget.unmountAll();
|
|
805
785
|
this.data.stop();
|
|
806
786
|
this.recording.stop();
|
package/dist/index.d.ts
CHANGED
|
@@ -12,17 +12,24 @@ export declare class ZerocostSDK {
|
|
|
12
12
|
widget: WidgetModule;
|
|
13
13
|
data: LLMDataModule;
|
|
14
14
|
recording: RecordingModule;
|
|
15
|
+
private configPollTimer;
|
|
16
|
+
private lastConfigHash;
|
|
15
17
|
constructor(config: ZerocostConfig);
|
|
16
18
|
/**
|
|
17
19
|
* Initialize the SDK. Automatically:
|
|
18
20
|
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
19
21
|
* 2. Starts LLM data collection if enabled
|
|
20
22
|
* 3. Starts UX session recording if enabled
|
|
23
|
+
* 4. Polls for config changes every 5s — instant ad format switching
|
|
21
24
|
*
|
|
22
25
|
* No custom components needed — ads render automatically.
|
|
23
26
|
* Enable `debug: true` in config to see detailed logs.
|
|
24
27
|
*/
|
|
25
28
|
init(): Promise<void>;
|
|
29
|
+
private fetchConfig;
|
|
30
|
+
private applyConfig;
|
|
31
|
+
private configToHash;
|
|
32
|
+
private startConfigPolling;
|
|
26
33
|
/**
|
|
27
34
|
* Tear down all modules.
|
|
28
35
|
*/
|
package/dist/index.js
CHANGED
|
@@ -117,23 +117,24 @@ var POSITION_STYLES = {
|
|
|
117
117
|
"sidebar-left": "position:fixed;top:50%;left:24px;transform:translateY(-50%);z-index:9999;",
|
|
118
118
|
"sidebar-right": "position:fixed;top:50%;right:24px;transform:translateY(-50%);z-index:9999;"
|
|
119
119
|
};
|
|
120
|
-
var FORMAT_PRIORITY = ["video-widget", "
|
|
120
|
+
var FORMAT_PRIORITY = ["video-widget", "tooltip-ad", "sponsored-card", "sidebar-display", "inline-text"];
|
|
121
|
+
var AUTO_SLOT_ID = "zerocost-auto-slot";
|
|
121
122
|
var WidgetModule = class {
|
|
122
123
|
constructor(client) {
|
|
123
124
|
this.client = client;
|
|
124
125
|
}
|
|
125
126
|
mounted = /* @__PURE__ */ new Map();
|
|
126
|
-
async autoInjectWithConfig(display) {
|
|
127
|
+
async autoInjectWithConfig(display, widget) {
|
|
127
128
|
try {
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (!
|
|
131
|
-
this.client.log("No
|
|
129
|
+
const selected = this.resolveSelectedWidget(display, widget);
|
|
130
|
+
this.clearAutoInjectedSlots();
|
|
131
|
+
if (!selected || !selected.enabled) {
|
|
132
|
+
this.client.log("No enabled widget format found. Skipping injection.");
|
|
132
133
|
return;
|
|
133
134
|
}
|
|
134
|
-
this.client.log(`Auto-inject: rendering "${
|
|
135
|
-
await this.mountSingleFormat(
|
|
136
|
-
this.client.log("\u2713
|
|
135
|
+
this.client.log(`Auto-inject: rendering configured format "${selected.format}" only.`);
|
|
136
|
+
await this.mountSingleFormat(selected);
|
|
137
|
+
this.client.log("\u2713 Single ad slot injected.");
|
|
137
138
|
} catch (err) {
|
|
138
139
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
139
140
|
}
|
|
@@ -141,21 +142,39 @@ var WidgetModule = class {
|
|
|
141
142
|
async autoInject() {
|
|
142
143
|
try {
|
|
143
144
|
this.client.log("Fetching ad placements from server...");
|
|
144
|
-
const { display } = await this.client.request("/get-placements");
|
|
145
|
-
await this.autoInjectWithConfig(display);
|
|
145
|
+
const { display, widget } = await this.client.request("/get-placements");
|
|
146
|
+
await this.autoInjectWithConfig(display, widget);
|
|
146
147
|
} catch (err) {
|
|
147
148
|
this.client.log(`Widget autoInject error: ${err}`);
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
+
resolveSelectedWidget(display, widget) {
|
|
152
|
+
if (widget?.enabled && widget?.format) {
|
|
153
|
+
return {
|
|
154
|
+
format: widget.format,
|
|
155
|
+
position: widget.position || "bottom-right",
|
|
156
|
+
theme: widget.theme || "dark",
|
|
157
|
+
autoplay: widget.autoplay ?? widget.format === "video-widget",
|
|
158
|
+
enabled: true
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const configs = this.normalizeConfigs(display);
|
|
151
162
|
for (const fmt of FORMAT_PRIORITY) {
|
|
152
163
|
const cfg = configs[fmt];
|
|
153
|
-
if (cfg?.enabled)
|
|
164
|
+
if (cfg?.enabled) {
|
|
165
|
+
return {
|
|
166
|
+
format: fmt,
|
|
167
|
+
position: cfg.position,
|
|
168
|
+
theme: cfg.theme,
|
|
169
|
+
autoplay: cfg.autoplay,
|
|
170
|
+
enabled: true
|
|
171
|
+
};
|
|
172
|
+
}
|
|
154
173
|
}
|
|
155
174
|
return null;
|
|
156
175
|
}
|
|
157
176
|
normalizeConfigs(display) {
|
|
158
|
-
if (display && "video-widget" in display) {
|
|
177
|
+
if (display && typeof display === "object" && "video-widget" in display) {
|
|
159
178
|
return display;
|
|
160
179
|
}
|
|
161
180
|
const pos = display?.position || "bottom-right";
|
|
@@ -164,22 +183,58 @@ var WidgetModule = class {
|
|
|
164
183
|
"video-widget": { position: pos, theme, autoplay: true, enabled: true },
|
|
165
184
|
"tooltip-ad": { position: pos, theme, autoplay: false, enabled: false },
|
|
166
185
|
"sponsored-card": { position: pos, theme, autoplay: false, enabled: false },
|
|
186
|
+
"sidebar-display": { position: pos, theme, autoplay: false, enabled: false },
|
|
167
187
|
"inline-text": { position: "after-paragraph-1", theme, autoplay: false, enabled: false }
|
|
168
188
|
};
|
|
169
189
|
}
|
|
170
|
-
async mountSingleFormat(
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
190
|
+
async mountSingleFormat(config) {
|
|
191
|
+
const isInline = config.format === "inline-text";
|
|
192
|
+
const targetElementId = isInline ? this.ensureInlineTarget(config.position) : AUTO_SLOT_ID;
|
|
193
|
+
if (!targetElementId) {
|
|
194
|
+
this.client.log("Inline target not found. Skipping inline ad render.");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!isInline) {
|
|
198
|
+
let el = document.getElementById(AUTO_SLOT_ID);
|
|
199
|
+
if (!el) {
|
|
200
|
+
el = document.createElement("div");
|
|
201
|
+
el.id = AUTO_SLOT_ID;
|
|
202
|
+
document.body.appendChild(el);
|
|
203
|
+
}
|
|
204
|
+
const posStyle = POSITION_STYLES[config.position] || POSITION_STYLES["bottom-right"];
|
|
205
|
+
const maxW = config.format === "video-widget" ? "max-width:200px;" : config.format === "sponsored-card" || config.format === "sidebar-display" ? "max-width:176px;" : "max-width:320px;";
|
|
206
|
+
el.setAttribute("style", `${posStyle}${maxW}`);
|
|
207
|
+
el.setAttribute("data-zerocost", "");
|
|
208
|
+
el.setAttribute("data-format", config.format);
|
|
209
|
+
}
|
|
210
|
+
await this.mount(targetElementId, {
|
|
211
|
+
format: config.format,
|
|
212
|
+
refreshInterval: 0,
|
|
213
|
+
// Config polling handles re-rendering
|
|
214
|
+
theme: config.theme,
|
|
215
|
+
autoplay: config.autoplay,
|
|
216
|
+
position: config.position
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
ensureInlineTarget(position) {
|
|
220
|
+
const paragraphMatch = /after-paragraph-(\d+)/.exec(position || "");
|
|
221
|
+
const index = paragraphMatch ? Number(paragraphMatch[1]) : 1;
|
|
222
|
+
const paragraphs = Array.from(document.querySelectorAll("p"));
|
|
223
|
+
const anchor = paragraphs[Math.max(0, Math.min(paragraphs.length - 1, index - 1))];
|
|
224
|
+
if (!anchor) return null;
|
|
225
|
+
const existing = document.getElementById(AUTO_SLOT_ID);
|
|
226
|
+
if (existing) existing.remove();
|
|
227
|
+
const inlineId = `${AUTO_SLOT_ID}-inline`;
|
|
228
|
+
let target = document.getElementById(inlineId);
|
|
229
|
+
if (!target) {
|
|
230
|
+
target = document.createElement("div");
|
|
231
|
+
target.id = inlineId;
|
|
232
|
+
target.setAttribute("data-zerocost", "");
|
|
233
|
+
target.setAttribute("data-format", "inline-text");
|
|
234
|
+
target.style.margin = "12px 0";
|
|
235
|
+
anchor.insertAdjacentElement("afterend", target);
|
|
236
|
+
}
|
|
237
|
+
return inlineId;
|
|
183
238
|
}
|
|
184
239
|
async mount(targetElementId, options = {}) {
|
|
185
240
|
const el = document.getElementById(targetElementId);
|
|
@@ -188,36 +243,25 @@ var WidgetModule = class {
|
|
|
188
243
|
const refreshMs = (options.refreshInterval ?? 30) * 1e3;
|
|
189
244
|
const theme = options.theme || "dark";
|
|
190
245
|
const format = options.format || "video-widget";
|
|
191
|
-
const autoplay = options.autoplay ??
|
|
246
|
+
const autoplay = options.autoplay ?? format === "video-widget";
|
|
192
247
|
const render = async () => {
|
|
193
248
|
try {
|
|
194
|
-
const formatMap = {
|
|
195
|
-
"video-widget": "video",
|
|
196
|
-
"sponsored-card": "display",
|
|
197
|
-
"sidebar-display": "display",
|
|
198
|
-
"tooltip-ad": "native",
|
|
199
|
-
"inline-text": "native"
|
|
200
|
-
};
|
|
201
249
|
const body = {
|
|
202
|
-
|
|
250
|
+
widget_style: format,
|
|
203
251
|
theme,
|
|
204
|
-
autoplay
|
|
252
|
+
autoplay,
|
|
253
|
+
position: options.position
|
|
205
254
|
};
|
|
206
255
|
const data = await this.client.request("/serve-widget", body);
|
|
207
256
|
const ad = data.ad;
|
|
208
257
|
if (!ad || !data.html) {
|
|
209
|
-
this.client.log(`No ad inventory available for format "${format}"
|
|
258
|
+
this.client.log(`No ad inventory available for configured format "${format}".`);
|
|
210
259
|
el.innerHTML = "";
|
|
211
260
|
return;
|
|
212
261
|
}
|
|
213
262
|
el.innerHTML = data.html;
|
|
214
263
|
el.setAttribute("data-zerocost-ad-id", ad.id);
|
|
215
|
-
|
|
216
|
-
if (video) {
|
|
217
|
-
video.muted = true;
|
|
218
|
-
video.play().catch(() => {
|
|
219
|
-
});
|
|
220
|
-
}
|
|
264
|
+
this.ensureVideoPlayback(el);
|
|
221
265
|
const ctas = el.querySelectorAll("[data-zc-cta]");
|
|
222
266
|
ctas.forEach((cta) => {
|
|
223
267
|
cta.addEventListener("click", () => {
|
|
@@ -233,10 +277,7 @@ var WidgetModule = class {
|
|
|
233
277
|
closeBtn.addEventListener("click", (e) => {
|
|
234
278
|
e.preventDefault();
|
|
235
279
|
e.stopPropagation();
|
|
236
|
-
|
|
237
|
-
if (mountInfo?.interval) clearInterval(mountInfo.interval);
|
|
238
|
-
this.mounted.delete(targetElementId);
|
|
239
|
-
el.remove();
|
|
280
|
+
this.unmount(targetElementId);
|
|
240
281
|
});
|
|
241
282
|
}
|
|
242
283
|
} catch (err) {
|
|
@@ -247,138 +288,37 @@ var WidgetModule = class {
|
|
|
247
288
|
const interval = refreshMs > 0 ? setInterval(render, refreshMs) : null;
|
|
248
289
|
this.mounted.set(targetElementId, { elementId: targetElementId, interval });
|
|
249
290
|
}
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
buildFormatHtml(format, ad, theme, autoplay) {
|
|
264
|
-
switch (format) {
|
|
265
|
-
case "video-widget":
|
|
266
|
-
return this.buildVideoWidget(ad, theme, autoplay);
|
|
267
|
-
case "tooltip-ad":
|
|
268
|
-
return this.buildTooltipAd(ad, theme);
|
|
269
|
-
case "sponsored-card":
|
|
270
|
-
return this.buildSponsoredCard(ad, theme);
|
|
271
|
-
case "inline-text":
|
|
272
|
-
return this.buildInlineText(ad, theme);
|
|
273
|
-
default:
|
|
274
|
-
return this.buildSponsoredCard(ad, theme);
|
|
291
|
+
ensureVideoPlayback(root) {
|
|
292
|
+
const video = root.querySelector("video");
|
|
293
|
+
if (!video) return;
|
|
294
|
+
video.muted = true;
|
|
295
|
+
video.autoplay = true;
|
|
296
|
+
video.loop = true;
|
|
297
|
+
video.playsInline = true;
|
|
298
|
+
video.preload = "auto";
|
|
299
|
+
const tryPlay = () => video.play().catch(() => {
|
|
300
|
+
});
|
|
301
|
+
if (video.readyState >= 2) {
|
|
302
|
+
tryPlay();
|
|
303
|
+
return;
|
|
275
304
|
}
|
|
305
|
+
video.addEventListener("loadeddata", tryPlay, { once: true });
|
|
306
|
+
video.addEventListener("canplay", tryPlay, { once: true });
|
|
276
307
|
}
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
buildVideoWidget(ad, theme, autoplay) {
|
|
282
|
-
const isDark = theme === "dark";
|
|
283
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
284
|
-
const videoSrc = ad.video_url || ad.image_url || "";
|
|
285
|
-
const hasVideo = !!ad.video_url;
|
|
286
|
-
const sponsor = ad.title || "Sponsor";
|
|
287
|
-
const cta = ad.cta_text || "Learn More";
|
|
288
|
-
return `
|
|
289
|
-
<div style="width:200px;aspect-ratio:9/16;border-radius:12px;border:1px solid ${border};overflow:hidden;position:relative;font-family:system-ui,-apple-system,sans-serif;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.5" : "0.15"});">
|
|
290
|
-
<button data-zc-close style="position:absolute;top:8px;right:8px;z-index:20;width:24px;height:24px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:14px;line-height:1;">\u2715</button>
|
|
291
|
-
${hasVideo ? `<video src="${this.esc(videoSrc)}" autoplay muted loop playsinline preload="auto" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></video>` : `<img src="${this.esc(videoSrc)}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;" />`}
|
|
292
|
-
<div style="position:absolute;inset-inline:0;bottom:0;padding:12px;background:linear-gradient(to top,rgba(0,0,0,0.9),rgba(0,0,0,0.5),transparent);z-index:10;">
|
|
293
|
-
<div style="font-size:8px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:rgba(255,255,255,0.5);margin-bottom:2px;">Sponsored by ${this.esc(sponsor)}</div>
|
|
294
|
-
<div style="color:#fff;font-weight:600;font-size:12px;line-height:1.3;">${this.esc(ad.description || "")}</div>
|
|
295
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;margin-top:8px;width:100%;padding:6px 0;background:#fff;color:#000;font-size:10px;font-weight:700;text-align:center;border-radius:6px;text-decoration:none;cursor:pointer;">${this.esc(cta)}</a>
|
|
296
|
-
</div>
|
|
297
|
-
</div>`;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Tooltip Ad — matches playground ContextualTextPreview
|
|
301
|
-
* Small floating card with sponsored label, inline text, and link
|
|
302
|
-
*/
|
|
303
|
-
buildTooltipAd(ad, theme) {
|
|
304
|
-
const isDark = theme === "dark";
|
|
305
|
-
const bg = isDark ? "#111" : "#ffffff";
|
|
306
|
-
const fg = isDark ? "#fff" : "#111";
|
|
307
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
308
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
309
|
-
return `
|
|
310
|
-
<div style="max-width:320px;border-radius:8px;padding:12px;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;position:relative;">
|
|
311
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
312
|
-
<div style="font-size:10px;font-family:monospace;margin-bottom:6px;display:flex;align-items:center;color:${fgFaint};">
|
|
313
|
-
<span style="width:6px;height:6px;border-radius:50%;background:#22c55e;margin-right:6px;display:inline-block;"></span>
|
|
314
|
-
Sponsored
|
|
315
|
-
</div>
|
|
316
|
-
<p style="font-size:12px;line-height:1.5;color:${fg};margin:0;">
|
|
317
|
-
${this.esc(ad.description || ad.title)}
|
|
318
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="color:#60a5fa;text-decoration:underline;text-underline-offset:2px;cursor:pointer;margin-left:4px;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
319
|
-
</p>
|
|
320
|
-
</div>`;
|
|
308
|
+
clearAutoInjectedSlots() {
|
|
309
|
+
this.unmountAll();
|
|
310
|
+
const existing = document.querySelectorAll("[data-zerocost], #zerocost-auto-slot, #zerocost-auto-slot-inline");
|
|
311
|
+
existing.forEach((node) => node.remove());
|
|
321
312
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
const bg = isDark ? "#111" : "#ffffff";
|
|
329
|
-
const fg = isDark ? "#fff" : "#111";
|
|
330
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
331
|
-
const border = isDark ? "#333" : "#e0e0e0";
|
|
332
|
-
return `
|
|
333
|
-
<div style="width:176px;border-radius:12px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,${isDark ? "0.4" : "0.12"});background:${bg};border:1px solid ${border};font-family:system-ui,-apple-system,sans-serif;position:relative;">
|
|
334
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);border:none;color:rgba(255,255,255,0.7);cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
335
|
-
${ad.image_url ? `<img src="${this.esc(ad.image_url)}" style="width:100%;height:80px;object-fit:cover;display:block;" />` : `<div style="height:80px;background:linear-gradient(135deg,rgba(249,115,22,0.2),rgba(236,72,153,0.2));"></div>`}
|
|
336
|
-
<div style="padding:12px;">
|
|
337
|
-
<div style="font-weight:700;font-size:12px;color:${fg};margin-bottom:2px;letter-spacing:-0.01em;">${this.esc(ad.title)}</div>
|
|
338
|
-
<div style="font-size:10px;color:${fgFaint};line-height:1.4;margin-bottom:12px;">${this.esc(ad.description || "")}</div>
|
|
339
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:block;width:100%;padding:6px 0;border:1px solid ${border};border-radius:4px;font-size:10px;font-weight:500;color:${fg};text-align:center;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")}</a>
|
|
340
|
-
</div>
|
|
341
|
-
</div>`;
|
|
313
|
+
unmount(targetElementId) {
|
|
314
|
+
const slot = this.mounted.get(targetElementId);
|
|
315
|
+
if (slot?.interval) clearInterval(slot.interval);
|
|
316
|
+
const el = document.getElementById(targetElementId);
|
|
317
|
+
if (el) el.remove();
|
|
318
|
+
this.mounted.delete(targetElementId);
|
|
342
319
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
* Card with sponsored header, icon, headline, body, CTA button
|
|
346
|
-
* Designed to be injected inside content/chat streams
|
|
347
|
-
*/
|
|
348
|
-
buildInlineText(ad, theme) {
|
|
349
|
-
const isDark = theme === "dark";
|
|
350
|
-
const bg = isDark ? "#141414" : "#fafafa";
|
|
351
|
-
const fg = isDark ? "#fff" : "#111";
|
|
352
|
-
const fgMuted = isDark ? "#aaa" : "#666";
|
|
353
|
-
const fgFaint = isDark ? "#888" : "#999";
|
|
354
|
-
const border = isDark ? "#2a2a2a" : "#d0d0d0";
|
|
355
|
-
const headerBorder = isDark ? "#222" : "#e8e8e8";
|
|
356
|
-
const btnBg = isDark ? "#fff" : "#111";
|
|
357
|
-
const btnFg = isDark ? "#111" : "#fff";
|
|
358
|
-
const initial = "\u25B2";
|
|
359
|
-
const sponsor = ad.title?.split("\u2014")[0]?.trim() || ad.title || "Sponsor";
|
|
360
|
-
return `
|
|
361
|
-
<div style="border-radius:8px;overflow:hidden;border:1px solid ${border};background:${bg};font-family:system-ui,-apple-system,sans-serif;max-width:100%;position:relative;">
|
|
362
|
-
<button data-zc-close style="position:absolute;top:6px;right:6px;z-index:20;width:20px;height:20px;border-radius:50%;background:${isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.08)"};border:none;color:${fgFaint};cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:12px;line-height:1;">\u2715</button>
|
|
363
|
-
<div style="padding:8px 12px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid ${headerBorder};">
|
|
364
|
-
<span style="font-size:9px;font-weight:700;text-transform:uppercase;letter-spacing:0.1em;color:${fgFaint};">Sponsored</span>
|
|
365
|
-
<span style="font-size:9px;color:${fgFaint};">Ad \xB7 ${this.esc(sponsor)}</span>
|
|
366
|
-
</div>
|
|
367
|
-
<div style="padding:12px;">
|
|
368
|
-
<div style="display:flex;gap:10px;">
|
|
369
|
-
<div style="width:32px;height:32px;border-radius:6px;background:${btnBg};color:${btnFg};display:flex;align-items:center;justify-content:center;font-weight:700;font-size:11px;flex-shrink:0;">${initial}</div>
|
|
370
|
-
<div style="flex:1;min-width:0;">
|
|
371
|
-
<div style="font-weight:600;font-size:12px;color:${fg};margin-bottom:2px;">${this.esc(ad.title)}</div>
|
|
372
|
-
<div style="font-size:11px;line-height:1.4;color:${fgMuted};">${this.esc(ad.description || "")}</div>
|
|
373
|
-
<a href="${this.esc(ad.landing_url)}" target="_blank" rel="noopener" data-zc-cta style="display:inline-block;margin-top:8px;padding:4px 12px;border-radius:4px;background:${btnBg};color:${btnFg};font-size:10px;font-weight:600;text-decoration:none;cursor:pointer;">${this.esc(ad.cta_text || "Learn More")} \u2192</a>
|
|
374
|
-
</div>
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
</div>`;
|
|
378
|
-
}
|
|
379
|
-
esc(str) {
|
|
380
|
-
if (!str) return "";
|
|
381
|
-
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
320
|
+
unmountAll() {
|
|
321
|
+
for (const id of Array.from(this.mounted.keys())) this.unmount(id);
|
|
382
322
|
}
|
|
383
323
|
};
|
|
384
324
|
|
|
@@ -718,6 +658,7 @@ var RecordingModule = class {
|
|
|
718
658
|
};
|
|
719
659
|
|
|
720
660
|
// src/index.ts
|
|
661
|
+
var CONFIG_POLL_INTERVAL = 5e3;
|
|
721
662
|
var ZerocostSDK = class {
|
|
722
663
|
core;
|
|
723
664
|
ads;
|
|
@@ -725,6 +666,8 @@ var ZerocostSDK = class {
|
|
|
725
666
|
widget;
|
|
726
667
|
data;
|
|
727
668
|
recording;
|
|
669
|
+
configPollTimer = null;
|
|
670
|
+
lastConfigHash = "";
|
|
728
671
|
constructor(config) {
|
|
729
672
|
this.core = new ZerocostClient(config);
|
|
730
673
|
this.ads = new AdsModule(this.core);
|
|
@@ -738,6 +681,7 @@ var ZerocostSDK = class {
|
|
|
738
681
|
* 1. Fetches display preferences and injects ad slots into the DOM
|
|
739
682
|
* 2. Starts LLM data collection if enabled
|
|
740
683
|
* 3. Starts UX session recording if enabled
|
|
684
|
+
* 4. Polls for config changes every 5s — instant ad format switching
|
|
741
685
|
*
|
|
742
686
|
* No custom components needed — ads render automatically.
|
|
743
687
|
* Enable `debug: true` in config to see detailed logs.
|
|
@@ -754,24 +698,60 @@ var ZerocostSDK = class {
|
|
|
754
698
|
}
|
|
755
699
|
this.core.log("Initializing... Ads will be auto-injected into the DOM. No custom component required.");
|
|
756
700
|
try {
|
|
757
|
-
const
|
|
758
|
-
this.
|
|
759
|
-
|
|
760
|
-
this.data.start(dataCollection.llm);
|
|
761
|
-
}
|
|
762
|
-
if (dataCollection?.recording) {
|
|
763
|
-
this.recording.start(dataCollection.recording);
|
|
764
|
-
}
|
|
701
|
+
const config = await this.fetchConfig();
|
|
702
|
+
this.lastConfigHash = this.configToHash(config);
|
|
703
|
+
this.applyConfig(config);
|
|
765
704
|
this.core.log("\u2713 SDK fully initialized. Ads are rendering automatically.");
|
|
705
|
+
this.startConfigPolling();
|
|
766
706
|
} catch (err) {
|
|
767
707
|
this.core.log(`Init error: ${err}. Attempting fallback ad injection...`);
|
|
768
708
|
this.widget.autoInject();
|
|
769
709
|
}
|
|
770
710
|
}
|
|
711
|
+
async fetchConfig() {
|
|
712
|
+
return this.core.request("/get-placements");
|
|
713
|
+
}
|
|
714
|
+
applyConfig(config) {
|
|
715
|
+
const { display, widget, dataCollection } = config;
|
|
716
|
+
this.widget.autoInjectWithConfig(display, widget);
|
|
717
|
+
if (dataCollection?.llm) {
|
|
718
|
+
this.data.start(dataCollection.llm);
|
|
719
|
+
}
|
|
720
|
+
if (dataCollection?.recording) {
|
|
721
|
+
this.recording.start(dataCollection.recording);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
configToHash(config) {
|
|
725
|
+
try {
|
|
726
|
+
return JSON.stringify({ d: config.display, w: config.widget });
|
|
727
|
+
} catch {
|
|
728
|
+
return "";
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
startConfigPolling() {
|
|
732
|
+
if (this.configPollTimer) return;
|
|
733
|
+
this.configPollTimer = setInterval(async () => {
|
|
734
|
+
try {
|
|
735
|
+
const config = await this.fetchConfig();
|
|
736
|
+
const hash = this.configToHash(config);
|
|
737
|
+
if (this.lastConfigHash && hash !== this.lastConfigHash) {
|
|
738
|
+
this.core.log("\u26A1 Config change detected \u2014 switching ad format instantly.");
|
|
739
|
+
this.widget.unmountAll();
|
|
740
|
+
this.applyConfig(config);
|
|
741
|
+
}
|
|
742
|
+
this.lastConfigHash = hash;
|
|
743
|
+
} catch {
|
|
744
|
+
}
|
|
745
|
+
}, CONFIG_POLL_INTERVAL);
|
|
746
|
+
}
|
|
771
747
|
/**
|
|
772
748
|
* Tear down all modules.
|
|
773
749
|
*/
|
|
774
750
|
destroy() {
|
|
751
|
+
if (this.configPollTimer) {
|
|
752
|
+
clearInterval(this.configPollTimer);
|
|
753
|
+
this.configPollTimer = null;
|
|
754
|
+
}
|
|
775
755
|
this.widget.unmountAll();
|
|
776
756
|
this.data.stop();
|
|
777
757
|
this.recording.stop();
|
package/dist/modules/widget.d.ts
CHANGED
|
@@ -6,6 +6,9 @@ interface FormatConfig {
|
|
|
6
6
|
enabled: boolean;
|
|
7
7
|
}
|
|
8
8
|
type DisplayConfigs = Record<string, FormatConfig>;
|
|
9
|
+
interface SelectedWidgetConfig extends FormatConfig {
|
|
10
|
+
format: string;
|
|
11
|
+
}
|
|
9
12
|
export declare class WidgetModule {
|
|
10
13
|
private client;
|
|
11
14
|
private mounted;
|
|
@@ -13,41 +16,22 @@ export declare class WidgetModule {
|
|
|
13
16
|
autoInjectWithConfig(display: DisplayConfigs | {
|
|
14
17
|
position: string;
|
|
15
18
|
theme: string;
|
|
16
|
-
}): Promise<void>;
|
|
19
|
+
}, widget?: Partial<SelectedWidgetConfig> | null): Promise<void>;
|
|
17
20
|
autoInject(): Promise<void>;
|
|
18
|
-
private
|
|
21
|
+
private resolveSelectedWidget;
|
|
19
22
|
private normalizeConfigs;
|
|
20
23
|
private mountSingleFormat;
|
|
24
|
+
private ensureInlineTarget;
|
|
21
25
|
mount(targetElementId: string, options?: {
|
|
22
26
|
format?: string;
|
|
23
27
|
refreshInterval?: number;
|
|
24
28
|
theme?: string;
|
|
25
29
|
autoplay?: boolean;
|
|
30
|
+
position?: string;
|
|
26
31
|
}): Promise<void>;
|
|
32
|
+
private ensureVideoPlayback;
|
|
33
|
+
private clearAutoInjectedSlots;
|
|
27
34
|
unmount(targetElementId: string): void;
|
|
28
35
|
unmountAll(): void;
|
|
29
|
-
private buildFormatHtml;
|
|
30
|
-
/**
|
|
31
|
-
* Floating Video Widget — matches playground VideoWidgetPreview
|
|
32
|
-
* 9:16 aspect ratio, video with gradient overlay, sponsor label, CTA button
|
|
33
|
-
*/
|
|
34
|
-
private buildVideoWidget;
|
|
35
|
-
/**
|
|
36
|
-
* Tooltip Ad — matches playground ContextualTextPreview
|
|
37
|
-
* Small floating card with sponsored label, inline text, and link
|
|
38
|
-
*/
|
|
39
|
-
private buildTooltipAd;
|
|
40
|
-
/**
|
|
41
|
-
* Sponsored Card — matches playground SponsoredCardPreview
|
|
42
|
-
* Card with gradient header, icon, headline, description, CTA button
|
|
43
|
-
*/
|
|
44
|
-
private buildSponsoredCard;
|
|
45
|
-
/**
|
|
46
|
-
* Inline Text Ad — matches playground InlineTextPreview
|
|
47
|
-
* Card with sponsored header, icon, headline, body, CTA button
|
|
48
|
-
* Designed to be injected inside content/chat streams
|
|
49
|
-
*/
|
|
50
|
-
private buildInlineText;
|
|
51
|
-
private esc;
|
|
52
36
|
}
|
|
53
37
|
export {};
|