mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(insights): assessment and prose readability pass
- assessments render in full; the three-line clamp and its show-more toggle are gone - AI prose (assessment, briefing, explainers, usual-range note) reads in the regular foreground; muted stays reserved for meta lines - the usual-range note becomes a proper card with the shared TileHeader (icon + title, labelled in all six locales) - the metric-page heading + explainer indent to the cards' text edge - the health-score card drops its Ask-the-Coach corner button
This commit is contained in:
1 parent
8579705fbf
commit
d83220a4e0
11 files changed
+36
-109
No files matched your search
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} Tokens · {model}",
|
||||
"askAboutThis": "Den Coach dazu fragen",
|
||||
"readStrip": {
|
||||
"label": "Coach-Einschätzung",
|
||||
"label": "Üblicher Bereich",
|
||||
"baselineWithin": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darin.",
|
||||
"baselineAbove": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darüber.",
|
||||
"baselineBelow": "Dein üblicher Bereich liegt bei {low}–{high} {unit}; heute liegt {value} darunter.",
|
||||
|
||||
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} tokens · {model}",
|
||||
"askAboutThis": "Ask the coach about this",
|
||||
"readStrip": {
|
||||
"label": "Coach read",
|
||||
"label": "Usual range",
|
||||
"baselineWithin": "Your usual range is {low}–{high} {unit}; today's {value} sits within it.",
|
||||
"baselineAbove": "Your usual range is {low}–{high} {unit}; today's {value} sits above it.",
|
||||
"baselineBelow": "Your usual range is {low}–{high} {unit}; today's {value} sits below it.",
|
||||
|
||||
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} tokens · {model}",
|
||||
"askAboutThis": "Pregúntale al coach sobre esto",
|
||||
"readStrip": {
|
||||
"label": "Lectura del Coach",
|
||||
"label": "Rango habitual",
|
||||
"baselineWithin": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa dentro de él.",
|
||||
"baselineAbove": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por encima.",
|
||||
"baselineBelow": "Tu rango habitual es {low}–{high} {unit}; hoy {value} se sitúa por debajo.",
|
||||
|
||||
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} jetons · {model}",
|
||||
"askAboutThis": "Interroger le coach à ce sujet",
|
||||
"readStrip": {
|
||||
"label": "Lecture du Coach",
|
||||
"label": "Plage habituelle",
|
||||
"baselineWithin": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} s'y situe.",
|
||||
"baselineAbove": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe au-dessus.",
|
||||
"baselineBelow": "Ta plage habituelle est de {low}–{high} {unit} ; aujourd'hui, {value} se situe en dessous.",
|
||||
|
||||
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} token · {model}",
|
||||
"askAboutThis": "Chiedi al coach",
|
||||
"readStrip": {
|
||||
"label": "Lettura del Coach",
|
||||
"label": "Intervallo abituale",
|
||||
"baselineWithin": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} rientra al suo interno.",
|
||||
"baselineAbove": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sopra.",
|
||||
"baselineBelow": "Il tuo intervallo abituale è {low}–{high} {unit}; oggi {value} si colloca al di sotto.",
|
||||
|
||||
+1
-1
@@ -2353,7 +2353,7 @@
|
||||
"tokensUsedWithModel": "{count} tokenów · {model}",
|
||||
"askAboutThis": "Zapytaj trenera o to",
|
||||
"readStrip": {
|
||||
"label": "Ocena Coacha",
|
||||
"label": "Typowy zakres",
|
||||
"baselineWithin": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} mieści się w nim.",
|
||||
"baselineAbove": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest powyżej niego.",
|
||||
"baselineBelow": "Twój zwykły zakres to {low}–{high} {unit}; dzisiejsza wartość {value} jest poniżej niego.",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useMounted } from "@/hooks/use-mounted";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiGet } from "@/lib/api/api-fetch";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape";
|
||||
|
||||
/**
|
||||
@@ -109,37 +109,27 @@ export function CoachReadStrip({
|
||||
// fires defensively.)
|
||||
if (!baselineLine && !driverLine) return null;
|
||||
|
||||
// Standard card anatomy — the same TileHeader-led shape as every other
|
||||
// insights card (foreground icon + title), instead of the former inline
|
||||
// mini-icon strip that read as chrome outside the card system. Body prose
|
||||
// in the regular foreground; muted stays reserved for meta lines.
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-read-strip"
|
||||
className={cn(
|
||||
"bg-card/60 border-border/60 rounded-xl border px-3.5 py-2.5",
|
||||
"text-muted-foreground space-y-1 text-sm leading-relaxed",
|
||||
)}
|
||||
className="bg-card/60 border-border/60 space-y-2 rounded-xl border px-4 py-3.5"
|
||||
>
|
||||
<div className="flex items-start gap-2">
|
||||
<Sparkles
|
||||
className="text-primary/70 mt-0.5 size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<span className="text-foreground/70 sr-only text-xs font-medium">
|
||||
{t("insights.coach.readStrip.label")}
|
||||
</span>
|
||||
{baselineLine ? (
|
||||
<p data-slot="coach-read-baseline" className="text-pretty">
|
||||
{baselineLine}
|
||||
</p>
|
||||
) : null}
|
||||
{driverLine ? (
|
||||
<p
|
||||
data-slot="coach-read-driver"
|
||||
className="text-muted-foreground/90 text-pretty"
|
||||
>
|
||||
{driverLine}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<TileHeader icon={Sparkles} title={t("insights.coach.readStrip.label")} />
|
||||
<div className="text-foreground min-w-0 space-y-1 text-sm leading-relaxed">
|
||||
{baselineLine ? (
|
||||
<p data-slot="coach-read-baseline" className="text-pretty">
|
||||
{baselineLine}
|
||||
</p>
|
||||
) : null}
|
||||
{driverLine ? (
|
||||
<p data-slot="coach-read-driver" className="text-pretty">
|
||||
{driverLine}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { HealthScoreDeltaExplainer } from "./health-score-delta-explainer";
|
||||
import { AskCoachAction } from "./ask-coach-action";
|
||||
|
||||
/**
|
||||
* v1.4.20 B5 — Personal Health Score panel.
|
||||
@@ -703,16 +702,6 @@ export function HealthScoreCard({
|
||||
{/* v1.18.6 (DISC-01) — the "indicative, not a clinical assessment"
|
||||
card disclaimer is removed; the one-time onboarding acknowledgment
|
||||
now covers the not-a-diagnosis framing app-wide. */}
|
||||
|
||||
{/* v1.21.0 (C4 H2) — the score is a whole-picture composite, so the
|
||||
hand-off opens the Coach against the default snapshot (no scope)
|
||||
seeded with the actual number. Mounts at the foot of the card as
|
||||
a single discreet entry, not a primary CTA. */}
|
||||
<div className="mt-auto flex justify-end pt-1">
|
||||
<AskCoachAction
|
||||
question={t("insights.coach.seed.healthScore", { score })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -300,7 +300,7 @@ export function HeroStrip({
|
||||
spacing (still plain text children, no markdown). */}
|
||||
<div
|
||||
data-slot="insights-hero-strip-subtitle"
|
||||
className="text-muted-foreground max-w-3xl text-sm"
|
||||
className="text-foreground max-w-3xl text-sm"
|
||||
>
|
||||
<ProseBlocks text={subtitle} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
@@ -287,67 +286,13 @@ export function InsightStatusCard({
|
||||
* overflow, and re-measures on resize / font load via `ResizeObserver`.
|
||||
*/
|
||||
function StatusBody({ text }: { text: string }) {
|
||||
const { t } = useTranslations();
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [overflows, setOverflows] = useState(false);
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// v1.22 (W6) — the assessment now renders through the shared `ProseBlocks`
|
||||
// helper (real paragraphs). The 3-line clamp therefore moves from the single
|
||||
// `<p>` to the BLOCK CONTAINER: a max-height + overflow-hidden when collapsed,
|
||||
// measured on the wrapper node so multi-paragraph text doesn't re-collapse.
|
||||
// When the text fits, `scrollHeight` equals `clientHeight`; an overflow means
|
||||
// the clamp is hiding content and the toggle earns its place. Gated on
|
||||
// `!expanded` so the comparison reads against the clamped element.
|
||||
useEffect(() => {
|
||||
if (expanded) return;
|
||||
const node = wrapperRef.current;
|
||||
if (!node) return;
|
||||
|
||||
const measure = () => {
|
||||
setOverflows(node.scrollHeight > node.clientHeight + 1);
|
||||
};
|
||||
measure();
|
||||
|
||||
if (typeof ResizeObserver === "undefined") return;
|
||||
const observer = new ResizeObserver(measure);
|
||||
observer.observe(node);
|
||||
return () => observer.disconnect();
|
||||
// Re-measure whenever the source text changes (a fresh assessment) or
|
||||
// the user collapses the card again.
|
||||
}, [text, expanded]);
|
||||
|
||||
// The assessment shows in full — the 3-line clamp + "show more" toggle
|
||||
// hid the prose behind an extra tap for no gain on a card whose whole
|
||||
// point is the text. Body prose reads in the regular foreground; muted
|
||||
// stays reserved for meta lines (timestamps, captions).
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={cn(
|
||||
"text-muted-foreground text-sm",
|
||||
// ~3 lines at text-sm / leading-relaxed; only when collapsed.
|
||||
!expanded && "max-h-[4.5rem] overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<ProseBlocks text={text} />
|
||||
</div>
|
||||
{/* Only render the toggle on a genuine overflow. When the full text
|
||||
fits inside three lines there is nothing more to show, so no
|
||||
affordance is painted. */}
|
||||
{(overflows || expanded) && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((prev) => !prev)}
|
||||
aria-expanded={expanded}
|
||||
data-slot="assessment-show-more"
|
||||
className={cn(
|
||||
"text-foreground/80 hover:text-foreground inline-flex text-xs font-medium",
|
||||
"focus-visible:ring-ring/50 focus-visible:ring-2 focus-visible:outline-none",
|
||||
)}
|
||||
>
|
||||
{expanded
|
||||
? t("insights.assessmentShowLess")
|
||||
: t("insights.assessmentShowMore")}
|
||||
</button>
|
||||
)}
|
||||
<div className="text-foreground text-sm">
|
||||
<ProseBlocks text={text} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,7 +218,10 @@ export function SubPageShell({
|
||||
<div data-slot="insights-subpage" className="space-y-3 md:space-y-4">
|
||||
{/* v1.8.7.1 — back-nav leads the page, above the heading. */}
|
||||
{backLink}
|
||||
<header className="space-y-1.5">
|
||||
{/* The heading + explainer indent to the cards' inner text edge
|
||||
(`px-6`, the Card content padding), so the page head reads as
|
||||
part of the card column instead of hanging outside it. */}
|
||||
<header className="space-y-1.5 px-6">
|
||||
{/* v1.8.6 — the heading row pins the Coach icon top-right while the
|
||||
title / nudge / badge wrap together on the left.
|
||||
|
||||
@@ -337,7 +340,7 @@ export function SubPageShell({
|
||||
// string was removed.
|
||||
<p
|
||||
data-slot="metric-explainer-inline"
|
||||
className="text-muted-foreground text-sm leading-relaxed"
|
||||
className="text-foreground text-sm leading-relaxed"
|
||||
>
|
||||
{t(`insights.subPage.explainer.${explainerMetric}Body`)}
|
||||
</p>
|
||||
|
||||
Reference in new issue
Block a user