@helpai/elements 0.30.0 → 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 = {
@@ -993,35 +989,153 @@ function parseLauncher(get, str2) {
993
989
  return Object.keys(base).length > 0 ? base : null;
994
990
  }
995
991
 
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);
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)}`;
1002
1007
  }
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;
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);
1008
1014
  }
1009
1015
 
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);
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
+ }
1024
1134
  };
1135
+ return persistence;
1136
+ }
1137
+ function isPlainObject(v) {
1138
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1025
1139
  }
1026
1140
 
1027
1141
  // src/styles/tokens.css
@@ -1048,7 +1162,7 @@ var FONT = true ? "Inter, system-ui, -apple-system, 'Segoe UI', sans-serif" : "I
1048
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);
1049
1163
 
1050
1164
  // src/shadow/mount.ts
1051
- var log2 = logger.scope("mount");
1165
+ var log3 = logger.scope("mount");
1052
1166
  var sheetsByDocument = /* @__PURE__ */ new WeakMap();
1053
1167
  var HOST_CSS = {
1054
1168
  floating: "all: initial; position: fixed; z-index: 2147483600;",
@@ -1065,7 +1179,7 @@ function hostPositionForMode(mode) {
1065
1179
  }
1066
1180
  function createShadowMount(opts = {}) {
1067
1181
  const position = opts.position ?? "flow";
1068
- log2.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1182
+ log3.debug("createShadowMount", { position, hasTarget: !!opts.target, hasHost: !!opts.existingHost });
1069
1183
  const tagName = `${BRAND.cssPrefix}-chat-host`;
1070
1184
  if (typeof customElements !== "undefined" && !customElements.get(tagName)) {
1071
1185
  customElements.define(tagName, class extends HTMLElement {
@@ -1149,6 +1263,44 @@ function applyHostAttributes(host, resolved) {
1149
1263
  applySize(host, resolved.size);
1150
1264
  }
1151
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
+
1152
1304
  // src/ui/host-mode.ts
1153
1305
  function computeHostMode(configured, panelSize, isOpen) {
1154
1306
  if (configured === "page") return "page";
@@ -1199,30 +1351,6 @@ function createAuth(opts) {
1199
1351
  };
1200
1352
  }
1201
1353
 
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
1354
  // src/stream/types.ts
1227
1355
  var StreamError = class extends Error {
1228
1356
  constructor(message, code, status, cause) {
@@ -1235,7 +1363,7 @@ var StreamError = class extends Error {
1235
1363
  };
1236
1364
 
1237
1365
  // src/stream/parser.ts
1238
- var log3 = logger.scope("parser");
1366
+ var log4 = logger.scope("parser");
1239
1367
  async function* parseChatStream(response, signal7) {
1240
1368
  if (!response.ok) {
1241
1369
  const text = await response.text().catch(() => "");
@@ -1287,10 +1415,10 @@ ${part}` : part;
1287
1415
  if (!dataPayload || dataPayload === "[DONE]") return null;
1288
1416
  try {
1289
1417
  const chunk = JSON.parse(dataPayload);
1290
- log3.trace("chunk", { type: chunk.type, eventId });
1418
+ log4.trace("chunk", { type: chunk.type, eventId });
1291
1419
  return { chunk, eventId };
1292
1420
  } catch (err) {
1293
- log3.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1421
+ log4.trace("frame parse failed", { err, payload: dataPayload.slice(0, 200) });
1294
1422
  return null;
1295
1423
  }
1296
1424
  }
@@ -1445,7 +1573,7 @@ function messageToWireParts(m) {
1445
1573
  }
1446
1574
 
1447
1575
  // src/stream/transport.ts
1448
- var log4 = logger.scope("transport");
1576
+ var log5 = logger.scope("transport");
1449
1577
  var MAX_RESUME_ATTEMPTS = 3;
1450
1578
  var RESUME_BACKOFF_MS = 400;
1451
1579
  var CONTENT_CACHE_TTL_MS = 6e4;
@@ -1571,13 +1699,13 @@ var AgentTransport = class {
1571
1699
  }
1572
1700
  /** One-shot runtime bootstrap. Called once after the widget mounts. */
1573
1701
  async handshake(body) {
1574
- log4.debug("handshake \u2192", {
1702
+ log5.debug("handshake \u2192", {
1575
1703
  visitorId: body.visitorId,
1576
1704
  conversationId: body.conversationId,
1577
1705
  locale: body.locale
1578
1706
  });
1579
1707
  const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
1580
- log4.debug("handshake \u2190", {
1708
+ log5.debug("handshake \u2190", {
1581
1709
  visitorId: res.visitorId,
1582
1710
  conversationId: res.conversationId,
1583
1711
  hasWelcome: !!res.welcome,
@@ -1585,7 +1713,7 @@ var AgentTransport = class {
1585
1713
  rebind: res.rebind
1586
1714
  });
1587
1715
  if (!isHandshakeResponseShape(res)) {
1588
- 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 });
1589
1717
  }
1590
1718
  this.visitorId = res.visitorId ?? body.visitorId;
1591
1719
  this.conversationId = res.conversationId;
@@ -1593,11 +1721,11 @@ var AgentTransport = class {
1593
1721
  }
1594
1722
  /** Upload a file attachment. Returns the `{id, url}` the backend assigns. */
1595
1723
  async uploadFile(file) {
1596
- 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 });
1597
1725
  const fd = new FormData();
1598
1726
  fd.append("file", file, file.name);
1599
1727
  const res = await this.postForm(this.uploadPath, fd, "upload");
1600
- log4.debug("uploadFile \u2190", { id: res.id, url: res.url });
1728
+ log5.debug("uploadFile \u2190", { id: res.id, url: res.url });
1601
1729
  return res;
1602
1730
  }
1603
1731
  /**
@@ -1605,12 +1733,12 @@ var AgentTransport = class {
1605
1733
  * Only used when `features.voice === 'server'`.
1606
1734
  */
1607
1735
  async transcribe(audio, mimeType = "audio/webm") {
1608
- log4.debug("transcribe \u2192", { size: audio.size, mimeType });
1736
+ log5.debug("transcribe \u2192", { size: audio.size, mimeType });
1609
1737
  const fd = new FormData();
1610
1738
  fd.append("audio", audio, `voice.${extensionFor(mimeType)}`);
1611
1739
  fd.append("mimeType", mimeType);
1612
1740
  const res = await this.postForm(this.transcribePath, fd, "transcribe");
1613
- log4.debug("transcribe \u2190", { textLen: res.text.length });
1741
+ log5.debug("transcribe \u2190", { textLen: res.text.length });
1614
1742
  return res;
1615
1743
  }
1616
1744
  async listConversations(params = {}) {
@@ -1618,14 +1746,14 @@ var AgentTransport = class {
1618
1746
  if (params.visitorId) url.searchParams.set("visitorId", params.visitorId);
1619
1747
  if (params.limit) url.searchParams.set("limit", String(params.limit));
1620
1748
  if (params.cursor) url.searchParams.set("cursor", params.cursor);
1621
- log4.debug("listConversations \u2192", {
1749
+ log5.debug("listConversations \u2192", {
1622
1750
  visitorId: params.visitorId ?? this.visitorId,
1623
1751
  limit: params.limit,
1624
1752
  cursor: params.cursor
1625
1753
  });
1626
1754
  const res = await this.getJson(url.toString(), "listConversations");
1627
1755
  const conversations = res.conversations ?? [];
1628
- log4.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1756
+ log5.debug("listConversations \u2190", { count: conversations.length, nextCursor: res.nextCursor });
1629
1757
  return {
1630
1758
  conversations: conversations.map((conversation) => ({
1631
1759
  conversationId: conversation.conversationId,
@@ -1641,10 +1769,10 @@ var AgentTransport = class {
1641
1769
  async loadConversation(conversationId) {
1642
1770
  const url = new URL(this.url(DEFAULT_PATHS.listMessages));
1643
1771
  url.searchParams.set("conversationId", conversationId);
1644
- log4.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1772
+ log5.debug("loadConversation \u2192", { conversationId, visitorId: this.visitorId });
1645
1773
  const res = await this.getJson(url.toString(), "loadConversation");
1646
1774
  const messages = res.messages ?? [];
1647
- log4.debug("loadConversation \u2190", {
1775
+ log5.debug("loadConversation \u2190", {
1648
1776
  conversationId: res.conversationId,
1649
1777
  canContinue: res.canContinue,
1650
1778
  messageCount: messages.length,
@@ -1666,11 +1794,11 @@ var AgentTransport = class {
1666
1794
  */
1667
1795
  async markRead(conversationId) {
1668
1796
  if (!this.visitorId || !conversationId) return;
1669
- log4.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1797
+ log5.debug("markRead \u2192", { conversationId, visitorId: this.visitorId });
1670
1798
  try {
1671
1799
  await this.postJson(DEFAULT_PATHS.markRead, { conversationId }, "markRead");
1672
1800
  } catch (err) {
1673
- log4.debug("markRead failed (non-fatal)", { err });
1801
+ log5.debug("markRead failed (non-fatal)", { err });
1674
1802
  }
1675
1803
  }
1676
1804
  /** Fetch content rows by filter. `GET /pai/content` (data-API), TTL-cached. */
@@ -1678,7 +1806,7 @@ var AgentTransport = class {
1678
1806
  const key = JSON.stringify(query, Object.keys(query).toSorted());
1679
1807
  const cached = this.contentCache.get(key);
1680
1808
  if (cached && Date.now() - cached.at < CONTENT_CACHE_TTL_MS) {
1681
- log4.debug("listContent \u2192 cache", query);
1809
+ log5.debug("listContent \u2192 cache", query);
1682
1810
  return cached.result;
1683
1811
  }
1684
1812
  const result = this.fetchContent(query).catch((err) => {
@@ -1694,10 +1822,10 @@ var AgentTransport = class {
1694
1822
  if (value === void 0) continue;
1695
1823
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
1696
1824
  }
1697
- log4.debug("listContent \u2192", query);
1825
+ log5.debug("listContent \u2192", query);
1698
1826
  const res = await this.getJson(url.toString(), "listContent");
1699
1827
  const items = res.items ?? [];
1700
- log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1828
+ log5.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1701
1829
  return { ...res, items };
1702
1830
  }
1703
1831
  /**
@@ -1715,21 +1843,21 @@ var AgentTransport = class {
1715
1843
  async bootstrap(conversationId) {
1716
1844
  const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1717
1845
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1718
- log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1846
+ log5.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1719
1847
  const res = await this.getJson(url.toString(), "bootstrap");
1720
1848
  const forms = res.forms ?? [];
1721
1849
  const formSubmissions = res.formSubmissions ?? [];
1722
- log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1850
+ log5.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1723
1851
  return { forms, formSubmissions };
1724
1852
  }
1725
1853
  async saveUserPrefs(userPrefs) {
1726
- log4.debug("saveUserPrefs \u2192", {
1854
+ log5.debug("saveUserPrefs \u2192", {
1727
1855
  visitorId: this.visitorId,
1728
1856
  conversationId: this.conversationId,
1729
1857
  settingsKeys: Object.keys(userPrefs)
1730
1858
  });
1731
1859
  const res = await this.postJson(DEFAULT_PATHS.updateSettings, { userPrefs }, "saveUserPrefs");
1732
- log4.debug("saveUserPrefs \u2190", { ok: res.ok });
1860
+ log5.debug("saveUserPrefs \u2190", { ok: res.ok });
1733
1861
  return res;
1734
1862
  }
1735
1863
  get uploadPath() {
@@ -1750,15 +1878,15 @@ var AgentTransport = class {
1750
1878
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
1751
1879
  */
1752
1880
  async submitForm(body) {
1753
- 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 });
1754
1882
  try {
1755
1883
  await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1756
1884
  } catch (err) {
1757
- log4.debug("submitForm failed (non-fatal)", { err });
1885
+ log5.debug("submitForm failed (non-fatal)", { err });
1758
1886
  }
1759
1887
  }
1760
1888
  sendMessage(body) {
1761
- log4.debug("message \u2192", {
1889
+ log5.debug("message \u2192", {
1762
1890
  conversationId: body.conversationId,
1763
1891
  visitorId: body.visitorId,
1764
1892
  messageCount: body.messages.length,
@@ -1838,17 +1966,17 @@ var AgentTransport = class {
1838
1966
  });
1839
1967
  } catch (err) {
1840
1968
  if (ctrl.signal.aborted) return;
1841
- log4.debug("resume fetch failed \u2014 retrying", { attempt, err });
1969
+ log5.debug("resume fetch failed \u2014 retrying", { attempt, err });
1842
1970
  continue;
1843
1971
  }
1844
1972
  if (res.status === 204 || !res.ok) {
1845
- log4.debug("resume \u2192 no in-flight stream", { status: res.status });
1973
+ log5.debug("resume \u2192 no in-flight stream", { status: res.status });
1846
1974
  return;
1847
1975
  }
1848
1976
  const before = seenIds.size;
1849
1977
  if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
1850
1978
  if (seenIds.size === before) {
1851
- 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");
1852
1980
  return;
1853
1981
  }
1854
1982
  }
@@ -1875,14 +2003,14 @@ var AgentTransport = class {
1875
2003
  } catch (err) {
1876
2004
  if (ctrl.signal.aborted) return true;
1877
2005
  if (err instanceof StreamError && err.status !== void 0) throw err;
1878
- log4.debug("stream segment dropped", { err });
2006
+ log5.debug("stream segment dropped", { err });
1879
2007
  }
1880
2008
  return false;
1881
2009
  }
1882
2010
  /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1883
2011
  cancelStream(ctrl, conversationId) {
1884
2012
  if (ctrl.signal.aborted) return;
1885
- log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
2013
+ log5.debug("cancel stream", { conversationId, visitorId: this.visitorId });
1886
2014
  ctrl.abort();
1887
2015
  if (!this.visitorId || !conversationId) return;
1888
2016
  this.fetchImpl(this.url(DEFAULT_PATHS.cancelStream), {
@@ -1891,7 +2019,7 @@ var AgentTransport = class {
1891
2019
  headers: { "content-type": "application/json", ...this.opts.auth.headers() },
1892
2020
  body: JSON.stringify(this.withEnvelope({})),
1893
2021
  keepalive: true
1894
- }).catch((err) => log4.debug("cancel POST failed (non-fatal)", { err }));
2022
+ }).catch((err) => log5.debug("cancel POST failed (non-fatal)", { err }));
1895
2023
  }
1896
2024
  // ---- Low-level fetch helpers ------------------------------------------
1897
2025
  async *openMessageStream(body, signal7) {
@@ -1973,13 +2101,13 @@ var AgentTransport = class {
1973
2101
  if (attempt >= MAX_REQUEST_RETRIES) {
1974
2102
  throw new StreamError(`${label} failed: network error`, "network", void 0, err);
1975
2103
  }
1976
- log4.debug("request network error \u2014 retrying", { label, attempt });
2104
+ log5.debug("request network error \u2014 retrying", { label, attempt });
1977
2105
  await sleep(backoffMs(attempt));
1978
2106
  continue;
1979
2107
  }
1980
2108
  if (res.ok || !RETRYABLE_STATUS.has(res.status) || attempt >= MAX_REQUEST_RETRIES) return res;
1981
2109
  const wait = retryAfterMs(res.headers) ?? backoffMs(attempt);
1982
- 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 });
1983
2111
  await sleep(wait);
1984
2112
  }
1985
2113
  }
@@ -2174,7 +2302,7 @@ var TRIGGER = {
2174
2302
  };
2175
2303
 
2176
2304
  // src/stream/reducer.ts
2177
- var log5 = logger.scope("reducer");
2305
+ var log6 = logger.scope("reducer");
2178
2306
  var StreamReducer = class {
2179
2307
  constructor(messagesSig) {
2180
2308
  __publicField(this, "messagesSig", messagesSig);
@@ -2196,13 +2324,13 @@ var StreamReducer = class {
2196
2324
  case "finish-step":
2197
2325
  return;
2198
2326
  case "finish":
2199
- log5.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2327
+ log6.debug("finish", { reason: chunk.finishReason, canContinue: chunk.canContinue });
2200
2328
  m.status = "complete";
2201
2329
  if (chunk.finishReason) m.finishReason = chunk.finishReason;
2202
2330
  this.bumpDone(m);
2203
2331
  return;
2204
2332
  case "error":
2205
- log5.warn("stream error chunk", { errorText: chunk.errorText });
2333
+ log6.warn("stream error chunk", { errorText: chunk.errorText });
2206
2334
  m.status = "error";
2207
2335
  m.errorText = chunk.errorText;
2208
2336
  this.bumpDone(m);
@@ -2331,131 +2459,6 @@ function applyTool(m, chunk) {
2331
2459
  }
2332
2460
  }
2333
2461
 
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
2462
  // src/core/feedback/index.ts
2460
2463
  import { signal as signal3 } from "@preact/signals";
2461
2464
 
@@ -7511,50 +7514,6 @@ var ErrorBoundary = class extends Component {
7511
7514
  }
7512
7515
  };
7513
7516
 
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
7517
  // src/core/tracking.ts
7559
7518
  var PROTOCOL_VERSION = "1";
7560
7519
  var MAX_HITS_PER_MINUTE = 60;
@@ -7670,10 +7629,60 @@ function createTracker(bus, deps) {
7670
7629
  };
7671
7630
  }
7672
7631
 
7673
- // src/core/hooks.ts
7674
- var log19 = logger.scope("hooks");
7675
-
7676
- // 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
+ }
7677
7686
  function resolveInitialHostMode(options) {
7678
7687
  if (options.mode === "standalone") return "standalone";
7679
7688
  if (options.mode === "inline") return "inline";
@@ -7687,39 +7696,52 @@ function resolveInitialHostMode(options) {
7687
7696
  return computeHostMode(options.mode, panelSize, panelOpen);
7688
7697
  }
7689
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
+
7690
7737
  // src/web-component.ts
7691
7738
  var ChatElement = class extends HTMLElement {
7692
7739
  constructor() {
7693
7740
  super(...arguments);
7694
7741
  __publicField(this, "bus", new EventBus());
7695
7742
  __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);
7743
+ /** Live widget composition — null between disconnect and the next connect. */
7744
+ __publicField(this, "runtime", null);
7723
7745
  }
7724
7746
  static get observedAttributes() {
7725
7747
  return REACTIVE_ATTRS;
@@ -7729,57 +7751,37 @@ var ChatElement = class extends HTMLElement {
7729
7751
  }
7730
7752
  disconnectedCallback() {
7731
7753
  if (!this.mountResult) return;
7732
- this.untrack?.();
7733
- this.untrack = null;
7734
- render2(null, this.mountResult.appRoot);
7754
+ this.runtime?.destroy();
7755
+ this.runtime = null;
7756
+ render(null, this.mountResult.appRoot);
7735
7757
  this.bus.clear();
7736
7758
  }
7737
7759
  attributeChangedCallback() {
7738
7760
  if (this.isConnected) this.boot();
7739
7761
  }
7740
7762
  boot() {
7741
- 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);
7742
7769
  if (!this.mountResult) {
7743
- const resolved = resolveOptions(this.rawOptions);
7744
7770
  this.mountResult = createShadowMount({
7745
7771
  existingHost: this,
7746
7772
  position: hostPositionForMode(resolved.mode)
7747
7773
  });
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
7774
  }
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
- );
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();
7783
7785
  }
7784
7786
  };
7785
7787
  var registered = false;