mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
insights(mood): order classification first, route every tile through TileHeader
Reorder the mood subpage spine so the classification tiles (in-target share + stability band) lead, then the better-days assessment, then the descriptive breakdown sections. The classification answers where the user stands; the better-days board, directly beneath, answers what associates with their good days. Every mood card header now routes through the shared TileHeader (icon + white heading) so the subpage speaks one card language. The in-target and stability tiles gain proper headings (new inTargetTitle / stability.title keys across all six locales) and drop the icon-in-a-circle headline glyph; the narrative feed and the discovered-relations board move into headed cards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
85c9f3ff3f
commit
fcd8eccf6e
10 files changed
+167
-113
No files matched your search
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Compass } from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
@@ -81,26 +84,32 @@ export function MoodDiscoveredRelations() {
|
||||
if (moodPairs.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2" data-slot="mood-discovered-relations">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("insights.mood.discovery.title")}
|
||||
</h3>
|
||||
<Card data-slot="mood-discovered-relations">
|
||||
<CardHeader className="pb-2">
|
||||
{/* v1.12.4 (C3) — the false-discovery footer + observational
|
||||
disclaimer were two full-width footnote rows. Fold both into a
|
||||
single explainer icon so the explanation is one hover/focus away
|
||||
instead of a low-density block under the list. */}
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.discovery.explainerLabel")}
|
||||
detail={`${t("insights.mood.discovery.footer", {
|
||||
tested: data.pairsTested,
|
||||
})} ${t("insights.mood.discovery.disclaimer")}`}
|
||||
instead of a low-density block under the list. v1.12.6 — the
|
||||
heading now rides the canonical `TileHeader`, with the explainer
|
||||
icon pinned to its trailing slot. */}
|
||||
<TileHeader
|
||||
icon={Compass}
|
||||
title={t("insights.mood.discovery.title")}
|
||||
right={
|
||||
<MoodExplainerIcon
|
||||
label={t("insights.mood.discovery.explainerLabel")}
|
||||
detail={`${t("insights.mood.discovery.footer", {
|
||||
tested: data.pairsTested,
|
||||
})} ${t("insights.mood.discovery.disclaimer")}`}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.mood.discovery.description")}
|
||||
</p>
|
||||
<ul className="divide-border divide-y">
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-muted-foreground mb-2 text-sm">
|
||||
{t("insights.mood.discovery.description")}
|
||||
</p>
|
||||
<ul className="divide-border divide-y">
|
||||
{moodPairs.map((pair) => {
|
||||
const moodIsOutcome = pair.outcome === "MOOD";
|
||||
const channelKey = moodIsOutcome ? pair.behaviour : pair.outcome;
|
||||
@@ -139,7 +148,8 @@ export function MoodDiscoveredRelations() {
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { Target } from "lucide-react";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
@@ -12,6 +13,10 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
* the LLM snapshot) to a visible stat: the share of recent days that
|
||||
* landed in the good-mood band. Renders nothing when the number is
|
||||
* unavailable (no recent data) so the page degrades gracefully.
|
||||
*
|
||||
* v1.12.6 — the tile now leads with the canonical `TileHeader`
|
||||
* (icon + white heading) so it speaks the same card language as every
|
||||
* other Insights tile; the icon-in-a-circle headline glyph is dropped.
|
||||
*/
|
||||
export function MoodInTargetTile({ pct }: { pct: number | null }) {
|
||||
const { t } = useTranslations();
|
||||
@@ -22,18 +27,16 @@ export function MoodInTargetTile({ pct }: { pct: number | null }) {
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-4">
|
||||
<div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
|
||||
<Target className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-foreground text-2xl font-semibold tabular-nums">
|
||||
{t("insights.mood.inTargetValue", { pct: String(rounded) })}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.mood.inTargetCaption")}
|
||||
</p>
|
||||
</div>
|
||||
<CardHeader className="pb-2">
|
||||
<TileHeader icon={Target} title={t("insights.mood.inTargetTitle")} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-foreground text-2xl font-semibold tabular-nums">
|
||||
{t("insights.mood.inTargetValue", { pct: String(rounded) })}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("insights.mood.inTargetCaption")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,22 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import type { ComponentType } from "react";
|
||||
import {
|
||||
BarChart3,
|
||||
CalendarDays,
|
||||
CalendarRange,
|
||||
Clock,
|
||||
Grid3x3,
|
||||
Link2,
|
||||
Sparkles,
|
||||
Tag,
|
||||
Tags,
|
||||
TrendingUp,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
@@ -136,15 +151,19 @@ interface MoodInsightsResponse {
|
||||
|
||||
function SectionCard({
|
||||
title,
|
||||
icon,
|
||||
children,
|
||||
}: {
|
||||
title: string;
|
||||
// v1.12.6 — every mood section header now routes through `TileHeader`
|
||||
// (icon + white heading) so the subpage reads as one card language.
|
||||
icon: ComponentType<{ className?: string }>;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
<TileHeader icon={icon} title={title} />
|
||||
</CardHeader>
|
||||
<CardContent>{children}</CardContent>
|
||||
</Card>
|
||||
@@ -210,7 +229,14 @@ export function MoodInsightsSections() {
|
||||
const hasCrosstab = crosstabRows.length > 0;
|
||||
|
||||
return (
|
||||
// v1.12.6 — the mood spine is ordered as: Einordnung (the classification
|
||||
// tiles: in-target share + stability band) FIRST, then the "what goes with
|
||||
// your better days" assessment read, then the descriptive breakdown
|
||||
// sections. The classification answers "where do I stand", the better-days
|
||||
// board answers "what associates with my good days", and the rest is the
|
||||
// supporting detail.
|
||||
<div className="space-y-4">
|
||||
{/* Einordnung — the classification tiles. */}
|
||||
{(hasInTargetTile || hasStabilityTile) && (
|
||||
// Two-up only when BOTH tiles render; a lone tile spans full width so
|
||||
// it never leaves a half-width orphan with dead space beside it.
|
||||
@@ -229,15 +255,25 @@ export function MoodInsightsSections() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<MoodNarrativeFeed items={narratives} />
|
||||
|
||||
{/* "Was zu deinen besseren Tagen passt" — the better-days assessment,
|
||||
ranked by effect size. The substance of the Einschätzung sits here,
|
||||
directly under the classification. */}
|
||||
{hasBetterDays && (
|
||||
<SectionCard title={t("insights.mood.betterDays.title")}>
|
||||
<SectionCard
|
||||
title={t("insights.mood.betterDays.title")}
|
||||
icon={Sparkles}
|
||||
>
|
||||
<MoodBetterDays factors={betterDays} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
<SectionCard title={t("insights.mood.heatmapTitle")}>
|
||||
{narratives.length > 0 && (
|
||||
<SectionCard title={t("insights.mood.narrative.title")} icon={TrendingUp}>
|
||||
<MoodNarrativeFeed items={narratives} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
<SectionCard title={t("insights.mood.heatmapTitle")} icon={CalendarDays}>
|
||||
<MoodHeatmap
|
||||
cells={heatmapCells}
|
||||
days={data.heatmap.windowDays}
|
||||
@@ -246,49 +282,58 @@ export function MoodInsightsSections() {
|
||||
</SectionCard>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
<SectionCard title={t("insights.mood.distributionTitle")}>
|
||||
<SectionCard
|
||||
title={t("insights.mood.distributionTitle")}
|
||||
icon={BarChart3}
|
||||
>
|
||||
<MoodDistributionChart distribution={data.distribution} />
|
||||
</SectionCard>
|
||||
<SectionCard title={t("insights.mood.weekdayTitle")}>
|
||||
<SectionCard
|
||||
title={t("insights.mood.weekdayTitle")}
|
||||
icon={CalendarRange}
|
||||
>
|
||||
<MoodWeekdayChart weekday={data.weekday} />
|
||||
</SectionCard>
|
||||
</div>
|
||||
|
||||
{data.timeOfDay.reliable && (
|
||||
<SectionCard title={t("insights.mood.timeOfDay.title")}>
|
||||
<SectionCard title={t("insights.mood.timeOfDay.title")} icon={Clock}>
|
||||
<MoodTimeOfDayChart pattern={data.timeOfDay} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{hasStructuredTags && (
|
||||
<SectionCard title={t("insights.mood.structuredTagsTitle")}>
|
||||
<SectionCard
|
||||
title={t("insights.mood.structuredTagsTitle")}
|
||||
icon={Tags}
|
||||
>
|
||||
<MoodStructuredTagBreakdown tags={data.structuredTags} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{hasTags && (
|
||||
<SectionCard title={t("insights.mood.tagsTitle")}>
|
||||
<SectionCard title={t("insights.mood.tagsTitle")} icon={Tag}>
|
||||
<MoodTagBreakdown tags={data.tags} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{hasInfluence && (
|
||||
<SectionCard title={t("insights.mood.influence.title")}>
|
||||
<SectionCard title={t("insights.mood.influence.title")} icon={TrendingUp}>
|
||||
<MoodTagInfluence rows={influenceRows} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
{hasCrosstab && (
|
||||
<SectionCard title={t("insights.mood.crosstab.title")}>
|
||||
<SectionCard title={t("insights.mood.crosstab.title")} icon={Grid3x3}>
|
||||
<MoodTagMetricCrosstab rows={crosstabRows} />
|
||||
</SectionCard>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-semibold">
|
||||
{t("insights.mood.correlationsTitle")}
|
||||
</h3>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
<SectionCard
|
||||
title={t("insights.mood.correlationsTitle")}
|
||||
icon={Link2}
|
||||
>
|
||||
<p className="text-muted-foreground mb-2 text-sm">
|
||||
{t("insights.mood.correlationsDescription")}
|
||||
</p>
|
||||
<MoodCorrelationCards
|
||||
@@ -298,7 +343,7 @@ export function MoodInsightsSections() {
|
||||
weight={data.correlations.weight}
|
||||
bloodPressureSystolic={data.correlations.bloodPressureSystolic}
|
||||
/>
|
||||
</div>
|
||||
</SectionCard>
|
||||
|
||||
{/* F3 — the FDR-controlled discovered mood relations. Self-fetches the
|
||||
correlation-discovery surface and renders nothing when the operator
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
import { Activity } from "lucide-react";
|
||||
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { TileHeader } from "@/components/insights/tile-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
@@ -46,20 +47,21 @@ export function MoodStabilityTile({
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardContent className="flex items-center gap-4 py-4">
|
||||
<div className="bg-muted text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-full">
|
||||
<Activity className="size-5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<p className="text-foreground text-2xl font-semibold tabular-nums">
|
||||
{t("insights.mood.stability.value", {
|
||||
score: String(stability.score),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(BAND_KEY[stability.band])}
|
||||
</p>
|
||||
</div>
|
||||
<CardHeader className="pb-2">
|
||||
<TileHeader
|
||||
icon={Activity}
|
||||
title={t("insights.mood.stability.title")}
|
||||
/>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-foreground text-2xl font-semibold tabular-nums">
|
||||
{t("insights.mood.stability.value", {
|
||||
score: String(stability.score),
|
||||
})}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t(BAND_KEY[stability.band])}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user