@helpai/elements 0.29.1 → 0.30.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
@@ -509,8 +509,9 @@ function resolveOptions(rawOpts) {
509
509
  const header = opts.header ?? {};
510
510
  const { themeMode, themeOverrides } = parseTheme(opts.theme);
511
511
  const mode = presentation.mode ?? "floating";
512
- const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
513
- const availableLocales = i18n.availableLocales ?? [locale];
512
+ const defaultLocale = resolveDefaultLocale(i18n.defaultLocale);
513
+ const availableLocales = i18n.availableLocales ?? [i18n.locale ?? defaultLocale];
514
+ const locale = clampLocale(i18n.locale ?? defaultLocale, availableLocales, defaultLocale);
514
515
  const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
515
516
  const agentApiBaseUrl = (opts.agentApiBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
516
517
  const dataApiBaseUrl = (opts.dataApiBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
@@ -552,6 +553,7 @@ function resolveOptions(rawOpts) {
552
553
  maxCount: opts.composer?.attachments?.maxCount ?? DEFAULT_ATTACHMENTS.maxCount
553
554
  },
554
555
  locale,
556
+ defaultLocale,
555
557
  availableLocales,
556
558
  startMinimized: behavior.startMinimized ?? true,
557
559
  actions: resolveActions(mode, header.actions),
@@ -582,6 +584,11 @@ function resolveDefaultLocale(input) {
582
584
  if (input && input !== "auto") return input;
583
585
  return typeof navigator !== "undefined" && navigator.language ? navigator.language : "en";
584
586
  }
587
+ function clampLocale(desired, availableLocales, defaultLocale) {
588
+ if (availableLocales.length <= 1 || availableLocales.includes(desired)) return desired;
589
+ if (availableLocales.includes(defaultLocale)) return defaultLocale;
590
+ return availableLocales[0] ?? desired;
591
+ }
585
592
  function resolveActions(mode, overrides) {
586
593
  return overrides ?? DEFAULT_ACTIONS_BY_MODE[mode];
587
594
  }
@@ -730,13 +737,14 @@ function resolveLauncher(overrides) {
730
737
 
731
738
  // src/core/config/overrides.ts
732
739
  function applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize) {
733
- if (activeLocale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
740
+ const locale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
741
+ if (locale === options.locale && activeThemeMode === options.themeMode && activeTextSize === options.textSize) {
734
742
  return options;
735
743
  }
736
744
  const next = { ...options };
737
- if (activeLocale !== options.locale) {
738
- next.locale = activeLocale;
739
- next.strings = resolveStrings(activeLocale, options.stringsOverride);
745
+ if (locale !== options.locale) {
746
+ next.locale = locale;
747
+ next.strings = resolveStrings(locale, options.stringsOverride);
740
748
  }
741
749
  if (activeThemeMode !== options.themeMode) next.themeMode = activeThemeMode;
742
750
  if (activeTextSize !== options.textSize) next.textSize = activeTextSize;
@@ -782,6 +790,8 @@ function mergeServerConfig(user, server) {
782
790
  serverI18n ?? {},
783
791
  userI18n
784
792
  );
793
+ if (serverI18n?.availableLocales !== void 0) merged.availableLocales = serverI18n.availableLocales;
794
+ if (serverI18n?.defaultLocale !== void 0) merged.defaultLocale = serverI18n.defaultLocale;
785
795
  const userStrings = userI18n?.strings;
786
796
  const serverStrings = serverI18n?.strings;
787
797
  if (userStrings || serverStrings) {
@@ -1477,6 +1487,12 @@ var AgentTransport = class {
1477
1487
  // always sees the visitor's latest context as they navigate.
1478
1488
  __publicField(this, "userContext");
1479
1489
  __publicField(this, "pageContext");
1490
+ // Active BCP-47 locale, carried as the envelope's `locale` on EVERY request
1491
+ // so the backend can localize replies AND the data module can return
1492
+ // localized content + forms. Refreshed via `setLocale` whenever the visitor
1493
+ // switches (no re-handshake needed). Resolved + clamped to the deployment's
1494
+ // availableLocales by `resolveOptions` before it reaches here.
1495
+ __publicField(this, "locale");
1480
1496
  // ---- Data API (content / forms — module-help-ai-data-api) --------------
1481
1497
  //
1482
1498
  // The DATA surfaces live on `dataApiBaseUrl` (default `${baseUrl}/api/data`)
@@ -1502,6 +1518,15 @@ var AgentTransport = class {
1502
1518
  this.userContext = userContext;
1503
1519
  this.pageContext = pageContext;
1504
1520
  }
1521
+ /**
1522
+ * Update the active locale carried on every subsequent request. Call on
1523
+ * mount (before the first handshake) and whenever the user picks a new
1524
+ * locale — the next content / forms / message request reflects it with no
1525
+ * extra round-trip.
1526
+ */
1527
+ setLocale(locale) {
1528
+ this.locale = locale;
1529
+ }
1505
1530
  /**
1506
1531
  * Seed the visitor + conversation identity from persistence BEFORE the first
1507
1532
  * `handshake` resolves. A mount-time `resumeStream()` runs in parallel
@@ -1518,6 +1543,7 @@ var AgentTransport = class {
1518
1543
  const out = {};
1519
1544
  if (this.visitorId) out.visitorId = this.visitorId;
1520
1545
  if (this.conversationId) out.conversationId = this.conversationId;
1546
+ if (this.locale) out.locale = this.locale;
1521
1547
  const context = encodeContext(this.userContext, this.pageContext);
1522
1548
  if (context) out[CONTEXT_PARAM] = context;
1523
1549
  return out;
@@ -6725,7 +6751,10 @@ function App({ options, hostElement, bus }) {
6725
6751
  visitorId,
6726
6752
  conversationId,
6727
6753
  userPrefs: persistence.loadUserPrefs(),
6728
- locale: options.locale
6754
+ // The visitor's ACTIVE locale (persisted pick, else auto-detected) —
6755
+ // not the config baseline — so the handshake learns what the visitor
6756
+ // wants. The response's availableLocales then clamps it for the rest.
6757
+ locale: activeLocale
6729
6758
  });
6730
6759
  } catch (err) {
6731
6760
  if (isStale()) return;
@@ -6787,7 +6816,18 @@ function App({ options, hostElement, bus }) {
6787
6816
  setConversationReady(true);
6788
6817
  }
6789
6818
  },
6790
- [transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
6819
+ [
6820
+ transport,
6821
+ visitorId,
6822
+ persistence,
6823
+ options,
6824
+ bus,
6825
+ conversationIdSig,
6826
+ feedback,
6827
+ playWelcome,
6828
+ loadThread,
6829
+ activeLocale
6830
+ ]
6791
6831
  );
6792
6832
  const userSig = JSON.stringify(options.userContext ?? null);
6793
6833
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6796,6 +6836,13 @@ function App({ options, hostElement, bus }) {
6796
6836
  const merged = Object.keys(formContext).length ? { ...formContext, ...options.userContext } : options.userContext;
6797
6837
  transport.setContext(merged, toWirePageContext(options.pageContext));
6798
6838
  }, [transport, userSig, pageSig, formCtxSig]);
6839
+ const effectiveLocale = clampLocale(activeLocale, options.availableLocales, options.defaultLocale);
6840
+ useEffect17(() => {
6841
+ transport.setLocale(effectiveLocale);
6842
+ }, [transport, effectiveLocale]);
6843
+ useEffect17(() => {
6844
+ if (effectiveLocale !== activeLocale) setActiveLocale(effectiveLocale);
6845
+ }, [effectiveLocale, activeLocale]);
6799
6846
  const runResume = useCallback6(
6800
6847
  async (handle) => {
6801
6848
  let bubble = null;
@@ -7446,12 +7493,26 @@ function emitMessage(bus, options, role, text) {
7446
7493
  options.onMessage?.(payload);
7447
7494
  }
7448
7495
 
7449
- // src/index.ts
7450
- import { h, render } from "preact";
7451
-
7452
7496
  // src/ui/error-boundary.tsx
7453
7497
  import { Component } from "preact";
7454
7498
  var log17 = logger.scope("error-boundary");
7499
+ var ErrorBoundary = class extends Component {
7500
+ constructor() {
7501
+ super(...arguments);
7502
+ __publicField(this, "state", { crashed: false });
7503
+ }
7504
+ componentDidCatch(error, info) {
7505
+ log17.error("widget render crashed", { error, componentStack: info?.componentStack });
7506
+ this.setState({ crashed: true });
7507
+ this.props.onError(error, info?.componentStack);
7508
+ }
7509
+ render() {
7510
+ return this.state.crashed ? this.props.fallback : this.props.children;
7511
+ }
7512
+ };
7513
+
7514
+ // src/index.ts
7515
+ import { h, render } from "preact";
7455
7516
 
7456
7517
  // src/shadow/iframe-mount.ts
7457
7518
  var log18 = logger.scope("iframe");
@@ -7648,9 +7709,13 @@ var ChatElement = class extends HTMLElement {
7648
7709
  * values (`tracking.enabled` / `endpoint` / `token`).
7649
7710
  *
7650
7711
  * 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.
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.
7654
7719
  */
7655
7720
  __publicField(this, "resolved", null);
7656
7721
  /** Tracker unsubscribe — released on disconnect. */
@@ -7707,7 +7772,14 @@ var ChatElement = class extends HTMLElement {
7707
7772
  const cachedThemeMode = createPersistence(resolved.widgetId, resolved.storage).loadUserPrefs().themeMode;
7708
7773
  applyHostAttributes(host, cachedThemeMode ? { ...resolved, themeMode: cachedThemeMode } : resolved);
7709
7774
  applyMode(host, resolveInitialHostMode(resolved));
7710
- render2(h2(App, { bus: this.bus, hostElement: host, options: resolved }), this.mountResult.appRoot);
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
+ );
7711
7783
  }
7712
7784
  };
7713
7785
  var registered = false;