@active-reach/web-sdk 1.19.0 → 1.21.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.js CHANGED
@@ -2095,7 +2095,7 @@ function renderGame(ctx, subType) {
2095
2095
  fn(ctx);
2096
2096
  return true;
2097
2097
  }
2098
- const ASPECT = { "16:9": "16 / 9", "9:16": "9 / 16", "1:1": "1 / 1", "4:5": "4 / 5" };
2098
+ const ASPECT$1 = { "16:9": "16 / 9", "9:16": "9 / 16", "1:1": "1 / 1", "4:5": "4 / 5" };
2099
2099
  function buildVideo(ctx, aspect, autoplay, loop) {
2100
2100
  var _a;
2101
2101
  const { campaign, sanitizeUrl } = ctx;
@@ -2103,7 +2103,7 @@ function buildVideo(ctx, aspect, autoplay, loop) {
2103
2103
  const videoUrl = sanitizeUrl(ic.video_url || campaign.video_url || "");
2104
2104
  const poster = campaign.image_url ? sanitizeUrl(campaign.image_url) : null;
2105
2105
  const wrap = document.createElement("div");
2106
- wrap.style.cssText = `position:relative;width:100%;aspect-ratio:${ASPECT[aspect] || ASPECT["16:9"]};background:#0b1220;border-radius:14px;overflow:hidden;`;
2106
+ wrap.style.cssText = `position:relative;width:100%;aspect-ratio:${ASPECT$1[aspect] || ASPECT$1["16:9"]};background:#0b1220;border-radius:14px;overflow:hidden;`;
2107
2107
  if (videoUrl) {
2108
2108
  const vid = document.createElement("video");
2109
2109
  vid.src = videoUrl;
@@ -2272,11 +2272,179 @@ function renderVideoInline(ctx, target) {
2272
2272
  target.appendChild(banner);
2273
2273
  return true;
2274
2274
  }
2275
+ const HEX = /^#[0-9a-fA-F]{3,8}$/;
2276
+ function safeColor(c, fallback) {
2277
+ return c && HEX.test(c) ? c : fallback;
2278
+ }
2279
+ const RADIUS = { none: "0", lg: "12px", xl: "16px", "2xl": "20px" };
2280
+ const ASPECT = { "16:9": "16 / 9", "4:3": "4 / 3", "21:9": "21 / 9" };
2281
+ function cardHeadline(c) {
2282
+ return c.headline ?? c.title ?? "";
2283
+ }
2284
+ function cardSub(c) {
2285
+ return c.subhead ?? c.body ?? "";
2286
+ }
2287
+ function cardCta(c) {
2288
+ return c.cta_label ?? c.cta_text ?? "";
2289
+ }
2290
+ function cardUrl(c) {
2291
+ if (c.cta_url) return c.cta_url;
2292
+ const t = c.cta_target;
2293
+ if (t && t.value) return t.type === "category" ? `?category=${encodeURIComponent(t.value)}` : t.value;
2294
+ return "";
2295
+ }
2296
+ function renderHeroInline(ctx, target) {
2297
+ const { campaign, sanitizeUrl, trackEvent } = ctx;
2298
+ const ic = campaign.interactive_config || {};
2299
+ const cards = Array.isArray(ic.cards) ? ic.cards : [];
2300
+ if (cards.length === 0) return false;
2301
+ const chrome = ic.chrome || {};
2302
+ const accent = safeColor(
2303
+ chrome.accent && chrome.accent !== "brand" ? chrome.accent : campaign.background_color,
2304
+ "#4169e1"
2305
+ );
2306
+ const radius = RADIUS[chrome.radius ?? "2xl"] ?? RADIUS["2xl"];
2307
+ const variant = chrome.variant ?? "hero_fullbleed";
2308
+ if (variant === "announcement_bar") {
2309
+ const c = cards[0];
2310
+ const bar = document.createElement("div");
2311
+ bar.style.cssText = `display:flex;align-items:center;justify-content:center;gap:10px;background:${accent};color:#fff;padding:8px 14px;font:600 13px/1.3 Inter,system-ui,sans-serif;text-align:center;`;
2312
+ bar.textContent = [cardHeadline(c), cardSub(c)].filter(Boolean).join(" · ");
2313
+ const url = sanitizeUrl(cardUrl(c));
2314
+ if (url && cardCta(c)) {
2315
+ const a = document.createElement("span");
2316
+ a.style.cssText = "text-decoration:underline;cursor:pointer;white-space:nowrap;";
2317
+ a.textContent = cardCta(c);
2318
+ a.addEventListener("click", () => {
2319
+ var _a;
2320
+ trackEvent(campaign.id, "clicked");
2321
+ (_a = ctx.navigate) == null ? void 0 : _a.call(ctx, url);
2322
+ });
2323
+ bar.appendChild(a);
2324
+ }
2325
+ target.appendChild(bar);
2326
+ trackEvent(campaign.id, "impression");
2327
+ return true;
2328
+ }
2329
+ const root = document.createElement("div");
2330
+ root.style.cssText = `position:relative;width:100%;overflow:hidden;border-radius:${radius};box-shadow:0 1px 2px rgba(16,24,40,0.06);${chrome.aspect_ratio && ASPECT[chrome.aspect_ratio] ? `aspect-ratio:${ASPECT[chrome.aspect_ratio]};` : "min-height:300px;"}`;
2331
+ root.setAttribute("data-campaign-id", campaign.id);
2332
+ const track = document.createElement("div");
2333
+ track.style.cssText = "display:flex;height:100%;width:100%;transition:transform 0.5s ease;";
2334
+ root.appendChild(track);
2335
+ const overlayCss = chrome.overlay === "none" ? "" : chrome.overlay === "full-scrim" ? "background:linear-gradient(0deg,rgba(0,0,0,0.55),rgba(0,0,0,0.25));" : "background:linear-gradient(to top right,rgba(0,0,0,0.7),rgba(0,0,0,0.25) 45%,transparent);";
2336
+ const justify = chrome.text_position === "center" ? "center" : "flex-end";
2337
+ const align = chrome.text_position === "bottom-left" || !chrome.text_position ? "flex-start" : "center";
2338
+ cards.forEach((c, i) => {
2339
+ const slide = document.createElement("div");
2340
+ slide.style.cssText = "position:relative;flex:0 0 100%;width:100%;height:100%;min-height:inherit;background:#f1f5f9;";
2341
+ const videoUrl = sanitizeUrl(c.video_url ?? "");
2342
+ const isVideo = !!videoUrl || c.media_type === "video";
2343
+ const mediaUrl = isVideo ? videoUrl || sanitizeUrl(c.media_url ?? "") : sanitizeUrl(c.image_url ?? c.media_url ?? "");
2344
+ if (mediaUrl) {
2345
+ if (isVideo) {
2346
+ const v = document.createElement("video");
2347
+ v.src = mediaUrl;
2348
+ v.autoplay = true;
2349
+ v.muted = true;
2350
+ v.loop = true;
2351
+ v.playsInline = true;
2352
+ v.style.cssText = "position:absolute;inset:0;width:100%;height:100%;object-fit:cover;";
2353
+ slide.appendChild(v);
2354
+ } else {
2355
+ const img = document.createElement("div");
2356
+ img.style.cssText = `position:absolute;inset:0;background:url("${mediaUrl}") center/cover no-repeat;`;
2357
+ slide.appendChild(img);
2358
+ }
2359
+ }
2360
+ if (overlayCss) {
2361
+ const scrim = document.createElement("div");
2362
+ scrim.style.cssText = `position:absolute;inset:0;${overlayCss}`;
2363
+ slide.appendChild(scrim);
2364
+ }
2365
+ const content = document.createElement("div");
2366
+ content.style.cssText = `position:absolute;inset:0;display:flex;flex-direction:column;justify-content:${justify};align-items:${align};gap:6px;padding:24px;text-align:${align === "center" ? "center" : "left"};`;
2367
+ const h = cardHeadline(c);
2368
+ if (h) {
2369
+ const head = document.createElement("h2");
2370
+ head.textContent = h;
2371
+ head.style.cssText = "margin:0;color:#fff;font:800 24px/1.2 Inter Tight,Inter,system-ui,sans-serif;letter-spacing:-0.02em;text-shadow:0 1px 8px rgba(0,0,0,0.35);max-width:90%;";
2372
+ content.appendChild(head);
2373
+ }
2374
+ const s = cardSub(c);
2375
+ if (s) {
2376
+ const sub = document.createElement("p");
2377
+ sub.textContent = s;
2378
+ sub.style.cssText = "margin:0;color:rgba(255,255,255,0.92);font:500 14px/1.4 Inter,system-ui,sans-serif;text-shadow:0 1px 6px rgba(0,0,0,0.3);max-width:90%;";
2379
+ content.appendChild(sub);
2380
+ }
2381
+ const cta = cardCta(c);
2382
+ const url = sanitizeUrl(cardUrl(c));
2383
+ if (cta && chrome.cta_style !== "none") {
2384
+ const btn = document.createElement("button");
2385
+ btn.textContent = cta;
2386
+ btn.style.cssText = chrome.cta_style === "underline" ? `margin-top:6px;background:none;border:none;color:#fff;font:700 14px Inter,system-ui,sans-serif;text-decoration:underline;cursor:pointer;padding:0;` : `margin-top:8px;background:${accent};color:#fff;border:none;border-radius:999px;padding:9px 18px;font:700 13px Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 4px 12px rgba(0,0,0,0.2);`;
2387
+ btn.addEventListener("click", () => {
2388
+ var _a;
2389
+ trackEvent(campaign.id, "clicked", { stepId: `card_${i}` });
2390
+ if (url) (_a = ctx.navigate) == null ? void 0 : _a.call(ctx, url);
2391
+ });
2392
+ content.appendChild(btn);
2393
+ }
2394
+ slide.appendChild(content);
2395
+ track.appendChild(slide);
2396
+ });
2397
+ const dotEls = [];
2398
+ let activeIdx = 0;
2399
+ const goto = (idx) => {
2400
+ activeIdx = (idx % cards.length + cards.length) % cards.length;
2401
+ track.style.transform = `translateX(-${activeIdx * 100}%)`;
2402
+ dotEls.forEach((d, i) => {
2403
+ d.style.width = i === activeIdx ? "18px" : "6px";
2404
+ d.style.background = i === activeIdx ? accent : "rgba(255,255,255,0.7)";
2405
+ });
2406
+ };
2407
+ const dots = document.createElement("div");
2408
+ dots.style.cssText = "position:absolute;bottom:8px;left:50%;transform:translateX(-50%);display:flex;gap:6px;z-index:2;";
2409
+ if (cards.length > 1) {
2410
+ cards.forEach((_, i) => {
2411
+ const dot = document.createElement("span");
2412
+ dot.style.cssText = "width:6px;height:6px;border-radius:999px;background:rgba(255,255,255,0.7);transition:all 0.25s;cursor:pointer;";
2413
+ dot.addEventListener("click", () => goto(i));
2414
+ dotEls.push(dot);
2415
+ dots.appendChild(dot);
2416
+ });
2417
+ root.appendChild(dots);
2418
+ }
2419
+ target.appendChild(root);
2420
+ goto(0);
2421
+ if (cards.length > 1 && chrome.loop !== false) {
2422
+ const ms = typeof chrome.autoplay_ms === "number" && chrome.autoplay_ms >= 1500 ? chrome.autoplay_ms : 4e3;
2423
+ let timer = window.setInterval(() => goto(activeIdx + 1), ms);
2424
+ root.addEventListener("mouseenter", () => window.clearInterval(timer));
2425
+ root.addEventListener("mouseleave", () => {
2426
+ timer = window.setInterval(() => goto(activeIdx + 1), ms);
2427
+ });
2428
+ }
2429
+ trackEvent(campaign.id, "impression");
2430
+ return true;
2431
+ }
2275
2432
  const STYLE_ID = "aegis-chat-styles";
2276
2433
  const POLL_INTERVAL_MS = 5e3;
2434
+ let currentLauncher = null;
2435
+ function getCurrentLauncher() {
2436
+ return currentLauncher;
2437
+ }
2438
+ function openChat(prefill) {
2439
+ currentLauncher == null ? void 0 : currentLauncher.openPanel(prefill);
2440
+ }
2441
+ function closeChat() {
2442
+ currentLauncher == null ? void 0 : currentLauncher.closePanel();
2443
+ }
2277
2444
  const ATTENTION_DELAY_MS = 4500;
2278
2445
  class AegisChat {
2279
2446
  constructor(config) {
2447
+ this.sessionResumed = false;
2280
2448
  this.open = false;
2281
2449
  this.initialized = false;
2282
2450
  this.unread = 0;
@@ -2294,12 +2462,14 @@ class AegisChat {
2294
2462
  this.icon = config.icon ?? "sparkle";
2295
2463
  this.logoUrl = config.logoUrl;
2296
2464
  this.position = config.position ?? "bottom-right";
2465
+ this.displayMode = config.displayMode ?? "bubble";
2297
2466
  this.agentPersona = config.agentPersona;
2298
2467
  this.quickReplies = config.quickReplies;
2299
2468
  this.onSessionStart = config.onSessionStart;
2300
2469
  this.onSessionEnd = config.onSessionEnd;
2301
2470
  this.onMessageSent = config.onMessageSent;
2302
2471
  this.anonymousId = this.readAnonId();
2472
+ this.contactId = this.contactId ?? this.readStoredContactId();
2303
2473
  }
2304
2474
  // ── Lifecycle ────────────────────────────────────────────────────────────
2305
2475
  initialize() {
@@ -2307,7 +2477,8 @@ class AegisChat {
2307
2477
  this.initialized = true;
2308
2478
  this.injectStyles();
2309
2479
  this.mount();
2310
- this.scheduleAttention();
2480
+ currentLauncher = this;
2481
+ if (this.displayMode !== "nav") this.scheduleAttention();
2311
2482
  }
2312
2483
  /** Called by the runtime when the visitor identifies. */
2313
2484
  updateContactId(contactId) {
@@ -2330,7 +2501,7 @@ class AegisChat {
2330
2501
  (_c = this.bubble) == null ? void 0 : _c.classList.add("aegis-chat-bubble--hidden");
2331
2502
  this.clearUnread();
2332
2503
  if (prefill && this.input) this.input.value = prefill;
2333
- void this.refreshHistory();
2504
+ void this.resumeSession();
2334
2505
  this.startPolling();
2335
2506
  (_d = this.input) == null ? void 0 : _d.focus();
2336
2507
  }
@@ -2355,13 +2526,22 @@ class AegisChat {
2355
2526
  this.stopPolling();
2356
2527
  (_a = this.root) == null ? void 0 : _a.remove();
2357
2528
  this.initialized = false;
2529
+ if (currentLauncher === this) currentLauncher = null;
2358
2530
  }
2359
2531
  // ── Send ─────────────────────────────────────────────────────────────────
2360
- async send(text) {
2532
+ async send(text, media) {
2361
2533
  var _a;
2362
- const message = text.trim();
2363
- if (!message) return;
2364
- this.appendBubble({ id: `local_${Date.now()}`, sender: "customer", role: "user", content: message });
2534
+ const caption2 = text.trim();
2535
+ if (!caption2 && !media) return;
2536
+ const message = caption2 || (media ? media.type === "image" ? "Sent a photo 📷" : "Sent a file 📎" : "");
2537
+ this.appendBubble({
2538
+ id: `local_${Date.now()}`,
2539
+ sender: "customer",
2540
+ role: "user",
2541
+ content: message,
2542
+ media_url: (media == null ? void 0 : media.url) ?? null,
2543
+ media_type: (media == null ? void 0 : media.type) ?? null
2544
+ });
2365
2545
  this.pendingEcho.push(message);
2366
2546
  if (this.input) {
2367
2547
  this.input.value = "";
@@ -2377,6 +2557,8 @@ class AegisChat {
2377
2557
  channel: this.channel,
2378
2558
  anonymous_id: this.anonymousId,
2379
2559
  contact_id: this.contactId,
2560
+ media_url: media == null ? void 0 : media.url,
2561
+ media_type: media == null ? void 0 : media.type,
2380
2562
  // Active Web Chat — binds the campaign-configured persona. Optional;
2381
2563
  // the backend falls back to the tenant default if unknown/absent
2382
2564
  // (honored server-side in Phase E). JSON.stringify drops it if absent.
@@ -2395,6 +2577,7 @@ class AegisChat {
2395
2577
  } catch {
2396
2578
  }
2397
2579
  this.contactId = data.contact_id;
2580
+ this.persistContactId(data.contact_id);
2398
2581
  this.sseTicket = data.sse_ticket;
2399
2582
  this.connectSSE(data.sse_ticket);
2400
2583
  void this.refreshHistory();
@@ -2403,6 +2586,51 @@ class AegisChat {
2403
2586
  this.appendSystem("Sorry — your message could not be delivered. Please try again.");
2404
2587
  }
2405
2588
  }
2589
+ // ── Media attachments ─────────────────────────────────────────────────────
2590
+ /** Validate + upload a picked file, then send it as a media message. */
2591
+ async handleFile(file) {
2592
+ var _a;
2593
+ const allowed = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf"]);
2594
+ if (!allowed.has(file.type)) {
2595
+ this.appendSystem("That file type isn’t supported (images or PDF only).");
2596
+ return;
2597
+ }
2598
+ if (file.size > 8 * 1024 * 1024) {
2599
+ this.appendSystem("That file is too large (max 8 MB).");
2600
+ return;
2601
+ }
2602
+ try {
2603
+ const data = await this.fileToBase64(file);
2604
+ const res = await fetch(`${this.apiHost}/v1/chat/upload`, {
2605
+ method: "POST",
2606
+ headers: { "Content-Type": "application/json", "X-Aegis-Write-Key": this.writeKey },
2607
+ body: JSON.stringify({ data, content_type: file.type, filename: file.name })
2608
+ });
2609
+ if (!res.ok) {
2610
+ this.log("upload failed", res.status);
2611
+ this.appendSystem("Sorry — that attachment couldn’t be uploaded.");
2612
+ return;
2613
+ }
2614
+ const out = await res.json();
2615
+ await this.send(((_a = this.input) == null ? void 0 : _a.value) ?? "", { url: out.media_url, type: out.media_type });
2616
+ } catch (err) {
2617
+ this.log("upload error", err);
2618
+ this.appendSystem("Sorry — that attachment couldn’t be uploaded.");
2619
+ }
2620
+ }
2621
+ /** Read a File as bare base64 (strip the `data:…;base64,` prefix). */
2622
+ fileToBase64(file) {
2623
+ return new Promise((resolve, reject) => {
2624
+ const reader = new FileReader();
2625
+ reader.onload = () => {
2626
+ const result = String(reader.result || "");
2627
+ const comma = result.indexOf(",");
2628
+ resolve(comma >= 0 ? result.slice(comma + 1) : result);
2629
+ };
2630
+ reader.onerror = () => reject(reader.error);
2631
+ reader.readAsDataURL(file);
2632
+ });
2633
+ }
2406
2634
  // ── Reply transport: SSE nudge + history refetch (poll fallback) ──────────
2407
2635
  connectSSE(ticket) {
2408
2636
  if (!this.open) return;
@@ -2540,6 +2768,21 @@ class AegisChat {
2540
2768
  void this.send(input.value);
2541
2769
  }
2542
2770
  });
2771
+ const fileInput = document.createElement("input");
2772
+ fileInput.type = "file";
2773
+ fileInput.accept = "image/*,application/pdf";
2774
+ fileInput.className = "aegis-chat-fileinput";
2775
+ fileInput.addEventListener("change", () => {
2776
+ const f = fileInput.files && fileInput.files[0];
2777
+ if (f) void this.handleFile(f);
2778
+ fileInput.value = "";
2779
+ });
2780
+ const attachBtn = document.createElement("button");
2781
+ attachBtn.type = "button";
2782
+ attachBtn.className = "aegis-chat-attach";
2783
+ attachBtn.setAttribute("aria-label", "Attach a file");
2784
+ attachBtn.innerHTML = '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>';
2785
+ attachBtn.addEventListener("click", () => fileInput.click());
2543
2786
  const sendBtn = document.createElement("button");
2544
2787
  sendBtn.type = "submit";
2545
2788
  sendBtn.className = "aegis-chat-send";
@@ -2549,8 +2792,10 @@ class AegisChat {
2549
2792
  e.preventDefault();
2550
2793
  void this.send(input.value);
2551
2794
  });
2795
+ composer.appendChild(attachBtn);
2552
2796
  composer.appendChild(input);
2553
2797
  composer.appendChild(sendBtn);
2798
+ composer.appendChild(fileInput);
2554
2799
  panel.appendChild(header);
2555
2800
  panel.appendChild(thread);
2556
2801
  panel.appendChild(composer);
@@ -2570,8 +2815,10 @@ class AegisChat {
2570
2815
  tip.appendChild(tipClose);
2571
2816
  tip.addEventListener("click", () => this.openPanel());
2572
2817
  root.appendChild(panel);
2573
- root.appendChild(tip);
2574
- root.appendChild(bubble);
2818
+ if (this.displayMode !== "nav") {
2819
+ root.appendChild(tip);
2820
+ root.appendChild(bubble);
2821
+ }
2575
2822
  document.body.appendChild(root);
2576
2823
  this.greetingTip = tip;
2577
2824
  this.root = root;
@@ -2609,6 +2856,26 @@ class AegisChat {
2609
2856
  if (msg.id) this.renderedIds.add(msg.id);
2610
2857
  const row = document.createElement("div");
2611
2858
  row.className = `aegis-chat-msg aegis-chat-msg--${msg.sender}`;
2859
+ if (msg.media_url) {
2860
+ if (msg.media_type === "image") {
2861
+ const img = document.createElement("img");
2862
+ img.className = "aegis-chat-media-img";
2863
+ img.src = msg.media_url;
2864
+ img.alt = "attachment";
2865
+ img.loading = "lazy";
2866
+ const url = msg.media_url;
2867
+ img.addEventListener("click", () => window.open(url, "_blank", "noopener"));
2868
+ row.appendChild(img);
2869
+ } else {
2870
+ const a = document.createElement("a");
2871
+ a.className = "aegis-chat-media-file";
2872
+ a.href = msg.media_url;
2873
+ a.target = "_blank";
2874
+ a.rel = "noopener noreferrer";
2875
+ a.textContent = "📎 Attachment";
2876
+ row.appendChild(a);
2877
+ }
2878
+ }
2612
2879
  if (msg.content) {
2613
2880
  const bubble = document.createElement("div");
2614
2881
  bubble.className = "aegis-chat-bubbletext";
@@ -2700,6 +2967,54 @@ class AegisChat {
2700
2967
  return void 0;
2701
2968
  }
2702
2969
  }
2970
+ readStoredContactId() {
2971
+ if (typeof document === "undefined") return void 0;
2972
+ try {
2973
+ return new Storage().get("aegis_chat_cid") ?? void 0;
2974
+ } catch {
2975
+ return void 0;
2976
+ }
2977
+ }
2978
+ persistContactId(id) {
2979
+ if (!id || typeof document === "undefined") return;
2980
+ try {
2981
+ new Storage().set("aegis_chat_cid", id, 365);
2982
+ } catch {
2983
+ }
2984
+ }
2985
+ /** Restore a returning visitor's thread on open. Resolves an existing contact
2986
+ * + mints a per-session ticket WITHOUT sending a message (history is ticket-
2987
+ * gated). No-ops for brand-new visitors and once a ticket is already held. */
2988
+ async resumeSession() {
2989
+ if (this.sseTicket) {
2990
+ void this.refreshHistory();
2991
+ return;
2992
+ }
2993
+ if (this.sessionResumed) return;
2994
+ this.sessionResumed = true;
2995
+ if (!this.contactId && !this.anonymousId) return;
2996
+ try {
2997
+ const res = await fetch(`${this.apiHost}/v1/chat/session`, {
2998
+ method: "POST",
2999
+ headers: { "Content-Type": "application/json", "X-Aegis-Write-Key": this.writeKey },
3000
+ body: JSON.stringify({
3001
+ anonymous_id: this.anonymousId,
3002
+ contact_id: this.contactId,
3003
+ channel: this.channel
3004
+ })
3005
+ });
3006
+ if (!res.ok) return;
3007
+ const data = await res.json();
3008
+ if (!data.contact_id || !data.sse_ticket) return;
3009
+ this.contactId = data.contact_id;
3010
+ this.sseTicket = data.sse_ticket;
3011
+ this.persistContactId(data.contact_id);
3012
+ await this.refreshHistory();
3013
+ if (this.open) this.connectSSE(data.sse_ticket);
3014
+ } catch (err) {
3015
+ this.log("resume session failed", err);
3016
+ }
3017
+ }
2703
3018
  // ── Typing indicator ───────────────────────────────────────────────────────
2704
3019
  showTyping() {
2705
3020
  if (!this.thread || this.typingEl) return;
@@ -2800,11 +3115,19 @@ const CHAT_CSS = `
2800
3115
  .aegis-chat-input{flex:1;resize:none;border:1px solid #e2e8f0;border-radius:12px;padding:9px 12px;font-size:14px;font-family:inherit;max-height:96px;outline:none}
2801
3116
  .aegis-chat-input:focus{border-color:var(--aegis-chat-accent,#4169e1)}
2802
3117
  .aegis-chat-send{width:40px;height:40px;border-radius:9999px;border:none;background:var(--aegis-chat-accent,#4169e1);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto}
3118
+ .aegis-chat-attach{width:38px;height:38px;border-radius:9999px;border:none;background:transparent;color:#64748b;display:flex;align-items:center;justify-content:center;cursor:pointer;flex:0 0 auto;transition:background .15s ease,color .15s ease}
3119
+ .aegis-chat-attach:hover{background:#f1f5f9;color:var(--aegis-chat-accent,#4169e1)}
3120
+ .aegis-chat-fileinput{display:none}
3121
+ .aegis-chat-media-img{max-width:200px;max-height:200px;border-radius:12px;object-fit:cover;cursor:pointer;display:block}
3122
+ .aegis-chat-media-file{display:inline-flex;align-items:center;gap:6px;padding:8px 12px;border-radius:12px;background:#fff;border:1px solid #e2e8f0;font-size:13px;color:#0f172a;text-decoration:none;max-width:220px}
3123
+ .aegis-chat-msg--customer .aegis-chat-media-file{background:rgba(255,255,255,.18);border-color:transparent;color:#fff}
2803
3124
  /* Mobile: raise the bubble above the storefront's full-width cart bar so they
2804
3125
  never overlap; widen the panel and reduce the tooltip width. */
2805
- @media (max-width:480px){
3126
+ @media (max-width:767px){
2806
3127
  .aegis-chat-root{bottom:84px;right:16px}
2807
- .aegis-chat-panel{width:calc(100vw - 24px);height:calc(100vh - 140px)}
3128
+ /* Full-page chat on mobile — the open panel covers the whole viewport (incl.
3129
+ the bottom nav) like a native chat screen, instead of a floating card. */
3130
+ .aegis-chat-panel{position:fixed;inset:0;left:0;right:0;width:100vw;height:100vh;height:100dvh;max-width:none;max-height:none;border-radius:0}
2808
3131
  .aegis-chat-tip{max-width:200px}
2809
3132
  }
2810
3133
  `;
@@ -3376,6 +3699,10 @@ function renderActiveWebChat(ctx, creds) {
3376
3699
  icon: icon === "sparkle" || icon === "chat" || icon === "logo" ? icon : void 0,
3377
3700
  logoUrl: logo ? ctx.sanitizeUrl(logo) ?? void 0 : void 0,
3378
3701
  position: ic.chat_position === "bottom-left" ? "bottom-left" : void 0,
3702
+ // 'nav' = headless (no bubble); the host opens it via aegis.chat.open() or a
3703
+ // client_trigger. Defaults to 'bubble'. Per-device override via device_type
3704
+ // targeting on the campaign (e.g. a mobile campaign with chat_display_mode='nav').
3705
+ displayMode: ic.chat_display_mode === "nav" ? "nav" : void 0,
3379
3706
  agentPersona: str(ic.chat_agent_persona),
3380
3707
  quickReplies: Array.isArray(ic.chat_quick_replies) ? ic.chat_quick_replies.filter((q) => typeof q === "string") : void 0,
3381
3708
  // Bridge the engine's session lifecycle to campaign analytics. Per the
@@ -3436,6 +3763,7 @@ const _AegisInAppManager = class _AegisInAppManager {
3436
3763
  constructor(config) {
3437
3764
  this.campaigns = [];
3438
3765
  this.displayedCampaigns = /* @__PURE__ */ new Set();
3766
+ this._displaySizeMul = 1;
3439
3767
  this.suppressedUntil = /* @__PURE__ */ new Map();
3440
3768
  this.isInitialized = false;
3441
3769
  this.reconnectAttempts = 0;
@@ -4015,7 +4343,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4015
4343
  const list = eligibleByCategory.get(key);
4016
4344
  if (!list || list.length === 0) return;
4017
4345
  slot.querySelectorAll(":scope > [data-aegis-slot-default]").forEach((d) => d.remove());
4018
- const rotateMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
4346
+ const attrMs = parseInt(slot.getAttribute("data-aegis-slot-rotate") || "0", 10);
4347
+ const declaredMs = list.reduce((m2, c) => {
4348
+ var _a;
4349
+ const v = Number((_a = c.interactive_config) == null ? void 0 : _a.slot_rotate_ms);
4350
+ return Number.isFinite(v) && v > m2 ? v : m2;
4351
+ }, 0);
4352
+ const rotateMs = attrMs > 0 ? attrMs : declaredMs;
4019
4353
  if (rotateMs > 0 && list.length > 1) {
4020
4354
  this.renderRotatingSlot(slot, list, rotateMs);
4021
4355
  } else {
@@ -4198,6 +4532,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4198
4532
  this.displayedCampaigns.add(campaign.id);
4199
4533
  this.addAnimationStyles();
4200
4534
  const ic = campaign.interactive_config || {};
4535
+ const inlineMul = this._sizeMul(ic);
4536
+ if (inlineMul !== 1) {
4537
+ const sizer = document.createElement("div");
4538
+ sizer.style.cssText = `transform: scale(${inlineMul}); transform-origin: top center;`;
4539
+ target.appendChild(sizer);
4540
+ target = sizer;
4541
+ }
4201
4542
  const { bg, text } = this._surfacePalette(campaign);
4202
4543
  const submitUrl = options == null ? void 0 : options.submitUrl;
4203
4544
  const _formFields = Array.isArray(ic.form_fields) ? ic.form_fields : null;
@@ -4214,70 +4555,73 @@ const _AegisInAppManager = class _AegisInAppManager {
4214
4555
  }
4215
4556
  return;
4216
4557
  }
4217
- switch (campaign.sub_type) {
4218
- case "star_rating":
4219
- rendered = this.renderStarRatingSlot(
4220
- campaign,
4221
- ic,
4222
- bg,
4223
- text,
4224
- target,
4225
- submitUrl
4226
- );
4227
- break;
4228
- case "nps_survey":
4229
- rendered = this.renderNPSSurveySlot(
4230
- campaign,
4231
- ic,
4232
- bg,
4233
- text,
4234
- target,
4235
- submitUrl
4236
- );
4237
- break;
4238
- // Parity tracker — built-in embedded gamification renderers (no
4239
- // AegisMessageRuntime callback needed, so they work on the bill).
4240
- case "spin_wheel":
4241
- rendered = this.renderSpinWheelSlot(campaign, ic, bg, text, target);
4242
- break;
4243
- case "quick_poll":
4244
- rendered = this.renderQuickPollSlot(campaign, ic, bg, text, target);
4245
- break;
4246
- case "carousel_cards":
4247
- rendered = this.renderCarouselCardsSlot(campaign, ic, bg, text, target);
4248
- break;
4249
- case "countdown_offer":
4250
- rendered = this.renderCountdownSlot(campaign, ic, bg, text, target);
4251
- break;
4252
- case "scratch_card":
4253
- rendered = this.renderScratchCardSlot(campaign, ic, bg, text, target);
4254
- break;
4255
- case "custom_html":
4256
- rendered = this.renderCustomHtmlSlot(campaign, ic, bg, text, target);
4257
- break;
4258
- case "stories":
4259
- rendered = renderStoriesRings(this.buildRenderContext(campaign), target);
4260
- break;
4261
- case "video":
4262
- rendered = renderVideoInline(this.buildRenderContext(campaign), target);
4263
- break;
4264
- case "progress_bar":
4265
- rendered = renderProgressBarInline(this.buildRenderContext(campaign), target);
4266
- break;
4267
- case "quiz":
4268
- rendered = this.renderQuizSlot(campaign, ic, bg, text, target);
4269
- if (!rendered) {
4558
+ if (campaign.widget_category === "hero") {
4559
+ rendered = renderHeroInline(this.buildRenderContext(campaign), target);
4560
+ } else
4561
+ switch (campaign.sub_type) {
4562
+ case "star_rating":
4563
+ rendered = this.renderStarRatingSlot(
4564
+ campaign,
4565
+ ic,
4566
+ bg,
4567
+ text,
4568
+ target,
4569
+ submitUrl
4570
+ );
4571
+ break;
4572
+ case "nps_survey":
4573
+ rendered = this.renderNPSSurveySlot(
4574
+ campaign,
4575
+ ic,
4576
+ bg,
4577
+ text,
4578
+ target,
4579
+ submitUrl
4580
+ );
4581
+ break;
4582
+ // Parity tracker built-in embedded gamification renderers (no
4583
+ // AegisMessageRuntime callback needed, so they work on the bill).
4584
+ case "spin_wheel":
4585
+ rendered = this.renderSpinWheelSlot(campaign, ic, bg, text, target);
4586
+ break;
4587
+ case "quick_poll":
4588
+ rendered = this.renderQuickPollSlot(campaign, ic, bg, text, target);
4589
+ break;
4590
+ case "carousel_cards":
4591
+ rendered = this.renderCarouselCardsSlot(campaign, ic, bg, text, target);
4592
+ break;
4593
+ case "countdown_offer":
4594
+ rendered = this.renderCountdownSlot(campaign, ic, bg, text, target);
4595
+ break;
4596
+ case "scratch_card":
4597
+ rendered = this.renderScratchCardSlot(campaign, ic, bg, text, target);
4598
+ break;
4599
+ case "custom_html":
4600
+ rendered = this.renderCustomHtmlSlot(campaign, ic, bg, text, target);
4601
+ break;
4602
+ case "stories":
4603
+ rendered = renderStoriesRings(this.buildRenderContext(campaign), target);
4604
+ break;
4605
+ case "video":
4606
+ rendered = renderVideoInline(this.buildRenderContext(campaign), target);
4607
+ break;
4608
+ case "progress_bar":
4609
+ rendered = renderProgressBarInline(this.buildRenderContext(campaign), target);
4610
+ break;
4611
+ case "quiz":
4612
+ rendered = this.renderQuizSlot(campaign, ic, bg, text, target);
4613
+ if (!rendered) {
4614
+ rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4615
+ }
4616
+ break;
4617
+ // Remaining sub_types (sticky_bar, progress_bar,
4618
+ // product_recommendation) render as a generic card on the bill slot
4619
+ // (overlay path handles their full UX); dedicated slot variants get added
4620
+ // here as merchants need them.
4621
+ default:
4270
4622
  rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4271
- }
4272
- break;
4273
- // Remaining sub_types (sticky_bar, progress_bar,
4274
- // product_recommendation) render as a generic card on the bill slot
4275
- // (overlay path handles their full UX); dedicated slot variants get added
4276
- // here as merchants need them.
4277
- default:
4278
- rendered = this.renderGenericCardSlot(campaign, ic, bg, text, target);
4279
- break;
4280
- }
4623
+ break;
4624
+ }
4281
4625
  if (!rendered) return;
4282
4626
  if (!(options == null ? void 0 : options.deferImpression)) {
4283
4627
  this.trackEvent(campaign.id, "impression");
@@ -4580,6 +4924,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4580
4924
  const labels = shown.map((s) => s.label);
4581
4925
  const n = labels.length >= 2 ? labels.length : 8;
4582
4926
  const isLossSeg = (i) => !!shown[i] && String(shown[i].prize_type || "").toLowerCase() === "no_prize";
4927
+ const alreadyWon = ic.already_won && typeof ic.already_won === "object" ? ic.already_won : null;
4583
4928
  const colorOf = (i) => {
4584
4929
  const c = shown[i] && typeof shown[i].color === "string" ? shown[i].color : "";
4585
4930
  return c || (i % 2 === 0 ? text : `${text}40`);
@@ -4615,6 +4960,10 @@ const _AegisInAppManager = class _AegisInAppManager {
4615
4960
  let soundOn = ic.spin_sound_enabled !== false;
4616
4961
  const pointerColor = cfgStr("spin_pointer_color") || "#ffffff";
4617
4962
  const hubImg = cfgStr("spin_hub_image_url");
4963
+ const wheelBgImg = cfgStr("wheel_background_url");
4964
+ const pointerImg = cfgStr("pointer_image_url");
4965
+ const pointerCenter = (cfgStr("spin_pointer_position") || "top") === "center";
4966
+ const pointerBase = pointerCenter ? "translate(-50%, -100%)" : "translateX(-50%)";
4618
4967
  const isUrl = (s) => /^https?:\/\//i.test(s);
4619
4968
  const now = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
4620
4969
  let rotation = 0;
@@ -4674,7 +5023,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4674
5023
  vibrate(18);
4675
5024
  };
4676
5025
  const body = document.createElement("div");
4677
- body.style.cssText = `padding: ${st.padY ?? 22}px ${st.padX ?? 20}px 20px; display: flex; flex-direction: column; align-items: center; gap: 16px;`;
5026
+ body.style.cssText = `padding: ${st.padY ?? 22}px ${st.padX ?? 20}px 20px; display: flex; flex-direction: column; align-items: center; gap: 10px;`;
4678
5027
  if (campaign.title) {
4679
5028
  const t = document.createElement("div");
4680
5029
  t.style.cssText = `font-size: 18px; font-weight: 800; letter-spacing: -0.01em; text-align: center; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
@@ -4692,11 +5041,56 @@ const _AegisInAppManager = class _AegisInAppManager {
4692
5041
  const wheelWrap = document.createElement("div");
4693
5042
  wheelWrap.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; filter: drop-shadow(0 10px 22px rgba(0,0,0,0.30));`;
4694
5043
  const pointer = document.createElement("div");
4695
- pointer.style.cssText = `position: absolute; left: 50%; top: -7px; transform: translateX(-50%); transform-origin: 50% 0; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 16px solid ${pointerColor}; z-index: 5; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));`;
5044
+ if (pointerCenter) {
5045
+ const needleH = Math.round(radius * 0.42);
5046
+ pointer.style.cssText = `position: absolute; left: 50%; top: 50%; width: 20px; height: ${needleH}px; transform: ${pointerBase}; transform-origin: 50% 100%; z-index: 6; pointer-events: none;`;
5047
+ if (pointerImg) {
5048
+ const pim = document.createElement("img");
5049
+ pim.src = pointerImg;
5050
+ pim.alt = "";
5051
+ pim.style.cssText = "width: 100%; height: 100%; object-fit: contain; object-position: top; display: block; filter: drop-shadow(0 1px 2px rgba(0,0,0,0.4));";
5052
+ pointer.appendChild(pim);
5053
+ } else {
5054
+ const stem = document.createElement("div");
5055
+ stem.style.cssText = `position: absolute; left: 50%; bottom: 0; transform: translateX(-50%); width: 4px; height: 100%; background: ${pointerColor}; border-radius: 2px; box-shadow: 0 1px 2px rgba(0,0,0,0.35);`;
5056
+ const head = document.createElement("div");
5057
+ head.style.cssText = `position: absolute; left: 50%; top: -1px; transform: translateX(-50%); width: 0; height: 0; border-left: 8px solid transparent; border-right: 8px solid transparent; border-bottom: 14px solid ${pointerColor}; filter: drop-shadow(0 1px 1px rgba(0,0,0,0.4));`;
5058
+ pointer.appendChild(stem);
5059
+ pointer.appendChild(head);
5060
+ }
5061
+ } else if (pointerImg) {
5062
+ pointer.style.cssText = `position: absolute; left: 50%; top: -12px; transform: ${pointerBase}; transform-origin: 50% 0; width: 30px; height: 34px; z-index: 5;`;
5063
+ const pim = document.createElement("img");
5064
+ pim.src = pointerImg;
5065
+ pim.alt = "";
5066
+ pim.style.cssText = "width: 100%; height: 100%; object-fit: contain; display: block; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));";
5067
+ pointer.appendChild(pim);
5068
+ } else {
5069
+ pointer.style.cssText = `position: absolute; left: 50%; top: -7px; transform: ${pointerBase}; transform-origin: 50% 0; width: 0; height: 0; border-left: 9px solid transparent; border-right: 9px solid transparent; border-top: 16px solid ${pointerColor}; z-index: 5; filter: drop-shadow(0 2px 2px rgba(0,0,0,0.35));`;
5070
+ }
4696
5071
  const wheel = document.createElement("div");
4697
- wheel.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%; border: 5px solid #ffffff; box-shadow: 0 0 0 2px ${text}2e, inset 0 0 22px rgba(0,0,0,0.20); background: conic-gradient(${stops}); overflow: hidden; will-change: transform; touch-action: none; cursor: ${spinMode === "button" ? "default" : "grab"};`;
5072
+ const wheelFace = wheelBgImg ? `center / cover no-repeat url("${wheelBgImg}")` : `conic-gradient(${stops})`;
5073
+ const wheelRim = wheelBgImg ? "0" : "5px solid #ffffff";
5074
+ const wheelShadow = wheelBgImg ? `0 0 0 2px ${text}2e` : `0 0 0 2px ${text}2e, inset 0 0 22px rgba(0,0,0,0.20)`;
5075
+ wheel.style.cssText = `position: relative; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%; border: ${wheelRim}; box-shadow: ${wheelShadow}; background: ${wheelFace}; overflow: hidden; will-change: transform; touch-action: none; cursor: ${spinMode === "button" ? "default" : "grab"};`;
4698
5076
  if (idleAnim === "wobble") wheel.style.animation = "aegisSpinWobble 3s ease-in-out infinite";
4699
- starts.forEach((b) => {
5077
+ const sliceImgFrac = Math.max(40, Math.min(100, Number(ic.spin_slice_image_size) || 100)) / 100;
5078
+ if (!wheelBgImg) shown.forEach((s, i) => {
5079
+ const simg = typeof s.image_url === "string" ? s.image_url : "";
5080
+ if (!simg || !isUrl(simg)) return;
5081
+ const pts = ["50% 50%"];
5082
+ const steps = Math.max(2, Math.ceil(spans[i] / 6));
5083
+ for (let st2 = 0; st2 <= steps; st2++) {
5084
+ const a = (starts[i] + spans[i] * st2 / steps) * Math.PI / 180;
5085
+ const x = 50 + 50 * sliceImgFrac * Math.sin(a);
5086
+ const y = 50 - 50 * sliceImgFrac * Math.cos(a);
5087
+ pts.push(`${x.toFixed(2)}% ${y.toFixed(2)}%`);
5088
+ }
5089
+ const segEl = document.createElement("div");
5090
+ segEl.style.cssText = `position: absolute; inset: 0; background: center / cover no-repeat url("${simg}"); clip-path: polygon(${pts.join(",")}); pointer-events: none;`;
5091
+ wheel.appendChild(segEl);
5092
+ });
5093
+ if (!wheelBgImg) starts.forEach((b) => {
4700
5094
  const ln = document.createElement("div");
4701
5095
  ln.style.cssText = `position: absolute; left: 50%; top: 0; width: 1.5px; height: 50%; background: rgba(255,255,255,0.55); transform-origin: bottom center; transform: translateX(-50%) rotate(${b}deg); pointer-events: none;`;
4702
5096
  wheel.appendChild(ln);
@@ -4705,7 +5099,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4705
5099
  const rRim = radius - 8;
4706
5100
  const bandLen = rRim - rHub;
4707
5101
  const rText = (rHub + rRim) / 2;
4708
- labels.forEach((label, i) => {
5102
+ if (!wheelBgImg) labels.forEach((label, i) => {
4709
5103
  const mid = midOf(i);
4710
5104
  const icoVal = shown[i] && typeof shown[i].icon === "string" ? shown[i].icon : "";
4711
5105
  const chord = 2 * rText * Math.sin(spans[i] * Math.PI / 180 / 2);
@@ -4750,28 +5144,13 @@ const _AegisInAppManager = class _AegisInAppManager {
4750
5144
  const hub = document.createElement("div");
4751
5145
  hub.style.cssText = `position: absolute; left: 50%; top: 50%; width: 40px; height: 40px; transform: translate(-50%, -50%); border-radius: 50%; background: radial-gradient(circle at 35% 30%, #ffffff, #e9edf5); border: 3px solid #fff; box-shadow: 0 3px 8px rgba(0,0,0,0.30); z-index: 4; overflow: hidden; display: flex; align-items: center; justify-content: center;`;
4752
5146
  if (hubImg) {
4753
- const hc = document.createElement("canvas");
4754
- hc.width = 40;
4755
- hc.height = 40;
4756
- hc.style.cssText = "width: 100%; height: 100%;";
4757
- const hcx = hc.getContext("2d");
4758
- const him = new Image();
4759
- him.crossOrigin = "anonymous";
4760
- him.onload = () => {
4761
- if (!hcx) return;
4762
- const ar = him.width / him.height || 1;
4763
- let w = 32, h = 32;
4764
- if (ar > 1) h = 32 / ar;
4765
- else w = 32 * ar;
4766
- hcx.drawImage(him, (40 - w) / 2, (40 - h) / 2, w, h);
4767
- hcx.globalCompositeOperation = "source-in";
4768
- hcx.fillStyle = "#5b626e";
4769
- hcx.fillRect(0, 0, 40, 40);
4770
- };
4771
- him.onerror = () => {
4772
- };
5147
+ hub.style.border = "none";
5148
+ hub.style.background = "transparent";
5149
+ const him = document.createElement("img");
4773
5150
  him.src = hubImg;
4774
- hub.appendChild(hc);
5151
+ him.alt = "";
5152
+ him.style.cssText = "width: 100%; height: 100%; object-fit: cover; border-radius: 50%; display: block;";
5153
+ hub.appendChild(him);
4775
5154
  } else {
4776
5155
  const hubDot = document.createElement("div");
4777
5156
  hubDot.style.cssText = `width: 11px; height: 11px; border-radius: 50%; background: ${bg};`;
@@ -4785,7 +5164,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4785
5164
  body.appendChild(wheelWrap);
4786
5165
  if (!card.style.position) card.style.position = "relative";
4787
5166
  const result = document.createElement("div");
4788
- result.style.cssText = "font-size: 14px; font-weight: 800; min-height: 20px; text-align: center; letter-spacing: 0.2px;";
5167
+ result.style.cssText = "font-size: 14px; font-weight: 800; min-height: 0; text-align: center; letter-spacing: 0.2px;";
4789
5168
  if (ic.spin_sound_enabled !== false) {
4790
5169
  const soundBtn = document.createElement("button");
4791
5170
  soundBtn.type = "button";
@@ -4800,7 +5179,7 @@ const _AegisInAppManager = class _AegisInAppManager {
4800
5179
  }
4801
5180
  const btn = document.createElement("button");
4802
5181
  btn.textContent = campaign.button_text || "Spin the wheel";
4803
- btn.style.cssText = `padding: 12px 32px; border-radius: 999px; border: none; background: #ffffff; color: ${bg}; font-size: 15px; font-weight: 800; letter-spacing: 0.2px; cursor: pointer; box-shadow: 0 6px 16px rgba(0,0,0,0.22); transition: transform 0.15s, box-shadow 0.15s; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
5182
+ btn.style.cssText = `padding: 9px 22px; border-radius: 999px; border: none; background: ${text}; color: ${bg}; font-size: 13px; font-weight: 700; letter-spacing: 0.2px; cursor: pointer; box-shadow: 0 4px 12px rgba(0,0,0,0.20); transition: transform 0.15s, box-shadow 0.15s; font-family: 'Inter Tight', Inter, system-ui, -apple-system, sans-serif;`;
4804
5183
  btn.addEventListener("mouseenter", () => {
4805
5184
  if (btn.disabled) return;
4806
5185
  btn.style.transform = "translateY(-1px)";
@@ -4829,12 +5208,12 @@ const _AegisInAppManager = class _AegisInAppManager {
4829
5208
  pVel *= 0.7;
4830
5209
  pDefl += pVel;
4831
5210
  if (Math.abs(pDefl) > 0.06 || Math.abs(pVel) > 0.06) {
4832
- pointer.style.transform = `translateX(-50%) rotate(${pDefl.toFixed(2)}deg)`;
5211
+ pointer.style.transform = `${pointerBase} rotate(${pDefl.toFixed(2)}deg)`;
4833
5212
  pointerRaf = requestAnimationFrame(stepPointer);
4834
5213
  } else {
4835
5214
  pDefl = 0;
4836
5215
  pVel = 0;
4837
- pointer.style.transform = "translateX(-50%) rotate(0deg)";
5216
+ pointer.style.transform = `${pointerBase} rotate(0deg)`;
4838
5217
  pointerRaf = 0;
4839
5218
  }
4840
5219
  };
@@ -4943,7 +5322,25 @@ const _AegisInAppManager = class _AegisInAppManager {
4943
5322
  this._playReaction(card, "empathize", void 0, ic);
4944
5323
  }
4945
5324
  if (winReveal === "takeover" && r.won) renderTakeover(r);
4946
- else result.textContent = r.label ? r.code ? `${r.label} — ${r.code}` : r.label : "Thanks for playing!";
5325
+ else if (r.label && r.code) {
5326
+ result.textContent = "";
5327
+ const tag = document.createElement("span");
5328
+ tag.textContent = `${r.label} — `;
5329
+ const chip = document.createElement("button");
5330
+ chip.type = "button";
5331
+ chip.textContent = `${r.code} ⧉`;
5332
+ chip.style.cssText = `padding: 2px 8px; border-radius: 999px; border: 1.5px dashed ${text}66; background: transparent; color: ${text}; font-weight: 800; font-size: 12px; letter-spacing: 0.5px; cursor: pointer;`;
5333
+ chip.addEventListener("click", () => {
5334
+ var _a;
5335
+ try {
5336
+ void ((_a = navigator.clipboard) == null ? void 0 : _a.writeText(r.code));
5337
+ chip.textContent = "Copied ✓";
5338
+ } catch {
5339
+ }
5340
+ });
5341
+ result.appendChild(tag);
5342
+ result.appendChild(chip);
5343
+ } else result.textContent = r.label || "Thanks for playing!";
4947
5344
  retireCta();
4948
5345
  };
4949
5346
  const beginSpin = (velocityDegPerMs) => {
@@ -4959,16 +5356,40 @@ const _AegisInAppManager = class _AegisInAppManager {
4959
5356
  const v = Math.min(2.4, Math.max(0.25, velocityDegPerMs || 0.6));
4960
5357
  const turns = Math.round(3 + v * 2);
4961
5358
  const provisional = Math.floor(Math.random() * n);
5359
+ const loopSpeed = 0.5 + v * 0.35;
5360
+ let looping = true;
4962
5361
  let done = false;
5362
+ let lastTs = now();
5363
+ const loopFrame = () => {
5364
+ if (!looping) return;
5365
+ const t = now();
5366
+ const moved = loopSpeed * (t - lastTs);
5367
+ lastTs = t;
5368
+ rotation += moved;
5369
+ spinSpeed = moved;
5370
+ wheel.style.transform = `rotate(${rotation}deg)`;
5371
+ playTicks(rotation);
5372
+ requestAnimationFrame(loopFrame);
5373
+ };
5374
+ requestAnimationFrame(loopFrame);
5375
+ const lossIndex = () => {
5376
+ for (let i = 0; i < n; i++) {
5377
+ if (isLossSeg(i)) return i;
5378
+ }
5379
+ return provisional;
5380
+ };
4963
5381
  const launch = (idx, r) => {
4964
5382
  if (done) return;
4965
5383
  done = true;
5384
+ looping = false;
4966
5385
  const segMid = midOf(idx);
4967
5386
  const want = ((360 - segMid) % 360 + 360) % 360;
4968
5387
  const base = rotation;
4969
5388
  let target2 = base - (base % 360 + 360) % 360 + want + turns * 360;
4970
5389
  if (target2 <= base + 360) target2 += 360;
4971
- animateTo(target2, 2300 + turns * 230, () => {
5390
+ const matched = 3 * (target2 - base) / Math.max(0.05, loopSpeed);
5391
+ const duration = Math.min(4200, Math.max(2300, matched));
5392
+ animateTo(target2, duration, () => {
4972
5393
  spinning = false;
4973
5394
  reveal(r);
4974
5395
  });
@@ -4982,16 +5403,18 @@ const _AegisInAppManager = class _AegisInAppManager {
4982
5403
  const kind = prize && typeof prize.prize_type === "string" ? prize.prize_type.toLowerCase() : shown[idx] && typeof shown[idx].prize_type === "string" ? String(shown[idx].prize_type).toLowerCase() : "";
4983
5404
  const lost = prize ? String(prize.prize_type || "").toLowerCase() === "no_prize" : isLossSeg(idx);
4984
5405
  launch(idx, { label, code, won: !!label && !lost, kind });
5406
+ }).catch(() => {
4985
5407
  });
4986
- window.setTimeout(
4987
- () => launch(provisional, {
4988
- label: labels[provisional] || "",
5408
+ window.setTimeout(() => {
5409
+ if (done) return;
5410
+ const li = lossIndex();
5411
+ launch(li, {
5412
+ label: labels[li] || "",
4989
5413
  code: "",
4990
- won: !isLossSeg(provisional),
4991
- kind: shown[provisional] && typeof shown[provisional].prize_type === "string" ? String(shown[provisional].prize_type).toLowerCase() : ""
4992
- }),
4993
- 900
4994
- );
5414
+ won: false,
5415
+ kind: shown[li] && typeof shown[li].prize_type === "string" ? String(shown[li].prize_type).toLowerCase() : "no_prize"
5416
+ });
5417
+ }, 8e3);
4995
5418
  };
4996
5419
  if (spinMode === "button" || spinMode === "both") {
4997
5420
  btn.addEventListener("click", () => beginSpin(0.7));
@@ -5063,6 +5486,16 @@ const _AegisInAppManager = class _AegisInAppManager {
5063
5486
  body.appendChild(btn);
5064
5487
  card.appendChild(body);
5065
5488
  target.appendChild(card);
5489
+ if (alreadyWon) {
5490
+ stopIdle();
5491
+ const wonKind = String(alreadyWon.prize_type || "").toLowerCase();
5492
+ reveal({
5493
+ label: String(alreadyWon.label || ""),
5494
+ code: String(alreadyWon.coupon_code || ""),
5495
+ won: wonKind !== "no_prize",
5496
+ kind: wonKind
5497
+ });
5498
+ }
5066
5499
  return true;
5067
5500
  }
5068
5501
  /** Built-in embedded QUICK POLL — option buttons submit the chosen index. */
@@ -5314,6 +5747,34 @@ const _AegisInAppManager = class _AegisInAppManager {
5314
5747
  target.appendChild(card);
5315
5748
  return true;
5316
5749
  }
5750
+ /** Foil palette — a brushed-metal gradient + a contrast "ink" (for the
5751
+ * SCRATCH-HERE text, sparkle motif, and logo tint) derived from the operator's
5752
+ * chosen foil colour (brand primary / custom hex). Unset / invalid → the
5753
+ * default silver. Light foils get dark ink, dark foils get white ink, so the
5754
+ * prompt stays legible on any colour. */
5755
+ _scratchFoilPalette(hex) {
5756
+ const clean = (hex || "").trim().replace(/^#/, "");
5757
+ if (!/^[0-9a-fA-F]{6}$/.test(clean)) {
5758
+ return { g0: "#d7dbe2", g1: "#b4bac4", g2: "#c9cdd6", ink: "#6b7280" };
5759
+ }
5760
+ const r = parseInt(clean.slice(0, 2), 16);
5761
+ const g = parseInt(clean.slice(2, 4), 16);
5762
+ const b = parseInt(clean.slice(4, 6), 16);
5763
+ const toward = (amt) => {
5764
+ const m2 = (c) => Math.round(c + (255 - c) * amt);
5765
+ return `rgb(${m2(r)}, ${m2(g)}, ${m2(b)})`;
5766
+ };
5767
+ const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
5768
+ return {
5769
+ g0: toward(0.34),
5770
+ // bright sheen
5771
+ g1: `#${clean}`,
5772
+ // base colour
5773
+ g2: toward(0.18),
5774
+ // soft sheen
5775
+ ink: lum > 0.62 ? "#5b6270" : "rgba(255,255,255,0.92)"
5776
+ };
5777
+ }
5317
5778
  /** Built-in embedded SCRATCH CARD — a canvas foil the customer scratches to
5318
5779
  * reveal the server-picked prize, then submits (records + grants). Confetti
5319
5780
  * on reveal. No AegisMessageRuntime callback dependency (works on the bill). */
@@ -5331,6 +5792,7 @@ const _AegisInAppManager = class _AegisInAppManager {
5331
5792
  let soundOn = ic.scratch_sound_enabled !== false;
5332
5793
  const revealImg = cfgStr("reveal_image_url");
5333
5794
  const foilImg = cfgStr("scratch_foil_image_url");
5795
+ const foilPal = this._scratchFoilPalette(cfgStr("scratch_foil_color"));
5334
5796
  const pool = Array.isArray(ic.prize_pool) ? ic.prize_pool : [];
5335
5797
  const isUrl = (s) => /^https?:\/\//i.test(s);
5336
5798
  const sampleWin = pool.find(
@@ -5473,9 +5935,9 @@ const _AegisInAppManager = class _AegisInAppManager {
5473
5935
  if (!ctx) return;
5474
5936
  ctx.globalCompositeOperation = "source-over";
5475
5937
  const foil = ctx.createLinearGradient(0, 0, CW, CH);
5476
- foil.addColorStop(0, "#d7dbe2");
5477
- foil.addColorStop(0.5, "#b4bac4");
5478
- foil.addColorStop(1, "#c9cdd6");
5938
+ foil.addColorStop(0, foilPal.g0);
5939
+ foil.addColorStop(0.5, foilPal.g1);
5940
+ foil.addColorStop(1, foilPal.g2);
5479
5941
  ctx.fillStyle = foil;
5480
5942
  ctx.fillRect(0, 0, CW, CH);
5481
5943
  ctx.textAlign = "center";
@@ -5484,17 +5946,19 @@ const _AegisInAppManager = class _AegisInAppManager {
5484
5946
  ctx.globalAlpha = 0.6;
5485
5947
  ctx.drawImage(logo, (CW - logo.width) / 2, CH * 0.42 - logo.height / 2, logo.width, logo.height);
5486
5948
  ctx.globalAlpha = 1;
5487
- ctx.fillStyle = "#6b7280";
5949
+ ctx.fillStyle = foilPal.ink;
5488
5950
  ctx.font = "700 12px system-ui, sans-serif";
5489
5951
  ctx.fillText("SCRATCH HERE", CW / 2, CH * 0.84);
5490
5952
  } else {
5491
- ctx.fillStyle = "rgba(108,116,128,0.18)";
5953
+ ctx.globalAlpha = 0.22;
5954
+ ctx.fillStyle = foilPal.ink;
5492
5955
  ctx.font = "12px system-ui, sans-serif";
5493
5956
  for (let gy = 18; gy < CH; gy += 30) {
5494
5957
  const off = (gy / 30 | 0) % 2 ? 16 : 0;
5495
5958
  for (let gx = 16 + off; gx < CW; gx += 32) ctx.fillText("✦", gx, gy);
5496
5959
  }
5497
- ctx.fillStyle = "#7c828e";
5960
+ ctx.globalAlpha = 1;
5961
+ ctx.fillStyle = foilPal.ink;
5498
5962
  ctx.font = "700 13px system-ui, sans-serif";
5499
5963
  ctx.fillText("SCRATCH HERE", CW / 2, CH / 2);
5500
5964
  }
@@ -5521,7 +5985,7 @@ const _AegisInAppManager = class _AegisInAppManager {
5521
5985
  if (!lctx) return;
5522
5986
  lctx.drawImage(im, 0, 0, lc.width, lc.height);
5523
5987
  lctx.globalCompositeOperation = "source-in";
5524
- lctx.fillStyle = "#5b626e";
5988
+ lctx.fillStyle = foilPal.ink;
5525
5989
  lctx.fillRect(0, 0, lc.width, lc.height);
5526
5990
  paintFoil(lc);
5527
5991
  };
@@ -6246,6 +6710,9 @@ const _AegisInAppManager = class _AegisInAppManager {
6246
6710
  return;
6247
6711
  }
6248
6712
  this.displayedCampaigns.add(campaign.id);
6713
+ this._displaySizeMul = this._sizeMul(
6714
+ campaign.interactive_config
6715
+ );
6249
6716
  const interactiveSubTypes = /* @__PURE__ */ new Set([
6250
6717
  "spin_wheel",
6251
6718
  "scratch_card",
@@ -6438,6 +6905,14 @@ const _AegisInAppManager = class _AegisInAppManager {
6438
6905
  * slot renderers (`renderSpinWheelSlot` / `renderScratchCardSlot`) in a modal
6439
6906
  * so the gamified widget is fully playable — not a dead frame.
6440
6907
  */
6908
+ /** Display-size multiplier — operator control over how big a bounded overlay
6909
+ * renders (small 0.82 / medium 1 / large 1.18). Default medium = prior size.
6910
+ * Read from interactive_config.display_size; mirrors the schema enum + the
6911
+ * dashboard preview harness so the operator's choice is honored identically. */
6912
+ _sizeMul(ic) {
6913
+ const s = ic && typeof ic.display_size === "string" ? ic.display_size : "medium";
6914
+ return s === "small" ? 0.82 : s === "large" ? 1.18 : 1;
6915
+ }
6441
6916
  renderGamificationOverlay(campaign, ic, bg, text) {
6442
6917
  const overlay = this.createOverlay(`aegis-in-app-${campaign.sub_type}-overlay`);
6443
6918
  const modal = document.createElement("div");
@@ -6452,7 +6927,11 @@ const _AegisInAppManager = class _AegisInAppManager {
6452
6927
  this.renderSpinWheelSlot(campaign, ic, bg, text, modal);
6453
6928
  }
6454
6929
  this._addCornerClose(modal, overlay, campaign.id, text);
6455
- overlay.appendChild(modal);
6930
+ const mul = this._sizeMul(ic);
6931
+ const sizer = document.createElement("div");
6932
+ sizer.style.cssText = `display: flex; justify-content: center; align-items: center; width: 100%; transform: scale(${mul}); transform-origin: center;`;
6933
+ sizer.appendChild(modal);
6934
+ overlay.appendChild(sizer);
6456
6935
  this.addAnimationStyles();
6457
6936
  document.body.appendChild(overlay);
6458
6937
  }
@@ -7115,8 +7594,30 @@ const _AegisInAppManager = class _AegisInAppManager {
7115
7594
  background: rgba(0,0,0,0.5); display: flex; align-items: center;
7116
7595
  justify-content: center; z-index: 99999; animation: aegisFadeIn 0.3s ease;
7117
7596
  `;
7597
+ if (this._displaySizeMul !== 1) {
7598
+ const mul = this._displaySizeMul;
7599
+ queueMicrotask(() => {
7600
+ const card = overlay.firstElementChild;
7601
+ if (!card || card.dataset.aegisSized) return;
7602
+ card.dataset.aegisSized = "1";
7603
+ const wrap = document.createElement("div");
7604
+ wrap.style.cssText = `display: flex; align-items: center; justify-content: center; transform: scale(${mul}); transform-origin: center;`;
7605
+ overlay.insertBefore(wrap, card);
7606
+ wrap.appendChild(card);
7607
+ });
7608
+ }
7118
7609
  return overlay;
7119
7610
  }
7611
+ /** Wrap a card in a display-size scaling container (no-op at medium/×1). Used
7612
+ * by formats that build their overlay inline (renderModal) rather than via
7613
+ * createOverlay, so size is honored identically. */
7614
+ _wrapScaled(card) {
7615
+ if (this._displaySizeMul === 1) return card;
7616
+ const wrap = document.createElement("div");
7617
+ wrap.style.cssText = `display: flex; align-items: center; justify-content: center; transform: scale(${this._displaySizeMul}); transform-origin: center;`;
7618
+ wrap.appendChild(card);
7619
+ return wrap;
7620
+ }
7120
7621
  createCTAButton(campaign, bg, text) {
7121
7622
  const btn = document.createElement("button");
7122
7623
  btn.className = "aegis-cta";
@@ -7549,7 +8050,7 @@ const _AegisInAppManager = class _AegisInAppManager {
7549
8050
  actions.appendChild(closeButton);
7550
8051
  content.appendChild(actions);
7551
8052
  modal.appendChild(content);
7552
- overlay.appendChild(modal);
8053
+ overlay.appendChild(this._wrapScaled(modal));
7553
8054
  if (ic.modal_dismiss_on_overlay !== false) {
7554
8055
  overlay.addEventListener("click", (e) => {
7555
8056
  if (e.target === overlay) {
@@ -8985,7 +9486,7 @@ _AegisInAppManager.KNOWN_SURFACES = /* @__PURE__ */ new Set([
8985
9486
  ]);
8986
9487
  _AegisInAppManager.SERVED_DEDUP_PREFIX = "aegis_served_fired:";
8987
9488
  let AegisInAppManager = _AegisInAppManager;
8988
- function renderPreview(config) {
9489
+ function renderPreview(config, opts) {
8989
9490
  document.querySelectorAll(
8990
9491
  '[class^="aegis-in-app-"]'
8991
9492
  ).forEach((el) => {
@@ -9009,12 +9510,33 @@ function renderPreview(config) {
9009
9510
  writeKey: "preview-mode",
9010
9511
  apiHost: "",
9011
9512
  debugMode: false,
9012
- enableSSE: false
9513
+ enableSSE: false,
9514
+ ...(opts == null ? void 0 : opts.cart) ? { getCartState: () => opts.cart } : {}
9013
9515
  });
9014
9516
  const m2 = manager;
9015
9517
  m2.trackEvent = async () => {
9016
9518
  };
9017
9519
  m2.addAnimationStyles();
9520
+ const cfgRec = config;
9521
+ const deliveryModes = Array.isArray(cfgRec.delivery_modes) ? cfgRec.delivery_modes : [];
9522
+ const widgetCategory = typeof cfgRec.widget_category === "string" ? cfgRec.widget_category : "";
9523
+ const inline = deliveryModes.includes("embedded_card") && !!widgetCategory;
9524
+ const coGroup = (opts == null ? void 0 : opts.coGroup) ?? [];
9525
+ if (typeof document !== "undefined" && (coGroup.length > 0 || inline)) {
9526
+ const anchors = document.querySelectorAll("[data-aegis-slot]");
9527
+ if (anchors.length > 0) {
9528
+ try {
9529
+ anchors.forEach((slot) => {
9530
+ slot.querySelectorAll(":scope > :not([data-aegis-slot-default])").forEach((el) => el.remove());
9531
+ });
9532
+ const mm = manager;
9533
+ mm.campaigns = (inline ? [config, ...coGroup] : coGroup).map((c) => ({ ...c, surface: void 0, target_screens: void 0 })).sort((a, b) => (b.priority || 0) - (a.priority || 0));
9534
+ mm.renderIntoSlots();
9535
+ if (inline) return;
9536
+ } catch {
9537
+ }
9538
+ }
9539
+ }
9018
9540
  if (config.type === "stories" || config.sub_type === "stories") {
9019
9541
  const host = document.createElement("div");
9020
9542
  host.className = "aegis-in-app-stories-host";
@@ -9144,10 +9666,19 @@ function maybeStartPairing(apiHost) {
9144
9666
  }
9145
9667
  }
9146
9668
  function isIntentLeaf(expr) {
9147
- return "signal" in expr;
9669
+ return !("operands" in expr);
9670
+ }
9671
+ function getValueByPath(namespaces, path) {
9672
+ if (!namespaces || !path) return void 0;
9673
+ let cur = namespaces;
9674
+ for (const part of path.split(".")) {
9675
+ if (cur === null || typeof cur !== "object" || Array.isArray(cur)) return void 0;
9676
+ cur = cur[part];
9677
+ }
9678
+ return cur;
9148
9679
  }
9149
- function evaluateLeaf(leaf, snapshot) {
9150
- const actual = snapshot[leaf.signal];
9680
+ function evaluateLeaf(leaf, snapshot, namespaces) {
9681
+ const actual = leaf.path !== void 0 ? getValueByPath(namespaces, leaf.path) : leaf.signal !== void 0 ? snapshot[leaf.signal] : void 0;
9151
9682
  if (actual === void 0 || actual === null) {
9152
9683
  return null;
9153
9684
  }
@@ -9179,14 +9710,14 @@ function evaluateLeaf(leaf, snapshot) {
9179
9710
  }
9180
9711
  }
9181
9712
  }
9182
- function evaluateExpr(expr, snapshot) {
9713
+ function evaluateExpr(expr, snapshot, namespaces) {
9183
9714
  if (isIntentLeaf(expr)) {
9184
- return evaluateLeaf(expr, snapshot);
9715
+ return evaluateLeaf(expr, snapshot, namespaces);
9185
9716
  }
9186
9717
  switch (expr.op) {
9187
9718
  case "AND": {
9188
9719
  for (const operand of expr.operands) {
9189
- const v = evaluateExpr(operand, snapshot);
9720
+ const v = evaluateExpr(operand, snapshot, namespaces);
9190
9721
  if (v === null) return false;
9191
9722
  if (!v) return false;
9192
9723
  }
@@ -9195,14 +9726,14 @@ function evaluateExpr(expr, snapshot) {
9195
9726
  case "OR": {
9196
9727
  let allNull = true;
9197
9728
  for (const operand of expr.operands) {
9198
- const v = evaluateExpr(operand, snapshot);
9729
+ const v = evaluateExpr(operand, snapshot, namespaces);
9199
9730
  if (v === true) return true;
9200
9731
  if (v !== null) allNull = false;
9201
9732
  }
9202
9733
  return allNull ? null : false;
9203
9734
  }
9204
9735
  case "NOT": {
9205
- const v = evaluateExpr(expr.operands[0], snapshot);
9736
+ const v = evaluateExpr(expr.operands[0], snapshot, namespaces);
9206
9737
  if (v === null) return null;
9207
9738
  return !v;
9208
9739
  }
@@ -9211,14 +9742,15 @@ function evaluateExpr(expr, snapshot) {
9211
9742
  }
9212
9743
  }
9213
9744
  }
9214
- function evaluateRule(rule, snapshot) {
9215
- const result = evaluateExpr(rule.when, snapshot);
9745
+ function evaluateRule(rule, snapshot, namespaces) {
9746
+ const result = evaluateExpr(rule.when, snapshot, namespaces);
9216
9747
  return result === true;
9217
9748
  }
9218
9749
  class IntentRuleEvaluator {
9219
9750
  constructor() {
9220
9751
  this.armed = [];
9221
9752
  this.snapshot = {};
9753
+ this.namespaces = {};
9222
9754
  this.firedThisSession = /* @__PURE__ */ new Set();
9223
9755
  this.silencedThisSession = /* @__PURE__ */ new Set();
9224
9756
  }
@@ -9232,6 +9764,21 @@ class IntentRuleEvaluator {
9232
9764
  getSnapshot() {
9233
9765
  return this.snapshot;
9234
9766
  }
9767
+ // ─── generic reactive context (updateContext) ───
9768
+ /** Merge a namespace's data (shallow). Path-leaves (`cart.total`) resolve
9769
+ * against this. The runtime's updateContext() delegates here so there is ONE
9770
+ * context store, then bridges well-known paths to enum signals. */
9771
+ updateContext(namespace, data) {
9772
+ if (!namespace || !data) return;
9773
+ this.namespaces[namespace] = { ...this.namespaces[namespace], ...data };
9774
+ }
9775
+ getNamespaces() {
9776
+ return this.namespaces;
9777
+ }
9778
+ /** Resolve a dot-path against the live context (for previews / debugging). */
9779
+ getValueByPath(path) {
9780
+ return getValueByPath(this.namespaces, path);
9781
+ }
9235
9782
  // ─── armed-campaign management ───
9236
9783
  setArmed(campaigns) {
9237
9784
  this.armed = [...campaigns];
@@ -9277,7 +9824,7 @@ class IntentRuleEvaluator {
9277
9824
  });
9278
9825
  for (let i = 0; i < candidates.length; i++) {
9279
9826
  const c = candidates[i];
9280
- if (!evaluateRule(c.rule, this.snapshot)) continue;
9827
+ if (!evaluateRule(c.rule, this.snapshot, this.namespaces)) continue;
9281
9828
  if (c.rule.suppress_competing) {
9282
9829
  const suppressIds = this.armed.filter((other) => other.id !== c.id).map((other) => other.id);
9283
9830
  suppressIds.forEach((id) => this.silencedThisSession.add(id));
@@ -9360,6 +9907,13 @@ class ContactScoresFetcher {
9360
9907
  this.latestSnapshot = payload;
9361
9908
  if (this.evaluator) {
9362
9909
  this.evaluator.updateSnapshot(toIntentSnapshot(payload));
9910
+ if (payload.context && typeof payload.context === "object") {
9911
+ for (const [ns, data] of Object.entries(payload.context)) {
9912
+ if (data && typeof data === "object") {
9913
+ this.evaluator.updateContext(ns, data);
9914
+ }
9915
+ }
9916
+ }
9363
9917
  }
9364
9918
  return payload;
9365
9919
  } catch (err) {
@@ -12118,6 +12672,9 @@ class AegisLoyaltyManager {
12118
12672
  }
12119
12673
  }
12120
12674
  class AegisMessageRuntime {
12675
+ // Context is held in ONE place — `intentRuleEvaluator.namespaces` (fed via
12676
+ // updateContext). The renderer cart reader + the cart_value signal are DERIVED
12677
+ // from it; there is no parallel cart/context store on the runtime.
12121
12678
  constructor(config) {
12122
12679
  var _a;
12123
12680
  this.initialized = false;
@@ -12156,7 +12713,7 @@ class AegisMessageRuntime {
12156
12713
  getWorkspaceId: config.getWorkspaceId,
12157
12714
  // First-party cart reader — renderers (progress-bar) read live cart
12158
12715
  // state from here instead of `window.*` cart globals. See setCartState().
12159
- getCartState: () => this.cartState
12716
+ getCartState: () => this.deriveCartState()
12160
12717
  });
12161
12718
  this.intentRuleEvaluator = new IntentRuleEvaluator();
12162
12719
  this.contactScores = new ContactScoresFetcher({
@@ -12237,31 +12794,77 @@ class AegisMessageRuntime {
12237
12794
  (_b = (_a = this.inApp).onClientEvent) == null ? void 0 : _b.call(_a, eventName, eventData);
12238
12795
  }
12239
12796
  /**
12240
- * Feed live cart state into the runtime the imperative, encapsulated
12241
- * equivalent of the `window.aegis_cart` global the SDK reads for
12242
- * third-party Shopify/Woo/Magento embeds. Headless storefronts (e.g. the
12243
- * Active Commerce Store) call this on every cart mutation.
12797
+ * Unified reactive context the GENERAL imperative state API (Plotline-style
12798
+ * "global context state engine"). The host app feeds named namespaces:
12799
+ * runtime.updateContext('cart', { total: 540, items: 3, currency: 'INR' })
12800
+ * runtime.updateContext('user', { tier: 'gold', lifecycle: 'active' })
12801
+ * runtime.updateContext('session', { idle_s: 60 })
12244
12802
  *
12245
- * The state is held in PRIVATE runtime state (no window pollution, not
12246
- * readable/writable by third-party scripts) and:
12247
- * 1. surfaces to renderers (progress-bar free-delivery / cart-total goals)
12248
- * via the getCartState reader passed into the InAppManager;
12249
- * 2. feeds the `cart_value` micro-intent signal so `CART_VALUE`-gated
12250
- * campaigns (e.g. a cart-idle nudge) can evaluate — this signal was
12251
- * previously never populated client-side;
12252
- * 3. taps a debounced in-app refresh so a newly eligible cart-gated
12253
- * campaign arms without waiting for the next identity flip.
12803
+ * Held in PRIVATE runtime state (no `window` pollution). Merged shallowly per
12804
+ * namespace and evaluated entirely client-side no round-trip. This supersedes
12805
+ * the single-purpose `setCartState` (kept as a thin wrapper for back-compat).
12254
12806
  *
12255
- * This is a visual/targeting hint only. Reward GATING that must agree with
12256
- * the server-side checkout guard should use the trusted SSE progress source.
12807
+ * Design note (vs the proposed rewrite): we DELIBERATELY do NOT introduce a
12808
+ * parallel rule schema. Our canonical `IntentRule` (shared with the cell-plane
12809
+ * Pydantic schema + the editor + seeded campaigns) stays the source of truth.
12810
+ * `updateContext` BRIDGES known paths into the existing enum-keyed
12811
+ * `IntentSnapshot` (e.g. cart.total → the `cart_value` signal) so today's rules
12812
+ * keep firing — fed by the generic API. Arbitrary dot-path leaves
12813
+ * (`cart.foo`) are a forward-compatible follow-up that adds an optional `path`
12814
+ * to the canonical `IntentLeaf` (schema-mirrored + drift-guarded), NOT a second
12815
+ * evaluator. Tri-state (null = unprovable) evaluation is preserved — we do not
12816
+ * regress missing paths to `false`.
12257
12817
  */
12258
- setCartState(state) {
12259
- this.cartState = state;
12260
- const total = Number(state == null ? void 0 : state.total);
12261
- if (Number.isFinite(total)) {
12262
- this.intentRuleEvaluator.updateSignal("cart_value", total);
12818
+ updateContext(namespace, data) {
12819
+ if (!namespace || typeof namespace !== "string" || !data) return;
12820
+ this.intentRuleEvaluator.updateContext(namespace, data);
12821
+ this.bridgeContextToSignals(namespace);
12822
+ this.inApp.refreshOnEvent("context_updated");
12823
+ }
12824
+ /** Read the current client-side context snapshot (for the editor scrubber /
12825
+ * preview bridge to mirror live values). Read-only view of the ONE store. */
12826
+ getContext() {
12827
+ return this.intentRuleEvaluator.getNamespaces();
12828
+ }
12829
+ /**
12830
+ * Bridge well-known context paths into the canonical enum-keyed IntentSnapshot,
12831
+ * so existing signal-based rules (cart_value) keep firing off the generic
12832
+ * `updateContext`. Reads from the ONE store (the evaluator). Extend here as new
12833
+ * signals map to context paths — no parallel engine.
12834
+ */
12835
+ bridgeContextToSignals(namespace) {
12836
+ if (namespace === "cart") {
12837
+ const total = Number(this.intentRuleEvaluator.getValueByPath("cart.total") ?? this.intentRuleEvaluator.getValueByPath("cart.cart_value"));
12838
+ if (Number.isFinite(total)) {
12839
+ this.intentRuleEvaluator.updateSignal("cart_value", total);
12840
+ }
12263
12841
  }
12264
- this.inApp.refreshOnEvent("cart_updated");
12842
+ }
12843
+ /** Derive the typed cart snapshot for the progress-bar renderer reader from
12844
+ * the ONE context store (the `cart` namespace). No separate cart field. */
12845
+ deriveCartState() {
12846
+ const total = Number(this.intentRuleEvaluator.getValueByPath("cart.total"));
12847
+ if (!Number.isFinite(total)) return void 0;
12848
+ const items = Number(
12849
+ this.intentRuleEvaluator.getValueByPath("cart.items") ?? this.intentRuleEvaluator.getValueByPath("cart.itemCount")
12850
+ );
12851
+ const currency = this.intentRuleEvaluator.getValueByPath("cart.currency");
12852
+ return {
12853
+ total,
12854
+ itemCount: Number.isFinite(items) ? items : 0,
12855
+ currency: typeof currency === "string" ? currency : "INR"
12856
+ };
12857
+ }
12858
+ /**
12859
+ * Back-compat wrapper over `updateContext('cart', …)`. The single-purpose cart
12860
+ * feed for headless storefronts; prefer `updateContext` for new code.
12861
+ */
12862
+ setCartState(state) {
12863
+ this.updateContext("cart", {
12864
+ total: state.total,
12865
+ items: state.itemCount,
12866
+ currency: state.currency
12867
+ });
12265
12868
  }
12266
12869
  // --- Qualifying-event refresh facade (DLR Tracker 1 / P1, 2026-05-28) ---
12267
12870
  //
@@ -12442,10 +13045,13 @@ export {
12442
13045
  TriggerEngine,
12443
13046
  U as UserNamespace,
12444
13047
  bootstrap,
13048
+ closeChat,
12445
13049
  debounce,
12446
13050
  aegis as default,
12447
13051
  deriveDeviceFingerprint,
13052
+ getCurrentLauncher,
12448
13053
  m as murmurhash3_x86_32,
13054
+ openChat,
12449
13055
  readFirstPartyCookie,
12450
13056
  renderPreview,
12451
13057
  throttle,