@helpai/elements 0.26.0 → 0.27.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
@@ -461,13 +461,13 @@ var DEFAULT_FEATURES = {
461
461
  };
462
462
  var DEFAULT_FORMS = { list: [], byTrigger: {} };
463
463
  var DEFAULT_TRACKING = {
464
- enabled: false,
464
+ enabled: true,
465
465
  sampleRate: 1
466
466
  };
467
467
  var DEFAULT_ENDPOINTS = {
468
- upload: "/ai-agent/upload-file",
469
- transcribe: "/ai-agent/transcribe-audio",
470
- submitForm: "/submit-form"
468
+ upload: "/pai/upload-file",
469
+ transcribe: "/pai/transcribe-audio",
470
+ submitForm: "/pai/submit-form"
471
471
  };
472
472
  var DEFAULT_LAUNCHER = {
473
473
  variant: "circle",
@@ -600,9 +600,9 @@ function resolveTracking(overrides, dataBaseUrl) {
600
600
  const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
601
601
  return {
602
602
  enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
603
- // The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
604
- // (e.g. `https://help.ai/api/data/elements/px.gif`).
605
- endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
603
+ // The collector rides the data module — `${dataBaseUrl}/pai/elements-px.gif`
604
+ // (e.g. `https://help.ai/api/data/pai/elements-px.gif`).
605
+ endpoint: overrides?.endpoint ?? `${dataBaseUrl}/pai/elements-px.gif`,
606
606
  // Clamp instead of reject — a malformed rate must never turn a
607
607
  // disabled tracker on or crash resolve (no Zod on the IIFE path).
608
608
  sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
@@ -1308,63 +1308,61 @@ function parseWireDate(value) {
1308
1308
  }
1309
1309
  var DEFAULT_PATHS = {
1310
1310
  /** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
1311
- handshake: "/ai-agent/handshake",
1311
+ handshake: "/pai/handshake",
1312
1312
  /** Send a message + stream the reply (SSE). `POST`. */
1313
- streamMessage: "/ai-agent/stream-message",
1313
+ streamMessage: "/pai/stream-message",
1314
1314
  /**
1315
1315
  * Resume an interrupted reply. `GET` (envelope rides as query params).
1316
1316
  * Stateless: the server re-streams the whole reply SSE again from the start —
1317
1317
  * there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
1318
1318
  * replayed events are skipped and only the continuation surfaces.
1319
1319
  */
1320
- streamResume: "/ai-agent/stream-resume",
1320
+ streamResume: "/pai/stream-resume",
1321
1321
  /** Abort the in-flight reply stream. `POST`. */
1322
- cancelStream: "/ai-agent/cancel-stream",
1322
+ cancelStream: "/pai/cancel-stream",
1323
1323
  /** List the visitor's conversations. `GET`. */
1324
- listConversations: "/ai-agent/list-conversations",
1324
+ listConversations: "/pai/list-conversations",
1325
1325
  /** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
1326
- listMessages: "/ai-agent/list-messages",
1326
+ listMessages: "/pai/list-messages",
1327
1327
  /**
1328
1328
  * Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
1329
1329
  * server persists, returns the merged authoritative copy. Fire-and-forget on
1330
1330
  * the client — a failure leaves the local cache valid; the next
1331
1331
  * `handshake()` reconciles.
1332
1332
  */
1333
- updateSettings: "/ai-agent/update-settings",
1333
+ updateSettings: "/pai/update-settings",
1334
1334
  /**
1335
1335
  * Mark a conversation read. POST `{ visitorId, conversationId }` → the server
1336
1336
  * records `lastReadAt = now` for that (visitor, conversation) and recomputes
1337
1337
  * `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
1338
1338
  * on the client; a failure just leaves the badge until the next sync.
1339
1339
  */
1340
- markRead: "/ai-agent/mark-read",
1340
+ markRead: "/pai/mark-read",
1341
1341
  // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1342
- /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1343
- content: "/content",
1344
1342
  /**
1345
- * Resolved form definitions for the deployment. `GET /forms` →
1346
- * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1347
- * handshake); form gating waits for it. A failure is treated as
1348
- * "no forms" (non-fatal).
1343
+ * The data module's one ACTIVATION read. `GET
1344
+ * /pai/bootstrap?visitorId=…&conversationId=…`
1345
+ * `{ forms, formSubmissions }`: the deployment's resolved form
1346
+ * definitions plus the visitor's recorded submissions for the envelope's
1347
+ * conversation (chronological — they rebuild the timeline markers).
1348
+ * Also re-read with an explicit `conversationId` selector on a
1349
+ * history-pane thread switch. A failure is treated as "no forms, no
1350
+ * records" (non-fatal) — the chat works regardless.
1351
+ */
1352
+ bootstrap: "/pai/bootstrap",
1353
+ /**
1354
+ * All widget content via one filtered endpoint — content stays OUT of the
1355
+ * bootstrap because its reads are query-driven (`tags` / `categories` /
1356
+ * `ids` / `search` + pagination). `GET /pai/content?tags=…`.
1349
1357
  */
1350
- forms: "/forms",
1358
+ content: "/pai/content",
1351
1359
  /**
1352
1360
  * Form submission (the event-driven forms engine). POST `{ visitorId,
1353
1361
  * conversationId, formId, trigger, values, skipped? }` → record the form's answers
1354
1362
  * immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
1355
1363
  * Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
1356
1364
  */
1357
- submitForm: "/submit-form",
1358
- /**
1359
- * Visitor interaction records for a conversation. `GET
1360
- * /activity?visitorId=…&conversationId=…` →
1361
- * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1362
- * the former `formSubmissions` echo on the handshake / list-messages;
1363
- * deliberately generic — future visitor interaction records ride the same
1364
- * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1365
- * deployment doesn't resolve) is treated as "no records" (non-fatal).
1366
- */
1367
- activity: "/activity"
1365
+ submitForm: "/pai/submit-form"
1368
1366
  };
1369
1367
  var CONTEXT_PARAM = "context";
1370
1368
  function buildSendMessageRequest(params) {
@@ -1653,38 +1651,26 @@ var AgentTransport = class {
1653
1651
  return { ...res, items };
1654
1652
  }
1655
1653
  /**
1656
- * Fetch the deployment's resolved form definitions. `GET /forms`
1657
- * (data-API). Replaces the former `config.forms` push on the handshake
1658
- * called once at boot, in parallel with `handshake`; form gating
1659
- * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1660
- * "no forms" (non-fatal).
1661
- */
1662
- async listForms() {
1663
- log4.debug("listForms \u2192");
1664
- const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
1665
- const forms = res.forms ?? [];
1666
- log4.debug("listForms \u2190", { count: forms.length });
1667
- return { forms };
1668
- }
1669
- /**
1670
- * Fetch the visitor's recorded activity for a conversation — currently the
1671
- * form submissions that rebuild the collapsed "form submitted / skipped"
1672
- * timeline markers on resume. `GET /activity` (data-API); the
1673
- * envelope carries `visitorId` / `conversationId` as query params.
1674
- * `conversationId` here is an optional resource selector (a thread that may
1675
- * differ from the active conversation — the history-pane switch); when
1676
- * omitted, the envelope's active conversation applies. Callers treat a
1677
- * thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
1678
- * "no activity" (non-fatal).
1654
+ * The data module's ONE activation read. `GET /pai/bootstrap` (data-API)
1655
+ * the deployment's resolved form definitions + the visitor's recorded
1656
+ * form submissions for the envelope's conversation. The handshake
1657
+ * (agent-API) is completely independent of these surfaces.
1658
+ *
1659
+ * `conversationId` is an optional resource selector (a thread that may
1660
+ * differ from the active conversation — the history-pane switch re-reads
1661
+ * that thread's submissions); when omitted, the envelope's active
1662
+ * conversation applies. Callers treat a thrown StreamError (e.g. 404 on a
1663
+ * deployment that doesn't resolve) as "no forms, no records" (non-fatal).
1679
1664
  */
1680
- async getActivity(conversationId) {
1681
- const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
1665
+ async bootstrap(conversationId) {
1666
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
1682
1667
  if (conversationId) url.searchParams.set("conversationId", conversationId);
1683
- log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
1684
- const res = await this.getJson(url.toString(), "getActivity");
1668
+ log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
1669
+ const res = await this.getJson(url.toString(), "bootstrap");
1670
+ const forms = res.forms ?? [];
1685
1671
  const formSubmissions = res.formSubmissions ?? [];
1686
- log4.debug("getActivity \u2190", { count: formSubmissions.length });
1687
- return { formSubmissions };
1672
+ log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
1673
+ return { forms, formSubmissions };
1688
1674
  }
1689
1675
  async saveUserPrefs(userPrefs) {
1690
1676
  log4.debug("saveUserPrefs \u2192", {
@@ -1697,10 +1683,10 @@ var AgentTransport = class {
1697
1683
  return res;
1698
1684
  }
1699
1685
  get uploadPath() {
1700
- return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
1686
+ return this.opts.endpoints?.upload ?? "/pai/upload-file";
1701
1687
  }
1702
1688
  get transcribePath() {
1703
- return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
1689
+ return this.opts.endpoints?.transcribe ?? "/pai/transcribe-audio";
1704
1690
  }
1705
1691
  get submitFormPath() {
1706
1692
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
@@ -1843,7 +1829,7 @@ var AgentTransport = class {
1843
1829
  }
1844
1830
  return false;
1845
1831
  }
1846
- /** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
1832
+ /** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
1847
1833
  cancelStream(ctrl, conversationId) {
1848
1834
  if (ctrl.signal.aborted) return;
1849
1835
  log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
@@ -1953,7 +1939,7 @@ var AgentTransport = class {
1953
1939
  return await res.json();
1954
1940
  }
1955
1941
  /**
1956
- * Agent-API URL — resolves a `/ai-agent/*` path against the resolved
1942
+ * Agent-API URL — resolves a `/pai/*` path against the resolved
1957
1943
  * agent module base. Absolute URLs pass through unchanged.
1958
1944
  */
1959
1945
  url(pathOrUrl) {
@@ -6616,10 +6602,11 @@ function App({ options, hostElement, bus }) {
6616
6602
  },
6617
6603
  [messagesSig]
6618
6604
  );
6605
+ const dataBootRef = useRef9(null);
6619
6606
  const fetchFormMarkers = useCallback6(
6620
6607
  async (conversationId) => {
6621
6608
  try {
6622
- const { formSubmissions: subs } = await transport.getActivity(conversationId);
6609
+ const { formSubmissions: subs } = await (conversationId ? transport.bootstrap(conversationId) : dataBootRef.current ?? transport.bootstrap());
6623
6610
  const lastIndexByForm = /* @__PURE__ */ new Map();
6624
6611
  subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6625
6612
  return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
@@ -6630,7 +6617,7 @@ function App({ options, hostElement, bus }) {
6630
6617
  values: rec.values
6631
6618
  }));
6632
6619
  } catch (err) {
6633
- log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
6620
+ log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
6634
6621
  return [];
6635
6622
  }
6636
6623
  },
@@ -6819,12 +6806,13 @@ function App({ options, hostElement, bus }) {
6819
6806
  }, []);
6820
6807
  useEffect17(() => {
6821
6808
  if (!activated) return;
6809
+ dataBootRef.current = transport.bootstrap();
6822
6810
  void (async () => {
6823
6811
  try {
6824
- const res = await transport.listForms();
6825
- setRemoteForms(resolveForms(res.forms));
6812
+ const res = await dataBootRef.current;
6813
+ setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
6826
6814
  } catch (err) {
6827
- log16.debug("listForms failed \u2014 treating as no forms", { err });
6815
+ log16.debug("bootstrap failed \u2014 treating as no forms", { err });
6828
6816
  } finally {
6829
6817
  setFormsReady(true);
6830
6818
  }
@@ -7209,6 +7197,20 @@ function App({ options, hostElement, bus }) {
7209
7197
  },
7210
7198
  [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
7211
7199
  );
7200
+ const pendingOpenFormRef = useRef9(null);
7201
+ const openForm = useCallback6(
7202
+ (id) => {
7203
+ if (typeof id !== "string") return;
7204
+ if (formsReady) forms.fire("manual", id);
7205
+ else pendingOpenFormRef.current = id;
7206
+ },
7207
+ [formsReady, forms]
7208
+ );
7209
+ useEffect17(() => {
7210
+ if (!formsReady || !pendingOpenFormRef.current) return;
7211
+ forms.fire("manual", pendingOpenFormRef.current);
7212
+ pendingOpenFormRef.current = null;
7213
+ }, [formsReady, forms]);
7212
7214
  useEffect17(() => {
7213
7215
  const unsub = bindHostCommands(hostElement, {
7214
7216
  open: handleOpen,
@@ -7216,10 +7218,10 @@ function App({ options, hostElement, bus }) {
7216
7218
  expand: handleExpand,
7217
7219
  fullscreen: handleFullscreen,
7218
7220
  popout: handlePopOut,
7219
- openForm: (id) => forms.fire("manual", typeof id === "string" ? id : void 0)
7221
+ openForm
7220
7222
  });
7221
7223
  return unsub;
7222
- }, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut, forms]);
7224
+ }, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut, openForm]);
7223
7225
  const effectiveOptions = useMemo3(
7224
7226
  () => applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize),
7225
7227
  [options, activeLocale, activeThemeMode, activeTextSize]