@helpai/elements 0.7.5 → 0.8.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/elements-web-component.esm.js +27 -27
- package/elements-web-component.esm.js.map +3 -3
- package/elements.cjs.js +27 -27
- package/elements.cjs.js.map +3 -3
- package/elements.esm.js +27 -27
- package/elements.esm.js.map +3 -3
- package/elements.js +27 -27
- package/elements.js.map +3 -3
- package/index.d.ts +1 -1
- package/index.mjs +120 -17
- package/package.json +1 -1
- package/schema.d.ts +35 -35
- package/web-component.mjs +120 -17
package/web-component.mjs
CHANGED
|
@@ -1226,6 +1226,17 @@ var AgentTransport = class {
|
|
|
1226
1226
|
this.userContext = userContext;
|
|
1227
1227
|
this.pageContext = pageContext;
|
|
1228
1228
|
}
|
|
1229
|
+
/**
|
|
1230
|
+
* Seed the visitor + session identity from persistence BEFORE the first
|
|
1231
|
+
* `startSession` resolves. A mount-time `resumeStream()` runs in parallel
|
|
1232
|
+
* with `startSession`, so without this its envelope would carry no
|
|
1233
|
+
* `sessionId` and the resume GET couldn't identify the thread. `startSession`
|
|
1234
|
+
* later overwrites these with the server's authoritative ids (rebind-safe).
|
|
1235
|
+
*/
|
|
1236
|
+
primeIdentity(visitorId, sessionId) {
|
|
1237
|
+
if (visitorId) this.visitorId = visitorId;
|
|
1238
|
+
if (sessionId) this.sessionId = sessionId;
|
|
1239
|
+
}
|
|
1229
1240
|
/** Build the per-request envelope from the current transport state. */
|
|
1230
1241
|
envelope() {
|
|
1231
1242
|
const out = {};
|
|
@@ -1309,9 +1320,10 @@ var AgentTransport = class {
|
|
|
1309
1320
|
cursor: params.cursor
|
|
1310
1321
|
});
|
|
1311
1322
|
const res = await this.getJson(url.toString(), "listSessions");
|
|
1312
|
-
|
|
1323
|
+
const sessions = res.sessions ?? [];
|
|
1324
|
+
log4.debug("listSessions \u2190", { count: sessions.length, nextCursor: res.nextCursor });
|
|
1313
1325
|
return {
|
|
1314
|
-
sessions:
|
|
1326
|
+
sessions: sessions.map((session) => ({
|
|
1315
1327
|
sessionId: session.sessionId,
|
|
1316
1328
|
title: session.title,
|
|
1317
1329
|
lastMessageAt: session.lastMessageAt,
|
|
@@ -1327,16 +1339,17 @@ var AgentTransport = class {
|
|
|
1327
1339
|
url.searchParams.set("sessionId", sessionId);
|
|
1328
1340
|
log4.debug("loadSession \u2192", { sessionId, visitorId: this.visitorId });
|
|
1329
1341
|
const res = await this.getJson(url.toString(), "loadSession");
|
|
1342
|
+
const messages = res.messages ?? [];
|
|
1330
1343
|
log4.debug("loadSession \u2190", {
|
|
1331
1344
|
sessionId: res.sessionId,
|
|
1332
1345
|
canContinue: res.canContinue,
|
|
1333
|
-
messageCount:
|
|
1346
|
+
messageCount: messages.length,
|
|
1334
1347
|
hasSuggestions: !!res.suggestions
|
|
1335
1348
|
});
|
|
1336
1349
|
return {
|
|
1337
1350
|
sessionId: res.sessionId,
|
|
1338
|
-
canContinue: res.canContinue,
|
|
1339
|
-
messages
|
|
1351
|
+
canContinue: res.canContinue ?? true,
|
|
1352
|
+
messages,
|
|
1340
1353
|
agent: res.agent,
|
|
1341
1354
|
suggestions: res.suggestions
|
|
1342
1355
|
};
|
|
@@ -1370,8 +1383,9 @@ var AgentTransport = class {
|
|
|
1370
1383
|
}
|
|
1371
1384
|
log4.debug("listContent \u2192", query);
|
|
1372
1385
|
const res = await this.getJson(url.toString(), "listContent");
|
|
1373
|
-
|
|
1374
|
-
|
|
1386
|
+
const items = res.items ?? [];
|
|
1387
|
+
log4.debug("listContent \u2190", { count: items.length, total: res.pagination?.total });
|
|
1388
|
+
return { ...res, items };
|
|
1375
1389
|
}
|
|
1376
1390
|
async saveUserPrefs(userPrefs) {
|
|
1377
1391
|
log4.debug("saveUserPrefs \u2192", {
|
|
@@ -1404,12 +1418,53 @@ var AgentTransport = class {
|
|
|
1404
1418
|
const cancel = () => this.cancelStream(ctrl, body.sessionId);
|
|
1405
1419
|
return { iter, cancel };
|
|
1406
1420
|
}
|
|
1421
|
+
/**
|
|
1422
|
+
* Re-attach to an in-flight reply on a cold page load / refresh.
|
|
1423
|
+
*
|
|
1424
|
+
* UNCONDITIONAL by design — the caller runs this in parallel with
|
|
1425
|
+
* `startSession`, never gated on whether there are persisted messages. If
|
|
1426
|
+
* the user refreshed while the assistant was still streaming, the
|
|
1427
|
+
* `startSession` reply carries no messages (the turn isn't persisted until
|
|
1428
|
+
* it finishes) and THIS is the channel that resumes the live reply. When
|
|
1429
|
+
* nothing is in flight the server answers 204 and the handle yields nothing
|
|
1430
|
+
* (the common case — no bubble is created).
|
|
1431
|
+
*
|
|
1432
|
+
* `cancel` only detaches the client (aborts the local read); unlike
|
|
1433
|
+
* `sendMessage` it does NOT POST `/cancel-stream` — the reply may still be
|
|
1434
|
+
* generating for another tab or about to be persisted, and a page-load
|
|
1435
|
+
* resume must never tear that down.
|
|
1436
|
+
*/
|
|
1437
|
+
resumeStream() {
|
|
1438
|
+
const ctrl = new AbortController();
|
|
1439
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
1440
|
+
const iter = this.resumeStreamGen(ctrl, seenIds, true);
|
|
1441
|
+
return { iter, cancel: () => ctrl.abort() };
|
|
1442
|
+
}
|
|
1407
1443
|
// ---- Stream orchestration ---------------------------------------------
|
|
1408
1444
|
async *runStream(body, ctrl, seenIds) {
|
|
1409
1445
|
if (yield* this.drain(this.openMessageStream(body, ctrl.signal), seenIds, ctrl)) return;
|
|
1446
|
+
yield* this.resumeStreamGen(ctrl, seenIds, false);
|
|
1447
|
+
}
|
|
1448
|
+
/**
|
|
1449
|
+
* Re-attach to an in-flight reply via GET /stream-resume, yielding the
|
|
1450
|
+
* (deduped) continuation. Two callers:
|
|
1451
|
+
* - after a POST /stream-message drops mid-reply (`immediate=false` — back
|
|
1452
|
+
* off first, the reply may still be spinning up server-side);
|
|
1453
|
+
* - on a cold page load / refresh (`immediate=true` — hit it at once, in
|
|
1454
|
+
* parallel with start-session, so a reply that was streaming when the
|
|
1455
|
+
* user reloaded keeps flowing).
|
|
1456
|
+
*
|
|
1457
|
+
* Returns (no throw) on 204 / non-OK — nothing in flight (or a backend
|
|
1458
|
+
* without the endpoint → graceful no-op) — or once a terminal chunk is
|
|
1459
|
+
* drained. Throws only when every attempt produced a 200 that dropped before
|
|
1460
|
+
* finishing: a genuinely incomplete reply the UI should flag with a retry.
|
|
1461
|
+
*/
|
|
1462
|
+
async *resumeStreamGen(ctrl, seenIds, immediate) {
|
|
1410
1463
|
for (let attempt = 1; attempt <= MAX_RESUME_ATTEMPTS && !ctrl.signal.aborted; attempt++) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1464
|
+
if (!immediate || attempt > 1) {
|
|
1465
|
+
await sleep(RESUME_BACKOFF_MS * attempt, ctrl.signal);
|
|
1466
|
+
if (ctrl.signal.aborted) return;
|
|
1467
|
+
}
|
|
1413
1468
|
let res;
|
|
1414
1469
|
try {
|
|
1415
1470
|
res = await this.fetchImpl(this.resumeUrl(seenIds.size), {
|
|
@@ -3779,7 +3834,7 @@ function ConversationList({ transport, strings, visitorId, onSelect, onNewChat }
|
|
|
3779
3834
|
let cancelled = false;
|
|
3780
3835
|
transport.listSessions({ visitorId }).then((res) => {
|
|
3781
3836
|
if (cancelled) return;
|
|
3782
|
-
setChats(res.sessions);
|
|
3837
|
+
setChats(res.sessions ?? []);
|
|
3783
3838
|
setState("loaded");
|
|
3784
3839
|
}).catch((err) => {
|
|
3785
3840
|
if (cancelled) return;
|
|
@@ -4670,7 +4725,7 @@ function HelpRoot({ transport, strings, config, nav, panelProps }) {
|
|
|
4670
4725
|
setState("loading");
|
|
4671
4726
|
transport.listContent({ tags, sortBy: "order", limit: 100 }).then((res) => {
|
|
4672
4727
|
if (cancelled) return;
|
|
4673
|
-
setItems(res.items);
|
|
4728
|
+
setItems(res.items ?? []);
|
|
4674
4729
|
setState("loaded");
|
|
4675
4730
|
}).catch((err) => {
|
|
4676
4731
|
if (cancelled) return;
|
|
@@ -4744,7 +4799,7 @@ function HomeRoot(props2) {
|
|
|
4744
4799
|
useEffect12(() => {
|
|
4745
4800
|
if (!config.showRecentMessages) return;
|
|
4746
4801
|
let cancelled = false;
|
|
4747
|
-
transport.listSessions({ limit: 1 }).then((res) => !cancelled && setRecent(res.sessions[0] ?? null)).catch((err) => !cancelled && log13.warn("listSessions (home) failed", { err }));
|
|
4802
|
+
transport.listSessions({ limit: 1 }).then((res) => !cancelled && setRecent(res.sessions?.[0] ?? null)).catch((err) => !cancelled && log13.warn("listSessions (home) failed", { err }));
|
|
4748
4803
|
return () => {
|
|
4749
4804
|
cancelled = true;
|
|
4750
4805
|
};
|
|
@@ -4752,7 +4807,7 @@ function HomeRoot(props2) {
|
|
|
4752
4807
|
useEffect12(() => {
|
|
4753
4808
|
if (!config.contentTags?.length) return;
|
|
4754
4809
|
let cancelled = false;
|
|
4755
|
-
transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items)).catch((err) => !cancelled && log13.warn("listContent (home) failed", { err }));
|
|
4810
|
+
transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log13.warn("listContent (home) failed", { err }));
|
|
4756
4811
|
return () => {
|
|
4757
4812
|
cancelled = true;
|
|
4758
4813
|
};
|
|
@@ -4848,7 +4903,7 @@ function NewsRoot({ transport, strings, config, nav, panelProps }) {
|
|
|
4848
4903
|
setState("loading");
|
|
4849
4904
|
transport.listContent({ tags, sortBy: "publishedAt", sortOrder: "desc" }).then((res) => {
|
|
4850
4905
|
if (cancelled) return;
|
|
4851
|
-
setItems(res.items);
|
|
4906
|
+
setItems(res.items ?? []);
|
|
4852
4907
|
setState("loaded");
|
|
4853
4908
|
}).catch((err) => {
|
|
4854
4909
|
if (cancelled) return;
|
|
@@ -5461,10 +5516,11 @@ function App({ options, hostElement, bus }) {
|
|
|
5461
5516
|
if (isResume) {
|
|
5462
5517
|
setLoadingMessages(true);
|
|
5463
5518
|
try {
|
|
5464
|
-
const chat = res.messages ? { sessionId: persistedChatId, canContinue: res.canContinue ?? true, messages: res.messages } : await transport.loadSession(persistedChatId);
|
|
5519
|
+
const chat = res.messages?.length ? { sessionId: persistedChatId, canContinue: res.canContinue ?? true, messages: res.messages } : await transport.loadSession(persistedChatId);
|
|
5465
5520
|
if (isStale()) return;
|
|
5466
|
-
|
|
5467
|
-
|
|
5521
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
5522
|
+
if (loaded.length) messagesSig.value = loaded;
|
|
5523
|
+
setCanSend(chat.canContinue ?? true);
|
|
5468
5524
|
setSuggestions(chat.suggestions ?? []);
|
|
5469
5525
|
} catch (err) {
|
|
5470
5526
|
if (isStale()) return;
|
|
@@ -5491,10 +5547,57 @@ function App({ options, hostElement, bus }) {
|
|
|
5491
5547
|
useEffect17(() => {
|
|
5492
5548
|
transport.setContext(options.userContext, toWirePageContext(options.pageContext));
|
|
5493
5549
|
}, [transport, userSig, pageSig]);
|
|
5550
|
+
const runResume = useCallback8(
|
|
5551
|
+
async (handle) => {
|
|
5552
|
+
let bubble = null;
|
|
5553
|
+
try {
|
|
5554
|
+
for await (const evt of handle.iter) {
|
|
5555
|
+
if (!bubble) {
|
|
5556
|
+
bubble = makeAssistantMessage();
|
|
5557
|
+
reducer.attach(bubble);
|
|
5558
|
+
messagesSig.value = [...messagesSig.value, bubble];
|
|
5559
|
+
setStreaming(true);
|
|
5560
|
+
setActiveCancel(() => handle.cancel);
|
|
5561
|
+
log16.info("resumed in-flight reply on mount");
|
|
5562
|
+
}
|
|
5563
|
+
reducer.apply(evt.chunk);
|
|
5564
|
+
if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) setCanSend(false);
|
|
5565
|
+
}
|
|
5566
|
+
if (bubble) {
|
|
5567
|
+
bubble.status = "complete";
|
|
5568
|
+
feedback.play("messageReceived");
|
|
5569
|
+
emitMessage(bus, options, "assistant", assistantText(bubble));
|
|
5570
|
+
}
|
|
5571
|
+
} catch (err) {
|
|
5572
|
+
if (bubble) {
|
|
5573
|
+
bubble.status = "error";
|
|
5574
|
+
bubble.errorText = errorMessageFor(err, options.strings);
|
|
5575
|
+
feedback.play("error");
|
|
5576
|
+
}
|
|
5577
|
+
log16.debug("mount resume ended without completing", { err });
|
|
5578
|
+
} finally {
|
|
5579
|
+
if (bubble) {
|
|
5580
|
+
reducer.detach();
|
|
5581
|
+
setStreaming(false);
|
|
5582
|
+
setActiveCancel(null);
|
|
5583
|
+
messagesSig.value = [...messagesSig.value];
|
|
5584
|
+
}
|
|
5585
|
+
}
|
|
5586
|
+
},
|
|
5587
|
+
[reducer, messagesSig, feedback, bus, options]
|
|
5588
|
+
);
|
|
5494
5589
|
useEffect17(() => {
|
|
5495
5590
|
void runStartSession({ newConversation: false });
|
|
5591
|
+
const persistedSession = sessionIdSig.value;
|
|
5592
|
+
let resume = null;
|
|
5593
|
+
if (persistedSession) {
|
|
5594
|
+
transport.primeIdentity(visitorId, persistedSession);
|
|
5595
|
+
resume = transport.resumeStream();
|
|
5596
|
+
void runResume(resume);
|
|
5597
|
+
}
|
|
5496
5598
|
return () => {
|
|
5497
5599
|
connectGenRef.current++;
|
|
5600
|
+
resume?.cancel();
|
|
5498
5601
|
};
|
|
5499
5602
|
}, []);
|
|
5500
5603
|
const lastUserSig = useRef8(userSig);
|