@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/index.mjs
CHANGED
|
@@ -480,8 +480,8 @@ var DEFAULT_TRACKING = {
|
|
|
480
480
|
sampleRate: 1
|
|
481
481
|
};
|
|
482
482
|
var DEFAULT_ENDPOINTS = {
|
|
483
|
-
upload: "/ai
|
|
484
|
-
transcribe: "/ai
|
|
483
|
+
upload: "/ai-agent/upload-file",
|
|
484
|
+
transcribe: "/ai-agent/transcribe-audio",
|
|
485
485
|
submitForm: "/submit-form"
|
|
486
486
|
};
|
|
487
487
|
var DEFAULT_LAUNCHER = {
|
|
@@ -525,12 +525,14 @@ function resolveOptions(rawOpts) {
|
|
|
525
525
|
const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
|
|
526
526
|
const availableLocales = i18n.availableLocales ?? [locale];
|
|
527
527
|
const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
|
|
528
|
+
const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
|
|
528
529
|
const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
|
|
529
530
|
return {
|
|
530
531
|
widgetId: opts.widgetId ?? "default",
|
|
531
532
|
publicKey: opts.publicKey,
|
|
532
533
|
aiAgentDeploymentId: opts.aiAgentDeploymentId,
|
|
533
534
|
baseUrl,
|
|
535
|
+
agentBaseUrl,
|
|
534
536
|
dataBaseUrl,
|
|
535
537
|
userContext: sanitizeUserContext(opts.userContext),
|
|
536
538
|
pageContext: sanitizePageContext(opts.pageContext),
|
|
@@ -844,6 +846,7 @@ var EMBED_SCALAR_ATTRS = [
|
|
|
844
846
|
["public-key", "publicKey"],
|
|
845
847
|
["ai-agent-deployment-id", "aiAgentDeploymentId"],
|
|
846
848
|
["base-url", "baseUrl"],
|
|
849
|
+
["agent-base-url", "agentBaseUrl"],
|
|
847
850
|
["data-base-url", "dataBaseUrl"],
|
|
848
851
|
["theme", "theme", (v) => v]
|
|
849
852
|
];
|
|
@@ -919,7 +922,7 @@ var REACTIVE_ATTRS = [
|
|
|
919
922
|
...SPECIAL_REACTIVE_ATTRS
|
|
920
923
|
];
|
|
921
924
|
|
|
922
|
-
// src/core/
|
|
925
|
+
// src/core/handshake-shape.ts
|
|
923
926
|
function isPlainObject(raw) {
|
|
924
927
|
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
925
928
|
}
|
|
@@ -1250,43 +1253,43 @@ function parseWireDate(value) {
|
|
|
1250
1253
|
}
|
|
1251
1254
|
var DEFAULT_PATHS = {
|
|
1252
1255
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1253
|
-
|
|
1256
|
+
handshake: "/ai-agent/handshake",
|
|
1254
1257
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1255
|
-
streamMessage: "/ai
|
|
1258
|
+
streamMessage: "/ai-agent/stream-message",
|
|
1256
1259
|
/**
|
|
1257
1260
|
* Resume an interrupted reply. `GET` (envelope rides as query params).
|
|
1258
1261
|
* Stateless: the server re-streams the whole reply SSE again from the start —
|
|
1259
1262
|
* there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
|
|
1260
1263
|
* replayed events are skipped and only the continuation surfaces.
|
|
1261
1264
|
*/
|
|
1262
|
-
streamResume: "/ai
|
|
1265
|
+
streamResume: "/ai-agent/stream-resume",
|
|
1263
1266
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1264
|
-
cancelStream: "/ai
|
|
1267
|
+
cancelStream: "/ai-agent/cancel-stream",
|
|
1265
1268
|
/** List the visitor's conversations. `GET`. */
|
|
1266
|
-
listConversations: "/ai
|
|
1267
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1268
|
-
listMessages: "/ai
|
|
1269
|
+
listConversations: "/ai-agent/list-conversations",
|
|
1270
|
+
/** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
|
|
1271
|
+
listMessages: "/ai-agent/list-messages",
|
|
1269
1272
|
/**
|
|
1270
1273
|
* Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
|
|
1271
1274
|
* server persists, returns the merged authoritative copy. Fire-and-forget on
|
|
1272
1275
|
* the client — a failure leaves the local cache valid; the next
|
|
1273
|
-
* `
|
|
1276
|
+
* `handshake()` reconciles.
|
|
1274
1277
|
*/
|
|
1275
|
-
updateSettings: "/ai
|
|
1278
|
+
updateSettings: "/ai-agent/update-settings",
|
|
1276
1279
|
/**
|
|
1277
1280
|
* Mark a conversation read. POST `{ visitorId, conversationId }` → the server
|
|
1278
1281
|
* records `lastReadAt = now` for that (visitor, conversation) and recomputes
|
|
1279
1282
|
* `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
|
|
1280
1283
|
* on the client; a failure just leaves the badge until the next sync.
|
|
1281
1284
|
*/
|
|
1282
|
-
markRead: "/ai
|
|
1285
|
+
markRead: "/ai-agent/mark-read",
|
|
1283
1286
|
// ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
|
|
1284
1287
|
/** All widget content via one filtered endpoint. `GET /content?tags=…`. */
|
|
1285
1288
|
content: "/content",
|
|
1286
1289
|
/**
|
|
1287
1290
|
* Resolved form definitions for the deployment. `GET /forms` →
|
|
1288
1291
|
* `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
|
|
1289
|
-
*
|
|
1292
|
+
* handshake); form gating waits for it. A failure is treated as
|
|
1290
1293
|
* "no forms" (non-fatal).
|
|
1291
1294
|
*/
|
|
1292
1295
|
forms: "/forms",
|
|
@@ -1301,7 +1304,7 @@ var DEFAULT_PATHS = {
|
|
|
1301
1304
|
* Visitor interaction records for a conversation. `GET
|
|
1302
1305
|
* /activity?visitorId=…&conversationId=…` →
|
|
1303
1306
|
* `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
|
|
1304
|
-
* the former `formSubmissions` echo on
|
|
1307
|
+
* the former `formSubmissions` echo on the handshake / list-messages;
|
|
1305
1308
|
* deliberately generic — future visitor interaction records ride the same
|
|
1306
1309
|
* endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
|
|
1307
1310
|
* deployment doesn't resolve) is treated as "no records" (non-fatal).
|
|
@@ -1407,7 +1410,7 @@ function sleep(ms, signal7) {
|
|
|
1407
1410
|
var AgentTransport = class {
|
|
1408
1411
|
constructor(opts) {
|
|
1409
1412
|
__publicField(this, "opts", opts);
|
|
1410
|
-
// Identity adopted from the
|
|
1413
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1411
1414
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1412
1415
|
// (visitor, conversation) pair.
|
|
1413
1416
|
__publicField(this, "visitorId");
|
|
@@ -1430,9 +1433,9 @@ var AgentTransport = class {
|
|
|
1430
1433
|
}
|
|
1431
1434
|
/**
|
|
1432
1435
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1433
|
-
* `
|
|
1434
|
-
* with `
|
|
1435
|
-
* `conversationId` and the resume GET couldn't identify the thread. `
|
|
1436
|
+
* `handshake` resolves. A mount-time `resumeStream()` runs in parallel
|
|
1437
|
+
* with `handshake`, so without this its envelope would carry no
|
|
1438
|
+
* `conversationId` and the resume GET couldn't identify the thread. `handshake`
|
|
1436
1439
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1437
1440
|
*/
|
|
1438
1441
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1449,7 +1452,7 @@ var AgentTransport = class {
|
|
|
1449
1452
|
return out;
|
|
1450
1453
|
}
|
|
1451
1454
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1452
|
-
*
|
|
1455
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1453
1456
|
* differs from the active one. */
|
|
1454
1457
|
withEnvelope(body) {
|
|
1455
1458
|
return { ...this.envelope(), ...body };
|
|
@@ -1470,28 +1473,22 @@ var AgentTransport = class {
|
|
|
1470
1473
|
return url.toString();
|
|
1471
1474
|
}
|
|
1472
1475
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1473
|
-
async
|
|
1474
|
-
log4.debug("
|
|
1476
|
+
async handshake(body) {
|
|
1477
|
+
log4.debug("handshake \u2192", {
|
|
1475
1478
|
visitorId: body.visitorId,
|
|
1476
1479
|
conversationId: body.conversationId,
|
|
1477
1480
|
locale: body.locale
|
|
1478
1481
|
});
|
|
1479
|
-
const res = await this.postJson(
|
|
1480
|
-
|
|
1481
|
-
body,
|
|
1482
|
-
"startConversation"
|
|
1483
|
-
);
|
|
1484
|
-
log4.debug("startConversation \u2190", {
|
|
1482
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1483
|
+
log4.debug("handshake \u2190", {
|
|
1485
1484
|
visitorId: res.visitorId,
|
|
1486
1485
|
conversationId: res.conversationId,
|
|
1487
|
-
canContinue: res.canContinue,
|
|
1488
|
-
hasMessages: !!res.messages?.length,
|
|
1489
1486
|
hasWelcome: !!res.welcome,
|
|
1490
1487
|
hasConfig: !!res.config,
|
|
1491
1488
|
rebind: res.rebind
|
|
1492
1489
|
});
|
|
1493
|
-
if (!
|
|
1494
|
-
log4.warn("
|
|
1490
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1491
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1495
1492
|
}
|
|
1496
1493
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1497
1494
|
this.conversationId = res.conversationId;
|
|
@@ -1603,7 +1600,7 @@ var AgentTransport = class {
|
|
|
1603
1600
|
/**
|
|
1604
1601
|
* Fetch the deployment's resolved form definitions. `GET /forms`
|
|
1605
1602
|
* (data-API). Replaces the former `config.forms` push on the handshake —
|
|
1606
|
-
* called once at boot, in parallel with `
|
|
1603
|
+
* called once at boot, in parallel with `handshake`; form gating
|
|
1607
1604
|
* (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
|
|
1608
1605
|
* "no forms" (non-fatal).
|
|
1609
1606
|
*/
|
|
@@ -1645,10 +1642,10 @@ var AgentTransport = class {
|
|
|
1645
1642
|
return res;
|
|
1646
1643
|
}
|
|
1647
1644
|
get uploadPath() {
|
|
1648
|
-
return this.opts.endpoints?.upload ?? "/ai
|
|
1645
|
+
return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
|
|
1649
1646
|
}
|
|
1650
1647
|
get transcribePath() {
|
|
1651
|
-
return this.opts.endpoints?.transcribe ?? "/ai
|
|
1648
|
+
return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
|
|
1652
1649
|
}
|
|
1653
1650
|
get submitFormPath() {
|
|
1654
1651
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
@@ -1688,9 +1685,9 @@ var AgentTransport = class {
|
|
|
1688
1685
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1689
1686
|
*
|
|
1690
1687
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1691
|
-
* `
|
|
1688
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1692
1689
|
* the user refreshed while the assistant was still streaming, the
|
|
1693
|
-
* `
|
|
1690
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1694
1691
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1695
1692
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1696
1693
|
* (the common case — no bubble is created).
|
|
@@ -1717,7 +1714,7 @@ var AgentTransport = class {
|
|
|
1717
1714
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1718
1715
|
* off first, the reply may still be spinning up server-side);
|
|
1719
1716
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1720
|
-
* parallel with
|
|
1717
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1721
1718
|
* user reloaded keeps flowing).
|
|
1722
1719
|
*
|
|
1723
1720
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1726,7 +1723,7 @@ var AgentTransport = class {
|
|
|
1726
1723
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1727
1724
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1728
1725
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1729
|
-
* turn comes from the DB via
|
|
1726
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1730
1727
|
*
|
|
1731
1728
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1732
1729
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1791,7 +1788,7 @@ var AgentTransport = class {
|
|
|
1791
1788
|
}
|
|
1792
1789
|
return false;
|
|
1793
1790
|
}
|
|
1794
|
-
/** Abort + fire-and-forget POST to `/ai
|
|
1791
|
+
/** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
|
|
1795
1792
|
cancelStream(ctrl, conversationId) {
|
|
1796
1793
|
if (ctrl.signal.aborted) return;
|
|
1797
1794
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1832,7 +1829,7 @@ var AgentTransport = class {
|
|
|
1832
1829
|
return url.toString();
|
|
1833
1830
|
}
|
|
1834
1831
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1835
|
-
// update-settings / mark-read /
|
|
1832
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1836
1833
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1837
1834
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1838
1835
|
async postJson(path, body, label) {
|
|
@@ -1901,23 +1898,22 @@ var AgentTransport = class {
|
|
|
1901
1898
|
return await res.json();
|
|
1902
1899
|
}
|
|
1903
1900
|
/**
|
|
1904
|
-
* Agent-API URL — resolves a `/ai
|
|
1905
|
-
* base
|
|
1901
|
+
* Agent-API URL — resolves a `/ai-agent/*` path against the resolved
|
|
1902
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1906
1903
|
*/
|
|
1907
1904
|
url(pathOrUrl) {
|
|
1908
1905
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1909
|
-
return `${this.opts.
|
|
1906
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1910
1907
|
}
|
|
1911
1908
|
/**
|
|
1912
1909
|
* Data-API variant of {@link url} — resolves a root-level path (content /
|
|
1913
|
-
* forms / submit-form / activity) against
|
|
1914
|
-
* (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1915
|
-
*
|
|
1916
|
-
* the main origin: `${baseUrl}/api/data`.
|
|
1910
|
+
* forms / submit-form / activity) against the resolved data module base.
|
|
1911
|
+
* Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1912
|
+
* CRM) pass through unchanged.
|
|
1917
1913
|
*/
|
|
1918
1914
|
dataUrl(pathOrUrl) {
|
|
1919
1915
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1920
|
-
return `${this.opts.dataBaseUrl
|
|
1916
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1921
1917
|
}
|
|
1922
1918
|
get fetchImpl() {
|
|
1923
1919
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1931,10 +1927,10 @@ function extensionFor(mimeType) {
|
|
|
1931
1927
|
if (mimeType.includes("wav")) return "wav";
|
|
1932
1928
|
return "bin";
|
|
1933
1929
|
}
|
|
1934
|
-
function
|
|
1930
|
+
function isHandshakeResponseShape(raw) {
|
|
1935
1931
|
if (!raw || typeof raw !== "object") return false;
|
|
1936
1932
|
const r = raw;
|
|
1937
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1933
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1938
1934
|
}
|
|
1939
1935
|
|
|
1940
1936
|
// src/stream/messages.ts
|
|
@@ -6449,6 +6445,14 @@ function App({ options, hostElement, bus }) {
|
|
|
6449
6445
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6450
6446
|
);
|
|
6451
6447
|
const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
|
|
6448
|
+
const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
|
|
6449
|
+
const activatedRef = useRef9(activated);
|
|
6450
|
+
activatedRef.current = activated;
|
|
6451
|
+
const pendingThreadRef = useRef9(false);
|
|
6452
|
+
const welcomeRef = useRef9(void 0);
|
|
6453
|
+
useEffect17(() => {
|
|
6454
|
+
if (isOpen) setActivated(true);
|
|
6455
|
+
}, [isOpen]);
|
|
6452
6456
|
const initialPanelApplied = useRef9(false);
|
|
6453
6457
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6454
6458
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6475,7 +6479,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6475
6479
|
const [formContext, setFormContext] = useState13({});
|
|
6476
6480
|
const [transport] = useState13(
|
|
6477
6481
|
() => new AgentTransport({
|
|
6478
|
-
|
|
6482
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6479
6483
|
dataBaseUrl: options.dataBaseUrl,
|
|
6480
6484
|
auth: createAuth({
|
|
6481
6485
|
publicKey: options.publicKey,
|
|
@@ -6578,7 +6582,46 @@ function App({ options, hostElement, bus }) {
|
|
|
6578
6582
|
[transport]
|
|
6579
6583
|
);
|
|
6580
6584
|
const connectGenRef = useRef9(0);
|
|
6581
|
-
const
|
|
6585
|
+
const loadThread = useCallback6(
|
|
6586
|
+
async (conversationId) => {
|
|
6587
|
+
const myGen = connectGenRef.current;
|
|
6588
|
+
const isStale = () => myGen !== connectGenRef.current;
|
|
6589
|
+
setLoadingMessages(true);
|
|
6590
|
+
try {
|
|
6591
|
+
const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
|
|
6592
|
+
if (isStale()) return;
|
|
6593
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6594
|
+
setFormMarkers(markers);
|
|
6595
|
+
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6596
|
+
(n) => n !== void 0
|
|
6597
|
+
);
|
|
6598
|
+
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6599
|
+
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6600
|
+
const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6601
|
+
const row = makeInstantWelcomeMessage(w);
|
|
6602
|
+
row.createdAt = welcomeAnchor;
|
|
6603
|
+
return row;
|
|
6604
|
+
});
|
|
6605
|
+
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6606
|
+
if (loaded.length || tail.length) {
|
|
6607
|
+
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6608
|
+
setCanSend(chat.canContinue ?? true);
|
|
6609
|
+
setSuggestions(chat.suggestions ?? []);
|
|
6610
|
+
} else {
|
|
6611
|
+
messagesSig.value = welcomeRows;
|
|
6612
|
+
}
|
|
6613
|
+
} catch (err) {
|
|
6614
|
+
if (isStale()) return;
|
|
6615
|
+
log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
|
|
6616
|
+
conversationIdSig.value = void 0;
|
|
6617
|
+
persistence.saveConversationId(void 0);
|
|
6618
|
+
} finally {
|
|
6619
|
+
if (!isStale()) setLoadingMessages(false);
|
|
6620
|
+
}
|
|
6621
|
+
},
|
|
6622
|
+
[transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
|
|
6623
|
+
);
|
|
6624
|
+
const runHandshake = useCallback6(
|
|
6582
6625
|
async ({ newConversation }) => {
|
|
6583
6626
|
const myGen = ++connectGenRef.current;
|
|
6584
6627
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6590,7 +6633,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6590
6633
|
}
|
|
6591
6634
|
let res;
|
|
6592
6635
|
try {
|
|
6593
|
-
res = await transport.
|
|
6636
|
+
res = await transport.handshake({
|
|
6594
6637
|
visitorId,
|
|
6595
6638
|
conversationId,
|
|
6596
6639
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6641,67 +6684,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6641
6684
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6642
6685
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6643
6686
|
}
|
|
6687
|
+
welcomeRef.current = res.welcome;
|
|
6644
6688
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6645
6689
|
if (isResume) {
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
const chatPromise = res.messages?.length ? Promise.resolve({
|
|
6649
|
-
conversationId: persistedChatId,
|
|
6650
|
-
canContinue: res.canContinue ?? true,
|
|
6651
|
-
messages: res.messages
|
|
6652
|
-
}) : transport.loadConversation(persistedChatId);
|
|
6653
|
-
const [chat, markers] = await Promise.all([chatPromise, fetchFormMarkers()]);
|
|
6654
|
-
if (isStale()) return;
|
|
6655
|
-
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6656
|
-
setFormMarkers(markers);
|
|
6657
|
-
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6658
|
-
(n) => n !== void 0
|
|
6659
|
-
);
|
|
6660
|
-
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6661
|
-
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6662
|
-
const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6663
|
-
const row = makeInstantWelcomeMessage(w);
|
|
6664
|
-
row.createdAt = welcomeAnchor;
|
|
6665
|
-
return row;
|
|
6666
|
-
});
|
|
6667
|
-
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6668
|
-
if (loaded.length || tail.length) {
|
|
6669
|
-
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6670
|
-
setCanSend(chat.canContinue ?? true);
|
|
6671
|
-
setSuggestions(chat.suggestions ?? []);
|
|
6672
|
-
} else {
|
|
6673
|
-
messagesSig.value = welcomeRows;
|
|
6674
|
-
}
|
|
6675
|
-
} catch (err) {
|
|
6676
|
-
if (isStale()) return;
|
|
6677
|
-
log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
|
|
6678
|
-
conversationIdSig.value = void 0;
|
|
6679
|
-
persistence.saveConversationId(void 0);
|
|
6680
|
-
} finally {
|
|
6681
|
-
if (!isStale()) setLoadingMessages(false);
|
|
6682
|
-
}
|
|
6690
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6691
|
+
else pendingThreadRef.current = true;
|
|
6683
6692
|
} else {
|
|
6684
6693
|
conversationIdSig.value = res.conversationId;
|
|
6685
6694
|
persistence.saveConversationId(res.conversationId);
|
|
6686
6695
|
playWelcome(res.welcome?.messages);
|
|
6687
6696
|
}
|
|
6688
6697
|
if (!newConversation) {
|
|
6689
|
-
bus.emit("
|
|
6698
|
+
bus.emit("handshake", res);
|
|
6690
6699
|
setConversationReady(true);
|
|
6691
6700
|
}
|
|
6692
6701
|
},
|
|
6693
|
-
[
|
|
6694
|
-
transport,
|
|
6695
|
-
visitorId,
|
|
6696
|
-
persistence,
|
|
6697
|
-
options,
|
|
6698
|
-
bus,
|
|
6699
|
-
messagesSig,
|
|
6700
|
-
conversationIdSig,
|
|
6701
|
-
feedback,
|
|
6702
|
-
playWelcome,
|
|
6703
|
-
fetchFormMarkers
|
|
6704
|
-
]
|
|
6702
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6705
6703
|
);
|
|
6706
6704
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6707
6705
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6754,8 +6752,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6754
6752
|
},
|
|
6755
6753
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6756
6754
|
);
|
|
6755
|
+
const resumeHandleRef = useRef9(null);
|
|
6757
6756
|
useEffect17(() => {
|
|
6758
|
-
|
|
6757
|
+
const persistedConversation = conversationIdSig.value;
|
|
6758
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6759
|
+
void runHandshake({ newConversation: false });
|
|
6760
|
+
return () => {
|
|
6761
|
+
connectGenRef.current++;
|
|
6762
|
+
resumeHandleRef.current?.cancel();
|
|
6763
|
+
};
|
|
6764
|
+
}, []);
|
|
6765
|
+
useEffect17(() => {
|
|
6766
|
+
if (!activated) return;
|
|
6759
6767
|
void (async () => {
|
|
6760
6768
|
try {
|
|
6761
6769
|
const res = await transport.listForms();
|
|
@@ -6766,24 +6774,23 @@ function App({ options, hostElement, bus }) {
|
|
|
6766
6774
|
setFormsReady(true);
|
|
6767
6775
|
}
|
|
6768
6776
|
})();
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
resume = transport.resumeStream();
|
|
6774
|
-
void runResume(resume);
|
|
6777
|
+
if (pendingThreadRef.current) {
|
|
6778
|
+
pendingThreadRef.current = false;
|
|
6779
|
+
const persisted = conversationIdSig.value;
|
|
6780
|
+
if (persisted) void loadThread(persisted);
|
|
6775
6781
|
}
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6782
|
+
if (conversationIdSig.value) {
|
|
6783
|
+
const handle = transport.resumeStream();
|
|
6784
|
+
resumeHandleRef.current = handle;
|
|
6785
|
+
void runResume(handle);
|
|
6786
|
+
}
|
|
6787
|
+
}, [activated]);
|
|
6781
6788
|
const lastUserSig = useRef9(userSig);
|
|
6782
6789
|
useEffect17(() => {
|
|
6783
6790
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6784
6791
|
lastUserSig.current = userSig;
|
|
6785
|
-
void
|
|
6786
|
-
}, [userSig, conversationReady,
|
|
6792
|
+
void runHandshake({ newConversation: false });
|
|
6793
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6787
6794
|
useEffect17(() => {
|
|
6788
6795
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6789
6796
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -7010,10 +7017,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7010
7017
|
}, [transport, options.modules, unreadCountSig]);
|
|
7011
7018
|
const unreadSeeded = useRef9(false);
|
|
7012
7019
|
useEffect17(() => {
|
|
7013
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7020
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
7014
7021
|
unreadSeeded.current = true;
|
|
7015
7022
|
refreshUnread();
|
|
7016
|
-
}, [conversationReady, refreshUnread]);
|
|
7023
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
7017
7024
|
const handleOpen = useCallback6(() => {
|
|
7018
7025
|
log16.info("open", { mode: options.mode });
|
|
7019
7026
|
feedback.unlock();
|
|
@@ -7035,8 +7042,8 @@ function App({ options, hostElement, bus }) {
|
|
|
7035
7042
|
persistence.clearConversation();
|
|
7036
7043
|
setCanSend(true);
|
|
7037
7044
|
setFormMarkers([]);
|
|
7038
|
-
void
|
|
7039
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7045
|
+
void runHandshake({ newConversation: true });
|
|
7046
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
7040
7047
|
const handleNewChat = useCallback6(() => {
|
|
7041
7048
|
handleClear();
|
|
7042
7049
|
setView("chat");
|
|
@@ -7452,7 +7459,7 @@ var TRACKED = {
|
|
|
7452
7459
|
clear: () => void 0,
|
|
7453
7460
|
suggestion: () => void 0,
|
|
7454
7461
|
toggleHistory: (p33) => ({ view: p33.view }),
|
|
7455
|
-
|
|
7462
|
+
handshake: () => void 0,
|
|
7456
7463
|
// Forms + human-in-the-loop — ids and outcomes, never values.
|
|
7457
7464
|
formSubmit: (p33) => ({ formId: p33.formId, skipped: p33.skipped }),
|
|
7458
7465
|
toolResult: () => void 0,
|
|
@@ -7621,7 +7628,7 @@ var EVENT_NAMES = [
|
|
|
7621
7628
|
"calloutDismiss",
|
|
7622
7629
|
// Lifecycle / errors
|
|
7623
7630
|
"error",
|
|
7624
|
-
"
|
|
7631
|
+
"handshake",
|
|
7625
7632
|
"visitorRebound",
|
|
7626
7633
|
"conversationRebound"
|
|
7627
7634
|
];
|
|
@@ -7678,7 +7685,7 @@ function mount(opts) {
|
|
|
7678
7685
|
);
|
|
7679
7686
|
};
|
|
7680
7687
|
renderApp();
|
|
7681
|
-
bus.on("
|
|
7688
|
+
bus.on("handshake", (response) => {
|
|
7682
7689
|
if (!response.config) return;
|
|
7683
7690
|
rawOptions = mergeServerConfig(rawOptions, response.config);
|
|
7684
7691
|
currentOptions = resolveOptions(rawOptions);
|
package/package.json
CHANGED