@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/index.mjs
CHANGED
|
@@ -476,13 +476,13 @@ var DEFAULT_FEATURES = {
|
|
|
476
476
|
};
|
|
477
477
|
var DEFAULT_FORMS = { list: [], byTrigger: {} };
|
|
478
478
|
var DEFAULT_TRACKING = {
|
|
479
|
-
enabled:
|
|
479
|
+
enabled: true,
|
|
480
480
|
sampleRate: 1
|
|
481
481
|
};
|
|
482
482
|
var DEFAULT_ENDPOINTS = {
|
|
483
|
-
upload: "/
|
|
484
|
-
transcribe: "/
|
|
485
|
-
submitForm: "/submit-form"
|
|
483
|
+
upload: "/pai/upload-file",
|
|
484
|
+
transcribe: "/pai/transcribe-audio",
|
|
485
|
+
submitForm: "/pai/submit-form"
|
|
486
486
|
};
|
|
487
487
|
var DEFAULT_LAUNCHER = {
|
|
488
488
|
variant: "circle",
|
|
@@ -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),
|
|
@@ -613,9 +615,9 @@ function resolveTracking(overrides, dataBaseUrl) {
|
|
|
613
615
|
const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
|
|
614
616
|
return {
|
|
615
617
|
enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
|
|
616
|
-
// The collector rides the data module — `${dataBaseUrl}/elements
|
|
617
|
-
// (e.g. `https://help.ai/api/data/elements
|
|
618
|
-
endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements
|
|
618
|
+
// The collector rides the data module — `${dataBaseUrl}/pai/elements-px.gif`
|
|
619
|
+
// (e.g. `https://help.ai/api/data/pai/elements-px.gif`).
|
|
620
|
+
endpoint: overrides?.endpoint ?? `${dataBaseUrl}/pai/elements-px.gif`,
|
|
619
621
|
// Clamp instead of reject — a malformed rate must never turn a
|
|
620
622
|
// disabled tracker on or crash resolve (no Zod on the IIFE path).
|
|
621
623
|
sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
|
|
@@ -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,63 +1253,61 @@ function parseWireDate(value) {
|
|
|
1250
1253
|
}
|
|
1251
1254
|
var DEFAULT_PATHS = {
|
|
1252
1255
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1253
|
-
|
|
1256
|
+
handshake: "/pai/handshake",
|
|
1254
1257
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1255
|
-
streamMessage: "/
|
|
1258
|
+
streamMessage: "/pai/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: "/
|
|
1265
|
+
streamResume: "/pai/stream-resume",
|
|
1263
1266
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1264
|
-
cancelStream: "/
|
|
1267
|
+
cancelStream: "/pai/cancel-stream",
|
|
1265
1268
|
/** List the visitor's conversations. `GET`. */
|
|
1266
|
-
listConversations: "/
|
|
1267
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1268
|
-
listMessages: "/
|
|
1269
|
+
listConversations: "/pai/list-conversations",
|
|
1270
|
+
/** Load one conversation's thread (messages + canContinue + suggestions). `GET ?conversationId=…`. */
|
|
1271
|
+
listMessages: "/pai/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: "/
|
|
1278
|
+
updateSettings: "/pai/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: "/
|
|
1285
|
+
markRead: "/pai/mark-read",
|
|
1283
1286
|
// ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
|
|
1284
|
-
/** All widget content via one filtered endpoint. `GET /content?tags=…`. */
|
|
1285
|
-
content: "/content",
|
|
1286
1287
|
/**
|
|
1287
|
-
*
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1288
|
+
* The data module's one ACTIVATION read. `GET
|
|
1289
|
+
* /pai/bootstrap?visitorId=…&conversationId=…` →
|
|
1290
|
+
* `{ forms, formSubmissions }`: the deployment's resolved form
|
|
1291
|
+
* definitions plus the visitor's recorded submissions for the envelope's
|
|
1292
|
+
* conversation (chronological — they rebuild the timeline markers).
|
|
1293
|
+
* Also re-read with an explicit `conversationId` selector on a
|
|
1294
|
+
* history-pane thread switch. A failure is treated as "no forms, no
|
|
1295
|
+
* records" (non-fatal) — the chat works regardless.
|
|
1296
|
+
*/
|
|
1297
|
+
bootstrap: "/pai/bootstrap",
|
|
1298
|
+
/**
|
|
1299
|
+
* All widget content via one filtered endpoint — content stays OUT of the
|
|
1300
|
+
* bootstrap because its reads are query-driven (`tags` / `categories` /
|
|
1301
|
+
* `ids` / `search` + pagination). `GET /pai/content?tags=…`.
|
|
1291
1302
|
*/
|
|
1292
|
-
|
|
1303
|
+
content: "/pai/content",
|
|
1293
1304
|
/**
|
|
1294
1305
|
* Form submission (the event-driven forms engine). POST `{ visitorId,
|
|
1295
1306
|
* conversationId, formId, trigger, values, skipped? }` → record the form's answers
|
|
1296
1307
|
* immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
|
|
1297
1308
|
* Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
|
|
1298
1309
|
*/
|
|
1299
|
-
submitForm: "/submit-form"
|
|
1300
|
-
/**
|
|
1301
|
-
* Visitor interaction records for a conversation. `GET
|
|
1302
|
-
* /activity?visitorId=…&conversationId=…` →
|
|
1303
|
-
* `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
|
|
1304
|
-
* the former `formSubmissions` echo on start-conversation / list-messages;
|
|
1305
|
-
* deliberately generic — future visitor interaction records ride the same
|
|
1306
|
-
* endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
|
|
1307
|
-
* deployment doesn't resolve) is treated as "no records" (non-fatal).
|
|
1308
|
-
*/
|
|
1309
|
-
activity: "/activity"
|
|
1310
|
+
submitForm: "/pai/submit-form"
|
|
1310
1311
|
};
|
|
1311
1312
|
var CONTEXT_PARAM = "context";
|
|
1312
1313
|
function buildSendMessageRequest(params) {
|
|
@@ -1407,7 +1408,7 @@ function sleep(ms, signal7) {
|
|
|
1407
1408
|
var AgentTransport = class {
|
|
1408
1409
|
constructor(opts) {
|
|
1409
1410
|
__publicField(this, "opts", opts);
|
|
1410
|
-
// Identity adopted from the
|
|
1411
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1411
1412
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1412
1413
|
// (visitor, conversation) pair.
|
|
1413
1414
|
__publicField(this, "visitorId");
|
|
@@ -1430,9 +1431,9 @@ var AgentTransport = class {
|
|
|
1430
1431
|
}
|
|
1431
1432
|
/**
|
|
1432
1433
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1433
|
-
* `
|
|
1434
|
-
* with `
|
|
1435
|
-
* `conversationId` and the resume GET couldn't identify the thread. `
|
|
1434
|
+
* `handshake` resolves. A mount-time `resumeStream()` runs in parallel
|
|
1435
|
+
* with `handshake`, so without this its envelope would carry no
|
|
1436
|
+
* `conversationId` and the resume GET couldn't identify the thread. `handshake`
|
|
1436
1437
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1437
1438
|
*/
|
|
1438
1439
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1449,7 +1450,7 @@ var AgentTransport = class {
|
|
|
1449
1450
|
return out;
|
|
1450
1451
|
}
|
|
1451
1452
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1452
|
-
*
|
|
1453
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1453
1454
|
* differs from the active one. */
|
|
1454
1455
|
withEnvelope(body) {
|
|
1455
1456
|
return { ...this.envelope(), ...body };
|
|
@@ -1470,28 +1471,22 @@ var AgentTransport = class {
|
|
|
1470
1471
|
return url.toString();
|
|
1471
1472
|
}
|
|
1472
1473
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1473
|
-
async
|
|
1474
|
-
log4.debug("
|
|
1474
|
+
async handshake(body) {
|
|
1475
|
+
log4.debug("handshake \u2192", {
|
|
1475
1476
|
visitorId: body.visitorId,
|
|
1476
1477
|
conversationId: body.conversationId,
|
|
1477
1478
|
locale: body.locale
|
|
1478
1479
|
});
|
|
1479
|
-
const res = await this.postJson(
|
|
1480
|
-
|
|
1481
|
-
body,
|
|
1482
|
-
"startConversation"
|
|
1483
|
-
);
|
|
1484
|
-
log4.debug("startConversation \u2190", {
|
|
1480
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1481
|
+
log4.debug("handshake \u2190", {
|
|
1485
1482
|
visitorId: res.visitorId,
|
|
1486
1483
|
conversationId: res.conversationId,
|
|
1487
|
-
canContinue: res.canContinue,
|
|
1488
|
-
hasMessages: !!res.messages?.length,
|
|
1489
1484
|
hasWelcome: !!res.welcome,
|
|
1490
1485
|
hasConfig: !!res.config,
|
|
1491
1486
|
rebind: res.rebind
|
|
1492
1487
|
});
|
|
1493
|
-
if (!
|
|
1494
|
-
log4.warn("
|
|
1488
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1489
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1495
1490
|
}
|
|
1496
1491
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1497
1492
|
this.conversationId = res.conversationId;
|
|
@@ -1601,38 +1596,26 @@ var AgentTransport = class {
|
|
|
1601
1596
|
return { ...res, items };
|
|
1602
1597
|
}
|
|
1603
1598
|
/**
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
* (
|
|
1608
|
-
*
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
log4.debug("listForms \u2190", { count: forms.length });
|
|
1615
|
-
return { forms };
|
|
1616
|
-
}
|
|
1617
|
-
/**
|
|
1618
|
-
* Fetch the visitor's recorded activity for a conversation — currently the
|
|
1619
|
-
* form submissions that rebuild the collapsed "form submitted / skipped"
|
|
1620
|
-
* timeline markers on resume. `GET /activity` (data-API); the
|
|
1621
|
-
* envelope carries `visitorId` / `conversationId` as query params.
|
|
1622
|
-
* `conversationId` here is an optional resource selector (a thread that may
|
|
1623
|
-
* differ from the active conversation — the history-pane switch); when
|
|
1624
|
-
* omitted, the envelope's active conversation applies. Callers treat a
|
|
1625
|
-
* thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
|
|
1626
|
-
* "no activity" (non-fatal).
|
|
1599
|
+
* The data module's ONE activation read. `GET /pai/bootstrap` (data-API)
|
|
1600
|
+
* → the deployment's resolved form definitions + the visitor's recorded
|
|
1601
|
+
* form submissions for the envelope's conversation. The handshake
|
|
1602
|
+
* (agent-API) is completely independent of these surfaces.
|
|
1603
|
+
*
|
|
1604
|
+
* `conversationId` is an optional resource selector (a thread that may
|
|
1605
|
+
* differ from the active conversation — the history-pane switch re-reads
|
|
1606
|
+
* that thread's submissions); when omitted, the envelope's active
|
|
1607
|
+
* conversation applies. Callers treat a thrown StreamError (e.g. 404 on a
|
|
1608
|
+
* deployment that doesn't resolve) as "no forms, no records" (non-fatal).
|
|
1627
1609
|
*/
|
|
1628
|
-
async
|
|
1629
|
-
const url = new URL(this.dataUrl(DEFAULT_PATHS.
|
|
1610
|
+
async bootstrap(conversationId) {
|
|
1611
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.bootstrap));
|
|
1630
1612
|
if (conversationId) url.searchParams.set("conversationId", conversationId);
|
|
1631
|
-
log4.debug("
|
|
1632
|
-
const res = await this.getJson(url.toString(), "
|
|
1613
|
+
log4.debug("bootstrap \u2192", { conversationId: conversationId ?? this.conversationId });
|
|
1614
|
+
const res = await this.getJson(url.toString(), "bootstrap");
|
|
1615
|
+
const forms = res.forms ?? [];
|
|
1633
1616
|
const formSubmissions = res.formSubmissions ?? [];
|
|
1634
|
-
log4.debug("
|
|
1635
|
-
return { formSubmissions };
|
|
1617
|
+
log4.debug("bootstrap \u2190", { forms: forms.length, formSubmissions: formSubmissions.length });
|
|
1618
|
+
return { forms, formSubmissions };
|
|
1636
1619
|
}
|
|
1637
1620
|
async saveUserPrefs(userPrefs) {
|
|
1638
1621
|
log4.debug("saveUserPrefs \u2192", {
|
|
@@ -1645,10 +1628,10 @@ var AgentTransport = class {
|
|
|
1645
1628
|
return res;
|
|
1646
1629
|
}
|
|
1647
1630
|
get uploadPath() {
|
|
1648
|
-
return this.opts.endpoints?.upload ?? "/
|
|
1631
|
+
return this.opts.endpoints?.upload ?? "/pai/upload-file";
|
|
1649
1632
|
}
|
|
1650
1633
|
get transcribePath() {
|
|
1651
|
-
return this.opts.endpoints?.transcribe ?? "/
|
|
1634
|
+
return this.opts.endpoints?.transcribe ?? "/pai/transcribe-audio";
|
|
1652
1635
|
}
|
|
1653
1636
|
get submitFormPath() {
|
|
1654
1637
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
@@ -1688,9 +1671,9 @@ var AgentTransport = class {
|
|
|
1688
1671
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1689
1672
|
*
|
|
1690
1673
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1691
|
-
* `
|
|
1674
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1692
1675
|
* the user refreshed while the assistant was still streaming, the
|
|
1693
|
-
* `
|
|
1676
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1694
1677
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1695
1678
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1696
1679
|
* (the common case — no bubble is created).
|
|
@@ -1717,7 +1700,7 @@ var AgentTransport = class {
|
|
|
1717
1700
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1718
1701
|
* off first, the reply may still be spinning up server-side);
|
|
1719
1702
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1720
|
-
* parallel with
|
|
1703
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1721
1704
|
* user reloaded keeps flowing).
|
|
1722
1705
|
*
|
|
1723
1706
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1726,7 +1709,7 @@ var AgentTransport = class {
|
|
|
1726
1709
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1727
1710
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1728
1711
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1729
|
-
* turn comes from the DB via
|
|
1712
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1730
1713
|
*
|
|
1731
1714
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1732
1715
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1791,7 +1774,7 @@ var AgentTransport = class {
|
|
|
1791
1774
|
}
|
|
1792
1775
|
return false;
|
|
1793
1776
|
}
|
|
1794
|
-
/** Abort + fire-and-forget POST to `/
|
|
1777
|
+
/** Abort + fire-and-forget POST to `/pai/cancel-stream`. Idempotent. */
|
|
1795
1778
|
cancelStream(ctrl, conversationId) {
|
|
1796
1779
|
if (ctrl.signal.aborted) return;
|
|
1797
1780
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1832,7 +1815,7 @@ var AgentTransport = class {
|
|
|
1832
1815
|
return url.toString();
|
|
1833
1816
|
}
|
|
1834
1817
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1835
|
-
// update-settings / mark-read /
|
|
1818
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1836
1819
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1837
1820
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1838
1821
|
async postJson(path, body, label) {
|
|
@@ -1901,23 +1884,22 @@ var AgentTransport = class {
|
|
|
1901
1884
|
return await res.json();
|
|
1902
1885
|
}
|
|
1903
1886
|
/**
|
|
1904
|
-
* Agent-API URL — resolves a `/
|
|
1905
|
-
* base
|
|
1887
|
+
* Agent-API URL — resolves a `/pai/*` path against the resolved
|
|
1888
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1906
1889
|
*/
|
|
1907
1890
|
url(pathOrUrl) {
|
|
1908
1891
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1909
|
-
return `${this.opts.
|
|
1892
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1910
1893
|
}
|
|
1911
1894
|
/**
|
|
1912
1895
|
* 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`.
|
|
1896
|
+
* forms / submit-form / activity) against the resolved data module base.
|
|
1897
|
+
* Absolute URLs (e.g. an `endpoints.submitForm` pointing straight at a
|
|
1898
|
+
* CRM) pass through unchanged.
|
|
1917
1899
|
*/
|
|
1918
1900
|
dataUrl(pathOrUrl) {
|
|
1919
1901
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1920
|
-
return `${this.opts.dataBaseUrl
|
|
1902
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1921
1903
|
}
|
|
1922
1904
|
get fetchImpl() {
|
|
1923
1905
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1931,10 +1913,10 @@ function extensionFor(mimeType) {
|
|
|
1931
1913
|
if (mimeType.includes("wav")) return "wav";
|
|
1932
1914
|
return "bin";
|
|
1933
1915
|
}
|
|
1934
|
-
function
|
|
1916
|
+
function isHandshakeResponseShape(raw) {
|
|
1935
1917
|
if (!raw || typeof raw !== "object") return false;
|
|
1936
1918
|
const r = raw;
|
|
1937
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1919
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1938
1920
|
}
|
|
1939
1921
|
|
|
1940
1922
|
// src/stream/messages.ts
|
|
@@ -6449,6 +6431,14 @@ function App({ options, hostElement, bus }) {
|
|
|
6449
6431
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6450
6432
|
);
|
|
6451
6433
|
const [isOpen, setIsOpen] = useState13(initialPanelRef.current.panelOpen);
|
|
6434
|
+
const [activated, setActivated] = useState13(initialPanelRef.current.panelOpen);
|
|
6435
|
+
const activatedRef = useRef9(activated);
|
|
6436
|
+
activatedRef.current = activated;
|
|
6437
|
+
const pendingThreadRef = useRef9(false);
|
|
6438
|
+
const welcomeRef = useRef9(void 0);
|
|
6439
|
+
useEffect17(() => {
|
|
6440
|
+
if (isOpen) setActivated(true);
|
|
6441
|
+
}, [isOpen]);
|
|
6452
6442
|
const initialPanelApplied = useRef9(false);
|
|
6453
6443
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6454
6444
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6475,7 +6465,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6475
6465
|
const [formContext, setFormContext] = useState13({});
|
|
6476
6466
|
const [transport] = useState13(
|
|
6477
6467
|
() => new AgentTransport({
|
|
6478
|
-
|
|
6468
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6479
6469
|
dataBaseUrl: options.dataBaseUrl,
|
|
6480
6470
|
auth: createAuth({
|
|
6481
6471
|
publicKey: options.publicKey,
|
|
@@ -6557,10 +6547,11 @@ function App({ options, hostElement, bus }) {
|
|
|
6557
6547
|
},
|
|
6558
6548
|
[messagesSig]
|
|
6559
6549
|
);
|
|
6550
|
+
const dataBootRef = useRef9(null);
|
|
6560
6551
|
const fetchFormMarkers = useCallback6(
|
|
6561
6552
|
async (conversationId) => {
|
|
6562
6553
|
try {
|
|
6563
|
-
const { formSubmissions: subs } = await transport.
|
|
6554
|
+
const { formSubmissions: subs } = await (conversationId ? transport.bootstrap(conversationId) : dataBootRef.current ?? transport.bootstrap());
|
|
6564
6555
|
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6565
6556
|
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6566
6557
|
return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
@@ -6571,14 +6562,53 @@ function App({ options, hostElement, bus }) {
|
|
|
6571
6562
|
values: rec.values
|
|
6572
6563
|
}));
|
|
6573
6564
|
} catch (err) {
|
|
6574
|
-
log16.debug("
|
|
6565
|
+
log16.debug("bootstrap failed \u2014 no form markers (non-fatal)", { err });
|
|
6575
6566
|
return [];
|
|
6576
6567
|
}
|
|
6577
6568
|
},
|
|
6578
6569
|
[transport]
|
|
6579
6570
|
);
|
|
6580
6571
|
const connectGenRef = useRef9(0);
|
|
6581
|
-
const
|
|
6572
|
+
const loadThread = useCallback6(
|
|
6573
|
+
async (conversationId) => {
|
|
6574
|
+
const myGen = connectGenRef.current;
|
|
6575
|
+
const isStale = () => myGen !== connectGenRef.current;
|
|
6576
|
+
setLoadingMessages(true);
|
|
6577
|
+
try {
|
|
6578
|
+
const [chat, markers] = await Promise.all([transport.loadConversation(conversationId), fetchFormMarkers()]);
|
|
6579
|
+
if (isStale()) return;
|
|
6580
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6581
|
+
setFormMarkers(markers);
|
|
6582
|
+
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6583
|
+
(n) => n !== void 0
|
|
6584
|
+
);
|
|
6585
|
+
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6586
|
+
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6587
|
+
const welcomeRows = (welcomeRef.current?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6588
|
+
const row = makeInstantWelcomeMessage(w);
|
|
6589
|
+
row.createdAt = welcomeAnchor;
|
|
6590
|
+
return row;
|
|
6591
|
+
});
|
|
6592
|
+
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6593
|
+
if (loaded.length || tail.length) {
|
|
6594
|
+
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6595
|
+
setCanSend(chat.canContinue ?? true);
|
|
6596
|
+
setSuggestions(chat.suggestions ?? []);
|
|
6597
|
+
} else {
|
|
6598
|
+
messagesSig.value = welcomeRows;
|
|
6599
|
+
}
|
|
6600
|
+
} catch (err) {
|
|
6601
|
+
if (isStale()) return;
|
|
6602
|
+
log16.warn("loadThread failed; resetting conversationId", { err, conversationId });
|
|
6603
|
+
conversationIdSig.value = void 0;
|
|
6604
|
+
persistence.saveConversationId(void 0);
|
|
6605
|
+
} finally {
|
|
6606
|
+
if (!isStale()) setLoadingMessages(false);
|
|
6607
|
+
}
|
|
6608
|
+
},
|
|
6609
|
+
[transport, fetchFormMarkers, messagesSig, conversationIdSig, persistence]
|
|
6610
|
+
);
|
|
6611
|
+
const runHandshake = useCallback6(
|
|
6582
6612
|
async ({ newConversation }) => {
|
|
6583
6613
|
const myGen = ++connectGenRef.current;
|
|
6584
6614
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6590,7 +6620,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6590
6620
|
}
|
|
6591
6621
|
let res;
|
|
6592
6622
|
try {
|
|
6593
|
-
res = await transport.
|
|
6623
|
+
res = await transport.handshake({
|
|
6594
6624
|
visitorId,
|
|
6595
6625
|
conversationId,
|
|
6596
6626
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6641,67 +6671,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6641
6671
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6642
6672
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6643
6673
|
}
|
|
6674
|
+
welcomeRef.current = res.welcome;
|
|
6644
6675
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6645
6676
|
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
|
-
}
|
|
6677
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6678
|
+
else pendingThreadRef.current = true;
|
|
6683
6679
|
} else {
|
|
6684
6680
|
conversationIdSig.value = res.conversationId;
|
|
6685
6681
|
persistence.saveConversationId(res.conversationId);
|
|
6686
6682
|
playWelcome(res.welcome?.messages);
|
|
6687
6683
|
}
|
|
6688
6684
|
if (!newConversation) {
|
|
6689
|
-
bus.emit("
|
|
6685
|
+
bus.emit("handshake", res);
|
|
6690
6686
|
setConversationReady(true);
|
|
6691
6687
|
}
|
|
6692
6688
|
},
|
|
6693
|
-
[
|
|
6694
|
-
transport,
|
|
6695
|
-
visitorId,
|
|
6696
|
-
persistence,
|
|
6697
|
-
options,
|
|
6698
|
-
bus,
|
|
6699
|
-
messagesSig,
|
|
6700
|
-
conversationIdSig,
|
|
6701
|
-
feedback,
|
|
6702
|
-
playWelcome,
|
|
6703
|
-
fetchFormMarkers
|
|
6704
|
-
]
|
|
6689
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6705
6690
|
);
|
|
6706
6691
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6707
6692
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6754,36 +6739,46 @@ function App({ options, hostElement, bus }) {
|
|
|
6754
6739
|
},
|
|
6755
6740
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6756
6741
|
);
|
|
6742
|
+
const resumeHandleRef = useRef9(null);
|
|
6743
|
+
useEffect17(() => {
|
|
6744
|
+
const persistedConversation = conversationIdSig.value;
|
|
6745
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6746
|
+
void runHandshake({ newConversation: false });
|
|
6747
|
+
return () => {
|
|
6748
|
+
connectGenRef.current++;
|
|
6749
|
+
resumeHandleRef.current?.cancel();
|
|
6750
|
+
};
|
|
6751
|
+
}, []);
|
|
6757
6752
|
useEffect17(() => {
|
|
6758
|
-
|
|
6753
|
+
if (!activated) return;
|
|
6754
|
+
dataBootRef.current = transport.bootstrap();
|
|
6759
6755
|
void (async () => {
|
|
6760
6756
|
try {
|
|
6761
|
-
const res = await
|
|
6762
|
-
setRemoteForms(resolveForms(res.forms));
|
|
6757
|
+
const res = await dataBootRef.current;
|
|
6758
|
+
setRemoteForms(resolveForms((res ?? { forms: [] }).forms));
|
|
6763
6759
|
} catch (err) {
|
|
6764
|
-
log16.debug("
|
|
6760
|
+
log16.debug("bootstrap failed \u2014 treating as no forms", { err });
|
|
6765
6761
|
} finally {
|
|
6766
6762
|
setFormsReady(true);
|
|
6767
6763
|
}
|
|
6768
6764
|
})();
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
resume = transport.resumeStream();
|
|
6774
|
-
void runResume(resume);
|
|
6765
|
+
if (pendingThreadRef.current) {
|
|
6766
|
+
pendingThreadRef.current = false;
|
|
6767
|
+
const persisted = conversationIdSig.value;
|
|
6768
|
+
if (persisted) void loadThread(persisted);
|
|
6775
6769
|
}
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6770
|
+
if (conversationIdSig.value) {
|
|
6771
|
+
const handle = transport.resumeStream();
|
|
6772
|
+
resumeHandleRef.current = handle;
|
|
6773
|
+
void runResume(handle);
|
|
6774
|
+
}
|
|
6775
|
+
}, [activated]);
|
|
6781
6776
|
const lastUserSig = useRef9(userSig);
|
|
6782
6777
|
useEffect17(() => {
|
|
6783
6778
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6784
6779
|
lastUserSig.current = userSig;
|
|
6785
|
-
void
|
|
6786
|
-
}, [userSig, conversationReady,
|
|
6780
|
+
void runHandshake({ newConversation: false });
|
|
6781
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6787
6782
|
useEffect17(() => {
|
|
6788
6783
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6789
6784
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -7010,10 +7005,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7010
7005
|
}, [transport, options.modules, unreadCountSig]);
|
|
7011
7006
|
const unreadSeeded = useRef9(false);
|
|
7012
7007
|
useEffect17(() => {
|
|
7013
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7008
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
7014
7009
|
unreadSeeded.current = true;
|
|
7015
7010
|
refreshUnread();
|
|
7016
|
-
}, [conversationReady, refreshUnread]);
|
|
7011
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
7017
7012
|
const handleOpen = useCallback6(() => {
|
|
7018
7013
|
log16.info("open", { mode: options.mode });
|
|
7019
7014
|
feedback.unlock();
|
|
@@ -7035,8 +7030,8 @@ function App({ options, hostElement, bus }) {
|
|
|
7035
7030
|
persistence.clearConversation();
|
|
7036
7031
|
setCanSend(true);
|
|
7037
7032
|
setFormMarkers([]);
|
|
7038
|
-
void
|
|
7039
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7033
|
+
void runHandshake({ newConversation: true });
|
|
7034
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
7040
7035
|
const handleNewChat = useCallback6(() => {
|
|
7041
7036
|
handleClear();
|
|
7042
7037
|
setView("chat");
|
|
@@ -7147,6 +7142,20 @@ function App({ options, hostElement, bus }) {
|
|
|
7147
7142
|
},
|
|
7148
7143
|
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
|
|
7149
7144
|
);
|
|
7145
|
+
const pendingOpenFormRef = useRef9(null);
|
|
7146
|
+
const openForm = useCallback6(
|
|
7147
|
+
(id) => {
|
|
7148
|
+
if (typeof id !== "string") return;
|
|
7149
|
+
if (formsReady) forms.fire("manual", id);
|
|
7150
|
+
else pendingOpenFormRef.current = id;
|
|
7151
|
+
},
|
|
7152
|
+
[formsReady, forms]
|
|
7153
|
+
);
|
|
7154
|
+
useEffect17(() => {
|
|
7155
|
+
if (!formsReady || !pendingOpenFormRef.current) return;
|
|
7156
|
+
forms.fire("manual", pendingOpenFormRef.current);
|
|
7157
|
+
pendingOpenFormRef.current = null;
|
|
7158
|
+
}, [formsReady, forms]);
|
|
7150
7159
|
useEffect17(() => {
|
|
7151
7160
|
const unsub = bindHostCommands(hostElement, {
|
|
7152
7161
|
open: handleOpen,
|
|
@@ -7154,10 +7163,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7154
7163
|
expand: handleExpand,
|
|
7155
7164
|
fullscreen: handleFullscreen,
|
|
7156
7165
|
popout: handlePopOut,
|
|
7157
|
-
openForm
|
|
7166
|
+
openForm
|
|
7158
7167
|
});
|
|
7159
7168
|
return unsub;
|
|
7160
|
-
}, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut,
|
|
7169
|
+
}, [hostElement, handleOpen, handleClose, handleExpand, handleFullscreen, handlePopOut, openForm]);
|
|
7161
7170
|
const effectiveOptions = useMemo3(
|
|
7162
7171
|
() => applyOptionOverrides(options, activeLocale, activeThemeMode, activeTextSize),
|
|
7163
7172
|
[options, activeLocale, activeThemeMode, activeTextSize]
|
|
@@ -7452,7 +7461,7 @@ var TRACKED = {
|
|
|
7452
7461
|
clear: () => void 0,
|
|
7453
7462
|
suggestion: () => void 0,
|
|
7454
7463
|
toggleHistory: (p33) => ({ view: p33.view }),
|
|
7455
|
-
|
|
7464
|
+
handshake: () => void 0,
|
|
7456
7465
|
// Forms + human-in-the-loop — ids and outcomes, never values.
|
|
7457
7466
|
formSubmit: (p33) => ({ formId: p33.formId, skipped: p33.skipped }),
|
|
7458
7467
|
toolResult: () => void 0,
|
|
@@ -7621,7 +7630,7 @@ var EVENT_NAMES = [
|
|
|
7621
7630
|
"calloutDismiss",
|
|
7622
7631
|
// Lifecycle / errors
|
|
7623
7632
|
"error",
|
|
7624
|
-
"
|
|
7633
|
+
"handshake",
|
|
7625
7634
|
"visitorRebound",
|
|
7626
7635
|
"conversationRebound"
|
|
7627
7636
|
];
|
|
@@ -7678,7 +7687,7 @@ function mount(opts) {
|
|
|
7678
7687
|
);
|
|
7679
7688
|
};
|
|
7680
7689
|
renderApp();
|
|
7681
|
-
bus.on("
|
|
7690
|
+
bus.on("handshake", (response) => {
|
|
7682
7691
|
if (!response.config) return;
|
|
7683
7692
|
rawOptions = mergeServerConfig(rawOptions, response.config);
|
|
7684
7693
|
currentOptions = resolveOptions(rawOptions);
|