@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/README.md +1 -1
- package/configurator.mjs +21 -20
- package/elements-web-component.esm.js +27 -27
- package/elements-web-component.esm.js.map +3 -3
- package/elements.cjs.js +27 -27
- package/elements.cjs.js.map +3 -3
- package/elements.esm.js +27 -27
- package/elements.esm.js.map +3 -3
- package/elements.js +27 -27
- package/elements.js.map +3 -3
- package/index.d.ts +50 -38
- package/index.mjs +175 -53
- package/package.json +1 -1
- package/schema.d.ts +54 -50
- package/schema.json +31 -385
- package/schema.mjs +21 -20
- package/web-component.mjs +175 -53
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
|
-
|
|
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: "/
|
|
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
|
|
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
|
-
|
|
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)),
|
|
@@ -665,7 +674,9 @@ function resolveForms(overrides) {
|
|
|
665
674
|
submitLabel: def.submitLabel,
|
|
666
675
|
skippable: def.skippable ?? false,
|
|
667
676
|
frequency: def.frequency ?? "once",
|
|
668
|
-
mirrorToContext: def.mirrorToContext ?? true
|
|
677
|
+
mirrorToContext: def.mirrorToContext ?? true,
|
|
678
|
+
reopenable: def.reopenable ?? true,
|
|
679
|
+
reviewable: def.reviewable ?? true
|
|
669
680
|
});
|
|
670
681
|
}
|
|
671
682
|
const byTrigger = {};
|
|
@@ -758,7 +769,7 @@ function mergeServerConfig(user, server) {
|
|
|
758
769
|
out[key] = mergeLeaves(sv, uv);
|
|
759
770
|
}
|
|
760
771
|
if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
|
|
761
|
-
if (server.
|
|
772
|
+
if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
|
|
762
773
|
const userI18n = user.i18n;
|
|
763
774
|
const serverI18n = server.i18n;
|
|
764
775
|
if (userI18n || serverI18n) {
|
|
@@ -818,6 +829,7 @@ var EMBED_SCALAR_ATTRS = [
|
|
|
818
829
|
["public-key", "publicKey"],
|
|
819
830
|
["ai-agent-deployment-id", "aiAgentDeploymentId"],
|
|
820
831
|
["base-url", "baseUrl"],
|
|
832
|
+
["data-base-url", "dataBaseUrl"],
|
|
821
833
|
["theme", "theme", (v) => v]
|
|
822
834
|
];
|
|
823
835
|
var PRESENTATION_ATTRS = [
|
|
@@ -1323,16 +1335,33 @@ var DEFAULT_PATHS = {
|
|
|
1323
1335
|
* on the client; a failure just leaves the badge until the next sync.
|
|
1324
1336
|
*/
|
|
1325
1337
|
markRead: "/ai/agent/mark-read",
|
|
1326
|
-
// ──
|
|
1327
|
-
/** All widget content via one filtered endpoint. `GET /
|
|
1328
|
-
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",
|
|
1329
1348
|
/**
|
|
1330
1349
|
* Form submission (the event-driven forms engine). POST `{ visitorId,
|
|
1331
1350
|
* conversationId, formId, trigger, values, skipped? }` → record the form's answers
|
|
1332
1351
|
* immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
|
|
1333
1352
|
* Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
|
|
1334
1353
|
*/
|
|
1335
|
-
submitForm: "/
|
|
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"
|
|
1336
1365
|
};
|
|
1337
1366
|
var CONTEXT_PARAM = "context";
|
|
1338
1367
|
function buildSendMessageRequest(params) {
|
|
@@ -1605,14 +1634,17 @@ var AgentTransport = class {
|
|
|
1605
1634
|
log4.debug("markRead failed (non-fatal)", { err });
|
|
1606
1635
|
}
|
|
1607
1636
|
}
|
|
1608
|
-
// ----
|
|
1637
|
+
// ---- Data API (content / forms — module-help-ai-data-api) --------------
|
|
1609
1638
|
//
|
|
1610
|
-
//
|
|
1611
|
-
//
|
|
1612
|
-
//
|
|
1613
|
-
|
|
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). */
|
|
1614
1646
|
async listContent(query = {}) {
|
|
1615
|
-
const url = new URL(this.
|
|
1647
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
|
|
1616
1648
|
for (const [key, value] of Object.entries(query)) {
|
|
1617
1649
|
if (value === void 0) continue;
|
|
1618
1650
|
url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
|
|
@@ -1623,6 +1655,40 @@ var AgentTransport = class {
|
|
|
1623
1655
|
log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
|
|
1624
1656
|
return { ...res, items };
|
|
1625
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
|
+
}
|
|
1626
1692
|
async saveUserPrefs(userPrefs) {
|
|
1627
1693
|
log4.debug("saveUserPrefs \u2192", {
|
|
1628
1694
|
visitorId: this.visitorId,
|
|
@@ -1643,8 +1709,9 @@ var AgentTransport = class {
|
|
|
1643
1709
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
1644
1710
|
}
|
|
1645
1711
|
/**
|
|
1646
|
-
* Record a completed form (intake, CSAT, claim, …).
|
|
1647
|
-
*
|
|
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`,
|
|
1648
1715
|
* `formId`, and `trigger` are explicit so the backend can tie + route;
|
|
1649
1716
|
* `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
|
|
1650
1717
|
* that doesn't implement the endpoint) is non-fatal, so callers don't await.
|
|
@@ -1652,7 +1719,7 @@ var AgentTransport = class {
|
|
|
1652
1719
|
async submitForm(body) {
|
|
1653
1720
|
log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
|
|
1654
1721
|
try {
|
|
1655
|
-
await this.postJson(this.submitFormPath, body, "submitForm");
|
|
1722
|
+
await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
|
|
1656
1723
|
} catch (err) {
|
|
1657
1724
|
log4.debug("submitForm failed (non-fatal)", { err });
|
|
1658
1725
|
}
|
|
@@ -1888,9 +1955,24 @@ var AgentTransport = class {
|
|
|
1888
1955
|
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
|
|
1889
1956
|
return await res.json();
|
|
1890
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
|
+
*/
|
|
1891
1962
|
url(pathOrUrl) {
|
|
1892
1963
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1893
|
-
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}`;
|
|
1894
1976
|
}
|
|
1895
1977
|
get fetchImpl() {
|
|
1896
1978
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -4357,7 +4439,8 @@ function FormDoneMarker({
|
|
|
4357
4439
|
"\xB7 ",
|
|
4358
4440
|
marker.title
|
|
4359
4441
|
] }) : null;
|
|
4360
|
-
const
|
|
4442
|
+
const reviewable = marker.form?.reviewable ?? false;
|
|
4443
|
+
const values = !skipped && reviewable && marker.values && Object.keys(marker.values).length > 0 ? marker.values : null;
|
|
4361
4444
|
if (values) {
|
|
4362
4445
|
const labelFor = (name) => marker.form?.fields.find((f) => f.name === name)?.label ?? name;
|
|
4363
4446
|
return /* @__PURE__ */ jsxs9("details", { class: `${p11}-form-done`, "data-outcome": marker.outcome, "data-testid": TID.formDone, children: [
|
|
@@ -4377,7 +4460,7 @@ function FormDoneMarker({
|
|
|
4377
4460
|
skipped ? /* @__PURE__ */ jsx11(SkipIcon, {}) : /* @__PURE__ */ jsx11(CheckIcon, {}),
|
|
4378
4461
|
/* @__PURE__ */ jsx11("span", { children: skipped ? strings.formSkipped : strings.formSubmitted }),
|
|
4379
4462
|
title,
|
|
4380
|
-
skipped && marker.form && onFill ? /* @__PURE__ */ jsx11("button", { type: "button", class: `${p11}-form-done-fill`, "data-testid": TID.formFill, onClick: onFill, children: strings.formFillOut }) : null
|
|
4463
|
+
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
|
|
4381
4464
|
] });
|
|
4382
4465
|
}
|
|
4383
4466
|
|
|
@@ -6414,6 +6497,8 @@ function App({ options, hostElement, bus }) {
|
|
|
6414
6497
|
const chatTabIdRef = useRef9(void 0);
|
|
6415
6498
|
chatTabIdRef.current = chatTabId();
|
|
6416
6499
|
const [conversationReady, setConversationReady] = useState13(false);
|
|
6500
|
+
const [remoteForms, setRemoteForms] = useState13(null);
|
|
6501
|
+
const [formsReady, setFormsReady] = useState13(false);
|
|
6417
6502
|
const isInlineLike = options.mode === "standalone" || options.mode === "inline";
|
|
6418
6503
|
const initialPanelRef = useRef9(
|
|
6419
6504
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
@@ -6446,6 +6531,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6446
6531
|
const [transport] = useState13(
|
|
6447
6532
|
() => new AgentTransport({
|
|
6448
6533
|
baseUrl: options.baseUrl,
|
|
6534
|
+
dataBaseUrl: options.dataBaseUrl,
|
|
6449
6535
|
auth: createAuth({
|
|
6450
6536
|
publicKey: options.publicKey,
|
|
6451
6537
|
aiAgentDeploymentId: options.aiAgentDeploymentId,
|
|
@@ -6526,6 +6612,26 @@ function App({ options, hostElement, bus }) {
|
|
|
6526
6612
|
},
|
|
6527
6613
|
[messagesSig]
|
|
6528
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
|
+
);
|
|
6529
6635
|
const connectGenRef = useRef9(0);
|
|
6530
6636
|
const runStartConversation = useCallback6(
|
|
6531
6637
|
async ({ newConversation }) => {
|
|
@@ -6594,24 +6700,14 @@ function App({ options, hostElement, bus }) {
|
|
|
6594
6700
|
if (isResume) {
|
|
6595
6701
|
setLoadingMessages(true);
|
|
6596
6702
|
try {
|
|
6597
|
-
const
|
|
6703
|
+
const chatPromise = res.messages?.length ? Promise.resolve({
|
|
6598
6704
|
conversationId: persistedChatId,
|
|
6599
6705
|
canContinue: res.canContinue ?? true,
|
|
6600
|
-
messages: res.messages
|
|
6601
|
-
|
|
6602
|
-
|
|
6706
|
+
messages: res.messages
|
|
6707
|
+
}) : transport.loadConversation(persistedChatId);
|
|
6708
|
+
const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
|
|
6603
6709
|
if (isStale()) return;
|
|
6604
6710
|
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6605
|
-
const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
|
|
6606
|
-
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6607
|
-
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6608
|
-
const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
6609
|
-
key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
|
|
6610
|
-
formId: rec.formId,
|
|
6611
|
-
outcome: rec.skipped ? "skipped" : "submitted",
|
|
6612
|
-
createdAt: parseWireDate(rec.createdAt),
|
|
6613
|
-
values: rec.values
|
|
6614
|
-
}));
|
|
6615
6711
|
setFormMarkers(markers);
|
|
6616
6712
|
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6617
6713
|
(n) => n !== void 0
|
|
@@ -6649,7 +6745,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6649
6745
|
setConversationReady(true);
|
|
6650
6746
|
}
|
|
6651
6747
|
},
|
|
6652
|
-
[
|
|
6748
|
+
[
|
|
6749
|
+
transport,
|
|
6750
|
+
visitorId,
|
|
6751
|
+
persistence,
|
|
6752
|
+
options,
|
|
6753
|
+
bus,
|
|
6754
|
+
messagesSig,
|
|
6755
|
+
conversationIdSig,
|
|
6756
|
+
feedback,
|
|
6757
|
+
playWelcome,
|
|
6758
|
+
fetchFormMarkers
|
|
6759
|
+
]
|
|
6653
6760
|
);
|
|
6654
6761
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6655
6762
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6704,6 +6811,16 @@ function App({ options, hostElement, bus }) {
|
|
|
6704
6811
|
);
|
|
6705
6812
|
useEffect17(() => {
|
|
6706
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
|
+
})();
|
|
6707
6824
|
const persistedConversation = conversationIdSig.value;
|
|
6708
6825
|
let resume = null;
|
|
6709
6826
|
if (persistedConversation) {
|
|
@@ -6840,8 +6957,9 @@ function App({ options, hostElement, bus }) {
|
|
|
6840
6957
|
[options.features.humanInLoop, handleToolResult, handleToolDecision]
|
|
6841
6958
|
);
|
|
6842
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;
|
|
6843
6961
|
const forms = useForms({
|
|
6844
|
-
forms:
|
|
6962
|
+
forms: effectiveForms,
|
|
6845
6963
|
persistence,
|
|
6846
6964
|
userContext: () => options.userContext,
|
|
6847
6965
|
messageCount: userMessageCount,
|
|
@@ -6874,18 +6992,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6874
6992
|
const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
|
|
6875
6993
|
const msgCount = useComputed7(() => messagesSig.value.length);
|
|
6876
6994
|
useEffect17(() => {
|
|
6877
|
-
if (conversationReady) forms.fire("pre-chat");
|
|
6878
|
-
}, [conversationReady, forms]);
|
|
6995
|
+
if (conversationReady && formsReady) forms.fire("pre-chat");
|
|
6996
|
+
}, [conversationReady, formsReady, forms]);
|
|
6879
6997
|
useEffect17(() => {
|
|
6880
|
-
if (!canSend) forms.fire("conversation-closed");
|
|
6881
|
-
}, [canSend, forms]);
|
|
6998
|
+
if (formsReady && !canSend) forms.fire("conversation-closed");
|
|
6999
|
+
}, [formsReady, canSend, forms]);
|
|
6882
7000
|
useEffect17(() => {
|
|
6883
|
-
if (conversationReady && pageArea) forms.fire("page-area");
|
|
6884
|
-
}, [conversationReady, pageArea, forms]);
|
|
7001
|
+
if (conversationReady && formsReady && pageArea) forms.fire("page-area");
|
|
7002
|
+
}, [conversationReady, formsReady, pageArea, forms]);
|
|
6885
7003
|
const idleMs = useMemo3(() => {
|
|
6886
|
-
const secs =
|
|
7004
|
+
const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
|
|
6887
7005
|
return secs.length ? Math.min(...secs) * 1e3 : 0;
|
|
6888
|
-
}, [
|
|
7006
|
+
}, [effectiveForms]);
|
|
6889
7007
|
useEffect17(() => {
|
|
6890
7008
|
if (!idleMs || !isOpen) return;
|
|
6891
7009
|
const id = setTimeout(() => forms.fire("idle"), idleMs);
|
|
@@ -7062,9 +7180,13 @@ function App({ options, hostElement, bus }) {
|
|
|
7062
7180
|
log16.info("selectConversation", { conversationId: targetConversationId });
|
|
7063
7181
|
bus.emit("selectConversation", { conversationId: targetConversationId });
|
|
7064
7182
|
try {
|
|
7065
|
-
const res = await
|
|
7183
|
+
const [res, markers] = await Promise.all([
|
|
7184
|
+
transport.loadConversation(targetConversationId),
|
|
7185
|
+
fetchFormMarkers(targetConversationId)
|
|
7186
|
+
]);
|
|
7066
7187
|
const hydrated = res.messages.map(fromWireMessage);
|
|
7067
7188
|
messagesSig.value = hydrated;
|
|
7189
|
+
setFormMarkers(markers);
|
|
7068
7190
|
conversationIdSig.value = res.conversationId;
|
|
7069
7191
|
persistence.saveConversationId(res.conversationId);
|
|
7070
7192
|
if (res.agent) setAgent(res.agent);
|
|
@@ -7078,7 +7200,7 @@ function App({ options, hostElement, bus }) {
|
|
|
7078
7200
|
options.onError?.(err);
|
|
7079
7201
|
}
|
|
7080
7202
|
},
|
|
7081
|
-
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
|
|
7203
|
+
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
|
|
7082
7204
|
);
|
|
7083
7205
|
useEffect17(() => {
|
|
7084
7206
|
const unsub = bindHostCommands(hostElement, {
|
|
@@ -7097,10 +7219,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7097
7219
|
);
|
|
7098
7220
|
const enrichedFormMarkers = useMemo3(
|
|
7099
7221
|
() => formMarkers.map((m) => {
|
|
7100
|
-
const def =
|
|
7222
|
+
const def = effectiveForms.list.find((f) => f.id === m.formId);
|
|
7101
7223
|
return { ...m, title: def?.title, form: def };
|
|
7102
7224
|
}),
|
|
7103
|
-
[formMarkers,
|
|
7225
|
+
[formMarkers, effectiveForms]
|
|
7104
7226
|
);
|
|
7105
7227
|
const panelProps = {
|
|
7106
7228
|
options: effectiveOptions,
|