@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/README.md +1 -1
- package/configurator.mjs +15 -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 +33 -39
- package/index.mjs +169 -50
- package/package.json +1 -1
- package/schema.d.ts +2 -2
- package/schema.json +18 -396
- package/schema.mjs +15 -20
- package/web-component.mjs +169 -50
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)),
|
|
@@ -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.
|
|
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
|
-
// ──
|
|
1329
|
-
/** All widget content via one filtered endpoint. `GET /
|
|
1330
|
-
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: "/
|
|
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
|
-
// ----
|
|
1637
|
+
// ---- Data API (content / forms — module-help-ai-data-api) --------------
|
|
1611
1638
|
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
|
|
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.
|
|
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, …).
|
|
1649
|
-
*
|
|
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
|
|
6703
|
+
const chatPromise = res.messages?.length ? Promise.resolve({
|
|
6601
6704
|
conversationId: persistedChatId,
|
|
6602
6705
|
canContinue: res.canContinue ?? true,
|
|
6603
|
-
messages: res.messages
|
|
6604
|
-
|
|
6605
|
-
|
|
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
|
-
[
|
|
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:
|
|
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 =
|
|
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
|
-
}, [
|
|
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
|
|
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 =
|
|
7222
|
+
const def = effectiveForms.list.find((f) => f.id === m.formId);
|
|
7104
7223
|
return { ...m, title: def?.title, form: def };
|
|
7105
7224
|
}),
|
|
7106
|
-
[formMarkers,
|
|
7225
|
+
[formMarkers, effectiveForms]
|
|
7107
7226
|
);
|
|
7108
7227
|
const panelProps = {
|
|
7109
7228
|
options: effectiveOptions,
|