@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/index.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",
|
|
@@ -472,13 +477,12 @@ var DEFAULT_FEATURES = {
|
|
|
472
477
|
var DEFAULT_FORMS = { list: [], byTrigger: {} };
|
|
473
478
|
var DEFAULT_TRACKING = {
|
|
474
479
|
enabled: false,
|
|
475
|
-
endpoint: `https://t.${BRAND.domain}/api/ai-elements/px.gif`,
|
|
476
480
|
sampleRate: 1
|
|
477
481
|
};
|
|
478
482
|
var DEFAULT_ENDPOINTS = {
|
|
479
|
-
upload: "/ai
|
|
480
|
-
transcribe: "/ai
|
|
481
|
-
submitForm: "/
|
|
483
|
+
upload: "/ai-agent/upload-file",
|
|
484
|
+
transcribe: "/ai-agent/transcribe-audio",
|
|
485
|
+
submitForm: "/submit-form"
|
|
482
486
|
};
|
|
483
487
|
var DEFAULT_LAUNCHER = {
|
|
484
488
|
variant: "circle",
|
|
@@ -520,11 +524,16 @@ function resolveOptions(rawOpts) {
|
|
|
520
524
|
const mode = presentation.mode ?? "floating";
|
|
521
525
|
const locale = i18n.locale ?? resolveDefaultLocale(i18n.defaultLocale);
|
|
522
526
|
const availableLocales = i18n.availableLocales ?? [locale];
|
|
527
|
+
const baseUrl = (opts.baseUrl ?? BRAND.defaultBaseUrl).replace(/\/$/, "");
|
|
528
|
+
const agentBaseUrl = (opts.agentBaseUrl ?? `${baseUrl}/api/agent`).replace(/\/$/, "");
|
|
529
|
+
const dataBaseUrl = (opts.dataBaseUrl ?? `${baseUrl}/api/data`).replace(/\/$/, "");
|
|
523
530
|
return {
|
|
524
531
|
widgetId: opts.widgetId ?? "default",
|
|
525
532
|
publicKey: opts.publicKey,
|
|
526
533
|
aiAgentDeploymentId: opts.aiAgentDeploymentId,
|
|
527
|
-
baseUrl
|
|
534
|
+
baseUrl,
|
|
535
|
+
agentBaseUrl,
|
|
536
|
+
dataBaseUrl,
|
|
528
537
|
userContext: sanitizeUserContext(opts.userContext),
|
|
529
538
|
pageContext: sanitizePageContext(opts.pageContext),
|
|
530
539
|
position: presentation.position ?? "bottom-right",
|
|
@@ -569,7 +578,7 @@ function resolveOptions(rawOpts) {
|
|
|
569
578
|
poweredBy: resolvePoweredBy(footer.poweredBy),
|
|
570
579
|
composer: resolveComposer(opts.composer),
|
|
571
580
|
modules: resolveModules(opts.modules),
|
|
572
|
-
tracking: resolveTracking(opts.tracking),
|
|
581
|
+
tracking: resolveTracking(opts.tracking, dataBaseUrl),
|
|
573
582
|
storage: opts.storage ?? defaultStorage,
|
|
574
583
|
onMessage: opts.onMessage,
|
|
575
584
|
onOpen: opts.onOpen,
|
|
@@ -602,11 +611,13 @@ function resolveHaptics(overrides) {
|
|
|
602
611
|
events: overrides?.events
|
|
603
612
|
};
|
|
604
613
|
}
|
|
605
|
-
function resolveTracking(overrides) {
|
|
614
|
+
function resolveTracking(overrides, dataBaseUrl) {
|
|
606
615
|
const sampleRate = overrides?.sampleRate ?? DEFAULT_TRACKING.sampleRate;
|
|
607
616
|
return {
|
|
608
617
|
enabled: overrides?.enabled ?? DEFAULT_TRACKING.enabled,
|
|
609
|
-
|
|
618
|
+
// The collector rides the data module — `${dataBaseUrl}/elements/px.gif`
|
|
619
|
+
// (e.g. `https://help.ai/api/data/elements/px.gif`).
|
|
620
|
+
endpoint: overrides?.endpoint ?? `${dataBaseUrl}/elements/px.gif`,
|
|
610
621
|
// Clamp instead of reject — a malformed rate must never turn a
|
|
611
622
|
// disabled tracker on or crash resolve (no Zod on the IIFE path).
|
|
612
623
|
sampleRate: Math.min(1, Math.max(0, Number.isFinite(sampleRate) ? sampleRate : 1)),
|
|
@@ -775,7 +786,7 @@ function mergeServerConfig(user, server) {
|
|
|
775
786
|
out[key] = mergeLeaves(sv, uv);
|
|
776
787
|
}
|
|
777
788
|
if (server.modules !== void 0 && user.modules === void 0) out.modules = server.modules;
|
|
778
|
-
if (server.
|
|
789
|
+
if (server.dataBaseUrl !== void 0 && user.dataBaseUrl === void 0) out.dataBaseUrl = server.dataBaseUrl;
|
|
779
790
|
const userI18n = user.i18n;
|
|
780
791
|
const serverI18n = server.i18n;
|
|
781
792
|
if (userI18n || serverI18n) {
|
|
@@ -835,6 +846,8 @@ var EMBED_SCALAR_ATTRS = [
|
|
|
835
846
|
["public-key", "publicKey"],
|
|
836
847
|
["ai-agent-deployment-id", "aiAgentDeploymentId"],
|
|
837
848
|
["base-url", "baseUrl"],
|
|
849
|
+
["agent-base-url", "agentBaseUrl"],
|
|
850
|
+
["data-base-url", "dataBaseUrl"],
|
|
838
851
|
["theme", "theme", (v) => v]
|
|
839
852
|
];
|
|
840
853
|
var PRESENTATION_ATTRS = [
|
|
@@ -909,7 +922,7 @@ var REACTIVE_ATTRS = [
|
|
|
909
922
|
...SPECIAL_REACTIVE_ATTRS
|
|
910
923
|
];
|
|
911
924
|
|
|
912
|
-
// src/core/
|
|
925
|
+
// src/core/handshake-shape.ts
|
|
913
926
|
function isPlainObject(raw) {
|
|
914
927
|
return !!raw && typeof raw === "object" && !Array.isArray(raw);
|
|
915
928
|
}
|
|
@@ -1240,46 +1253,63 @@ function parseWireDate(value) {
|
|
|
1240
1253
|
}
|
|
1241
1254
|
var DEFAULT_PATHS = {
|
|
1242
1255
|
/** Conversation bootstrap. `POST` → deployment config + per-visitor state. */
|
|
1243
|
-
|
|
1256
|
+
handshake: "/ai-agent/handshake",
|
|
1244
1257
|
/** Send a message + stream the reply (SSE). `POST`. */
|
|
1245
|
-
streamMessage: "/ai
|
|
1258
|
+
streamMessage: "/ai-agent/stream-message",
|
|
1246
1259
|
/**
|
|
1247
1260
|
* Resume an interrupted reply. `GET` (envelope rides as query params).
|
|
1248
1261
|
* Stateless: the server re-streams the whole reply SSE again from the start —
|
|
1249
1262
|
* there is NO `Last-Event-ID`. The client reconciles by id (`seenIds`), so
|
|
1250
1263
|
* replayed events are skipped and only the continuation surfaces.
|
|
1251
1264
|
*/
|
|
1252
|
-
streamResume: "/ai
|
|
1265
|
+
streamResume: "/ai-agent/stream-resume",
|
|
1253
1266
|
/** Abort the in-flight reply stream. `POST`. */
|
|
1254
|
-
cancelStream: "/ai
|
|
1267
|
+
cancelStream: "/ai-agent/cancel-stream",
|
|
1255
1268
|
/** List the visitor's conversations. `GET`. */
|
|
1256
|
-
listConversations: "/ai
|
|
1257
|
-
/** Load one conversation's messages. `GET ?conversationId=…`. */
|
|
1258
|
-
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",
|
|
1259
1272
|
/**
|
|
1260
1273
|
* Persist a user-driven settings change. POST `{ visitorId, userPrefs }` →
|
|
1261
1274
|
* server persists, returns the merged authoritative copy. Fire-and-forget on
|
|
1262
1275
|
* the client — a failure leaves the local cache valid; the next
|
|
1263
|
-
* `
|
|
1276
|
+
* `handshake()` reconciles.
|
|
1264
1277
|
*/
|
|
1265
|
-
updateSettings: "/ai
|
|
1278
|
+
updateSettings: "/ai-agent/update-settings",
|
|
1266
1279
|
/**
|
|
1267
1280
|
* Mark a conversation read. POST `{ visitorId, conversationId }` → the server
|
|
1268
1281
|
* records `lastReadAt = now` for that (visitor, conversation) and recomputes
|
|
1269
1282
|
* `unreadCount` (returned on `/list-conversations`) accordingly. Fire-and-forget
|
|
1270
1283
|
* on the client; a failure just leaves the badge until the next sync.
|
|
1271
1284
|
*/
|
|
1272
|
-
markRead: "/ai
|
|
1273
|
-
// ──
|
|
1274
|
-
/** All widget content via one filtered endpoint. `GET /
|
|
1275
|
-
content: "/
|
|
1285
|
+
markRead: "/ai-agent/mark-read",
|
|
1286
|
+
// ── Data API (module-help-ai-data-api, via `dataBaseUrl`) ─────────
|
|
1287
|
+
/** All widget content via one filtered endpoint. `GET /content?tags=…`. */
|
|
1288
|
+
content: "/content",
|
|
1289
|
+
/**
|
|
1290
|
+
* Resolved form definitions for the deployment. `GET /forms` →
|
|
1291
|
+
* `{ forms: FormDef[] }`. Fetched once at boot (in parallel with
|
|
1292
|
+
* handshake); form gating waits for it. A failure is treated as
|
|
1293
|
+
* "no forms" (non-fatal).
|
|
1294
|
+
*/
|
|
1295
|
+
forms: "/forms",
|
|
1276
1296
|
/**
|
|
1277
1297
|
* Form submission (the event-driven forms engine). POST `{ visitorId,
|
|
1278
1298
|
* conversationId, formId, trigger, values, skipped? }` → record the form's answers
|
|
1279
1299
|
* immediately, keyed by the same `visitorId` + `conversationId` the chat uses.
|
|
1280
1300
|
* Overridable via `endpoints.submitForm`. Fire-and-forget; 404 is ignored.
|
|
1281
1301
|
*/
|
|
1282
|
-
submitForm: "/
|
|
1302
|
+
submitForm: "/submit-form",
|
|
1303
|
+
/**
|
|
1304
|
+
* Visitor interaction records for a conversation. `GET
|
|
1305
|
+
* /activity?visitorId=…&conversationId=…` →
|
|
1306
|
+
* `{ formSubmissions: FormSubmissionRecord[] }` (chronological). Replaces
|
|
1307
|
+
* the former `formSubmissions` echo on the handshake / list-messages;
|
|
1308
|
+
* deliberately generic — future visitor interaction records ride the same
|
|
1309
|
+
* endpoint. A failure (e.g. 404 `AI_AGENT_PUBLIC_ACCESS_NOT_FOUND` when the
|
|
1310
|
+
* deployment doesn't resolve) is treated as "no records" (non-fatal).
|
|
1311
|
+
*/
|
|
1312
|
+
activity: "/activity"
|
|
1283
1313
|
};
|
|
1284
1314
|
var CONTEXT_PARAM = "context";
|
|
1285
1315
|
function buildSendMessageRequest(params) {
|
|
@@ -1380,7 +1410,7 @@ function sleep(ms, signal7) {
|
|
|
1380
1410
|
var AgentTransport = class {
|
|
1381
1411
|
constructor(opts) {
|
|
1382
1412
|
__publicField(this, "opts", opts);
|
|
1383
|
-
// Identity adopted from the
|
|
1413
|
+
// Identity adopted from the handshake response; rides the request envelope
|
|
1384
1414
|
// (see `envelope()`) on every subsequent call so the backend can validate the
|
|
1385
1415
|
// (visitor, conversation) pair.
|
|
1386
1416
|
__publicField(this, "visitorId");
|
|
@@ -1403,9 +1433,9 @@ var AgentTransport = class {
|
|
|
1403
1433
|
}
|
|
1404
1434
|
/**
|
|
1405
1435
|
* Seed the visitor + conversation identity from persistence BEFORE the first
|
|
1406
|
-
* `
|
|
1407
|
-
* with `
|
|
1408
|
-
* `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`
|
|
1409
1439
|
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1410
1440
|
*/
|
|
1411
1441
|
primeIdentity(visitorId, conversationId) {
|
|
@@ -1422,7 +1452,7 @@ var AgentTransport = class {
|
|
|
1422
1452
|
return out;
|
|
1423
1453
|
}
|
|
1424
1454
|
/** Merge the envelope into a JSON body (POSTs). Explicit body fields win — e.g.
|
|
1425
|
-
*
|
|
1455
|
+
* handshake's client-proposed ids, or a mark-read target conversation that
|
|
1426
1456
|
* differs from the active one. */
|
|
1427
1457
|
withEnvelope(body) {
|
|
1428
1458
|
return { ...this.envelope(), ...body };
|
|
@@ -1443,28 +1473,22 @@ var AgentTransport = class {
|
|
|
1443
1473
|
return url.toString();
|
|
1444
1474
|
}
|
|
1445
1475
|
/** One-shot runtime bootstrap. Called once after the widget mounts. */
|
|
1446
|
-
async
|
|
1447
|
-
log4.debug("
|
|
1476
|
+
async handshake(body) {
|
|
1477
|
+
log4.debug("handshake \u2192", {
|
|
1448
1478
|
visitorId: body.visitorId,
|
|
1449
1479
|
conversationId: body.conversationId,
|
|
1450
1480
|
locale: body.locale
|
|
1451
1481
|
});
|
|
1452
|
-
const res = await this.postJson(
|
|
1453
|
-
|
|
1454
|
-
body,
|
|
1455
|
-
"startConversation"
|
|
1456
|
-
);
|
|
1457
|
-
log4.debug("startConversation \u2190", {
|
|
1482
|
+
const res = await this.postJson(DEFAULT_PATHS.handshake, body, "handshake");
|
|
1483
|
+
log4.debug("handshake \u2190", {
|
|
1458
1484
|
visitorId: res.visitorId,
|
|
1459
1485
|
conversationId: res.conversationId,
|
|
1460
|
-
canContinue: res.canContinue,
|
|
1461
|
-
hasMessages: !!res.messages?.length,
|
|
1462
1486
|
hasWelcome: !!res.welcome,
|
|
1463
1487
|
hasConfig: !!res.config,
|
|
1464
1488
|
rebind: res.rebind
|
|
1465
1489
|
});
|
|
1466
|
-
if (!
|
|
1467
|
-
log4.warn("
|
|
1490
|
+
if (!isHandshakeResponseShape(res)) {
|
|
1491
|
+
log4.warn("handshake response did not match expected shape; using raw response", { response: res });
|
|
1468
1492
|
}
|
|
1469
1493
|
this.visitorId = res.visitorId ?? body.visitorId;
|
|
1470
1494
|
this.conversationId = res.conversationId;
|
|
@@ -1552,14 +1576,17 @@ var AgentTransport = class {
|
|
|
1552
1576
|
log4.debug("markRead failed (non-fatal)", { err });
|
|
1553
1577
|
}
|
|
1554
1578
|
}
|
|
1555
|
-
// ----
|
|
1579
|
+
// ---- Data API (content / forms — module-help-ai-data-api) --------------
|
|
1556
1580
|
//
|
|
1557
|
-
//
|
|
1558
|
-
//
|
|
1559
|
-
//
|
|
1560
|
-
|
|
1581
|
+
// The DATA surfaces live on `dataBaseUrl` (default `${baseUrl}/api/data`)
|
|
1582
|
+
// with ROOT-level paths — same auth headers + envelope, same retry
|
|
1583
|
+
// behavior. One endpoint for every content surface (Home / Help / News /
|
|
1584
|
+
// docs); callers pass filters; a thrown StreamError (e.g. 404 on an older
|
|
1585
|
+
// backend) is treated as "no content" by the module and the tab degrades
|
|
1586
|
+
// gracefully.
|
|
1587
|
+
/** Fetch content rows by filter. `GET /content` (data-API). */
|
|
1561
1588
|
async listContent(query = {}) {
|
|
1562
|
-
const url = new URL(this.
|
|
1589
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.content));
|
|
1563
1590
|
for (const [key, value] of Object.entries(query)) {
|
|
1564
1591
|
if (value === void 0) continue;
|
|
1565
1592
|
url.searchParams.set(key, Array.isArray(value) ? value.join(",") : String(value));
|
|
@@ -1570,6 +1597,40 @@ var AgentTransport = class {
|
|
|
1570
1597
|
log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
|
|
1571
1598
|
return { ...res, items };
|
|
1572
1599
|
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Fetch the deployment's resolved form definitions. `GET /forms`
|
|
1602
|
+
* (data-API). Replaces the former `config.forms` push on the handshake —
|
|
1603
|
+
* called once at boot, in parallel with `handshake`; form gating
|
|
1604
|
+
* (pre-chat etc.) waits for it. Callers treat a thrown StreamError as
|
|
1605
|
+
* "no forms" (non-fatal).
|
|
1606
|
+
*/
|
|
1607
|
+
async listForms() {
|
|
1608
|
+
log4.debug("listForms \u2192");
|
|
1609
|
+
const res = await this.getJson(this.dataUrl(DEFAULT_PATHS.forms), "listForms");
|
|
1610
|
+
const forms = res.forms ?? [];
|
|
1611
|
+
log4.debug("listForms \u2190", { count: forms.length });
|
|
1612
|
+
return { forms };
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Fetch the visitor's recorded activity for a conversation — currently the
|
|
1616
|
+
* form submissions that rebuild the collapsed "form submitted / skipped"
|
|
1617
|
+
* timeline markers on resume. `GET /activity` (data-API); the
|
|
1618
|
+
* envelope carries `visitorId` / `conversationId` as query params.
|
|
1619
|
+
* `conversationId` here is an optional resource selector (a thread that may
|
|
1620
|
+
* differ from the active conversation — the history-pane switch); when
|
|
1621
|
+
* omitted, the envelope's active conversation applies. Callers treat a
|
|
1622
|
+
* thrown StreamError (e.g. 404 on a deployment that doesn't resolve) as
|
|
1623
|
+
* "no activity" (non-fatal).
|
|
1624
|
+
*/
|
|
1625
|
+
async getActivity(conversationId) {
|
|
1626
|
+
const url = new URL(this.dataUrl(DEFAULT_PATHS.activity));
|
|
1627
|
+
if (conversationId) url.searchParams.set("conversationId", conversationId);
|
|
1628
|
+
log4.debug("getActivity \u2192", { conversationId: conversationId ?? this.conversationId });
|
|
1629
|
+
const res = await this.getJson(url.toString(), "getActivity");
|
|
1630
|
+
const formSubmissions = res.formSubmissions ?? [];
|
|
1631
|
+
log4.debug("getActivity \u2190", { count: formSubmissions.length });
|
|
1632
|
+
return { formSubmissions };
|
|
1633
|
+
}
|
|
1573
1634
|
async saveUserPrefs(userPrefs) {
|
|
1574
1635
|
log4.debug("saveUserPrefs \u2192", {
|
|
1575
1636
|
visitorId: this.visitorId,
|
|
@@ -1581,17 +1642,18 @@ var AgentTransport = class {
|
|
|
1581
1642
|
return res;
|
|
1582
1643
|
}
|
|
1583
1644
|
get uploadPath() {
|
|
1584
|
-
return this.opts.endpoints?.upload ?? "/ai
|
|
1645
|
+
return this.opts.endpoints?.upload ?? "/ai-agent/upload-file";
|
|
1585
1646
|
}
|
|
1586
1647
|
get transcribePath() {
|
|
1587
|
-
return this.opts.endpoints?.transcribe ?? "/ai
|
|
1648
|
+
return this.opts.endpoints?.transcribe ?? "/ai-agent/transcribe-audio";
|
|
1588
1649
|
}
|
|
1589
1650
|
get submitFormPath() {
|
|
1590
1651
|
return this.opts.endpoints?.submitForm ?? DEFAULT_PATHS.submitForm;
|
|
1591
1652
|
}
|
|
1592
1653
|
/**
|
|
1593
|
-
* Record a completed form (intake, CSAT, claim, …).
|
|
1594
|
-
*
|
|
1654
|
+
* Record a completed form (intake, CSAT, claim, …). `POST
|
|
1655
|
+
* /submit-form` (data-API). Fire-and-forget — the record is
|
|
1656
|
+
* captured even if the visitor never sends a message. `conversationId`,
|
|
1595
1657
|
* `formId`, and `trigger` are explicit so the backend can tie + route;
|
|
1596
1658
|
* `visitorId` + context ride the envelope. A failure (e.g. 404 on a backend
|
|
1597
1659
|
* that doesn't implement the endpoint) is non-fatal, so callers don't await.
|
|
@@ -1599,7 +1661,7 @@ var AgentTransport = class {
|
|
|
1599
1661
|
async submitForm(body) {
|
|
1600
1662
|
log4.debug("submitForm \u2192", { formId: body.formId, trigger: body.trigger, fields: Object.keys(body.values).length });
|
|
1601
1663
|
try {
|
|
1602
|
-
await this.postJson(this.submitFormPath, body, "submitForm");
|
|
1664
|
+
await this.postJson(this.dataUrl(this.submitFormPath), body, "submitForm");
|
|
1603
1665
|
} catch (err) {
|
|
1604
1666
|
log4.debug("submitForm failed (non-fatal)", { err });
|
|
1605
1667
|
}
|
|
@@ -1623,9 +1685,9 @@ var AgentTransport = class {
|
|
|
1623
1685
|
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1624
1686
|
*
|
|
1625
1687
|
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1626
|
-
* `
|
|
1688
|
+
* `handshake`, never gated on whether there are persisted messages. If
|
|
1627
1689
|
* the user refreshed while the assistant was still streaming, the
|
|
1628
|
-
* `
|
|
1690
|
+
* `handshake` reply carries no messages (the turn isn't persisted until
|
|
1629
1691
|
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1630
1692
|
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1631
1693
|
* (the common case — no bubble is created).
|
|
@@ -1652,7 +1714,7 @@ var AgentTransport = class {
|
|
|
1652
1714
|
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1653
1715
|
* off first, the reply may still be spinning up server-side);
|
|
1654
1716
|
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1655
|
-
* parallel with
|
|
1717
|
+
* parallel with handshake, so a reply that was streaming when the
|
|
1656
1718
|
* user reloaded keeps flowing).
|
|
1657
1719
|
*
|
|
1658
1720
|
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
@@ -1661,7 +1723,7 @@ var AgentTransport = class {
|
|
|
1661
1723
|
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1662
1724
|
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1663
1725
|
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1664
|
-
* turn comes from the DB via
|
|
1726
|
+
* turn comes from the DB via the handshake / list-messages.
|
|
1665
1727
|
*
|
|
1666
1728
|
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1667
1729
|
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
@@ -1726,7 +1788,7 @@ var AgentTransport = class {
|
|
|
1726
1788
|
}
|
|
1727
1789
|
return false;
|
|
1728
1790
|
}
|
|
1729
|
-
/** Abort + fire-and-forget POST to `/ai
|
|
1791
|
+
/** Abort + fire-and-forget POST to `/ai-agent/cancel-stream`. Idempotent. */
|
|
1730
1792
|
cancelStream(ctrl, conversationId) {
|
|
1731
1793
|
if (ctrl.signal.aborted) return;
|
|
1732
1794
|
log4.debug("cancel stream", { conversationId, visitorId: this.visitorId });
|
|
@@ -1767,7 +1829,7 @@ var AgentTransport = class {
|
|
|
1767
1829
|
return url.toString();
|
|
1768
1830
|
}
|
|
1769
1831
|
// JSON requests are idempotent (reads, or full-snapshot writes like
|
|
1770
|
-
// update-settings / mark-read /
|
|
1832
|
+
// update-settings / mark-read / handshake), so they auto-retry transient
|
|
1771
1833
|
// failures. The message POST is NOT in here — re-sending could duplicate the
|
|
1772
1834
|
// reply, so it surfaces an error for the user to retry (see `openMessageStream`).
|
|
1773
1835
|
async postJson(path, body, label) {
|
|
@@ -1835,9 +1897,23 @@ var AgentTransport = class {
|
|
|
1835
1897
|
if (!res.ok) throw new StreamError(`${label} failed: ${res.status}`, "server", res.status);
|
|
1836
1898
|
return await res.json();
|
|
1837
1899
|
}
|
|
1900
|
+
/**
|
|
1901
|
+
* Agent-API URL — resolves a `/ai-agent/*` path against the resolved
|
|
1902
|
+
* agent module base. Absolute URLs pass through unchanged.
|
|
1903
|
+
*/
|
|
1838
1904
|
url(pathOrUrl) {
|
|
1839
1905
|
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1840
|
-
return `${this.opts.
|
|
1906
|
+
return `${this.opts.agentBaseUrl}${pathOrUrl}`;
|
|
1907
|
+
}
|
|
1908
|
+
/**
|
|
1909
|
+
* Data-API variant of {@link url} — resolves a root-level path (content /
|
|
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.
|
|
1913
|
+
*/
|
|
1914
|
+
dataUrl(pathOrUrl) {
|
|
1915
|
+
if (/^https?:\/\//i.test(pathOrUrl)) return pathOrUrl;
|
|
1916
|
+
return `${this.opts.dataBaseUrl}${pathOrUrl}`;
|
|
1841
1917
|
}
|
|
1842
1918
|
get fetchImpl() {
|
|
1843
1919
|
return this.opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
@@ -1851,10 +1927,10 @@ function extensionFor(mimeType) {
|
|
|
1851
1927
|
if (mimeType.includes("wav")) return "wav";
|
|
1852
1928
|
return "bin";
|
|
1853
1929
|
}
|
|
1854
|
-
function
|
|
1930
|
+
function isHandshakeResponseShape(raw) {
|
|
1855
1931
|
if (!raw || typeof raw !== "object") return false;
|
|
1856
1932
|
const r = raw;
|
|
1857
|
-
return typeof r.visitorId === "string" && typeof r.conversationId === "string"
|
|
1933
|
+
return typeof r.visitorId === "string" && typeof r.conversationId === "string";
|
|
1858
1934
|
}
|
|
1859
1935
|
|
|
1860
1936
|
// src/stream/messages.ts
|
|
@@ -6362,11 +6438,21 @@ function App({ options, hostElement, bus }) {
|
|
|
6362
6438
|
const chatTabIdRef = useRef9(void 0);
|
|
6363
6439
|
chatTabIdRef.current = chatTabId();
|
|
6364
6440
|
const [conversationReady, setConversationReady] = useState13(false);
|
|
6441
|
+
const [remoteForms, setRemoteForms] = useState13(null);
|
|
6442
|
+
const [formsReady, setFormsReady] = useState13(false);
|
|
6365
6443
|
const isInlineLike = options.mode === "standalone" || options.mode === "inline";
|
|
6366
6444
|
const initialPanelRef = useRef9(
|
|
6367
6445
|
resolveInitialPanelState(options, currentViewportWidth(), persistence.loadPanelOpen(), persistence.loadPanelSize())
|
|
6368
6446
|
);
|
|
6369
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]);
|
|
6370
6456
|
const initialPanelApplied = useRef9(false);
|
|
6371
6457
|
const [launcherLeaving, setLauncherLeaving] = useState13(false);
|
|
6372
6458
|
const { dismissed: calloutDismissed, dismissCallout: dismissCalloutRaw } = useLauncherCallout({
|
|
@@ -6393,7 +6479,8 @@ function App({ options, hostElement, bus }) {
|
|
|
6393
6479
|
const [formContext, setFormContext] = useState13({});
|
|
6394
6480
|
const [transport] = useState13(
|
|
6395
6481
|
() => new AgentTransport({
|
|
6396
|
-
|
|
6482
|
+
agentBaseUrl: options.agentBaseUrl,
|
|
6483
|
+
dataBaseUrl: options.dataBaseUrl,
|
|
6397
6484
|
auth: createAuth({
|
|
6398
6485
|
publicKey: options.publicKey,
|
|
6399
6486
|
aiAgentDeploymentId: options.aiAgentDeploymentId,
|
|
@@ -6474,8 +6561,67 @@ function App({ options, hostElement, bus }) {
|
|
|
6474
6561
|
},
|
|
6475
6562
|
[messagesSig]
|
|
6476
6563
|
);
|
|
6564
|
+
const fetchFormMarkers = useCallback6(
|
|
6565
|
+
async (conversationId) => {
|
|
6566
|
+
try {
|
|
6567
|
+
const { formSubmissions: subs } = await transport.getActivity(conversationId);
|
|
6568
|
+
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6569
|
+
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6570
|
+
return subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
6571
|
+
key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
|
|
6572
|
+
formId: rec.formId,
|
|
6573
|
+
outcome: rec.skipped ? "skipped" : "submitted",
|
|
6574
|
+
createdAt: parseWireDate(rec.createdAt),
|
|
6575
|
+
values: rec.values
|
|
6576
|
+
}));
|
|
6577
|
+
} catch (err) {
|
|
6578
|
+
log16.debug("getActivity failed \u2014 no form markers (non-fatal)", { err });
|
|
6579
|
+
return [];
|
|
6580
|
+
}
|
|
6581
|
+
},
|
|
6582
|
+
[transport]
|
|
6583
|
+
);
|
|
6477
6584
|
const connectGenRef = useRef9(0);
|
|
6478
|
-
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(
|
|
6479
6625
|
async ({ newConversation }) => {
|
|
6480
6626
|
const myGen = ++connectGenRef.current;
|
|
6481
6627
|
const isStale = () => myGen !== connectGenRef.current;
|
|
@@ -6487,7 +6633,7 @@ function App({ options, hostElement, bus }) {
|
|
|
6487
6633
|
}
|
|
6488
6634
|
let res;
|
|
6489
6635
|
try {
|
|
6490
|
-
res = await transport.
|
|
6636
|
+
res = await transport.handshake({
|
|
6491
6637
|
visitorId,
|
|
6492
6638
|
conversationId,
|
|
6493
6639
|
userPrefs: persistence.loadUserPrefs(),
|
|
@@ -6538,66 +6684,22 @@ function App({ options, hostElement, bus }) {
|
|
|
6538
6684
|
if (res.userPrefs.themeMode) setActiveThemeMode(res.userPrefs.themeMode);
|
|
6539
6685
|
if (res.userPrefs.textSize) setActiveTextSize(res.userPrefs.textSize);
|
|
6540
6686
|
}
|
|
6687
|
+
welcomeRef.current = res.welcome;
|
|
6541
6688
|
const isResume = !newConversation && persistedChatId && res.conversationId === persistedChatId;
|
|
6542
6689
|
if (isResume) {
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
const chat = res.messages?.length ? {
|
|
6546
|
-
conversationId: persistedChatId,
|
|
6547
|
-
canContinue: res.canContinue ?? true,
|
|
6548
|
-
messages: res.messages,
|
|
6549
|
-
formSubmissions: res.formSubmissions
|
|
6550
|
-
} : await transport.loadConversation(persistedChatId);
|
|
6551
|
-
if (isStale()) return;
|
|
6552
|
-
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
6553
|
-
const subs = chat.formSubmissions ?? res.formSubmissions ?? [];
|
|
6554
|
-
const lastIndexByForm = /* @__PURE__ */ new Map();
|
|
6555
|
-
subs.forEach((rec, i) => lastIndexByForm.set(rec.formId, i));
|
|
6556
|
-
const markers = subs.filter((rec, i) => !rec.skipped || lastIndexByForm.get(rec.formId) === i).map((rec, i) => ({
|
|
6557
|
-
key: `wire:${rec.formId}:${rec.createdAt ?? i}`,
|
|
6558
|
-
formId: rec.formId,
|
|
6559
|
-
outcome: rec.skipped ? "skipped" : "submitted",
|
|
6560
|
-
createdAt: parseWireDate(rec.createdAt),
|
|
6561
|
-
values: rec.values
|
|
6562
|
-
}));
|
|
6563
|
-
setFormMarkers(markers);
|
|
6564
|
-
const timelineStamps = [...loaded.map((m) => m.createdAt), ...markers.map((m) => m.createdAt)].filter(
|
|
6565
|
-
(n) => n !== void 0
|
|
6566
|
-
);
|
|
6567
|
-
const welcomeAnchor = (timelineStamps.length ? Math.min(...timelineStamps) : Date.now()) - 1;
|
|
6568
|
-
const loadedIds = new Set(loaded.map((m) => m.id));
|
|
6569
|
-
const welcomeRows = (res.welcome?.messages ?? []).filter((w) => !loadedIds.has(w.id)).map((w) => {
|
|
6570
|
-
const row = makeInstantWelcomeMessage(w);
|
|
6571
|
-
row.createdAt = welcomeAnchor;
|
|
6572
|
-
return row;
|
|
6573
|
-
});
|
|
6574
|
-
const tail = resumeBubbleRef.current ? [resumeBubbleRef.current] : [];
|
|
6575
|
-
if (loaded.length || tail.length) {
|
|
6576
|
-
messagesSig.value = [...welcomeRows, ...loaded, ...tail];
|
|
6577
|
-
setCanSend(chat.canContinue ?? true);
|
|
6578
|
-
setSuggestions(chat.suggestions ?? []);
|
|
6579
|
-
} else {
|
|
6580
|
-
messagesSig.value = welcomeRows;
|
|
6581
|
-
}
|
|
6582
|
-
} catch (err) {
|
|
6583
|
-
if (isStale()) return;
|
|
6584
|
-
log16.warn("loadConversation failed; resetting conversationId", { err, conversationId: persistedChatId });
|
|
6585
|
-
conversationIdSig.value = void 0;
|
|
6586
|
-
persistence.saveConversationId(void 0);
|
|
6587
|
-
} finally {
|
|
6588
|
-
if (!isStale()) setLoadingMessages(false);
|
|
6589
|
-
}
|
|
6690
|
+
if (activatedRef.current) void loadThread(persistedChatId);
|
|
6691
|
+
else pendingThreadRef.current = true;
|
|
6590
6692
|
} else {
|
|
6591
6693
|
conversationIdSig.value = res.conversationId;
|
|
6592
6694
|
persistence.saveConversationId(res.conversationId);
|
|
6593
6695
|
playWelcome(res.welcome?.messages);
|
|
6594
6696
|
}
|
|
6595
6697
|
if (!newConversation) {
|
|
6596
|
-
bus.emit("
|
|
6698
|
+
bus.emit("handshake", res);
|
|
6597
6699
|
setConversationReady(true);
|
|
6598
6700
|
}
|
|
6599
6701
|
},
|
|
6600
|
-
[transport, visitorId, persistence, options, bus,
|
|
6702
|
+
[transport, visitorId, persistence, options, bus, conversationIdSig, feedback, playWelcome, loadThread]
|
|
6601
6703
|
);
|
|
6602
6704
|
const userSig = JSON.stringify(options.userContext ?? null);
|
|
6603
6705
|
const pageSig = JSON.stringify(options.pageContext ?? null);
|
|
@@ -6650,26 +6752,45 @@ function App({ options, hostElement, bus }) {
|
|
|
6650
6752
|
},
|
|
6651
6753
|
[reducer, messagesSig, feedback, bus, options, homeNav]
|
|
6652
6754
|
);
|
|
6755
|
+
const resumeHandleRef = useRef9(null);
|
|
6653
6756
|
useEffect17(() => {
|
|
6654
|
-
void runStartConversation({ newConversation: false });
|
|
6655
6757
|
const persistedConversation = conversationIdSig.value;
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
transport.primeIdentity(visitorId, persistedConversation);
|
|
6659
|
-
resume = transport.resumeStream();
|
|
6660
|
-
void runResume(resume);
|
|
6661
|
-
}
|
|
6758
|
+
if (persistedConversation) transport.primeIdentity(visitorId, persistedConversation);
|
|
6759
|
+
void runHandshake({ newConversation: false });
|
|
6662
6760
|
return () => {
|
|
6663
6761
|
connectGenRef.current++;
|
|
6664
|
-
|
|
6762
|
+
resumeHandleRef.current?.cancel();
|
|
6665
6763
|
};
|
|
6666
6764
|
}, []);
|
|
6765
|
+
useEffect17(() => {
|
|
6766
|
+
if (!activated) return;
|
|
6767
|
+
void (async () => {
|
|
6768
|
+
try {
|
|
6769
|
+
const res = await transport.listForms();
|
|
6770
|
+
setRemoteForms(resolveForms(res.forms));
|
|
6771
|
+
} catch (err) {
|
|
6772
|
+
log16.debug("listForms failed \u2014 treating as no forms", { err });
|
|
6773
|
+
} finally {
|
|
6774
|
+
setFormsReady(true);
|
|
6775
|
+
}
|
|
6776
|
+
})();
|
|
6777
|
+
if (pendingThreadRef.current) {
|
|
6778
|
+
pendingThreadRef.current = false;
|
|
6779
|
+
const persisted = conversationIdSig.value;
|
|
6780
|
+
if (persisted) void loadThread(persisted);
|
|
6781
|
+
}
|
|
6782
|
+
if (conversationIdSig.value) {
|
|
6783
|
+
const handle = transport.resumeStream();
|
|
6784
|
+
resumeHandleRef.current = handle;
|
|
6785
|
+
void runResume(handle);
|
|
6786
|
+
}
|
|
6787
|
+
}, [activated]);
|
|
6667
6788
|
const lastUserSig = useRef9(userSig);
|
|
6668
6789
|
useEffect17(() => {
|
|
6669
6790
|
if (!conversationReady || lastUserSig.current === userSig) return;
|
|
6670
6791
|
lastUserSig.current = userSig;
|
|
6671
|
-
void
|
|
6672
|
-
}, [userSig, conversationReady,
|
|
6792
|
+
void runHandshake({ newConversation: false });
|
|
6793
|
+
}, [userSig, conversationReady, runHandshake]);
|
|
6673
6794
|
useEffect17(() => {
|
|
6674
6795
|
applyMode(hostElement, computeHostMode(options.mode, panelSize, isOpen));
|
|
6675
6796
|
}, [hostElement, isOpen, panelSize, options.mode]);
|
|
@@ -6788,8 +6909,9 @@ function App({ options, hostElement, bus }) {
|
|
|
6788
6909
|
[options.features.humanInLoop, handleToolResult, handleToolDecision]
|
|
6789
6910
|
);
|
|
6790
6911
|
const userMessageCount = () => messagesSig.value.reduce((n, m) => n + (m.role === "user" ? 1 : 0), 0);
|
|
6912
|
+
const effectiveForms = options.forms.list.length > 0 ? options.forms : remoteForms ?? options.forms;
|
|
6791
6913
|
const forms = useForms({
|
|
6792
|
-
forms:
|
|
6914
|
+
forms: effectiveForms,
|
|
6793
6915
|
persistence,
|
|
6794
6916
|
userContext: () => options.userContext,
|
|
6795
6917
|
messageCount: userMessageCount,
|
|
@@ -6822,18 +6944,18 @@ function App({ options, hostElement, bus }) {
|
|
|
6822
6944
|
const pageArea = options.pageContext?.area ? String(options.pageContext.area) : void 0;
|
|
6823
6945
|
const msgCount = useComputed7(() => messagesSig.value.length);
|
|
6824
6946
|
useEffect17(() => {
|
|
6825
|
-
if (conversationReady) forms.fire("pre-chat");
|
|
6826
|
-
}, [conversationReady, forms]);
|
|
6947
|
+
if (conversationReady && formsReady) forms.fire("pre-chat");
|
|
6948
|
+
}, [conversationReady, formsReady, forms]);
|
|
6827
6949
|
useEffect17(() => {
|
|
6828
|
-
if (!canSend) forms.fire("conversation-closed");
|
|
6829
|
-
}, [canSend, forms]);
|
|
6950
|
+
if (formsReady && !canSend) forms.fire("conversation-closed");
|
|
6951
|
+
}, [formsReady, canSend, forms]);
|
|
6830
6952
|
useEffect17(() => {
|
|
6831
|
-
if (conversationReady && pageArea) forms.fire("page-area");
|
|
6832
|
-
}, [conversationReady, pageArea, forms]);
|
|
6953
|
+
if (conversationReady && formsReady && pageArea) forms.fire("page-area");
|
|
6954
|
+
}, [conversationReady, formsReady, pageArea, forms]);
|
|
6833
6955
|
const idleMs = useMemo3(() => {
|
|
6834
|
-
const secs =
|
|
6956
|
+
const secs = effectiveForms.list.flatMap((f) => f.triggers).filter((t) => t.kind === "idle").map((t) => Number(t.param));
|
|
6835
6957
|
return secs.length ? Math.min(...secs) * 1e3 : 0;
|
|
6836
|
-
}, [
|
|
6958
|
+
}, [effectiveForms]);
|
|
6837
6959
|
useEffect17(() => {
|
|
6838
6960
|
if (!idleMs || !isOpen) return;
|
|
6839
6961
|
const id = setTimeout(() => forms.fire("idle"), idleMs);
|
|
@@ -6895,10 +7017,10 @@ function App({ options, hostElement, bus }) {
|
|
|
6895
7017
|
}, [transport, options.modules, unreadCountSig]);
|
|
6896
7018
|
const unreadSeeded = useRef9(false);
|
|
6897
7019
|
useEffect17(() => {
|
|
6898
|
-
if (!conversationReady || unreadSeeded.current) return;
|
|
7020
|
+
if (!activated || !conversationReady || unreadSeeded.current) return;
|
|
6899
7021
|
unreadSeeded.current = true;
|
|
6900
7022
|
refreshUnread();
|
|
6901
|
-
}, [conversationReady, refreshUnread]);
|
|
7023
|
+
}, [activated, conversationReady, refreshUnread]);
|
|
6902
7024
|
const handleOpen = useCallback6(() => {
|
|
6903
7025
|
log16.info("open", { mode: options.mode });
|
|
6904
7026
|
feedback.unlock();
|
|
@@ -6920,8 +7042,8 @@ function App({ options, hostElement, bus }) {
|
|
|
6920
7042
|
persistence.clearConversation();
|
|
6921
7043
|
setCanSend(true);
|
|
6922
7044
|
setFormMarkers([]);
|
|
6923
|
-
void
|
|
6924
|
-
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence,
|
|
7045
|
+
void runHandshake({ newConversation: true });
|
|
7046
|
+
}, [streaming, activeCancel, messagesSig, conversationIdSig, persistence, runHandshake, bus]);
|
|
6925
7047
|
const handleNewChat = useCallback6(() => {
|
|
6926
7048
|
handleClear();
|
|
6927
7049
|
setView("chat");
|
|
@@ -7010,9 +7132,13 @@ function App({ options, hostElement, bus }) {
|
|
|
7010
7132
|
log16.info("selectConversation", { conversationId: targetConversationId });
|
|
7011
7133
|
bus.emit("selectConversation", { conversationId: targetConversationId });
|
|
7012
7134
|
try {
|
|
7013
|
-
const res = await
|
|
7135
|
+
const [res, markers] = await Promise.all([
|
|
7136
|
+
transport.loadConversation(targetConversationId),
|
|
7137
|
+
fetchFormMarkers(targetConversationId)
|
|
7138
|
+
]);
|
|
7014
7139
|
const hydrated = res.messages.map(fromWireMessage);
|
|
7015
7140
|
messagesSig.value = hydrated;
|
|
7141
|
+
setFormMarkers(markers);
|
|
7016
7142
|
conversationIdSig.value = res.conversationId;
|
|
7017
7143
|
persistence.saveConversationId(res.conversationId);
|
|
7018
7144
|
if (res.agent) setAgent(res.agent);
|
|
@@ -7026,7 +7152,7 @@ function App({ options, hostElement, bus }) {
|
|
|
7026
7152
|
options.onError?.(err);
|
|
7027
7153
|
}
|
|
7028
7154
|
},
|
|
7029
|
-
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread]
|
|
7155
|
+
[transport, messagesSig, conversationIdSig, bus, options, persistence, refreshUnread, fetchFormMarkers]
|
|
7030
7156
|
);
|
|
7031
7157
|
useEffect17(() => {
|
|
7032
7158
|
const unsub = bindHostCommands(hostElement, {
|
|
@@ -7045,10 +7171,10 @@ function App({ options, hostElement, bus }) {
|
|
|
7045
7171
|
);
|
|
7046
7172
|
const enrichedFormMarkers = useMemo3(
|
|
7047
7173
|
() => formMarkers.map((m) => {
|
|
7048
|
-
const def =
|
|
7174
|
+
const def = effectiveForms.list.find((f) => f.id === m.formId);
|
|
7049
7175
|
return { ...m, title: def?.title, form: def };
|
|
7050
7176
|
}),
|
|
7051
|
-
[formMarkers,
|
|
7177
|
+
[formMarkers, effectiveForms]
|
|
7052
7178
|
);
|
|
7053
7179
|
const panelProps = {
|
|
7054
7180
|
options: effectiveOptions,
|
|
@@ -7333,7 +7459,7 @@ var TRACKED = {
|
|
|
7333
7459
|
clear: () => void 0,
|
|
7334
7460
|
suggestion: () => void 0,
|
|
7335
7461
|
toggleHistory: (p33) => ({ view: p33.view }),
|
|
7336
|
-
|
|
7462
|
+
handshake: () => void 0,
|
|
7337
7463
|
// Forms + human-in-the-loop — ids and outcomes, never values.
|
|
7338
7464
|
formSubmit: (p33) => ({ formId: p33.formId, skipped: p33.skipped }),
|
|
7339
7465
|
toolResult: () => void 0,
|
|
@@ -7502,7 +7628,7 @@ var EVENT_NAMES = [
|
|
|
7502
7628
|
"calloutDismiss",
|
|
7503
7629
|
// Lifecycle / errors
|
|
7504
7630
|
"error",
|
|
7505
|
-
"
|
|
7631
|
+
"handshake",
|
|
7506
7632
|
"visitorRebound",
|
|
7507
7633
|
"conversationRebound"
|
|
7508
7634
|
];
|
|
@@ -7559,7 +7685,7 @@ function mount(opts) {
|
|
|
7559
7685
|
);
|
|
7560
7686
|
};
|
|
7561
7687
|
renderApp();
|
|
7562
|
-
bus.on("
|
|
7688
|
+
bus.on("handshake", (response) => {
|
|
7563
7689
|
if (!response.config) return;
|
|
7564
7690
|
rawOptions = mergeServerConfig(rawOptions, response.config);
|
|
7565
7691
|
currentOptions = resolveOptions(rawOptions);
|