feat(insights): wire <HeroStrip> + <DailyBriefing> into /insights

Replaces `<InsightsPageHero>` with the new `<HeroStrip>` band and
mounts a full-width `<DailyBriefing>` directly below it, ahead of
the existing advisor + status cards which remain untouched.

`useInsightsAdvisorQuery` now lifts the optional `dailyBriefing`
block off the cached payload and validates it client-side via
`dailyBriefingSchema.safeParse`. A malformed cached briefing falls
back to null so the briefing card paints its empty-state CTA
instead of a half-rendered card. Legacy v1.4.19 cached payloads
(no `dailyBriefing` field) round-trip unchanged.

The CompareToggle migrates from the hero meta band into the
DailyBriefing's metaSlot — keeps the page-level comparison control
adjacent to the briefing it scopes.

The hero greeting reads the user's username (HealthLog has no
separate display-name field) and stops at the first whitespace so
"alice" / "alice.smith" both surface "Good morning, alice".

`insights-polish.test.ts` v1.4.19 A3 ratchet updated: the original
"exactly one onRegenerate prop on the page" assertion was a
string-count check that mis-fires now that the briefing card's
empty-state CTA legitimately wires the same handler. The
load-bearing constraint stays — the advisor card must NOT carry
onRegenerate — and is enforced via a tighter JSX-block check.

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 15:44:40 +02:00
1 parent 59e63a0641
commit 7d29596998
3 files changed
+81 -9

No files matched your search

@@ -2,6 +2,10 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { InsightResult } from "@/lib/ai/types";
import {
dailyBriefingSchema,
type DailyBriefing as DailyBriefingPayload,
} from "@/lib/ai/schema";
import { queryKeys } from "@/lib/query-keys";
/**
@@ -26,6 +30,15 @@ export interface InsightAdvisorPayload {
cached: boolean;
cachedAt?: string | null;
legacyPayload?: boolean;
/**
* v1.4.20 phase B1 — Daily Briefing block surfaced for the new hero
* strip + briefing card. Lives on the cached payload alongside the
* legacy `insights` shape (see `aiInsightResponseSchema` — the
* `.passthrough()` lets the field round-trip through any provider).
* Validated client-side via the schema's `safeParse` to keep a
* malformed payload from poisoning the briefing card.
*/
dailyBriefing?: DailyBriefingPayload | null;
}
async function fetchAdvisor(
@@ -46,7 +59,25 @@ async function fetchAdvisor(
throw new Error(`HTTP ${res.status}`);
}
const json = await res.json();
return json.data as InsightAdvisorPayload;
const payload = json.data as InsightAdvisorPayload;
// The cached `insights` blob may carry a `dailyBriefing` from a fresh
// PROMPT_VERSION 4.20.x generation. Lift it onto the payload so
// consumers don't have to know the legacy shape.
const briefingCandidate = (payload?.insights as Record<string, unknown>)
?.dailyBriefing;
if (briefingCandidate != null) {
const parsed = dailyBriefingSchema.safeParse(briefingCandidate);
if (parsed.success) {
payload.dailyBriefing = parsed.data;
} else {
// Malformed cached briefing — keep null so the UI shows the
// empty-state CTA instead of a half-rendered card.
payload.dailyBriefing = null;
}
} else {
payload.dailyBriefing = null;
}
return payload;
}
export interface UseInsightsAdvisorResult {