@helpai/elements 0.29.1 → 0.30.1

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 = {
@@ -509,8 +505,9 @@ function resolveOptions(rawOpts) {
509
505
  const header = opts.header ?? {};
510
506
  const { themeMode, themeOverrides } = parseTheme(opts.theme);
511
507
  const mode = presentation.mode ?? "floating";
512
- const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
513
- const availableLocales = i18n.availableLocales ?? [locale];
508
+ const defaultLocale = resolveDefaultLocale(i18n.defaultLocale);
509
+ const availableLocales = i18n.availableLocales ?? [i18n.locale ?? defaultLocale];
510
+ const locale = clampLocale(i18n.locale ?? defaultLocale, availableLocales, defaultLocale);
514
511
  const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
515
512
  const agentApiBaseUrl = (opts.agentApiBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
516
513
  const dataApiBaseUrl = (opts.dataApiBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
@@ -552,6 +549,7 @@ function resolveOptions(rawOpts) {
552
549
  maxCount: opts.composer?.attachments?.maxCount ?? DEFAULT_ATTACHMENTS.maxCount
553
550
  },
554
551
  locale,
552
+ defaultLocale,
555
553
  availableLocales,
556
554
  startMinimized: behavior.startMinimized ?? true,
557
555
  actions: resolveActions(mode, header.actions),
@@ -582,6 +580,11 @@ function resolveDefaultLocale(input) {
582
580
  if (input && input !== "auto") return input;
583
581
  return typeof navigator !== "undefined" && navigator.language ? navigator.language : "en";
584
582
  }
583
+ function clampLocale(desired, availableLocales, defaultLocale) {
584
+ if (availableLocales.length <= 1 || availableLocales.includes(desired)) return desired;
585
+ if (availableLocales.includes(defaultLocale)) return defaultLocale;
586
+ return availableLocales[0] ?? desired;
587
+ }
585
588
  function resolveActions(mode, overrides) {
586
589
  return overrides ?? DEFAULT_ACTIONS_BY_MODE[mode];
587
590
  }
@@ -730,13 +733,14 @@ function resolveLauncher(overrides) {
730
733
 
731
734
  // src/core/config/overrides.ts
732
735
  function applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize) {
733
- if (activeLocale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
736
+ const locale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
737
+ if (locale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
734
738
  return options;
735
739
  }
736
740
  const next = { ...options };
737
- if (activeLocale !== options.locale) {
738
- next.locale = activeLocale;
739
- next.strings = resolveStrings(activeLocale, options.stringsOverride);
741
+ if (locale !== options.locale) {
742
+ next.locale = locale;
743
+ next.strings = resolveStrings(locale, options.stringsOverride);
740
744
  }
741
745
  if (activeThemeMode !== options.themeMode) next.themeMode = activeThemeMode;
742
746
  if (activeTextSize !== options.textSize) next.textSize = activeTextSize;
@@ -782,6 +786,8 @@ function mergeServerConfig(user, server) {
782
786
  serverI18n ?? {},
783
787
  userI18n
784
788
  );
789
+ if (serverI18n?.availableLocales !== void 0) merged.availableLocales = serverI18n.availableLocales;
790
+ if (serverI18n?.defaultLocale !== void 0) merged.defaultLocale = serverI18n.defaultLocale;
785
791
  const userStrings = userI18n?.strings;
786
792
  const serverStrings = serverI18n?.strings;
787
793
  if (userStrings || serverStrings) {
@@ -983,35 +989,153 @@ function parseLauncher(get, str2) {
983
989
  return Object.keys(base).length > 0 ? base : null;
984
990
  }
985
991
 
986
- // src/core/handshake-shape.ts
987
- function isPlainObject(raw) {
988
- return !!raw && typeof raw === "object" && !Array.isArray(raw);
989
- }
990
- function isSiteConfigShape(raw) {
991
- return isPlainObject(raw);
992
+ // src/core/ids.ts
993
+ function uuid7() {
994
+ const ts = Date.now();
995
+ const bytes = new Uint8Array(16);
996
+ bytes[0] = Math.floor(ts / 2 ** 40) & 255;
997
+ bytes[1] = Math.floor(ts / 2 ** 32) & 255;
998
+ bytes[2] = ts >>> 24 & 255;
999
+ bytes[3] = ts >>> 16 & 255;
1000
+ bytes[4] = ts >>> 8 & 255;
1001
+ bytes[5] = ts & 255;
1002
+ fillRandom(bytes.subarray(6));
1003
+ bytes[6] = bytes[6] & 15 | 112;
1004
+ bytes[8] = bytes[8] & 63 | 128;
1005
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1006
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
992
1007
  }
993
- function isBlocksConfigShape(raw) {
994
- if (!isPlainObject(raw)) return false;
995
- if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
996
- if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
997
- return true;
1008
+ function fillRandom(view) {
1009
+ if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1010
+ crypto.getRandomValues(view);
1011
+ return;
1012
+ }
1013
+ for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
998
1014
  }
999
1015
 
1000
- // src/core/host-commands.ts
1001
- function commandEventName(cmd) {
1002
- return `${BRAND.cssPrefix}:${cmd}`;
1003
- }
1004
- function bindHostCommands(host, handlers) {
1005
- const entries = Object.entries(handlers);
1006
- const wrapped = entries.map(([cmd, fn]) => {
1007
- const event = commandEventName(cmd);
1008
- const listener = (e) => fn(e.detail);
1009
- host.addEventListener(event, listener);
1010
- return [event, listener];
1011
- });
1012
- return () => {
1013
- for (const [event, listener] of wrapped) host.removeEventListener(event, listener);
1016
+ // src/core/persistence.ts
1017
+ var log2 = logger.scope("persistence");
1018
+ function createPersistence(widgetId, storage = defaultStorage) {
1019
+ const prefix = `${BRAND.cssPrefix}.${widgetId}`;
1020
+ const KEY_VISITOR = `${prefix}.visitorId`;
1021
+ const KEY_CONVERSATION = `${prefix}.conversationId`;
1022
+ const KEY_USER_PREFS = `${prefix}.userPrefs`;
1023
+ const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
1024
+ const KEY_PANEL_SIZE = `${prefix}.panelSize`;
1025
+ const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
1026
+ const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
1027
+ const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
1028
+ const KEY_FORMS_DONE = `${prefix}.formsDone`;
1029
+ const persistence = {
1030
+ getVisitorId() {
1031
+ const existing = storage.get(KEY_VISITOR);
1032
+ if (existing) return existing;
1033
+ const minted = uuid7();
1034
+ storage.set(KEY_VISITOR, minted);
1035
+ return minted;
1036
+ },
1037
+ setVisitorId(id) {
1038
+ log2.debug("setVisitorId (rebind)", { id, widgetId });
1039
+ storage.set(KEY_VISITOR, id);
1040
+ },
1041
+ loadConversationId() {
1042
+ return storage.get(KEY_CONVERSATION) ?? void 0;
1043
+ },
1044
+ saveConversationId(id) {
1045
+ log2.trace("saveConversationId", { id, widgetId });
1046
+ if (id) storage.set(KEY_CONVERSATION, id);
1047
+ else storage.remove(KEY_CONVERSATION);
1048
+ },
1049
+ loadUserPrefs() {
1050
+ const raw = storage.get(KEY_USER_PREFS);
1051
+ if (!raw) return {};
1052
+ try {
1053
+ const parsed = JSON.parse(raw);
1054
+ return isPlainObject(parsed) ? parsed : {};
1055
+ } catch (error) {
1056
+ log2.warn("loadUserPrefs parse failed", { error });
1057
+ return {};
1058
+ }
1059
+ },
1060
+ saveUserPrefs(s) {
1061
+ try {
1062
+ storage.set(KEY_USER_PREFS, JSON.stringify(s));
1063
+ } catch (error) {
1064
+ log2.warn("saveUserPrefs serialise failed", { error });
1065
+ }
1066
+ },
1067
+ patchUserPrefs(patch) {
1068
+ const current = persistence.loadUserPrefs();
1069
+ persistence.saveUserPrefs({ ...current, ...patch });
1070
+ },
1071
+ loadPanelOpen() {
1072
+ const raw = storage.get(KEY_PANEL_OPEN);
1073
+ if (raw === "1") return true;
1074
+ if (raw === "0") return false;
1075
+ return null;
1076
+ },
1077
+ savePanelOpen(open) {
1078
+ storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
1079
+ },
1080
+ loadPanelSize() {
1081
+ const raw = storage.get(KEY_PANEL_SIZE);
1082
+ return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
1083
+ },
1084
+ savePanelSize(size) {
1085
+ storage.set(KEY_PANEL_SIZE, size);
1086
+ },
1087
+ loadCalloutDismissed(currentText) {
1088
+ if (!currentText) return false;
1089
+ const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
1090
+ return !!dismissedText && dismissedText === currentText;
1091
+ },
1092
+ saveCalloutDismissed(currentText) {
1093
+ if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
1094
+ else storage.remove(KEY_CALLOUT_DISMISSED);
1095
+ },
1096
+ loadSidebarCollapsed() {
1097
+ const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
1098
+ if (raw === "1") return true;
1099
+ if (raw === "0") return false;
1100
+ return null;
1101
+ },
1102
+ saveSidebarCollapsed(collapsed) {
1103
+ storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
1104
+ },
1105
+ loadActiveModule() {
1106
+ return storage.get(KEY_ACTIVE_MODULE) || null;
1107
+ },
1108
+ saveActiveModule(id) {
1109
+ storage.set(KEY_ACTIVE_MODULE, id);
1110
+ },
1111
+ loadFormsDone() {
1112
+ const raw = storage.get(KEY_FORMS_DONE);
1113
+ if (!raw) return {};
1114
+ try {
1115
+ const parsed = JSON.parse(raw);
1116
+ return isPlainObject(parsed) ? parsed : {};
1117
+ } catch (error) {
1118
+ log2.warn("loadFormsDone parse failed", { error });
1119
+ return {};
1120
+ }
1121
+ },
1122
+ saveFormDone(id) {
1123
+ try {
1124
+ const done = persistence.loadFormsDone();
1125
+ done[id] = Date.now();
1126
+ storage.set(KEY_FORMS_DONE, JSON.stringify(done));
1127
+ } catch (error) {
1128
+ log2.warn("saveFormDone serialise failed", { error });
1129
+ }
1130
+ },
1131
+ clearConversation() {
1132
+ storage.remove(KEY_CONVERSATION);
1133
+ }
1014
1134
  };
1135
+ return persistence;
1136
+ }
1137
+ function isPlainObject(v) {
1138
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1015
1139
  }
1016
1140
 
1017
1141
  // src/styles/tokens.css
@@ -1038,7 +1162,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
1038
1162
  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);
1039
1163
 
1040
1164
  // src/shadow/mount.ts
1041
- var log2 = logger.scope("mount");
1165
+ var log3 = logger.scope("mount");
1042
1166
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
1043
1167
  var HOST_CSS = {
1044
1168
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1055,7 +1179,7 @@ function hostPositionForMode(mode) {
1055
1179
  }
1056
1180
  function createShadowMount(opts = {}) {
1057
1181
  const position = opts.position ?? "flow";
1058
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1182
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1059
1183
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1060
1184
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1061
1185
  customElements.define(tagName, class extends HTMLElement {
@@ -1139,6 +1263,44 @@ function applyHostAttributes(host, resolved) {
1139
1263
  applySize(host, resolved.size);
1140
1264
  }
1141
1265
 
1266
+ // src/core/boot.ts
1267
+ import { h, render as renderPreact } from "preact";
1268
+
1269
+ // src/ui/app.tsx
1270
+ import { useCallback as useCallback6, useEffect as useEffect17, useMemo as useMemo3, useRef as useRef9, useState as useState13 } from "preact/hooks";
1271
+ import { useComputed as useComputed7, useSignal as useSignal2 } from "@preact/signals";
1272
+
1273
+ // src/core/handshake-shape.ts
1274
+ function isPlainObject2(raw) {
1275
+ return !!raw && typeof raw === "object" && !Array.isArray(raw);
1276
+ }
1277
+ function isSiteConfigShape(raw) {
1278
+ return isPlainObject2(raw);
1279
+ }
1280
+ function isBlocksConfigShape(raw) {
1281
+ if (!isPlainObject2(raw)) return false;
1282
+ if (raw.navigation !== void 0 && !Array.isArray(raw.navigation)) return false;
1283
+ if (raw.linkCards !== void 0 && !Array.isArray(raw.linkCards)) return false;
1284
+ return true;
1285
+ }
1286
+
1287
+ // src/core/host-commands.ts
1288
+ function commandEventName(cmd) {
1289
+ return `${BRAND.cssPrefix}:${cmd}`;
1290
+ }
1291
+ function bindHostCommands(host, handlers) {
1292
+ const entries = Object.entries(handlers);
1293
+ const wrapped = entries.map(([cmd, fn]) => {
1294
+ const event = commandEventName(cmd);
1295
+ const listener = (e) => fn(e.detail);
1296
+ host.addEventListener(event, listener);
1297
+ return [event, listener];
1298
+ });
1299
+ return () => {
1300
+ for (const [event, listener] of wrapped) host.removeEventListener(event, listener);
1301
+ };
1302
+ }
1303
+
1142
1304
  // src/ui/host-mode.ts
1143
1305
  function computeHostMode(configured, panelSize, isOpen) {
1144
1306
  if (configured === "page") return "page";
@@ -1189,30 +1351,6 @@ function createAuth(opts) {
1189
1351
  };
1190
1352
  }
1191
1353
 
1192
- // src/core/ids.ts
1193
- function uuid7() {
1194
- const ts = Date.now();
1195
- const bytes = new Uint8Array(16);
1196
- bytes[0] = Math.floor(ts / 2 ** 40) & 255;
1197
- bytes[1] = Math.floor(ts / 2 ** 32) & 255;
1198
- bytes[2] = ts >>> 24 & 255;
1199
- bytes[3] = ts >>> 16 & 255;
1200
- bytes[4] = ts >>> 8 & 255;
1201
- bytes[5] = ts & 255;
1202
- fillRandom(bytes.subarray(6));
1203
- bytes[6] = bytes[6] & 15 | 112;
1204
- bytes[8] = bytes[8] & 63 | 128;
1205
- const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
1206
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
1207
- }
1208
- function fillRandom(view) {
1209
- if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") {
1210
- crypto.getRandomValues(view);
1211
- return;
1212
- }
1213
- for (let i = 0; i < view.length; i++) view[i] = Math.floor(Math.random() * 256);
1214
- }
1215
-
1216
1354
  // src/stream/types.ts
1217
1355
  var StreamError = class extends Error {
1218
1356
  constructor(message, code, status, cause) {
@@ -1225,7 +1363,7 @@ var StreamError = class extends Error {
1225
1363
  };
1226
1364
 
1227
1365
  // src/stream/parser.ts
1228
- var log3 = logger.scope("parser");
1366
+ var log4 = logger.scope("parser");
1229
1367
  async function* parseChatStream(response, signal7) {
1230
1368
  if (!response.ok) {
1231
1369
  const text = await response.text().catch(() => "");
@@ -1277,10 +1415,10 @@ ${part}` : part;
1277
1415
  if (!dataPayload || dataPayload === "[DONE]") return null;
1278
1416
  try {
1279
1417
  const chunk = JSON.parse(dataPayload);
1280
- log3.trace("chunk", { type: chunk.type, eventId });
1418
+ log4.trace("chunk", { type: chunk.type, eventId });
1281
1419
  return { chunk, eventId };
1282
1420
  } catch (err) {
1283
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1421
+ log4.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1284
1422
  return null;
1285
1423
  }
1286
1424
  }
@@ -1435,7 +1573,7 @@ function messageToWireParts(m) {
1435
1573
  }
1436
1574
 
1437
1575
  // src/stream/transport.ts
1438
- var log4 = logger.scope("transport");
1576
+ var log5 = logger.scope("transport");
1439
1577
  var MAX_RESUME_ATTEMPTS = 3;
1440
1578
  var RESUME_BACKOFF_MS = 400;
1441
1579
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1477,6 +1615,12 @@ var AgentTransport = class {
1477
1615
  // always sees the visitor's latest context as they navigate.
1478
1616
  __publicField(this, "userContext");
1479
1617
  __publicField(this, "pageContext");
1618
+ // Active BCP-47 locale, carried as the envelope's `locale` on EVERY request
1619
+ // so the backend can localize replies AND the data module can return
1620
+ // localized content + forms. Refreshed via `setLocale` whenever the visitor
1621
+ // switches (no re-handshake needed). Resolved + clamped to the deployment's
1622
+ // availableLocales by `resolveOptions` before it reaches here.
1623
+ __publicField(this, "locale");
1480
1624
  // ---- Data API (content / forms — module-help-ai-data-api) --------------
1481
1625
  //
1482
1626
  // The DATA surfaces live on `dataApiBaseUrl` (default `${baseUrl}/api/data`)
@@ -1502,6 +1646,15 @@ var AgentTransport = class {
1502
1646
  this.userContext = userContext;
1503
1647
  this.pageContext = pageContext;
1504
1648
  }
1649
+ /**
1650
+ * Update the active locale carried on every subsequent request. Call on
1651
+ * mount (before the first handshake) and whenever the user picks a new
1652
+ * locale — the next content / forms / message request reflects it with no
1653
+ * extra round-trip.
1654
+ */
1655
+ setLocale(locale) {
1656
+ this.locale = locale;
1657
+ }
1505
1658
  /**
1506
1659
  * Seed the visitor + conversation identity from persistence BEFORE the first
1507
1660
  * `handshake` resolves. A mount-time `resumeStream()` runs in parallel
@@ -1518,6 +1671,7 @@ var AgentTransport = class {
1518
1671
  const out = {};
1519
1672
  if (this.visitorId) out.visitorId = this.visitorId;
1520
1673
  if (this.conversationId) out.conversationId = this.conversationId;
1674
+ if (this.locale) out.locale = this.locale;
1521
1675
  const context = encodeContext(this.userContext, this.pageContext);
1522
1676
  if (context) out[CONTEXT_PARAM] = context;
1523
1677
  return out;
@@ -1545,13 +1699,13 @@ var AgentTransport = class {
1545
1699
  }
1546
1700
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1547
1701
  async handshake(body) {
1548
- log4.debug("handshake \u2192", {
1702
+ log5.debug("handshake \u2192", {
1549
1703
  visitorId: body.visitorId,
1550
1704
  conversationId: body.conversationId,
1551
1705
  locale: body.locale
1552
1706
  });
1553
1707
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1554
- log4.debug("handshake \u2190", {
1708
+ log5.debug("handshake \u2190", {
1555
1709
  visitorId: res.visitorId,
1556
1710
  conversationId: res.conversationId,
1557
1711
  hasWelcome: !!res.welcome,
@@ -1559,7 +1713,7 @@ var AgentTransport = class {
1559
1713
  rebind: res.rebind
1560
1714
  });
1561
1715
  if (!isHandshakeResponseShape(res)) {
1562
- log4.warn("handshake response did not match expected shape; using raw response", { response: res });
1716
+ log5.warn("handshake response did not match expected shape; using raw response", { response: res });
1563
1717
  }
1564
1718
  this.visitorId = res.visitorId ?? body.visitorId;
1565
1719
  this.conversationId = res.conversationId;
@@ -1567,11 +1721,11 @@ var AgentTransport = class {
1567
1721
  }
1568
1722
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1569
1723
  async uploadFile(file) {
1570
- log4.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1724
+ log5.debug("uploadFile \u2192", { name: file.name, size: file.size, type: file.type });
1571
1725
  const fd = new FormData();
1572
1726
  fd.append("file", file, file.name);
1573
1727
  const res = await this.postForm(this.uploadPath, fd, "upload");
1574
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1728
+ log5.debug("uploadFile \u2190", { id: res.id, url: res.url });
1575
1729
  return res;
1576
1730
  }
1577
1731
  /**
@@ -1579,12 +1733,12 @@ var AgentTransport = class {
1579
1733
  * Only used when `features.voice === 'server'`.
1580
1734
  */
1581
1735
  async transcribe(audio, mimeType = "audio/webm") {
1582
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1736
+ log5.debug("transcribe \u2192", { size: audio.size, mimeType });
1583
1737
  const fd = new FormData();
1584
1738
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1585
1739
  fd.append("mimeType", mimeType);
1586
1740
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1587
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1741
+ log5.debug("transcribe \u2190", { textLen: res.text.length });
1588
1742
  return res;
1589
1743
  }
1590
1744
  async listConversations(params = {}) {
@@ -1592,14 +1746,14 @@ var AgentTransport = class {
1592
1746
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1593
1747
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1594
1748
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1595
- log4.debug("listConversations \u2192", {
1749
+ log5.debug("listConversations \u2192", {
1596
1750
  visitorId: params.visitorId ?? this.visitorId,
1597
1751
  limit: params.limit,
1598
1752
  cursor: params.cursor
1599
1753
  });
1600
1754
  const res = await this.getJson(url.toString(), "listConversations");
1601
1755
  const conversations = res.conversations ?? [];
1602
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1756
+ log5.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1603
1757
  return {
1604
1758
  conversations: conversations.map((conversation) => ({
1605
1759
  conversationId: conversation.conversationId,
@@ -1615,10 +1769,10 @@ var AgentTransport = class {
1615
1769
  async loadConversation(conversationId) {
1616
1770
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1617
1771
  url.searchParams.set("conversationId", conversationId);
1618
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1772
+ log5.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1619
1773
  const res = await this.getJson(url.toString(), "loadConversation");
1620
1774
  const messages = res.messages ?? [];
1621
- log4.debug("loadConversation \u2190", {
1775
+ log5.debug("loadConversation \u2190", {
1622
1776
  conversationId: res.conversationId,
1623
1777
  canContinue: res.canContinue,
1624
1778
  messageCount: messages.length,
@@ -1640,11 +1794,11 @@ var AgentTransport = class {
1640
1794
  */
1641
1795
  async markRead(conversationId) {
1642
1796
  if (!this.visitorId || !conversationId) return;
1643
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1797
+ log5.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1644
1798
  try {
1645
1799
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1646
1800
  } catch (err) {
1647
- log4.debug("markRead failed (non-fatal)", { err });
1801
+ log5.debug("markRead failed (non-fatal)", { err });
1648
1802
  }
1649
1803
  }
1650
1804
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
@@ -1652,7 +1806,7 @@ var AgentTransport = class {
1652
1806
  const key = JSON.stringify(query, Object.keys(query).toSorted());
1653
1807
  const cached = this.contentCache.get(key);
1654
1808
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1655
- log4.debug("listContent \u2192 cache", query);
1809
+ log5.debug("listContent \u2192 cache", query);
1656
1810
  return cached.result;
1657
1811
  }
1658
1812
  const result = this.fetchContent(query).catch((err) => {
@@ -1668,10 +1822,10 @@ var AgentTransport = class {
1668
1822
  if (value === void 0) continue;
1669
1823
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1670
1824
  }
1671
- log4.debug("listContent \u2192", query);
1825
+ log5.debug("listContent \u2192", query);
1672
1826
  const res = await this.getJson(url.toString(), "listContent");
1673
1827
  const items = res.items ?? [];
1674
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1828
+ log5.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1675
1829
  return { ...res, items };
1676
1830
  }
1677
1831
  /**
@@ -1689,21 +1843,21 @@ var AgentTransport = class {
1689
1843
  async bootstrap(conversationId) {
1690
1844
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1691
1845
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1692
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1846
+ log5.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1693
1847
  const res = await this.getJson(url.toString(), "bootstrap");
1694
1848
  const forms = res.forms ?? [];
1695
1849
  const formSubmissions = res.formSubmissions ?? [];
1696
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1850
+ log5.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1697
1851
  return { forms, formSubmissions };
1698
1852
  }
1699
1853
  async saveUserPrefs(userPrefs) {
1700
- log4.debug("saveUserPrefs \u2192", {
1854
+ log5.debug("saveUserPrefs \u2192", {
1701
1855
  visitorId: this.visitorId,
1702
1856
  conversationId: this.conversationId,
1703
1857
  settingsKeys: Object.keys(userPrefs)
1704
1858
  });
1705
1859
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1706
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1860
+ log5.debug("saveUserPrefs \u2190", { ok: res.ok });
1707
1861
  return res;
1708
1862
  }
1709
1863
  get uploadPath() {
@@ -1724,15 +1878,15 @@ var AgentTransport = class {
1724
1878
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1725
1879
  */
1726
1880
  async submitForm(body) {
1727
- log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1881
+ log5.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1728
1882
  try {
1729
1883
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1730
1884
  } catch (err) {
1731
- log4.debug("submitForm failed (non-fatal)", { err });
1885
+ log5.debug("submitForm failed (non-fatal)", { err });
1732
1886
  }
1733
1887
  }
1734
1888
  sendMessage(body) {
1735
- log4.debug("message \u2192", {
1889
+ log5.debug("message \u2192", {
1736
1890
  conversationId: body.conversationId,
1737
1891
  visitorId: body.visitorId,
1738
1892
  messageCount: body.messages.length,
@@ -1812,17 +1966,17 @@ var AgentTransport = class {
1812
1966
  });
1813
1967
  } catch (err) {
1814
1968
  if (ctrl.signal.aborted) return;
1815
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
1969
+ log5.debug("resume fetch failed \u2014 retrying", { attempt, err });
1816
1970
  continue;
1817
1971
  }
1818
1972
  if (res.status === 204 || !res.ok) {
1819
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
1973
+ log5.debug("resume \u2192 no in-flight stream", { status: res.status });
1820
1974
  return;
1821
1975
  }
1822
1976
  const before = seenIds.size;
1823
1977
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1824
1978
  if (seenIds.size === before) {
1825
- log4.debug("resume \u2192 stream closed with no new events; nothing to resume");
1979
+ log5.debug("resume \u2192 stream closed with no new events; nothing to resume");
1826
1980
  return;
1827
1981
  }
1828
1982
  }
@@ -1849,14 +2003,14 @@ var AgentTransport = class {
1849
2003
  } catch (err) {
1850
2004
  if (ctrl.signal.aborted) return true;
1851
2005
  if (err instanceof StreamError && err.status !== void 0) throw err;
1852
- log4.debug("stream segment dropped", { err });
2006
+ log5.debug("stream segment dropped", { err });
1853
2007
  }
1854
2008
  return false;
1855
2009
  }
1856
2010
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1857
2011
  cancelStream(ctrl, conversationId) {
1858
2012
  if (ctrl.signal.aborted) return;
1859
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2013
+ log5.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1860
2014
  ctrl.abort();
1861
2015
  if (!this.visitorId || !conversationId) return;
1862
2016
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1865,7 +2019,7 @@ var AgentTransport = class {
1865
2019
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1866
2020
  body: JSON.stringify(this.withEnvelope({})),
1867
2021
  keepalive: true
1868
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2022
+ }).catch((err) => log5.debug("cancel POST failed (non-fatal)", { err }));
1869
2023
  }
1870
2024
  // ---- Low-level fetch helpers ------------------------------------------
1871
2025
  async *openMessageStream(body, signal7) {
@@ -1947,13 +2101,13 @@ var AgentTransport = class {
1947
2101
  if (attempt >= MAX_REQUEST_RETRIES) {
1948
2102
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1949
2103
  }
1950
- log4.debug("request network error \u2014 retrying", { label, attempt });
2104
+ log5.debug("request network error \u2014 retrying", { label, attempt });
1951
2105
  await sleep(backoffMs(attempt));
1952
2106
  continue;
1953
2107
  }
1954
2108
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1955
2109
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1956
- log4.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
2110
+ log5.debug("request retryable status \u2014 retrying", { label, status: res.status, attempt, wait });
1957
2111
  await sleep(wait);
1958
2112
  }
1959
2113
  }
@@ -2148,7 +2302,7 @@ var TRIGGER = {
2148
2302
  };
2149
2303
 
2150
2304
  // src/stream/reducer.ts
2151
- var log5 = logger.scope("reducer");
2305
+ var log6 = logger.scope("reducer");
2152
2306
  var StreamReducer = class {
2153
2307
  constructor(messagesSig) {
2154
2308
  __publicField(this, "messagesSig", messagesSig);
@@ -2170,13 +2324,13 @@ var StreamReducer = class {
2170
2324
  case "finish-step":
2171
2325
  return;
2172
2326
  case "finish":
2173
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2327
+ log6.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2174
2328
  m.status = "complete";
2175
2329
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2176
2330
  this.bumpDone(m);
2177
2331
  return;
2178
2332
  case "error":
2179
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2333
+ log6.warn("stream error chunk", { errorText: chunk.errorText });
2180
2334
  m.status = "error";
2181
2335
  m.errorText = chunk.errorText;
2182
2336
  this.bumpDone(m);
@@ -2305,131 +2459,6 @@ function applyTool(m, chunk) {
2305
2459
  }
2306
2460
  }
2307
2461
 
2308
- // src/core/persistence.ts
2309
- var log6 = logger.scope("persistence");
2310
- function createPersistence(widgetId, storage = defaultStorage) {
2311
- const prefix = `${BRAND.cssPrefix}.${widgetId}`;
2312
- const KEY_VISITOR = `${prefix}.visitorId`;
2313
- const KEY_CONVERSATION = `${prefix}.conversationId`;
2314
- const KEY_USER_PREFS = `${prefix}.userPrefs`;
2315
- const KEY_PANEL_OPEN = `${prefix}.panelOpen`;
2316
- const KEY_PANEL_SIZE = `${prefix}.panelSize`;
2317
- const KEY_CALLOUT_DISMISSED = `${prefix}.calloutDismissed`;
2318
- const KEY_SIDEBAR_COLLAPSED = `${prefix}.sidebarCollapsed`;
2319
- const KEY_ACTIVE_MODULE = `${prefix}.activeModule`;
2320
- const KEY_FORMS_DONE = `${prefix}.formsDone`;
2321
- const persistence = {
2322
- getVisitorId() {
2323
- const existing = storage.get(KEY_VISITOR);
2324
- if (existing) return existing;
2325
- const minted = uuid7();
2326
- storage.set(KEY_VISITOR, minted);
2327
- return minted;
2328
- },
2329
- setVisitorId(id) {
2330
- log6.debug("setVisitorId (rebind)", { id, widgetId });
2331
- storage.set(KEY_VISITOR, id);
2332
- },
2333
- loadConversationId() {
2334
- return storage.get(KEY_CONVERSATION) ?? void 0;
2335
- },
2336
- saveConversationId(id) {
2337
- log6.trace("saveConversationId", { id, widgetId });
2338
- if (id) storage.set(KEY_CONVERSATION, id);
2339
- else storage.remove(KEY_CONVERSATION);
2340
- },
2341
- loadUserPrefs() {
2342
- const raw = storage.get(KEY_USER_PREFS);
2343
- if (!raw) return {};
2344
- try {
2345
- const parsed = JSON.parse(raw);
2346
- return isPlainObject2(parsed) ? parsed : {};
2347
- } catch (error) {
2348
- log6.warn("loadUserPrefs parse failed", { error });
2349
- return {};
2350
- }
2351
- },
2352
- saveUserPrefs(s) {
2353
- try {
2354
- storage.set(KEY_USER_PREFS, JSON.stringify(s));
2355
- } catch (error) {
2356
- log6.warn("saveUserPrefs serialise failed", { error });
2357
- }
2358
- },
2359
- patchUserPrefs(patch) {
2360
- const current = persistence.loadUserPrefs();
2361
- persistence.saveUserPrefs({ ...current, ...patch });
2362
- },
2363
- loadPanelOpen() {
2364
- const raw = storage.get(KEY_PANEL_OPEN);
2365
- if (raw === "1") return true;
2366
- if (raw === "0") return false;
2367
- return null;
2368
- },
2369
- savePanelOpen(open) {
2370
- storage.set(KEY_PANEL_OPEN, open ? "1" : "0");
2371
- },
2372
- loadPanelSize() {
2373
- const raw = storage.get(KEY_PANEL_SIZE);
2374
- return raw === "normal" || raw === "expanded" || raw === "fullscreen" ? raw : null;
2375
- },
2376
- savePanelSize(size) {
2377
- storage.set(KEY_PANEL_SIZE, size);
2378
- },
2379
- loadCalloutDismissed(currentText) {
2380
- if (!currentText) return false;
2381
- const dismissedText = storage.get(KEY_CALLOUT_DISMISSED);
2382
- return !!dismissedText && dismissedText === currentText;
2383
- },
2384
- saveCalloutDismissed(currentText) {
2385
- if (currentText) storage.set(KEY_CALLOUT_DISMISSED, currentText);
2386
- else storage.remove(KEY_CALLOUT_DISMISSED);
2387
- },
2388
- loadSidebarCollapsed() {
2389
- const raw = storage.get(KEY_SIDEBAR_COLLAPSED);
2390
- if (raw === "1") return true;
2391
- if (raw === "0") return false;
2392
- return null;
2393
- },
2394
- saveSidebarCollapsed(collapsed) {
2395
- storage.set(KEY_SIDEBAR_COLLAPSED, collapsed ? "1" : "0");
2396
- },
2397
- loadActiveModule() {
2398
- return storage.get(KEY_ACTIVE_MODULE) || null;
2399
- },
2400
- saveActiveModule(id) {
2401
- storage.set(KEY_ACTIVE_MODULE, id);
2402
- },
2403
- loadFormsDone() {
2404
- const raw = storage.get(KEY_FORMS_DONE);
2405
- if (!raw) return {};
2406
- try {
2407
- const parsed = JSON.parse(raw);
2408
- return isPlainObject2(parsed) ? parsed : {};
2409
- } catch (error) {
2410
- log6.warn("loadFormsDone parse failed", { error });
2411
- return {};
2412
- }
2413
- },
2414
- saveFormDone(id) {
2415
- try {
2416
- const done = persistence.loadFormsDone();
2417
- done[id] = Date.now();
2418
- storage.set(KEY_FORMS_DONE, JSON.stringify(done));
2419
- } catch (error) {
2420
- log6.warn("saveFormDone serialise failed", { error });
2421
- }
2422
- },
2423
- clearConversation() {
2424
- storage.remove(KEY_CONVERSATION);
2425
- }
2426
- };
2427
- return persistence;
2428
- }
2429
- function isPlainObject2(v) {
2430
- return typeof v === "object" && v !== null && !Array.isArray(v);
2431
- }
2432
-
2433
2462
  // src/core/feedback/index.ts
2434
2463
  import { signal as signal3 } from "@preact/signals";
2435
2464
 
@@ -6725,7 +6754,10 @@ function App({ options, hostElement, bus }) {
6725
6754
  visitorId,
6726
6755
  conversationId,
6727
6756
  userPrefs: persistence.loadUserPrefs(),
6728
- locale: options.locale
6757
+ // The visitor's ACTIVE locale (persisted pick, else auto-detected) —
6758
+ // not the config baseline — so the handshake learns what the visitor
6759
+ // wants. The response's availableLocales then clamps it for the rest.
6760
+ locale: activeLocale
6729
6761
  });
6730
6762
  } catch (err) {
6731
6763
  if (isStale()) return;
@@ -6787,7 +6819,18 @@ function App({ options, hostElement, bus }) {
6787
6819
  setConversationReady(true);
6788
6820
  }
6789
6821
  },
6790
- [transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
6822
+ [
6823
+ transport,
6824
+ visitorId,
6825
+ persistence,
6826
+ options,
6827
+ bus,
6828
+ conversationIdSig,
6829
+ feedback,
6830
+ playWelcome,
6831
+ loadThread,
6832
+ activeLocale
6833
+ ]
6791
6834
  );
6792
6835
  const userSig = JSON.stringify(options.userContext ?? null);
6793
6836
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6796,6 +6839,13 @@ function App({ options, hostElement, bus }) {
6796
6839
  const merged = Object.keys(formContext).length ? { ...formContext, ...options.userContext } : options.userContext;
6797
6840
  transport.setContext(merged, toWirePageContext(options.pageContext));
6798
6841
  }, [transport, userSig, pageSig, formCtxSig]);
6842
+ const effectiveLocale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
6843
+ useEffect17(() => {
6844
+ transport.setLocale(effectiveLocale);
6845
+ }, [transport, effectiveLocale]);
6846
+ useEffect17(() => {
6847
+ if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
6848
+ }, [effectiveLocale, activeLocale]);
6799
6849
  const runResume = useCallback6(
6800
6850
  async (handle) => {
6801
6851
  let bubble = null;
@@ -7446,51 +7496,21 @@ function emitMessage(bus, options, role, text) {
7446
7496
  options.onMessage?.(payload);
7447
7497
  }
7448
7498
 
7449
- // src/index.ts
7450
- import { h, render } from "preact";
7451
-
7452
7499
  // src/ui/error-boundary.tsx
7453
7500
  import { Component } from "preact";
7454
7501
  var log17 = logger.scope("error-boundary");
7455
-
7456
- // src/shadow/iframe-mount.ts
7457
- var log18 = logger.scope("iframe");
7458
-
7459
- // src/core/events.ts
7460
- var EventBus = class {
7502
+ var ErrorBoundary = class extends Component {
7461
7503
  constructor() {
7462
- __publicField(this, "handlers", /* @__PURE__ */ new Map());
7463
- }
7464
- /**
7465
- * Register a listener for `name`.
7466
- * @returns an `off` function that removes this listener.
7467
- */
7468
- on(name, fn) {
7469
- let set = this.handlers.get(name);
7470
- if (!set) {
7471
- set = /* @__PURE__ */ new Set();
7472
- this.handlers.set(name, set);
7473
- }
7474
- set.add(fn);
7475
- return () => this.handlers.get(name)?.delete(fn);
7504
+ super(...arguments);
7505
+ __publicField(this, "state", { crashed: false });
7476
7506
  }
7477
- /**
7478
- * Dispatch an event synchronously to all listeners.
7479
- * Listener errors are swallowed — the widget must not crash on user callbacks.
7480
- */
7481
- emit(name, payload) {
7482
- const set = this.handlers.get(name);
7483
- if (!set) return;
7484
- for (const fn of set) {
7485
- try {
7486
- fn(payload);
7487
- } catch {
7488
- }
7489
- }
7507
+ componentDidCatch(error, info) {
7508
+ log17.error("widget render crashed", { error, componentStack: info?.componentStack });
7509
+ this.setState({ crashed: true });
7510
+ this.props.onError(error, info?.componentStack);
7490
7511
  }
7491
- /** Remove every listener. Called by the instance `destroy()`. */
7492
- clear() {
7493
- this.handlers.clear();
7512
+ render() {
7513
+ return this.state.crashed ? this.props.fallback : this.props.children;
7494
7514
  }
7495
7515
  };
7496
7516
 
@@ -7609,10 +7629,60 @@ function createTracker(bus, deps) {
7609
7629
  };
7610
7630
  }
7611
7631
 
7612
- // src/core/hooks.ts
7613
- var log19 = logger.scope("hooks");
7614
-
7615
- // src/index.ts
7632
+ // src/core/boot.ts
7633
+ function createWidgetRuntime(params) {
7634
+ const { bus, host, appRoot, persistence, reportError } = params;
7635
+ let userRaw = params.initialRaw;
7636
+ let serverConfig;
7637
+ let currentOptions = resolveOptions(userRaw);
7638
+ let firstRender = true;
7639
+ const recompute = () => {
7640
+ currentOptions = resolveOptions(serverConfig ? mergeServerConfig(userRaw, serverConfig) : userRaw);
7641
+ };
7642
+ const applyHostStyling = () => {
7643
+ applyHostPosition(host, hostPositionForMode(currentOptions.mode));
7644
+ const cachedThemeMode = persistence.loadUserPrefs().themeMode;
7645
+ applyHostAttributes(host, cachedThemeMode ? { ...currentOptions, themeMode: cachedThemeMode } : currentOptions);
7646
+ if (firstRender) {
7647
+ applyMode(host, resolveInitialHostMode(currentOptions));
7648
+ firstRender = false;
7649
+ }
7650
+ };
7651
+ const render2 = () => {
7652
+ applyHostStyling();
7653
+ renderPreact(
7654
+ h(ErrorBoundary, { onError: reportError }, h(App, { bus, hostElement: host, options: currentOptions })),
7655
+ appRoot
7656
+ );
7657
+ };
7658
+ bus.on("handshake", (response) => {
7659
+ if (!response.config) return;
7660
+ serverConfig = response.config;
7661
+ recompute();
7662
+ render2();
7663
+ });
7664
+ const untrack = createTracker(bus, {
7665
+ getOptions: () => currentOptions,
7666
+ getVisitorId: () => persistence.getVisitorId(),
7667
+ getConversationId: () => persistence.loadConversationId()
7668
+ });
7669
+ return {
7670
+ getOptions: () => currentOptions,
7671
+ getUserOptions: () => userRaw,
7672
+ render: render2,
7673
+ setUserOptions: (raw) => {
7674
+ userRaw = raw;
7675
+ recompute();
7676
+ render2();
7677
+ },
7678
+ patchUserOptions: (patch) => {
7679
+ userRaw = { ...userRaw, ...patch };
7680
+ recompute();
7681
+ render2();
7682
+ },
7683
+ destroy: () => untrack()
7684
+ };
7685
+ }
7616
7686
  function resolveInitialHostMode(options) {
7617
7687
  if (options.mode === "standalone") return "standalone";
7618
7688
  if (options.mode === "inline") return "inline";
@@ -7626,35 +7696,52 @@ function resolveInitialHostMode(options) {
7626
7696
  return computeHostMode(options.mode, panelSize, panelOpen);
7627
7697
  }
7628
7698
 
7699
+ // src/core/events.ts
7700
+ var EventBus = class {
7701
+ constructor() {
7702
+ __publicField(this, "handlers", /* @__PURE__ */ new Map());
7703
+ }
7704
+ /**
7705
+ * Register a listener for `name`.
7706
+ * @returns an `off` function that removes this listener.
7707
+ */
7708
+ on(name, fn) {
7709
+ let set = this.handlers.get(name);
7710
+ if (!set) {
7711
+ set = /* @__PURE__ */ new Set();
7712
+ this.handlers.set(name, set);
7713
+ }
7714
+ set.add(fn);
7715
+ return () => this.handlers.get(name)?.delete(fn);
7716
+ }
7717
+ /**
7718
+ * Dispatch an event synchronously to all listeners.
7719
+ * Listener errors are swallowed — the widget must not crash on user callbacks.
7720
+ */
7721
+ emit(name, payload) {
7722
+ const set = this.handlers.get(name);
7723
+ if (!set) return;
7724
+ for (const fn of set) {
7725
+ try {
7726
+ fn(payload);
7727
+ } catch {
7728
+ }
7729
+ }
7730
+ }
7731
+ /** Remove every listener. Called by the instance `destroy()`. */
7732
+ clear() {
7733
+ this.handlers.clear();
7734
+ }
7735
+ };
7736
+
7629
7737
  // src/web-component.ts
7630
7738
  var ChatElement = class extends HTMLElement {
7631
7739
  constructor() {
7632
7740
  super(...arguments);
7633
7741
  __publicField(this, "bus", new EventBus());
7634
7742
  __publicField(this, "mountResult", null);
7635
- /**
7636
- * Raw options accumulator. Starts as whatever the page-owner set via
7637
- * HTML attributes; gets server-side settings merged in on the
7638
- * `handshake` event so subsequent re-renders pick up the server's
7639
- * mode / theme / launcher / etc.
7640
- *
7641
- * Mirrors `index.ts`'s `rawOptions` so both entry points share the
7642
- * same user-wins-per-field merge policy.
7643
- */
7644
- __publicField(this, "rawOptions", {});
7645
- /**
7646
- * Latest resolved options — refreshed every `renderApp()` so the usage
7647
- * tracker (created once in `boot`) reads live, post-handshake-merge
7648
- * values (`tracking.enabled` / `endpoint` / `token`).
7649
- *
7650
- * This path is independent of `mount()` (it supports N elements per page
7651
- * without a shared `widgetId` registry), so anything `mount()` wires onto
7652
- * the bus must be wired here too — the tracker is the current example;
7653
- * keep them in lockstep.
7654
- */
7655
- __publicField(this, "resolved", null);
7656
- /** Tracker unsubscribe — released on disconnect. */
7657
- __publicField(this, "untrack", null);
7743
+ /** Live widget composition — null between disconnect and the next connect. */
7744
+ __publicField(this, "runtime", null);
7658
7745
  }
7659
7746
  static get observedAttributes() {
7660
7747
  return REACTIVE_ATTRS;
@@ -7664,50 +7751,37 @@ var ChatElement = class extends HTMLElement {
7664
7751
  }
7665
7752
  disconnectedCallback() {
7666
7753
  if (!this.mountResult) return;
7667
- this.untrack?.();
7668
- this.untrack = null;
7669
- render2(null, this.mountResult.appRoot);
7754
+ this.runtime?.destroy();
7755
+ this.runtime = null;
7756
+ render(null, this.mountResult.appRoot);
7670
7757
  this.bus.clear();
7671
7758
  }
7672
7759
  attributeChangedCallback() {
7673
7760
  if (this.isConnected) this.boot();
7674
7761
  }
7675
7762
  boot() {
7676
- this.rawOptions = parseAttributes((name) => this.getAttribute(name));
7763
+ const rawOptions = parseAttributes((name) => this.getAttribute(name));
7764
+ if (this.runtime) {
7765
+ this.runtime.setUserOptions(rawOptions);
7766
+ return;
7767
+ }
7768
+ const resolved = resolveOptions(rawOptions);
7677
7769
  if (!this.mountResult) {
7678
- const resolved = resolveOptions(this.rawOptions);
7679
7770
  this.mountResult = createShadowMount({
7680
7771
  existingHost: this,
7681
7772
  position: hostPositionForMode(resolved.mode)
7682
7773
  });
7683
- this.bus.on("handshake", (response) => {
7684
- if (!response.config) return;
7685
- this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
7686
- this.renderApp();
7687
- });
7688
- const persistence = createPersistence(resolved.widgetId, resolved.storage);
7689
- this.untrack = createTracker(this.bus, {
7690
- getOptions: () => this.resolved ?? resolved,
7691
- getVisitorId: () => persistence.getVisitorId(),
7692
- getConversationId: () => persistence.loadConversationId()
7693
- });
7694
7774
  }
7695
- this.renderApp();
7696
- }
7697
- /**
7698
- * Apply the current `rawOptions` to the host element + App tree.
7699
- * Called by both the initial boot and every `handshake` merge.
7700
- */
7701
- renderApp() {
7702
- if (!this.mountResult) return;
7703
- const resolved = resolveOptions(this.rawOptions);
7704
- this.resolved = resolved;
7705
- const host = this.mountResult.hostElement;
7706
- applyHostPosition(host, hostPositionForMode(resolved.mode));
7707
- const cachedThemeMode = createPersistence(resolved.widgetId, resolved.storage).loadUserPrefs().themeMode;
7708
- applyHostAttributes(host, cachedThemeMode ? { ...resolved, themeMode: cachedThemeMode } : resolved);
7709
- applyMode(host, resolveInitialHostMode(resolved));
7710
- render2(h2(App, { bus: this.bus, hostElement: host, options: resolved }), this.mountResult.appRoot);
7775
+ const persistence = createPersistence(resolved.widgetId, resolved.storage);
7776
+ this.runtime = createWidgetRuntime({
7777
+ bus: this.bus,
7778
+ host: this.mountResult.hostElement,
7779
+ appRoot: this.mountResult.appRoot,
7780
+ persistence,
7781
+ initialRaw: rawOptions,
7782
+ reportError: (error) => this.bus.emit("error", error)
7783
+ });
7784
+ this.runtime.render();
7711
7785
  }
7712
7786
  };
7713
7787
  var registered = false;