From eaca8674f5a6c96321e110fa6bfa72ce797bb7fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 6 Apr 2026 13:30:53 +0200 Subject: [PATCH] feat(ui): add premium InsightAdvisorCard and InsightStatusCard components New insight card components for the health advisor feature: - InsightAdvisorCard: full structured display with primary recommendation, findings, correlations, recommendations, and collapsible data quality - InsightStatusCard: compact per-metric status card with classification indicator - Subtle fade-in animation for insight cards in globals.css Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/globals.css | 16 + .../insights/insight-advisor-card.tsx | 398 ++++++++++++++++++ .../insights/insight-status-card.tsx | 119 ++++++ 3 files changed, 533 insertions(+) create mode 100644 src/components/insights/insight-advisor-card.tsx create mode 100644 src/components/insights/insight-status-card.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 95584be75..763c03b17 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -166,3 +166,19 @@ color: var(--foreground); } } + +/* ── Insight card animations ─────────────────────── */ +@keyframes insight-fade-in { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.animate-insight-in { + animation: insight-fade-in 0.4s ease-out forwards; +} diff --git a/src/components/insights/insight-advisor-card.tsx b/src/components/insights/insight-advisor-card.tsx new file mode 100644 index 000000000..682be2fa2 --- /dev/null +++ b/src/components/insights/insight-advisor-card.tsx @@ -0,0 +1,398 @@ +"use client"; + +import { useState } from "react"; +import type { InsightResult, InsightFinding } from "@/lib/ai/types"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { + Sparkles, + RefreshCw, + Loader2, + CheckCircle2, + Minus, + AlertCircle, + AlertTriangle, + ChevronDown, + ChevronUp, + Info, +} from "lucide-react"; + +// ─── Types ──────────────────────────────────────────────── + +interface InsightAdvisorCardProps { + title: string; + icon?: React.ReactNode; + insight: InsightResult | null; + loading?: boolean; + error?: string | null; + onRegenerate?: () => void; + regenerating?: boolean; + cachedAt?: string | null; +} + +// ─── Classification Colors ─────────────────────────────── + +const CLASSIFICATION_STYLES: Record< + string, + { badge: string; border: string } +> = { + optimal: { + badge: + "bg-[#50fa7b]/10 text-[#50fa7b] border border-[#50fa7b]/25 hover:bg-[#50fa7b]/15", + border: "border-l-[#50fa7b]", + }, + gut: { + badge: + "bg-[#8be9fd]/10 text-[#8be9fd] border border-[#8be9fd]/25 hover:bg-[#8be9fd]/15", + border: "border-l-[#8be9fd]", + }, + grenzwertig: { + badge: + "bg-[#f1fa8c]/10 text-[#f1fa8c] border border-[#f1fa8c]/25 hover:bg-[#f1fa8c]/15", + border: "border-l-[#f1fa8c]", + }, + erhoht: { + badge: + "bg-[#ffb86c]/10 text-[#ffb86c] border border-[#ffb86c]/25 hover:bg-[#ffb86c]/15", + border: "border-l-[#ffb86c]", + }, + kritisch: { + badge: + "bg-[#ff5555]/10 text-[#ff5555] border border-[#ff5555]/25 hover:bg-[#ff5555]/15", + border: "border-l-[#ff5555]", + }, +}; + +// ─── Assessment Icons ───────────────────────────────────── + +function AssessmentIcon({ + assessment, +}: { + assessment: InsightFinding["assessment"]; +}) { + switch (assessment) { + case "positive": + return ; + case "neutral": + return ; + case "attention": + return ; + case "warning": + return ; + } +} + +// ─── Confidence Colors ──────────────────────────────────── + +const CONFIDENCE_STYLES: Record = { + hoch: "text-[#50fa7b]", + mittel: "text-[#f1fa8c]", + gering: "text-[#ffb86c]", +}; + +// ─── Section Separator ──────────────────────────────────── + +function SectionSeparator({ label }: { label: string }) { + return ( +
+ {label} +
+
+ ); +} + +// ─── Main Component ─────────────────────────────────────── + +export function InsightAdvisorCard({ + title, + icon, + insight, + loading = false, + error = null, + onRegenerate, + regenerating = false, + cachedAt, +}: InsightAdvisorCardProps) { + const [dataQualityOpen, setDataQualityOpen] = useState(false); + + const classStyle = insight + ? CLASSIFICATION_STYLES[insight.classification] ?? + CLASSIFICATION_STYLES.gut + : CLASSIFICATION_STYLES.gut; + + // ── Loading State ───────────────────────────────────── + if (loading && !insight) { + return ( + + + + + {/* TODO: i18n */} + Analyse wird erstellt... + + + + ); + } + + // ── Empty / No Data State ───────────────────────────── + if (!insight) { + return ( + + +
+
+ {icon ?? ( + + )} + {/* TODO: i18n */} + KI-Gesundheitsanalyse +
+
+

{title}

+
+ + {error && ( +
+ + {error} +
+ )} + {!error && ( +
+

+ {/* TODO: i18n */} + Noch keine Analyse vorhanden. +

+ {onRegenerate && ( + + )} +
+ )} +
+
+ ); + } + + // ── Full Insight Card ───────────────────────────────── + return ( + + {/* Header */} + +
+
+ {icon ?? ( + + )} + {/* TODO: i18n */} + KI-Gesundheitsanalyse +
+
+ + {insight.classificationLabel ?? insight.classification} + + {onRegenerate && ( + + )} +
+
+

{title}

+
+ + + {/* Error banner */} + {error && ( +
+ + {error} +
+ )} + + {/* Primary Recommendation */} + {insight.primaryRecommendation && ( +
+

+ {/* TODO: i18n */} + Das Wichtigste +

+

{insight.primaryRecommendation}

+
+ )} + + {/* Summary */} +

+ {insight.summary} +

+ + {/* Findings */} + {insight.findings.length > 0 && ( +
+ {/* TODO: i18n */} + +
+ {insight.findings.map((finding, i) => ( +
+
+
+ + {finding.label} +
+ + {finding.value} + +
+ {finding.guideline && ( +

+ {finding.guideline} +

+ )} +
+ ))} +
+
+ )} + + {/* Correlations */} + {insight.correlations.length > 0 && ( +
+ {/* TODO: i18n */} + +
+ {insight.correlations.map((corr, i) => ( +
+ {corr.factor} + ↔ + {corr.effect} + + {corr.confidence} + +
+ ))} +
+
+ )} + + {/* Recommendations */} + {insight.recommendations.length > 0 && ( +
+ {/* TODO: i18n */} + +
    + {insight.recommendations.map((rec, i) => ( +
  1. + + {i + 1}. + + {rec} +
  2. + ))} +
+
+ )} + + {/* Data Quality (collapsible) */} + {insight.dataQuality && ( +
+ + + {dataQualityOpen && ( +
+

{insight.dataQuality.coverage}

+ {insight.dataQuality.gaps.length > 0 && ( +
+ {/* TODO: i18n */} +

Datenlücken:

+
    + {insight.dataQuality.gaps.map((gap, i) => ( +
  • {gap}
  • + ))} +
+
+ )} +
+ )} +
+ )} + + {/* Cached timestamp */} + {cachedAt && ( +

+ {/* TODO: i18n */} + Zuletzt aktualisiert:{" "} + {new Date(cachedAt).toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +

+ )} + + {/* Disclaimer */} +
+ +

+ {insight.disclaimer} +

+
+
+
+ ); +} diff --git a/src/components/insights/insight-status-card.tsx b/src/components/insights/insight-status-card.tsx new file mode 100644 index 000000000..cbff7b357 --- /dev/null +++ b/src/components/insights/insight-status-card.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Loader2 } from "lucide-react"; + +// ─── Types ──────────────────────────────────────────────── + +interface InsightStatusCardProps { + title: string; + icon: React.ReactNode; + text: string | null; + hasProvider: boolean; + cached: boolean; + updatedAt: string | null; + loading?: boolean; +} + +// ─── Main Component ─────────────────────────────────────── + +export function InsightStatusCard({ + title, + icon, + text, + hasProvider, + cached, + updatedAt, + loading = false, +}: InsightStatusCardProps) { + // ── Loading State ───────────────────────────────────── + if (loading) { + return ( + + + + + {/* TODO: i18n */} + Wird geladen... + + + + ); + } + + // ── No Provider State ───────────────────────────────── + if (!hasProvider) { + return ( + + +
+ {icon} + {title} +
+
+ +

+ {/* TODO: i18n */} + KI-Provider nicht konfiguriert. +

+
+
+ ); + } + + // ── No Data State ───────────────────────────────────── + if (!text) { + return ( + + +
+ {icon} + {title} +
+
+ +

+ {/* TODO: i18n */} + Noch keine Analyse vorhanden. +

+
+
+ ); + } + + // ── Populated Card ──────────────────────────────────── + return ( + + +
+
+ {icon} + {title} +
+ {cached && ( + + {/* TODO: i18n */} + Zwischengespeichert + + )} +
+
+ +

{text}

+ {updatedAt && ( +

+ {/* TODO: i18n */} + Zuletzt aktualisiert:{" "} + {new Date(updatedAt).toLocaleString("de-DE", { + day: "2-digit", + month: "2-digit", + year: "numeric", + hour: "2-digit", + minute: "2-digit", + })} +

+ )} +
+
+ ); +}