mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
feat(coach): quote the real BP range, address by name, render action cards
Feed the user's own usual blood-pressure range — the median plus or minus MAD band, computed from the readings already in the snapshot — into the Coach context with a clause to quote those bounds verbatim, so a band is never invented from window means. Thread the stored first name into the prompt and surface it sparingly on roughly one turn in three, never as a rote greeting. Add the context-layer rule that names the relevant context before a suggestion, a single-chart-token allowance that works on every provider, and a per-turn opener hint. Wire the already-shipped suggested-action SSE frame to render: the streaming hook collects it and the message thread shows the action card, mirroring the reminder-suggestion path. Extend the eval golden set with band-verbatim, chart-token, and no-overclaim cases, and pin a red-team case proving the chart-token syntax cannot smuggle a dose past the outbound screen.
This commit is contained in:
1 parent
b393527cac
commit
5674f10b2f
10 files changed
+329
-13
No files matched your search
@@ -73,6 +73,7 @@ function streaming(
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: null,
|
||||
|
||||
@@ -45,6 +45,7 @@ import { ProseBlocks } from "@/components/insights/prose-blocks";
|
||||
|
||||
import { SourceChips } from "./source-chips";
|
||||
import { ReminderSuggestionCard } from "./reminder-suggestion-card";
|
||||
import { SuggestedActionCard } from "./suggested-action-card";
|
||||
import { StreamedProse } from "./streamed-prose";
|
||||
import { MessageTokenFooter } from "./message-token-footer";
|
||||
import type {
|
||||
@@ -766,6 +767,7 @@ export function MessageThread({
|
||||
content={streaming.content}
|
||||
metricSource={streaming.metricSource}
|
||||
suggestion={streaming.suggestion}
|
||||
suggestedAction={streaming.suggestedAction}
|
||||
providerType={streaming.inProgress ? "streaming" : null}
|
||||
inProgress={streaming.inProgress}
|
||||
errorCode={streaming.errorCode}
|
||||
@@ -794,6 +796,14 @@ interface ChatBubbleProps {
|
||||
* instead; the bubble falls back to that so the card survives reload.
|
||||
*/
|
||||
suggestion?: import("@/lib/ai/coach/types").CoachSuggestion | null;
|
||||
/**
|
||||
* v1.22 (W7/W6) — live generalised confirm-card action from the streaming
|
||||
* hook. Persisted messages carry it on `metricSource.suggestedAction`; the
|
||||
* bubble falls back to that so the card survives reload.
|
||||
*/
|
||||
suggestedAction?:
|
||||
| import("@/lib/ai/coach/suggest-action").CoachSuggestedAction
|
||||
| null;
|
||||
providerType?: string | null;
|
||||
inProgress?: boolean;
|
||||
errorCode?: string | null;
|
||||
@@ -834,6 +844,7 @@ function ChatBubble({
|
||||
content,
|
||||
metricSource,
|
||||
suggestion,
|
||||
suggestedAction,
|
||||
providerType,
|
||||
inProgress,
|
||||
errorCode,
|
||||
@@ -1161,6 +1172,16 @@ function ChatBubble({
|
||||
const sug = suggestion ?? metricSource?.suggestion ?? null;
|
||||
return sug ? <ReminderSuggestionCard suggestion={sug} /> : null;
|
||||
})()}
|
||||
{/* v1.22 (W7/W6) — generalised confirm-card action. Live from the
|
||||
streaming hook, or restored from persisted message provenance on
|
||||
reload. Mirrors the reminder-suggestion block above. */}
|
||||
{!inProgress &&
|
||||
!errorCode &&
|
||||
(() => {
|
||||
const action =
|
||||
suggestedAction ?? metricSource?.suggestedAction ?? null;
|
||||
return action ? <SuggestedActionCard action={action} /> : null;
|
||||
})()}
|
||||
{/* v1.18.9 — quiet per-message token footer. The just-finished
|
||||
streaming turn reads the `done.usage` envelope; a persisted /
|
||||
reloaded turn reads the message's own `tokensUsed` + `model`.
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
CoachSuggestion,
|
||||
CoachUsage,
|
||||
} from "@/lib/ai/coach/types";
|
||||
import type { CoachSuggestedAction } from "@/lib/ai/coach/suggest-action";
|
||||
import { apiDelete, apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
|
||||
@@ -186,6 +187,12 @@ export interface CoachStreamingMessage {
|
||||
* `suggestion` frame, if the turn carried one. Null otherwise.
|
||||
*/
|
||||
suggestion: CoachSuggestion | null;
|
||||
/**
|
||||
* v1.22 (W7/W6) — generalised confirm-card action from the additive
|
||||
* `suggestedAction` frame (mirrors `suggestion`). Persisted messages carry it
|
||||
* on `metricSource.suggestedAction` instead. Null otherwise.
|
||||
*/
|
||||
suggestedAction: CoachSuggestedAction | null;
|
||||
/** True until the `done` frame closes the stream. */
|
||||
inProgress: boolean;
|
||||
/** Final messageId once `done` lands; null otherwise. */
|
||||
@@ -236,6 +243,7 @@ const EMPTY_STREAMING: CoachStreamingMessage = {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: null,
|
||||
@@ -330,6 +338,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: true,
|
||||
messageId: null,
|
||||
errorCode: null,
|
||||
@@ -344,6 +353,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: "coach.network",
|
||||
@@ -378,6 +388,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: "coach.network",
|
||||
@@ -391,6 +402,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: `coach.http.${response.status}`,
|
||||
@@ -429,6 +441,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: "",
|
||||
metricSource: null,
|
||||
suggestion: null,
|
||||
suggestedAction: null,
|
||||
inProgress: false,
|
||||
messageId: null,
|
||||
errorCode: structured ?? `coach.http.${response.status}`,
|
||||
@@ -449,6 +462,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
let collectedContent = "";
|
||||
let collectedProvenance: CoachProvenance | null = null;
|
||||
let collectedSuggestion: CoachSuggestion | null = null;
|
||||
let collectedSuggestedAction: CoachSuggestedAction | null = null;
|
||||
let lastError: string | null = null;
|
||||
let messageId: string | null = null;
|
||||
let collectedUsage: CoachUsage | null = null;
|
||||
@@ -483,6 +497,13 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
suggestion: evt.suggestion,
|
||||
}));
|
||||
break;
|
||||
case "suggestedAction":
|
||||
collectedSuggestedAction = evt.suggestedAction;
|
||||
setStreaming((prev) => ({
|
||||
...prev,
|
||||
suggestedAction: evt.suggestedAction,
|
||||
}));
|
||||
break;
|
||||
case "reasoning":
|
||||
// v1.19.1 — the thinking disclosure was removed, so the
|
||||
// additive `reasoning` frame is accepted off the wire and
|
||||
@@ -522,6 +543,7 @@ export function useSendCoachMessage(opts: UseSendCoachMessageOptions = {}) {
|
||||
content: collectedContent,
|
||||
metricSource: collectedProvenance,
|
||||
suggestion: collectedSuggestion,
|
||||
suggestedAction: collectedSuggestedAction,
|
||||
inProgress: false,
|
||||
messageId,
|
||||
errorCode: lastError,
|
||||
|
||||
Reference in new issue
Block a user