@helpai/elements 0.23.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-D7_pjSGq.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
@@ -579,6 +551,20 @@ interface FormDef {
579
551
  * submit endpoint, never onto the always-on context envelope.
580
552
  */
581
553
  mirrorToContext?: boolean;
554
+ /**
555
+ * Skipped marker offers a "Fill out" button that reopens the form (skip is
556
+ * a deferral, not a refusal). Default `true`. Set `false` when a skip must
557
+ * be final — e.g. a consent ask that should not be re-surfaced.
558
+ */
559
+ reopenable?: boolean;
560
+ /**
561
+ * Submitted marker expands in place to a read-only list of the visitor's
562
+ * answers. Default `true`. Set `false` to keep the marker plain — e.g.
563
+ * sensitive answers that shouldn't linger on screen (pairs with
564
+ * `mirrorToContext: false`; the backend should also omit `values` from the
565
+ * `/activity` records for such forms).
566
+ */
567
+ reviewable?: boolean;
582
568
  }
583
569
  /** The `forms` block is an ordered list (max 20); order = priority. */
584
570
  type FormsOptions = FormDef[];
@@ -595,6 +581,10 @@ interface ResolvedForm {
595
581
  frequency: FormFrequency;
596
582
  /** Mirror answers into `userContext` (false → PII, endpoint only). */
597
583
  mirrorToContext: boolean;
584
+ /** Skipped marker offers "Fill out" (false → a skip is final). */
585
+ reopenable: boolean;
586
+ /** Submitted marker expands to a read-only answers list. */
587
+ reviewable: boolean;
598
588
  }
599
589
  /** Fully-resolved forms block: ordered list + an event→forms index. */
600
590
  interface ResolvedForms {
@@ -680,7 +670,7 @@ interface LauncherOptions {
680
670
  *
681
671
  * `modules` is an ordered list of tabs; each picks a UI `layout`, optional
682
672
  * content `contentTags` (the scope passed as the `tags` filter to
683
- * `GET /ai/agent/content`), and a translatable `label`.
673
+ * `GET /content`), and a translatable `label`.
684
674
  */
685
675
  /** The UI layouts a tab can render with. `chat` is the conversation; the rest render content. */
686
676
  type ModuleLayout = "chat" | "home" | "help" | "news";
@@ -964,9 +954,11 @@ interface PageContext {
964
954
  [key: string]: string | number | boolean | undefined;
965
955
  }
966
956
  /**
967
- * Override the HTTP endpoints the widget calls. Paths are appended to
968
- * `baseUrl`. Absolute URLs also work (useful when the upload endpoint
969
- * 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).
970
962
  */
971
963
  interface Endpoints {
972
964
  /** File upload endpoint. Default: `'/ai/agent/upload-file'`. */
@@ -974,7 +966,7 @@ interface Endpoints {
974
966
  /** Voice transcription endpoint. Default: `'/ai/agent/transcribe-audio'`. */
975
967
  transcribe?: string;
976
968
  /**
977
- * Form submission endpoint. Default: `'/ai/agent/submit-form'`. When any form
969
+ * Form submission endpoint. Default: `'/submit-form'`. When any form
978
970
  * (a pre-chat lead capture, a CSAT survey, etc.) is completed, the widget POSTs
979
971
  * the captured values here (fire-and-forget), keyed by the same `visitorId` +
980
972
  * `conversationId` the conversation uses; the payload carries `formId` + `trigger`
@@ -1006,8 +998,8 @@ interface TrackingOptions {
1006
998
  */
1007
999
  enabled?: boolean;
1008
1000
  /**
1009
- * Pixel collector URL. Default: `https://t.<brand domain>/api/ai-elements/px.gif`
1010
- * (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`).
1011
1003
  */
1012
1004
  endpoint?: string;
1013
1005
  /**
@@ -1072,8 +1064,21 @@ interface ChatWidgetOptions {
1072
1064
  publicKey?: string;
1073
1065
  /** Deployment ID (UUID). Sent as `x-ai-agent-deployment-id`. */
1074
1066
  aiAgentDeploymentId?: string;
1075
- /** 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
+ */
1076
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;
1077
1082
  /** Host-asserted, informational end-user context (untrusted; not auth). */
1078
1083
  userContext?: UserContext;
1079
1084
  /** Host-asserted, informational page/visit context (untrusted; `area`, url, …). */
@@ -1128,6 +1133,8 @@ interface ResolvedOptions {
1128
1133
  publicKey?: string;
1129
1134
  aiAgentDeploymentId?: string;
1130
1135
  baseUrl: string;
1136
+ /** Resolved data-API base — see {@link ChatWidgetOptions.dataBaseUrl}. */
1137
+ dataBaseUrl: string;
1131
1138
  userContext?: UserContext;
1132
1139
  pageContext?: PageContext;
1133
1140
  position: Position;
@@ -1219,8 +1226,14 @@ interface ResolvedOptions {
1219
1226
  * Matches a subset of {@link ChatWidgetOptions} — anything the server can
1220
1227
  * reasonably dictate (UI, behaviour, routing). Callbacks (`onMessage` etc.)
1221
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.
1222
1233
  */
1223
1234
  interface ServerConfig {
1235
+ /** Data-API base override — adopted only when the host didn't set one. */
1236
+ dataBaseUrl?: string;
1224
1237
  presentation?: PresentationOptions;
1225
1238
  behavior?: BehaviorOptions;
1226
1239
  theme?: ThemePreference | ThemeOverrides;
@@ -1231,7 +1244,6 @@ interface ServerConfig {
1231
1244
  i18n?: I18nOptions;
1232
1245
  footer?: FooterOptions;
1233
1246
  features?: FeatureFlags;
1234
- forms?: FormsOptions;
1235
1247
  modules?: ModulesOptions;
1236
1248
  tracking?: TrackingOptions;
1237
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)),
@@ -680,7 +689,9 @@ function resolveForms(overrides) {
680
689
  submitLabel: def.submitLabel,
681
690
  skippable: def.skippable ?? false,
682
691
  frequency: def.frequency ?? "once",
683
- mirrorToContext: def.mirrorToContext ?? true
692
+ mirrorToContext: def.mirrorToContext ?? true,
693
+ reopenable: def.reopenable ?? true,
694
+ reviewable: def.reviewable ?? true
684
695
  });
685
696
  }
686
697
  const byTrigger = {};
@@ -773,7 +784,7 @@ function mergeServerConfig(user, server) {
773
784
  out[key] = mergeLeaves(sv, uv);
774
785
  }
775
786
  if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
776
- 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;
777
788
  const userI18n = user.i18n;
778
789
  const serverI18n = server.i18n;
779
790
  if (userI18n || serverI18n) {
@@ -833,6 +844,7 @@ var EMBED_SCALAR_ATTRS = [
833
844
  ["public-key", "publicKey"],
834
845
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
835
846
  ["base-url", "baseUrl"],
847
+ ["data-base-url", "dataBaseUrl"],
836
848
  ["theme", "theme", (v) => v]
837
849
  ];
838
850
  var PRESENTATION_ATTRS = [
@@ -1268,16 +1280,33 @@ var DEFAULT_PATHS = {
1268
1280
  * on the client; a failure just leaves the badge until the next sync.
1269
1281
  */
1270
1282
  markRead: "/ai/agent/mark-read",
1271
- // ── CMS content (unified Home / Help / News / docs) ─────────────
1272
- /** All widget content via one filtered endpoint. `GET /ai/agent/content?tags=…`. */
1273
- 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",
1274
1293
  /**
1275
1294
  * Form submission (the event-driven forms engine). POST `{ visitorId,
1276
1295
  * conversationId, formId, trigger, values, skipped? }` → record the form's answers
1277
1296
  * immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
1278
1297
  * Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
1279
1298
  */
1280
- 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"
1281
1310
  };
1282
1311
  var CONTEXT_PARAM = "context";
1283
1312
  function buildSendMessageRequest(params) {
@@ -1550,14 +1579,17 @@ var AgentTransport = class {
1550
1579
  log4.debug("markRead failed (non-fatal)", { err });
1551
1580
  }
1552
1581
  }
1553
- // ---- CMS content (unified) --------------------------------------------
1582
+ // ---- Data API (content / forms — module-help-ai-data-api) --------------
1554
1583
  //
1555
- // One endpoint for every content surface (Home / Help / News / docs).
1556
- // Callers pass filters; a thrown StreamError (e.g. 404 on an older backend)
1557
- // is treated as "no content" by the module and the tab degrades gracefully.
1558
- /** 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). */
1559
1591
  async listContent(query = {}) {
1560
- const url = new URL(this.url(DEFAULT_PATHS.content));
1592
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
1561
1593
  for (const [key, value] of Object.entries(query)) {
1562
1594
  if (value === void 0) continue;
1563
1595
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
@@ -1568,6 +1600,40 @@ var AgentTransport = class {
1568
1600
  log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1569
1601
  return { ...res, items };
1570
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
+ }
1571
1637
  async saveUserPrefs(userPrefs) {
1572
1638
  log4.debug("saveUserPrefs \u2192", {
1573
1639
  visitorId: this.visitorId,
@@ -1588,8 +1654,9 @@ var AgentTransport = class {
1588
1654
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
1589
1655
  }
1590
1656
  /**
1591
- * Record a completed form (intake, CSAT, claim, …). Fire-and-forget — the
1592
- * 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`,
1593
1660
  * `formId`, and `trigger` are explicit so the backend can tie + route;
1594
1661
  * `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
1595
1662
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
@@ -1597,7 +1664,7 @@ var AgentTransport = class {
1597
1664
  async submitForm(body) {
1598
1665
  log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1599
1666
  try {
1600
- await this.postJson(this.submitFormPath, body, "submitForm");
1667
+ await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1601
1668
  } catch (err) {
1602
1669
  log4.debug("submitForm failed (non-fatal)", { err });
1603
1670
  }
@@ -1833,9 +1900,24 @@ var AgentTransport = class {
1833
1900
  if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
1834
1901
  return await res.json();
1835
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
+ */
1836
1907
  url(pathOrUrl) {
1837
1908
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1838
- 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}`;
1839
1921
  }
1840
1922
  get fetchImpl() {
1841
1923
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -4302,7 +4384,8 @@ function FormDoneMarker({
4302
4384
  "\xB7 ",
4303
4385
  marker.title
4304
4386
  ] }) : null;
4305
- const values = !skipped && marker.values && Object.keys(marker.values).length > 0 ? marker.values : null;
4387
+ const reviewable = marker.form?.reviewable ?? false;
4388
+ const values = !skipped && reviewable && marker.values && Object.keys(marker.values).length > 0 ? marker.values : null;
4306
4389
  if (values) {
4307
4390
  const labelFor = (name) => marker.form?.fields.find((f) => f.name === name)?.label ?? name;
4308
4391
  return /* @__PURE__ */ jsxs9("details", { class: `${p11}-form-done`, "data-outcome": marker.outcome, "data-testid": TID.formDone, children: [
@@ -4322,7 +4405,7 @@ function FormDoneMarker({
4322
4405
  skipped ? /* @__PURE__ */ jsx11(SkipIcon, {}) : /* @__PURE__ */ jsx11(CheckIcon, {}),
4323
4406
  /* @__PURE__ */ jsx11("span", { children: skipped ? strings.formSkipped : strings.formSubmitted }),
4324
4407
  title,
4325
- skipped && marker.form && onFill ? /* @__PURE__ */ jsx11("button", { type: "button", class: `${p11}-form-done-fill`, "data-testid": TID.formFill, onClick: onFill, children: strings.formFillOut }) : null
4408
+ skipped && marker.form?.reopenable && onFill ? /* @__PURE__ */ jsx11("button", { type: "button", class: `${p11}-form-done-fill`, "data-testid": TID.formFill, onClick: onFill, children: strings.formFillOut }) : null
4326
4409
  ] });
4327
4410
  }
4328
4411
 
@@ -6359,6 +6442,8 @@ function App({ options, hostElement, bus }) {
6359
6442
  const chatTabIdRef = useRef9(void 0);
6360
6443
  chatTabIdRef.current = chatTabId();
6361
6444
  const [conversationReady, setConversationReady] = useState13(false);
6445
+ const [remoteForms, setRemoteForms] = useState13(null);
6446
+ const [formsReady, setFormsReady] = useState13(false);
6362
6447
  const isInlineLike = options.mode === "standalone" || options.mode === "inline";
6363
6448
  const initialPanelRef = useRef9(
6364
6449
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
@@ -6391,6 +6476,7 @@ function App({ options, hostElement, bus }) {
6391
6476
  const [transport] = useState13(
6392
6477
  () => new AgentTransport({
6393
6478
  baseUrl: options.baseUrl,
6479
+ dataBaseUrl: options.dataBaseUrl,
6394
6480
  auth: createAuth({
6395
6481
  publicKey: options.publicKey,
6396
6482
  aiAgentDeploymentId: options.aiAgentDeploymentId,
@@ -6471,6 +6557,26 @@ function App({ options, hostElement, bus }) {
6471
6557
  },
6472
6558
  [messagesSig]
6473
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
+ );
6474
6580
  const connectGenRef = useRef9(0);
6475
6581
  const runStartConversation = useCallback6(
6476
6582
  async ({ newConversation }) => {
@@ -6539,24 +6645,14 @@ function App({ options, hostElement, bus }) {
6539
6645
  if (isResume) {
6540
6646
  setLoadingMessages(true);
6541
6647
  try {
6542
- const chat = res.messages?.length ? {
6648
+ const chatPromise = res.messages?.length ? Promise.resolve({
6543
6649
  conversationId: persistedChatId,
6544
6650
  canContinue: res.canContinue ?? true,
6545
- messages: res.messages,
6546
- formSubmissions: res.formSubmissions
6547
- } : await transport.loadConversation(persistedChatId);
6651
+ messages: res.messages
6652
+ }) : transport.loadConversation(persistedChatId);
6653
+ const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6548
6654
  if (isStale()) return;
6549
6655
  const loaded = (chat.messages ?? []).map(fromWireMessage);
6550
- const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
6551
- const lastIndexByForm = /* @__PURE__ */ new Map();
6552
- subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6553
- const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6554
- key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6555
- formId: rec.formId,
6556
- outcome: rec.skipped ? "skipped" : "submitted",
6557
- createdAt: parseWireDate(rec.createdAt),
6558
- values: rec.values
6559
- }));
6560
6656
  setFormMarkers(markers);
6561
6657
  const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6562
6658
  (n) => n !== void 0
@@ -6594,7 +6690,18 @@ function App({ options, hostElement, bus }) {
6594
6690
  setConversationReady(true);
6595
6691
  }
6596
6692
  },
6597
- [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
+ ]
6598
6705
  );
6599
6706
  const userSig = JSON.stringify(options.userContext ?? null);
6600
6707
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6649,6 +6756,16 @@ function App({ options, hostElement, bus }) {
6649
6756
  );
6650
6757
  useEffect17(() => {
6651
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
+ })();
6652
6769
  const persistedConversation = conversationIdSig.value;
6653
6770
  let resume = null;
6654
6771
  if (persistedConversation) {
@@ -6785,8 +6902,9 @@ function App({ options, hostElement, bus }) {
6785
6902
  [options.features.humanInLoop, handleToolResult, handleToolDecision]
6786
6903
  );
6787
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;
6788
6906
  const forms = useForms({
6789
- forms: options.forms,
6907
+ forms: effectiveForms,
6790
6908
  persistence,
6791
6909
  userContext: () => options.userContext,
6792
6910
  messageCount: userMessageCount,
@@ -6819,18 +6937,18 @@ function App({ options, hostElement, bus }) {
6819
6937
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
6820
6938
  const msgCount = useComputed7(() => messagesSig.value.length);
6821
6939
  useEffect17(() => {
6822
- if (conversationReady) forms.fire("pre-chat");
6823
- }, [conversationReady, forms]);
6940
+ if (conversationReady && formsReady) forms.fire("pre-chat");
6941
+ }, [conversationReady, formsReady, forms]);
6824
6942
  useEffect17(() => {
6825
- if (!canSend) forms.fire("conversation-closed");
6826
- }, [canSend, forms]);
6943
+ if (formsReady && !canSend) forms.fire("conversation-closed");
6944
+ }, [formsReady, canSend, forms]);
6827
6945
  useEffect17(() => {
6828
- if (conversationReady && pageArea) forms.fire("page-area");
6829
- }, [conversationReady, pageArea, forms]);
6946
+ if (conversationReady && formsReady && pageArea) forms.fire("page-area");
6947
+ }, [conversationReady, formsReady, pageArea, forms]);
6830
6948
  const idleMs = useMemo3(() => {
6831
- 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));
6832
6950
  return secs.length ? Math.min(...secs) * 1e3 : 0;
6833
- }, [options.forms]);
6951
+ }, [effectiveForms]);
6834
6952
  useEffect17(() => {
6835
6953
  if (!idleMs || !isOpen) return;
6836
6954
  const id = setTimeout(() => forms.fire("idle"), idleMs);
@@ -7007,9 +7125,13 @@ function App({ options, hostElement, bus }) {
7007
7125
  log16.info("selectConversation", { conversationId: targetConversationId });
7008
7126
  bus.emit("selectConversation", { conversationId: targetConversationId });
7009
7127
  try {
7010
- const res = await transport.loadConversation(targetConversationId);
7128
+ const [res, markers] = await Promise.all([
7129
+ transport.loadConversation(targetConversationId),
7130
+ fetchFormMarkers(targetConversationId)
7131
+ ]);
7011
7132
  const hydrated = res.messages.map(fromWireMessage);
7012
7133
  messagesSig.value = hydrated;
7134
+ setFormMarkers(markers);
7013
7135
  conversationIdSig.value = res.conversationId;
7014
7136
  persistence.saveConversationId(res.conversationId);
7015
7137
  if (res.agent) setAgent(res.agent);
@@ -7023,7 +7145,7 @@ function App({ options, hostElement, bus }) {
7023
7145
  options.onError?.(err);
7024
7146
  }
7025
7147
  },
7026
- [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
7148
+ [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
7027
7149
  );
7028
7150
  useEffect17(() => {
7029
7151
  const unsub = bindHostCommands(hostElement, {
@@ -7042,10 +7164,10 @@ function App({ options, hostElement, bus }) {
7042
7164
  );
7043
7165
  const enrichedFormMarkers = useMemo3(
7044
7166
  () => formMarkers.map((m) => {
7045
- const def = options.forms.list.find((f) => f.id === m.formId);
7167
+ const def = effectiveForms.list.find((f) => f.id === m.formId);
7046
7168
  return { ...m, title: def?.title, form: def };
7047
7169
  }),
7048
- [formMarkers, options.forms]
7170
+ [formMarkers, effectiveForms]
7049
7171
  );
7050
7172
  const panelProps = {
7051
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.23.0"
83
+ "version": "0.25.0"
84
84
  }