@helpai/elements 0.25.0 → 0.26.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 +13 -14
- package/elements-web-component.esm.js +19 -19
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +19 -19
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +19 -19
- package/elements.esm.js.map +4 -4
- package/elements.js +19 -19
- package/elements.js.map +4 -4
- package/index.d.ts +37 -83
- package/index.mjs +132 -125
- package/package.json +1 -1
- package/schema.d.ts +50 -50
- package/schema.json +19 -22
- package/schema.mjs +15 -16
- package/web-component.d.ts +1 -1
- package/web-component.mjs +132 -125
package/web-component.mjs
CHANGED
|
@@ -465,8 +465,8 @@ var DEFAULT_TRACKING = {
|
|
|
465
465
|
sampleRate: 1
|
|
466
466
|
};
|
|
467
467
|
var DEFAULT_ENDPOINTS = {
|
|
468
|
-
upload: "/ai
|
|
469
|
-
transcribe: "/ai
|
|
468
|
+
upload: "/ai-agent/upload-file",
|
|
469
|
+
transcribe: "/ai-agent/transcribe-audio",
|
|
470
470
|
submitForm: "/submit-form"
|
|
471
471
|
};
|
|
472
472
|
var DEFAULT_LAUNCHER = {
|
|
@@ -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),
|
|
@@ -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,43 +1308,43 @@ function parseWireDate(value) {
|
|
|
1305
1308
|
}
|
|
1306
1309
|
var DEFAULT_PATHS = {
|
|
1307
1310
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1308
|
-
|
|
1311
|
+
handshake: "/ai-agent/handshake",
|
|
1309
1312
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1310
|
-
streamMessage: "/ai
|
|
1313
|
+
streamMessage: "/ai-agent/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: "/ai
|
|
1320
|
+
streamResume: "/ai-agent/stream-resume",
|
|
1318
1321
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1319
|
-
cancelStream: "/ai
|
|
1322
|
+
cancelStream: "/ai-agent/cancel-stream",
|
|
1320
1323
|
/** List the visitor's conversations. `GET`. */
|
|
1321
|
-
listConversations: "/ai
|
|
1322
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1323
|
-
listMessages: "/ai
|
|
1324
|
+
listConversations: "/ai-agent/list-conversations",
|
|
1325
|
+
/** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
|
|
1326
|
+
listMessages: "/ai-agent/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: "/ai
|
|
1333
|
+
updateSettings: "/ai-agent/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: "/ai
|
|
1340
|
+
markRead: "/ai-agent/mark-read",
|
|
1338
1341
|
// ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
|
|
1339
1342
|
/** All widget content via one filtered endpoint. `GET /content?tags=…`. */
|
|
1340
1343
|
content: "/content",
|
|
1341
1344
|
/**
|
|
1342
1345
|
* Resolved form definitions for the deployment. `GET /forms` →
|
|
1343
1346
|
* `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
|
|
1344
|
-
*
|
|
1347
|
+
* handshake); form gating waits for it. A failure is treated as
|
|
1345
1348
|
* "no forms" (non-fatal).
|
|
1346
1349
|
*/
|
|
1347
1350
|
forms: "/forms",
|
|
@@ -1356,7 +1359,7 @@ var DEFAULT_PATHS = {
|
|
|
1356
1359
|
* Visitor interaction records for a conversation. `GET
|
|
1357
1360
|
* /activity?visitorId=…&conversationId=…` →
|
|
1358
1361
|
* `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
|
|
1359
|
-
* the former `formSubmissions` echo on
|
|
1362
|
+
* the former `formSubmissions` echo on the handshake / list-messages;
|
|
1360
1363
|
* deliberately generic — future visitor interaction records ride the same
|
|
1361
1364
|
* endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
|
|
1362
1365
|
* deployment doesn't resolve) is treated as "no records" (non-fatal).
|
|
@@ -1462,7 +1465,7 @@ function sleep(ms, signal7) {
|
|
|
1462
1465
|
var AgentTransport = class {
|
|
1463
1466
|
constructor(opts) {
|
|
1464
1467
|
__publicField(this, "opts", opts);
|
|
1465
|
-
// Identity adopted from the
|
|
1468
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1466
1469
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1467
1470
|
// (visitor, conversation) pair.
|
|
1468
1471
|
__publicField(this, "visitorId");
|
|
@@ -1485,9 +1488,9 @@ var AgentTransport = class {
|
|
|
1485
1488
|
}
|
|
1486
1489
|
/**
|
|
1487
1490
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1488
|
-
* `
|
|
1489
|
-
* with `
|
|
1490
|
-
* `conversationId` and the resume GET couldn't identify the thread. `
|
|
1491
|
+
* `handshake` resolves. A mount-time `resumeStream()` runs in parallel
|
|
1492
|
+
* with `handshake`, so without this its envelope would carry no
|
|
1493
|
+
* `conversationId` and the resume GET couldn't identify the thread. `handshake`
|
|
1491
1494
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1492
1495
|
*/
|
|
1493
1496
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1504,7 +1507,7 @@ var AgentTransport = class {
|
|
|
1504
1507
|
return out;
|
|
1505
1508
|
}
|
|
1506
1509
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1507
|
-
*
|
|
1510
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1508
1511
|
* differs from the active one. */
|
|
1509
1512
|
withEnvelope(body) {
|
|
1510
1513
|
return { ...this.envelope(), ...body };
|
|
@@ -1525,28 +1528,22 @@ var AgentTransport = class {
|
|
|
1525
1528
|
return url.toString();
|
|
1526
1529
|
}
|
|
1527
1530
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1528
|
-
async
|
|
1529
|
-
log4.debug("
|
|
1531
|
+
async handshake(body) {
|
|
1532
|
+
log4.debug("handshake \u2192", {
|
|
1530
1533
|
visitorId: body.visitorId,
|
|
1531
1534
|
conversationId: body.conversationId,
|
|
1532
1535
|
locale: body.locale
|
|
1533
1536
|
});
|
|
1534
|
-
const res = await this.postJson(
|
|
1535
|
-
|
|
1536
|
-
body,
|
|
1537
|
-
"startConversation"
|
|
1538
|
-
);
|
|
1539
|
-
log4.debug("startConversation \u2190", {
|
|
1537
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1538
|
+
log4.debug("handshake \u2190", {
|
|
1540
1539
|
visitorId: res.visitorId,
|
|
1541
1540
|
conversationId: res.conversationId,
|
|
1542
|
-
canContinue: res.canContinue,
|
|
1543
|
-
hasMessages: !!res.messages?.length,
|
|
1544
1541
|
hasWelcome: !!res.welcome,
|
|
1545
1542
|
hasConfig: !!res.config,
|
|
1546
1543
|
rebind: res.rebind
|
|
1547
1544
|
});
|
|
1548
|
-
if (!
|
|
1549
|
-
log4.warn("
|
|
1545
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1546
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1550
1547
|
}
|
|
1551
1548
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1552
1549
|
this.conversationId = res.conversationId;
|
|
@@ -1658,7 +1655,7 @@ var AgentTransport = class {
|
|
|
1658
1655
|
/**
|
|
1659
1656
|
* Fetch the deployment's resolved form definitions. `GET /forms`
|
|
1660
1657
|
* (data-API). Replaces the former `config.forms` push on the handshake —
|
|
1661
|
-
* called once at boot, in parallel with `
|
|
1658
|
+
* called once at boot, in parallel with `handshake`; form gating
|
|
1662
1659
|
* (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
|
|
1663
1660
|
* "no forms" (non-fatal).
|
|
1664
1661
|
*/
|
|
@@ -1700,10 +1697,10 @@ var AgentTransport = class {
|
|
|
1700
1697
|
return res;
|
|
1701
1698
|
}
|
|
1702
1699
|
get uploadPath() {
|
|
1703
|
-
return this.opts.endpoints?.upload ?? "/ai
|
|
1700
|
+
return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
|
|
1704
1701
|
}
|
|
1705
1702
|
get transcribePath() {
|
|
1706
|
-
return this.opts.endpoints?.transcribe ?? "/ai
|
|
1703
|
+
return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
|
|
1707
1704
|
}
|
|
1708
1705
|
get submitFormPath() {
|
|
1709
1706
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
@@ -1743,9 +1740,9 @@ var AgentTransport = class {
|
|
|
1743
1740
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1744
1741
|
*
|
|
1745
1742
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1746
|
-
* `
|
|
1743
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1747
1744
|
* the user refreshed while the assistant was still streaming, the
|
|
1748
|
-
* `
|
|
1745
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1749
1746
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1750
1747
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1751
1748
|
* (the common case — no bubble is created).
|
|
@@ -1772,7 +1769,7 @@ var AgentTransport = class {
|
|
|
1772
1769
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1773
1770
|
* off first, the reply may still be spinning up server-side);
|
|
1774
1771
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1775
|
-
* parallel with
|
|
1772
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1776
1773
|
* user reloaded keeps flowing).
|
|
1777
1774
|
*
|
|
1778
1775
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1781,7 +1778,7 @@ var AgentTransport = class {
|
|
|
1781
1778
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1782
1779
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1783
1780
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1784
|
-
* turn comes from the DB via
|
|
1781
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1785
1782
|
*
|
|
1786
1783
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1787
1784
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1846,7 +1843,7 @@ var AgentTransport = class {
|
|
|
1846
1843
|
}
|
|
1847
1844
|
return false;
|
|
1848
1845
|
}
|
|
1849
|
-
/** Abort + fire-and-forget POST to `/ai
|
|
1846
|
+
/** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
|
|
1850
1847
|
cancelStream(ctrl, conversationId) {
|
|
1851
1848
|
if (ctrl.signal.aborted) return;
|
|
1852
1849
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1887,7 +1884,7 @@ var AgentTransport = class {
|
|
|
1887
1884
|
return url.toString();
|
|
1888
1885
|
}
|
|
1889
1886
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1890
|
-
// update-settings / mark-read /
|
|
1887
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1891
1888
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1892
1889
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1893
1890
|
async postJson(path, body, label) {
|
|
@@ -1956,23 +1953,22 @@ var AgentTransport = class {
|
|
|
1956
1953
|
return await res.json();
|
|
1957
1954
|
}
|
|
1958
1955
|
/**
|
|
1959
|
-
* Agent-API URL — resolves a `/ai
|
|
1960
|
-
* base
|
|
1956
|
+
* Agent-API URL — resolves a `/ai-agent/*` path against the resolved
|
|
1957
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1961
1958
|
*/
|
|
1962
1959
|
url(pathOrUrl) {
|
|
1963
1960
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1964
|
-
return `${this.opts.
|
|
1961
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1965
1962
|
}
|
|
1966
1963
|
/**
|
|
1967
1964
|
* 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`.
|
|
1965
|
+
* forms / submit-form / activity) against the resolved data module base.
|
|
1966
|
+
* Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1967
|
+
* CRM) pass through unchanged.
|
|
1972
1968
|
*/
|
|
1973
1969
|
dataUrl(pathOrUrl) {
|
|
1974
1970
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1975
|
-
return `${this.opts.dataBaseUrl
|
|
1971
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1976
1972
|
}
|
|
1977
1973
|
get fetchImpl() {
|
|
1978
1974
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1986,10 +1982,10 @@ function extensionFor(mimeType) {
|
|
|
1986
1982
|
if (mimeType.includes("wav")) return "wav";
|
|
1987
1983
|
return "bin";
|
|
1988
1984
|
}
|
|
1989
|
-
function
|
|
1985
|
+
function isHandshakeResponseShape(raw) {
|
|
1990
1986
|
if (!raw || typeof raw !== "object") return false;
|
|
1991
1987
|
const r = raw;
|
|
1992
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1988
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1993
1989
|
}
|
|
1994
1990
|
|
|
1995
1991
|
// src/stream/messages.ts
|
|
@@ -6504,6 +6500,14 @@ function App({ options, hostElement, bus }) {
|
|
|
6504
6500
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6505
6501
|
);
|
|
6506
6502
|
const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
|
|
6503
|
+
const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
|
|
6504
|
+
const activatedRef = useRef9(activated);
|
|
6505
|
+
activatedRef.current = activated;
|
|
6506
|
+
const pendingThreadRef = useRef9(false);
|
|
6507
|
+
const welcomeRef = useRef9(void 0);
|
|
6508
|
+
useEffect17(() => {
|
|
6509
|
+
if (isOpen) setActivated(true);
|
|
6510
|
+
}, [isOpen]);
|
|
6507
6511
|
const initialPanelApplied = useRef9(false);
|
|
6508
6512
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6509
6513
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6530,7 +6534,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6530
6534
|
const [formContext, setFormContext] = useState13({});
|
|
6531
6535
|
const [transport] = useState13(
|
|
6532
6536
|
() => new AgentTransport({
|
|
6533
|
-
|
|
6537
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6534
6538
|
dataBaseUrl: options.dataBaseUrl,
|
|
6535
6539
|
auth: createAuth({
|
|
6536
6540
|
publicKey: options.publicKey,
|
|
@@ -6633,7 +6637,46 @@ function App({ options, hostElement, bus }) {
|
|
|
6633
6637
|
[transport]
|
|
6634
6638
|
);
|
|
6635
6639
|
const connectGenRef = useRef9(0);
|
|
6636
|
-
const
|
|
6640
|
+
const loadThread = useCallback6(
|
|
6641
|
+
async (conversationId) => {
|
|
6642
|
+
const myGen = connectGenRef.current;
|
|
6643
|
+
const isStale = () => myGen !== connectGenRef.current;
|
|
6644
|
+
setLoadingMessages(true);
|
|
6645
|
+
try {
|
|
6646
|
+
const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
|
|
6647
|
+
if (isStale()) return;
|
|
6648
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6649
|
+
setFormMarkers(markers);
|
|
6650
|
+
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6651
|
+
(n) => n !== void 0
|
|
6652
|
+
);
|
|
6653
|
+
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6654
|
+
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6655
|
+
const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6656
|
+
const row = makeInstantWelcomeMessage(w);
|
|
6657
|
+
row.createdAt = welcomeAnchor;
|
|
6658
|
+
return row;
|
|
6659
|
+
});
|
|
6660
|
+
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6661
|
+
if (loaded.length || tail.length) {
|
|
6662
|
+
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6663
|
+
setCanSend(chat.canContinue ?? true);
|
|
6664
|
+
setSuggestions(chat.suggestions ?? []);
|
|
6665
|
+
} else {
|
|
6666
|
+
messagesSig.value = welcomeRows;
|
|
6667
|
+
}
|
|
6668
|
+
} catch (err) {
|
|
6669
|
+
if (isStale()) return;
|
|
6670
|
+
log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
|
|
6671
|
+
conversationIdSig.value = void 0;
|
|
6672
|
+
persistence.saveConversationId(void 0);
|
|
6673
|
+
} finally {
|
|
6674
|
+
if (!isStale()) setLoadingMessages(false);
|
|
6675
|
+
}
|
|
6676
|
+
},
|
|
6677
|
+
[transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
|
|
6678
|
+
);
|
|
6679
|
+
const runHandshake = useCallback6(
|
|
6637
6680
|
async ({ newConversation }) => {
|
|
6638
6681
|
const myGen = ++connectGenRef.current;
|
|
6639
6682
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6645,7 +6688,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6645
6688
|
}
|
|
6646
6689
|
let res;
|
|
6647
6690
|
try {
|
|
6648
|
-
res = await transport.
|
|
6691
|
+
res = await transport.handshake({
|
|
6649
6692
|
visitorId,
|
|
6650
6693
|
conversationId,
|
|
6651
6694
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6696,67 +6739,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6696
6739
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6697
6740
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6698
6741
|
}
|
|
6742
|
+
welcomeRef.current = res.welcome;
|
|
6699
6743
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6700
6744
|
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
|
-
}
|
|
6745
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6746
|
+
else pendingThreadRef.current = true;
|
|
6738
6747
|
} else {
|
|
6739
6748
|
conversationIdSig.value = res.conversationId;
|
|
6740
6749
|
persistence.saveConversationId(res.conversationId);
|
|
6741
6750
|
playWelcome(res.welcome?.messages);
|
|
6742
6751
|
}
|
|
6743
6752
|
if (!newConversation) {
|
|
6744
|
-
bus.emit("
|
|
6753
|
+
bus.emit("handshake", res);
|
|
6745
6754
|
setConversationReady(true);
|
|
6746
6755
|
}
|
|
6747
6756
|
},
|
|
6748
|
-
[
|
|
6749
|
-
transport,
|
|
6750
|
-
visitorId,
|
|
6751
|
-
persistence,
|
|
6752
|
-
options,
|
|
6753
|
-
bus,
|
|
6754
|
-
messagesSig,
|
|
6755
|
-
conversationIdSig,
|
|
6756
|
-
feedback,
|
|
6757
|
-
playWelcome,
|
|
6758
|
-
fetchFormMarkers
|
|
6759
|
-
]
|
|
6757
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6760
6758
|
);
|
|
6761
6759
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6762
6760
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6809,8 +6807,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6809
6807
|
},
|
|
6810
6808
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6811
6809
|
);
|
|
6810
|
+
const resumeHandleRef = useRef9(null);
|
|
6812
6811
|
useEffect17(() => {
|
|
6813
|
-
|
|
6812
|
+
const persistedConversation = conversationIdSig.value;
|
|
6813
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6814
|
+
void runHandshake({ newConversation: false });
|
|
6815
|
+
return () => {
|
|
6816
|
+
connectGenRef.current++;
|
|
6817
|
+
resumeHandleRef.current?.cancel();
|
|
6818
|
+
};
|
|
6819
|
+
}, []);
|
|
6820
|
+
useEffect17(() => {
|
|
6821
|
+
if (!activated) return;
|
|
6814
6822
|
void (async () => {
|
|
6815
6823
|
try {
|
|
6816
6824
|
const res = await transport.listForms();
|
|
@@ -6821,24 +6829,23 @@ function App({ options, hostElement, bus }) {
|
|
|
6821
6829
|
setFormsReady(true);
|
|
6822
6830
|
}
|
|
6823
6831
|
})();
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
resume = transport.resumeStream();
|
|
6829
|
-
void runResume(resume);
|
|
6832
|
+
if (pendingThreadRef.current) {
|
|
6833
|
+
pendingThreadRef.current = false;
|
|
6834
|
+
const persisted = conversationIdSig.value;
|
|
6835
|
+
if (persisted) void loadThread(persisted);
|
|
6830
6836
|
}
|
|
6831
|
-
|
|
6832
|
-
|
|
6833
|
-
|
|
6834
|
-
|
|
6835
|
-
|
|
6837
|
+
if (conversationIdSig.value) {
|
|
6838
|
+
const handle = transport.resumeStream();
|
|
6839
|
+
resumeHandleRef.current = handle;
|
|
6840
|
+
void runResume(handle);
|
|
6841
|
+
}
|
|
6842
|
+
}, [activated]);
|
|
6836
6843
|
const lastUserSig = useRef9(userSig);
|
|
6837
6844
|
useEffect17(() => {
|
|
6838
6845
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6839
6846
|
lastUserSig.current = userSig;
|
|
6840
|
-
void
|
|
6841
|
-
}, [userSig, conversationReady,
|
|
6847
|
+
void runHandshake({ newConversation: false });
|
|
6848
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6842
6849
|
useEffect17(() => {
|
|
6843
6850
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6844
6851
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -7065,10 +7072,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7065
7072
|
}, [transport, options.modules, unreadCountSig]);
|
|
7066
7073
|
const unreadSeeded = useRef9(false);
|
|
7067
7074
|
useEffect17(() => {
|
|
7068
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7075
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
7069
7076
|
unreadSeeded.current = true;
|
|
7070
7077
|
refreshUnread();
|
|
7071
|
-
}, [conversationReady, refreshUnread]);
|
|
7078
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
7072
7079
|
const handleOpen = useCallback6(() => {
|
|
7073
7080
|
log16.info("open", { mode: options.mode });
|
|
7074
7081
|
feedback.unlock();
|
|
@@ -7090,8 +7097,8 @@ function App({ options, hostElement, bus }) {
|
|
|
7090
7097
|
persistence.clearConversation();
|
|
7091
7098
|
setCanSend(true);
|
|
7092
7099
|
setFormMarkers([]);
|
|
7093
|
-
void
|
|
7094
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7100
|
+
void runHandshake({ newConversation: true });
|
|
7101
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
7095
7102
|
const handleNewChat = useCallback6(() => {
|
|
7096
7103
|
handleClear();
|
|
7097
7104
|
setView("chat");
|
|
@@ -7449,7 +7456,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7449
7456
|
/**
|
|
7450
7457
|
* Raw options accumulator. Starts as whatever the page-owner set via
|
|
7451
7458
|
* HTML attributes; gets server-side settings merged in on the
|
|
7452
|
-
* `
|
|
7459
|
+
* `handshake` event so subsequent re-renders pick up the server's
|
|
7453
7460
|
* mode / theme / launcher / etc.
|
|
7454
7461
|
*
|
|
7455
7462
|
* Mirrors `index.ts`'s `rawOptions` so both entry points share the
|
|
@@ -7479,7 +7486,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7479
7486
|
existingHost: this,
|
|
7480
7487
|
position: hostPositionForMode(resolved.mode)
|
|
7481
7488
|
});
|
|
7482
|
-
this.bus.on("
|
|
7489
|
+
this.bus.on("handshake", (response) => {
|
|
7483
7490
|
if (!response.config) return;
|
|
7484
7491
|
this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
|
|
7485
7492
|
this.renderApp();
|
|
@@ -7489,7 +7496,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7489
7496
|
}
|
|
7490
7497
|
/**
|
|
7491
7498
|
* Apply the current `rawOptions` to the host element + App tree.
|
|
7492
|
-
* Called by both the initial boot and every `
|
|
7499
|
+
* Called by both the initial boot and every `handshake` merge.
|
|
7493
7500
|
*/
|
|
7494
7501
|
renderApp() {
|
|
7495
7502
|
if (!this.mountResult) return;
|