@helpai/elements 0.24.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 +3 -3
- package/configurator.mjs +27 -33
- package/elements-web-component.esm.js +27 -27
- package/elements-web-component.esm.js.map +4 -4
- package/elements.cjs.js +27 -27
- package/elements.cjs.js.map +4 -4
- package/elements.esm.js +27 -27
- package/elements.esm.js.map +4 -4
- package/elements.js +27 -27
- package/elements.js.map +4 -4
- package/index.d.ts +67 -119
- package/index.mjs +270 -144
- package/package.json +1 -1
- package/schema.d.ts +51 -51
- package/schema.json +36 -417
- package/schema.mjs +29 -35
- package/web-component.d.ts +1 -1
- package/web-component.mjs +270 -144
package/web-component.mjs
CHANGED
|
@@ -19,7 +19,12 @@ var BRAND = {
|
|
|
19
19
|
// and @goai builds.
|
|
20
20
|
tagName: true ? "web-ai-chat" : "web-ai-chat",
|
|
21
21
|
cssPrefix: true ? "helpai" : "helpai",
|
|
22
|
-
|
|
22
|
+
/**
|
|
23
|
+
* MAIN site origin (e.g. `https://help.ai`) — module bases derive from it:
|
|
24
|
+
* agent API at `${defaultBaseUrl}/api/agent`, data API at
|
|
25
|
+
* `${defaultBaseUrl}/api/data`.
|
|
26
|
+
*/
|
|
27
|
+
defaultBaseUrl: true ? "https://help.ai" : "",
|
|
23
28
|
standaloneUrl: true ? "https://help.ai/chat" : "",
|
|
24
29
|
/** Public-facing brand label, e.g. "Help AI" / "Go AI". */
|
|
25
30
|
displayName: true ? "Help AI" : "Help AI",
|
|
@@ -457,13 +462,12 @@ var DEFAULT_FEATURES = {
|
|
|
457
462
|
var DEFAULT_FORMS = { list: [], byTrigger: {} };
|
|
458
463
|
var DEFAULT_TRACKING = {
|
|
459
464
|
enabled: false,
|
|
460
|
-
endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
|
|
461
465
|
sampleRate: 1
|
|
462
466
|
};
|
|
463
467
|
var DEFAULT_ENDPOINTS = {
|
|
464
|
-
upload: "/ai
|
|
465
|
-
transcribe: "/ai
|
|
466
|
-
submitForm: "/
|
|
468
|
+
upload: "/ai-agent/upload-file",
|
|
469
|
+
transcribe: "/ai-agent/transcribe-audio",
|
|
470
|
+
submitForm: "/submit-form"
|
|
467
471
|
};
|
|
468
472
|
var DEFAULT_LAUNCHER = {
|
|
469
473
|
variant: "circle",
|
|
@@ -505,11 +509,16 @@ function resolveOptions(rawOpts) {
|
|
|
505
509
|
const mode = presentation.mode ?? "floating";
|
|
506
510
|
const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
|
|
507
511
|
const availableLocales = i18n.availableLocales ?? [locale];
|
|
512
|
+
const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
|
|
513
|
+
const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
|
|
514
|
+
const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
|
|
508
515
|
return {
|
|
509
516
|
widgetId: opts.widgetId ?? "default",
|
|
510
517
|
publicKey: opts.publicKey,
|
|
511
518
|
aiAgentDeploymentId: opts.aiAgentDeploymentId,
|
|
512
|
-
baseUrl
|
|
519
|
+
baseUrl,
|
|
520
|
+
agentBaseUrl,
|
|
521
|
+
dataBaseUrl,
|
|
513
522
|
userContext: sanitizeUserContext(opts.userContext),
|
|
514
523
|
pageContext: sanitizePageContext(opts.pageContext),
|
|
515
524
|
position: presentation.position ?? "bottom-right",
|
|
@@ -554,7 +563,7 @@ function resolveOptions(rawOpts) {
|
|
|
554
563
|
poweredBy: resolvePoweredBy(footer.poweredBy),
|
|
555
564
|
composer: resolveComposer(opts.composer),
|
|
556
565
|
modules: resolveModules(opts.modules),
|
|
557
|
-
tracking: resolveTracking(opts.tracking),
|
|
566
|
+
tracking: resolveTracking(opts.tracking, dataBaseUrl),
|
|
558
567
|
storage: opts.storage ?? defaultStorage,
|
|
559
568
|
onMessage: opts.onMessage,
|
|
560
569
|
onOpen: opts.onOpen,
|
|
@@ -587,11 +596,13 @@ function resolveHaptics(overrides) {
|
|
|
587
596
|
events: overrides?.events
|
|
588
597
|
};
|
|
589
598
|
}
|
|
590
|
-
function resolveTracking(overrides) {
|
|
599
|
+
function resolveTracking(overrides, dataBaseUrl) {
|
|
591
600
|
const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
|
|
592
601
|
return {
|
|
593
602
|
enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
|
|
594
|
-
|
|
603
|
+
// The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
|
|
604
|
+
// (e.g. `https://help.ai/api/data/elements/px.gif`).
|
|
605
|
+
endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
|
|
595
606
|
// Clamp instead of reject — a malformed rate must never turn a
|
|
596
607
|
// disabled tracker on or crash resolve (no Zod on the IIFE path).
|
|
597
608
|
sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
|
|
@@ -760,7 +771,7 @@ function mergeServerConfig(user, server) {
|
|
|
760
771
|
out[key] = mergeLeaves(sv, uv);
|
|
761
772
|
}
|
|
762
773
|
if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
|
|
763
|
-
if (server.
|
|
774
|
+
if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
|
|
764
775
|
const userI18n = user.i18n;
|
|
765
776
|
const serverI18n = server.i18n;
|
|
766
777
|
if (userI18n || serverI18n) {
|
|
@@ -820,6 +831,8 @@ var EMBED_SCALAR_ATTRS = [
|
|
|
820
831
|
["public-key", "publicKey"],
|
|
821
832
|
["ai-agent-deployment-id", "aiAgentDeploymentId"],
|
|
822
833
|
["base-url", "baseUrl"],
|
|
834
|
+
["agent-base-url", "agentBaseUrl"],
|
|
835
|
+
["data-base-url", "dataBaseUrl"],
|
|
823
836
|
["theme", "theme", (v) => v]
|
|
824
837
|
];
|
|
825
838
|
var PRESENTATION_ATTRS = [
|
|
@@ -967,7 +980,7 @@ function parseLauncher(get, str2) {
|
|
|
967
980
|
return Object.keys(base).length > 0 ? base : null;
|
|
968
981
|
}
|
|
969
982
|
|
|
970
|
-
// src/core/
|
|
983
|
+
// src/core/handshake-shape.ts
|
|
971
984
|
function isPlainObject(raw) {
|
|
972
985
|
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
973
986
|
}
|
|
@@ -1295,46 +1308,63 @@ function parseWireDate(value) {
|
|
|
1295
1308
|
}
|
|
1296
1309
|
var DEFAULT_PATHS = {
|
|
1297
1310
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1298
|
-
|
|
1311
|
+
handshake: "/ai-agent/handshake",
|
|
1299
1312
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1300
|
-
streamMessage: "/ai
|
|
1313
|
+
streamMessage: "/ai-agent/stream-message",
|
|
1301
1314
|
/**
|
|
1302
1315
|
* Resume an interrupted reply. `GET` (envelope rides as query params).
|
|
1303
1316
|
* Stateless: the server re-streams the whole reply SSE again from the start —
|
|
1304
1317
|
* there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
|
|
1305
1318
|
* replayed events are skipped and only the continuation surfaces.
|
|
1306
1319
|
*/
|
|
1307
|
-
streamResume: "/ai
|
|
1320
|
+
streamResume: "/ai-agent/stream-resume",
|
|
1308
1321
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1309
|
-
cancelStream: "/ai
|
|
1322
|
+
cancelStream: "/ai-agent/cancel-stream",
|
|
1310
1323
|
/** List the visitor's conversations. `GET`. */
|
|
1311
|
-
listConversations: "/ai
|
|
1312
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1313
|
-
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",
|
|
1314
1327
|
/**
|
|
1315
1328
|
* Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
|
|
1316
1329
|
* server persists, returns the merged authoritative copy. Fire-and-forget on
|
|
1317
1330
|
* the client — a failure leaves the local cache valid; the next
|
|
1318
|
-
* `
|
|
1331
|
+
* `handshake()` reconciles.
|
|
1319
1332
|
*/
|
|
1320
|
-
updateSettings: "/ai
|
|
1333
|
+
updateSettings: "/ai-agent/update-settings",
|
|
1321
1334
|
/**
|
|
1322
1335
|
* Mark a conversation read. POST `{ visitorId, conversationId }` → the server
|
|
1323
1336
|
* records `lastReadAt = now` for that (visitor, conversation) and recomputes
|
|
1324
1337
|
* `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
|
|
1325
1338
|
* on the client; a failure just leaves the badge until the next sync.
|
|
1326
1339
|
*/
|
|
1327
|
-
markRead: "/ai
|
|
1328
|
-
// ──
|
|
1329
|
-
/** All widget content via one filtered endpoint. `GET /
|
|
1330
|
-
content: "/
|
|
1340
|
+
markRead: "/ai-agent/mark-read",
|
|
1341
|
+
// ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
|
|
1342
|
+
/** All widget content via one filtered endpoint. `GET /content?tags=…`. */
|
|
1343
|
+
content: "/content",
|
|
1344
|
+
/**
|
|
1345
|
+
* Resolved form definitions for the deployment. `GET /forms` →
|
|
1346
|
+
* `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
|
|
1347
|
+
* handshake); form gating waits for it. A failure is treated as
|
|
1348
|
+
* "no forms" (non-fatal).
|
|
1349
|
+
*/
|
|
1350
|
+
forms: "/forms",
|
|
1331
1351
|
/**
|
|
1332
1352
|
* Form submission (the event-driven forms engine). POST `{ visitorId,
|
|
1333
1353
|
* conversationId, formId, trigger, values, skipped? }` → record the form's answers
|
|
1334
1354
|
* immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
|
|
1335
1355
|
* Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
|
|
1336
1356
|
*/
|
|
1337
|
-
submitForm: "/
|
|
1357
|
+
submitForm: "/submit-form",
|
|
1358
|
+
/**
|
|
1359
|
+
* Visitor interaction records for a conversation. `GET
|
|
1360
|
+
* /activity?visitorId=…&conversationId=…` →
|
|
1361
|
+
* `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
|
|
1362
|
+
* the former `formSubmissions` echo on the handshake / list-messages;
|
|
1363
|
+
* deliberately generic — future visitor interaction records ride the same
|
|
1364
|
+
* endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
|
|
1365
|
+
* deployment doesn't resolve) is treated as "no records" (non-fatal).
|
|
1366
|
+
*/
|
|
1367
|
+
activity: "/activity"
|
|
1338
1368
|
};
|
|
1339
1369
|
var CONTEXT_PARAM = "context";
|
|
1340
1370
|
function buildSendMessageRequest(params) {
|
|
@@ -1435,7 +1465,7 @@ function sleep(ms, signal7) {
|
|
|
1435
1465
|
var AgentTransport = class {
|
|
1436
1466
|
constructor(opts) {
|
|
1437
1467
|
__publicField(this, "opts", opts);
|
|
1438
|
-
// Identity adopted from the
|
|
1468
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1439
1469
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1440
1470
|
// (visitor, conversation) pair.
|
|
1441
1471
|
__publicField(this, "visitorId");
|
|
@@ -1458,9 +1488,9 @@ var AgentTransport = class {
|
|
|
1458
1488
|
}
|
|
1459
1489
|
/**
|
|
1460
1490
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1461
|
-
* `
|
|
1462
|
-
* with `
|
|
1463
|
-
* `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`
|
|
1464
1494
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1465
1495
|
*/
|
|
1466
1496
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1477,7 +1507,7 @@ var AgentTransport = class {
|
|
|
1477
1507
|
return out;
|
|
1478
1508
|
}
|
|
1479
1509
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1480
|
-
*
|
|
1510
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1481
1511
|
* differs from the active one. */
|
|
1482
1512
|
withEnvelope(body) {
|
|
1483
1513
|
return { ...this.envelope(), ...body };
|
|
@@ -1498,28 +1528,22 @@ var AgentTransport = class {
|
|
|
1498
1528
|
return url.toString();
|
|
1499
1529
|
}
|
|
1500
1530
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1501
|
-
async
|
|
1502
|
-
log4.debug("
|
|
1531
|
+
async handshake(body) {
|
|
1532
|
+
log4.debug("handshake \u2192", {
|
|
1503
1533
|
visitorId: body.visitorId,
|
|
1504
1534
|
conversationId: body.conversationId,
|
|
1505
1535
|
locale: body.locale
|
|
1506
1536
|
});
|
|
1507
|
-
const res = await this.postJson(
|
|
1508
|
-
|
|
1509
|
-
body,
|
|
1510
|
-
"startConversation"
|
|
1511
|
-
);
|
|
1512
|
-
log4.debug("startConversation \u2190", {
|
|
1537
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1538
|
+
log4.debug("handshake \u2190", {
|
|
1513
1539
|
visitorId: res.visitorId,
|
|
1514
1540
|
conversationId: res.conversationId,
|
|
1515
|
-
canContinue: res.canContinue,
|
|
1516
|
-
hasMessages: !!res.messages?.length,
|
|
1517
1541
|
hasWelcome: !!res.welcome,
|
|
1518
1542
|
hasConfig: !!res.config,
|
|
1519
1543
|
rebind: res.rebind
|
|
1520
1544
|
});
|
|
1521
|
-
if (!
|
|
1522
|
-
log4.warn("
|
|
1545
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1546
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1523
1547
|
}
|
|
1524
1548
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1525
1549
|
this.conversationId = res.conversationId;
|
|
@@ -1607,14 +1631,17 @@ var AgentTransport = class {
|
|
|
1607
1631
|
log4.debug("markRead failed (non-fatal)", { err });
|
|
1608
1632
|
}
|
|
1609
1633
|
}
|
|
1610
|
-
// ----
|
|
1634
|
+
// ---- Data API (content / forms — module-help-ai-data-api) --------------
|
|
1611
1635
|
//
|
|
1612
|
-
//
|
|
1613
|
-
//
|
|
1614
|
-
//
|
|
1615
|
-
|
|
1636
|
+
// The DATA surfaces live on `dataBaseUrl` (default `${baseUrl}/api/data`)
|
|
1637
|
+
// with ROOT-level paths — same auth headers + envelope, same retry
|
|
1638
|
+
// behavior. One endpoint for every content surface (Home / Help / News /
|
|
1639
|
+
// docs); callers pass filters; a thrown StreamError (e.g. 404 on an older
|
|
1640
|
+
// backend) is treated as "no content" by the module and the tab degrades
|
|
1641
|
+
// gracefully.
|
|
1642
|
+
/** Fetch content rows by filter. `GET /content` (data-API). */
|
|
1616
1643
|
async listContent(query = {}) {
|
|
1617
|
-
const url = new URL(this.
|
|
1644
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
|
|
1618
1645
|
for (const [key, value] of Object.entries(query)) {
|
|
1619
1646
|
if (value === void 0) continue;
|
|
1620
1647
|
url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
|
|
@@ -1625,6 +1652,40 @@ var AgentTransport = class {
|
|
|
1625
1652
|
log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
|
|
1626
1653
|
return { ...res, items };
|
|
1627
1654
|
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Fetch the deployment's resolved form definitions. `GET /forms`
|
|
1657
|
+
* (data-API). Replaces the former `config.forms` push on the handshake —
|
|
1658
|
+
* called once at boot, in parallel with `handshake`; form gating
|
|
1659
|
+
* (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
|
|
1660
|
+
* "no forms" (non-fatal).
|
|
1661
|
+
*/
|
|
1662
|
+
async listForms() {
|
|
1663
|
+
log4.debug("listForms \u2192");
|
|
1664
|
+
const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
|
|
1665
|
+
const forms = res.forms ?? [];
|
|
1666
|
+
log4.debug("listForms \u2190", { count: forms.length });
|
|
1667
|
+
return { forms };
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Fetch the visitor's recorded activity for a conversation — currently the
|
|
1671
|
+
* form submissions that rebuild the collapsed "form submitted / skipped"
|
|
1672
|
+
* timeline markers on resume. `GET /activity` (data-API); the
|
|
1673
|
+
* envelope carries `visitorId` / `conversationId` as query params.
|
|
1674
|
+
* `conversationId` here is an optional resource selector (a thread that may
|
|
1675
|
+
* differ from the active conversation — the history-pane switch); when
|
|
1676
|
+
* omitted, the envelope's active conversation applies. Callers treat a
|
|
1677
|
+
* thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
|
|
1678
|
+
* "no activity" (non-fatal).
|
|
1679
|
+
*/
|
|
1680
|
+
async getActivity(conversationId) {
|
|
1681
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
|
|
1682
|
+
if (conversationId) url.searchParams.set("conversationId", conversationId);
|
|
1683
|
+
log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
|
|
1684
|
+
const res = await this.getJson(url.toString(), "getActivity");
|
|
1685
|
+
const formSubmissions = res.formSubmissions ?? [];
|
|
1686
|
+
log4.debug("getActivity \u2190", { count: formSubmissions.length });
|
|
1687
|
+
return { formSubmissions };
|
|
1688
|
+
}
|
|
1628
1689
|
async saveUserPrefs(userPrefs) {
|
|
1629
1690
|
log4.debug("saveUserPrefs \u2192", {
|
|
1630
1691
|
visitorId: this.visitorId,
|
|
@@ -1636,17 +1697,18 @@ var AgentTransport = class {
|
|
|
1636
1697
|
return res;
|
|
1637
1698
|
}
|
|
1638
1699
|
get uploadPath() {
|
|
1639
|
-
return this.opts.endpoints?.upload ?? "/ai
|
|
1700
|
+
return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
|
|
1640
1701
|
}
|
|
1641
1702
|
get transcribePath() {
|
|
1642
|
-
return this.opts.endpoints?.transcribe ?? "/ai
|
|
1703
|
+
return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
|
|
1643
1704
|
}
|
|
1644
1705
|
get submitFormPath() {
|
|
1645
1706
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
1646
1707
|
}
|
|
1647
1708
|
/**
|
|
1648
|
-
* Record a completed form (intake, CSAT, claim, …).
|
|
1649
|
-
*
|
|
1709
|
+
* Record a completed form (intake, CSAT, claim, …). `POST
|
|
1710
|
+
* /submit-form` (data-API). Fire-and-forget — the record is
|
|
1711
|
+
* captured even if the visitor never sends a message. `conversationId`,
|
|
1650
1712
|
* `formId`, and `trigger` are explicit so the backend can tie + route;
|
|
1651
1713
|
* `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
|
|
1652
1714
|
* that doesn't implement the endpoint) is non-fatal, so callers don't await.
|
|
@@ -1654,7 +1716,7 @@ var AgentTransport = class {
|
|
|
1654
1716
|
async submitForm(body) {
|
|
1655
1717
|
log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
|
|
1656
1718
|
try {
|
|
1657
|
-
await this.postJson(this.submitFormPath, body, "submitForm");
|
|
1719
|
+
await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
|
|
1658
1720
|
} catch (err) {
|
|
1659
1721
|
log4.debug("submitForm failed (non-fatal)", { err });
|
|
1660
1722
|
}
|
|
@@ -1678,9 +1740,9 @@ var AgentTransport = class {
|
|
|
1678
1740
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1679
1741
|
*
|
|
1680
1742
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1681
|
-
* `
|
|
1743
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1682
1744
|
* the user refreshed while the assistant was still streaming, the
|
|
1683
|
-
* `
|
|
1745
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1684
1746
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1685
1747
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1686
1748
|
* (the common case — no bubble is created).
|
|
@@ -1707,7 +1769,7 @@ var AgentTransport = class {
|
|
|
1707
1769
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1708
1770
|
* off first, the reply may still be spinning up server-side);
|
|
1709
1771
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1710
|
-
* parallel with
|
|
1772
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1711
1773
|
* user reloaded keeps flowing).
|
|
1712
1774
|
*
|
|
1713
1775
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1716,7 +1778,7 @@ var AgentTransport = class {
|
|
|
1716
1778
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1717
1779
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1718
1780
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1719
|
-
* turn comes from the DB via
|
|
1781
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1720
1782
|
*
|
|
1721
1783
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1722
1784
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1781,7 +1843,7 @@ var AgentTransport = class {
|
|
|
1781
1843
|
}
|
|
1782
1844
|
return false;
|
|
1783
1845
|
}
|
|
1784
|
-
/** Abort + fire-and-forget POST to `/ai
|
|
1846
|
+
/** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
|
|
1785
1847
|
cancelStream(ctrl, conversationId) {
|
|
1786
1848
|
if (ctrl.signal.aborted) return;
|
|
1787
1849
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1822,7 +1884,7 @@ var AgentTransport = class {
|
|
|
1822
1884
|
return url.toString();
|
|
1823
1885
|
}
|
|
1824
1886
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1825
|
-
// update-settings / mark-read /
|
|
1887
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1826
1888
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1827
1889
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1828
1890
|
async postJson(path, body, label) {
|
|
@@ -1890,9 +1952,23 @@ var AgentTransport = class {
|
|
|
1890
1952
|
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
|
|
1891
1953
|
return await res.json();
|
|
1892
1954
|
}
|
|
1955
|
+
/**
|
|
1956
|
+
* Agent-API URL — resolves a `/ai-agent/*` path against the resolved
|
|
1957
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1958
|
+
*/
|
|
1893
1959
|
url(pathOrUrl) {
|
|
1894
1960
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1895
|
-
return `${this.opts.
|
|
1961
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1962
|
+
}
|
|
1963
|
+
/**
|
|
1964
|
+
* Data-API variant of {@link url} — resolves a root-level path (content /
|
|
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.
|
|
1968
|
+
*/
|
|
1969
|
+
dataUrl(pathOrUrl) {
|
|
1970
|
+
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1971
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1896
1972
|
}
|
|
1897
1973
|
get fetchImpl() {
|
|
1898
1974
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1906,10 +1982,10 @@ function extensionFor(mimeType) {
|
|
|
1906
1982
|
if (mimeType.includes("wav")) return "wav";
|
|
1907
1983
|
return "bin";
|
|
1908
1984
|
}
|
|
1909
|
-
function
|
|
1985
|
+
function isHandshakeResponseShape(raw) {
|
|
1910
1986
|
if (!raw || typeof raw !== "object") return false;
|
|
1911
1987
|
const r = raw;
|
|
1912
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1988
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1913
1989
|
}
|
|
1914
1990
|
|
|
1915
1991
|
// src/stream/messages.ts
|
|
@@ -6417,11 +6493,21 @@ function App({ options, hostElement, bus }) {
|
|
|
6417
6493
|
const chatTabIdRef = useRef9(void 0);
|
|
6418
6494
|
chatTabIdRef.current = chatTabId();
|
|
6419
6495
|
const [conversationReady, setConversationReady] = useState13(false);
|
|
6496
|
+
const [remoteForms, setRemoteForms] = useState13(null);
|
|
6497
|
+
const [formsReady, setFormsReady] = useState13(false);
|
|
6420
6498
|
const isInlineLike = options.mode === "standalone" || options.mode === "inline";
|
|
6421
6499
|
const initialPanelRef = useRef9(
|
|
6422
6500
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6423
6501
|
);
|
|
6424
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]);
|
|
6425
6511
|
const initialPanelApplied = useRef9(false);
|
|
6426
6512
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6427
6513
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6448,7 +6534,8 @@ function App({ options, hostElement, bus }) {
|
|
|
6448
6534
|
const [formContext, setFormContext] = useState13({});
|
|
6449
6535
|
const [transport] = useState13(
|
|
6450
6536
|
() => new AgentTransport({
|
|
6451
|
-
|
|
6537
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6538
|
+
dataBaseUrl: options.dataBaseUrl,
|
|
6452
6539
|
auth: createAuth({
|
|
6453
6540
|
publicKey: options.publicKey,
|
|
6454
6541
|
aiAgentDeploymentId: options.aiAgentDeploymentId,
|
|
@@ -6529,8 +6616,67 @@ function App({ options, hostElement, bus }) {
|
|
|
6529
6616
|
},
|
|
6530
6617
|
[messagesSig]
|
|
6531
6618
|
);
|
|
6619
|
+
const fetchFormMarkers = useCallback6(
|
|
6620
|
+
async (conversationId) => {
|
|
6621
|
+
try {
|
|
6622
|
+
const { formSubmissions: subs } = await transport.getActivity(conversationId);
|
|
6623
|
+
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6624
|
+
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6625
|
+
return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
6626
|
+
key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
|
|
6627
|
+
formId: rec.formId,
|
|
6628
|
+
outcome: rec.skipped ? "skipped" : "submitted",
|
|
6629
|
+
createdAt: parseWireDate(rec.createdAt),
|
|
6630
|
+
values: rec.values
|
|
6631
|
+
}));
|
|
6632
|
+
} catch (err) {
|
|
6633
|
+
log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
|
|
6634
|
+
return [];
|
|
6635
|
+
}
|
|
6636
|
+
},
|
|
6637
|
+
[transport]
|
|
6638
|
+
);
|
|
6532
6639
|
const connectGenRef = useRef9(0);
|
|
6533
|
-
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(
|
|
6534
6680
|
async ({ newConversation }) => {
|
|
6535
6681
|
const myGen = ++connectGenRef.current;
|
|
6536
6682
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6542,7 +6688,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6542
6688
|
}
|
|
6543
6689
|
let res;
|
|
6544
6690
|
try {
|
|
6545
|
-
res = await transport.
|
|
6691
|
+
res = await transport.handshake({
|
|
6546
6692
|
visitorId,
|
|
6547
6693
|
conversationId,
|
|
6548
6694
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6593,66 +6739,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6593
6739
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6594
6740
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6595
6741
|
}
|
|
6742
|
+
welcomeRef.current = res.welcome;
|
|
6596
6743
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6597
6744
|
if (isResume) {
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
const chat = res.messages?.length ? {
|
|
6601
|
-
conversationId: persistedChatId,
|
|
6602
|
-
canContinue: res.canContinue ?? true,
|
|
6603
|
-
messages: res.messages,
|
|
6604
|
-
formSubmissions: res.formSubmissions
|
|
6605
|
-
} : await transport.loadConversation(persistedChatId);
|
|
6606
|
-
if (isStale()) return;
|
|
6607
|
-
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6608
|
-
const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
|
|
6609
|
-
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6610
|
-
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6611
|
-
const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
6612
|
-
key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
|
|
6613
|
-
formId: rec.formId,
|
|
6614
|
-
outcome: rec.skipped ? "skipped" : "submitted",
|
|
6615
|
-
createdAt: parseWireDate(rec.createdAt),
|
|
6616
|
-
values: rec.values
|
|
6617
|
-
}));
|
|
6618
|
-
setFormMarkers(markers);
|
|
6619
|
-
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6620
|
-
(n) => n !== void 0
|
|
6621
|
-
);
|
|
6622
|
-
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6623
|
-
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6624
|
-
const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6625
|
-
const row = makeInstantWelcomeMessage(w);
|
|
6626
|
-
row.createdAt = welcomeAnchor;
|
|
6627
|
-
return row;
|
|
6628
|
-
});
|
|
6629
|
-
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6630
|
-
if (loaded.length || tail.length) {
|
|
6631
|
-
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6632
|
-
setCanSend(chat.canContinue ?? true);
|
|
6633
|
-
setSuggestions(chat.suggestions ?? []);
|
|
6634
|
-
} else {
|
|
6635
|
-
messagesSig.value = welcomeRows;
|
|
6636
|
-
}
|
|
6637
|
-
} catch (err) {
|
|
6638
|
-
if (isStale()) return;
|
|
6639
|
-
log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
|
|
6640
|
-
conversationIdSig.value = void 0;
|
|
6641
|
-
persistence.saveConversationId(void 0);
|
|
6642
|
-
} finally {
|
|
6643
|
-
if (!isStale()) setLoadingMessages(false);
|
|
6644
|
-
}
|
|
6745
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6746
|
+
else pendingThreadRef.current = true;
|
|
6645
6747
|
} else {
|
|
6646
6748
|
conversationIdSig.value = res.conversationId;
|
|
6647
6749
|
persistence.saveConversationId(res.conversationId);
|
|
6648
6750
|
playWelcome(res.welcome?.messages);
|
|
6649
6751
|
}
|
|
6650
6752
|
if (!newConversation) {
|
|
6651
|
-
bus.emit("
|
|
6753
|
+
bus.emit("handshake", res);
|
|
6652
6754
|
setConversationReady(true);
|
|
6653
6755
|
}
|
|
6654
6756
|
},
|
|
6655
|
-
[transport, visitorId, persistence, options, bus,
|
|
6757
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6656
6758
|
);
|
|
6657
6759
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6658
6760
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6705,26 +6807,45 @@ function App({ options, hostElement, bus }) {
|
|
|
6705
6807
|
},
|
|
6706
6808
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6707
6809
|
);
|
|
6810
|
+
const resumeHandleRef = useRef9(null);
|
|
6708
6811
|
useEffect17(() => {
|
|
6709
|
-
void runStartConversation({ newConversation: false });
|
|
6710
6812
|
const persistedConversation = conversationIdSig.value;
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
transport.primeIdentity(visitorId, persistedConversation);
|
|
6714
|
-
resume = transport.resumeStream();
|
|
6715
|
-
void runResume(resume);
|
|
6716
|
-
}
|
|
6813
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6814
|
+
void runHandshake({ newConversation: false });
|
|
6717
6815
|
return () => {
|
|
6718
6816
|
connectGenRef.current++;
|
|
6719
|
-
|
|
6817
|
+
resumeHandleRef.current?.cancel();
|
|
6720
6818
|
};
|
|
6721
6819
|
}, []);
|
|
6820
|
+
useEffect17(() => {
|
|
6821
|
+
if (!activated) return;
|
|
6822
|
+
void (async () => {
|
|
6823
|
+
try {
|
|
6824
|
+
const res = await transport.listForms();
|
|
6825
|
+
setRemoteForms(resolveForms(res.forms));
|
|
6826
|
+
} catch (err) {
|
|
6827
|
+
log16.debug("listForms failed \u2014 treating as no forms", { err });
|
|
6828
|
+
} finally {
|
|
6829
|
+
setFormsReady(true);
|
|
6830
|
+
}
|
|
6831
|
+
})();
|
|
6832
|
+
if (pendingThreadRef.current) {
|
|
6833
|
+
pendingThreadRef.current = false;
|
|
6834
|
+
const persisted = conversationIdSig.value;
|
|
6835
|
+
if (persisted) void loadThread(persisted);
|
|
6836
|
+
}
|
|
6837
|
+
if (conversationIdSig.value) {
|
|
6838
|
+
const handle = transport.resumeStream();
|
|
6839
|
+
resumeHandleRef.current = handle;
|
|
6840
|
+
void runResume(handle);
|
|
6841
|
+
}
|
|
6842
|
+
}, [activated]);
|
|
6722
6843
|
const lastUserSig = useRef9(userSig);
|
|
6723
6844
|
useEffect17(() => {
|
|
6724
6845
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6725
6846
|
lastUserSig.current = userSig;
|
|
6726
|
-
void
|
|
6727
|
-
}, [userSig, conversationReady,
|
|
6847
|
+
void runHandshake({ newConversation: false });
|
|
6848
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6728
6849
|
useEffect17(() => {
|
|
6729
6850
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6730
6851
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -6843,8 +6964,9 @@ function App({ options, hostElement, bus }) {
|
|
|
6843
6964
|
[options.features.humanInLoop, handleToolResult, handleToolDecision]
|
|
6844
6965
|
);
|
|
6845
6966
|
const userMessageCount = () => messagesSig.value.reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0);
|
|
6967
|
+
const effectiveForms = options.forms.list.length > 0 ? options.forms : remoteForms ?? options.forms;
|
|
6846
6968
|
const forms = useForms({
|
|
6847
|
-
forms:
|
|
6969
|
+
forms: effectiveForms,
|
|
6848
6970
|
persistence,
|
|
6849
6971
|
userContext: () => options.userContext,
|
|
6850
6972
|
messageCount: userMessageCount,
|
|
@@ -6877,18 +6999,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6877
6999
|
const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
|
|
6878
7000
|
const msgCount = useComputed7(() => messagesSig.value.length);
|
|
6879
7001
|
useEffect17(() => {
|
|
6880
|
-
if (conversationReady) forms.fire("pre-chat");
|
|
6881
|
-
}, [conversationReady, forms]);
|
|
7002
|
+
if (conversationReady && formsReady) forms.fire("pre-chat");
|
|
7003
|
+
}, [conversationReady, formsReady, forms]);
|
|
6882
7004
|
useEffect17(() => {
|
|
6883
|
-
if (!canSend) forms.fire("conversation-closed");
|
|
6884
|
-
}, [canSend, forms]);
|
|
7005
|
+
if (formsReady && !canSend) forms.fire("conversation-closed");
|
|
7006
|
+
}, [formsReady, canSend, forms]);
|
|
6885
7007
|
useEffect17(() => {
|
|
6886
|
-
if (conversationReady && pageArea) forms.fire("page-area");
|
|
6887
|
-
}, [conversationReady, pageArea, forms]);
|
|
7008
|
+
if (conversationReady && formsReady && pageArea) forms.fire("page-area");
|
|
7009
|
+
}, [conversationReady, formsReady, pageArea, forms]);
|
|
6888
7010
|
const idleMs = useMemo3(() => {
|
|
6889
|
-
const secs =
|
|
7011
|
+
const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
|
|
6890
7012
|
return secs.length ? Math.min(...secs) * 1e3 : 0;
|
|
6891
|
-
}, [
|
|
7013
|
+
}, [effectiveForms]);
|
|
6892
7014
|
useEffect17(() => {
|
|
6893
7015
|
if (!idleMs || !isOpen) return;
|
|
6894
7016
|
const id = setTimeout(() => forms.fire("idle"), idleMs);
|
|
@@ -6950,10 +7072,10 @@ function App({ options, hostElement, bus }) {
|
|
|
6950
7072
|
}, [transport, options.modules, unreadCountSig]);
|
|
6951
7073
|
const unreadSeeded = useRef9(false);
|
|
6952
7074
|
useEffect17(() => {
|
|
6953
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7075
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
6954
7076
|
unreadSeeded.current = true;
|
|
6955
7077
|
refreshUnread();
|
|
6956
|
-
}, [conversationReady, refreshUnread]);
|
|
7078
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
6957
7079
|
const handleOpen = useCallback6(() => {
|
|
6958
7080
|
log16.info("open", { mode: options.mode });
|
|
6959
7081
|
feedback.unlock();
|
|
@@ -6975,8 +7097,8 @@ function App({ options, hostElement, bus }) {
|
|
|
6975
7097
|
persistence.clearConversation();
|
|
6976
7098
|
setCanSend(true);
|
|
6977
7099
|
setFormMarkers([]);
|
|
6978
|
-
void
|
|
6979
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7100
|
+
void runHandshake({ newConversation: true });
|
|
7101
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
6980
7102
|
const handleNewChat = useCallback6(() => {
|
|
6981
7103
|
handleClear();
|
|
6982
7104
|
setView("chat");
|
|
@@ -7065,9 +7187,13 @@ function App({ options, hostElement, bus }) {
|
|
|
7065
7187
|
log16.info("selectConversation", { conversationId: targetConversationId });
|
|
7066
7188
|
bus.emit("selectConversation", { conversationId: targetConversationId });
|
|
7067
7189
|
try {
|
|
7068
|
-
const res = await
|
|
7190
|
+
const [res, markers] = await Promise.all([
|
|
7191
|
+
transport.loadConversation(targetConversationId),
|
|
7192
|
+
fetchFormMarkers(targetConversationId)
|
|
7193
|
+
]);
|
|
7069
7194
|
const hydrated = res.messages.map(fromWireMessage);
|
|
7070
7195
|
messagesSig.value = hydrated;
|
|
7196
|
+
setFormMarkers(markers);
|
|
7071
7197
|
conversationIdSig.value = res.conversationId;
|
|
7072
7198
|
persistence.saveConversationId(res.conversationId);
|
|
7073
7199
|
if (res.agent) setAgent(res.agent);
|
|
@@ -7081,7 +7207,7 @@ function App({ options, hostElement, bus }) {
|
|
|
7081
7207
|
options.onError?.(err);
|
|
7082
7208
|
}
|
|
7083
7209
|
},
|
|
7084
|
-
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
|
|
7210
|
+
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
|
|
7085
7211
|
);
|
|
7086
7212
|
useEffect17(() => {
|
|
7087
7213
|
const unsub = bindHostCommands(hostElement, {
|
|
@@ -7100,10 +7226,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7100
7226
|
);
|
|
7101
7227
|
const enrichedFormMarkers = useMemo3(
|
|
7102
7228
|
() => formMarkers.map((m) => {
|
|
7103
|
-
const def =
|
|
7229
|
+
const def = effectiveForms.list.find((f) => f.id === m.formId);
|
|
7104
7230
|
return { ...m, title: def?.title, form: def };
|
|
7105
7231
|
}),
|
|
7106
|
-
[formMarkers,
|
|
7232
|
+
[formMarkers, effectiveForms]
|
|
7107
7233
|
);
|
|
7108
7234
|
const panelProps = {
|
|
7109
7235
|
options: effectiveOptions,
|
|
@@ -7330,7 +7456,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7330
7456
|
/**
|
|
7331
7457
|
* Raw options accumulator. Starts as whatever the page-owner set via
|
|
7332
7458
|
* HTML attributes; gets server-side settings merged in on the
|
|
7333
|
-
* `
|
|
7459
|
+
* `handshake` event so subsequent re-renders pick up the server's
|
|
7334
7460
|
* mode / theme / launcher / etc.
|
|
7335
7461
|
*
|
|
7336
7462
|
* Mirrors `index.ts`'s `rawOptions` so both entry points share the
|
|
@@ -7360,7 +7486,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7360
7486
|
existingHost: this,
|
|
7361
7487
|
position: hostPositionForMode(resolved.mode)
|
|
7362
7488
|
});
|
|
7363
|
-
this.bus.on("
|
|
7489
|
+
this.bus.on("handshake", (response) => {
|
|
7364
7490
|
if (!response.config) return;
|
|
7365
7491
|
this.rawOptions = mergeServerConfig(this.rawOptions, response.config);
|
|
7366
7492
|
this.renderApp();
|
|
@@ -7370,7 +7496,7 @@ var ChatElement = class extends HTMLElement {
|
|
|
7370
7496
|
}
|
|
7371
7497
|
/**
|
|
7372
7498
|
* Apply the current `rawOptions` to the host element + App tree.
|
|
7373
|
-
* Called by both the initial boot and every `
|
|
7499
|
+
* Called by both the initial boot and every `handshake` merge.
|
|
7374
7500
|
*/
|
|
7375
7501
|
renderApp() {
|
|
7376
7502
|
if (!this.mountResult) return;
|