@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/web-component.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",
@@ -457,13 +462,12 @@ var DEFAULT_FEATURES = {
457
462
  var DEFAULT_FORMS = { list: [], byTrigger: {} };
458
463
  var DEFAULT_TRACKING = {
459
464
  enabled: false,
460
- endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
461
465
  sampleRate: 1
462
466
  };
463
467
  var DEFAULT_ENDPOINTS = {
464
468
  upload: "/ai/agent/upload-file",
465
469
  transcribe: "/ai/agent/transcribe-audio",
466
- submitForm: "/ai/agent/submit-form"
470
+ submitForm: "/submit-form"
467
471
  };
468
472
  var DEFAULT_LAUNCHER = {
469
473
  variant: "circle",
@@ -505,11 +509,14 @@ function resolveOptions(rawOpts) {
505
509
  const mode = presentation.mode ?? "floating";
506
510
  const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
507
511
  const availableLocales = i18n.availableLocales ?? [locale];
512
+ const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
513
+ const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
508
514
  return {
509
515
  widgetId: opts.widgetId ?? "default",
510
516
  publicKey: opts.publicKey,
511
517
  aiAgentDeploymentId: opts.aiAgentDeploymentId,
512
- baseUrl: (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, ""),
518
+ baseUrl,
519
+ dataBaseUrl,
513
520
  userContext: sanitizeUserContext(opts.userContext),
514
521
  pageContext: sanitizePageContext(opts.pageContext),
515
522
  position: presentation.position ?? "bottom-right",
@@ -554,7 +561,7 @@ function resolveOptions(rawOpts) {
554
561
  poweredBy: resolvePoweredBy(footer.poweredBy),
555
562
  composer: resolveComposer(opts.composer),
556
563
  modules: resolveModules(opts.modules),
557
- tracking: resolveTracking(opts.tracking),
564
+ tracking: resolveTracking(opts.tracking, dataBaseUrl),
558
565
  storage: opts.storage ?? defaultStorage,
559
566
  onMessage: opts.onMessage,
560
567
  onOpen: opts.onOpen,
@@ -587,11 +594,13 @@ function resolveHaptics(overrides) {
587
594
  events: overrides?.events
588
595
  };
589
596
  }
590
- function resolveTracking(overrides) {
597
+ function resolveTracking(overrides, dataBaseUrl) {
591
598
  const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
592
599
  return {
593
600
  enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
594
- endpoint: overrides?.endpoint ?? DEFAULT_TRACKING.endpoint,
601
+ // The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
602
+ // (e.g. `https://help.ai/api/data/elements/px.gif`).
603
+ endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
595
604
  // Clamp instead of reject — a malformed rate must never turn a
596
605
  // disabled tracker on or crash resolve (no Zod on the IIFE path).
597
606
  sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
@@ -760,7 +769,7 @@ function mergeServerConfig(user, server) {
760
769
  out[key] = mergeLeaves(sv, uv);
761
770
  }
762
771
  if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
763
- if (server.forms !== void 0 && user.forms === void 0) out.forms = server.forms;
772
+ if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
764
773
  const userI18n = user.i18n;
765
774
  const serverI18n = server.i18n;
766
775
  if (userI18n || serverI18n) {
@@ -820,6 +829,7 @@ var EMBED_SCALAR_ATTRS = [
820
829
  ["public-key", "publicKey"],
821
830
  ["ai-agent-deployment-id", "aiAgentDeploymentId"],
822
831
  ["base-url", "baseUrl"],
832
+ ["data-base-url", "dataBaseUrl"],
823
833
  ["theme", "theme", (v) => v]
824
834
  ];
825
835
  var PRESENTATION_ATTRS = [
@@ -1325,16 +1335,33 @@ var DEFAULT_PATHS = {
1325
1335
  * on the client; a failure just leaves the badge until the next sync.
1326
1336
  */
1327
1337
  markRead: "/ai/agent/mark-read",
1328
- // ── CMS content (unified Home / Help / News / docs) ─────────────
1329
- /** All widget content via one filtered endpoint. `GET /ai/agent/content?tags=…`. */
1330
- content: "/ai/agent/content",
1338
+ // ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
1339
+ /** All widget content via one filtered endpoint. `GET /content?tags=…`. */
1340
+ content: "/content",
1341
+ /**
1342
+ * Resolved form definitions for the deployment. `GET /forms` →
1343
+ * `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
1344
+ * start-conversation); form gating waits for it. A failure is treated as
1345
+ * "no forms" (non-fatal).
1346
+ */
1347
+ forms: "/forms",
1331
1348
  /**
1332
1349
  * Form submission (the event-driven forms engine). POST `{ visitorId,
1333
1350
  * conversationId, formId, trigger, values, skipped? }` → record the form's answers
1334
1351
  * immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
1335
1352
  * Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
1336
1353
  */
1337
- submitForm: "/ai/agent/submit-form"
1354
+ submitForm: "/submit-form",
1355
+ /**
1356
+ * Visitor interaction records for a conversation. `GET
1357
+ * /activity?visitorId=…&conversationId=…` →
1358
+ * `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
1359
+ * the former `formSubmissions` echo on start-conversation / list-messages;
1360
+ * deliberately generic — future visitor interaction records ride the same
1361
+ * endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
1362
+ * deployment doesn't resolve) is treated as "no records" (non-fatal).
1363
+ */
1364
+ activity: "/activity"
1338
1365
  };
1339
1366
  var CONTEXT_PARAM = "context";
1340
1367
  function buildSendMessageRequest(params) {
@@ -1607,14 +1634,17 @@ var AgentTransport = class {
1607
1634
  log4.debug("markRead failed (non-fatal)", { err });
1608
1635
  }
1609
1636
  }
1610
- // ---- CMS content (unified) --------------------------------------------
1637
+ // ---- Data API (content / forms — module-help-ai-data-api) --------------
1611
1638
  //
1612
- // One endpoint for every content surface (Home / Help / News / docs).
1613
- // Callers pass filters; a thrown StreamError (e.g. 404 on an older backend)
1614
- // is treated as "no content" by the module and the tab degrades gracefully.
1615
- /** Fetch content rows by filter. `GET /ai/agent/content`. */
1639
+ // The DATA surfaces live on `dataBaseUrl` (default `${baseUrl}/api/data`)
1640
+ // with ROOT-level paths same auth headers + envelope, same retry
1641
+ // behavior. One endpoint for every content surface (Home / Help / News /
1642
+ // docs); callers pass filters; a thrown StreamError (e.g. 404 on an older
1643
+ // backend) is treated as "no content" by the module and the tab degrades
1644
+ // gracefully.
1645
+ /** Fetch content rows by filter. `GET /content` (data-API). */
1616
1646
  async listContent(query = {}) {
1617
- const url = new URL(this.url(DEFAULT_PATHS.content));
1647
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
1618
1648
  for (const [key, value] of Object.entries(query)) {
1619
1649
  if (value === void 0) continue;
1620
1650
  url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
@@ -1625,6 +1655,40 @@ var AgentTransport = class {
1625
1655
  log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
1626
1656
  return { ...res, items };
1627
1657
  }
1658
+ /**
1659
+ * Fetch the deployment's resolved form definitions. `GET /forms`
1660
+ * (data-API). Replaces the former `config.forms` push on the handshake —
1661
+ * called once at boot, in parallel with `startConversation`; form gating
1662
+ * (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
1663
+ * "no forms" (non-fatal).
1664
+ */
1665
+ async listForms() {
1666
+ log4.debug("listForms \u2192");
1667
+ const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
1668
+ const forms = res.forms ?? [];
1669
+ log4.debug("listForms \u2190", { count: forms.length });
1670
+ return { forms };
1671
+ }
1672
+ /**
1673
+ * Fetch the visitor's recorded activity for a conversation — currently the
1674
+ * form submissions that rebuild the collapsed "form submitted / skipped"
1675
+ * timeline markers on resume. `GET /activity` (data-API); the
1676
+ * envelope carries `visitorId` / `conversationId` as query params.
1677
+ * `conversationId` here is an optional resource selector (a thread that may
1678
+ * differ from the active conversation — the history-pane switch); when
1679
+ * omitted, the envelope's active conversation applies. Callers treat a
1680
+ * thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
1681
+ * "no activity" (non-fatal).
1682
+ */
1683
+ async getActivity(conversationId) {
1684
+ const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
1685
+ if (conversationId) url.searchParams.set("conversationId", conversationId);
1686
+ log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
1687
+ const res = await this.getJson(url.toString(), "getActivity");
1688
+ const formSubmissions = res.formSubmissions ?? [];
1689
+ log4.debug("getActivity \u2190", { count: formSubmissions.length });
1690
+ return { formSubmissions };
1691
+ }
1628
1692
  async saveUserPrefs(userPrefs) {
1629
1693
  log4.debug("saveUserPrefs \u2192", {
1630
1694
  visitorId: this.visitorId,
@@ -1645,8 +1709,9 @@ var AgentTransport = class {
1645
1709
  return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
1646
1710
  }
1647
1711
  /**
1648
- * Record a completed form (intake, CSAT, claim, …). Fire-and-forget — the
1649
- * record is captured even if the visitor never sends a message. `conversationId`,
1712
+ * Record a completed form (intake, CSAT, claim, …). `POST
1713
+ * /submit-form` (data-API). Fire-and-forget the record is
1714
+ * captured even if the visitor never sends a message. `conversationId`,
1650
1715
  * `formId`, and `trigger` are explicit so the backend can tie + route;
1651
1716
  * `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
1652
1717
  * that doesn't implement the endpoint) is non-fatal, so callers don't await.
@@ -1654,7 +1719,7 @@ var AgentTransport = class {
1654
1719
  async submitForm(body) {
1655
1720
  log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
1656
1721
  try {
1657
- await this.postJson(this.submitFormPath, body, "submitForm");
1722
+ await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
1658
1723
  } catch (err) {
1659
1724
  log4.debug("submitForm failed (non-fatal)", { err });
1660
1725
  }
@@ -1890,9 +1955,24 @@ var AgentTransport = class {
1890
1955
  if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
1891
1956
  return await res.json();
1892
1957
  }
1958
+ /**
1959
+ * Agent-API URL — resolves a `/ai/agent/*` path against the agent module
1960
+ * base, `${baseUrl}/api/agent`. Absolute URLs pass through unchanged.
1961
+ */
1893
1962
  url(pathOrUrl) {
1894
1963
  if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1895
- return `${this.opts.baseUrl}${pathOrUrl}`;
1964
+ return `${this.opts.baseUrl}/api/agent${pathOrUrl}`;
1965
+ }
1966
+ /**
1967
+ * Data-API variant of {@link url} — resolves a root-level path (content /
1968
+ * forms / submit-form / activity) against `dataBaseUrl`. Absolute URLs
1969
+ * (e.g. an `endpoints.submitForm` pointing straight at a CRM) pass through
1970
+ * unchanged; with no `dataBaseUrl` configured the data module derives from
1971
+ * the main origin: `${baseUrl}/api/data`.
1972
+ */
1973
+ dataUrl(pathOrUrl) {
1974
+ if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
1975
+ return `${this.opts.dataBaseUrl ?? `${this.opts.baseUrl}/api/data`}${pathOrUrl}`;
1896
1976
  }
1897
1977
  get fetchImpl() {
1898
1978
  return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
@@ -6417,6 +6497,8 @@ function App({ options, hostElement, bus }) {
6417
6497
  const chatTabIdRef = useRef9(void 0);
6418
6498
  chatTabIdRef.current = chatTabId();
6419
6499
  const [conversationReady, setConversationReady] = useState13(false);
6500
+ const [remoteForms, setRemoteForms] = useState13(null);
6501
+ const [formsReady, setFormsReady] = useState13(false);
6420
6502
  const isInlineLike = options.mode === "standalone" || options.mode === "inline";
6421
6503
  const initialPanelRef = useRef9(
6422
6504
  resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
@@ -6449,6 +6531,7 @@ function App({ options, hostElement, bus }) {
6449
6531
  const [transport] = useState13(
6450
6532
  () => new AgentTransport({
6451
6533
  baseUrl: options.baseUrl,
6534
+ dataBaseUrl: options.dataBaseUrl,
6452
6535
  auth: createAuth({
6453
6536
  publicKey: options.publicKey,
6454
6537
  aiAgentDeploymentId: options.aiAgentDeploymentId,
@@ -6529,6 +6612,26 @@ function App({ options, hostElement, bus }) {
6529
6612
  },
6530
6613
  [messagesSig]
6531
6614
  );
6615
+ const fetchFormMarkers = useCallback6(
6616
+ async (conversationId) => {
6617
+ try {
6618
+ const { formSubmissions: subs } = await transport.getActivity(conversationId);
6619
+ const lastIndexByForm = /* @__PURE__ */ new Map();
6620
+ subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6621
+ return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6622
+ key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6623
+ formId: rec.formId,
6624
+ outcome: rec.skipped ? "skipped" : "submitted",
6625
+ createdAt: parseWireDate(rec.createdAt),
6626
+ values: rec.values
6627
+ }));
6628
+ } catch (err) {
6629
+ log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
6630
+ return [];
6631
+ }
6632
+ },
6633
+ [transport]
6634
+ );
6532
6635
  const connectGenRef = useRef9(0);
6533
6636
  const runStartConversation = useCallback6(
6534
6637
  async ({ newConversation }) => {
@@ -6597,24 +6700,14 @@ function App({ options, hostElement, bus }) {
6597
6700
  if (isResume) {
6598
6701
  setLoadingMessages(true);
6599
6702
  try {
6600
- const chat = res.messages?.length ? {
6703
+ const chatPromise = res.messages?.length ? Promise.resolve({
6601
6704
  conversationId: persistedChatId,
6602
6705
  canContinue: res.canContinue ?? true,
6603
- messages: res.messages,
6604
- formSubmissions: res.formSubmissions
6605
- } : await transport.loadConversation(persistedChatId);
6706
+ messages: res.messages
6707
+ }) : transport.loadConversation(persistedChatId);
6708
+ const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
6606
6709
  if (isStale()) return;
6607
6710
  const loaded = (chat.messages ?? []).map(fromWireMessage);
6608
- const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
6609
- const lastIndexByForm = /* @__PURE__ */ new Map();
6610
- subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
6611
- const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
6612
- key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
6613
- formId: rec.formId,
6614
- outcome: rec.skipped ? "skipped" : "submitted",
6615
- createdAt: parseWireDate(rec.createdAt),
6616
- values: rec.values
6617
- }));
6618
6711
  setFormMarkers(markers);
6619
6712
  const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
6620
6713
  (n) => n !== void 0
@@ -6652,7 +6745,18 @@ function App({ options, hostElement, bus }) {
6652
6745
  setConversationReady(true);
6653
6746
  }
6654
6747
  },
6655
- [transport, visitorId, persistence, options, bus, messagesSig, conversationIdSig, feedback, playWelcome]
6748
+ [
6749
+ transport,
6750
+ visitorId,
6751
+ persistence,
6752
+ options,
6753
+ bus,
6754
+ messagesSig,
6755
+ conversationIdSig,
6756
+ feedback,
6757
+ playWelcome,
6758
+ fetchFormMarkers
6759
+ ]
6656
6760
  );
6657
6761
  const userSig = JSON.stringify(options.userContext ?? null);
6658
6762
  const pageSig = JSON.stringify(options.pageContext ?? null);
@@ -6707,6 +6811,16 @@ function App({ options, hostElement, bus }) {
6707
6811
  );
6708
6812
  useEffect17(() => {
6709
6813
  void runStartConversation({ newConversation: false });
6814
+ void (async () => {
6815
+ try {
6816
+ const res = await transport.listForms();
6817
+ setRemoteForms(resolveForms(res.forms));
6818
+ } catch (err) {
6819
+ log16.debug("listForms failed \u2014 treating as no forms", { err });
6820
+ } finally {
6821
+ setFormsReady(true);
6822
+ }
6823
+ })();
6710
6824
  const persistedConversation = conversationIdSig.value;
6711
6825
  let resume = null;
6712
6826
  if (persistedConversation) {
@@ -6843,8 +6957,9 @@ function App({ options, hostElement, bus }) {
6843
6957
  [options.features.humanInLoop, handleToolResult, handleToolDecision]
6844
6958
  );
6845
6959
  const userMessageCount = () => messagesSig.value.reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0);
6960
+ const effectiveForms = options.forms.list.length > 0 ? options.forms : remoteForms ?? options.forms;
6846
6961
  const forms = useForms({
6847
- forms: options.forms,
6962
+ forms: effectiveForms,
6848
6963
  persistence,
6849
6964
  userContext: () => options.userContext,
6850
6965
  messageCount: userMessageCount,
@@ -6877,18 +6992,18 @@ function App({ options, hostElement, bus }) {
6877
6992
  const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
6878
6993
  const msgCount = useComputed7(() => messagesSig.value.length);
6879
6994
  useEffect17(() => {
6880
- if (conversationReady) forms.fire("pre-chat");
6881
- }, [conversationReady, forms]);
6995
+ if (conversationReady && formsReady) forms.fire("pre-chat");
6996
+ }, [conversationReady, formsReady, forms]);
6882
6997
  useEffect17(() => {
6883
- if (!canSend) forms.fire("conversation-closed");
6884
- }, [canSend, forms]);
6998
+ if (formsReady && !canSend) forms.fire("conversation-closed");
6999
+ }, [formsReady, canSend, forms]);
6885
7000
  useEffect17(() => {
6886
- if (conversationReady && pageArea) forms.fire("page-area");
6887
- }, [conversationReady, pageArea, forms]);
7001
+ if (conversationReady && formsReady && pageArea) forms.fire("page-area");
7002
+ }, [conversationReady, formsReady, pageArea, forms]);
6888
7003
  const idleMs = useMemo3(() => {
6889
- const secs = options.forms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
7004
+ const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
6890
7005
  return secs.length ? Math.min(...secs) * 1e3 : 0;
6891
- }, [options.forms]);
7006
+ }, [effectiveForms]);
6892
7007
  useEffect17(() => {
6893
7008
  if (!idleMs || !isOpen) return;
6894
7009
  const id = setTimeout(() => forms.fire("idle"), idleMs);
@@ -7065,9 +7180,13 @@ function App({ options, hostElement, bus }) {
7065
7180
  log16.info("selectConversation", { conversationId: targetConversationId });
7066
7181
  bus.emit("selectConversation", { conversationId: targetConversationId });
7067
7182
  try {
7068
- const res = await transport.loadConversation(targetConversationId);
7183
+ const [res, markers] = await Promise.all([
7184
+ transport.loadConversation(targetConversationId),
7185
+ fetchFormMarkers(targetConversationId)
7186
+ ]);
7069
7187
  const hydrated = res.messages.map(fromWireMessage);
7070
7188
  messagesSig.value = hydrated;
7189
+ setFormMarkers(markers);
7071
7190
  conversationIdSig.value = res.conversationId;
7072
7191
  persistence.saveConversationId(res.conversationId);
7073
7192
  if (res.agent) setAgent(res.agent);
@@ -7081,7 +7200,7 @@ function App({ options, hostElement, bus }) {
7081
7200
  options.onError?.(err);
7082
7201
  }
7083
7202
  },
7084
- [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
7203
+ [transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
7085
7204
  );
7086
7205
  useEffect17(() => {
7087
7206
  const unsub = bindHostCommands(hostElement, {
@@ -7100,10 +7219,10 @@ function App({ options, hostElement, bus }) {
7100
7219
  );
7101
7220
  const enrichedFormMarkers = useMemo3(
7102
7221
  () => formMarkers.map((m) => {
7103
- const def = options.forms.list.find((f) => f.id === m.formId);
7222
+ const def = effectiveForms.list.find((f) => f.id === m.formId);
7104
7223
  return { ...m, title: def?.title, form: def };
7105
7224
  }),
7106
- [formMarkers, options.forms]
7225
+ [formMarkers, effectiveForms]
7107
7226
  );
7108
7227
  const panelProps = {
7109
7228
  options: effectiveOptions,