refactor(insights): consolidate relative-time and confidence-band tables

Three live byte-identical formatRelativeTime copies (hero-strip,
daily-briefing, coach history-rail) lifted to src/lib/i18n/relative-
time.ts. Both correlation-card and trend-annotation duplicated the
exact same low/moderate/high Dracula classnames; extracted to
src/components/insights/confidence-badge.ts (per-component i18n
labels stay local — the copy differs).

Drop the dead "null" placeholder branch in useCoachConversation's
queryKey: the ternary picked a different key when id was null, but
enabled: id !== null kept the query disabled so the placeholder never
fired. Key directly on id.

Hoist STORYBOARD_COLOR_BY_CATEGORY in /insights/page.tsx to module
scope so the map isn't recreated on every render.

Fixes Simplify-S-02, Simplify-S-03, Simplify-S-04, Simplify-S-05,
Code-LOW-02.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 18:09:45 +02:00
1 parent cfe6d1c577
commit 340239423b
9 files changed
+66 -84

No files matched your search

+13 -8
View File
@@ -518,6 +518,16 @@ function getMedicationComplianceSectionStatus(input: {
};
}
// v1.4.20 phase D reconcile — module-scope so the map isn't recreated
// on every render of `<InsightsPage>` (the page renders often on
// TanStack-Query refetches; this map carries no closures over locals).
const STORYBOARD_COLOR_BY_CATEGORY: Record<string, string> = {
medication: "var(--dracula-pink)",
event: "var(--dracula-cyan)",
milestone: "var(--dracula-green)",
warning: "var(--dracula-orange)",
};
export default function InsightsPage() {
const { isAuthenticated, user } = useAuth();
const { t, locale } = useTranslations();
@@ -915,14 +925,9 @@ export default function InsightsPage() {
// v1.4.20 phase B4 — storyboard annotations for the 90-day BP chart.
// The advisor payload may carry up to 20 entries; we transform them
// into the {date, label, color} shape <HealthChart> consumes. The
// colour map mirrors the four canonical categories. Cached payloads
// from before PROMPT_VERSION 4.20.2 simply produce an empty array.
const STORYBOARD_COLOR_BY_CATEGORY: Record<string, string> = {
medication: "var(--dracula-pink)",
event: "var(--dracula-cyan)",
milestone: "var(--dracula-green)",
warning: "var(--dracula-orange)",
};
// colour map mirrors the four canonical categories (hoisted to
// module scope above). Cached payloads from before PROMPT_VERSION
// 4.20.2 simply produce an empty array.
const rawStoryboard = (
advisor.payload?.insights as
| { storyboardAnnotations?: Array<{
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
import { formatRelativeTime } from "@/lib/i18n/relative-time";
import {
useCoachConversations,
@@ -37,22 +38,6 @@ export interface HistoryRailProps {
className?: string;
}
function formatRelativeTime(
iso: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const target = new Date(iso).getTime();
if (Number.isNaN(target)) return "";
const diffMs = Date.now() - target;
if (diffMs < 60_000) return t("insights.relativeJustNow");
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
const days = Math.floor(hours / 24);
return t("insights.relativeDaysAgo", { count: days });
}
export function HistoryRail({
activeId,
onSelect,
@@ -82,7 +82,11 @@ export function useCoachConversations(enabled = true) {
*/
export function useCoachConversation(id: string | null) {
return useQuery({
queryKey: id ? QUERY_KEYS.one(id) : ["coachConversation", "null"],
// The query is gated on `enabled: id !== null`, so the queryFn
// never fires when `id` is null. Key directly on `id` (TanStack
// Query accepts `null` in the array) — the previous "null"
// placeholder branch was structurally equivalent dead code.
queryKey: ["coachConversation", id] as const,
queryFn: async (): Promise<CoachConversationDetailDTO> => {
if (!id) throw new Error("missing id");
const res = await fetch(`/api/insights/chat/${id}`, {
@@ -0,0 +1,16 @@
/**
* v1.4.20 phase D reconcile — shared confidence-band classnames.
*
* Both `<CorrelationCard>` and `<TrendAnnotation>` paint a discrete
* "low / moderate / high" badge with byte-identical Dracula utility
* classes. Per-component i18n key maps stay local because the copy
* differs (correlationRow.* vs trendAnnotation.*); only the classname
* table is the load-bearing duplication.
*/
export type ConfidenceBand = "low" | "moderate" | "high";
export const CONFIDENCE_BADGE_CLASS: Record<ConfidenceBand, string> = {
high: "border-dracula-green/40 bg-dracula-green/10 text-dracula-green",
moderate: "border-dracula-orange/40 bg-dracula-orange/10 text-dracula-orange",
low: "border-dracula-comment/40 bg-dracula-comment/10 text-muted-foreground",
};
+2 -9
View File
@@ -9,6 +9,7 @@ import { EmptyState } from "@/components/ui/empty-state";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import type { CorrelationResult } from "@/lib/insights/correlations";
import { CONFIDENCE_BADGE_CLASS } from "./confidence-badge";
/**
* v1.4.20 phase B3 — single Correlation card.
@@ -58,14 +59,6 @@ const SUBTITLE_KEY: Record<CorrelationResult["kind"], string> = {
"weight-weekday": "insights.correlationRow.card.weightWeekdaySubtitle",
};
const CONFIDENCE_BADGE_CLASSNAME: Record<"low" | "moderate" | "high", string> =
{
high: "border-dracula-green/40 bg-dracula-green/10 text-dracula-green",
moderate:
"border-dracula-orange/40 bg-dracula-orange/10 text-dracula-orange",
low: "border-dracula-comment/40 bg-dracula-comment/10 text-muted-foreground",
};
const CONFIDENCE_LABEL_KEY: Record<"low" | "moderate" | "high", string> = {
high: "insights.correlationRow.confidenceHigh",
moderate: "insights.correlationRow.confidenceModerate",
@@ -102,7 +95,7 @@ export function CorrelationCard({ result }: CorrelationCardProps) {
variant="outline"
className={cn(
"shrink-0 text-[10px]",
CONFIDENCE_BADGE_CLASSNAME[result.confidenceBand.label],
CONFIDENCE_BADGE_CLASS[result.confidenceBand.label],
)}
>
{t(CONFIDENCE_LABEL_KEY[result.confidenceBand.label])}
+1 -20
View File
@@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { EmptyState } from "@/components/ui/empty-state";
import { useTranslations } from "@/lib/i18n/context";
import { formatRelativeTime } from "@/lib/i18n/relative-time";
import { cn } from "@/lib/utils";
import type {
DailyBriefing as DailyBriefingPayload,
@@ -251,23 +252,3 @@ export function DailyBriefing({
);
}
/**
* Bucketed relative-time using i18n translation keys so every surface
* shares vocabulary. Buckets: <1m, <1h, <24h, else days. Mirrors the
* helper in `<InsightsPageHero>` so the two cards format the same way.
*/
function formatRelativeTime(
iso: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const target = new Date(iso).getTime();
if (Number.isNaN(target)) return "";
const diffMs = Date.now() - target;
if (diffMs < 60_000) return t("insights.relativeJustNow");
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
const days = Math.floor(hours / 24);
return t("insights.relativeDaysAgo", { count: days });
}
+1 -20
View File
@@ -12,6 +12,7 @@ import {
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { useTranslations } from "@/lib/i18n/context";
import { formatRelativeTime } from "@/lib/i18n/relative-time";
import { cn } from "@/lib/utils";
import { SuggestedPrompts } from "./suggested-prompts";
import {
@@ -134,26 +135,6 @@ function resolveGreetingKey(now: Date): string {
return "insights.heroGreetingNight";
}
/**
* Bucketed relative-time using i18n translation keys so every surface
* shares vocabulary. Mirrors the helper in `<InsightsPageHero>`.
*/
function formatRelativeTime(
iso: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const target = new Date(iso).getTime();
if (Number.isNaN(target)) return "";
const diffMs = Date.now() - target;
if (diffMs < 60_000) return t("insights.relativeJustNow");
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
const days = Math.floor(hours / 24);
return t("insights.relativeDaysAgo", { count: days });
}
export function HeroStrip({
briefing,
updatedAt,
+2 -10
View File
@@ -4,6 +4,7 @@ import { Sparkles } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { useTranslations } from "@/lib/i18n/context";
import { cn } from "@/lib/utils";
import { CONFIDENCE_BADGE_CLASS } from "./confidence-badge";
/**
* v1.4.20 phase B3 — single-sentence AI annotation rendered directly
@@ -30,15 +31,6 @@ interface TrendAnnotationProps {
confidence?: TrendAnnotationConfidenceBand;
}
const CONFIDENCE_BADGE_CLASSNAME: Record<
TrendAnnotationConfidenceBand,
string
> = {
high: "border-dracula-green/40 bg-dracula-green/10 text-dracula-green",
moderate: "border-dracula-orange/40 bg-dracula-orange/10 text-dracula-orange",
low: "border-dracula-comment/40 bg-dracula-comment/10 text-muted-foreground",
};
const CONFIDENCE_LABEL_KEY: Record<TrendAnnotationConfidenceBand, string> = {
high: "insights.trendAnnotation.confidenceHigh",
moderate: "insights.trendAnnotation.confidenceModerate",
@@ -86,7 +78,7 @@ export function TrendAnnotation({
<Badge
data-slot="trend-annotation-confidence"
variant="outline"
className={cn("text-[10px]", CONFIDENCE_BADGE_CLASSNAME[confidence])}
className={cn("text-[10px]", CONFIDENCE_BADGE_CLASS[confidence])}
>
{t(CONFIDENCE_LABEL_KEY[confidence])}
</Badge>
+25
View File
@@ -0,0 +1,25 @@
/**
* v1.4.20 phase D reconcile — shared relative-time helper.
*
* Formats an ISO timestamp as a localised "just now" / "N minutes ago"
* string via i18n keys. Three Insights surfaces (hero strip, daily
* briefing, history rail) carried byte-identical copies of this helper;
* lift the body here so a future i18n key change lands once.
*
* Buckets: <1m → just-now, <1h → minutes, <24h → hours, else days.
*/
export function formatRelativeTime(
iso: string,
t: (key: string, params?: Record<string, string | number>) => string,
): string {
const target = new Date(iso).getTime();
if (Number.isNaN(target)) return "";
const diffMs = Date.now() - target;
if (diffMs < 60_000) return t("insights.relativeJustNow");
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 60) return t("insights.relativeMinutesAgo", { count: minutes });
const hours = Math.floor(minutes / 60);
if (hours < 24) return t("insights.relativeHoursAgo", { count: hours });
const days = Math.floor(hours / 24);
return t("insights.relativeDaysAgo", { count: days });
}