@helpai/elements 0.24.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, L as Link, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial } from './deployment-BP2axIFu.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, L as Link, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial } from './deployment-B0uvvJYK.js';
2
2
  import 'zod';
3
3
 
4
4
  /**
@@ -180,27 +180,6 @@ type WirePart = {
180
180
  errorText?: string;
181
181
  approval?: ToolApproval;
182
182
  };
183
- /**
184
- * One recorded form submission, returned on `/ai/agent/start-conversation`
185
- * (alongside `messages`) so the widget can re-place the "form submitted /
186
- * skipped" marker at its chronological spot in the timeline.
187
- */
188
- interface FormSubmissionRecord {
189
- formId: string;
190
- /** The trigger kind that surfaced the form. */
191
- trigger: string;
192
- /** True when the visitor skipped instead of answering. */
193
- skipped?: boolean;
194
- /** ISO-8601 — same format + name as {@link WireMessage.createdAt}. */
195
- createdAt?: string;
196
- /**
197
- * The visitor's own answers, echoed back so the submitted marker can offer
198
- * a read-only review after a reload. Scoped to the same `visitorId` that
199
- * submitted them — this is the visitor seeing their own data, independent
200
- * of `submit.toContext` (which governs mirroring into request context).
201
- */
202
- values?: Record<string, string>;
203
- }
204
183
  /** One-shot bot persona shown in the panel header. */
205
184
  interface ConversationAgent {
206
185
  name: string;
@@ -262,13 +241,6 @@ interface StartConversationResponse {
262
241
  /** Page-mode sidebar blocks (navigation, link cards). Backend ships it whenever the deployment renders the page-shell sidebar. */
263
242
  blocks?: Record<string, unknown>;
264
243
  messages?: WireMessage[];
265
- /**
266
- * Form submissions recorded for this conversation (via
267
- * `/ai/agent/submit-form`), returned like `messages` so the widget can
268
- * reconstruct the timeline on resume — each renders as a collapsed
269
- * "form submitted" marker at its chronological position.
270
- */
271
- formSubmissions?: FormSubmissionRecord[];
272
244
  /**
273
245
  * Server's authoritative copy of the visitor's preferences. When present,
274
246
  * client adopts it (overrides local cache) and re-applies it to the UI
@@ -590,7 +562,7 @@ interface FormDef {
590
562
  * answers. Default `true`. Set `false` to keep the marker plain — e.g.
591
563
  * sensitive answers that shouldn't linger on screen (pairs with
592
564
  * `mirrorToContext: false`; the backend should also omit `values` from the
593
- * `formSubmissions` echo for such forms).
565
+ * `/activity` records for such forms).
594
566
  */
595
567
  reviewable?: boolean;
596
568
  }
@@ -698,7 +670,7 @@ interface LauncherOptions {
698
670
  *
699
671
  * `modules` is an ordered list of tabs; each picks a UI `layout`, optional
700
672
  * content `contentTags` (the scope passed as the `tags` filter to
701
- * `GET /ai/agent/content`), and a translatable `label`.
673
+ * `GET /content`), and a translatable `label`.
702
674
  */
703
675
  /** The UI layouts a tab can render with. `chat` is the conversation; the rest render content. */
704
676
  type ModuleLayout = "chat" | "home" | "help" | "news";
@@ -982,9 +954,11 @@ interface PageContext {
982
954
  [key: string]: string | number | boolean | undefined;
983
955
  }
984
956
  /**
985
- * Override the HTTP endpoints the widget calls. Paths are appended to
986
- * `baseUrl`. Absolute URLs also work (useful when the upload endpoint
987
- * lives on a different origin like a CDN presign service).
957
+ * Override the HTTP endpoints the widget calls. `upload` / `transcribe`
958
+ * paths are appended to the agent base (`${baseUrl}/api/agent`);
959
+ * `submitForm` is appended to the data base (`dataBaseUrl`). Absolute URLs
960
+ * also work (useful when the upload endpoint lives on a different origin
961
+ * like a CDN presign service).
988
962
  */
989
963
  interface Endpoints {
990
964
  /** File upload endpoint. Default: `'/ai/agent/upload-file'`. */
@@ -992,7 +966,7 @@ interface Endpoints {
992
966
  /** Voice transcription endpoint. Default: `'/ai/agent/transcribe-audio'`. */
993
967
  transcribe?: string;
994
968
  /**
995
- * Form submission endpoint. Default: `'/ai/agent/submit-form'`. When any form
969
+ * Form submission endpoint. Default: `'/submit-form'`. When any form
996
970
  * (a pre-chat lead capture, a CSAT survey, etc.) is completed, the widget POSTs
997
971
  * the captured values here (fire-and-forget), keyed by the same `visitorId` +
998
972
  * `conversationId` the conversation uses; the payload carries `formId` + `trigger`
@@ -1024,8 +998,8 @@ interface TrackingOptions {
1024
998
  */
1025
999
  enabled?: boolean;
1026
1000
  /**
1027
- * Pixel collector URL. Default: `https://t.<brand domain>/api/ai-elements/px.gif`
1028
- * (e.g. `https://t.help.ai/api/ai-elements/px.gif`).
1001
+ * Pixel collector URL. Default: `${dataBaseUrl}/elements/px.gif`
1002
+ * (e.g. `https://help.ai/api/data/elements/px.gif`).
1029
1003
  */
1030
1004
  endpoint?: string;
1031
1005
  /**
@@ -1090,8 +1064,21 @@ interface ChatWidgetOptions {
1090
1064
  publicKey?: string;
1091
1065
  /** Deployment ID (UUID). Sent as `x-ai-agent-deployment-id`. */
1092
1066
  aiAgentDeploymentId?: string;
1093
- /** Backend origin (no trailing slash). Defaults to the brand's default. */
1067
+ /**
1068
+ * MAIN site origin (no trailing slash, e.g. `https://help.ai`). The API
1069
+ * modules derive from it: the agent API lives at `${baseUrl}/api/agent`,
1070
+ * the data API at `${baseUrl}/api/data`. Defaults to the brand's origin.
1071
+ */
1094
1072
  baseUrl?: string;
1073
+ /**
1074
+ * Data-API base (no trailing slash) — the `module-help-ai-data-api`
1075
+ * service serving content, form definitions, and form submissions at
1076
+ * root-level paths (`/content`, `/forms`, `/activity`, `/submit-form`).
1077
+ * Precedence: this option > server-pushed `config.dataBaseUrl` > derived
1078
+ * from the main origin (`${baseUrl}/api/data`, e.g.
1079
+ * `https://help.ai/api/data`).
1080
+ */
1081
+ dataBaseUrl?: string;
1095
1082
  /** Host-asserted, informational end-user context (untrusted; not auth). */
1096
1083
  userContext?: UserContext;
1097
1084
  /** Host-asserted, informational page/visit context (untrusted; `area`, url, …). */
@@ -1146,6 +1133,8 @@ interface ResolvedOptions {
1146
1133
  publicKey?: string;
1147
1134
  aiAgentDeploymentId?: string;
1148
1135
  baseUrl: string;
1136
+ /** Resolved data-API base — see {@link ChatWidgetOptions.dataBaseUrl}. */
1137
+ dataBaseUrl: string;
1149
1138
  userContext?: UserContext;
1150
1139
  pageContext?: PageContext;
1151
1140
  position: Position;
@@ -1237,8 +1226,14 @@ interface ResolvedOptions {
1237
1226
  * Matches a subset of {@link ChatWidgetOptions} — anything the server can
1238
1227
  * reasonably dictate (UI, behaviour, routing). Callbacks (`onMessage` etc.)
1239
1228
  * and the public key are deliberately excluded — those are client concerns.
1229
+ *
1230
+ * Form definitions no longer ride this config: the widget fetches them from
1231
+ * the data-API (`GET /forms`). A stale backend still pushing
1232
+ * `config.forms` is ignored.
1240
1233
  */
1241
1234
  interface ServerConfig {
1235
+ /** Data-API base override — adopted only when the host didn't set one. */
1236
+ dataBaseUrl?: string;
1242
1237
  presentation?: PresentationOptions;
1243
1238
  behavior?: BehaviorOptions;
1244
1239
  theme?: ThemePreference | ThemeOverrides;
@@ -1249,7 +1244,6 @@ interface ServerConfig {
1249
1244
  i18n?: I18nOptions;
1250
1245
  footer?: FooterOptions;
1251
1246
  features?: FeatureFlags;
1252
- forms?: FormsOptions;
1253
1247
  modules?: ModulesOptions;
1254
1248
  tracking?: TrackingOptions;
1255
1249
  endpoints?: Endpoints;
package/index.mjs CHANGED
@@ -19,7 +19,12 @@ var BRAND = {
19
19
  // and @goai builds.
20
20
  tagName: true ? "web-ai-chat" : "web-ai-chat",
21
21
  cssPrefix: true ? "helpai" : "helpai",
22
- defaultBaseUrl: true ? "https://help.ai/api" : "",
22
+ /**
23
+ * MAIN site origin (e.g. `https://help.ai`) — module bases derive from it:
24
+ * agent API at `${defaultBaseUrl}/api/agent`, data API at
25
+ * `${defaultBaseUrl}/api/data`.
26
+ */
27
+ defaultBaseUrl: true ? "https://help.ai" : "",
23
28
  standaloneUrl: true ? "https://help.ai/chat" : "",
24
29
  /** Public-facing brand label, e.g. "Help AI" / "Go AI". */
25
30
  displayName: true ? "Help AI" : "Help AI",
@@ -472,13 +477,12 @@ var DEFAULT_FEATURES = {
472
477
  var DEFAULT_FORMS = { list: [], byTrigger: {} };
473
478
  var DEFAULT_TRACKING = {
474
479
  enabled: false,
475
- endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
476
480
  sampleRate: 1
477
481
  };
478
482
  var DEFAULT_ENDPOINTS = {
479
483
  upload: "/ai/agent/upload-file",
480
484
  transcribe: "/ai/agent/transcribe-audio",
481
- submitForm: "/ai/agent/submit-form"
485
+ submitForm: "/submit-form"
482
486
  };
483
487
  var DEFAULT_LAUNCHER = {
484
488
  variant: "circle",
@@ -520,11 +524,14 @@ function resolveOptions(rawOpts) {
520
524
  const mode = presentation.mode ?? "floating";
521
525
  const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
522
526
  const availableLocales = i18n.availableLocales ?? [locale];
527
+ const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
528
+ const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
523
529
  return {
524
530
  widgetId: opts.widgetId ?? "default",
525
531
  publicKey: opts.publicKey,
526
532
  aiAgentDeploymentId: opts.aiAgentDeploymentId,
527
- baseUrl: (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, ""),
533
+ baseUrl,
534
+ dataBaseUrl,
528
535
  userContext: sanitizeUserContext(opts.userContext),
529
536
  pageContext: sanitizePageContext(opts.pageContext),
530
537
  position: presentation.position ?? "bottom-right",
@@ -569,7 +576,7 @@ function resolveOptions(rawOpts) {
569
576
  poweredBy: resolvePoweredBy(footer.poweredBy),
570
577
  composer: resolveComposer(opts.composer),
571
578
  modules: resolveModules(opts.modules),
572
- tracking: resolveTracking(opts.tracking),
579
+ tracking: resolveTracking(opts.tracking, dataBaseUrl),
573
580
  storage: opts.storage ?? defaultStorage,
574
581
  onMessage: opts.onMessage,
575
582
  onOpen: opts.onOpen,
@@ -602,11 +609,13 @@ function resolveHaptics(overrides) {
602
609
  events: overrides?.events
603
610
  };
604
611
  }
605
- function resolveTracking(overrides) {
612
+ function resolveTracking(overrides, dataBaseUrl) {
606
613
  const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
607
614
  return {
608
615
  enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
609
- endpoint: overrides?.endpoint ?? DEFAULT_TRACKING.endpoint,
616
+ // The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
617
+ // (e.g. `https://help.ai/api/data/elements/px.gif`).
618
+ endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
610
619
  // Clamp instead of reject — a malformed rate must never turn a
611
620
  // disabled tracker on or crash resolve (no Zod on the IIFE path).
612
621
  sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
@@ -775,7 +784,7 @@ function mergeServerConfig(user, server) {
775
784
  out[key] = mergeLeaves(sv, uv);
776
785
  }
777
786
  if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
778
- if (server.forms !== void 0 && user.forms === void 0) out.forms = server.forms;
787
+ if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
779
788
  const userI18n = user.i18n;
780
789
  const serverI18n = server.i18n;
781
790
  if (userI18n || serverI18n) {
@@ -835,6 +844,7 @@ var EMBED_SCALAR_ATTRS = [
835
844
  ["public-key", "publicKey"],
836
845
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
837
846
  ["base-url", "baseUrl"],
847
+ ["data-base-url", "dataBaseUrl"],
838
848
  ["theme", "theme", (v) => v]
839
849
  ];
840
850
  var PRESENTATION_ATTRS = [
@@ -1270,16 +1280,33 @@ var DEFAULT_PATHS = {
1270
1280
  * on the client; a failure just leaves the badge until the next sync.
1271
1281
  */
1272
1282
  markRead: "/ai/agent/mark-read",
1273
- // ── CMS content (unified Home / Help / News / docs) ─────────────
1274
- /** All widget content via one filtered endpoint. `GET /ai/agent/content?tags=…`. */
1275
- content: "/ai/agent/content",
1283
+ // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1284
+ /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1285
+ content: "/content",
1286
+ /**
1287
+ * Resolved form definitions for the deployment. `GET /forms` →
1288
+ * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1289
+ * start-conversation); form gating waits for it. A failure is treated as
1290
+ * "no forms" (non-fatal).
1291
+ */
1292
+ forms: "/forms",
1276
1293
  /**
1277
1294
  * Form submission (the event-driven forms engine). POST `{ visitorId,
1278
1295
  * conversationId, formId, trigger, values, skipped? }` → record the form's answers
1279
1296
  * immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
1280
1297
  * Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
1281
1298
  */
1282
- submitForm: "/ai/agent/submit-form"
1299
+ submitForm: "/submit-form",
1300
+ /**
1301
+ * Visitor interaction records for a conversation. `GET
1302
+ * /activity?visitorId=…&conversationId=…` →
1303
+ * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1304
+ * the former `formSubmissions` echo on start-conversation / list-messages;
1305
+ * deliberately generic — future visitor interaction records ride the same
1306
+ * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1307
+ * deployment doesn't resolve) is treated as "no records" (non-fatal).
1308
+ */
1309
+ activity: "/activity"
1283
1310
  };
1284
1311
  var CONTEXT_PARAM = "context";
1285
1312
  function buildSendMessageRequest(params) {
@@ -1552,14 +1579,17 @@ var AgentTransport = class {
1552
1579
  log4.debug("markRead failed (non-fatal)", { err });
1553
1580
  }
1554
1581
  }
1555
- // ---- CMS content (unified) --------------------------------------------
1582
+ // ---- Data API (content / forms — module-help-ai-data-api) --------------
1556
1583
  //
1557
- // One endpoint for every content surface (Home / Help / News / docs).
1558
- // Callers pass filters; a thrown StreamError (e.g. 404 on an older backend)
1559
- // is treated as "no content" by the module and the tab degrades gracefully.
1560
- /** Fetch content rows by filter. `GET /ai/agent/content`. */
1584
+ // The DATA surfaces live on `dataBaseUrl` (default `${baseUrl}/api/data`)
1585
+ // with ROOT-level paths same auth headers + envelope, same retry
1586
+ // behavior. One endpoint for every content surface (Home / Help / News /
1587
+ // docs); callers pass filters; a thrown StreamError (e.g. 404 on an older
1588
+ // backend) is treated as "no content" by the module and the tab degrades
1589
+ // gracefully.
1590
+ /** Fetch content rows by filter. `GET /content` (data-API). */
1561
1591
  async listContent(query = {}) {
1562
- const url = new URL(this.url(DEFAULT_PATHS.content));
1592
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
1563
1593
  for (const [key, value] of Object.entries(query)) {
1564
1594
  if (value === void 0) continue;
1565
1595
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
@@ -1570,6 +1600,40 @@ var AgentTransport = class {
1570
1600
  log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1571
1601
  return { ...res, items };
1572
1602
  }
1603
+ /**
1604
+ * Fetch the deployment's resolved form definitions. `GET /forms`
1605
+ * (data-API). Replaces the former `config.forms` push on the handshake —
1606
+ * called once at boot, in parallel with `startConversation`; form gating
1607
+ * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1608
+ * "no forms" (non-fatal).
1609
+ */
1610
+ async listForms() {
1611
+ log4.debug("listForms \u2192");
1612
+ const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
1613
+ const forms = res.forms ?? [];
1614
+ log4.debug("listForms \u2190", { count: forms.length });
1615
+ return { forms };
1616
+ }
1617
+ /**
1618
+ * Fetch the visitor's recorded activity for a conversation — currently the
1619
+ * form submissions that rebuild the collapsed "form submitted / skipped"
1620
+ * timeline markers on resume. `GET /activity` (data-API); the
1621
+ * envelope carries `visitorId` / `conversationId` as query params.
1622
+ * `conversationId` here is an optional resource selector (a thread that may
1623
+ * differ from the active conversation — the history-pane switch); when
1624
+ * omitted, the envelope's active conversation applies. Callers treat a
1625
+ * thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
1626
+ * "no activity" (non-fatal).
1627
+ */
1628
+ async getActivity(conversationId) {
1629
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
1630
+ if (conversationId) url.searchParams.set("conversationId", conversationId);
1631
+ log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
1632
+ const res = await this.getJson(url.toString(), "getActivity");
1633
+ const formSubmissions = res.formSubmissions ?? [];
1634
+ log4.debug("getActivity \u2190", { count: formSubmissions.length });
1635
+ return { formSubmissions };
1636
+ }
1573
1637
  async saveUserPrefs(userPrefs) {
1574
1638
  log4.debug("saveUserPrefs \u2192", {
1575
1639
  visitorId: this.visitorId,
@@ -1590,8 +1654,9 @@ var AgentTransport = class {
1590
1654
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
1591
1655
  }
1592
1656
  /**
1593
- * Record a completed form (intake, CSAT, claim, …). Fire-and-forget — the
1594
- * record is captured even if the visitor never sends a message. `conversationId`,
1657
+ * Record a completed form (intake, CSAT, claim, …). `POST
1658
+ * /submit-form` (data-API). Fire-and-forget the record is
1659
+ * captured even if the visitor never sends a message. `conversationId`,
1595
1660
  * `formId`, and `trigger` are explicit so the backend can tie + route;
1596
1661
  * `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
1597
1662
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
@@ -1599,7 +1664,7 @@ var AgentTransport = class {
1599
1664
  async submitForm(body) {
1600
1665
  log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1601
1666
  try {
1602
- await this.postJson(this.submitFormPath, body, "submitForm");
1667
+ await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1603
1668
  } catch (err) {
1604
1669
  log4.debug("submitForm failed (non-fatal)", { err });
1605
1670
  }
@@ -1835,9 +1900,24 @@ var AgentTransport = class {
1835
1900
  if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
1836
1901
  return await res.json();
1837
1902
  }
1903
+ /**
1904
+ * Agent-API URL — resolves a `/ai/agent/*` path against the agent module
1905
+ * base, `${baseUrl}/api/agent`. Absolute URLs pass through unchanged.
1906
+ */
1838
1907
  url(pathOrUrl) {
1839
1908
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1840
- return `${this.opts.baseUrl}${pathOrUrl}`;
1909
+ return `${this.opts.baseUrl}/api/agent${pathOrUrl}`;
1910
+ }
1911
+ /**
1912
+ * Data-API variant of {@link url} — resolves a root-level path (content /
1913
+ * forms / submit-form / activity) against `dataBaseUrl`. Absolute URLs
1914
+ * (e.g. an `endpoints.submitForm` pointing straight at a CRM) pass through
1915
+ * unchanged; with no `dataBaseUrl` configured the data module derives from
1916
+ * the main origin: `${baseUrl}/api/data`.
1917
+ */
1918
+ dataUrl(pathOrUrl) {
1919
+ if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1920
+ return `${this.opts.dataBaseUrl ?? `${this.opts.baseUrl}/api/data`}${pathOrUrl}`;
1841
1921
  }
1842
1922
  get fetchImpl() {
1843
1923
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -6362,6 +6442,8 @@ function App({ options, hostElement, bus }) {
6362
6442
  const chatTabIdRef = useRef9(void 0);
6363
6443
  chatTabIdRef.current = chatTabId();
6364
6444
  const [conversationReady, setConversationReady] = useState13(false);
6445
+ const [remoteForms, setRemoteForms] = useState13(null);
6446
+ const [formsReady, setFormsReady] = useState13(false);
6365
6447
  const isInlineLike = options.mode === "standalone" || options.mode === "inline";
6366
6448
  const initialPanelRef = useRef9(
6367
6449
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
@@ -6394,6 +6476,7 @@ function App({ options, hostElement, bus }) {
6394
6476
  const [transport] = useState13(
6395
6477
  () => new AgentTransport({
6396
6478
  baseUrl: options.baseUrl,
6479
+ dataBaseUrl: options.dataBaseUrl,
6397
6480
  auth: createAuth({
6398
6481
  publicKey: options.publicKey,
6399
6482
  aiAgentDeploymentId: options.aiAgentDeploymentId,
@@ -6474,6 +6557,26 @@ function App({ options, hostElement, bus }) {
6474
6557
  },
6475
6558
  [messagesSig]
6476
6559
  );
6560
+ const fetchFormMarkers = useCallback6(
6561
+ async (conversationId) => {
6562
+ try {
6563
+ const { formSubmissions: subs } = await transport.getActivity(conversationId);
6564
+ const lastIndexByForm = /* @__PURE__ */ new Map();
6565
+ subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6566
+ return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6567
+ key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6568
+ formId: rec.formId,
6569
+ outcome: rec.skipped ? "skipped" : "submitted",
6570
+ createdAt: parseWireDate(rec.createdAt),
6571
+ values: rec.values
6572
+ }));
6573
+ } catch (err) {
6574
+ log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
6575
+ return [];
6576
+ }
6577
+ },
6578
+ [transport]
6579
+ );
6477
6580
  const connectGenRef = useRef9(0);
6478
6581
  const runStartConversation = useCallback6(
6479
6582
  async ({ newConversation }) => {
@@ -6542,24 +6645,14 @@ function App({ options, hostElement, bus }) {
6542
6645
  if (isResume) {
6543
6646
  setLoadingMessages(true);
6544
6647
  try {
6545
- const chat = res.messages?.length ? {
6648
+ const chatPromise = res.messages?.length ? Promise.resolve({
6546
6649
  conversationId: persistedChatId,
6547
6650
  canContinue: res.canContinue ?? true,
6548
- messages: res.messages,
6549
- formSubmissions: res.formSubmissions
6550
- } : await transport.loadConversation(persistedChatId);
6651
+ messages: res.messages
6652
+ }) : transport.loadConversation(persistedChatId);
6653
+ const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6551
6654
  if (isStale()) return;
6552
6655
  const loaded = (chat.messages ?? []).map(fromWireMessage);
6553
- const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
6554
- const lastIndexByForm = /* @__PURE__ */ new Map();
6555
- subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6556
- const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6557
- key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6558
- formId: rec.formId,
6559
- outcome: rec.skipped ? "skipped" : "submitted",
6560
- createdAt: parseWireDate(rec.createdAt),
6561
- values: rec.values
6562
- }));
6563
6656
  setFormMarkers(markers);
6564
6657
  const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6565
6658
  (n) => n !== void 0
@@ -6597,7 +6690,18 @@ function App({ options, hostElement, bus }) {
6597
6690
  setConversationReady(true);
6598
6691
  }
6599
6692
  },
6600
- [transport, visitorId, persistence, options, bus, messagesSig, conversationIdSig, feedback, playWelcome]
6693
+ [
6694
+ transport,
6695
+ visitorId,
6696
+ persistence,
6697
+ options,
6698
+ bus,
6699
+ messagesSig,
6700
+ conversationIdSig,
6701
+ feedback,
6702
+ playWelcome,
6703
+ fetchFormMarkers
6704
+ ]
6601
6705
  );
6602
6706
  const userSig = JSON.stringify(options.userContext ?? null);
6603
6707
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6652,6 +6756,16 @@ function App({ options, hostElement, bus }) {
6652
6756
  );
6653
6757
  useEffect17(() => {
6654
6758
  void runStartConversation({ newConversation: false });
6759
+ void (async () => {
6760
+ try {
6761
+ const res = await transport.listForms();
6762
+ setRemoteForms(resolveForms(res.forms));
6763
+ } catch (err) {
6764
+ log16.debug("listForms failed \u2014 treating as no forms", { err });
6765
+ } finally {
6766
+ setFormsReady(true);
6767
+ }
6768
+ })();
6655
6769
  const persistedConversation = conversationIdSig.value;
6656
6770
  let resume = null;
6657
6771
  if (persistedConversation) {
@@ -6788,8 +6902,9 @@ function App({ options, hostElement, bus }) {
6788
6902
  [options.features.humanInLoop, handleToolResult, handleToolDecision]
6789
6903
  );
6790
6904
  const userMessageCount = () => messagesSig.value.reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0);
6905
+ const effectiveForms = options.forms.list.length > 0 ? options.forms : remoteForms ?? options.forms;
6791
6906
  const forms = useForms({
6792
- forms: options.forms,
6907
+ forms: effectiveForms,
6793
6908
  persistence,
6794
6909
  userContext: () => options.userContext,
6795
6910
  messageCount: userMessageCount,
@@ -6822,18 +6937,18 @@ function App({ options, hostElement, bus }) {
6822
6937
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
6823
6938
  const msgCount = useComputed7(() => messagesSig.value.length);
6824
6939
  useEffect17(() => {
6825
- if (conversationReady) forms.fire("pre-chat");
6826
- }, [conversationReady, forms]);
6940
+ if (conversationReady && formsReady) forms.fire("pre-chat");
6941
+ }, [conversationReady, formsReady, forms]);
6827
6942
  useEffect17(() => {
6828
- if (!canSend) forms.fire("conversation-closed");
6829
- }, [canSend, forms]);
6943
+ if (formsReady && !canSend) forms.fire("conversation-closed");
6944
+ }, [formsReady, canSend, forms]);
6830
6945
  useEffect17(() => {
6831
- if (conversationReady && pageArea) forms.fire("page-area");
6832
- }, [conversationReady, pageArea, forms]);
6946
+ if (conversationReady && formsReady && pageArea) forms.fire("page-area");
6947
+ }, [conversationReady, formsReady, pageArea, forms]);
6833
6948
  const idleMs = useMemo3(() => {
6834
- const secs = options.forms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
6949
+ const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
6835
6950
  return secs.length ? Math.min(...secs) * 1e3 : 0;
6836
- }, [options.forms]);
6951
+ }, [effectiveForms]);
6837
6952
  useEffect17(() => {
6838
6953
  if (!idleMs || !isOpen) return;
6839
6954
  const id = setTimeout(() => forms.fire("idle"), idleMs);
@@ -7010,9 +7125,13 @@ function App({ options, hostElement, bus }) {
7010
7125
  log16.info("selectConversation", { conversationId: targetConversationId });
7011
7126
  bus.emit("selectConversation", { conversationId: targetConversationId });
7012
7127
  try {
7013
- const res = await transport.loadConversation(targetConversationId);
7128
+ const [res, markers] = await Promise.all([
7129
+ transport.loadConversation(targetConversationId),
7130
+ fetchFormMarkers(targetConversationId)
7131
+ ]);
7014
7132
  const hydrated = res.messages.map(fromWireMessage);
7015
7133
  messagesSig.value = hydrated;
7134
+ setFormMarkers(markers);
7016
7135
  conversationIdSig.value = res.conversationId;
7017
7136
  persistence.saveConversationId(res.conversationId);
7018
7137
  if (res.agent) setAgent(res.agent);
@@ -7026,7 +7145,7 @@ function App({ options, hostElement, bus }) {
7026
7145
  options.onError?.(err);
7027
7146
  }
7028
7147
  },
7029
- [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
7148
+ [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
7030
7149
  );
7031
7150
  useEffect17(() => {
7032
7151
  const unsub = bindHostCommands(hostElement, {
@@ -7045,10 +7164,10 @@ function App({ options, hostElement, bus }) {
7045
7164
  );
7046
7165
  const enrichedFormMarkers = useMemo3(
7047
7166
  () => formMarkers.map((m) => {
7048
- const def = options.forms.list.find((f) => f.id === m.formId);
7167
+ const def = effectiveForms.list.find((f) => f.id === m.formId);
7049
7168
  return { ...m, title: def?.title, form: def };
7050
7169
  }),
7051
- [formMarkers, options.forms]
7170
+ [formMarkers, effectiveForms]
7052
7171
  );
7053
7172
  const panelProps = {
7054
7173
  options: effectiveOptions,
package/package.json CHANGED
@@ -80,5 +80,5 @@
80
80
  ],
81
81
  "type": "module",
82
82
  "types": "./index.d.ts",
83
- "version": "0.24.0"
83
+ "version": "0.25.0"
84
84
  }
package/schema.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, L as Link, P as PAGE_AREA_SUGGESTIONS, g as PageContext, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, U as UserContext, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial, h as assetSchema, i as blocksConfigSchema, j as connectionConfigPartialSchema, k as connectionConfigSchema, l as cssColorSchema, m as cssLengthSchema, n as endpointsSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, t as startConversationResponseSchema, u as userContextSchema, v as uuid7Schema, w as widgetConfigPartialSchema, x as widgetConfigSchema, y as widgetSettingsPartialSchema, z as widgetSettingsSchema } from './deployment-BP2axIFu.js';
1
+ export { A as Asset, B as BlocksConfig, C as ConnectionConfig, a as ConnectionConfigPartial, E as Endpoints, L as Link, P as PAGE_AREA_SUGGESTIONS, g as PageContext, S as ServerConfig, b as SiteConfig, c as StartConversationResponse, U as UserContext, W as WidgetConfig, d as WidgetConfigPartial, e as WidgetSettings, f as WidgetSettingsPartial, h as assetSchema, i as blocksConfigSchema, j as connectionConfigPartialSchema, k as connectionConfigSchema, l as cssColorSchema, m as cssLengthSchema, n as endpointsSchema, o as linkSchema, p as localeSchema, q as pageContextSchema, s as serverConfigSchema, r as siteConfigSchema, t as startConversationResponseSchema, u as userContextSchema, v as uuid7Schema, w as widgetConfigPartialSchema, x as widgetConfigSchema, y as widgetSettingsPartialSchema, z as widgetSettingsSchema } from './deployment-B0uvvJYK.js';
2
2
  import { z } from 'zod';
3
3
 
4
4
  /**
@@ -776,7 +776,7 @@ type I18nOptions = z.infer<typeof i18nSchema>;
776
776
  * meaningful in `floating` / `drawer` / `modal` modes.
777
777
  *
778
778
  * The list arrives via the start-conversation (`config.modules`); each non-chat
779
- * tab lazy-fetches its content from `GET /ai/agent/content` scoped by its
779
+ * tab lazy-fetches its content from `GET /content` scoped by its
780
780
  * `category`. See `docs/api/messenger-modules.md` + `docs/api/content.md`.
781
781
  */
782
782