mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(01-20): render explicit assessment states
- prioritize safe fallback text over provider metadata - keep terminal and pending card geometry stable
This commit is contained in:
1 parent
b92b3e4261
commit
0bf3a3573d
3 files changed
+57
-4
No files matched your search
@@ -17,6 +17,7 @@ import { cn } from "@/lib/utils";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
import { AskCoachIconButton } from "@/components/insights/ask-coach-action";
|
||||
import type { CoachLaunchScope } from "@/lib/insights/coach-launch-context";
|
||||
import type { AssessmentStatus } from "@/lib/insights/status-shared";
|
||||
|
||||
// ─── Types ────────────────────────────────────────────────
|
||||
|
||||
@@ -62,6 +63,12 @@ interface InsightStatusCardProps {
|
||||
* card is being assembled, not stuck. The client polls until text lands.
|
||||
*/
|
||||
preparing?: boolean;
|
||||
/**
|
||||
* Explicit producer/hook state. Legacy scalar props remain supported for
|
||||
* direct mounts while routes roll forward, but this discriminator owns
|
||||
* branch ordering whenever it is present.
|
||||
*/
|
||||
assessment?: AssessmentStatus;
|
||||
/**
|
||||
* v1.21.0 (C4 H2) — opt-in "Ask the Coach about this assessment" hand-off.
|
||||
* When `coachQuestion` is set (the metric-aware caller supplies it), the
|
||||
@@ -88,6 +95,7 @@ export function InsightStatusCard({
|
||||
updatedAt,
|
||||
loading = false,
|
||||
preparing = false,
|
||||
assessment,
|
||||
coachQuestion,
|
||||
coachScope,
|
||||
coachAutoSend,
|
||||
@@ -99,12 +107,22 @@ export function InsightStatusCard({
|
||||
// suppressed in full so the layout collapses naturally.
|
||||
if (!flags.insightStatus) return null;
|
||||
|
||||
const displayText =
|
||||
assessment?.kind === "generated" ||
|
||||
assessment?.kind === "screened-fallback" ||
|
||||
assessment?.kind === "revalidating"
|
||||
? assessment.text
|
||||
: text;
|
||||
const isPreparing =
|
||||
assessment?.kind === "preparing" ||
|
||||
(!assessment && preparing && hasProvider && !displayText);
|
||||
|
||||
// v1.8.3 — preparing: the read-only route enqueued a generation and the
|
||||
// client is polling. Render the loading skeleton geometry with a
|
||||
// preparing caption so the card reads as "assembling", not stuck. Only
|
||||
// shown when a provider is configured (the route returns the no-key
|
||||
// fallback otherwise).
|
||||
if (preparing && hasProvider && !text) {
|
||||
if (isPreparing) {
|
||||
return (
|
||||
<Card
|
||||
aria-busy="true"
|
||||
@@ -165,7 +183,40 @@ export function InsightStatusCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasProvider) {
|
||||
const terminalKind =
|
||||
assessment?.kind === "timeout" ||
|
||||
assessment?.kind === "error" ||
|
||||
assessment?.kind === "exhausted"
|
||||
? assessment.kind
|
||||
: null;
|
||||
if (terminalKind) {
|
||||
const message =
|
||||
terminalKind === "error"
|
||||
? t("common.loadFailed")
|
||||
: terminalKind === "timeout"
|
||||
? t("common.networkError")
|
||||
: t("insights.noAnalysisYet");
|
||||
return (
|
||||
<Card
|
||||
aria-live="polite"
|
||||
role="status"
|
||||
data-testid={`insight-status-card-${terminalKind}`}
|
||||
className="gap-2 py-3 md:py-4"
|
||||
>
|
||||
<CardHeader>
|
||||
<TileHeader icon={nodeIcon(icon)} title={title} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground text-sm">{message}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const isNoProvider =
|
||||
assessment?.kind === "no-provider" ||
|
||||
(!assessment && !hasProvider && !displayText);
|
||||
if (isNoProvider) {
|
||||
return (
|
||||
<Card className="gap-2 py-3 opacity-80 md:py-4">
|
||||
<CardHeader>
|
||||
@@ -187,7 +238,7 @@ export function InsightStatusCard({
|
||||
);
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
if (!displayText) {
|
||||
return (
|
||||
<Card className="gap-2 py-3 md:py-4">
|
||||
<CardHeader>
|
||||
@@ -255,7 +306,7 @@ export function InsightStatusCard({
|
||||
token surfaces verbatim. The producer side now strips too;
|
||||
this consumer-side wrap keeps existing caches clean while
|
||||
they roll forward. */}
|
||||
<StatusBody text={stripChartTokens(text)} />
|
||||
<StatusBody text={stripChartTokens(displayText)} />
|
||||
<LastUpdatedFooter updatedAt={updatedAt} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -66,6 +66,7 @@ export function MetricStatusCard({
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
assessment={status?.assessment}
|
||||
coachQuestion={coachQuestion}
|
||||
coachScope={coachSource ? { metric: coachSource } : undefined}
|
||||
coachAutoSend
|
||||
|
||||
@@ -71,6 +71,7 @@ export function SlugInsightStatusCard({
|
||||
text={status?.text ?? null}
|
||||
hasProvider={status?.hasProvider ?? false}
|
||||
updatedAt={status?.updatedAt ?? null}
|
||||
assessment={status?.assessment}
|
||||
coachQuestion={t("insights.coach.assessmentPrompt", {
|
||||
metric: t(coach.labelKey),
|
||||
})}
|
||||
|
||||
Reference in new issue
Block a user