@helpai/elements 0.25.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/README.md +2 -2
- package/configurator.mjs +18 -17
- package/elements-web-component.esm.js +19 -19
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +22 -22
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +22 -22
- package/elements.esm.js.map +4 -4
- package/elements.js +19 -19
- package/elements.js.map +4 -4
- package/index.d.ts +42 -87
- package/index.mjs +190 -181
- package/package.json +1 -1
- package/schema.d.ts +50 -50
- package/schema.json +24 -27
- package/schema.mjs +20 -19
- package/web-component.d.ts +1 -1
- package/web-component.mjs +190 -181
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:
|
|
464
|
+
enabled: true,
|
|
465
465
|
sampleRate: 1
|
|
466
466
|
};
|
|
467
467
|
var DEFAULT_ENDPOINTS = {
|
|
468
|
-
upload: "/
|
|
469
|
-
transcribe: "/
|
|
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",
|
|
@@ -510,12 +510,14 @@ function resolveOptions(rawOpts) {
|
|
|
510
510
|
const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
|
|
511
511
|
const availableLocales = i18n.availableLocales ?? [locale];
|
|
512
512
|
const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
|
|
513
|
+
const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
|
|
513
514
|
const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
|
|
514
515
|
return {
|
|
515
516
|
widgetId: opts.widgetId ?? "default",
|
|
516
517
|
publicKey: opts.publicKey,
|
|
517
518
|
aiAgentDeploymentId: opts.aiAgentDeploymentId,
|
|
518
519
|
baseUrl,
|
|
520
|
+
agentBaseUrl,
|
|
519
521
|
dataBaseUrl,
|
|
520
522
|
userContext: sanitizeUserContext(opts.userContext),
|
|
521
523
|
pageContext: sanitizePageContext(opts.pageContext),
|
|
@@ -598,9 +600,9 @@ function resolveTracking(overrides, dataBaseUrl) {
|
|
|
598
600
|
const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
|
|
599
601
|
return {
|
|
600
602
|
enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
|
|
601
|
-
// The collector rides the data module — `${dataBaseUrl}/elements
|
|
602
|
-
// (e.g. `https://help.ai/api/data/elements
|
|
603
|
-
endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements
|
|
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`,
|
|
604
606
|
// Clamp instead of reject — a malformed rate must never turn a
|
|
605
607
|
// disabled tracker on or crash resolve (no Zod on the IIFE path).
|
|
606
608
|
sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
|
|
@@ -829,6 +831,7 @@ var EMBED_SCALAR_ATTRS = [
|
|
|
829
831
|
["public-key", "publicKey"],
|
|
830
832
|
["ai-agent-deployment-id", "aiAgentDeploymentId"],
|
|
831
833
|
["base-url", "baseUrl"],
|
|
834
|
+
["agent-base-url", "agentBaseUrl"],
|
|
832
835
|
["data-base-url", "dataBaseUrl"],
|
|
833
836
|
["theme", "theme", (v) => v]
|
|
834
837
|
];
|
|
@@ -977,7 +980,7 @@ function parseLauncher(get, str2) {
|
|
|
977
980
|
return Object.keys(base).length > 0 ? base : null;
|
|
978
981
|
}
|
|
979
982
|
|
|
980
|
-
// src/core/
|
|
983
|
+
// src/core/handshake-shape.ts
|
|
981
984
|
function isPlainObject(raw) {
|
|
982
985
|
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
983
986
|
}
|
|
@@ -1305,63 +1308,61 @@ function parseWireDate(value) {
|
|
|
1305
1308
|
}
|
|
1306
1309
|
var DEFAULT_PATHS = {
|
|
1307
1310
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1308
|
-
|
|
1311
|
+
handshake: "/pai/handshake",
|
|
1309
1312
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1310
|
-
streamMessage: "/
|
|
1313
|
+
streamMessage: "/pai/stream-message",
|
|
1311
1314
|
/**
|
|
1312
1315
|
* Resume an interrupted reply. `GET` (envelope rides as query params).
|
|
1313
1316
|
* Stateless: the server re-streams the whole reply SSE again from the start —
|
|
1314
1317
|
* there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
|
|
1315
1318
|
* replayed events are skipped and only the continuation surfaces.
|
|
1316
1319
|
*/
|
|
1317
|
-
streamResume: "/
|
|
1320
|
+
streamResume: "/pai/stream-resume",
|
|
1318
1321
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1319
|
-
cancelStream: "/
|
|
1322
|
+
cancelStream: "/pai/cancel-stream",
|
|
1320
1323
|
/** List the visitor's conversations. `GET`. */
|
|
1321
|
-
listConversations: "/
|
|
1322
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1323
|
-
listMessages: "/
|
|
1324
|
+
listConversations: "/pai/list-conversations",
|
|
1325
|
+
/** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
|
|
1326
|
+
listMessages: "/pai/list-messages",
|
|
1324
1327
|
/**
|
|
1325
1328
|
* Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
|
|
1326
1329
|
* server persists, returns the merged authoritative copy. Fire-and-forget on
|
|
1327
1330
|
* the client — a failure leaves the local cache valid; the next
|
|
1328
|
-
* `
|
|
1331
|
+
* `handshake()` reconciles.
|
|
1329
1332
|
*/
|
|
1330
|
-
updateSettings: "/
|
|
1333
|
+
updateSettings: "/pai/update-settings",
|
|
1331
1334
|
/**
|
|
1332
1335
|
* Mark a conversation read. POST `{ visitorId, conversationId }` → the server
|
|
1333
1336
|
* records `lastReadAt = now` for that (visitor, conversation) and recomputes
|
|
1334
1337
|
* `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
|
|
1335
1338
|
* on the client; a failure just leaves the badge until the next sync.
|
|
1336
1339
|
*/
|
|
1337
|
-
markRead: "/
|
|
1340
|
+
markRead: "/pai/mark-read",
|
|
1338
1341
|
// ── 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
|
/**
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
1345
|
-
*
|
|
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=…`.
|
|
1346
1357
|
*/
|
|
1347
|
-
|
|
1358
|
+
content: "/pai/content",
|
|
1348
1359
|
/**
|
|
1349
1360
|
* Form submission (the event-driven forms engine). POST `{ visitorId,
|
|
1350
1361
|
* conversationId, formId, trigger, values, skipped? }` → record the form's answers
|
|
1351
1362
|
* immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
|
|
1352
1363
|
* Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
|
|
1353
1364
|
*/
|
|
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"
|
|
1365
|
+
submitForm: "/pai/submit-form"
|
|
1365
1366
|
};
|
|
1366
1367
|
var CONTEXT_PARAM = "context";
|
|
1367
1368
|
function buildSendMessageRequest(params) {
|
|
@@ -1462,7 +1463,7 @@ function sleep(ms, signal7) {
|
|
|
1462
1463
|
var AgentTransport = class {
|
|
1463
1464
|
constructor(opts) {
|
|
1464
1465
|
__publicField(this, "opts", opts);
|
|
1465
|
-
// Identity adopted from the
|
|
1466
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1466
1467
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1467
1468
|
// (visitor, conversation) pair.
|
|
1468
1469
|
__publicField(this, "visitorId");
|
|
@@ -1485,9 +1486,9 @@ var AgentTransport = class {
|
|
|
1485
1486
|
}
|
|
1486
1487
|
/**
|
|
1487
1488
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1488
|
-
* `
|
|
1489
|
-
* with `
|
|
1490
|
-
* `conversationId` and the resume GET couldn't identify the thread. `
|
|
1489
|
+
* `handshake` resolves. A mount-time `resumeStream()` runs in parallel
|
|
1490
|
+
* with `handshake`, so without this its envelope would carry no
|
|
1491
|
+
* `conversationId` and the resume GET couldn't identify the thread. `handshake`
|
|
1491
1492
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1492
1493
|
*/
|
|
1493
1494
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1504,7 +1505,7 @@ var AgentTransport = class {
|
|
|
1504
1505
|
return out;
|
|
1505
1506
|
}
|
|
1506
1507
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1507
|
-
*
|
|
1508
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1508
1509
|
* differs from the active one. */
|
|
1509
1510
|
withEnvelope(body) {
|
|
1510
1511
|
return { ...this.envelope(), ...body };
|
|
@@ -1525,28 +1526,22 @@ var AgentTransport = class {
|
|
|
1525
1526
|
return url.toString();
|
|
1526
1527
|
}
|
|
1527
1528
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1528
|
-
async
|
|
1529
|
-
log4.debug("
|
|
1529
|
+
async handshake(body) {
|
|
1530
|
+
log4.debug("handshake \u2192", {
|
|
1530
1531
|
visitorId: body.visitorId,
|
|
1531
1532
|
conversationId: body.conversationId,
|
|
1532
1533
|
locale: body.locale
|
|
1533
1534
|
});
|
|
1534
|
-
const res = await this.postJson(
|
|
1535
|
-
|
|
1536
|
-
body,
|
|
1537
|
-
"startConversation"
|
|
1538
|
-
);
|
|
1539
|
-
log4.debug("startConversation \u2190", {
|
|
1535
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1536
|
+
log4.debug("handshake \u2190", {
|
|
1540
1537
|
visitorId: res.visitorId,
|
|
1541
1538
|
conversationId: res.conversationId,
|
|
1542
|
-
canContinue: res.canContinue,
|
|
1543
|
-
hasMessages: !!res.messages?.length,
|
|
1544
1539
|
hasWelcome: !!res.welcome,
|
|
1545
1540
|
hasConfig: !!res.config,
|
|
1546
1541
|
rebind: res.rebind
|
|
1547
1542
|
});
|
|
1548
|
-
if (!
|
|
1549
|
-
log4.warn("
|
|
1543
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1544
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1550
1545
|
}
|
|
1551
1546
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1552
1547
|
this.conversationId = res.conversationId;
|
|
@@ -1656,38 +1651,26 @@ var AgentTransport = class {
|
|
|
1656
1651
|
return { ...res, items };
|
|
1657
1652
|
}
|
|
1658
1653
|
/**
|
|
1659
|
-
*
|
|
1660
|
-
*
|
|
1661
|
-
*
|
|
1662
|
-
* (
|
|
1663
|
-
*
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
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).
|
|
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).
|
|
1682
1664
|
*/
|
|
1683
|
-
async
|
|
1684
|
-
const url = new URL(this.dataUrl(DEFAULT_PATHS.
|
|
1665
|
+
async bootstrap(conversationId) {
|
|
1666
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
|
|
1685
1667
|
if (conversationId) url.searchParams.set("conversationId", conversationId);
|
|
1686
|
-
log4.debug("
|
|
1687
|
-
const res = await this.getJson(url.toString(), "
|
|
1668
|
+
log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
|
|
1669
|
+
const res = await this.getJson(url.toString(), "bootstrap");
|
|
1670
|
+
const forms = res.forms ?? [];
|
|
1688
1671
|
const formSubmissions = res.formSubmissions ?? [];
|
|
1689
|
-
log4.debug("
|
|
1690
|
-
return { formSubmissions };
|
|
1672
|
+
log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
|
|
1673
|
+
return { forms, formSubmissions };
|
|
1691
1674
|
}
|
|
1692
1675
|
async saveUserPrefs(userPrefs) {
|
|
1693
1676
|
log4.debug("saveUserPrefs \u2192", {
|
|
@@ -1700,10 +1683,10 @@ var AgentTransport = class {
|
|
|
1700
1683
|
return res;
|
|
1701
1684
|
}
|
|
1702
1685
|
get uploadPath() {
|
|
1703
|
-
return this.opts.endpoints?.upload ?? "/
|
|
1686
|
+
return this.opts.endpoints?.upload ?? "/pai/upload-file";
|
|
1704
1687
|
}
|
|
1705
1688
|
get transcribePath() {
|
|
1706
|
-
return this.opts.endpoints?.transcribe ?? "/
|
|
1689
|
+
return this.opts.endpoints?.transcribe ?? "/pai/transcribe-audio";
|
|
1707
1690
|
}
|
|
1708
1691
|
get submitFormPath() {
|
|
1709
1692
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
@@ -1743,9 +1726,9 @@ var AgentTransport = class {
|
|
|
1743
1726
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1744
1727
|
*
|
|
1745
1728
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1746
|
-
* `
|
|
1729
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1747
1730
|
* the user refreshed while the assistant was still streaming, the
|
|
1748
|
-
* `
|
|
1731
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1749
1732
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1750
1733
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1751
1734
|
* (the common case — no bubble is created).
|
|
@@ -1772,7 +1755,7 @@ var AgentTransport = class {
|
|
|
1772
1755
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1773
1756
|
* off first, the reply may still be spinning up server-side);
|
|
1774
1757
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1775
|
-
* parallel with
|
|
1758
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1776
1759
|
* user reloaded keeps flowing).
|
|
1777
1760
|
*
|
|
1778
1761
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1781,7 +1764,7 @@ var AgentTransport = class {
|
|
|
1781
1764
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1782
1765
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1783
1766
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1784
|
-
* turn comes from the DB via
|
|
1767
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1785
1768
|
*
|
|
1786
1769
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1787
1770
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1846,7 +1829,7 @@ var AgentTransport = class {
|
|
|
1846
1829
|
}
|
|
1847
1830
|
return false;
|
|
1848
1831
|
}
|
|
1849
|
-
/** Abort + fire-and-forget POST to `/
|
|
1832
|
+
/** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
|
|
1850
1833
|
cancelStream(ctrl, conversationId) {
|
|
1851
1834
|
if (ctrl.signal.aborted) return;
|
|
1852
1835
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1887,7 +1870,7 @@ var AgentTransport = class {
|
|
|
1887
1870
|
return url.toString();
|
|
1888
1871
|
}
|
|
1889
1872
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1890
|
-
// update-settings / mark-read /
|
|
1873
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1891
1874
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1892
1875
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1893
1876
|
async postJson(path, body, label) {
|
|
@@ -1956,23 +1939,22 @@ var AgentTransport = class {
|
|
|
1956
1939
|
return await res.json();
|
|
1957
1940
|
}
|
|
1958
1941
|
/**
|
|
1959
|
-
* Agent-API URL — resolves a `/
|
|
1960
|
-
* base
|
|
1942
|
+
* Agent-API URL — resolves a `/pai/*` path against the resolved
|
|
1943
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1961
1944
|
*/
|
|
1962
1945
|
url(pathOrUrl) {
|
|
1963
1946
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1964
|
-
return `${this.opts.
|
|
1947
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1965
1948
|
}
|
|
1966
1949
|
/**
|
|
1967
1950
|
* Data-API variant of {@link url} — resolves a root-level path (content /
|
|
1968
|
-
* forms / submit-form / activity) against
|
|
1969
|
-
* (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1970
|
-
*
|
|
1971
|
-
* the main origin: `${baseUrl}/api/data`.
|
|
1951
|
+
* forms / submit-form / activity) against the resolved data module base.
|
|
1952
|
+
* Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1953
|
+
* CRM) pass through unchanged.
|
|
1972
1954
|
*/
|
|
1973
1955
|
dataUrl(pathOrUrl) {
|
|
1974
1956
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1975
|
-
return `${this.opts.dataBaseUrl
|
|
1957
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1976
1958
|
}
|
|
1977
1959
|
get fetchImpl() {
|
|
1978
1960
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1986,10 +1968,10 @@ function extensionFor(mimeType) {
|
|
|
1986
1968
|
if (mimeType.includes("wav")) return "wav";
|
|
1987
1969
|
return "bin";
|
|
1988
1970
|
}
|
|
1989
|
-
function
|
|
1971
|
+
function isHandshakeResponseShape(raw) {
|
|
1990
1972
|
if (!raw || typeof raw !== "object") return false;
|
|
1991
1973
|
const r = raw;
|
|
1992
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1974
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1993
1975
|
}
|
|
1994
1976
|
|
|
1995
1977
|
// src/stream/messages.ts
|
|
@@ -6504,6 +6486,14 @@ function App({ options, hostElement, bus }) {
|
|
|
6504
6486
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6505
6487
|
);
|
|
6506
6488
|
const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
|
|
6489
|
+
const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
|
|
6490
|
+
const activatedRef = useRef9(activated);
|
|
6491
|
+
activatedRef.current = activated;
|
|
6492
|
+
const pendingThreadRef = useRef9(false);
|
|
6493
|
+
const welcomeRef = useRef9(void 0);
|
|
6494
|
+
useEffect17(() => {
|
|
6495
|
+
if (isOpen) setActivated(true);
|
|
6496
|
+
}, [isOpen]);
|
|
6507
6497
|
const initialPanelApplied = useRef9(false);
|
|
6508
6498
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6509
6499
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6530,7 +6520,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6530
6520
|
const [formContext, setFormContext] = useState13({});
|
|
6531
6521
|
const [transport] = useState13(
|
|
6532
6522
|
() => new AgentTransport({
|
|
6533
|
-
|
|
6523
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6534
6524
|
dataBaseUrl: options.dataBaseUrl,
|
|
6535
6525
|
auth: createAuth({
|
|
6536
6526
|
publicKey: options.publicKey,
|
|
@@ -6612,10 +6602,11 @@ function App({ options, hostElement, bus }) {
|
|
|
6612
6602
|
},
|
|
6613
6603
|
[messagesSig]
|
|
6614
6604
|
);
|
|
6605
|
+
const dataBootRef = useRef9(null);
|
|
6615
6606
|
const fetchFormMarkers = useCallback6(
|
|
6616
6607
|
async (conversationId) => {
|
|
6617
6608
|
try {
|
|
6618
|
-
const { formSubmissions: subs } = await transport.
|
|
6609
|
+
const { formSubmissions: subs } = await (conversationId ? transport.bootstrap(conversationId) : dataBootRef.current ?? transport.bootstrap());
|
|
6619
6610
|
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6620
6611
|
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6621
6612
|
return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
@@ -6626,14 +6617,53 @@ function App({ options, hostElement, bus }) {
|
|
|
6626
6617
|
values: rec.values
|
|
6627
6618
|
}));
|
|
6628
6619
|
} catch (err) {
|
|
6629
|
-
log16.debug("
|
|
6620
|
+
log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
|
|
6630
6621
|
return [];
|
|
6631
6622
|
}
|
|
6632
6623
|
},
|
|
6633
6624
|
[transport]
|
|
6634
6625
|
);
|
|
6635
6626
|
const connectGenRef = useRef9(0);
|
|
6636
|
-
const
|
|
6627
|
+
const loadThread = useCallback6(
|
|
6628
|
+
async (conversationId) => {
|
|
6629
|
+
const myGen = connectGenRef.current;
|
|
6630
|
+
const isStale = () => myGen !== connectGenRef.current;
|
|
6631
|
+
setLoadingMessages(true);
|
|
6632
|
+
try {
|
|
6633
|
+
const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
|
|
6634
|
+
if (isStale()) return;
|
|
6635
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6636
|
+
setFormMarkers(markers);
|
|
6637
|
+
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6638
|
+
(n) => n !== void 0
|
|
6639
|
+
);
|
|
6640
|
+
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6641
|
+
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6642
|
+
const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6643
|
+
const row = makeInstantWelcomeMessage(w);
|
|
6644
|
+
row.createdAt = welcomeAnchor;
|
|
6645
|
+
return row;
|
|
6646
|
+
});
|
|
6647
|
+
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6648
|
+
if (loaded.length || tail.length) {
|
|
6649
|
+
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6650
|
+
setCanSend(chat.canContinue ?? true);
|
|
6651
|
+
setSuggestions(chat.suggestions ?? []);
|
|
6652
|
+
} else {
|
|
6653
|
+
messagesSig.value = welcomeRows;
|
|
6654
|
+
}
|
|
6655
|
+
} catch (err) {
|
|
6656
|
+
if (isStale()) return;
|
|
6657
|
+
log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
|
|
6658
|
+
conversationIdSig.value = void 0;
|
|
6659
|
+
persistence.saveConversationId(void 0);
|
|
6660
|
+
} finally {
|
|
6661
|
+
if (!isStale()) setLoadingMessages(false);
|
|
6662
|
+
}
|
|
6663
|
+
},
|
|
6664
|
+
[transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
|
|
6665
|
+
);
|
|
6666
|
+
const runHandshake = useCallback6(
|
|
6637
6667
|
async ({ newConversation }) => {
|
|
6638
6668
|
const myGen = ++connectGenRef.current;
|
|
6639
6669
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6645,7 +6675,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6645
6675
|
}
|
|
6646
6676
|
let res;
|
|
6647
6677
|
try {
|
|
6648
|
-
res = await transport.
|
|
6678
|
+
res = await transport.handshake({
|
|
6649
6679
|
visitorId,
|
|
6650
6680
|
conversationId,
|
|
6651
6681
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6696,67 +6726,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6696
6726
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6697
6727
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6698
6728
|
}
|
|
6729
|
+
welcomeRef.current = res.welcome;
|
|
6699
6730
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6700
6731
|
if (isResume) {
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
const chatPromise = res.messages?.length ? Promise.resolve({
|
|
6704
|
-
conversationId: persistedChatId,
|
|
6705
|
-
canContinue: res.canContinue ?? true,
|
|
6706
|
-
messages: res.messages
|
|
6707
|
-
}) : transport.loadConversation(persistedChatId);
|
|
6708
|
-
const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
|
|
6709
|
-
if (isStale()) return;
|
|
6710
|
-
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6711
|
-
setFormMarkers(markers);
|
|
6712
|
-
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6713
|
-
(n) => n !== void 0
|
|
6714
|
-
);
|
|
6715
|
-
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6716
|
-
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6717
|
-
const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6718
|
-
const row = makeInstantWelcomeMessage(w);
|
|
6719
|
-
row.createdAt = welcomeAnchor;
|
|
6720
|
-
return row;
|
|
6721
|
-
});
|
|
6722
|
-
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6723
|
-
if (loaded.length || tail.length) {
|
|
6724
|
-
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6725
|
-
setCanSend(chat.canContinue ?? true);
|
|
6726
|
-
setSuggestions(chat.suggestions ?? []);
|
|
6727
|
-
} else {
|
|
6728
|
-
messagesSig.value = welcomeRows;
|
|
6729
|
-
}
|
|
6730
|
-
} catch (err) {
|
|
6731
|
-
if (isStale()) return;
|
|
6732
|
-
log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
|
|
6733
|
-
conversationIdSig.value = void 0;
|
|
6734
|
-
persistence.saveConversationId(void 0);
|
|
6735
|
-
} finally {
|
|
6736
|
-
if (!isStale()) setLoadingMessages(false);
|
|
6737
|
-
}
|
|
6732
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6733
|
+
else pendingThreadRef.current = true;
|
|
6738
6734
|
} else {
|
|
6739
6735
|
conversationIdSig.value = res.conversationId;
|
|
6740
6736
|
persistence.saveConversationId(res.conversationId);
|
|
6741
6737
|
playWelcome(res.welcome?.messages);
|
|
6742
6738
|
}
|
|
6743
6739
|
if (!newConversation) {
|
|
6744
|
-
bus.emit("
|
|
6740
|
+
bus.emit("handshake", res);
|
|
6745
6741
|
setConversationReady(true);
|
|
6746
6742
|
}
|
|
6747
6743
|
},
|
|
6748
|
-
[
|
|
6749
|
-
transport,
|
|
6750
|
-
visitorId,
|
|
6751
|
-
persistence,
|
|
6752
|
-
options,
|
|
6753
|
-
bus,
|
|
6754
|
-
messagesSig,
|
|
6755
|
-
conversationIdSig,
|
|
6756
|
-
feedback,
|
|
6757
|
-
playWelcome,
|
|
6758
|
-
fetchFormMarkers
|
|
6759
|
-
]
|
|
6744
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6760
6745
|
);
|
|
6761
6746
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6762
6747
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6809,36 +6794,46 @@ function App({ options, hostElement, bus }) {
|
|
|
6809
6794
|
},
|
|
6810
6795
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6811
6796
|
);
|
|
6797
|
+
const resumeHandleRef = useRef9(null);
|
|
6798
|
+
useEffect17(() => {
|
|
6799
|
+
const persistedConversation = conversationIdSig.value;
|
|
6800
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6801
|
+
void runHandshake({ newConversation: false });
|
|
6802
|
+
return () => {
|
|
6803
|
+
connectGenRef.current++;
|
|
6804
|
+
resumeHandleRef.current?.cancel();
|
|
6805
|
+
};
|
|
6806
|
+
}, []);
|
|
6812
6807
|
useEffect17(() => {
|
|
6813
|
-
|
|
6808
|
+
if (!activated) return;
|
|
6809
|
+
dataBootRef.current = transport.bootstrap();
|
|
6814
6810
|
void (async () => {
|
|
6815
6811
|
try {
|
|
6816
|
-
const res = await
|
|
6817
|
-
setRemoteForms(resolveForms(res.forms));
|
|
6812
|
+
const res = await dataBootRef.current;
|
|
6813
|
+
setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
|
|
6818
6814
|
} catch (err) {
|
|
6819
|
-
log16.debug("
|
|
6815
|
+
log16.debug("bootstrap failed \u2014 treating as no forms", { err });
|
|
6820
6816
|
} finally {
|
|
6821
6817
|
setFormsReady(true);
|
|
6822
6818
|
}
|
|
6823
6819
|
})();
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
resume = transport.resumeStream();
|
|
6829
|
-
void runResume(resume);
|
|
6820
|
+
if (pendingThreadRef.current) {
|
|
6821
|
+
pendingThreadRef.current = false;
|
|
6822
|
+
const persisted = conversationIdSig.value;
|
|
6823
|
+
if (persisted) void loadThread(persisted);
|
|
6830
6824
|
}
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6825
|
+
if (conversationIdSig.value) {
|
|
6826
|
+
const handle = transport.resumeStream();
|
|
6827
|
+
resumeHandleRef.current = handle;
|
|
6828
|
+
void runResume(handle);
|
|
6829
|
+
}
|
|
6830
|
+
}, [activated]);
|
|
6836
6831
|
const lastUserSig = useRef9(userSig);
|
|
6837
6832
|
useEffect17(() => {
|
|
6838
6833
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6839
6834
|
lastUserSig.current = userSig;
|
|
6840
|
-
void
|
|
6841
|
-
}, [userSig, conversationReady,
|
|
6835
|
+
void runHandshake({ newConversation: false });
|
|
6836
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6842
6837
|
useEffect17(() => {
|
|
6843
6838
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6844
6839
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -7065,10 +7060,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7065
7060
|
}, [transport, options.modules, unreadCountSig]);
|
|
7066
7061
|
const unreadSeeded = useRef9(false);
|
|
7067
7062
|
useEffect17(() => {
|
|
7068
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7063
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
7069
7064
|
unreadSeeded.current = true;
|
|
7070
7065
|
refreshUnread();
|
|
7071
|
-
}, [conversationReady, refreshUnread]);
|
|
7066
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
7072
7067
|
const handleOpen = useCallback6(() => {
|
|
7073
7068
|
log16.info("open", { mode: options.mode });
|
|
7074
7069
|
feedback.unlock();
|
|
@@ -7090,8 +7085,8 @@ function App({ options, hostElement, bus }) {
|
|
|
7090
7085
|
persistence.clearConversation();
|
|
7091
7086
|
setCanSend(true);
|
|
7092
7087
|
setFormMarkers([]);
|
|
7093
|
-
void
|
|
7094
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7088
|
+
void runHandshake({ newConversation: true });
|
|
7089
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
7095
7090
|
const handleNewChat = useCallback6(() => {
|
|
7096
7091
|
handleClear();
|
|
7097
7092
|
setView("chat");
|
|
@@ -7202,6 +7197,20 @@ function App({ options, hostElement, bus }) {
|
|
|
7202
7197
|
},
|
|
7203
7198
|
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
|
|
7204
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]);
|
|
7205
7214
|
useEffect17(() => {
|
|
7206
7215
|
const unsub = bindHostCommands(hostElement, {
|
|
7207
7216
|
open: handleOpen,
|
|
@@ -7209,10 +7218,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7209
7218
|
expand: handleExpand,
|
|
7210
7219
|
fullscreen: handleFullscreen,
|
|
7211
7220
|
popout: handlePopOut,
|
|
7212
|
-
openForm
|
|
7221
|
+
openForm
|
|
7213
7222
|
});
|
|
7214
7223
|
return unsub;
|
|
7215
|
-
}, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut,
|
|
7224
|
+
}, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut, openForm]);
|
|
7216
7225
|
const effectiveOptions = useMemo3(
|
|
7217
7226
|
() => applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize),
|
|
7218
7227
|
[options, activeLocale, activeThemeMode, activeTextSize]
|
|
@@ -7449,7 +7458,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7449
7458
|
/**
|
|
7450
7459
|
* Raw options accumulator. Starts as whatever the page-owner set via
|
|
7451
7460
|
* HTML attributes; gets server-side settings merged in on the
|
|
7452
|
-
* `
|
|
7461
|
+
* `handshake` event so subsequent re-renders pick up the server's
|
|
7453
7462
|
* mode / theme / launcher / etc.
|
|
7454
7463
|
*
|
|
7455
7464
|
* Mirrors `index.ts`'s `rawOptions` so both entry points share the
|
|
@@ -7479,7 +7488,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7479
7488
|
existingHost: this,
|
|
7480
7489
|
position: hostPositionForMode(resolved.mode)
|
|
7481
7490
|
});
|
|
7482
|
-
this.bus.on("
|
|
7491
|
+
this.bus.on("handshake", (response) => {
|
|
7483
7492
|
if (!response.config) return;
|
|
7484
7493
|
this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
|
|
7485
7494
|
this.renderApp();
|
|
@@ -7489,7 +7498,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7489
7498
|
}
|
|
7490
7499
|
/**
|
|
7491
7500
|
* Apply the current `rawOptions` to the host element + App tree.
|
|
7492
|
-
* Called by both the initial boot and every `
|
|
7501
|
+
* Called by both the initial boot and every `handshake` merge.
|
|
7493
7502
|
*/
|
|
7494
7503
|
renderApp() {
|
|
7495
7504
|
if (!this.mountResult) return;
|