mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
feat(coach): stream replies incrementally, collapsible history and provenance, single disclaimer
Token-by-token streaming was buffering on the server. The chat route enqueued every tokenised SSE frame synchronously inside the stream's start() callback, so the runtime coalesced them into a single network read and the client painted the full answer at once despite the per-token render path already wired in use-coach. Make the emit callback async and yield to the event loop between token frames so each one flushes as its own chunk; the visible cadence now reads ChatGPT/Claude- style. Refusal and error paths stay single-frame. The conversation history was a permanent left column on lg+, eating chat width on laptops and reading as a takeover behind the narrowed drawer cap. It now lives behind an always-visible "Conversations" toggle on every viewport and opens as the left-edge tray on demand; the thread keeps the full width by default and never loses it on mobile. The provenance block (source chips plus raw key-values) rendered fully expanded under every answer and was often taller than the reply. Fold the chips into the existing collapsed disclosure alongside the key- values so the whole grounding is one click-to-expand surface, collapsed by default, with the disclosure's aria-expanded/aria-controls intact. The clinical-decisions disclaimer rendered both at the bottom of the thread and in the sources-rail footer. Consolidate to a single line directly above the composer, always visible on every viewport. The auto-growing composer (single line growing to a capped multi-line with internal scroll, Enter-to-send, Shift+Enter-newline) and the aria-live streaming region are preserved. Plain-text React children throughout; no new dependencies.
This commit is contained in:
1 parent
7ab2cbaa56
commit
9e6865841d
11 files changed
+265
-140
No files matched your search
@@ -100,6 +100,20 @@ function tokeniseForStreaming(content: string): string[] {
|
||||
return matches ?? [content];
|
||||
}
|
||||
|
||||
/**
|
||||
* v1.12.0 — yield control back to the event loop for one tick so the
|
||||
* stream controller flushes the just-enqueued frame before the next one
|
||||
* is produced. `setTimeout(0)` (rather than a bare `Promise.resolve()`
|
||||
* microtask) hands the turn back to the platform's stream pump so each
|
||||
* SSE frame lands in its own network chunk; a microtask would drain
|
||||
* before the runtime gets a chance to flush. The delay is intentionally
|
||||
* zero — we want incremental delivery, not an artificial typewriter
|
||||
* pause.
|
||||
*/
|
||||
function flushTick(): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
interface CoachTurn {
|
||||
role: "user" | "assistant";
|
||||
content: string;
|
||||
@@ -485,9 +499,20 @@ Reply now as the assistant, in ${locale === "de" ? "German" : "English"}.`;
|
||||
});
|
||||
|
||||
// ── Stream the body to the client ────────────────────────────
|
||||
const stream = createSseStream((controller) => {
|
||||
// v1.12.0 — yield to the event loop between token frames so each one
|
||||
// flushes as its own network chunk. The provider clients return the
|
||||
// full reply in one shot; without the yield the whole tokenised body
|
||||
// was enqueued synchronously inside `start()` and the runtime
|
||||
// coalesced every frame into a single read, so the client painted the
|
||||
// answer all at once despite the per-token render path in `use-coach`.
|
||||
// A zero-delay yield is enough to land each frame on its own tick —
|
||||
// the visible cadence reads ChatGPT/Claude-style without a contrived
|
||||
// sleep. The refusal + error paths stay single-frame (nothing to
|
||||
// pace).
|
||||
const stream = createSseStream(async (controller) => {
|
||||
for (const tok of tokeniseForStreaming(replyText)) {
|
||||
controller.enqueue(encodeFrame({ type: "token", token: tok }));
|
||||
await flushTick();
|
||||
}
|
||||
controller.enqueue(
|
||||
encodeFrame({
|
||||
|
||||
Reference in new issue
Block a user