feat: Add advanced analytics UI components in frontend
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

- Add comprehensive API client methods for all advanced analytics endpoints
- Create CircadianRhythmCard component for sleep pattern visualization
- Create AnomalyAlertsPanel for anomaly detection and alerts
- Create GrowthPercentileChart with WHO/CDC percentiles
- Create CorrelationInsights for activity correlations
- Create TrendAnalysisChart with predictions
- Add advanced analytics page with all new components
- Add UI component library (shadcn/ui) setup
- Add navigation link to advanced analytics from insights page

All advanced analytics features are now accessible from the frontend UI.
This commit is contained in:
2025-10-06 11:46:05 +00:00
parent 56d2d83418
commit b0ac2f71df
19 changed files with 3112 additions and 13 deletions

View File

@@ -0,0 +1,242 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
AlertTriangle,
AlertCircle,
Info,
TrendingUp,
Activity,
Clock,
ChevronRight,
} from 'lucide-react';
import { AnomalyDetection } from '@/lib/api/analytics';
import { formatDistanceToNow } from 'date-fns';
interface AnomalyAlertsPanelProps {
data: AnomalyDetection | null;
loading?: boolean;
error?: Error | null;
}
export function AnomalyAlertsPanel({ data, loading, error }: AnomalyAlertsPanelProps) {
const [expandedAnomaly, setExpandedAnomaly] = useState<string | null>(null);
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing patterns for anomalies...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading anomaly detection data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getSeverityColor = (severity: string) => {
switch (severity) {
case 'high':
case 'critical':
return 'bg-red-100 text-red-800 border-red-300';
case 'medium':
case 'warning':
return 'bg-yellow-100 text-yellow-800 border-yellow-300';
case 'low':
case 'info':
return 'bg-blue-100 text-blue-800 border-blue-300';
default:
return 'bg-gray-100 text-gray-800 border-gray-300';
}
};
const getSeverityIcon = (severity: string) => {
switch (severity) {
case 'high':
case 'critical':
return <AlertTriangle className="h-4 w-4" />;
case 'medium':
case 'warning':
return <AlertCircle className="h-4 w-4" />;
default:
return <Info className="h-4 w-4" />;
}
};
const criticalAlerts = data.alerts.filter(a => a.severity === 'critical');
const warningAlerts = data.alerts.filter(a => a.severity === 'warning');
const infoAlerts = data.alerts.filter(a => a.severity === 'info');
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Activity className="h-5 w-5" />
Anomaly Detection
</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-500">Confidence</span>
<Badge variant="outline">
{Math.round(data.confidenceScore * 100)}%
</Badge>
</div>
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
{/* Confidence Score Bar */}
<div className="space-y-2">
<Progress value={data.confidenceScore * 100} className="h-2" />
<p className="text-xs text-gray-500">
Analysis based on {data.anomalies.length} detected patterns
</p>
</div>
{/* Alert Summary */}
{(criticalAlerts.length > 0 || warningAlerts.length > 0 || infoAlerts.length > 0) && (
<div className="grid grid-cols-3 gap-2 mb-4">
{criticalAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-red-50 rounded-lg">
<AlertTriangle className="h-4 w-4 text-red-600" />
<span className="text-sm font-medium text-red-700">
{criticalAlerts.length} Critical
</span>
</div>
)}
{warningAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-yellow-50 rounded-lg">
<AlertCircle className="h-4 w-4 text-yellow-600" />
<span className="text-sm font-medium text-yellow-700">
{warningAlerts.length} Warning
</span>
</div>
)}
{infoAlerts.length > 0 && (
<div className="flex items-center gap-2 p-2 bg-blue-50 rounded-lg">
<Info className="h-4 w-4 text-blue-600" />
<span className="text-sm font-medium text-blue-700">
{infoAlerts.length} Info
</span>
</div>
)}
</div>
)}
<Tabs defaultValue="alerts" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="alerts">Alerts ({data.alerts.length})</TabsTrigger>
<TabsTrigger value="anomalies">Anomalies ({data.anomalies.length})</TabsTrigger>
</TabsList>
<TabsContent value="alerts" className="space-y-3">
{data.alerts.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No alerts detected - everything looks normal!
</div>
) : (
data.alerts.map((alert, index) => (
<Alert
key={index}
className={`${getSeverityColor(alert.severity)} border`}
>
<div className="flex items-start gap-3">
{getSeverityIcon(alert.severity)}
<div className="flex-1">
<AlertTitle className="mb-2">{alert.type}</AlertTitle>
<AlertDescription>{alert.message}</AlertDescription>
{alert.recommendations && alert.recommendations.length > 0 && (
<div className="mt-3 space-y-1">
<p className="text-xs font-medium">Recommendations:</p>
<ul className="text-xs space-y-1">
{alert.recommendations.map((rec, idx) => (
<li key={idx} className="flex items-start gap-1">
<ChevronRight className="h-3 w-3 mt-0.5 flex-shrink-0" />
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</div>
</div>
</Alert>
))
)}
</TabsContent>
<TabsContent value="anomalies" className="space-y-3">
{data.anomalies.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No anomalies detected in recent activities
</div>
) : (
data.anomalies.map((anomaly) => (
<div
key={anomaly.activityId}
className="border rounded-lg p-3 space-y-2 hover:bg-gray-50 cursor-pointer transition-colors"
onClick={() => setExpandedAnomaly(
expandedAnomaly === anomaly.activityId ? null : anomaly.activityId
)}
>
<div className="flex items-start justify-between">
<div className="flex items-start gap-2">
<Badge className={getSeverityColor(anomaly.severity)}>
{anomaly.severity}
</Badge>
<div>
<p className="text-sm font-medium capitalize">
{anomaly.type} Activity
</p>
<p className="text-xs text-gray-500 mt-1">
<Clock className="h-3 w-3 inline mr-1" />
{formatDistanceToNow(anomaly.timestamp, { addSuffix: true })}
</p>
</div>
</div>
<div className="text-right">
<p className="text-xs text-gray-500">Deviation</p>
<p className="text-sm font-medium">
{anomaly.deviation.toFixed(1)}σ
</p>
</div>
</div>
{expandedAnomaly === anomaly.activityId && (
<div className="pt-2 border-t space-y-2">
<p className="text-sm text-gray-600">{anomaly.description}</p>
<div className="flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-gray-400" />
<span className="text-xs text-gray-500">
Statistical deviation: {anomaly.deviation.toFixed(2)} standard deviations from normal
</span>
</div>
</div>
)}
</div>
))
)}
</TabsContent>
</Tabs>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,197 @@
'use client';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import { Badge } from '@/components/ui/badge';
import { Moon, Sun, Clock, Brain, AlertCircle, CheckCircle2 } from 'lucide-react';
import { CircadianRhythm } from '@/lib/api/analytics';
interface CircadianRhythmCardProps {
data: CircadianRhythm | null;
loading?: boolean;
error?: Error | null;
}
export function CircadianRhythmCard({ data, loading, error }: CircadianRhythmCardProps) {
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Loading circadian rhythm analysis...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading circadian rhythm data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getChronotypeIcon = () => {
switch (data.chronotype) {
case 'early_bird':
return <Sun className="h-5 w-5 text-yellow-500" />;
case 'night_owl':
return <Moon className="h-5 w-5 text-indigo-500" />;
default:
return <Clock className="h-5 w-5 text-gray-500" />;
}
};
const getChronotypeColor = () => {
switch (data.chronotype) {
case 'early_bird':
return 'bg-yellow-100 text-yellow-800';
case 'night_owl':
return 'bg-indigo-100 text-indigo-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const formatTime = (time: string) => {
// Convert HH:MM to 12-hour format
const [hours, minutes] = time.split(':');
const hour = parseInt(hours);
const ampm = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour;
return `${displayHour}:${minutes} ${ampm}`;
};
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Brain className="h-5 w-5" />
Circadian Rhythm Analysis
</span>
<Badge className={getChronotypeColor()}>
<span className="flex items-center gap-1">
{getChronotypeIcon()}
{data.chronotype.replace('_', ' ')}
</span>
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Sleep Consistency Score */}
<div className="space-y-2">
<div className="flex justify-between text-sm">
<span className="text-gray-600">Sleep Consistency</span>
<span className="font-medium">{Math.round(data.consistency * 100)}%</span>
</div>
<Progress value={data.consistency * 100} className="h-2" />
<p className="text-xs text-gray-500">
{data.consistency > 0.8
? 'Excellent - Very consistent sleep schedule'
: data.consistency > 0.6
? 'Good - Fairly consistent schedule'
: 'Needs improvement - Irregular sleep pattern'}
</p>
</div>
{/* Optimal Times */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<div className="flex items-center gap-2">
<Moon className="h-4 w-4 text-blue-500" />
<span className="text-sm text-gray-600">Optimal Bedtime</span>
</div>
<p className="text-lg font-semibold">{formatTime(data.optimalBedtime)}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2">
<Sun className="h-4 w-4 text-yellow-500" />
<span className="text-sm text-gray-600">Optimal Wake Time</span>
</div>
<p className="text-lg font-semibold">{formatTime(data.optimalWakeTime)}</p>
</div>
</div>
{/* Sleep Phase Shift */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Sleep Phase Shift</span>
<span className="text-sm font-medium">
{data.sleepPhaseShift > 0 ? '+' : ''}{data.sleepPhaseShift.toFixed(1)} hours
</span>
</div>
<div className="flex items-center gap-2">
{Math.abs(data.sleepPhaseShift) < 1 ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<AlertCircle className="h-4 w-4 text-yellow-500" />
)}
<p className="text-xs text-gray-500">
{Math.abs(data.sleepPhaseShift) < 1
? 'Sleep schedule aligned with typical patterns'
: data.sleepPhaseShift > 0
? `Bedtime is ${Math.abs(data.sleepPhaseShift).toFixed(1)} hours later than typical`
: `Bedtime is ${Math.abs(data.sleepPhaseShift).toFixed(1)} hours earlier than typical`}
</p>
</div>
</div>
{/* Melatonin Onset */}
<div className="p-3 bg-purple-50 rounded-lg">
<div className="flex items-center gap-2 mb-1">
<Brain className="h-4 w-4 text-purple-600" />
<span className="text-sm font-medium text-purple-900">Estimated Melatonin Onset</span>
</div>
<p className="text-lg font-semibold text-purple-700">{formatTime(data.melatoninOnset)}</p>
<p className="text-xs text-purple-600 mt-1">
Natural sleepiness begins around this time
</p>
</div>
{/* Recommended Schedule */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700">Recommended Daily Schedule</h4>
<div className="space-y-2 text-sm">
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Wake Time</span>
<span className="font-medium">{formatTime(data.recommendedSchedule.wakeTime)}</span>
</div>
{data.recommendedSchedule.morningNap && (
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Morning Nap</span>
<span className="font-medium">
{formatTime(data.recommendedSchedule.morningNap.start)} ({data.recommendedSchedule.morningNap.duration} min)
</span>
</div>
)}
{data.recommendedSchedule.afternoonNap && (
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Afternoon Nap</span>
<span className="font-medium">
{formatTime(data.recommendedSchedule.afternoonNap.start)} ({data.recommendedSchedule.afternoonNap.duration} min)
</span>
</div>
)}
<div className="flex justify-between py-2 border-b">
<span className="text-gray-600">Bedtime</span>
<span className="font-medium">{formatTime(data.recommendedSchedule.bedtime)}</span>
</div>
<div className="flex justify-between py-2">
<span className="text-gray-600">Daily Sleep Target</span>
<span className="font-medium">{Math.round(data.recommendedSchedule.totalSleepTarget / 60)} hours</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,235 @@
'use client';
import React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import {
Link,
Link2Off,
TrendingUp,
TrendingDown,
Moon,
Utensils,
Baby,
Activity,
Info,
CheckCircle,
} from 'lucide-react';
import { CorrelationAnalysis } from '@/lib/api/analytics';
interface CorrelationInsightsProps {
data: CorrelationAnalysis | null;
loading?: boolean;
error?: Error | null;
}
export function CorrelationInsights({ data, loading, error }: CorrelationInsightsProps) {
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing activity correlations...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<Link2Off className="h-5 w-5 mr-2" />
Error loading correlation data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getCorrelationStrength = (value: number) => {
const absValue = Math.abs(value);
if (absValue > 0.7) return 'Strong';
if (absValue > 0.4) return 'Moderate';
if (absValue > 0.2) return 'Weak';
return 'Negligible';
};
const getCorrelationColor = (value: number) => {
const absValue = Math.abs(value);
if (absValue > 0.7) return 'bg-purple-100 text-purple-800';
if (absValue > 0.4) return 'bg-blue-100 text-blue-800';
if (absValue > 0.2) return 'bg-gray-100 text-gray-800';
return 'bg-gray-50 text-gray-600';
};
const getCorrelationIcon = (value: number) => {
if (value > 0.3) return <TrendingUp className="h-4 w-4 text-green-500" />;
if (value < -0.3) return <TrendingDown className="h-4 w-4 text-red-500" />;
return <Activity className="h-4 w-4 text-gray-400" />;
};
const formatCorrelation = (value: number) => {
return (value * 100).toFixed(0) + '%';
};
const correlations = [
{
name: 'Feeding & Sleep',
value: data.feedingSleepCorrelation,
icon1: <Utensils className="h-4 w-4" />,
icon2: <Moon className="h-4 w-4" />,
description: data.feedingSleepCorrelation > 0
? 'Better feeding patterns correlate with better sleep'
: data.feedingSleepCorrelation < 0
? 'More feedings may be disrupting sleep patterns'
: 'No significant relationship detected',
},
{
name: 'Activity & Diapers',
value: data.activityDiaperCorrelation,
icon1: <Activity className="h-4 w-4" />,
icon2: <Baby className="h-4 w-4" />,
description: data.activityDiaperCorrelation > 0
? 'More activity correlates with more diaper changes'
: data.activityDiaperCorrelation < 0
? 'Less active periods show more diaper changes'
: 'No clear pattern between activity and diapers',
},
...(data.sleepMoodCorrelation !== undefined ? [{
name: 'Sleep & Mood',
value: data.sleepMoodCorrelation,
icon1: <Moon className="h-4 w-4" />,
icon2: <Activity className="h-4 w-4" />,
description: data.sleepMoodCorrelation > 0
? 'Better sleep strongly correlates with better mood'
: data.sleepMoodCorrelation < 0
? 'Sleep patterns inversely affect mood'
: 'Sleep and mood appear independent',
}] : []),
];
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Link className="h-5 w-5" />
Activity Correlations
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Correlation Visualizations */}
<div className="space-y-4">
{correlations.map((correlation) => (
<div key={correlation.name} className="space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 text-gray-600">
{correlation.icon1}
<span className="text-sm font-medium">&</span>
{correlation.icon2}
</div>
<span className="text-sm font-medium">{correlation.name}</span>
</div>
<div className="flex items-center gap-2">
{getCorrelationIcon(correlation.value)}
<Badge className={getCorrelationColor(correlation.value)}>
{getCorrelationStrength(correlation.value)}
</Badge>
</div>
</div>
{/* Correlation Bar */}
<div className="relative h-8 bg-gray-100 rounded-full overflow-hidden">
<div
className="absolute top-0 h-full bg-gradient-to-r from-red-400 via-gray-300 to-green-400"
style={{
width: '100%',
opacity: 0.3,
}}
/>
<div
className="absolute top-0 h-full flex items-center justify-center"
style={{
left: '50%',
transform: 'translateX(-50%)',
}}
>
<div className="w-0.5 h-full bg-gray-400" />
</div>
<div
className="absolute top-0 h-full flex items-center"
style={{
left: correlation.value > 0 ? '50%' : `${50 + correlation.value * 50}%`,
width: `${Math.abs(correlation.value) * 50}%`,
}}
>
<div
className={`h-4 rounded-full ${
correlation.value > 0 ? 'bg-green-500' : 'bg-red-500'
}`}
style={{ width: '100%' }}
/>
</div>
<div
className="absolute top-0 h-full flex items-center justify-end pr-2"
style={{
left: correlation.value > 0 ? `${50 + correlation.value * 50}%` : '50%',
transform: correlation.value > 0 ? 'translateX(-100%)' : 'none',
}}
>
<span className="text-xs font-medium text-white drop-shadow">
{formatCorrelation(correlation.value)}
</span>
</div>
</div>
<p className="text-xs text-gray-600">{correlation.description}</p>
</div>
))}
</div>
{/* Correlation Scale Legend */}
<div className="p-3 bg-gray-50 rounded-lg space-y-2">
<h4 className="text-xs font-medium text-gray-700 uppercase tracking-wider">
Correlation Scale
</h4>
<div className="grid grid-cols-2 gap-2 text-xs">
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-green-500" />
<span>Positive: Activities increase together</span>
</div>
<div className="flex items-center gap-2">
<div className="w-3 h-3 rounded-full bg-red-500" />
<span>Negative: One increases, other decreases</span>
</div>
</div>
</div>
{/* Insights */}
{data.insights.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Info className="h-4 w-4" />
Key Insights
</h4>
<div className="space-y-2">
{data.insights.map((insight, index) => (
<div
key={index}
className="flex items-start gap-2 p-3 bg-blue-50 rounded-lg"
>
<CheckCircle className="h-4 w-4 text-blue-500 mt-0.5 flex-shrink-0" />
<p className="text-sm text-blue-900">{insight}</p>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,307 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Progress } from '@/components/ui/progress';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Alert, AlertDescription } from '@/components/ui/alert';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
Area,
AreaChart,
} from 'recharts';
import {
Ruler,
Weight,
TrendingUp,
Baby,
AlertCircle,
ChevronUp,
ChevronDown,
Minus,
} from 'lucide-react';
import { GrowthAnalysis, GrowthPercentile } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface GrowthPercentileChartProps {
data: GrowthAnalysis | null;
loading?: boolean;
error?: Error | null;
}
export function GrowthPercentileChart({ data, loading, error }: GrowthPercentileChartProps) {
const [selectedMetric, setSelectedMetric] = useState<'weight' | 'height' | 'headCircumference'>('weight');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Loading growth analysis...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<AlertCircle className="h-5 w-5 mr-2" />
Error loading growth data
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getPercentileColor = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'text-red-600';
if (percentile < 15 || percentile > 85) return 'text-yellow-600';
return 'text-green-600';
};
const getPercentileBadge = (percentile: number) => {
if (percentile < 5 || percentile > 95) return 'bg-red-100 text-red-800';
if (percentile < 15 || percentile > 85) return 'bg-yellow-100 text-yellow-800';
return 'bg-green-100 text-green-800';
};
const getVelocityIcon = (value: number) => {
if (value > 0) return <ChevronUp className="h-4 w-4 text-green-500" />;
if (value < 0) return <ChevronDown className="h-4 w-4 text-red-500" />;
return <Minus className="h-4 w-4 text-gray-500" />;
};
const formatMeasurement = (value: number | undefined, metric: string) => {
if (!value) return 'N/A';
switch (metric) {
case 'weight':
return `${value.toFixed(1)} kg`;
case 'height':
return `${value.toFixed(1)} cm`;
case 'headCircumference':
return `${value.toFixed(1)} cm`;
default:
return value.toFixed(1);
}
};
// Prepare chart data
const chartData = data.growthCurve.measurements.map(m => ({
date: format(m.date, 'MMM dd'),
weight: m.weight,
height: m.height,
headCircumference: m.headCircumference,
p3: data.growthCurve.percentileCurves[selectedMetric]?.p3?.[0],
p15: data.growthCurve.percentileCurves[selectedMetric]?.p15?.[0],
p50: data.growthCurve.percentileCurves[selectedMetric]?.p50?.[0],
p85: data.growthCurve.percentileCurves[selectedMetric]?.p85?.[0],
p97: data.growthCurve.percentileCurves[selectedMetric]?.p97?.[0],
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<Baby className="h-5 w-5" />
Growth Analysis
</span>
{data.alerts.length > 0 && (
<Badge variant="destructive">
{data.alerts.length} Alert{data.alerts.length > 1 ? 's' : ''}
</Badge>
)}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Current Percentiles */}
{data.currentPercentiles && (
<div className="grid grid-cols-3 gap-4">
{data.currentPercentiles.weight && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Weight className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Weight</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.weight.value.toFixed(1)} kg
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.weight.percentile)}>
{Math.round(data.currentPercentiles.weight.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.weight.interpretation}
</p>
</div>
)}
{data.currentPercentiles.height && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Ruler className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Height</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.height.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.height.percentile)}>
{Math.round(data.currentPercentiles.height.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.height.interpretation}
</p>
</div>
)}
{data.currentPercentiles.headCircumference && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Baby className="h-4 w-4 text-gray-500" />
<span className="text-sm text-gray-600">Head Circ.</span>
</div>
<p className="text-lg font-semibold">
{data.currentPercentiles.headCircumference.value.toFixed(1)} cm
</p>
<Badge className={getPercentileBadge(data.currentPercentiles.headCircumference.percentile)}>
{Math.round(data.currentPercentiles.headCircumference.percentile)}th percentile
</Badge>
<p className="text-xs text-gray-500">
{data.currentPercentiles.headCircumference.interpretation}
</p>
</div>
)}
</div>
)}
{/* Growth Velocity */}
{data.growthVelocity && (
<div className="p-4 bg-blue-50 rounded-lg space-y-3">
<h4 className="text-sm font-medium text-blue-900">Growth Velocity</h4>
<div className="grid grid-cols-2 gap-4">
{data.growthVelocity.weightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Weight Gain</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.weightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.weightVelocity.value.toFixed(2)} {data.growthVelocity.weightVelocity.unit}
</span>
</div>
</div>
)}
{data.growthVelocity.heightVelocity && (
<div className="flex items-center justify-between">
<span className="text-sm text-blue-700">Height Growth</span>
<div className="flex items-center gap-1">
{getVelocityIcon(data.growthVelocity.heightVelocity.value)}
<span className="font-medium text-blue-900">
{data.growthVelocity.heightVelocity.value.toFixed(2)} {data.growthVelocity.heightVelocity.unit}
</span>
</div>
</div>
)}
</div>
<p className="text-xs text-blue-600">{data.growthVelocity.interpretation}</p>
</div>
)}
{/* Growth Chart */}
<Tabs value={selectedMetric} onValueChange={(v) => setSelectedMetric(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="weight">Weight</TabsTrigger>
<TabsTrigger value="height">Height</TabsTrigger>
<TabsTrigger value="headCircumference">Head Circ.</TabsTrigger>
</TabsList>
<TabsContent value={selectedMetric} className="mt-4">
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="date" />
<YAxis />
<Tooltip />
<Legend />
{/* Percentile reference lines */}
<ReferenceLine
y={chartData[0]?.p50}
stroke="#22c55e"
strokeDasharray="3 3"
label="50th %ile"
/>
<ReferenceLine
y={chartData[0]?.p15}
stroke="#eab308"
strokeDasharray="3 3"
label="15th %ile"
/>
<ReferenceLine
y={chartData[0]?.p85}
stroke="#eab308"
strokeDasharray="3 3"
label="85th %ile"
/>
{/* Child's measurements */}
<Line
type="monotone"
dataKey={selectedMetric}
stroke="#3b82f6"
strokeWidth={3}
dot={{ fill: '#3b82f6', r: 6 }}
name={`Child's ${selectedMetric}`}
/>
</LineChart>
</ResponsiveContainer>
</TabsContent>
</Tabs>
{/* Alerts */}
{data.alerts.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Growth Alerts</h4>
{data.alerts.map((alert, index) => (
<Alert key={index} className={
alert.severity === 'high' ? 'border-red-300 bg-red-50' :
alert.severity === 'medium' ? 'border-yellow-300 bg-yellow-50' :
'border-blue-300 bg-blue-50'
}>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
<p className="font-medium">{alert.message}</p>
<p className="text-sm mt-1">{alert.action}</p>
</AlertDescription>
</Alert>
))}
</div>
)}
{/* Recommendations */}
{data.recommendations.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-medium text-gray-700">Recommendations</h4>
<ul className="space-y-1">
{data.recommendations.map((rec, index) => (
<li key={index} className="flex items-start gap-2 text-sm text-gray-600">
<TrendingUp className="h-4 w-4 text-green-500 mt-0.5 flex-shrink-0" />
<span>{rec}</span>
</li>
))}
</ul>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,273 @@
'use client';
import React, { useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Progress } from '@/components/ui/progress';
import {
LineChart,
Line,
Area,
AreaChart,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
ReferenceLine,
} from 'recharts';
import {
TrendingUp,
TrendingDown,
Minus,
Calendar,
Target,
ChartLine,
Activity,
} from 'lucide-react';
import { TrendAnalysis } from '@/lib/api/analytics';
import { format } from 'date-fns';
interface TrendAnalysisChartProps {
data: TrendAnalysis | null;
activityType: string;
loading?: boolean;
error?: Error | null;
}
export function TrendAnalysisChart({ data, activityType, loading, error }: TrendAnalysisChartProps) {
const [selectedTimeframe, setSelectedTimeframe] = useState<'short' | 'medium' | 'long'>('short');
if (loading) {
return (
<Card>
<CardContent className="flex items-center justify-center h-64">
<div className="animate-pulse">Analyzing trends...</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="border-red-200">
<CardContent className="flex items-center justify-center h-64 text-red-500">
<Activity className="h-5 w-5 mr-2" />
Error loading trend analysis
</CardContent>
</Card>
);
}
if (!data) {
return null;
}
const getTrendIcon = (direction: string) => {
switch (direction) {
case 'improving':
return <TrendingUp className="h-4 w-4 text-green-500" />;
case 'declining':
return <TrendingDown className="h-4 w-4 text-red-500" />;
default:
return <Minus className="h-4 w-4 text-gray-500" />;
}
};
const getTrendColor = (direction: string) => {
switch (direction) {
case 'improving':
return 'bg-green-100 text-green-800';
case 'declining':
return 'bg-red-100 text-red-800';
default:
return 'bg-gray-100 text-gray-800';
}
};
const getTrendData = () => {
switch (selectedTimeframe) {
case 'short':
return data.shortTermTrend;
case 'medium':
return data.mediumTermTrend;
case 'long':
return data.longTermTrend;
}
};
const currentTrend = getTrendData();
// Prepare chart data for predictions
const chartData = data.prediction.next7Days.map((point, index) => ({
day: format(point.date, 'MMM dd'),
predicted: point.predictedValue,
upperBound: point.confidenceInterval.upper,
lowerBound: point.confidenceInterval.lower,
}));
return (
<Card className="w-full">
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2">
<ChartLine className="h-5 w-5" />
{activityType} Trend Analysis
</span>
<Badge className={getTrendColor(currentTrend.direction)}>
<span className="flex items-center gap-1">
{getTrendIcon(currentTrend.direction)}
{currentTrend.direction}
</span>
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
{/* Timeframe Tabs */}
<Tabs value={selectedTimeframe} onValueChange={(v) => setSelectedTimeframe(v as any)}>
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="short">Short (7 days)</TabsTrigger>
<TabsTrigger value="medium">Medium (14 days)</TabsTrigger>
<TabsTrigger value="long">Long (30 days)</TabsTrigger>
</TabsList>
<TabsContent value={selectedTimeframe} className="space-y-4 mt-4">
{/* Trend Metrics */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Change</span>
<span className="font-medium">
{currentTrend.changePercent > 0 ? '+' : ''}{currentTrend.changePercent.toFixed(1)}%
</span>
</div>
<Progress
value={Math.abs(currentTrend.changePercent)}
className="h-2"
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm text-gray-600">Confidence</span>
<span className="font-medium">{(currentTrend.confidence * 100).toFixed(0)}%</span>
</div>
<Progress value={currentTrend.confidence * 100} className="h-2" />
</div>
</div>
{/* Statistical Details */}
<div className="p-3 bg-gray-50 rounded-lg grid grid-cols-3 gap-3 text-sm">
<div>
<p className="text-gray-600">Slope</p>
<p className="font-medium">{currentTrend.slope.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">R² Score</p>
<p className="font-medium">{currentTrend.r2Score.toFixed(3)}</p>
</div>
<div>
<p className="text-gray-600">Trend</p>
<p className="font-medium capitalize">{currentTrend.direction}</p>
</div>
</div>
</TabsContent>
</Tabs>
{/* Prediction Chart */}
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Target className="h-4 w-4" />
7-Day Forecast
<Badge variant="outline" className="ml-auto">
{(data.prediction.confidence * 100).toFixed(0)}% confidence
</Badge>
</h4>
<ResponsiveContainer width="100%" height={250}>
<AreaChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="day" />
<YAxis />
<Tooltip />
<Legend />
{/* Confidence interval area */}
<Area
type="monotone"
dataKey="upperBound"
stroke="none"
fill="#e0e7ff"
fillOpacity={0.3}
name="Upper bound"
/>
<Area
type="monotone"
dataKey="lowerBound"
stroke="none"
fill="#ffffff"
fillOpacity={1}
name="Lower bound"
/>
{/* Predicted trend line */}
<Line
type="monotone"
dataKey="predicted"
stroke="#6366f1"
strokeWidth={2}
dot={{ fill: '#6366f1', r: 4 }}
name="Predicted"
/>
</AreaChart>
</ResponsiveContainer>
{/* Prediction Factors */}
{data.prediction.factors.length > 0 && (
<div className="p-3 bg-blue-50 rounded-lg space-y-2">
<p className="text-xs font-medium text-blue-900 uppercase tracking-wider">
Factors Influencing Prediction
</p>
<div className="flex flex-wrap gap-2">
{data.prediction.factors.map((factor, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{factor}
</Badge>
))}
</div>
</div>
)}
</div>
{/* Seasonal Patterns */}
{data.seasonalPatterns && data.seasonalPatterns.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-medium text-gray-700 flex items-center gap-2">
<Calendar className="h-4 w-4" />
Seasonal Patterns Detected
</h4>
<div className="space-y-2">
{data.seasonalPatterns.map((pattern, index) => (
<div key={index} className="flex items-center justify-between p-3 bg-purple-50 rounded-lg">
<div>
<p className="text-sm font-medium text-purple-900 capitalize">
{pattern.type} Pattern
</p>
<p className="text-xs text-purple-700">{pattern.pattern}</p>
</div>
<div className="text-right">
<p className="text-xs text-purple-600">Strength</p>
<p className="text-sm font-medium text-purple-900">
{(pattern.strength * 100).toFixed(0)}%
</p>
</div>
</div>
))}
</div>
</div>
)}
</CardContent>
</Card>
);
}

View File

@@ -14,6 +14,7 @@ import {
Alert,
Grid,
IconButton,
Button,
} from '@mui/material';
import { Timeline, TrendingUp, Assessment, ArrowBack } from '@mui/icons-material';
import { useRouter } from 'next/navigation';
@@ -170,18 +171,28 @@ export function UnifiedInsightsDashboard() {
>
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
{/* Header with Back Button */}
<Box sx={{ display: 'flex', alignItems: 'center', mb: 3 }}>
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
<ArrowBack />
</IconButton>
<Box>
<Typography variant="h4" fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<IconButton onClick={() => router.back()} sx={{ mr: 2 }}>
<ArrowBack />
</IconButton>
<Box>
<Typography variant="h4" fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
</Box>
</Box>
<Button
variant="outlined"
color="primary"
onClick={() => router.push('/analytics/advanced')}
startIcon={<TrendingUp />}
>
Advanced Analytics
</Button>
</Box>
{/* Error Alert */}

View File

@@ -22,7 +22,7 @@ export const TabBar = () => {
{ label: t('navigation.home'), icon: <Home />, value: '/' },
{ label: t('navigation.track'), icon: <Timeline />, value: '/track' },
{ label: '', icon: null, value: 'voice' }, // Placeholder for center button
{ label: t('navigation.insights'), icon: <Insights />, value: '/insights' },
{ label: t('navigation.insights'), icon: <Insights />, value: pathname.startsWith('/analytics') ? pathname : '/insights' },
{ label: t('navigation.aiChat'), icon: <Chat />, value: '/ai-assistant' },
];

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View File

@@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@@ -0,0 +1,28 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
))
Progress.displayName = ProgressPrimitive.Root.displayName
export { Progress }

View File

@@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }