mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
fix(01-16): remove redundant score and summary presentation
- keep useful Today prose without repeating the ring score - remove stored-summary regeneration and Health Score notice/method/list copy
This commit is contained in:
1 parent
b39ddeb5b5
commit
d67b899138
3 files changed
+42
-96
No files matched your search
@@ -64,6 +64,28 @@ function formatDelta(
|
||||
return t("daily.today.deltaVsBaseline", { delta: signed });
|
||||
}
|
||||
|
||||
/**
|
||||
* The ring already owns the headline number. AI briefing copy can still carry
|
||||
* an older lead sentence that repeats that exact score, so remove only the
|
||||
* sentence containing the displayed number and retain every other useful
|
||||
* sentence verbatim. If the score sentence was the whole lead, render no lead
|
||||
* rather than inventing replacement prose.
|
||||
*/
|
||||
function withoutRepeatedScore(
|
||||
text: string | null,
|
||||
score: number | null,
|
||||
): string | null {
|
||||
if (!text || score === null) return text;
|
||||
const shownScore = String(Math.round(score));
|
||||
const scorePattern = new RegExp(
|
||||
`(^|[^0-9])${shownScore.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}([^0-9]|$)`,
|
||||
);
|
||||
const sentences = text.match(/[^.!?]+[.!?]+(?:\s+|$)|[^.!?]+$/g) ?? [text];
|
||||
const retained = sentences.filter((sentence) => !scorePattern.test(sentence));
|
||||
const result = retained.join(" ").replace(/\s+/g, " ").trim();
|
||||
return result.length > 0 ? result : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The "just in" chip's clock face.
|
||||
*
|
||||
@@ -128,7 +150,14 @@ export function TodayHero({
|
||||
// paragraph, so the hero stays exactly one lead line tall and nothing below
|
||||
// it shifts. The briefing lead is the standing read; the deterministic
|
||||
// `line` is the floor a keyless self-hoster still gets.
|
||||
const lead = digest.reactionLine ?? digest.briefingLead ?? digest.line;
|
||||
const rawLead =
|
||||
digest.reactionLine ??
|
||||
digest.briefingLead ??
|
||||
// The deterministic line is a useful floor only once the ring has a real
|
||||
// score. A provisional/null ring must not pair with a fabricated score
|
||||
// sentence from that fallback.
|
||||
(hasScore ? digest.line : null);
|
||||
const lead = withoutRepeatedScore(rawLead, digest.score?.value ?? null);
|
||||
const topSignal = digest.topSignal;
|
||||
const justInTime = digest.justIn ? formatJustInTime(digest.justIn.at) : null;
|
||||
|
||||
@@ -179,12 +208,14 @@ export function TodayHero({
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
{/* Hero numeric face: the read leads large in the foreground
|
||||
token, calm and legible — the day's read, not a slogan. */}
|
||||
<div
|
||||
data-slot="today-hero-lead"
|
||||
className="text-foreground text-lg leading-snug font-semibold tracking-tight sm:text-xl"
|
||||
>
|
||||
<ProseBlocks text={lead} strip linkify={false} />
|
||||
</div>
|
||||
{lead ? (
|
||||
<div
|
||||
data-slot="today-hero-lead"
|
||||
className="text-foreground text-lg leading-snug font-semibold tracking-tight sm:text-xl"
|
||||
>
|
||||
<ProseBlocks text={lead} strip linkify={false} />
|
||||
</div>
|
||||
) : null}
|
||||
{/* Top signal — present-tense headline + optional delta, one
|
||||
muted step down so it supports the lead without competing. */}
|
||||
{topSignal ? (
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
* provider call. With no AI provider configured the block renders the state
|
||||
* without an action rather than an button that would 422.
|
||||
*/
|
||||
import { FileText, RefreshCw } from "lucide-react";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -63,9 +63,8 @@ export function DocumentSummaryBlock({
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
// A stored summary wins over every state: it is shown from storage. The
|
||||
// adjacent action is explicit, so opening the sheet never spends a provider
|
||||
// call; a successful replacement is persisted by the summary route.
|
||||
// A stored summary wins over every state and is shown read-only from storage.
|
||||
// Generation remains available only for absent terminal states below.
|
||||
if (summary) {
|
||||
return (
|
||||
<section className="space-y-2" data-slot="document-detail-summary">
|
||||
@@ -76,28 +75,6 @@ export function DocumentSummaryBlock({
|
||||
<p className="text-sm leading-none font-medium">
|
||||
{t("documents.detail.summary.title")}
|
||||
</p>
|
||||
{aiEnabled ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
data-slot="document-detail-summary-regenerate"
|
||||
onClick={onGenerate}
|
||||
disabled={isGenerating || actionsDisabled}
|
||||
>
|
||||
<RefreshCw
|
||||
className={
|
||||
isGenerating
|
||||
? "size-4 animate-spin motion-reduce:animate-none"
|
||||
: "size-4"
|
||||
}
|
||||
aria-hidden
|
||||
/>
|
||||
{isGenerating
|
||||
? t("documents.detail.summary.generating")
|
||||
: t("documents.detail.summary.regenerate")}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-foreground text-sm">{summary}</p>
|
||||
{generatedAtLabel ? (
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { ScoreAnatomyView } from "@/components/insights/derived/score-anatomy-view";
|
||||
import { CoverageMeter } from "@/components/insights/derived/coverage-meter";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { apiPost } from "@/lib/api/api-fetch";
|
||||
import { useOptionalQueryClient } from "@/hooks/_internal/use-query-client-safe";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useUnitDisplay } from "@/hooks/use-unit-display";
|
||||
import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
|
||||
@@ -102,10 +95,6 @@ export function HealthScoreCard({
|
||||
const { t } = useTranslations();
|
||||
const { user } = useAuth();
|
||||
const glucoseUnit = resolveGlucoseUnit(user?.glucoseUnit);
|
||||
const queryClient = useOptionalQueryClient();
|
||||
const [noticeDismissed, setNoticeDismissed] = useState(
|
||||
report.algorithmNotice?.dismissed ?? false,
|
||||
);
|
||||
|
||||
function reasonText(reason: string): string {
|
||||
const key = REASON_KEY[reason] ?? "unavailable";
|
||||
@@ -234,55 +223,11 @@ export function HealthScoreCard({
|
||||
? 1 / eligibleCount
|
||||
: 0,
|
||||
}));
|
||||
const algorithmNotice = report.algorithmNotice;
|
||||
|
||||
async function dismissAlgorithmNotice() {
|
||||
if (!algorithmNotice) return;
|
||||
setNoticeDismissed(true);
|
||||
try {
|
||||
await apiPost("/api/daily/digest/dismiss", {
|
||||
itemKey: algorithmNotice.itemKey,
|
||||
});
|
||||
queryClient?.setQueriesData(
|
||||
{ queryKey: queryKeys.analytics() },
|
||||
(payload) =>
|
||||
markAlgorithmNoticeDismissed(payload, algorithmNotice.itemKey),
|
||||
);
|
||||
await queryClient?.invalidateQueries({
|
||||
queryKey: queryKeys.analytics(),
|
||||
refetchType: "active",
|
||||
});
|
||||
} catch {
|
||||
setNoticeDismissed(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="health-score-card"
|
||||
className={cn("flex min-w-0 flex-col gap-3", className)}
|
||||
>
|
||||
{algorithmNotice && !noticeDismissed ? (
|
||||
<div
|
||||
data-slot="health-score-algorithm-notice"
|
||||
className="border-border/70 bg-muted/35 flex items-start gap-3 rounded-lg border px-3 py-2.5"
|
||||
>
|
||||
<p className="text-muted-foreground min-w-0 flex-1 text-xs leading-relaxed">
|
||||
{t("insights.healthScore.algorithmChanged")}
|
||||
</p>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="-mt-1 -mr-2 h-9 w-9 shrink-0"
|
||||
aria-label={t("insights.healthScore.dismissAlgorithmChanged")}
|
||||
onClick={() => void dismissAlgorithmNotice()}
|
||||
>
|
||||
<X className="h-4 w-4" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<ScoreAnatomyView
|
||||
title={t("insights.healthScore.label")}
|
||||
score={composite.status === "ok" ? composite.value.score : null}
|
||||
@@ -296,7 +241,7 @@ export function HealthScoreCard({
|
||||
coverage={composite.coverage}
|
||||
confidence={composite.status === "ok" ? composite.confidence : null}
|
||||
provenance={composite.provenance}
|
||||
method={t("insights.healthScore.method")}
|
||||
method={null}
|
||||
insufficient={composite.status !== "ok"}
|
||||
insufficientNote={
|
||||
composite.status === "insufficient"
|
||||
@@ -315,13 +260,6 @@ export function HealthScoreCard({
|
||||
<div className="border-border/70 bg-card/80 space-y-2 rounded-xl border p-4">
|
||||
{composite.status === "ok" ? (
|
||||
<>
|
||||
<p className="text-foreground text-xs font-semibold">
|
||||
{t("insights.healthScore.composition", {
|
||||
pillars: composite.value.composition
|
||||
.map((id) => t(PILLAR_LABEL_KEY[id]))
|
||||
.join(", "),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("insights.healthScore.bandSetter", {
|
||||
pillar: composite.value.bandSetter
|
||||
|
||||
Reference in new issue
Block a user