feat(insights): show the wellness assessment and add score explainers

The score-anatomy detail page now renders the per-score AI assessment that
already rides GET /api/insights/derived (shipped to iOS in v1.13.2) — the web
type dropped the field, so the card was silently discarded. The assessment
mounts below the composition card via the same InsightStatusCard the metric
sub-pages use, so it reads identically for readiness, sleep, recovery, strain
and stress.

Each score detail page also gains an educational intro caption beneath the
heading, mirroring the metric sub-pages: new explainer bodies for the five
scores across all six locales (sleepScore kept distinct from the sleep
metric's own body), wired through SubPageShell's explainerMetric prop.

The explainer-key guard resolves the score page's closed lookup record
statically so the single dynamic prop stays verifiable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.8 committed 2026-06-07 18:03:49 +02:00
1 parent b07d77cbfa
commit e9171c1e5f
10 files changed
+156 -27

No files matched your search

@@ -1,8 +1,11 @@
"use client";
import { Sparkles } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
import { useDerivedMetric } from "./use-derived-metric";
import { Skeleton } from "@/components/ui/skeleton";
import { InsightStatusCard } from "@/components/insights/insight-status-card";
import type {
ReadinessValue,
SleepScoreValue,
@@ -193,21 +196,43 @@ export function CompositeScoreAnatomy({
}
}
// v1.15.12 B3 — the per-score AI assessment ("why is this score what it is")
// already rides the single `/api/insights/derived` route as `data.assessment`
// (shipped to iOS in v1.13.2); the web simply discarded it. Render it BELOW
// the anatomy card, reusing the same `<InsightStatusCard>` the metric
// sub-pages mount so it reads identically. The route always populates a
// non-empty assessment when the score `status === "ok"` (the deterministic
// template at minimum, warmer AI prose when a provider is configured), so a
// present `assessment` implies a real text — `hasProvider` is held true and
// the card always renders its prose rather than the no-provider fallback.
const assessment = data.assessment;
return (
<ScoreAnatomyView
title={title}
score={score}
hue={METRIC_HUE[metric]}
caption={caption}
contributors={contributors}
coverage={data.coverage}
confidence={data.confidence}
provenance={data.provenance}
method={method}
standard={standard}
insufficient={insufficient}
className={className}
/>
<div className="space-y-3">
<ScoreAnatomyView
title={title}
score={score}
hue={METRIC_HUE[metric]}
caption={caption}
contributors={contributors}
coverage={data.coverage}
confidence={data.confidence}
provenance={data.provenance}
method={method}
standard={standard}
insufficient={insufficient}
className={className}
/>
{assessment ? (
<InsightStatusCard
title={t("insights.assessmentTitle")}
icon={<Sparkles className="h-5 w-5" />}
text={assessment.text}
hasProvider
updatedAt={assessment.updatedAt}
/>
) : null}
</div>
);
}
@@ -8,6 +8,7 @@ import type {
DerivedConfidence,
DerivedProvenance,
} from "@/lib/insights/derived/types";
import type { DerivedAssessment } from "@/lib/insights/derived/derived-assessment";
/**
* v1.10.0 — the single TanStack Query hook for the generic derived-metric
@@ -44,6 +45,17 @@ export interface DerivedMetricResponse<T> {
confidence: DerivedConfidence | null;
provenance: DerivedProvenance;
reason: string | null;
/**
* v1.13.2 — the per-score "why is this score what it is" assessment
* ({ text, source, updatedAt }). Populated only for the assessable scores
* (READINESS, SLEEP_SCORE, RECOVERY_SCORE, STRAIN_SCORE, STRESS_SCORE) when
* `status === "ok"`, null otherwise. The single-metric route serves the
* AI-warm prose (falling back to the deterministic template); the batch route
* serves the cheap deterministic text only. Optional on the wire shape so
* grid reads that drop it stay typed. v1.15.12 B3 surfaces it on the web
* score-anatomy detail page (it already shipped to iOS in v1.13.2).
*/
assessment?: DerivedAssessment | null;
}
export interface UseDerivedMetricOptions {