@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/web-component.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/web-component.ts
6
- import { h as h2, render as render2 } 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) {
@@ -541,7 +582,7 @@ function resolveOptions(rawOpts) {
541
582
  humanInLoop: opts.features?.humanInLoop ?? DEFAULT_FEATURES.humanInLoop,
542
583
  tools: opts.features?.tools
543
584
  },
544
- forms: resolveForms(opts.forms),
585
+ forms: resolveForms(opts.forms, locale),
545
586
  endpoints: {
546
587
  upload: opts.endpoints?.upload ?? DEFAULT_ENDPOINTS.upload,
547
588
  transcribe: opts.endpoints?.transcribe ?? DEFAULT_ENDPOINTS.transcribe,
@@ -664,13 +705,14 @@ function resolveModules(overrides) {
664
705
  const byId = Object.fromEntries(list.map((m) => [m.id, m]));
665
706
  return { list, byId };
666
707
  }
667
- function resolveForms(overrides) {
708
+ function resolveForms(overrides, locale) {
668
709
  var _a;
669
710
  if (!overrides?.length) return DEFAULT_FORMS;
670
711
  const list = [];
671
712
  const seen = /* @__PURE__ */ new Set();
672
- for (const def of overrides) {
673
- if (!def?.id || seen.has(def.id)) continue;
713
+ for (const raw of overrides) {
714
+ if (!raw?.id || seen.has(raw.id)) continue;
715
+ const def = overlayFormDef(raw, locale);
674
716
  const fields = (def.fields ?? []).filter((f) => Boolean(f && f.name && f.label && f.type));
675
717
  const triggers = (Array.isArray(def.on) ? def.on : [def.on]).map(parseTrigger).filter((t) => t !== null);
676
718
  if (fields.length === 0 || triggers.length === 0) continue;
@@ -993,35 +1035,153 @@ function parseLauncher(get, str2) {
993
1035
  return Object.keys(base).length > 0 ? base : null;
994
1036
  }
995
1037
 
996
- // src/core/handshake-shape.ts
997
- function isPlainObject(raw) {
998
- return !!raw && typeof raw === "object" && !Array.isArray(raw);
999
- }
1000
- function isSiteConfigShape(raw) {
1001
- return isPlainObject(raw);
1038
+ // src/core/ids.ts
1039
+ function uuid7() {
1040
+ const ts = Date.now();
1041
+ const bytes = new Uint8Array(16);
1042
+ bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1043
+ bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1044
+ bytes[2] = ts >>> 24 & 255;
1045
+ bytes[3] = ts >>> 16 & 255;
1046
+ bytes[4] = ts >>> 8 & 255;
1047
+ bytes[5] = ts & 255;
1048
+ fillRandom(bytes.subarray(6));
1049
+ bytes[6] = bytes[6] & 15 | 112;
1050
+ bytes[8] = bytes[8] & 63 | 128;
1051
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1052
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1002
1053
  }
1003
- function isBlocksConfigShape(raw) {
1004
- if (!isPlainObject(raw)) return false;
1005
- if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
1006
- if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
1007
- return true;
1054
+ function fillRandom(view) {
1055
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1056
+ crypto.getRandomValues(view);
1057
+ return;
1058
+ }
1059
+ for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1008
1060
  }
1009
1061
 
1010
- // src/core/host-commands.ts
1011
- function commandEventName(cmd) {
1012
- return `${BRAND.cssPrefix}:${cmd}`;
1013
- }
1014
- function bindHostCommands(host, handlers) {
1015
- const entries = Object.entries(handlers);
1016
- const wrapped = entries.map(([cmd, fn]) => {
1017
- const event = commandEventName(cmd);
1018
- const listener = (e) => fn(e.detail);
1019
- host.addEventListener(event, listener);
1020
- return [event, listener];
1021
- });
1022
- return () => {
1023
- for (const [event, listener] of wrapped) host.removeEventListener(event, listener);
1062
+ // src/core/persistence.ts
1063
+ var log2 = logger.scope("persistence");
1064
+ function createPersistence(widgetId, storage = defaultStorage) {
1065
+ const prefix = `${BRAND.cssPrefix}.${widgetId}`;
1066
+ const KEY_VISITOR = `${prefix}.visitorId`;
1067
+ const KEY_CONVERSATION = `${prefix}.conversationId`;
1068
+ const KEY_USER_PREFS = `${prefix}.userPrefs`;
1069
+ const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
1070
+ const KEY_PANEL_SIZE = `${prefix}.panelSize`;
1071
+ const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
1072
+ const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
1073
+ const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
1074
+ const KEY_FORMS_DONE = `${prefix}.formsDone`;
1075
+ const persistence = {
1076
+ getVisitorId() {
1077
+ const existing = storage.get(KEY_VISITOR);
1078
+ if (existing) return existing;
1079
+ const minted = uuid7();
1080
+ storage.set(KEY_VISITOR, minted);
1081
+ return minted;
1082
+ },
1083
+ setVisitorId(id) {
1084
+ log2.debug("setVisitorId (rebind)", { id, widgetId });
1085
+ storage.set(KEY_VISITOR, id);
1086
+ },
1087
+ loadConversationId() {
1088
+ return storage.get(KEY_CONVERSATION) ?? void 0;
1089
+ },
1090
+ saveConversationId(id) {
1091
+ log2.trace("saveConversationId", { id, widgetId });
1092
+ if (id) storage.set(KEY_CONVERSATION, id);
1093
+ else storage.remove(KEY_CONVERSATION);
1094
+ },
1095
+ loadUserPrefs() {
1096
+ const raw = storage.get(KEY_USER_PREFS);
1097
+ if (!raw) return {};
1098
+ try {
1099
+ const parsed = JSON.parse(raw);
1100
+ return isPlainObject(parsed) ? parsed : {};
1101
+ } catch (error) {
1102
+ log2.warn("loadUserPrefs parse failed", { error });
1103
+ return {};
1104
+ }
1105
+ },
1106
+ saveUserPrefs(s) {
1107
+ try {
1108
+ storage.set(KEY_USER_PREFS, JSON.stringify(s));
1109
+ } catch (error) {
1110
+ log2.warn("saveUserPrefs serialise failed", { error });
1111
+ }
1112
+ },
1113
+ patchUserPrefs(patch) {
1114
+ const current = persistence.loadUserPrefs();
1115
+ persistence.saveUserPrefs({ ...current, ...patch });
1116
+ },
1117
+ loadPanelOpen() {
1118
+ const raw = storage.get(KEY_PANEL_OPEN);
1119
+ if (raw === "1") return true;
1120
+ if (raw === "0") return false;
1121
+ return null;
1122
+ },
1123
+ savePanelOpen(open) {
1124
+ storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
1125
+ },
1126
+ loadPanelSize() {
1127
+ const raw = storage.get(KEY_PANEL_SIZE);
1128
+ return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
1129
+ },
1130
+ savePanelSize(size) {
1131
+ storage.set(KEY_PANEL_SIZE, size);
1132
+ },
1133
+ loadCalloutDismissed(currentText) {
1134
+ if (!currentText) return false;
1135
+ const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
1136
+ return !!dismissedText && dismissedText === currentText;
1137
+ },
1138
+ saveCalloutDismissed(currentText) {
1139
+ if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
1140
+ else storage.remove(KEY_CALLOUT_DISMISSED);
1141
+ },
1142
+ loadSidebarCollapsed() {
1143
+ const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
1144
+ if (raw === "1") return true;
1145
+ if (raw === "0") return false;
1146
+ return null;
1147
+ },
1148
+ saveSidebarCollapsed(collapsed) {
1149
+ storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
1150
+ },
1151
+ loadActiveModule() {
1152
+ return storage.get(KEY_ACTIVE_MODULE) || null;
1153
+ },
1154
+ saveActiveModule(id) {
1155
+ storage.set(KEY_ACTIVE_MODULE, id);
1156
+ },
1157
+ loadFormsDone() {
1158
+ const raw = storage.get(KEY_FORMS_DONE);
1159
+ if (!raw) return {};
1160
+ try {
1161
+ const parsed = JSON.parse(raw);
1162
+ return isPlainObject(parsed) ? parsed : {};
1163
+ } catch (error) {
1164
+ log2.warn("loadFormsDone parse failed", { error });
1165
+ return {};
1166
+ }
1167
+ },
1168
+ saveFormDone(id) {
1169
+ try {
1170
+ const done = persistence.loadFormsDone();
1171
+ done[id] = Date.now();
1172
+ storage.set(KEY_FORMS_DONE, JSON.stringify(done));
1173
+ } catch (error) {
1174
+ log2.warn("saveFormDone serialise failed", { error });
1175
+ }
1176
+ },
1177
+ clearConversation() {
1178
+ storage.remove(KEY_CONVERSATION);
1179
+ }
1024
1180
  };
1181
+ return persistence;
1182
+ }
1183
+ function isPlainObject(v) {
1184
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1025
1185
  }
1026
1186
 
1027
1187
  // src/styles/tokens.css
@@ -1048,7 +1208,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
1048
1208
  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);
1049
1209
 
1050
1210
  // src/shadow/mount.ts
1051
- var log2 = logger.scope("mount");
1211
+ var log3 = logger.scope("mount");
1052
1212
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
1053
1213
  var HOST_CSS = {
1054
1214
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1065,7 +1225,7 @@ function hostPositionForMode(mode) {
1065
1225
  }
1066
1226
  function createShadowMount(opts = {}) {
1067
1227
  const position = opts.position ?? "flow";
1068
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1228
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1069
1229
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1070
1230
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1071
1231
  customElements.define(tagName, class extends HTMLElement {
@@ -1149,6 +1309,44 @@ function applyHostAttributes(host, resolved) {
1149
1309
  applySize(host, resolved.size);
1150
1310
  }
1151
1311
 
1312
+ // src/core/boot.ts
1313
+ import { h, render as renderPreact } from "preact";
1314
+
1315
+ // src/ui/app.tsx
1316
+ import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
1317
+ import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
1318
+
1319
+ // src/core/handshake-shape.ts
1320
+ function isPlainObject2(raw) {
1321
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
1322
+ }
1323
+ function isSiteConfigShape(raw) {
1324
+ return isPlainObject2(raw);
1325
+ }
1326
+ function isBlocksConfigShape(raw) {
1327
+ if (!isPlainObject2(raw)) return false;
1328
+ if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
1329
+ if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
1330
+ return true;
1331
+ }
1332
+
1333
+ // src/core/host-commands.ts
1334
+ function commandEventName(cmd) {
1335
+ return `${BRAND.cssPrefix}:${cmd}`;
1336
+ }
1337
+ function bindHostCommands(host, handlers) {
1338
+ const entries = Object.entries(handlers);
1339
+ const wrapped = entries.map(([cmd, fn]) => {
1340
+ const event = commandEventName(cmd);
1341
+ const listener = (e) => fn(e.detail);
1342
+ host.addEventListener(event, listener);
1343
+ return [event, listener];
1344
+ });
1345
+ return () => {
1346
+ for (const [event, listener] of wrapped) host.removeEventListener(event, listener);
1347
+ };
1348
+ }
1349
+
1152
1350
  // src/ui/host-mode.ts
1153
1351
  function computeHostMode(configured, panelSize, isOpen) {
1154
1352
  if (configured === "page") return "page";
@@ -1199,30 +1397,6 @@ function createAuth(opts) {
1199
1397
  };
1200
1398
  }
1201
1399
 
1202
- // src/core/ids.ts
1203
- function uuid7() {
1204
- const ts = Date.now();
1205
- const bytes = new Uint8Array(16);
1206
- bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1207
- bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1208
- bytes[2] = ts >>> 24 & 255;
1209
- bytes[3] = ts >>> 16 & 255;
1210
- bytes[4] = ts >>> 8 & 255;
1211
- bytes[5] = ts & 255;
1212
- fillRandom(bytes.subarray(6));
1213
- bytes[6] = bytes[6] & 15 | 112;
1214
- bytes[8] = bytes[8] & 63 | 128;
1215
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1216
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1217
- }
1218
- function fillRandom(view) {
1219
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1220
- crypto.getRandomValues(view);
1221
- return;
1222
- }
1223
- for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1224
- }
1225
-
1226
1400
  // src/stream/types.ts
1227
1401
  var StreamError = class extends Error {
1228
1402
  constructor(message, code, status, cause) {
@@ -1235,7 +1409,7 @@ var StreamError = class extends Error {
1235
1409
  };
1236
1410
 
1237
1411
  // src/stream/parser.ts
1238
- var log3 = logger.scope("parser");
1412
+ var log4 = logger.scope("parser");
1239
1413
  async function* parseChatStream(response, signal7) {
1240
1414
  if (!response.ok) {
1241
1415
  const text = await response.text().catch(() => "");
@@ -1287,10 +1461,10 @@ ${part}` : part;
1287
1461
  if (!dataPayload || dataPayload === "[DONE]") return null;
1288
1462
  try {
1289
1463
  const chunk = JSON.parse(dataPayload);
1290
- log3.trace("chunk", { type: chunk.type, eventId });
1464
+ log4.trace("chunk", { type: chunk.type, eventId });
1291
1465
  return { chunk, eventId };
1292
1466
  } catch (err) {
1293
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1467
+ log4.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1294
1468
  return null;
1295
1469
  }
1296
1470
  }
@@ -1445,7 +1619,7 @@ function messageToWireParts(m) {
1445
1619
  }
1446
1620
 
1447
1621
  // src/stream/transport.ts
1448
- var log4 = logger.scope("transport");
1622
+ var log5 = logger.scope("transport");
1449
1623
  var MAX_RESUME_ATTEMPTS = 3;
1450
1624
  var RESUME_BACKOFF_MS = 400;
1451
1625
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1571,13 +1745,13 @@ var AgentTransport = class {
1571
1745
  }
1572
1746
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1573
1747
  async handshake(body) {
1574
- log4.debug("handshake \u2192", {
1748
+ log5.debug("handshake \u2192", {
1575
1749
  visitorId: body.visitorId,
1576
1750
  conversationId: body.conversationId,
1577
1751
  locale: body.locale
1578
1752
  });
1579
1753
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1580
- log4.debug("handshake \u2190", {
1754
+ log5.debug("handshake \u2190", {
1581
1755
  visitorId: res.visitorId,
1582
1756
  conversationId: res.conversationId,
1583
1757
  hasWelcome: !!res.welcome,
@@ -1585,7 +1759,7 @@ var AgentTransport = class {
1585
1759
  rebind: res.rebind
1586
1760
  });
1587
1761
  if (!isHandshakeResponseShape(res)) {
1588
- log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1762
+ log5.warn("handshake response did not match expected shape; using raw response", { response: res });
1589
1763
  }
1590
1764
  this.visitorId = res.visitorId ?? body.visitorId;
1591
1765
  this.conversationId = res.conversationId;
@@ -1593,11 +1767,11 @@ var AgentTransport = class {
1593
1767
  }
1594
1768
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1595
1769
  async uploadFile(file) {
1596
- log4.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1770
+ log5.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1597
1771
  const fd = new FormData();
1598
1772
  fd.append("file", file, file.name);
1599
1773
  const res = await this.postForm(this.uploadPath, fd, "upload");
1600
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1774
+ log5.debug("uploadFile \u2190", { id: res.id, url: res.url });
1601
1775
  return res;
1602
1776
  }
1603
1777
  /**
@@ -1605,12 +1779,12 @@ var AgentTransport = class {
1605
1779
  * Only used when `features.voice === 'server'`.
1606
1780
  */
1607
1781
  async transcribe(audio, mimeType = "audio/webm") {
1608
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1782
+ log5.debug("transcribe \u2192", { size: audio.size, mimeType });
1609
1783
  const fd = new FormData();
1610
1784
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1611
1785
  fd.append("mimeType", mimeType);
1612
1786
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1613
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1787
+ log5.debug("transcribe \u2190", { textLen: res.text.length });
1614
1788
  return res;
1615
1789
  }
1616
1790
  async listConversations(params = {}) {
@@ -1618,14 +1792,14 @@ var AgentTransport = class {
1618
1792
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1619
1793
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1620
1794
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1621
- log4.debug("listConversations \u2192", {
1795
+ log5.debug("listConversations \u2192", {
1622
1796
  visitorId: params.visitorId ?? this.visitorId,
1623
1797
  limit: params.limit,
1624
1798
  cursor: params.cursor
1625
1799
  });
1626
1800
  const res = await this.getJson(url.toString(), "listConversations");
1627
1801
  const conversations = res.conversations ?? [];
1628
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1802
+ log5.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1629
1803
  return {
1630
1804
  conversations: conversations.map((conversation) => ({
1631
1805
  conversationId: conversation.conversationId,
@@ -1641,10 +1815,10 @@ var AgentTransport = class {
1641
1815
  async loadConversation(conversationId) {
1642
1816
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1643
1817
  url.searchParams.set("conversationId", conversationId);
1644
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1818
+ log5.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1645
1819
  const res = await this.getJson(url.toString(), "loadConversation");
1646
1820
  const messages = res.messages ?? [];
1647
- log4.debug("loadConversation \u2190", {
1821
+ log5.debug("loadConversation \u2190", {
1648
1822
  conversationId: res.conversationId,
1649
1823
  canContinue: res.canContinue,
1650
1824
  messageCount: messages.length,
@@ -1666,19 +1840,19 @@ var AgentTransport = class {
1666
1840
  */
1667
1841
  async markRead(conversationId) {
1668
1842
  if (!this.visitorId || !conversationId) return;
1669
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1843
+ log5.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1670
1844
  try {
1671
1845
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1672
1846
  } catch (err) {
1673
- log4.debug("markRead failed (non-fatal)", { err });
1847
+ log5.debug("markRead failed (non-fatal)", { err });
1674
1848
  }
1675
1849
  }
1676
1850
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
1677
1851
  listContent(query = {}) {
1678
- const key = JSON.stringify(query, Object.keys(query).toSorted());
1852
+ const key = `${this.locale ?? ""}|${JSON.stringify(query, Object.keys(query).toSorted())}`;
1679
1853
  const cached = this.contentCache.get(key);
1680
1854
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1681
- log4.debug("listContent \u2192 cache", query);
1855
+ log5.debug("listContent \u2192 cache", query);
1682
1856
  return cached.result;
1683
1857
  }
1684
1858
  const result = this.fetchContent(query).catch((err) => {
@@ -1694,10 +1868,10 @@ var AgentTransport = class {
1694
1868
  if (value === void 0) continue;
1695
1869
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1696
1870
  }
1697
- log4.debug("listContent \u2192", query);
1871
+ log5.debug("listContent \u2192", query);
1698
1872
  const res = await this.getJson(url.toString(), "listContent");
1699
- const items = res.items ?? [];
1700
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1873
+ const items = (res.items ?? []).map((item) => overlayContent(item, this.locale));
1874
+ log5.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1701
1875
  return { ...res, items };
1702
1876
  }
1703
1877
  /**
@@ -1715,21 +1889,21 @@ var AgentTransport = class {
1715
1889
  async bootstrap(conversationId) {
1716
1890
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1717
1891
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1718
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1892
+ log5.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1719
1893
  const res = await this.getJson(url.toString(), "bootstrap");
1720
1894
  const forms = res.forms ?? [];
1721
1895
  const formSubmissions = res.formSubmissions ?? [];
1722
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1896
+ log5.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1723
1897
  return { forms, formSubmissions };
1724
1898
  }
1725
1899
  async saveUserPrefs(userPrefs) {
1726
- log4.debug("saveUserPrefs \u2192", {
1900
+ log5.debug("saveUserPrefs \u2192", {
1727
1901
  visitorId: this.visitorId,
1728
1902
  conversationId: this.conversationId,
1729
1903
  settingsKeys: Object.keys(userPrefs)
1730
1904
  });
1731
1905
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1732
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1906
+ log5.debug("saveUserPrefs \u2190", { ok: res.ok });
1733
1907
  return res;
1734
1908
  }
1735
1909
  get uploadPath() {
@@ -1750,15 +1924,15 @@ var AgentTransport = class {
1750
1924
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1751
1925
  */
1752
1926
  async submitForm(body) {
1753
- log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1927
+ log5.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1754
1928
  try {
1755
1929
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1756
1930
  } catch (err) {
1757
- log4.debug("submitForm failed (non-fatal)", { err });
1931
+ log5.debug("submitForm failed (non-fatal)", { err });
1758
1932
  }
1759
1933
  }
1760
1934
  sendMessage(body) {
1761
- log4.debug("message \u2192", {
1935
+ log5.debug("message \u2192", {
1762
1936
  conversationId: body.conversationId,
1763
1937
  visitorId: body.visitorId,
1764
1938
  messageCount: body.messages.length,
@@ -1838,17 +2012,17 @@ var AgentTransport = class {
1838
2012
  });
1839
2013
  } catch (err) {
1840
2014
  if (ctrl.signal.aborted) return;
1841
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
2015
+ log5.debug("resume fetch failed \u2014 retrying", { attempt, err });
1842
2016
  continue;
1843
2017
  }
1844
2018
  if (res.status === 204 || !res.ok) {
1845
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
2019
+ log5.debug("resume \u2192 no in-flight stream", { status: res.status });
1846
2020
  return;
1847
2021
  }
1848
2022
  const before = seenIds.size;
1849
2023
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1850
2024
  if (seenIds.size === before) {
1851
- log4.debug("resume \u2192 stream closed with no new events; nothing to resume");
2025
+ log5.debug("resume \u2192 stream closed with no new events; nothing to resume");
1852
2026
  return;
1853
2027
  }
1854
2028
  }
@@ -1875,14 +2049,14 @@ var AgentTransport = class {
1875
2049
  } catch (err) {
1876
2050
  if (ctrl.signal.aborted) return true;
1877
2051
  if (err instanceof StreamError && err.status !== void 0) throw err;
1878
- log4.debug("stream segment dropped", { err });
2052
+ log5.debug("stream segment dropped", { err });
1879
2053
  }
1880
2054
  return false;
1881
2055
  }
1882
2056
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1883
2057
  cancelStream(ctrl, conversationId) {
1884
2058
  if (ctrl.signal.aborted) return;
1885
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2059
+ log5.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1886
2060
  ctrl.abort();
1887
2061
  if (!this.visitorId || !conversationId) return;
1888
2062
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1891,7 +2065,7 @@ var AgentTransport = class {
1891
2065
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1892
2066
  body: JSON.stringify(this.withEnvelope({})),
1893
2067
  keepalive: true
1894
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2068
+ }).catch((err) => log5.debug("cancel POST failed (non-fatal)", { err }));
1895
2069
  }
1896
2070
  // ---- Low-level fetch helpers ------------------------------------------
1897
2071
  async *openMessageStream(body, signal7) {
@@ -1973,13 +2147,13 @@ var AgentTransport = class {
1973
2147
  if (attempt >= MAX_REQUEST_RETRIES) {
1974
2148
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1975
2149
  }
1976
- log4.debug("request network error \u2014 retrying", { label, attempt });
2150
+ log5.debug("request network error \u2014 retrying", { label, attempt });
1977
2151
  await sleep(backoffMs(attempt));
1978
2152
  continue;
1979
2153
  }
1980
2154
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1981
2155
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1982
- log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
2156
+ log5.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
1983
2157
  await sleep(wait);
1984
2158
  }
1985
2159
  }
@@ -2174,7 +2348,7 @@ var TRIGGER = {
2174
2348
  };
2175
2349
 
2176
2350
  // src/stream/reducer.ts
2177
- var log5 = logger.scope("reducer");
2351
+ var log6 = logger.scope("reducer");
2178
2352
  var StreamReducer = class {
2179
2353
  constructor(messagesSig) {
2180
2354
  __publicField(this, "messagesSig", messagesSig);
@@ -2196,13 +2370,13 @@ var StreamReducer = class {
2196
2370
  case "finish-step":
2197
2371
  return;
2198
2372
  case "finish":
2199
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2373
+ log6.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2200
2374
  m.status = "complete";
2201
2375
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2202
2376
  this.bumpDone(m);
2203
2377
  return;
2204
2378
  case "error":
2205
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2379
+ log6.warn("stream error chunk", { errorText: chunk.errorText });
2206
2380
  m.status = "error";
2207
2381
  m.errorText = chunk.errorText;
2208
2382
  this.bumpDone(m);
@@ -2331,131 +2505,6 @@ function applyTool(m, chunk) {
2331
2505
  }
2332
2506
  }
2333
2507
 
2334
- // src/core/persistence.ts
2335
- var log6 = logger.scope("persistence");
2336
- function createPersistence(widgetId, storage = defaultStorage) {
2337
- const prefix = `${BRAND.cssPrefix}.${widgetId}`;
2338
- const KEY_VISITOR = `${prefix}.visitorId`;
2339
- const KEY_CONVERSATION = `${prefix}.conversationId`;
2340
- const KEY_USER_PREFS = `${prefix}.userPrefs`;
2341
- const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
2342
- const KEY_PANEL_SIZE = `${prefix}.panelSize`;
2343
- const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
2344
- const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
2345
- const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
2346
- const KEY_FORMS_DONE = `${prefix}.formsDone`;
2347
- const persistence = {
2348
- getVisitorId() {
2349
- const existing = storage.get(KEY_VISITOR);
2350
- if (existing) return existing;
2351
- const minted = uuid7();
2352
- storage.set(KEY_VISITOR, minted);
2353
- return minted;
2354
- },
2355
- setVisitorId(id) {
2356
- log6.debug("setVisitorId (rebind)", { id, widgetId });
2357
- storage.set(KEY_VISITOR, id);
2358
- },
2359
- loadConversationId() {
2360
- return storage.get(KEY_CONVERSATION) ?? void 0;
2361
- },
2362
- saveConversationId(id) {
2363
- log6.trace("saveConversationId", { id, widgetId });
2364
- if (id) storage.set(KEY_CONVERSATION, id);
2365
- else storage.remove(KEY_CONVERSATION);
2366
- },
2367
- loadUserPrefs() {
2368
- const raw = storage.get(KEY_USER_PREFS);
2369
- if (!raw) return {};
2370
- try {
2371
- const parsed = JSON.parse(raw);
2372
- return isPlainObject2(parsed) ? parsed : {};
2373
- } catch (error) {
2374
- log6.warn("loadUserPrefs parse failed", { error });
2375
- return {};
2376
- }
2377
- },
2378
- saveUserPrefs(s) {
2379
- try {
2380
- storage.set(KEY_USER_PREFS, JSON.stringify(s));
2381
- } catch (error) {
2382
- log6.warn("saveUserPrefs serialise failed", { error });
2383
- }
2384
- },
2385
- patchUserPrefs(patch) {
2386
- const current = persistence.loadUserPrefs();
2387
- persistence.saveUserPrefs({ ...current, ...patch });
2388
- },
2389
- loadPanelOpen() {
2390
- const raw = storage.get(KEY_PANEL_OPEN);
2391
- if (raw === "1") return true;
2392
- if (raw === "0") return false;
2393
- return null;
2394
- },
2395
- savePanelOpen(open) {
2396
- storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
2397
- },
2398
- loadPanelSize() {
2399
- const raw = storage.get(KEY_PANEL_SIZE);
2400
- return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
2401
- },
2402
- savePanelSize(size) {
2403
- storage.set(KEY_PANEL_SIZE, size);
2404
- },
2405
- loadCalloutDismissed(currentText) {
2406
- if (!currentText) return false;
2407
- const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
2408
- return !!dismissedText && dismissedText === currentText;
2409
- },
2410
- saveCalloutDismissed(currentText) {
2411
- if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
2412
- else storage.remove(KEY_CALLOUT_DISMISSED);
2413
- },
2414
- loadSidebarCollapsed() {
2415
- const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
2416
- if (raw === "1") return true;
2417
- if (raw === "0") return false;
2418
- return null;
2419
- },
2420
- saveSidebarCollapsed(collapsed) {
2421
- storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
2422
- },
2423
- loadActiveModule() {
2424
- return storage.get(KEY_ACTIVE_MODULE) || null;
2425
- },
2426
- saveActiveModule(id) {
2427
- storage.set(KEY_ACTIVE_MODULE, id);
2428
- },
2429
- loadFormsDone() {
2430
- const raw = storage.get(KEY_FORMS_DONE);
2431
- if (!raw) return {};
2432
- try {
2433
- const parsed = JSON.parse(raw);
2434
- return isPlainObject2(parsed) ? parsed : {};
2435
- } catch (error) {
2436
- log6.warn("loadFormsDone parse failed", { error });
2437
- return {};
2438
- }
2439
- },
2440
- saveFormDone(id) {
2441
- try {
2442
- const done = persistence.loadFormsDone();
2443
- done[id] = Date.now();
2444
- storage.set(KEY_FORMS_DONE, JSON.stringify(done));
2445
- } catch (error) {
2446
- log6.warn("saveFormDone serialise failed", { error });
2447
- }
2448
- },
2449
- clearConversation() {
2450
- storage.remove(KEY_CONVERSATION);
2451
- }
2452
- };
2453
- return persistence;
2454
- }
2455
- function isPlainObject2(v) {
2456
- return typeof v === "object" && v !== null && !Array.isArray(v);
2457
- }
2458
-
2459
2508
  // src/core/feedback/index.ts
2460
2509
  import { signal as signal3 } from "@preact/signals";
2461
2510
 
@@ -3081,7 +3130,7 @@ function mountRawSvg(host, source) {
3081
3130
  } catch {
3082
3131
  }
3083
3132
  }
3084
- function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3133
+ function LauncherCallout({ callout, launcherPosition, strings, closeLabel, onDismiss }) {
3085
3134
  const [visible, setVisible] = useState(false);
3086
3135
  useEffect(() => {
3087
3136
  const id = setTimeout(() => setVisible(true), callout.delayMs);
@@ -3103,7 +3152,7 @@ function LauncherCallout({ callout, launcherPosition, closeLabel, onDismiss }) {
3103
3152
  "data-animated": callout.animated ? "true" : void 0,
3104
3153
  "data-testid": TID.callout,
3105
3154
  children: [
3106
- /* @__PURE__ */ jsx2("span", { children: callout.text }),
3155
+ /* @__PURE__ */ jsx2("span", { children: localizeText(strings, callout.text) }),
3107
3156
  /* @__PURE__ */ jsx2(
3108
3157
  "button",
3109
3158
  {
@@ -5663,9 +5712,16 @@ function createController(depsRef) {
5663
5712
  else if (form.frequency !== "always") depsRef.current.persistence.saveFormDone(form.id);
5664
5713
  depsRef.current.onComplete(form, trigger, values, skipped);
5665
5714
  };
5715
+ const refresh = () => {
5716
+ const active = activeForm.value;
5717
+ if (!active) return;
5718
+ const next = depsRef.current.forms.list.find((f) => f.id === active.form.id);
5719
+ if (next && next !== active.form) activeForm.value = { form: next, trigger: active.trigger };
5720
+ };
5666
5721
  return {
5667
5722
  activeForm,
5668
5723
  fire,
5724
+ refresh,
5669
5725
  complete: (values) => finish(values, false),
5670
5726
  skip: () => finish({}, true)
5671
5727
  };
@@ -6077,7 +6133,7 @@ function HomeRoot(props2) {
6077
6133
  const greeting = resolveGreeting(props2);
6078
6134
  const avatars = config.userAvatars ?? [];
6079
6135
  const status = config.status;
6080
- const contentTitle = config.contentBlockTitle ? moduleLabel(strings, config.contentBlockTitle) : strings.homeContentTitle;
6136
+ const contentTitle = config.contentBlockTitle ? localizeText(strings, config.contentBlockTitle) : strings.homeContentTitle;
6081
6137
  return /* @__PURE__ */ jsx29("div", { class: `${p25}-module ${p25}-home`, "data-testid": TID.homeView, children: /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-scroll`, children: [
6082
6138
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero`, children: [
6083
6139
  /* @__PURE__ */ jsxs24("div", { class: `${p25}-home-hero-top`, children: [
@@ -6232,7 +6288,7 @@ function HomeTabBar({ modules, activeTab, strings, unreadCount, onSelect }) {
6232
6288
  /* @__PURE__ */ jsx31(Icon, {}),
6233
6289
  badge ? /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-badge`, "data-testid": TID.tabBadge, children: badge }) : null
6234
6290
  ] }),
6235
- /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: moduleLabel(strings, m.label) })
6291
+ /* @__PURE__ */ jsx31("span", { class: `${p27}-tab-label`, children: localizeText(strings, m.label) })
6236
6292
  ]
6237
6293
  },
6238
6294
  m.id
@@ -6903,7 +6959,7 @@ function App({ options, hostElement, bus }) {
6903
6959
  void (async () => {
6904
6960
  try {
6905
6961
  const res = await dataBootRef.current;
6906
- setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
6962
+ setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
6907
6963
  } catch (err) {
6908
6964
  log16.debug("bootstrap failed \u2014 treating as no forms", { err });
6909
6965
  } finally {
@@ -6921,6 +6977,23 @@ function App({ options, hostElement, bus }) {
6921
6977
  void runResume(handle);
6922
6978
  }
6923
6979
  }, [activated]);
6980
+ const formsLocaleRef = useRef9(null);
6981
+ useEffect17(() => {
6982
+ if (!activated) return;
6983
+ if (formsLocaleRef.current === null) {
6984
+ formsLocaleRef.current = effectiveLocale;
6985
+ return;
6986
+ }
6987
+ if (formsLocaleRef.current === effectiveLocale) return;
6988
+ formsLocaleRef.current = effectiveLocale;
6989
+ let cancelled = false;
6990
+ void transport.bootstrap().then((res) => {
6991
+ if (!cancelled) setRemoteForms(resolveForms((res ?? { forms: [] }).forms, effectiveLocale));
6992
+ }).catch((err) => log16.debug("forms re-localize failed \u2014 keeping current copy", { err }));
6993
+ return () => {
6994
+ cancelled = true;
6995
+ };
6996
+ }, [activated, effectiveLocale, transport]);
6924
6997
  const lastUserSig = useRef9(userSig);
6925
6998
  useEffect17(() => {
6926
6999
  if (!conversationReady || lastUserSig.current === userSig) return;
@@ -7077,6 +7150,9 @@ function App({ options, hostElement, bus }) {
7077
7150
  }
7078
7151
  });
7079
7152
  const activeForm = useComputed7(() => forms.activeForm.value);
7153
+ useEffect17(() => {
7154
+ forms.refresh();
7155
+ }, [effectiveForms, forms]);
7080
7156
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
7081
7157
  const msgCount = useComputed7(() => messagesSig.value.length);
7082
7158
  useEffect17(() => {
@@ -7463,6 +7539,7 @@ function App({ options, hostElement, bus }) {
7463
7539
  {
7464
7540
  callout: calloutToRender,
7465
7541
  launcherPosition: effectiveOptions.position,
7542
+ strings: effectiveOptions.strings,
7466
7543
  closeLabel: effectiveOptions.strings.close,
7467
7544
  onDismiss: dismissCallout
7468
7545
  }
@@ -7511,50 +7588,6 @@ var ErrorBoundary = class extends Component {
7511
7588
  }
7512
7589
  };
7513
7590
 
7514
- // src/index.ts
7515
- import { h, render } from "preact";
7516
-
7517
- // src/shadow/iframe-mount.ts
7518
- var log18 = logger.scope("iframe");
7519
-
7520
- // src/core/events.ts
7521
- var EventBus = class {
7522
- constructor() {
7523
- __publicField(this, "handlers", /* @__PURE__ */ new Map());
7524
- }
7525
- /**
7526
- * Register a listener for `name`.
7527
- * @returns an `off` function that removes this listener.
7528
- */
7529
- on(name, fn) {
7530
- let set = this.handlers.get(name);
7531
- if (!set) {
7532
- set = /* @__PURE__ */ new Set();
7533
- this.handlers.set(name, set);
7534
- }
7535
- set.add(fn);
7536
- return () => this.handlers.get(name)?.delete(fn);
7537
- }
7538
- /**
7539
- * Dispatch an event synchronously to all listeners.
7540
- * Listener errors are swallowed — the widget must not crash on user callbacks.
7541
- */
7542
- emit(name, payload) {
7543
- const set = this.handlers.get(name);
7544
- if (!set) return;
7545
- for (const fn of set) {
7546
- try {
7547
- fn(payload);
7548
- } catch {
7549
- }
7550
- }
7551
- }
7552
- /** Remove every listener. Called by the instance `destroy()`. */
7553
- clear() {
7554
- this.handlers.clear();
7555
- }
7556
- };
7557
-
7558
7591
  // src/core/tracking.ts
7559
7592
  var PROTOCOL_VERSION = "1";
7560
7593
  var MAX_HITS_PER_MINUTE = 60;
@@ -7670,10 +7703,60 @@ function createTracker(bus, deps) {
7670
7703
  };
7671
7704
  }
7672
7705
 
7673
- // src/core/hooks.ts
7674
- var log19 = logger.scope("hooks");
7675
-
7676
- // src/index.ts
7706
+ // src/core/boot.ts
7707
+ function createWidgetRuntime(params) {
7708
+ const { bus, host, appRoot, persistence, reportError } = params;
7709
+ let userRaw = params.initialRaw;
7710
+ let serverConfig;
7711
+ let currentOptions = resolveOptions(userRaw);
7712
+ let firstRender = true;
7713
+ const recompute = () => {
7714
+ currentOptions = resolveOptions(serverConfig ? mergeServerConfig(userRaw, serverConfig) : userRaw);
7715
+ };
7716
+ const applyHostStyling = () => {
7717
+ applyHostPosition(host, hostPositionForMode(currentOptions.mode));
7718
+ const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7719
+ applyHostAttributes(host, cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions);
7720
+ if (firstRender) {
7721
+ applyMode(host, resolveInitialHostMode(currentOptions));
7722
+ firstRender = false;
7723
+ }
7724
+ };
7725
+ const render2 = () => {
7726
+ applyHostStyling();
7727
+ renderPreact(
7728
+ h(ErrorBoundary, { onError: reportError }, h(App, { bus, hostElement: host, options: currentOptions })),
7729
+ appRoot
7730
+ );
7731
+ };
7732
+ bus.on("handshake", (response) => {
7733
+ if (!response.config) return;
7734
+ serverConfig = response.config;
7735
+ recompute();
7736
+ render2();
7737
+ });
7738
+ const untrack = createTracker(bus, {
7739
+ getOptions: () => currentOptions,
7740
+ getVisitorId: () => persistence.getVisitorId(),
7741
+ getConversationId: () => persistence.loadConversationId()
7742
+ });
7743
+ return {
7744
+ getOptions: () => currentOptions,
7745
+ getUserOptions: () => userRaw,
7746
+ render: render2,
7747
+ setUserOptions: (raw) => {
7748
+ userRaw = raw;
7749
+ recompute();
7750
+ render2();
7751
+ },
7752
+ patchUserOptions: (patch) => {
7753
+ userRaw = { ...userRaw, ...patch };
7754
+ recompute();
7755
+ render2();
7756
+ },
7757
+ destroy: () => untrack()
7758
+ };
7759
+ }
7677
7760
  function resolveInitialHostMode(options) {
7678
7761
  if (options.mode === "standalone") return "standalone";
7679
7762
  if (options.mode === "inline") return "inline";
@@ -7687,39 +7770,52 @@ function resolveInitialHostMode(options) {
7687
7770
  return computeHostMode(options.mode, panelSize, panelOpen);
7688
7771
  }
7689
7772
 
7773
+ // src/core/events.ts
7774
+ var EventBus = class {
7775
+ constructor() {
7776
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
7777
+ }
7778
+ /**
7779
+ * Register a listener for `name`.
7780
+ * @returns an `off` function that removes this listener.
7781
+ */
7782
+ on(name, fn) {
7783
+ let set = this.handlers.get(name);
7784
+ if (!set) {
7785
+ set = /* @__PURE__ */ new Set();
7786
+ this.handlers.set(name, set);
7787
+ }
7788
+ set.add(fn);
7789
+ return () => this.handlers.get(name)?.delete(fn);
7790
+ }
7791
+ /**
7792
+ * Dispatch an event synchronously to all listeners.
7793
+ * Listener errors are swallowed — the widget must not crash on user callbacks.
7794
+ */
7795
+ emit(name, payload) {
7796
+ const set = this.handlers.get(name);
7797
+ if (!set) return;
7798
+ for (const fn of set) {
7799
+ try {
7800
+ fn(payload);
7801
+ } catch {
7802
+ }
7803
+ }
7804
+ }
7805
+ /** Remove every listener. Called by the instance `destroy()`. */
7806
+ clear() {
7807
+ this.handlers.clear();
7808
+ }
7809
+ };
7810
+
7690
7811
  // src/web-component.ts
7691
7812
  var ChatElement = class extends HTMLElement {
7692
7813
  constructor() {
7693
7814
  super(...arguments);
7694
7815
  __publicField(this, "bus", new EventBus());
7695
7816
  __publicField(this, "mountResult", null);
7696
- /**
7697
- * Raw options accumulator. Starts as whatever the page-owner set via
7698
- * HTML attributes; gets server-side settings merged in on the
7699
- * `handshake` event so subsequent re-renders pick up the server's
7700
- * mode / theme / launcher / etc.
7701
- *
7702
- * Mirrors `index.ts`'s `rawOptions` so both entry points share the
7703
- * same user-wins-per-field merge policy.
7704
- */
7705
- __publicField(this, "rawOptions", {});
7706
- /**
7707
- * Latest resolved options — refreshed every `renderApp()` so the usage
7708
- * tracker (created once in `boot`) reads live, post-handshake-merge
7709
- * values (`tracking.enabled` / `endpoint` / `token`).
7710
- *
7711
- * This path is independent of `mount()` (it supports N elements per page
7712
- * without a shared `widgetId` registry), so everything `mount()` composes
7713
- * around `<App>` must be mirrored here: the handshake-config merge, the
7714
- * usage tracker, and the ErrorBoundary wrapper. This has drifted three
7715
- * times now (mode-merge, tracker, ErrorBoundary). The proper fix is to
7716
- * extract the shared `<App>` composition into one `bootWidget(opts,
7717
- * { hostElement })` helper both entry points call — tracked as a
7718
- * follow-up; until then, keep these two boot paths in lockstep.
7719
- */
7720
- __publicField(this, "resolved", null);
7721
- /** Tracker unsubscribe — released on disconnect. */
7722
- __publicField(this, "untrack", null);
7817
+ /** Live widget composition — null between disconnect and the next connect. */
7818
+ __publicField(this, "runtime", null);
7723
7819
  }
7724
7820
  static get observedAttributes() {
7725
7821
  return REACTIVE_ATTRS;
@@ -7729,57 +7825,37 @@ var ChatElement = class extends HTMLElement {
7729
7825
  }
7730
7826
  disconnectedCallback() {
7731
7827
  if (!this.mountResult) return;
7732
- this.untrack?.();
7733
- this.untrack = null;
7734
- render2(null, this.mountResult.appRoot);
7828
+ this.runtime?.destroy();
7829
+ this.runtime = null;
7830
+ render(null, this.mountResult.appRoot);
7735
7831
  this.bus.clear();
7736
7832
  }
7737
7833
  attributeChangedCallback() {
7738
7834
  if (this.isConnected) this.boot();
7739
7835
  }
7740
7836
  boot() {
7741
- this.rawOptions = parseAttributes((name) => this.getAttribute(name));
7837
+ const rawOptions = parseAttributes((name) => this.getAttribute(name));
7838
+ if (this.runtime) {
7839
+ this.runtime.setUserOptions(rawOptions);
7840
+ return;
7841
+ }
7842
+ const resolved = resolveOptions(rawOptions);
7742
7843
  if (!this.mountResult) {
7743
- const resolved = resolveOptions(this.rawOptions);
7744
7844
  this.mountResult = createShadowMount({
7745
7845
  existingHost: this,
7746
7846
  position: hostPositionForMode(resolved.mode)
7747
7847
  });
7748
- this.bus.on("handshake", (response) => {
7749
- if (!response.config) return;
7750
- this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
7751
- this.renderApp();
7752
- });
7753
- const persistence = createPersistence(resolved.widgetId, resolved.storage);
7754
- this.untrack = createTracker(this.bus, {
7755
- getOptions: () => this.resolved ?? resolved,
7756
- getVisitorId: () => persistence.getVisitorId(),
7757
- getConversationId: () => persistence.loadConversationId()
7758
- });
7759
7848
  }
7760
- this.renderApp();
7761
- }
7762
- /**
7763
- * Apply the current `rawOptions` to the host element + App tree.
7764
- * Called by both the initial boot and every `handshake` merge.
7765
- */
7766
- renderApp() {
7767
- if (!this.mountResult) return;
7768
- const resolved = resolveOptions(this.rawOptions);
7769
- this.resolved = resolved;
7770
- const host = this.mountResult.hostElement;
7771
- applyHostPosition(host, hostPositionForMode(resolved.mode));
7772
- const cachedThemeMode = createPersistence(resolved.widgetId, resolved.storage).loadUserPrefs().themeMode;
7773
- applyHostAttributes(host, cachedThemeMode ? { ...resolved, themeMode: cachedThemeMode } : resolved);
7774
- applyMode(host, resolveInitialHostMode(resolved));
7775
- render2(
7776
- h2(
7777
- ErrorBoundary,
7778
- { onError: (error) => this.bus.emit("error", error) },
7779
- h2(App, { bus: this.bus, hostElement: host, options: resolved })
7780
- ),
7781
- this.mountResult.appRoot
7782
- );
7849
+ const persistence = createPersistence(resolved.widgetId, resolved.storage);
7850
+ this.runtime = createWidgetRuntime({
7851
+ bus: this.bus,
7852
+ host: this.mountResult.hostElement,
7853
+ appRoot: this.mountResult.appRoot,
7854
+ persistence,
7855
+ initialRaw: rawOptions,
7856
+ reportError: (error) => this.bus.emit("error", error)
7857
+ });
7858
+ this.runtime.render();
7783
7859
  }
7784
7860
  };
7785
7861
  var registered = false;