mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 12:22:03 +00:00
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:
1 parent
cfe6d1c577
commit
340239423b
9 files changed
+66
-84
No files matched your search
@@ -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",
|
||||
};
|
||||
@@ -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])}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
Reference in new issue
Block a user