@helpai/elements 0.7.5 → 0.8.1
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 +134 -17
- package/package.json +1 -1
- package/schema.d.ts +35 -35
- package/web-component.mjs +134 -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,62 @@ 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
|
+
* The endpoint ALWAYS opens a 200 SSE stream (mirrors the AI SDK
|
|
1458
|
+
* resumable-stream lib): it replays the in-flight reply from the buffer, or —
|
|
1459
|
+
* when nothing is in flight (reply finished + the buffer was flushed, or
|
|
1460
|
+
* nothing ever started) — closes an EMPTY stream. So "nothing to resume" is
|
|
1461
|
+
* signalled by a segment that ends without a terminal chunk AND surfaced no
|
|
1462
|
+
* NEW events — not by a status code. We stop gracefully there; the completed
|
|
1463
|
+
* turn comes from the DB via start-session / list-messages.
|
|
1464
|
+
*
|
|
1465
|
+
* Returns (no throw) on a terminal chunk, an empty/no-new-events segment, or
|
|
1466
|
+
* a 204 / non-OK (a backend that signals "nothing" by status, or lacks the
|
|
1467
|
+
* endpoint). Retries only a segment that yielded new events but no finish — a
|
|
1468
|
+
* real mid-reply drop. Throws only if every attempt kept yielding new events
|
|
1469
|
+
* yet never finished: a genuinely stuck, incomplete reply.
|
|
1470
|
+
*/
|
|
1471
|
+
async *resumeStreamGen(ctrl, seenIds, immediate) {
|
|
1410
1472
|
for (let attempt = 1; attempt <= MAX_RESUME_ATTEMPTS && !ctrl.signal.aborted; attempt++) {
|
|
1411
|
-
|
|
1412
|
-
|
|
1473
|
+
if (!immediate || attempt > 1) {
|
|
1474
|
+
await sleep(RESUME_BACKOFF_MS * attempt, ctrl.signal);
|
|
1475
|
+
if (ctrl.signal.aborted) return;
|
|
1476
|
+
}
|
|
1413
1477
|
let res;
|
|
1414
1478
|
try {
|
|
1415
1479
|
res = await this.fetchImpl(this.resumeUrl(seenIds.size), {
|
|
@@ -1427,7 +1491,12 @@ var AgentTransport = class {
|
|
|
1427
1491
|
log4.debug("resume \u2192 no in-flight stream", { status: res.status });
|
|
1428
1492
|
return;
|
|
1429
1493
|
}
|
|
1494
|
+
const before = seenIds.size;
|
|
1430
1495
|
if (yield* this.drain(parseChatStream(res, ctrl.signal), seenIds, ctrl)) return;
|
|
1496
|
+
if (seenIds.size === before) {
|
|
1497
|
+
log4.debug("resume \u2192 stream closed with no new events; nothing to resume");
|
|
1498
|
+
return;
|
|
1499
|
+
}
|
|
1431
1500
|
}
|
|
1432
1501
|
if (ctrl.signal.aborted) return;
|
|
1433
1502
|
throw new StreamError("stream incomplete after resume attempts", "server");
|
|
@@ -3779,7 +3848,7 @@ function ConversationList({ transport, strings, visitorId, onSelect, onNewChat }
|
|
|
3779
3848
|
let cancelled = false;
|
|
3780
3849
|
transport.listSessions({ visitorId }).then((res) => {
|
|
3781
3850
|
if (cancelled) return;
|
|
3782
|
-
setChats(res.sessions);
|
|
3851
|
+
setChats(res.sessions ?? []);
|
|
3783
3852
|
setState("loaded");
|
|
3784
3853
|
}).catch((err) => {
|
|
3785
3854
|
if (cancelled) return;
|
|
@@ -4670,7 +4739,7 @@ function HelpRoot({ transport, strings, config, nav, panelProps }) {
|
|
|
4670
4739
|
setState("loading");
|
|
4671
4740
|
transport.listContent({ tags, sortBy: "order", limit: 100 }).then((res) => {
|
|
4672
4741
|
if (cancelled) return;
|
|
4673
|
-
setItems(res.items);
|
|
4742
|
+
setItems(res.items ?? []);
|
|
4674
4743
|
setState("loaded");
|
|
4675
4744
|
}).catch((err) => {
|
|
4676
4745
|
if (cancelled) return;
|
|
@@ -4744,7 +4813,7 @@ function HomeRoot(props2) {
|
|
|
4744
4813
|
useEffect12(() => {
|
|
4745
4814
|
if (!config.showRecentMessages) return;
|
|
4746
4815
|
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 }));
|
|
4816
|
+
transport.listSessions({ limit: 1 }).then((res) => !cancelled && setRecent(res.sessions?.[0] ?? null)).catch((err) => !cancelled && log13.warn("listSessions (home) failed", { err }));
|
|
4748
4817
|
return () => {
|
|
4749
4818
|
cancelled = true;
|
|
4750
4819
|
};
|
|
@@ -4752,7 +4821,7 @@ function HomeRoot(props2) {
|
|
|
4752
4821
|
useEffect12(() => {
|
|
4753
4822
|
if (!config.contentTags?.length) return;
|
|
4754
4823
|
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 }));
|
|
4824
|
+
transport.listContent({ tags: config.contentTags, limit: 6 }).then((res) => !cancelled && setContent(res.items ?? [])).catch((err) => !cancelled && log13.warn("listContent (home) failed", { err }));
|
|
4756
4825
|
return () => {
|
|
4757
4826
|
cancelled = true;
|
|
4758
4827
|
};
|
|
@@ -4848,7 +4917,7 @@ function NewsRoot({ transport, strings, config, nav, panelProps }) {
|
|
|
4848
4917
|
setState("loading");
|
|
4849
4918
|
transport.listContent({ tags, sortBy: "publishedAt", sortOrder: "desc" }).then((res) => {
|
|
4850
4919
|
if (cancelled) return;
|
|
4851
|
-
setItems(res.items);
|
|
4920
|
+
setItems(res.items ?? []);
|
|
4852
4921
|
setState("loaded");
|
|
4853
4922
|
}).catch((err) => {
|
|
4854
4923
|
if (cancelled) return;
|
|
@@ -5461,10 +5530,11 @@ function App({ options, hostElement, bus }) {
|
|
|
5461
5530
|
if (isResume) {
|
|
5462
5531
|
setLoadingMessages(true);
|
|
5463
5532
|
try {
|
|
5464
|
-
const chat = res.messages ? { sessionId: persistedChatId, canContinue: res.canContinue ?? true, messages: res.messages } : await transport.loadSession(persistedChatId);
|
|
5533
|
+
const chat = res.messages?.length ? { sessionId: persistedChatId, canContinue: res.canContinue ?? true, messages: res.messages } : await transport.loadSession(persistedChatId);
|
|
5465
5534
|
if (isStale()) return;
|
|
5466
|
-
|
|
5467
|
-
|
|
5535
|
+
const loaded = (chat.messages ?? []).map(fromWireMessage);
|
|
5536
|
+
if (loaded.length) messagesSig.value = loaded;
|
|
5537
|
+
setCanSend(chat.canContinue ?? true);
|
|
5468
5538
|
setSuggestions(chat.suggestions ?? []);
|
|
5469
5539
|
} catch (err) {
|
|
5470
5540
|
if (isStale()) return;
|
|
@@ -5491,10 +5561,57 @@ function App({ options, hostElement, bus }) {
|
|
|
5491
5561
|
useEffect17(() => {
|
|
5492
5562
|
transport.setContext(options.userContext, toWirePageContext(options.pageContext));
|
|
5493
5563
|
}, [transport, userSig, pageSig]);
|
|
5564
|
+
const runResume = useCallback8(
|
|
5565
|
+
async (handle) => {
|
|
5566
|
+
let bubble = null;
|
|
5567
|
+
try {
|
|
5568
|
+
for await (const evt of handle.iter) {
|
|
5569
|
+
if (!bubble) {
|
|
5570
|
+
bubble = makeAssistantMessage();
|
|
5571
|
+
reducer.attach(bubble);
|
|
5572
|
+
messagesSig.value = [...messagesSig.value, bubble];
|
|
5573
|
+
setStreaming(true);
|
|
5574
|
+
setActiveCancel(() => handle.cancel);
|
|
5575
|
+
log16.info("resumed in-flight reply on mount");
|
|
5576
|
+
}
|
|
5577
|
+
reducer.apply(evt.chunk);
|
|
5578
|
+
if (evt.chunk.type === "finish" && evt.chunk.canContinue === false) setCanSend(false);
|
|
5579
|
+
}
|
|
5580
|
+
if (bubble) {
|
|
5581
|
+
bubble.status = "complete";
|
|
5582
|
+
feedback.play("messageReceived");
|
|
5583
|
+
emitMessage(bus, options, "assistant", assistantText(bubble));
|
|
5584
|
+
}
|
|
5585
|
+
} catch (err) {
|
|
5586
|
+
if (bubble) {
|
|
5587
|
+
bubble.status = "error";
|
|
5588
|
+
bubble.errorText = errorMessageFor(err, options.strings);
|
|
5589
|
+
feedback.play("error");
|
|
5590
|
+
}
|
|
5591
|
+
log16.debug("mount resume ended without completing", { err });
|
|
5592
|
+
} finally {
|
|
5593
|
+
if (bubble) {
|
|
5594
|
+
reducer.detach();
|
|
5595
|
+
setStreaming(false);
|
|
5596
|
+
setActiveCancel(null);
|
|
5597
|
+
messagesSig.value = [...messagesSig.value];
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
},
|
|
5601
|
+
[reducer, messagesSig, feedback, bus, options]
|
|
5602
|
+
);
|
|
5494
5603
|
useEffect17(() => {
|
|
5495
5604
|
void runStartSession({ newConversation: false });
|
|
5605
|
+
const persistedSession = sessionIdSig.value;
|
|
5606
|
+
let resume = null;
|
|
5607
|
+
if (persistedSession) {
|
|
5608
|
+
transport.primeIdentity(visitorId, persistedSession);
|
|
5609
|
+
resume = transport.resumeStream();
|
|
5610
|
+
void runResume(resume);
|
|
5611
|
+
}
|
|
5496
5612
|
return () => {
|
|
5497
5613
|
connectGenRef.current++;
|
|
5614
|
+
resume?.cancel();
|
|
5498
5615
|
};
|
|
5499
5616
|
}, []);
|
|
5500
5617
|
const lastUserSig = useRef8(userSig);
|