@helpai/elements 0.30.0 → 0.31.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/index.mjs CHANGED
@@ -3,11 +3,7 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
 
5
5
  // src/index.ts
6
- import { h, render } from "preact";
7
-
8
- // src/ui/app.tsx
9
- import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
10
- import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
6
+ import { render } from "preact";
11
7
 
12
8
  // src/core/brand.ts
13
9
  var BRAND = {
@@ -279,8 +275,8 @@ function resolveStrings(locale, override) {
279
275
  function primaryTag(locale) {
280
276
  return (locale.split("-")[0] ?? "en").toLowerCase();
281
277
  }
282
- function moduleLabel(strings, label) {
283
- return strings[label] ?? label;
278
+ function localizeText(strings, value) {
279
+ return strings[value] ?? value;
284
280
  }
285
281
  function isNestedShape(override) {
286
282
  if (!override) return false;
@@ -290,6 +286,51 @@ function isNestedShape(override) {
290
286
  return false;
291
287
  }
292
288
 
289
+ // src/i18n/overlay.ts
290
+ function pickTranslation(translations, locale) {
291
+ if (!translations?.length || !locale) return void 0;
292
+ return translations.find((t) => t.locale === locale);
293
+ }
294
+ function overlayContent(item, locale) {
295
+ const { translations, defaultLocale: _defaultLocale, ...base } = item;
296
+ const tr = pickTranslation(translations, locale);
297
+ if (!tr) return base;
298
+ return {
299
+ ...base,
300
+ title: tr.title ?? base.title,
301
+ description: tr.description ?? base.description,
302
+ content: tr.content ?? base.content
303
+ };
304
+ }
305
+ function overlayFormDef(def, locale) {
306
+ const { translations, defaultLocale: _defaultLocale, ...base } = def;
307
+ const tr = pickTranslation(translations, locale);
308
+ if (!tr) return base;
309
+ const fieldTrByName = new Map((tr.fields ?? []).map((f) => [f.name, f]));
310
+ const fields = base.fields.map((field) => {
311
+ const ftr = fieldTrByName.get(field.name);
312
+ if (!ftr) return field;
313
+ const optionTrByValue = new Map((ftr.options ?? []).map((o) => [o.value, o]));
314
+ return {
315
+ ...field,
316
+ label: ftr.label ?? field.label,
317
+ placeholder: ftr.placeholder ?? field.placeholder,
318
+ validationHint: ftr.validationHint ?? field.validationHint,
319
+ options: field.options?.map((option) => {
320
+ const otr = optionTrByValue.get(option.value);
321
+ return otr ? { ...option, label: otr.label ?? option.label, description: otr.description ?? option.description } : option;
322
+ })
323
+ };
324
+ });
325
+ return {
326
+ ...base,
327
+ title: tr.title ?? base.title,
328
+ description: tr.description ?? base.description,
329
+ submitLabel: tr.submitLabel ?? base.submitLabel,
330
+ fields
331
+ };
332
+ }
333
+
293
334
  // src/core/storage.ts
294
335
  var LocalStorageAdapter = class {
295
336
  get(key) {
@@ -556,7 +597,7 @@ function resolveOptions(rawOpts) {
556
597
  humanInLoop: opts.features?.humanInLoop ?? DEFAULT_FEATURES.humanInLoop,
557
598
  tools: opts.features?.tools
558
599
  },
559
- forms: resolveForms(opts.forms),
600
+ forms: resolveForms(opts.forms, locale),
560
601
  endpoints: {
561
602
  upload: opts.endpoints?.upload ?? DEFAULT_ENDPOINTS.upload,
562
603
  transcribe: opts.endpoints?.transcribe ?? DEFAULT_ENDPOINTS.transcribe,
@@ -679,13 +720,14 @@ function resolveModules(overrides) {
679
720
  const byId = Object.fromEntries(list.map((m) => [m.id, m]));
680
721
  return { list, byId };
681
722
  }
682
- function resolveForms(overrides) {
723
+ function resolveForms(overrides, locale) {
683
724
  var _a;
684
725
  if (!overrides?.length) return DEFAULT_FORMS;
685
726
  const list = [];
686
727
  const seen = /* @__PURE__ */ new Set();
687
- for (const def of overrides) {
688
- if (!def?.id || seen.has(def.id)) continue;
728
+ for (const raw of overrides) {
729
+ if (!raw?.id || seen.has(raw.id)) continue;
730
+ const def = overlayFormDef(raw, locale);
689
731
  const fields = (def.fields ?? []).filter((f) => Boolean(f && f.name && f.label && f.type));
690
732
  const triggers = (Array.isArray(def.on) ? def.on : [def.on]).map(parseTrigger).filter((t) => t !== null);
691
733
  if (fields.length === 0 || triggers.length === 0) continue;
@@ -935,20 +977,6 @@ var REACTIVE_ATTRS = [
935
977
  ...SPECIAL_REACTIVE_ATTRS
936
978
  ];
937
979
 
938
- // src/core/handshake-shape.ts
939
- function isPlainObject(raw) {
940
- return !!raw && typeof raw === "object" && !Array.isArray(raw);
941
- }
942
- function isSiteConfigShape(raw) {
943
- return isPlainObject(raw);
944
- }
945
- function isBlocksConfigShape(raw) {
946
- if (!isPlainObject(raw)) return false;
947
- if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
948
- if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
949
- return true;
950
- }
951
-
952
980
  // src/core/host-commands.ts
953
981
  function commandEventName(cmd) {
954
982
  return `${BRAND.cssPrefix}:${cmd}`;
@@ -969,6 +997,155 @@ function bindHostCommands(host, handlers) {
969
997
  };
970
998
  }
971
999
 
1000
+ // src/core/ids.ts
1001
+ function uuid7() {
1002
+ const ts = Date.now();
1003
+ const bytes = new Uint8Array(16);
1004
+ bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1005
+ bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1006
+ bytes[2] = ts >>> 24 & 255;
1007
+ bytes[3] = ts >>> 16 & 255;
1008
+ bytes[4] = ts >>> 8 & 255;
1009
+ bytes[5] = ts & 255;
1010
+ fillRandom(bytes.subarray(6));
1011
+ bytes[6] = bytes[6] & 15 | 112;
1012
+ bytes[8] = bytes[8] & 63 | 128;
1013
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1014
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1015
+ }
1016
+ function fillRandom(view) {
1017
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1018
+ crypto.getRandomValues(view);
1019
+ return;
1020
+ }
1021
+ for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1022
+ }
1023
+
1024
+ // src/core/persistence.ts
1025
+ var log2 = logger.scope("persistence");
1026
+ function createPersistence(widgetId, storage = defaultStorage) {
1027
+ const prefix = `${BRAND.cssPrefix}.${widgetId}`;
1028
+ const KEY_VISITOR = `${prefix}.visitorId`;
1029
+ const KEY_CONVERSATION = `${prefix}.conversationId`;
1030
+ const KEY_USER_PREFS = `${prefix}.userPrefs`;
1031
+ const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
1032
+ const KEY_PANEL_SIZE = `${prefix}.panelSize`;
1033
+ const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
1034
+ const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
1035
+ const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
1036
+ const KEY_FORMS_DONE = `${prefix}.formsDone`;
1037
+ const persistence = {
1038
+ getVisitorId() {
1039
+ const existing = storage.get(KEY_VISITOR);
1040
+ if (existing) return existing;
1041
+ const minted = uuid7();
1042
+ storage.set(KEY_VISITOR, minted);
1043
+ return minted;
1044
+ },
1045
+ setVisitorId(id) {
1046
+ log2.debug("setVisitorId (rebind)", { id, widgetId });
1047
+ storage.set(KEY_VISITOR, id);
1048
+ },
1049
+ loadConversationId() {
1050
+ return storage.get(KEY_CONVERSATION) ?? void 0;
1051
+ },
1052
+ saveConversationId(id) {
1053
+ log2.trace("saveConversationId", { id, widgetId });
1054
+ if (id) storage.set(KEY_CONVERSATION, id);
1055
+ else storage.remove(KEY_CONVERSATION);
1056
+ },
1057
+ loadUserPrefs() {
1058
+ const raw = storage.get(KEY_USER_PREFS);
1059
+ if (!raw) return {};
1060
+ try {
1061
+ const parsed = JSON.parse(raw);
1062
+ return isPlainObject(parsed) ? parsed : {};
1063
+ } catch (error) {
1064
+ log2.warn("loadUserPrefs parse failed", { error });
1065
+ return {};
1066
+ }
1067
+ },
1068
+ saveUserPrefs(s) {
1069
+ try {
1070
+ storage.set(KEY_USER_PREFS, JSON.stringify(s));
1071
+ } catch (error) {
1072
+ log2.warn("saveUserPrefs serialise failed", { error });
1073
+ }
1074
+ },
1075
+ patchUserPrefs(patch) {
1076
+ const current = persistence.loadUserPrefs();
1077
+ persistence.saveUserPrefs({ ...current, ...patch });
1078
+ },
1079
+ loadPanelOpen() {
1080
+ const raw = storage.get(KEY_PANEL_OPEN);
1081
+ if (raw === "1") return true;
1082
+ if (raw === "0") return false;
1083
+ return null;
1084
+ },
1085
+ savePanelOpen(open) {
1086
+ storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
1087
+ },
1088
+ loadPanelSize() {
1089
+ const raw = storage.get(KEY_PANEL_SIZE);
1090
+ return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
1091
+ },
1092
+ savePanelSize(size) {
1093
+ storage.set(KEY_PANEL_SIZE, size);
1094
+ },
1095
+ loadCalloutDismissed(currentText) {
1096
+ if (!currentText) return false;
1097
+ const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
1098
+ return !!dismissedText && dismissedText === currentText;
1099
+ },
1100
+ saveCalloutDismissed(currentText) {
1101
+ if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
1102
+ else storage.remove(KEY_CALLOUT_DISMISSED);
1103
+ },
1104
+ loadSidebarCollapsed() {
1105
+ const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
1106
+ if (raw === "1") return true;
1107
+ if (raw === "0") return false;
1108
+ return null;
1109
+ },
1110
+ saveSidebarCollapsed(collapsed) {
1111
+ storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
1112
+ },
1113
+ loadActiveModule() {
1114
+ return storage.get(KEY_ACTIVE_MODULE) || null;
1115
+ },
1116
+ saveActiveModule(id) {
1117
+ storage.set(KEY_ACTIVE_MODULE, id);
1118
+ },
1119
+ loadFormsDone() {
1120
+ const raw = storage.get(KEY_FORMS_DONE);
1121
+ if (!raw) return {};
1122
+ try {
1123
+ const parsed = JSON.parse(raw);
1124
+ return isPlainObject(parsed) ? parsed : {};
1125
+ } catch (error) {
1126
+ log2.warn("loadFormsDone parse failed", { error });
1127
+ return {};
1128
+ }
1129
+ },
1130
+ saveFormDone(id) {
1131
+ try {
1132
+ const done = persistence.loadFormsDone();
1133
+ done[id] = Date.now();
1134
+ storage.set(KEY_FORMS_DONE, JSON.stringify(done));
1135
+ } catch (error) {
1136
+ log2.warn("saveFormDone serialise failed", { error });
1137
+ }
1138
+ },
1139
+ clearConversation() {
1140
+ storage.remove(KEY_CONVERSATION);
1141
+ }
1142
+ };
1143
+ return persistence;
1144
+ }
1145
+ function isPlainObject(v) {
1146
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1147
+ }
1148
+
972
1149
  // src/styles/tokens.css
973
1150
  var tokens_default = ':host{--__P__-accent-user: __BRAND_ACCENT__;--__P__-accent: var(--__P__-accent-user);--__P__-accent-text-user: __BRAND_ACCENT_TEXT__;--__P__-accent-text: var(--__P__-accent-text-user);--__P__-radius: __BRAND_RADIUS__;--__P__-radius-sm: 8px;--__P__-radius-md: 10px;--__P__-radius-lg: 16px;--__P__-font: __BRAND_FONT__;--__P__-font-display: var(--__P__-font);--__P__-font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;--__P__-bg: oklch(1 0 0);--__P__-bg-elevated: oklch(.9789 .0029 278.3);--__P__-surface: var(--__P__-bg-elevated);--__P__-muted: var(--__P__-fg-muted);--__P__-hover: oklch(.1686 .011 278.3 / .06);--__P__-fg: oklch(.1686 .011 278.3);--__P__-fg-muted: oklch(.4932 .0301 278.3);--__P__-border: oklch(.9312 .0084 278.3);--__P__-border-strong: oklch(.8757 .0131 278.3);--__P__-bubble-user: var(--__P__-accent);--__P__-bubble-user-text: var(--__P__-accent-text);--__P__-bubble-assistant: oklch(.9638 .0058 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-on-accent: var(--__P__-accent-text);--__P__-success: oklch(.6456 .1654 151.53);--__P__-warning: oklch(.7135 .1494 74.05);--__P__-danger: oklch(.5966 .1818 24.51);--__P__-danger-text: oklch(.5054 .1905 27.52);--__P__-danger-bg: oklch(.5966 .1818 24.51 / .1);--__P__-neutral: oklch(.7137 .0192 278.3);--__P__-focus: var(--__P__-accent);--__P__-shadow-panel: 0 8px 24px -8px rgba(13, 15, 20, .18), 0 24px 64px -24px rgba(13, 15, 20, .24);--__P__-shadow-card: 0 1px 2px rgba(13, 15, 20, .06), 0 4px 12px -6px rgba(13, 15, 20, .12);--__P__-backdrop: oklch(.1686 .011 278.3 / .55);--__P__-shadow-fab: 0 6px 16px -4px color-mix(in oklch, var(--__P__-accent) 45%, transparent), 0 12px 32px -12px rgba(13, 15, 20, .25);--__P__-z-fab: 2147483600;--__P__-z-panel: 2147483601;--__P__-panel-w: 380px;--__P__-panel-h: 600px;--__P__-fab-size: 56px;--__P__-space-1: 4px;--__P__-space-2: 8px;--__P__-space-3: 12px;--__P__-space-4: 16px;--__P__-space-5: 20px;--__P__-space-6: 24px;--__P__-space-8: 32px;--__P__-text-scale: 1;--__P__-text-xs: calc(12px * var(--__P__-text-scale));--__P__-text-sm: calc(13px * var(--__P__-text-scale));--__P__-text-base: calc(15px * var(--__P__-text-scale));--__P__-text-md: calc(16px * var(--__P__-text-scale));--__P__-text-lg: calc(18px * var(--__P__-text-scale));--__P__-text-xl: calc(22px * var(--__P__-text-scale));--__P__-text-2xl: calc(30px * var(--__P__-text-scale))}:host([data-text-size="small"]){--__P__-text-scale: .9}:host([data-text-size="large"]){--__P__-text-scale: 1.15}:host{--__P__-ease: cubic-bezier(.2, .7, .2, 1);--__P__-ease-out: cubic-bezier(.2, .7, .2, 1);--__P__-ease-in: cubic-bezier(.4, 0, 1, 1);--__P__-ease-in-out: cubic-bezier(.65, 0, .35, 1);--__P__-dur-quick: .12s;--__P__-dur-base: .22s;--__P__-dur-slow: .32s;font-family:var(--__P__-font);color:var(--__P__-fg);color-scheme:light;--__P__-color-scheme: light}:host([data-theme="dark"]){--__P__-bg: oklch(.1783 .0128 278.3);--__P__-bg-elevated: oklch(.2232 .0182 278.3);--__P__-fg: oklch(.9368 .01 278.3);--__P__-fg-muted: oklch(.7048 .0296 278.3);--__P__-border: oklch(.2752 .0266 278.3);--__P__-border-strong: oklch(.335 .0291 278.3);--__P__-hover: oklch(.9368 .01 278.3 / .08);--__P__-bubble-assistant: oklch(.2474 .0311 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-danger-text: oklch(.8077 .1035 19.57);--__P__-danger-bg: oklch(.5966 .1818 24.51 / .18);--__P__-shadow-panel: 0 8px 24px -8px rgba(0, 0, 0, .6), 0 24px 64px -24px rgba(0, 0, 0, .7);--__P__-shadow-card: 0 1px 2px rgba(0, 0, 0, .4), 0 4px 12px -6px rgba(0, 0, 0, .5);--__P__-backdrop: rgba(0, 0, 0, .65);color-scheme:dark;--__P__-color-scheme: dark}@media(prefers-color-scheme:dark){:host([data-theme="auto"]){--__P__-bg: oklch(.1783 .0128 278.3);--__P__-bg-elevated: oklch(.2232 .0182 278.3);--__P__-fg: oklch(.9368 .01 278.3);--__P__-fg-muted: oklch(.7048 .0296 278.3);--__P__-border: oklch(.2752 .0266 278.3);--__P__-border-strong: oklch(.335 .0291 278.3);--__P__-hover: oklch(.9368 .01 278.3 / .08);--__P__-bubble-assistant: oklch(.2474 .0311 278.3);--__P__-bubble-assistant-text: var(--__P__-fg);--__P__-shadow-panel: 0 8px 24px -8px rgba(0, 0, 0, .6), 0 24px 64px -24px rgba(0, 0, 0, .7);color-scheme:dark;--__P__-color-scheme: dark}}@supports (color: oklch(from red l c h)){:host{--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.3, l, .62) c h);--__P__-accent-text: oklch(from var(--__P__-accent) clamp(0, (l / .623 - 1) * -infinity, 1) 0 h)}:host([data-theme="dark"]){--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.65, l, .85) c h)}@media(prefers-color-scheme:dark){:host([data-theme="auto"]){--__P__-accent: oklch(from var(--__P__-accent-user) clamp(.65, l, .85) c h)}}}@media(prefers-reduced-motion:reduce){:host{--__P__-dur-quick: 1ms;--__P__-dur-base: 1ms;--__P__-dur-slow: 1ms}}\n';
974
1151
 
@@ -993,7 +1170,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
993
1170
  var CSS_TEXT = [tokens_default, reset_default, panel_default, standalone_default, page_default].join("\n").replace(/__P__/g, PREFIX).replaceAll("__BRAND_ACCENT_TEXT__", ACCENT_TEXT).replaceAll("__BRAND_ACCENT__", ACCENT).replaceAll("__BRAND_RADIUS__", RADIUS).replaceAll("__BRAND_FONT__", FONT);
994
1171
 
995
1172
  // src/shadow/mount.ts
996
- var log2 = logger.scope("mount");
1173
+ var log3 = logger.scope("mount");
997
1174
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
998
1175
  var HOST_CSS = {
999
1176
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1010,7 +1187,7 @@ function hostPositionForMode(mode) {
1010
1187
  }
1011
1188
  function createShadowMount(opts = {}) {
1012
1189
  const position = opts.position ?? "flow";
1013
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1190
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1014
1191
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1015
1192
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1016
1193
  customElements.define(tagName, class extends HTMLElement {
@@ -1094,6 +1271,123 @@ function applyHostAttributes(host, resolved) {
1094
1271
  applySize(host, resolved.size);
1095
1272
  }
1096
1273
 
1274
+ // src/shadow/iframe-mount.ts
1275
+ var log4 = logger.scope("iframe");
1276
+ var IFRAME_SRCDOC = '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;overflow:hidden}</style></head><body></body></html>';
1277
+ function createIframeMount(opts, position, initialMode = "closed", layoutMode = "floating", target = null) {
1278
+ log4.debug("createIframeMount", { position, initialMode, layoutMode, hasTarget: !!target });
1279
+ const iframe = document.createElement("iframe");
1280
+ iframe.title = `${BRAND.name} chat`;
1281
+ iframe.setAttribute("allow", "microphone; clipboard-write");
1282
+ sizeIframe(iframe, position, initialMode, layoutMode);
1283
+ const parent = layoutMode === "inline" && target ? target : document.body;
1284
+ parent.appendChild(iframe);
1285
+ const doc = iframe.contentDocument;
1286
+ if (!doc) throw new Error("iframe contentDocument unavailable (cross-origin?)");
1287
+ doc.open();
1288
+ doc.write(IFRAME_SRCDOC);
1289
+ doc.close();
1290
+ const mount2 = createShadowMount({ ...opts, target: doc.body });
1291
+ const mo = new MutationObserver(() => {
1292
+ sizeIframe(iframe, position, mount2.hostElement.dataset.mode ?? "closed", layoutMode);
1293
+ });
1294
+ mo.observe(mount2.hostElement, { attributes: true, attributeFilter: ["data-mode"] });
1295
+ return {
1296
+ ...mount2,
1297
+ iframeElement: iframe,
1298
+ destroy() {
1299
+ mo.disconnect();
1300
+ mount2.destroy();
1301
+ iframe.remove();
1302
+ }
1303
+ };
1304
+ }
1305
+ var BASE_CSS = "border:0;background:transparent;z-index:2147483647;pointer-events:auto;";
1306
+ var FLOATING_SIZE_BY_MODE = {
1307
+ closed: { w: "88px", h: "88px" },
1308
+ expanded: { w: "min(calc(100vw - 32px), 680px)", h: "min(calc(100vh - 32px), 860px)" },
1309
+ // `open` (normal) — also the fallback for any other non-fullscreen state.
1310
+ open: { w: "min(calc(100vw - 32px), 440px)", h: "min(calc(100vh - 32px), 700px)" }
1311
+ };
1312
+ function sizeIframe(iframe, position, mode, layoutMode) {
1313
+ if (mode === "fullscreen" || layoutMode === "standalone") {
1314
+ iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
1315
+ return;
1316
+ }
1317
+ if (layoutMode === "inline") {
1318
+ iframe.style.cssText = `${BASE_CSS}position:static;display:block;width:100%;height:100%;`;
1319
+ return;
1320
+ }
1321
+ const compact = typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches;
1322
+ if (compact && (mode === "open" || mode === "expanded")) {
1323
+ iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
1324
+ return;
1325
+ }
1326
+ const vKey = position.startsWith("top-") ? "top" : "bottom";
1327
+ const hKey = position.endsWith("-left") ? "left" : "right";
1328
+ const { w, h: h2 } = FLOATING_SIZE_BY_MODE[mode] ?? FLOATING_SIZE_BY_MODE.open;
1329
+ iframe.style.cssText = `${BASE_CSS}position:fixed;${vKey}:16px;${hKey}:16px;width:${w};height:${h2};`;
1330
+ }
1331
+
1332
+ // src/core/events.ts
1333
+ var EventBus = class {
1334
+ constructor() {
1335
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
1336
+ }
1337
+ /**
1338
+ * Register a listener for `name`.
1339
+ * @returns an `off` function that removes this listener.
1340
+ */
1341
+ on(name, fn) {
1342
+ let set = this.handlers.get(name);
1343
+ if (!set) {
1344
+ set = /* @__PURE__ */ new Set();
1345
+ this.handlers.set(name, set);
1346
+ }
1347
+ set.add(fn);
1348
+ return () => this.handlers.get(name)?.delete(fn);
1349
+ }
1350
+ /**
1351
+ * Dispatch an event synchronously to all listeners.
1352
+ * Listener errors are swallowed — the widget must not crash on user callbacks.
1353
+ */
1354
+ emit(name, payload) {
1355
+ const set = this.handlers.get(name);
1356
+ if (!set) return;
1357
+ for (const fn of set) {
1358
+ try {
1359
+ fn(payload);
1360
+ } catch {
1361
+ }
1362
+ }
1363
+ }
1364
+ /** Remove every listener. Called by the instance `destroy()`. */
1365
+ clear() {
1366
+ this.handlers.clear();
1367
+ }
1368
+ };
1369
+
1370
+ // src/core/boot.ts
1371
+ import { h, render as renderPreact } from "preact";
1372
+
1373
+ // src/ui/app.tsx
1374
+ import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
1375
+ import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
1376
+
1377
+ // src/core/handshake-shape.ts
1378
+ function isPlainObject2(raw) {
1379
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
1380
+ }
1381
+ function isSiteConfigShape(raw) {
1382
+ return isPlainObject2(raw);
1383
+ }
1384
+ function isBlocksConfigShape(raw) {
1385
+ if (!isPlainObject2(raw)) return false;
1386
+ if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
1387
+ if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
1388
+ return true;
1389
+ }
1390
+
1097
1391
  // src/ui/host-mode.ts
1098
1392
  function computeHostMode(configured, panelSize, isOpen) {
1099
1393
  if (configured === "page") return "page";
@@ -1144,30 +1438,6 @@ function createAuth(opts) {
1144
1438
  };
1145
1439
  }
1146
1440
 
1147
- // src/core/ids.ts
1148
- function uuid7() {
1149
- const ts = Date.now();
1150
- const bytes = new Uint8Array(16);
1151
- bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1152
- bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1153
- bytes[2] = ts >>> 24 & 255;
1154
- bytes[3] = ts >>> 16 & 255;
1155
- bytes[4] = ts >>> 8 & 255;
1156
- bytes[5] = ts & 255;
1157
- fillRandom(bytes.subarray(6));
1158
- bytes[6] = bytes[6] & 15 | 112;
1159
- bytes[8] = bytes[8] & 63 | 128;
1160
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1161
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1162
- }
1163
- function fillRandom(view) {
1164
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1165
- crypto.getRandomValues(view);
1166
- return;
1167
- }
1168
- for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1169
- }
1170
-
1171
1441
  // src/stream/types.ts
1172
1442
  var StreamError = class extends Error {
1173
1443
  constructor(message, code, status, cause) {
@@ -1180,7 +1450,7 @@ var StreamError = class extends Error {
1180
1450
  };
1181
1451
 
1182
1452
  // src/stream/parser.ts
1183
- var log3 = logger.scope("parser");
1453
+ var log5 = logger.scope("parser");
1184
1454
  async function* parseChatStream(response, signal7) {
1185
1455
  if (!response.ok) {
1186
1456
  const text = await response.text().catch(() => "");
@@ -1232,10 +1502,10 @@ ${part}` : part;
1232
1502
  if (!dataPayload || dataPayload === "[DONE]") return null;
1233
1503
  try {
1234
1504
  const chunk = JSON.parse(dataPayload);
1235
- log3.trace("chunk", { type: chunk.type, eventId });
1505
+ log5.trace("chunk", { type: chunk.type, eventId });
1236
1506
  return { chunk, eventId };
1237
1507
  } catch (err) {
1238
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1508
+ log5.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1239
1509
  return null;
1240
1510
  }
1241
1511
  }
@@ -1390,7 +1660,7 @@ function messageToWireParts(m) {
1390
1660
  }
1391
1661
 
1392
1662
  // src/stream/transport.ts
1393
- var log4 = logger.scope("transport");
1663
+ var log6 = logger.scope("transport");
1394
1664
  var MAX_RESUME_ATTEMPTS = 3;
1395
1665
  var RESUME_BACKOFF_MS = 400;
1396
1666
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1516,13 +1786,13 @@ var AgentTransport = class {
1516
1786
  }
1517
1787
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1518
1788
  async handshake(body) {
1519
- log4.debug("handshake \u2192", {
1789
+ log6.debug("handshake \u2192", {
1520
1790
  visitorId: body.visitorId,
1521
1791
  conversationId: body.conversationId,
1522
1792
  locale: body.locale
1523
1793
  });
1524
1794
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1525
- log4.debug("handshake \u2190", {
1795
+ log6.debug("handshake \u2190", {
1526
1796
  visitorId: res.visitorId,
1527
1797
  conversationId: res.conversationId,
1528
1798
  hasWelcome: !!res.welcome,
@@ -1530,7 +1800,7 @@ var AgentTransport = class {
1530
1800
  rebind: res.rebind
1531
1801
  });
1532
1802
  if (!isHandshakeResponseShape(res)) {
1533
- log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1803
+ log6.warn("handshake response did not match expected shape; using raw response", { response: res });
1534
1804
  }
1535
1805
  this.visitorId = res.visitorId ?? body.visitorId;
1536
1806
  this.conversationId = res.conversationId;
@@ -1538,11 +1808,11 @@ var AgentTransport = class {
1538
1808
  }
1539
1809
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1540
1810
  async uploadFile(file) {
1541
- log4.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1811
+ log6.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1542
1812
  const fd = new FormData();
1543
1813
  fd.append("file", file, file.name);
1544
1814
  const res = await this.postForm(this.uploadPath, fd, "upload");
1545
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1815
+ log6.debug("uploadFile \u2190", { id: res.id, url: res.url });
1546
1816
  return res;
1547
1817
  }
1548
1818
  /**
@@ -1550,12 +1820,12 @@ var AgentTransport = class {
1550
1820
  * Only used when `features.voice === 'server'`.
1551
1821
  */
1552
1822
  async transcribe(audio, mimeType = "audio/webm") {
1553
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1823
+ log6.debug("transcribe \u2192", { size: audio.size, mimeType });
1554
1824
  const fd = new FormData();
1555
1825
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1556
1826
  fd.append("mimeType", mimeType);
1557
1827
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1558
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1828
+ log6.debug("transcribe \u2190", { textLen: res.text.length });
1559
1829
  return res;
1560
1830
  }
1561
1831
  async listConversations(params = {}) {
@@ -1563,14 +1833,14 @@ var AgentTransport = class {
1563
1833
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1564
1834
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1565
1835
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1566
- log4.debug("listConversations \u2192", {
1836
+ log6.debug("listConversations \u2192", {
1567
1837
  visitorId: params.visitorId ?? this.visitorId,
1568
1838
  limit: params.limit,
1569
1839
  cursor: params.cursor
1570
1840
  });
1571
1841
  const res = await this.getJson(url.toString(), "listConversations");
1572
1842
  const conversations = res.conversations ?? [];
1573
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1843
+ log6.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1574
1844
  return {
1575
1845
  conversations: conversations.map((conversation) => ({
1576
1846
  conversationId: conversation.conversationId,
@@ -1586,10 +1856,10 @@ var AgentTransport = class {
1586
1856
  async loadConversation(conversationId) {
1587
1857
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1588
1858
  url.searchParams.set("conversationId", conversationId);
1589
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1859
+ log6.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1590
1860
  const res = await this.getJson(url.toString(), "loadConversation");
1591
1861
  const messages = res.messages ?? [];
1592
- log4.debug("loadConversation \u2190", {
1862
+ log6.debug("loadConversation \u2190", {
1593
1863
  conversationId: res.conversationId,
1594
1864
  canContinue: res.canContinue,
1595
1865
  messageCount: messages.length,
@@ -1611,19 +1881,19 @@ var AgentTransport = class {
1611
1881
  */
1612
1882
  async markRead(conversationId) {
1613
1883
  if (!this.visitorId || !conversationId) return;
1614
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1884
+ log6.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1615
1885
  try {
1616
1886
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1617
1887
  } catch (err) {
1618
- log4.debug("markRead failed (non-fatal)", { err });
1888
+ log6.debug("markRead failed (non-fatal)", { err });
1619
1889
  }
1620
1890
  }
1621
1891
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
1622
1892
  listContent(query = {}) {
1623
- const key = JSON.stringify(query, Object.keys(query).toSorted());
1893
+ const key = `${this.locale ?? ""}|${JSON.stringify(query, Object.keys(query).toSorted())}`;
1624
1894
  const cached = this.contentCache.get(key);
1625
1895
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1626
- log4.debug("listContent \u2192 cache", query);
1896
+ log6.debug("listContent \u2192 cache", query);
1627
1897
  return cached.result;
1628
1898
  }
1629
1899
  const result = this.fetchContent(query).catch((err) => {
@@ -1639,10 +1909,10 @@ var AgentTransport = class {
1639
1909
  if (value === void 0) continue;
1640
1910
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1641
1911
  }
1642
- log4.debug("listContent \u2192", query);
1912
+ log6.debug("listContent \u2192", query);
1643
1913
  const res = await this.getJson(url.toString(), "listContent");
1644
- const items = res.items ?? [];
1645
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1914
+ const items = (res.items ?? []).map((item) => overlayContent(item, this.locale));
1915
+ log6.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1646
1916
  return { ...res, items };
1647
1917
  }
1648
1918
  /**
@@ -1660,21 +1930,21 @@ var AgentTransport = class {
1660
1930
  async bootstrap(conversationId) {
1661
1931
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1662
1932
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1663
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1933
+ log6.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1664
1934
  const res = await this.getJson(url.toString(), "bootstrap");
1665
1935
  const forms = res.forms ?? [];
1666
1936
  const formSubmissions = res.formSubmissions ?? [];
1667
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1937
+ log6.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1668
1938
  return { forms, formSubmissions };
1669
1939
  }
1670
1940
  async saveUserPrefs(userPrefs) {
1671
- log4.debug("saveUserPrefs \u2192", {
1941
+ log6.debug("saveUserPrefs \u2192", {
1672
1942
  visitorId: this.visitorId,
1673
1943
  conversationId: this.conversationId,
1674
1944
  settingsKeys: Object.keys(userPrefs)
1675
1945
  });
1676
1946
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1677
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1947
+ log6.debug("saveUserPrefs \u2190", { ok: res.ok });
1678
1948
  return res;
1679
1949
  }
1680
1950
  get uploadPath() {
@@ -1695,15 +1965,15 @@ var AgentTransport = class {
1695
1965
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1696
1966
  */
1697
1967
  async submitForm(body) {
1698
- log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1968
+ log6.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1699
1969
  try {
1700
1970
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1701
1971
  } catch (err) {
1702
- log4.debug("submitForm failed (non-fatal)", { err });
1972
+ log6.debug("submitForm failed (non-fatal)", { err });
1703
1973
  }
1704
1974
  }
1705
1975
  sendMessage(body) {
1706
- log4.debug("message \u2192", {
1976
+ log6.debug("message \u2192", {
1707
1977
  conversationId: body.conversationId,
1708
1978
  visitorId: body.visitorId,
1709
1979
  messageCount: body.messages.length,
@@ -1783,17 +2053,17 @@ var AgentTransport = class {
1783
2053
  });
1784
2054
  } catch (err) {
1785
2055
  if (ctrl.signal.aborted) return;
1786
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
2056
+ log6.debug("resume fetch failed \u2014 retrying", { attempt, err });
1787
2057
  continue;
1788
2058
  }
1789
2059
  if (res.status === 204 || !res.ok) {
1790
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
2060
+ log6.debug("resume \u2192 no in-flight stream", { status: res.status });
1791
2061
  return;
1792
2062
  }
1793
2063
  const before = seenIds.size;
1794
2064
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1795
2065
  if (seenIds.size === before) {
1796
- log4.debug("resume \u2192 stream closed with no new events; nothing to resume");
2066
+ log6.debug("resume \u2192 stream closed with no new events; nothing to resume");
1797
2067
  return;
1798
2068
  }
1799
2069
  }
@@ -1820,14 +2090,14 @@ var AgentTransport = class {
1820
2090
  } catch (err) {
1821
2091
  if (ctrl.signal.aborted) return true;
1822
2092
  if (err instanceof StreamError && err.status !== void 0) throw err;
1823
- log4.debug("stream segment dropped", { err });
2093
+ log6.debug("stream segment dropped", { err });
1824
2094
  }
1825
2095
  return false;
1826
2096
  }
1827
2097
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1828
2098
  cancelStream(ctrl, conversationId) {
1829
2099
  if (ctrl.signal.aborted) return;
1830
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2100
+ log6.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1831
2101
  ctrl.abort();
1832
2102
  if (!this.visitorId || !conversationId) return;
1833
2103
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1836,7 +2106,7 @@ var AgentTransport = class {
1836
2106
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1837
2107
  body: JSON.stringify(this.withEnvelope({})),
1838
2108
  keepalive: true
1839
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2109
+ }).catch((err) => log6.debug("cancel POST failed (non-fatal)", { err }));
1840
2110
  }
1841
2111
  // ---- Low-level fetch helpers ------------------------------------------
1842
2112
  async *openMessageStream(body, signal7) {
@@ -1918,13 +2188,13 @@ var AgentTransport = class {
1918
2188
  if (attempt >= MAX_REQUEST_RETRIES) {
1919
2189
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1920
2190
  }
1921
- log4.debug("request network error \u2014 retrying", { label, attempt });
2191
+ log6.debug("request network error \u2014 retrying", { label, attempt });
1922
2192
  await sleep(backoffMs(attempt));
1923
2193
  continue;
1924
2194
  }
1925
2195
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1926
2196
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1927
- log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
2197
+ log6.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
1928
2198
  await sleep(wait);
1929
2199
  }
1930
2200
  }
@@ -2119,7 +2389,7 @@ var TRIGGER = {
2119
2389
  };
2120
2390
 
2121
2391
  // src/stream/reducer.ts
2122
- var log5 = logger.scope("reducer");
2392
+ var log7 = logger.scope("reducer");
2123
2393
  var StreamReducer = class {
2124
2394
  constructor(messagesSig) {
2125
2395
  __publicField(this, "messagesSig", messagesSig);
@@ -2141,13 +2411,13 @@ var StreamReducer = class {
2141
2411
  case "finish-step":
2142
2412
  return;
2143
2413
  case "finish":
2144
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2414
+ log7.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2145
2415
  m.status = "complete";
2146
2416
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2147
2417
  this.bumpDone(m);
2148
2418
  return;
2149
2419
  case "error":
2150
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2420
+ log7.warn("stream error chunk", { errorText: chunk.errorText });
2151
2421
  m.status = "error";
2152
2422
  m.errorText = chunk.errorText;
2153
2423
  this.bumpDone(m);
@@ -2191,221 +2461,96 @@ var StreamReducer = class {
2191
2461
  appendPart(m, { kind: "source", sourceId: chunk.sourceId, url: chunk.url, title: chunk.title });
2192
2462
  return;
2193
2463
  }
2194
- case "data-notification":
2195
- return;
2196
- default: {
2197
- const _exhaustive = chunk;
2198
- void _exhaustive;
2199
- }
2200
- }
2201
- }
2202
- /**
2203
- * Terminal-state bump (`finish`/`error`). Touches the message's own `partsSig`
2204
- * AND the list so subscribers re-render: a buffered bubble (subscribed to its
2205
- * parts) flips out of the loading indicator into the finished reply, and the
2206
- * list reflects the new `status`.
2207
- */
2208
- bumpDone(m) {
2209
- m.partsSig.value = [...m.partsSig.value];
2210
- this.messagesSig.value = [...this.messagesSig.value];
2211
- }
2212
- };
2213
- function ensureTextPart(m, kind, id) {
2214
- const existing = m.partsSig.value.find((p33) => p33.kind === kind && p33.id === id);
2215
- if (existing) return existing;
2216
- const part = { kind, id, textSig: signal2(""), doneSig: signal2(false) };
2217
- appendPart(m, part);
2218
- return part;
2219
- }
2220
- function ensureToolPart(m, toolCallId, toolName) {
2221
- const existing = m.partsSig.value.find((p33) => p33.kind === "tool" && p33.toolCallId === toolCallId);
2222
- if (existing) return existing;
2223
- const part = {
2224
- kind: "tool",
2225
- toolCallId,
2226
- toolName: toolName ?? "tool",
2227
- inputPartialSig: signal2(""),
2228
- inputSig: signal2(void 0),
2229
- outputSig: signal2(void 0),
2230
- errorSig: signal2(void 0),
2231
- stateSig: signal2("input"),
2232
- approvalSig: signal2(void 0)
2233
- };
2234
- appendPart(m, part);
2235
- return part;
2236
- }
2237
- function appendPart(m, part) {
2238
- m.partsSig.value = [...m.partsSig.value, part];
2239
- }
2240
- function applyTool(m, chunk) {
2241
- switch (chunk.type) {
2242
- case "tool-input-start":
2243
- ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2244
- return;
2245
- case "tool-input-delta": {
2246
- const p33 = ensureToolPart(m, chunk.toolCallId);
2247
- p33.inputPartialSig.value += chunk.delta;
2248
- return;
2249
- }
2250
- case "tool-input-available": {
2251
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2252
- p33.inputSig.value = chunk.input;
2253
- p33.stateSig.value = isAskUserInputTool(p33.toolName) ? "awaiting-input" : "awaiting";
2254
- return;
2255
- }
2256
- case "tool-approval-request": {
2257
- const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2258
- p33.approvalSig.value = { id: chunk.approvalId };
2259
- p33.stateSig.value = "awaiting-approval";
2260
- return;
2261
- }
2262
- case "tool-output-available": {
2263
- const p33 = ensureToolPart(m, chunk.toolCallId);
2264
- p33.outputSig.value = chunk.output;
2265
- p33.stateSig.value = "output";
2266
- return;
2267
- }
2268
- case "tool-output-error": {
2269
- const p33 = ensureToolPart(m, chunk.toolCallId);
2270
- p33.errorSig.value = chunk.errorText;
2271
- p33.stateSig.value = "error";
2272
- return;
2273
- }
2274
- default:
2275
- return;
2276
- }
2277
- }
2278
-
2279
- // src/core/persistence.ts
2280
- var log6 = logger.scope("persistence");
2281
- function createPersistence(widgetId, storage = defaultStorage) {
2282
- const prefix = `${BRAND.cssPrefix}.${widgetId}`;
2283
- const KEY_VISITOR = `${prefix}.visitorId`;
2284
- const KEY_CONVERSATION = `${prefix}.conversationId`;
2285
- const KEY_USER_PREFS = `${prefix}.userPrefs`;
2286
- const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
2287
- const KEY_PANEL_SIZE = `${prefix}.panelSize`;
2288
- const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
2289
- const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
2290
- const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
2291
- const KEY_FORMS_DONE = `${prefix}.formsDone`;
2292
- const persistence = {
2293
- getVisitorId() {
2294
- const existing = storage.get(KEY_VISITOR);
2295
- if (existing) return existing;
2296
- const minted = uuid7();
2297
- storage.set(KEY_VISITOR, minted);
2298
- return minted;
2299
- },
2300
- setVisitorId(id) {
2301
- log6.debug("setVisitorId (rebind)", { id, widgetId });
2302
- storage.set(KEY_VISITOR, id);
2303
- },
2304
- loadConversationId() {
2305
- return storage.get(KEY_CONVERSATION) ?? void 0;
2306
- },
2307
- saveConversationId(id) {
2308
- log6.trace("saveConversationId", { id, widgetId });
2309
- if (id) storage.set(KEY_CONVERSATION, id);
2310
- else storage.remove(KEY_CONVERSATION);
2311
- },
2312
- loadUserPrefs() {
2313
- const raw = storage.get(KEY_USER_PREFS);
2314
- if (!raw) return {};
2315
- try {
2316
- const parsed = JSON.parse(raw);
2317
- return isPlainObject2(parsed) ? parsed : {};
2318
- } catch (error) {
2319
- log6.warn("loadUserPrefs parse failed", { error });
2320
- return {};
2321
- }
2322
- },
2323
- saveUserPrefs(s) {
2324
- try {
2325
- storage.set(KEY_USER_PREFS, JSON.stringify(s));
2326
- } catch (error) {
2327
- log6.warn("saveUserPrefs serialise failed", { error });
2328
- }
2329
- },
2330
- patchUserPrefs(patch) {
2331
- const current = persistence.loadUserPrefs();
2332
- persistence.saveUserPrefs({ ...current, ...patch });
2333
- },
2334
- loadPanelOpen() {
2335
- const raw = storage.get(KEY_PANEL_OPEN);
2336
- if (raw === "1") return true;
2337
- if (raw === "0") return false;
2338
- return null;
2339
- },
2340
- savePanelOpen(open) {
2341
- storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
2342
- },
2343
- loadPanelSize() {
2344
- const raw = storage.get(KEY_PANEL_SIZE);
2345
- return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
2346
- },
2347
- savePanelSize(size) {
2348
- storage.set(KEY_PANEL_SIZE, size);
2349
- },
2350
- loadCalloutDismissed(currentText) {
2351
- if (!currentText) return false;
2352
- const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
2353
- return !!dismissedText && dismissedText === currentText;
2354
- },
2355
- saveCalloutDismissed(currentText) {
2356
- if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
2357
- else storage.remove(KEY_CALLOUT_DISMISSED);
2358
- },
2359
- loadSidebarCollapsed() {
2360
- const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
2361
- if (raw === "1") return true;
2362
- if (raw === "0") return false;
2363
- return null;
2364
- },
2365
- saveSidebarCollapsed(collapsed) {
2366
- storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
2367
- },
2368
- loadActiveModule() {
2369
- return storage.get(KEY_ACTIVE_MODULE) || null;
2370
- },
2371
- saveActiveModule(id) {
2372
- storage.set(KEY_ACTIVE_MODULE, id);
2373
- },
2374
- loadFormsDone() {
2375
- const raw = storage.get(KEY_FORMS_DONE);
2376
- if (!raw) return {};
2377
- try {
2378
- const parsed = JSON.parse(raw);
2379
- return isPlainObject2(parsed) ? parsed : {};
2380
- } catch (error) {
2381
- log6.warn("loadFormsDone parse failed", { error });
2382
- return {};
2383
- }
2384
- },
2385
- saveFormDone(id) {
2386
- try {
2387
- const done = persistence.loadFormsDone();
2388
- done[id] = Date.now();
2389
- storage.set(KEY_FORMS_DONE, JSON.stringify(done));
2390
- } catch (error) {
2391
- log6.warn("saveFormDone serialise failed", { error });
2464
+ case "data-notification":
2465
+ return;
2466
+ default: {
2467
+ const _exhaustive = chunk;
2468
+ void _exhaustive;
2392
2469
  }
2393
- },
2394
- clearConversation() {
2395
- storage.remove(KEY_CONVERSATION);
2396
2470
  }
2471
+ }
2472
+ /**
2473
+ * Terminal-state bump (`finish`/`error`). Touches the message's own `partsSig`
2474
+ * AND the list so subscribers re-render: a buffered bubble (subscribed to its
2475
+ * parts) flips out of the loading indicator into the finished reply, and the
2476
+ * list reflects the new `status`.
2477
+ */
2478
+ bumpDone(m) {
2479
+ m.partsSig.value = [...m.partsSig.value];
2480
+ this.messagesSig.value = [...this.messagesSig.value];
2481
+ }
2482
+ };
2483
+ function ensureTextPart(m, kind, id) {
2484
+ const existing = m.partsSig.value.find((p33) => p33.kind === kind && p33.id === id);
2485
+ if (existing) return existing;
2486
+ const part = { kind, id, textSig: signal2(""), doneSig: signal2(false) };
2487
+ appendPart(m, part);
2488
+ return part;
2489
+ }
2490
+ function ensureToolPart(m, toolCallId, toolName) {
2491
+ const existing = m.partsSig.value.find((p33) => p33.kind === "tool" && p33.toolCallId === toolCallId);
2492
+ if (existing) return existing;
2493
+ const part = {
2494
+ kind: "tool",
2495
+ toolCallId,
2496
+ toolName: toolName ?? "tool",
2497
+ inputPartialSig: signal2(""),
2498
+ inputSig: signal2(void 0),
2499
+ outputSig: signal2(void 0),
2500
+ errorSig: signal2(void 0),
2501
+ stateSig: signal2("input"),
2502
+ approvalSig: signal2(void 0)
2397
2503
  };
2398
- return persistence;
2504
+ appendPart(m, part);
2505
+ return part;
2399
2506
  }
2400
- function isPlainObject2(v) {
2401
- return typeof v === "object" && v !== null && !Array.isArray(v);
2507
+ function appendPart(m, part) {
2508
+ m.partsSig.value = [...m.partsSig.value, part];
2509
+ }
2510
+ function applyTool(m, chunk) {
2511
+ switch (chunk.type) {
2512
+ case "tool-input-start":
2513
+ ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2514
+ return;
2515
+ case "tool-input-delta": {
2516
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2517
+ p33.inputPartialSig.value += chunk.delta;
2518
+ return;
2519
+ }
2520
+ case "tool-input-available": {
2521
+ const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2522
+ p33.inputSig.value = chunk.input;
2523
+ p33.stateSig.value = isAskUserInputTool(p33.toolName) ? "awaiting-input" : "awaiting";
2524
+ return;
2525
+ }
2526
+ case "tool-approval-request": {
2527
+ const p33 = ensureToolPart(m, chunk.toolCallId, chunk.toolName);
2528
+ p33.approvalSig.value = { id: chunk.approvalId };
2529
+ p33.stateSig.value = "awaiting-approval";
2530
+ return;
2531
+ }
2532
+ case "tool-output-available": {
2533
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2534
+ p33.outputSig.value = chunk.output;
2535
+ p33.stateSig.value = "output";
2536
+ return;
2537
+ }
2538
+ case "tool-output-error": {
2539
+ const p33 = ensureToolPart(m, chunk.toolCallId);
2540
+ p33.errorSig.value = chunk.errorText;
2541
+ p33.stateSig.value = "error";
2542
+ return;
2543
+ }
2544
+ default:
2545
+ return;
2546
+ }
2402
2547
  }
2403
2548
 
2404
2549
  // src/core/feedback/index.ts
2405
2550
  import { signal as signal3 } from "@preact/signals";
2406
2551
 
2407
2552
  // src/core/feedback/audio.ts
2408
- var log7 = logger.scope("feedback:audio");
2553
+ var log8 = logger.scope("feedback:audio");
2409
2554
  var AudioEngine = class {
2410
2555
  constructor() {
2411
2556
  __publicField(this, "ctx", null);
@@ -2426,7 +2571,7 @@ var AudioEngine = class {
2426
2571
  this.master.gain.value = 1;
2427
2572
  this.master.connect(this.ctx.destination);
2428
2573
  } catch (err) {
2429
- log7.warn("AudioContext init failed", { err });
2574
+ log8.warn("AudioContext init failed", { err });
2430
2575
  }
2431
2576
  }
2432
2577
  /** Route all sounds through this multiplier. Call on volume change. */
@@ -2483,7 +2628,7 @@ var AudioEngine = class {
2483
2628
  this.buffers.set(url, buf);
2484
2629
  return buf;
2485
2630
  }).catch((err) => {
2486
- log7.warn("audio fetch/decode failed", { url, err });
2631
+ log8.warn("audio fetch/decode failed", { url, err });
2487
2632
  this.buffers.delete(url);
2488
2633
  return null;
2489
2634
  });
@@ -2569,7 +2714,7 @@ function acquireHaptics() {
2569
2714
  }
2570
2715
 
2571
2716
  // src/core/feedback/index.ts
2572
- var log8 = logger.scope("feedback");
2717
+ var log9 = logger.scope("feedback");
2573
2718
  var DEDUP_WINDOW_MS = 100;
2574
2719
  var FeedbackBus = class {
2575
2720
  constructor(sound, haptics) {
@@ -2647,7 +2792,7 @@ var FeedbackBus = class {
2647
2792
  const override = this.sound.events?.[event];
2648
2793
  if (override === false) return;
2649
2794
  if (typeof override === "string") {
2650
- this.audio.playUrl(override).catch((err) => log8.warn("audio play failed", { err, url: override }));
2795
+ this.audio.playUrl(override).catch((err) => log9.warn("audio play failed", { err, url: override }));
2651
2796
  return;
2652
2797
  }
2653
2798
  const tone = DEFAULT_TONES[event];
@@ -3026,7 +3171,7 @@ function mountRawSvg(host, source) {
3026
3171
  } catch {
3027
3172
  }
3028
3173
  }
3029
- function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3174
+ function LauncherCallout({ callout, launcherPosition, strings, closeLabel, onDismiss }) {
3030
3175
  const [visible, setVisible] = useState(false);
3031
3176
  useEffect(() => {
3032
3177
  const id = setTimeout(() => setVisible(true), callout.delayMs);
@@ -3048,7 +3193,7 @@ function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3048
3193
  "data-animated": callout.animated ? "true" : void 0,
3049
3194
  "data-testid": TID.callout,
3050
3195
  children: [
3051
- /* @__PURE__ */ jsx2("span", { children: callout.text }),
3196
+ /* @__PURE__ */ jsx2("span", { children: localizeText(strings, callout.text) }),
3052
3197
  /* @__PURE__ */ jsx2(
3053
3198
  "button",
3054
3199
  {
@@ -3292,7 +3437,7 @@ function AgentBadge({ agent }) {
3292
3437
  import { useEffect as useEffect4, useRef as useRef2, useState as useState2 } from "preact/hooks";
3293
3438
 
3294
3439
  // src/core/attachments.ts
3295
- var log9 = logger.scope("attachments");
3440
+ var log10 = logger.scope("attachments");
3296
3441
  function ingest(source, existing, limits) {
3297
3442
  const incoming = collectFiles(source);
3298
3443
  const accepted = [];
@@ -3311,7 +3456,7 @@ function ingest(source, existing, limits) {
3311
3456
  totalCount += 1;
3312
3457
  }
3313
3458
  if (accepted.length || rejected.length) {
3314
- log9.debug("ingest", {
3459
+ log10.debug("ingest", {
3315
3460
  accepted: accepted.length,
3316
3461
  rejected: rejected.map((r) => ({ name: r.file.name, reason: r.reason }))
3317
3462
  });
@@ -3322,7 +3467,7 @@ function revoke(att) {
3322
3467
  try {
3323
3468
  URL.revokeObjectURL(att.previewUrl);
3324
3469
  } catch (error) {
3325
- log9.debug("revokeObjectURL failed", { error });
3470
+ log10.debug("revokeObjectURL failed", { error });
3326
3471
  }
3327
3472
  }
3328
3473
  function reject(file, count, bytes, limits, accept) {
@@ -3498,7 +3643,7 @@ function pickSupportedMimeType() {
3498
3643
  // src/ui/composer.tsx
3499
3644
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
3500
3645
  var p7 = BRAND.cssPrefix;
3501
- var log10 = logger.scope("composer");
3646
+ var log11 = logger.scope("composer");
3502
3647
  function totalBytes(items) {
3503
3648
  return items.reduce((acc, a) => acc + a.size, 0);
3504
3649
  }
@@ -3551,7 +3696,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3551
3696
  attachFromDrop: (items) => {
3552
3697
  const result = ingest(items, attsRef.current, options.attachments);
3553
3698
  if (result.accepted.length === 0) return;
3554
- log10.info("attach (drop)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3699
+ log11.info("attach (drop)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3555
3700
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3556
3701
  setAtts((prev) => [...prev, ...result.accepted]);
3557
3702
  }
@@ -3572,7 +3717,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3572
3717
  const result = ingest(data.items, attsRef.current, options.attachments);
3573
3718
  if (result.accepted.length > 0) {
3574
3719
  e.preventDefault();
3575
- log10.info("attach (paste)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3720
+ log11.info("attach (paste)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3576
3721
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3577
3722
  setAtts((prev) => [...prev, ...result.accepted]);
3578
3723
  return;
@@ -3606,14 +3751,14 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3606
3751
  const input = e.target;
3607
3752
  const result = ingest(input.files, atts, options.attachments);
3608
3753
  if (result.accepted.length > 0) {
3609
- log10.info("attach (file picker)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3754
+ log11.info("attach (file picker)", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3610
3755
  bus.emit("attach", { count: result.accepted.length, totalBytes: totalBytes(result.accepted) });
3611
3756
  setAtts((prev) => [...prev, ...result.accepted]);
3612
3757
  }
3613
3758
  input.value = "";
3614
3759
  };
3615
3760
  const removeAtt = (id) => {
3616
- log10.info("removeAttachment", { id });
3761
+ log11.info("removeAttachment", { id });
3617
3762
  bus.emit("removeAttachment", { id });
3618
3763
  setAtts((prev) => {
3619
3764
  const target = prev.find((a) => a.id === id);
@@ -3626,7 +3771,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3626
3771
  if (!voice.isSupported) return;
3627
3772
  if (voiceOn) {
3628
3773
  const durationMs = voiceStartedAtRef.current ? Date.now() - voiceStartedAtRef.current : 0;
3629
- log10.info("voiceStop", { durationMs });
3774
+ log11.info("voiceStop", { durationMs });
3630
3775
  bus.emit("voiceStop", { durationMs });
3631
3776
  voice.stop();
3632
3777
  setVoiceOn(false);
@@ -3635,7 +3780,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3635
3780
  return;
3636
3781
  }
3637
3782
  voiceStartedAtRef.current = Date.now();
3638
- log10.info("voiceStart");
3783
+ log11.info("voiceStart");
3639
3784
  bus.emit("voiceStart", void 0);
3640
3785
  setVoiceOn(true);
3641
3786
  feedback.play("voiceStart");
@@ -3645,7 +3790,7 @@ function Composer({ options, transport, feedback, bus, isStreaming, onSend, onSt
3645
3790
  setVoiceOn(false);
3646
3791
  if (voiceStartedAtRef.current) {
3647
3792
  const durationMs = Date.now() - voiceStartedAtRef.current;
3648
- log10.debug("voiceStop (auto)", { durationMs });
3793
+ log11.debug("voiceStop (auto)", { durationMs });
3649
3794
  bus.emit("voiceStop", { durationMs });
3650
3795
  voiceStartedAtRef.current = null;
3651
3796
  }
@@ -5206,7 +5351,7 @@ function startOfDay(ms) {
5206
5351
 
5207
5352
  // src/ui/conversation-list.tsx
5208
5353
  import { Fragment as Fragment3, jsx as jsx18, jsxs as jsxs15 } from "preact/jsx-runtime";
5209
- var log11 = logger.scope("history");
5354
+ var log12 = logger.scope("history");
5210
5355
  var BUCKET_TO_STRING = {
5211
5356
  today: "dateToday",
5212
5357
  yesterday: "dateYesterday",
@@ -5239,7 +5384,7 @@ function ConversationList({
5239
5384
  setState("loaded");
5240
5385
  }).catch((err) => {
5241
5386
  if (cancelled) return;
5242
- log11.warn("listConversations failed", { err });
5387
+ log12.warn("listConversations failed", { err });
5243
5388
  setState("error");
5244
5389
  });
5245
5390
  return () => {
@@ -5608,9 +5753,16 @@ function createController(depsRef) {
5608
5753
  else if (form.frequency !== "always") depsRef.current.persistence.saveFormDone(form.id);
5609
5754
  depsRef.current.onComplete(form, trigger, values, skipped);
5610
5755
  };
5756
+ const refresh = () => {
5757
+ const active = activeForm.value;
5758
+ if (!active) return;
5759
+ const next = depsRef.current.forms.list.find((f) => f.id === active.form.id);
5760
+ if (next && next !== active.form) activeForm.value = { form: next, trigger: active.trigger };
5761
+ };
5611
5762
  return {
5612
5763
  activeForm,
5613
5764
  fire,
5765
+ refresh,
5614
5766
  complete: (values) => finish(values, false),
5615
5767
  skip: () => finish({}, true)
5616
5768
  };
@@ -5882,7 +6034,7 @@ function ModuleState({
5882
6034
  // src/ui/modules/help.tsx
5883
6035
  import { jsx as jsx27, jsxs as jsxs23 } from "preact/jsx-runtime";
5884
6036
  var p23 = BRAND.cssPrefix;
5885
- var log12 = logger.scope("help");
6037
+ var log13 = logger.scope("help");
5886
6038
  var openArticle = (nav, a) => a.url ? nav.push({ kind: "iframe", url: a.url, title: a.title }) : nav.push({ kind: "content", id: a.id, title: a.title });
5887
6039
  function groupByCategory(items) {
5888
6040
  const groups = /* @__PURE__ */ new Map();
@@ -5936,7 +6088,7 @@ function HelpRoot({ transport, strings, config, nav, panelProps }) {
5936
6088
  setState("loaded");
5937
6089
  }).catch((err) => {
5938
6090
  if (cancelled) return;
5939
- log12.warn("listContent (help) failed", { err });
6091
+ log13.warn("listContent (help) failed", { err });
5940
6092
  setErrorMsg(errorMessageFor(err, strings));
5941
6093
  setState("error");
5942
6094
  });
@@ -5987,7 +6139,7 @@ function HomeCard({ onClick, children, testid }) {
5987
6139
  // src/ui/modules/home.tsx
5988
6140
  import { Fragment as Fragment6, jsx as jsx29, jsxs as jsxs24 } from "preact/jsx-runtime";
5989
6141
  var p25 = BRAND.cssPrefix;
5990
- var log13 = logger.scope("home");
6142
+ var log14 = logger.scope("home");
5991
6143
  function resolveGreeting(props2) {
5992
6144
  const name = props2.options.userContext?.name;
5993
6145
  const fromConfig = props2.config.greetingText;
@@ -6006,7 +6158,7 @@ function HomeRoot(props2) {
6006
6158
  useEffect12(() => {
6007
6159
  if (!config.showRecentConversations) return;
6008
6160
  let cancelled = false;
6009
- transport.listConversations({ limit: 1 }).then((res) => !cancelled && setRecent(res.conversations?.[0] ?? null)).catch((err) => !cancelled && log13.warn("listConversations (home) failed", { err }));
6161
+ transport.listConversations({ limit: 1 }).then((res) => !cancelled && setRecent(res.conversations?.[0] ?? null)).catch((err) => !cancelled && log14.warn("listConversations (home) failed", { err }));
6010
6162
  return () => {
6011
6163
  cancelled = true;
6012
6164
  };
@@ -6014,7 +6166,7 @@ function HomeRoot(props2) {
6014
6166
  useEffect12(() => {
6015
6167
  if (!config.contentTags?.length) return;
6016
6168
  let cancelled = false;
6017
- transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log13.warn("listContent (home) failed", { err }));
6169
+ transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log14.warn("listContent (home) failed", { err }));
6018
6170
  return () => {
6019
6171
  cancelled = true;
6020
6172
  };
@@ -6022,7 +6174,7 @@ function HomeRoot(props2) {
6022
6174
  const greeting = resolveGreeting(props2);
6023
6175
  const avatars = config.userAvatars ?? [];
6024
6176
  const status = config.status;
6025
- const contentTitle = config.contentBlockTitle ? moduleLabel(strings, config.contentBlockTitle) : strings.homeContentTitle;
6177
+ const contentTitle = config.contentBlockTitle ? localizeText(strings, config.contentBlockTitle) : strings.homeContentTitle;
6026
6178
  return /* @__PURE__ */ jsx29("div", { class: `${p25}-module ${p25}-home`, "data-testid": TID.homeView, children: /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-scroll`, children: [
6027
6179
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero`, children: [
6028
6180
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero-top`, children: [
@@ -6088,7 +6240,7 @@ var homeLayout = {
6088
6240
  import { useEffect as useEffect13, useState as useState10 } from "preact/hooks";
6089
6241
  import { jsx as jsx30, jsxs as jsxs25 } from "preact/jsx-runtime";
6090
6242
  var p26 = BRAND.cssPrefix;
6091
- var log14 = logger.scope("news");
6243
+ var log15 = logger.scope("news");
6092
6244
  function NewsRoot({ transport, strings, config, nav, panelProps }) {
6093
6245
  const tags = config.contentTags;
6094
6246
  const [state, setState] = useState10("loading");
@@ -6104,7 +6256,7 @@ function NewsRoot({ transport, strings, config, nav, panelProps }) {
6104
6256
  setState("loaded");
6105
6257
  }).catch((err) => {
6106
6258
  if (cancelled) return;
6107
- log14.warn("listContent (news) failed", { err });
6259
+ log15.warn("listContent (news) failed", { err });
6108
6260
  setErrorMsg(errorMessageFor(err, strings));
6109
6261
  setState("error");
6110
6262
  });
@@ -6177,7 +6329,7 @@ function HomeTabBar({ modules, activeTab, strings, unreadCount, onSelect }) {
6177
6329
  /* @__PURE__ */ jsx31(Icon, {}),
6178
6330
  badge ? /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-badge`, "data-testid": TID.tabBadge, children: badge }) : null
6179
6331
  ] }),
6180
- /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: moduleLabel(strings, m.label) })
6332
+ /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: localizeText(strings, m.label) })
6181
6333
  ]
6182
6334
  },
6183
6335
  m.id
@@ -6219,7 +6371,7 @@ function IframeView({ url, title, strings, onBack, actions }) {
6219
6371
  import { useCallback as useCallback3, useEffect as useEffect14, useState as useState11 } from "preact/hooks";
6220
6372
  import { jsx as jsx33, jsxs as jsxs28 } from "preact/jsx-runtime";
6221
6373
  var p29 = BRAND.cssPrefix;
6222
- var log15 = logger.scope("content");
6374
+ var log16 = logger.scope("content");
6223
6375
  function ContentView({ id, title, transport, strings, onBack, actions }) {
6224
6376
  const [item, setItem] = useState11(null);
6225
6377
  const [failed, setFailed] = useState11(false);
@@ -6238,7 +6390,7 @@ function ContentView({ id, title, transport, strings, onBack, actions }) {
6238
6390
  else setFailed(true);
6239
6391
  }).catch((err) => {
6240
6392
  if (cancelled) return;
6241
- log15.warn("listContent (reader) failed", { err });
6393
+ log16.warn("listContent (reader) failed", { err });
6242
6394
  setFailed(true);
6243
6395
  });
6244
6396
  return () => {
@@ -6458,7 +6610,7 @@ function useLauncherCallout({ callout, persistence }) {
6458
6610
 
6459
6611
  // src/ui/app.tsx
6460
6612
  import { jsx as jsx36, jsxs as jsxs31 } from "preact/jsx-runtime";
6461
- var log16 = logger.scope("app");
6613
+ var log17 = logger.scope("app");
6462
6614
  var p32 = BRAND.cssPrefix;
6463
6615
  function App({ options, hostElement, bus }) {
6464
6616
  const [persistence] = useState13(() => createPersistence(options.widgetId, options.storage));
@@ -6516,7 +6668,7 @@ function App({ options, hostElement, bus }) {
6516
6668
  });
6517
6669
  const calloutPersistent = options.launcher.callout?.persistent ?? true;
6518
6670
  const dismissCallout = useCallback6(() => {
6519
- log16.info("calloutDismiss");
6671
+ log17.info("calloutDismiss");
6520
6672
  bus.emit("calloutDismiss", void 0);
6521
6673
  dismissCalloutRaw();
6522
6674
  }, [bus, dismissCalloutRaw]);
@@ -6550,7 +6702,7 @@ function App({ options, hostElement, bus }) {
6550
6702
  (patch) => {
6551
6703
  persistence.patchUserPrefs(patch);
6552
6704
  transport.saveUserPrefs(persistence.loadUserPrefs()).catch((err) => {
6553
- log16.warn("saveUserPrefs failed", { err });
6705
+ log17.warn("saveUserPrefs failed", { err });
6554
6706
  });
6555
6707
  },
6556
6708
  [persistence, transport]
@@ -6631,7 +6783,7 @@ function App({ options, hostElement, bus }) {
6631
6783
  values: rec.values
6632
6784
  }));
6633
6785
  } catch (err) {
6634
- log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6786
+ log17.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6635
6787
  return [];
6636
6788
  }
6637
6789
  },
@@ -6671,7 +6823,7 @@ function App({ options, hostElement, bus }) {
6671
6823
  }
6672
6824
  } catch (err) {
6673
6825
  if (isStale()) return;
6674
- log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
6826
+ log17.warn("loadThread failed; resetting conversationId", { err, conversationId });
6675
6827
  conversationIdSig.value = void 0;
6676
6828
  persistence.saveConversationId(void 0);
6677
6829
  } finally {
@@ -6710,7 +6862,7 @@ function App({ options, hostElement, bus }) {
6710
6862
  }
6711
6863
  if (isStale()) return;
6712
6864
  if (res.visitorId && res.visitorId !== visitorId) {
6713
- log16.info("visitor rebound", { previous: visitorId, current: res.visitorId, reason: res.rebind?.visitorId });
6865
+ log17.info("visitor rebound", { previous: visitorId, current: res.visitorId, reason: res.rebind?.visitorId });
6714
6866
  persistence.setVisitorId(res.visitorId);
6715
6867
  setVisitorId(res.visitorId);
6716
6868
  bus.emit("visitorRebound", {
@@ -6720,7 +6872,7 @@ function App({ options, hostElement, bus }) {
6720
6872
  });
6721
6873
  }
6722
6874
  if (res.conversationId && res.conversationId !== conversationId) {
6723
- log16.info("conversation rebound", {
6875
+ log17.info("conversation rebound", {
6724
6876
  previous: conversationId,
6725
6877
  current: res.conversationId,
6726
6878
  reason: res.rebind?.conversationId
@@ -6735,9 +6887,9 @@ function App({ options, hostElement, bus }) {
6735
6887
  if (res.welcome?.suggestions) setSuggestions(res.welcome.suggestions);
6736
6888
  if (options.mode === "page") {
6737
6889
  if (res.site && isSiteConfigShape(res.site)) setParsedSite(res.site);
6738
- else if (res.site) log16.warn("invalid site config, using fallback", { site: res.site });
6890
+ else if (res.site) log17.warn("invalid site config, using fallback", { site: res.site });
6739
6891
  if (res.blocks && isBlocksConfigShape(res.blocks)) setParsedBlocks(res.blocks);
6740
- else if (res.blocks) log16.warn("invalid blocks config, using fallback", { blocks: res.blocks });
6892
+ else if (res.blocks) log17.warn("invalid blocks config, using fallback", { blocks: res.blocks });
6741
6893
  }
6742
6894
  if (res.userPrefs && !newConversation) {
6743
6895
  persistence.saveUserPrefs(res.userPrefs);
@@ -6803,7 +6955,7 @@ function App({ options, hostElement, bus }) {
6803
6955
  resumeActiveRef.current = true;
6804
6956
  const chatTab = chatTabIdRef.current;
6805
6957
  if (chatTab) homeNav.switchTab(chatTab);
6806
- log16.info("resumed in-flight reply on mount");
6958
+ log17.info("resumed in-flight reply on mount");
6807
6959
  }
6808
6960
  reducer.apply(evt.chunk);
6809
6961
  if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) setCanSend(false);
@@ -6819,7 +6971,7 @@ function App({ options, hostElement, bus }) {
6819
6971
  bubble.errorText = errorMessageFor(err, options.strings);
6820
6972
  feedback.play("error");
6821
6973
  }
6822
- log16.debug("mount resume ended without completing", { err });
6974
+ log17.debug("mount resume ended without completing", { err });
6823
6975
  } finally {
6824
6976
  resumeBubbleRef.current = null;
6825
6977
  if (bubble) {
@@ -6848,9 +7000,9 @@ function App({ options, hostElement, bus }) {
6848
7000
  void (async () => {
6849
7001
  try {
6850
7002
  const res = await dataBootRef.current;
6851
- setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
7003
+ setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
6852
7004
  } catch (err) {
6853
- log16.debug("bootstrap failed \u2014 treating as no forms", { err });
7005
+ log17.debug("bootstrap failed \u2014 treating as no forms", { err });
6854
7006
  } finally {
6855
7007
  setFormsReady(true);
6856
7008
  }
@@ -6866,6 +7018,23 @@ function App({ options, hostElement, bus }) {
6866
7018
  void runResume(handle);
6867
7019
  }
6868
7020
  }, [activated]);
7021
+ const formsLocaleRef = useRef9(null);
7022
+ useEffect17(() => {
7023
+ if (!activated) return;
7024
+ if (formsLocaleRef.current === null) {
7025
+ formsLocaleRef.current = effectiveLocale;
7026
+ return;
7027
+ }
7028
+ if (formsLocaleRef.current === effectiveLocale) return;
7029
+ formsLocaleRef.current = effectiveLocale;
7030
+ let cancelled = false;
7031
+ void transport.bootstrap().then((res) => {
7032
+ if (!cancelled) setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
7033
+ }).catch((err) => log17.debug("forms re-localize failed \u2014 keeping current copy", { err }));
7034
+ return () => {
7035
+ cancelled = true;
7036
+ };
7037
+ }, [activated, effectiveLocale, transport]);
6869
7038
  const lastUserSig = useRef9(userSig);
6870
7039
  useEffect17(() => {
6871
7040
  if (!conversationReady || lastUserSig.current === userSig) return;
@@ -6950,7 +7119,7 @@ function App({ options, hostElement, bus }) {
6950
7119
  (pt) => pt.kind === "tool" && pt.toolCallId === toolCallId
6951
7120
  );
6952
7121
  if (!part) {
6953
- log16.warn("resolveTool: tool part not found", { toolCallId });
7122
+ log17.warn("resolveTool: tool part not found", { toolCallId });
6954
7123
  return;
6955
7124
  }
6956
7125
  mutate(part);
@@ -6964,7 +7133,7 @@ function App({ options, hostElement, bus }) {
6964
7133
  );
6965
7134
  const handleToolResult = useCallback6(
6966
7135
  (toolCallId, output) => {
6967
- log16.info("toolResult", { toolCallId });
7136
+ log17.info("toolResult", { toolCallId });
6968
7137
  bus.emit("toolResult", { toolCallId });
6969
7138
  resolveTool(toolCallId, TRIGGER.toolResult, (part) => {
6970
7139
  part.outputSig.value = output;
@@ -6975,7 +7144,7 @@ function App({ options, hostElement, bus }) {
6975
7144
  );
6976
7145
  const handleToolDecision = useCallback6(
6977
7146
  (toolCallId, approved, reason) => {
6978
- log16.info("toolDecision", { toolCallId, approved });
7147
+ log17.info("toolDecision", { toolCallId, approved });
6979
7148
  bus.emit("toolDecision", { toolCallId, approved });
6980
7149
  resolveTool(toolCallId, TRIGGER.toolApproval, (part) => {
6981
7150
  const id = part.approvalSig.value?.id ?? toolCallId;
@@ -6999,7 +7168,7 @@ function App({ options, hostElement, bus }) {
6999
7168
  pageArea: () => options.pageContext?.area ? String(options.pageContext.area) : void 0,
7000
7169
  conversationId: () => conversationIdSig.value,
7001
7170
  onComplete: (form, trigger, values, skipped) => {
7002
- log16.info("formSubmit", { formId: form.id, trigger, skipped });
7171
+ log17.info("formSubmit", { formId: form.id, trigger, skipped });
7003
7172
  bus.emit("formSubmit", { formId: form.id, values, skipped });
7004
7173
  setFormMarkers((prev) => [
7005
7174
  ...prev.filter((m) => !(m.formId === form.id && m.outcome === "skipped")),
@@ -7022,6 +7191,9 @@ function App({ options, hostElement, bus }) {
7022
7191
  }
7023
7192
  });
7024
7193
  const activeForm = useComputed7(() => forms.activeForm.value);
7194
+ useEffect17(() => {
7195
+ forms.refresh();
7196
+ }, [effectiveForms, forms]);
7025
7197
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
7026
7198
  const msgCount = useComputed7(() => messagesSig.value.length);
7027
7199
  useEffect17(() => {
@@ -7044,12 +7216,12 @@ function App({ options, hostElement, bus }) {
7044
7216
  }, [idleMs, isOpen, forms, msgCount.value]);
7045
7217
  const handleSend = useCallback6(
7046
7218
  async (text, attachments) => {
7047
- log16.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7219
+ log17.info("send", { textLen: text.length, attachments: attachments.length, streaming });
7048
7220
  if (streaming) return;
7049
7221
  bus.emit("send", { text, attachmentCount: attachments.length });
7050
7222
  if (suggestions.length > 0) setSuggestions([]);
7051
7223
  const uploaded = await uploadAttachments(transport, attachments, (err) => {
7052
- log16.warn("upload failed", { err });
7224
+ log17.warn("upload failed", { err });
7053
7225
  bus.emit("error", err);
7054
7226
  options.onError?.(err);
7055
7227
  });
@@ -7066,24 +7238,24 @@ function App({ options, hostElement, bus }) {
7066
7238
  [streaming, transport, bus, options, suggestions.length, messagesSig, reducer, streamAssistant, forms]
7067
7239
  );
7068
7240
  const handleStop = useCallback6(() => {
7069
- log16.info("stop");
7241
+ log17.info("stop");
7070
7242
  bus.emit("stop", void 0);
7071
7243
  activeCancel?.();
7072
7244
  }, [activeCancel, bus]);
7073
7245
  const handleSoundToggle = useCallback6(() => {
7074
7246
  feedback.toggleMuted();
7075
7247
  const muted = feedback.mutedSig.value;
7076
- log16.info("soundToggle", { muted });
7248
+ log17.info("soundToggle", { muted });
7077
7249
  bus.emit("soundToggle", { muted });
7078
7250
  }, [feedback, bus]);
7079
7251
  const handleClose = useCallback6(() => {
7080
7252
  if (resolveCloseAction(options.mode, panelSize) === "exit-fullscreen") {
7081
- log16.info("close \u2192 exit-fullscreen", { mode: options.mode });
7253
+ log17.info("close \u2192 exit-fullscreen", { mode: options.mode });
7082
7254
  setPanelSize("normal");
7083
7255
  bus.emit("fullscreen", false);
7084
7256
  return;
7085
7257
  }
7086
- log16.info("close \u2192 panel closed", { mode: options.mode });
7258
+ log17.info("close \u2192 panel closed", { mode: options.mode });
7087
7259
  forms.fire("panel-close");
7088
7260
  setIsOpen(false);
7089
7261
  persistence.savePanelOpen(false);
@@ -7094,7 +7266,7 @@ function App({ options, hostElement, bus }) {
7094
7266
  if (!options.modules.list.some((m) => m.layout === "chat")) return;
7095
7267
  transport.listConversations({ limit: 50 }).then((res) => {
7096
7268
  unreadCountSig.value = res.conversations.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0);
7097
- }).catch((err) => log16.debug("refreshUnread failed (non-fatal)", { err }));
7269
+ }).catch((err) => log17.debug("refreshUnread failed (non-fatal)", { err }));
7098
7270
  }, [transport, options.modules, unreadCountSig]);
7099
7271
  const unreadSeeded = useRef9(false);
7100
7272
  useEffect17(() => {
@@ -7103,7 +7275,7 @@ function App({ options, hostElement, bus }) {
7103
7275
  refreshUnread();
7104
7276
  }, [activated, conversationReady, refreshUnread]);
7105
7277
  const handleOpen = useCallback6(() => {
7106
- log16.info("open", { mode: options.mode });
7278
+ log17.info("open", { mode: options.mode });
7107
7279
  feedback.unlock();
7108
7280
  setLauncherLeaving(true);
7109
7281
  setIsOpen(true);
@@ -7117,11 +7289,11 @@ function App({ options, hostElement, bus }) {
7117
7289
  const handleClear = useCallback6(() => {
7118
7290
  const hasUserMessages = messagesSig.value.some((m) => m.role === "user");
7119
7291
  if (!hasUserMessages && canSend && conversationIdSig.value) {
7120
- log16.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7292
+ log17.info("clear \u2192 reusing empty conversation", { conversationId: conversationIdSig.value });
7121
7293
  if (messagesSig.value.length === 0) playWelcome(welcomeRef.current?.messages);
7122
7294
  return;
7123
7295
  }
7124
- log16.info("clear \u2192 new conversation", { wasStreaming: streaming });
7296
+ log17.info("clear \u2192 new conversation", { wasStreaming: streaming });
7125
7297
  bus.emit("clear", void 0);
7126
7298
  if (streaming) activeCancel?.();
7127
7299
  messagesSig.value = [];
@@ -7139,14 +7311,14 @@ function App({ options, hostElement, bus }) {
7139
7311
  }, [handleClear, homeNav, options]);
7140
7312
  const handleExpand = useCallback6(() => {
7141
7313
  const nextSize = panelSize === "expanded" ? "normal" : "expanded";
7142
- log16.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7314
+ log17.info("expand", { from: panelSize, expanded: nextSize === "expanded" });
7143
7315
  setPanelSize(nextSize);
7144
7316
  persistence.savePanelSize(nextSize);
7145
7317
  bus.emit("expand", nextSize === "expanded");
7146
7318
  }, [bus, panelSize, persistence]);
7147
7319
  const handleFullscreen = useCallback6(() => {
7148
7320
  const nextSize = panelSize === "fullscreen" ? "normal" : "fullscreen";
7149
- log16.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7321
+ log17.info("fullscreen", { from: panelSize, fullscreen: nextSize === "fullscreen" });
7150
7322
  setPanelSize(nextSize);
7151
7323
  persistence.savePanelSize(nextSize);
7152
7324
  bus.emit("fullscreen", nextSize === "fullscreen");
@@ -7156,21 +7328,21 @@ function App({ options, hostElement, bus }) {
7156
7328
  const url = new URL(options.popOutUrl);
7157
7329
  if (conversationIdSig.value) url.searchParams.set("conversation", conversationIdSig.value);
7158
7330
  if (options.widgetId !== "default") url.searchParams.set("widgetId", options.widgetId);
7159
- log16.info("popOut", { url: url.toString() });
7331
+ log17.info("popOut", { url: url.toString() });
7160
7332
  window.open(url.toString(), "_blank", "noopener,noreferrer");
7161
7333
  bus.emit("popOut", void 0);
7162
7334
  }, [bus, conversationIdSig, options.popOutUrl, options.widgetId]);
7163
7335
  const handleToggleHistory = useCallback6(() => {
7164
7336
  setView((v) => {
7165
7337
  const next = v === "history" ? "chat" : "history";
7166
- log16.info("toggleHistory", { view: next });
7338
+ log17.info("toggleHistory", { view: next });
7167
7339
  bus.emit("toggleHistory", { view: next });
7168
7340
  return next;
7169
7341
  });
7170
7342
  }, [bus]);
7171
7343
  const handleLocaleChange = useCallback6(
7172
7344
  (locale) => {
7173
- log16.info("localeChange", { locale });
7345
+ log17.info("localeChange", { locale });
7174
7346
  setActiveLocale(locale);
7175
7347
  patchAndSync({ locale });
7176
7348
  bus.emit("localeChange", locale);
@@ -7179,7 +7351,7 @@ function App({ options, hostElement, bus }) {
7179
7351
  );
7180
7352
  const handleThemeChange = useCallback6(
7181
7353
  (mode) => {
7182
- log16.info("themeChange", { mode });
7354
+ log17.info("themeChange", { mode });
7183
7355
  setActiveThemeMode(mode);
7184
7356
  hostElement.dataset.theme = mode;
7185
7357
  patchAndSync({ themeMode: mode });
@@ -7189,7 +7361,7 @@ function App({ options, hostElement, bus }) {
7189
7361
  );
7190
7362
  const handleTextSizeChange = useCallback6(
7191
7363
  (size) => {
7192
- log16.info("textSizeChange", { size });
7364
+ log17.info("textSizeChange", { size });
7193
7365
  setActiveTextSize(size);
7194
7366
  hostElement.dataset.textSize = size;
7195
7367
  patchAndSync({ textSize: size });
@@ -7200,7 +7372,7 @@ function App({ options, hostElement, bus }) {
7200
7372
  const handleToggleSidebarCollapsed = useCallback6(() => {
7201
7373
  setSidebarCollapsed((prev) => {
7202
7374
  const next = !prev;
7203
- log16.info("sidebarToggle", { collapsed: next });
7375
+ log17.info("sidebarToggle", { collapsed: next });
7204
7376
  persistence.saveSidebarCollapsed(next);
7205
7377
  bus.emit("sidebarToggle", { collapsed: next });
7206
7378
  return next;
@@ -7208,7 +7380,7 @@ function App({ options, hostElement, bus }) {
7208
7380
  }, [persistence, bus]);
7209
7381
  const handleWidgetSizeChange = useCallback6(
7210
7382
  (size) => {
7211
- log16.info("resize", size);
7383
+ log17.info("resize", size);
7212
7384
  patchAndSync({ widgetSize: size });
7213
7385
  bus.emit("resize", size);
7214
7386
  },
@@ -7216,7 +7388,7 @@ function App({ options, hostElement, bus }) {
7216
7388
  );
7217
7389
  const handleSelectHistoryConversation = useCallback6(
7218
7390
  async (targetConversationId) => {
7219
- log16.info("selectConversation", { conversationId: targetConversationId });
7391
+ log17.info("selectConversation", { conversationId: targetConversationId });
7220
7392
  bus.emit("selectConversation", { conversationId: targetConversationId });
7221
7393
  try {
7222
7394
  const [res, markers] = await Promise.all([
@@ -7318,7 +7490,7 @@ function App({ options, hostElement, bus }) {
7318
7490
  onStop: handleStop,
7319
7491
  onSuggestion: (s) => {
7320
7492
  const text = s.text ?? s.label;
7321
- log16.info("suggestion", { text });
7493
+ log17.info("suggestion", { text });
7322
7494
  bus.emit("suggestion", { text });
7323
7495
  void handleSend(text, []);
7324
7496
  },
@@ -7408,6 +7580,7 @@ function App({ options, hostElement, bus }) {
7408
7580
  {
7409
7581
  callout: calloutToRender,
7410
7582
  launcherPosition: effectiveOptions.position,
7583
+ strings: effectiveOptions.strings,
7411
7584
  closeLabel: effectiveOptions.strings.close,
7412
7585
  onDismiss: dismissCallout
7413
7586
  }
@@ -7440,14 +7613,14 @@ function emitMessage(bus, options, role, text) {
7440
7613
 
7441
7614
  // src/ui/error-boundary.tsx
7442
7615
  import { Component } from "preact";
7443
- var log17 = logger.scope("error-boundary");
7616
+ var log18 = logger.scope("error-boundary");
7444
7617
  var ErrorBoundary = class extends Component {
7445
7618
  constructor() {
7446
7619
  super(...arguments);
7447
7620
  __publicField(this, "state", { crashed: false });
7448
7621
  }
7449
7622
  componentDidCatch(error, info) {
7450
- log17.error("widget render crashed", { error, componentStack: info?.componentStack });
7623
+ log18.error("widget render crashed", { error, componentStack: info?.componentStack });
7451
7624
  this.setState({ crashed: true });
7452
7625
  this.props.onError(error, info?.componentStack);
7453
7626
  }
@@ -7456,102 +7629,6 @@ var ErrorBoundary = class extends Component {
7456
7629
  }
7457
7630
  };
7458
7631
 
7459
- // src/shadow/iframe-mount.ts
7460
- var log18 = logger.scope("iframe");
7461
- var IFRAME_SRCDOC = '<!doctype html><html><head><meta charset="utf-8"><style>html,body{margin:0;padding:0;background:transparent;overflow:hidden}</style></head><body></body></html>';
7462
- function createIframeMount(opts, position, initialMode = "closed", layoutMode = "floating", target = null) {
7463
- log18.debug("createIframeMount", { position, initialMode, layoutMode, hasTarget: !!target });
7464
- const iframe = document.createElement("iframe");
7465
- iframe.title = `${BRAND.name} chat`;
7466
- iframe.setAttribute("allow", "microphone; clipboard-write");
7467
- sizeIframe(iframe, position, initialMode, layoutMode);
7468
- const parent = layoutMode === "inline" && target ? target : document.body;
7469
- parent.appendChild(iframe);
7470
- const doc = iframe.contentDocument;
7471
- if (!doc) throw new Error("iframe contentDocument unavailable (cross-origin?)");
7472
- doc.open();
7473
- doc.write(IFRAME_SRCDOC);
7474
- doc.close();
7475
- const mount2 = createShadowMount({ ...opts, target: doc.body });
7476
- const mo = new MutationObserver(() => {
7477
- sizeIframe(iframe, position, mount2.hostElement.dataset.mode ?? "closed", layoutMode);
7478
- });
7479
- mo.observe(mount2.hostElement, { attributes: true, attributeFilter: ["data-mode"] });
7480
- return {
7481
- ...mount2,
7482
- iframeElement: iframe,
7483
- destroy() {
7484
- mo.disconnect();
7485
- mount2.destroy();
7486
- iframe.remove();
7487
- }
7488
- };
7489
- }
7490
- var BASE_CSS = "border:0;background:transparent;z-index:2147483647;pointer-events:auto;";
7491
- var FLOATING_SIZE_BY_MODE = {
7492
- closed: { w: "88px", h: "88px" },
7493
- expanded: { w: "min(calc(100vw - 32px), 680px)", h: "min(calc(100vh - 32px), 860px)" },
7494
- // `open` (normal) — also the fallback for any other non-fullscreen state.
7495
- open: { w: "min(calc(100vw - 32px), 440px)", h: "min(calc(100vh - 32px), 700px)" }
7496
- };
7497
- function sizeIframe(iframe, position, mode, layoutMode) {
7498
- if (mode === "fullscreen" || layoutMode === "standalone") {
7499
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7500
- return;
7501
- }
7502
- if (layoutMode === "inline") {
7503
- iframe.style.cssText = `${BASE_CSS}position:static;display:block;width:100%;height:100%;`;
7504
- return;
7505
- }
7506
- const compact = typeof matchMedia === "function" && matchMedia("(max-width: 640px)").matches;
7507
- if (compact && (mode === "open" || mode === "expanded")) {
7508
- iframe.style.cssText = `${BASE_CSS}position:fixed;inset:0;width:100vw;height:100vh;`;
7509
- return;
7510
- }
7511
- const vKey = position.startsWith("top-") ? "top" : "bottom";
7512
- const hKey = position.endsWith("-left") ? "left" : "right";
7513
- const { w, h: h2 } = FLOATING_SIZE_BY_MODE[mode] ?? FLOATING_SIZE_BY_MODE.open;
7514
- iframe.style.cssText = `${BASE_CSS}position:fixed;${vKey}:16px;${hKey}:16px;width:${w};height:${h2};`;
7515
- }
7516
-
7517
- // src/core/events.ts
7518
- var EventBus = class {
7519
- constructor() {
7520
- __publicField(this, "handlers", /* @__PURE__ */ new Map());
7521
- }
7522
- /**
7523
- * Register a listener for `name`.
7524
- * @returns an `off` function that removes this listener.
7525
- */
7526
- on(name, fn) {
7527
- let set = this.handlers.get(name);
7528
- if (!set) {
7529
- set = /* @__PURE__ */ new Set();
7530
- this.handlers.set(name, set);
7531
- }
7532
- set.add(fn);
7533
- return () => this.handlers.get(name)?.delete(fn);
7534
- }
7535
- /**
7536
- * Dispatch an event synchronously to all listeners.
7537
- * Listener errors are swallowed — the widget must not crash on user callbacks.
7538
- */
7539
- emit(name, payload) {
7540
- const set = this.handlers.get(name);
7541
- if (!set) return;
7542
- for (const fn of set) {
7543
- try {
7544
- fn(payload);
7545
- } catch {
7546
- }
7547
- }
7548
- }
7549
- /** Remove every listener. Called by the instance `destroy()`. */
7550
- clear() {
7551
- this.handlers.clear();
7552
- }
7553
- };
7554
-
7555
7632
  // src/core/tracking.ts
7556
7633
  var PROTOCOL_VERSION = "1";
7557
7634
  var MAX_HITS_PER_MINUTE = 60;
@@ -7667,6 +7744,73 @@ function createTracker(bus, deps) {
7667
7744
  };
7668
7745
  }
7669
7746
 
7747
+ // src/core/boot.ts
7748
+ function createWidgetRuntime(params) {
7749
+ const { bus, host, appRoot, persistence, reportError } = params;
7750
+ let userRaw = params.initialRaw;
7751
+ let serverConfig;
7752
+ let currentOptions = resolveOptions(userRaw);
7753
+ let firstRender = true;
7754
+ const recompute = () => {
7755
+ currentOptions = resolveOptions(serverConfig ? mergeServerConfig(userRaw, serverConfig) : userRaw);
7756
+ };
7757
+ const applyHostStyling = () => {
7758
+ applyHostPosition(host, hostPositionForMode(currentOptions.mode));
7759
+ const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7760
+ applyHostAttributes(host, cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions);
7761
+ if (firstRender) {
7762
+ applyMode(host, resolveInitialHostMode(currentOptions));
7763
+ firstRender = false;
7764
+ }
7765
+ };
7766
+ const render2 = () => {
7767
+ applyHostStyling();
7768
+ renderPreact(
7769
+ h(ErrorBoundary, { onError: reportError }, h(App, { bus, hostElement: host, options: currentOptions })),
7770
+ appRoot
7771
+ );
7772
+ };
7773
+ bus.on("handshake", (response) => {
7774
+ if (!response.config) return;
7775
+ serverConfig = response.config;
7776
+ recompute();
7777
+ render2();
7778
+ });
7779
+ const untrack = createTracker(bus, {
7780
+ getOptions: () => currentOptions,
7781
+ getVisitorId: () => persistence.getVisitorId(),
7782
+ getConversationId: () => persistence.loadConversationId()
7783
+ });
7784
+ return {
7785
+ getOptions: () => currentOptions,
7786
+ getUserOptions: () => userRaw,
7787
+ render: render2,
7788
+ setUserOptions: (raw) => {
7789
+ userRaw = raw;
7790
+ recompute();
7791
+ render2();
7792
+ },
7793
+ patchUserOptions: (patch) => {
7794
+ userRaw = { ...userRaw, ...patch };
7795
+ recompute();
7796
+ render2();
7797
+ },
7798
+ destroy: () => untrack()
7799
+ };
7800
+ }
7801
+ function resolveInitialHostMode(options) {
7802
+ if (options.mode === "standalone") return "standalone";
7803
+ if (options.mode === "inline") return "inline";
7804
+ const persistence = createPersistence(options.widgetId, options.storage);
7805
+ const { panelOpen, panelSize } = resolveInitialPanelState(
7806
+ options,
7807
+ currentViewportWidth(),
7808
+ persistence.loadPanelOpen(),
7809
+ persistence.loadPanelSize()
7810
+ );
7811
+ return computeHostMode(options.mode, panelSize, panelOpen);
7812
+ }
7813
+
7670
7814
  // src/core/hooks.ts
7671
7815
  var log19 = logger.scope("hooks");
7672
7816
  var HOOK_WILDCARD = "*";
@@ -7752,73 +7896,45 @@ function mount(opts) {
7752
7896
  if (opts.debug !== void 0) setLogLevel(opts.debug);
7753
7897
  const log20 = logger.scope("mount");
7754
7898
  log20.info("mount", { widgetId: opts.widgetId, publicKey: opts.publicKey, mode: opts.presentation?.mode });
7755
- let rawOptions = opts;
7756
- let currentOptions = resolveOptions(rawOptions);
7757
- const widgetId = currentOptions.widgetId;
7899
+ const resolved = resolveOptions(opts);
7900
+ const widgetId = resolved.widgetId;
7758
7901
  const existing = instances.get(widgetId);
7759
7902
  if (existing) {
7760
7903
  log20.debug("destroy existing instance at slot", { widgetId });
7761
7904
  existing.destroy();
7762
7905
  }
7763
- const initialHostMode = resolveInitialHostMode(currentOptions);
7764
- const mountResult = currentOptions.iframe ? createIframeMount(
7765
- { position: hostPositionForMode(currentOptions.mode) },
7766
- currentOptions.position,
7906
+ const initialHostMode = resolveInitialHostMode(resolved);
7907
+ const mountResult = resolved.iframe ? createIframeMount(
7908
+ { position: hostPositionForMode(resolved.mode) },
7909
+ resolved.position,
7767
7910
  initialHostMode,
7768
- currentOptions.mode,
7911
+ resolved.mode,
7769
7912
  opts.target ?? null
7770
7913
  ) : createShadowMount({
7771
- position: hostPositionForMode(currentOptions.mode),
7914
+ position: hostPositionForMode(resolved.mode),
7772
7915
  target: opts.target ?? null
7773
7916
  });
7774
- const persistence = createPersistence(currentOptions.widgetId, currentOptions.storage);
7775
- const applyResolvedHostAttributes = () => {
7776
- const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7777
- applyHostAttributes(
7778
- mountResult.hostElement,
7779
- cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions
7780
- );
7781
- };
7782
- applyResolvedHostAttributes();
7783
- applyMode(mountResult.hostElement, initialHostMode);
7784
7917
  mountResult.hostElement.dataset.widgetId = widgetId;
7785
7918
  const bus = new EventBus();
7919
+ const persistence = createPersistence(widgetId, resolved.storage);
7786
7920
  const reportError = (error) => {
7787
7921
  bus.emit("error", error);
7788
- currentOptions.onError?.(error);
7789
- };
7790
- const renderApp = () => {
7791
- render(
7792
- h(
7793
- ErrorBoundary,
7794
- { onError: reportError },
7795
- h(App, { bus, hostElement: mountResult.hostElement, options: currentOptions })
7796
- ),
7797
- mountResult.appRoot
7798
- );
7922
+ runtime.getOptions().onError?.(error);
7799
7923
  };
7800
- renderApp();
7801
- bus.on("handshake", (response) => {
7802
- if (!response.config) return;
7803
- rawOptions = mergeServerConfig(rawOptions, response.config);
7804
- currentOptions = resolveOptions(rawOptions);
7805
- applyHostPosition(mountResult.hostElement, hostPositionForMode(currentOptions.mode));
7806
- applyResolvedHostAttributes();
7807
- renderApp();
7808
- });
7809
- const unsubscribeTracker = createTracker(bus, {
7810
- getOptions: () => currentOptions,
7811
- getVisitorId: () => persistence.getVisitorId(),
7812
- getConversationId: () => persistence.loadConversationId()
7924
+ const runtime = createWidgetRuntime({
7925
+ bus,
7926
+ host: mountResult.hostElement,
7927
+ appRoot: mountResult.appRoot,
7928
+ persistence,
7929
+ initialRaw: opts,
7930
+ reportError
7813
7931
  });
7932
+ runtime.render();
7814
7933
  const host = mountResult.hostElement;
7815
7934
  let unsubscribeHooks = null;
7816
7935
  const applyUpdate = (patch) => {
7817
7936
  if (patch.debug !== void 0) setLogLevel(patch.debug);
7818
- rawOptions = { ...rawOptions, ...patch };
7819
- currentOptions = resolveOptions(rawOptions);
7820
- applyResolvedHostAttributes();
7821
- renderApp();
7937
+ runtime.patchUserOptions(patch);
7822
7938
  };
7823
7939
  const instance = {
7824
7940
  widgetId,
@@ -7837,7 +7953,7 @@ function mount(opts) {
7837
7953
  // context; App re-handshakes with the SAME visitor/conversation.
7838
7954
  setUserContext: (userContext) => {
7839
7955
  log20.debug("setUserContext", { keys: Object.keys(userContext) });
7840
- applyUpdate({ userContext: { ...rawOptions.userContext, ...userContext } });
7956
+ applyUpdate({ userContext: { ...runtime.getUserOptions().userContext, ...userContext } });
7841
7957
  },
7842
7958
  clearUserContext: () => {
7843
7959
  log20.debug("clearUserContext");
@@ -7845,7 +7961,7 @@ function mount(opts) {
7845
7961
  },
7846
7962
  setPageContext: (pageContext) => {
7847
7963
  log20.debug("setPageContext", { keys: Object.keys(pageContext) });
7848
- applyUpdate({ pageContext: { ...rawOptions.pageContext, ...pageContext } });
7964
+ applyUpdate({ pageContext: { ...runtime.getUserOptions().pageContext, ...pageContext } });
7849
7965
  },
7850
7966
  clearPageContext: () => {
7851
7967
  log20.debug("clearPageContext");
@@ -7860,7 +7976,7 @@ function mount(opts) {
7860
7976
  destroy: () => {
7861
7977
  log20.info("destroy", { widgetId });
7862
7978
  unsubscribeHooks?.();
7863
- unsubscribeTracker();
7979
+ runtime.destroy();
7864
7980
  bus.clear();
7865
7981
  render(null, mountResult.appRoot);
7866
7982
  mountResult.destroy();
@@ -7899,18 +8015,6 @@ function hook(widgetId, event, listener) {
7899
8015
  }
7900
8016
  var version = "0.1.0";
7901
8017
  var brand = BRAND;
7902
- function resolveInitialHostMode(options) {
7903
- if (options.mode === "standalone") return "standalone";
7904
- if (options.mode === "inline") return "inline";
7905
- const persistence = createPersistence(options.widgetId, options.storage);
7906
- const { panelOpen, panelSize } = resolveInitialPanelState(
7907
- options,
7908
- currentViewportWidth(),
7909
- persistence.loadPanelOpen(),
7910
- persistence.loadPanelSize()
7911
- );
7912
- return computeHostMode(options.mode, panelSize, panelOpen);
7913
- }
7914
8018
  export {
7915
8019
  HOOK_WILDCARD,
7916
8020
  brand,