diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index cd6514e93..f19ffd9c2 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -1,9 +1,10 @@ "use client"; +import { Suspense } from "react"; import { useQuery } from "@tanstack/react-query"; import dynamic from "next/dynamic"; import Link from "next/link"; -import { Loader2, TrendingUp } from "lucide-react"; +import { TrendingUp } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { useFeatureFlags } from "@/hooks/use-feature-flags"; @@ -158,7 +159,16 @@ export default function InsightsPage() { const coachLaunch = useCoachLaunch(); const flags = useFeatureFlags(); - const { data, isLoading } = useQuery({ + // v1.4.36 W1 — drop the page-level `isLoading` gate that used to + // block the entire shell on `/api/insights/comprehensive`. The + // comprehensive payload still feeds the empty-state decision but + // every other section now mounts in parallel under its own + // boundary, so the user sees the hero + tile skeletons + // within ~500 ms instead of waiting on the slowest fan-out. The + // empty-state branch only fires once the query has resolved AND + // reported zero measurements; while it's in-flight the page paints + // the regular shell and the tiles fill in as their data lands. + const { data, isLoading, isFetched } = useQuery({ queryKey: ["insights", "comprehensive"], queryFn: async () => { const res = await fetch("/api/insights/comprehensive"); @@ -181,15 +191,11 @@ export default function InsightsPage() { const analyticsQuery = useAnalyticsQuery(); const analytics = analyticsQuery.data as AnalyticsData | undefined; - if (isLoading) { - return ( -
- -
- ); - } - - if (!data) { + // Empty-state shortcut — only paint once the comprehensive query has + // resolved AND reported zero measurements. While it's in-flight we + // fall through to the streamed shell so the user gets the hero + + // skeleton tiles inside the first paint budget. + if (!isLoading && isFetched && !data) { return ( } @@ -213,6 +219,20 @@ export default function InsightsPage() { const briefingPayload = advisor.payload?.dailyBriefing ?? null; const heroStripUpdatedAt = advisor.payload?.cachedAt ?? null; + // Skeleton fallbacks for each Suspense boundary. Match the loaded- + // chunk height so the layout doesn't shift when the lazy import + // resolves. The TrendsRow skeleton mirrors the chart-strip shell so + // the equal-height contract survives the streamed paint. + const briefingFallback = ( +
+ ); + const correlationFallback = ( +
+ ); + const trendsFallback = ( +
+ ); + return (
{flags.briefing && ( - + + + )} {flags.correlations && analytics?.correlations && ( - + + + )} - + + +
); }