feat: Add advanced analytics UI components in frontend
- 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:
242
maternal-web/components/analytics/AnomalyAlertsPanel.tsx
Normal file
242
maternal-web/components/analytics/AnomalyAlertsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
197
maternal-web/components/analytics/CircadianRhythmCard.tsx
Normal file
197
maternal-web/components/analytics/CircadianRhythmCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
235
maternal-web/components/analytics/CorrelationInsights.tsx
Normal file
235
maternal-web/components/analytics/CorrelationInsights.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal file
307
maternal-web/components/analytics/GrowthPercentileChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
273
maternal-web/components/analytics/TrendAnalysisChart.tsx
Normal file
273
maternal-web/components/analytics/TrendAnalysisChart.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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' },
|
||||
];
|
||||
|
||||
|
||||
59
maternal-web/components/ui/alert.tsx
Normal file
59
maternal-web/components/ui/alert.tsx
Normal 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 }
|
||||
36
maternal-web/components/ui/badge.tsx
Normal file
36
maternal-web/components/ui/badge.tsx
Normal 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 }
|
||||
56
maternal-web/components/ui/button.tsx
Normal file
56
maternal-web/components/ui/button.tsx
Normal 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 }
|
||||
79
maternal-web/components/ui/card.tsx
Normal file
79
maternal-web/components/ui/card.tsx
Normal 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 }
|
||||
28
maternal-web/components/ui/progress.tsx
Normal file
28
maternal-web/components/ui/progress.tsx
Normal 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 }
|
||||
55
maternal-web/components/ui/tabs.tsx
Normal file
55
maternal-web/components/ui/tabs.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user