Add Phase 4, 5 & 6: AI Assistant, Analytics & Testing
Phase 4: AI Assistant Integration - AI chat interface with suggested questions - Real-time messaging with backend OpenAI integration - Material UI chat bubbles and animations - Medical disclaimer and user-friendly UX Phase 5: Pattern Recognition & Analytics - Analytics dashboard with tabbed interface - Weekly sleep chart with bar/line visualizations - Feeding frequency graphs with type distribution - Growth curve with WHO percentiles (0-24 months) - Pattern insights with AI-powered recommendations - PDF report export functionality - Recharts integration for all data visualizations Phase 6: Testing & Optimization - Jest and React Testing Library setup - Unit tests for auth, API client, and components - Integration tests with full coverage - WCAG AA accessibility compliance testing - Performance optimizations (SWC, image optimization) - Accessibility monitoring with axe-core - 70% code coverage threshold 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
318
maternal-web/components/analytics/PatternInsights.tsx
Normal file
318
maternal-web/components/analytics/PatternInsights.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Typography,
|
||||
Paper,
|
||||
Grid,
|
||||
Chip,
|
||||
Alert,
|
||||
CircularProgress,
|
||||
LinearProgress,
|
||||
Card,
|
||||
CardContent,
|
||||
} from '@mui/material';
|
||||
import {
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Schedule,
|
||||
Lightbulb,
|
||||
Warning,
|
||||
CheckCircle,
|
||||
} from '@mui/icons-material';
|
||||
import { motion } from 'framer-motion';
|
||||
import apiClient from '@/lib/api/client';
|
||||
|
||||
interface Pattern {
|
||||
type: string;
|
||||
description: string;
|
||||
confidence: number;
|
||||
trend: 'up' | 'down' | 'stable';
|
||||
recommendations?: string[];
|
||||
}
|
||||
|
||||
interface Insight {
|
||||
category: string;
|
||||
title: string;
|
||||
description: string;
|
||||
severity: 'info' | 'warning' | 'success';
|
||||
patterns?: Pattern[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
insights?: any;
|
||||
}
|
||||
|
||||
export default function PatternInsights({ insights: propInsights }: Props) {
|
||||
const [insights, setInsights] = useState<Insight[]>([]);
|
||||
const [patterns, setPatterns] = useState<Pattern[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (propInsights) {
|
||||
processInsights(propInsights);
|
||||
setIsLoading(false);
|
||||
} else {
|
||||
fetchPatterns();
|
||||
}
|
||||
}, [propInsights]);
|
||||
|
||||
const fetchPatterns = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiClient.get('/api/v1/insights/patterns');
|
||||
const data = response.data.data;
|
||||
|
||||
processInsights(data);
|
||||
} catch (err: any) {
|
||||
console.error('Failed to fetch patterns:', err);
|
||||
setError(err.response?.data?.message || 'Failed to load insights');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const processInsights = (data: any) => {
|
||||
// Process sleep patterns
|
||||
const sleepInsights: Insight[] = [];
|
||||
if (data?.sleep) {
|
||||
const avgHours = data.sleep.averageHours || 0;
|
||||
if (avgHours < 10) {
|
||||
sleepInsights.push({
|
||||
category: 'Sleep',
|
||||
title: 'Low Sleep Duration',
|
||||
description: `Average sleep is ${avgHours}h/day. Recommended: 12-16h for infants, 10-13h for toddlers.`,
|
||||
severity: 'warning',
|
||||
});
|
||||
} else {
|
||||
sleepInsights.push({
|
||||
category: 'Sleep',
|
||||
title: 'Healthy Sleep Duration',
|
||||
description: `Great! Your child is averaging ${avgHours}h of sleep per day.`,
|
||||
severity: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
if (data.sleep.patterns) {
|
||||
sleepInsights[0].patterns = data.sleep.patterns.map((p: any) => ({
|
||||
type: p.type || 'sleep',
|
||||
description: p.description || 'Sleep pattern detected',
|
||||
confidence: p.confidence || 0.8,
|
||||
trend: p.trend || 'stable',
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Process feeding patterns
|
||||
const feedingInsights: Insight[] = [];
|
||||
if (data?.feeding) {
|
||||
const avgPerDay = data.feeding.averagePerDay || 0;
|
||||
feedingInsights.push({
|
||||
category: 'Feeding',
|
||||
title: 'Feeding Frequency',
|
||||
description: `Your child is feeding ${avgPerDay} times per day on average.`,
|
||||
severity: 'info',
|
||||
});
|
||||
|
||||
if (data.feeding.patterns) {
|
||||
feedingInsights[0].patterns = data.feeding.patterns.map((p: any) => ({
|
||||
type: p.type || 'feeding',
|
||||
description: p.description || 'Feeding pattern detected',
|
||||
confidence: p.confidence || 0.8,
|
||||
trend: p.trend || 'stable',
|
||||
recommendations: p.recommendations,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Process diaper patterns
|
||||
const diaperInsights: Insight[] = [];
|
||||
if (data?.diaper) {
|
||||
const avgPerDay = data.diaper.averagePerDay || 0;
|
||||
if (avgPerDay < 5) {
|
||||
diaperInsights.push({
|
||||
category: 'Diaper',
|
||||
title: 'Low Diaper Changes',
|
||||
description: `Average ${avgPerDay} diaper changes/day. Consider checking hydration if this continues.`,
|
||||
severity: 'warning',
|
||||
});
|
||||
} else {
|
||||
diaperInsights.push({
|
||||
category: 'Diaper',
|
||||
title: 'Normal Diaper Activity',
|
||||
description: `Averaging ${avgPerDay} diaper changes per day - within normal range.`,
|
||||
severity: 'success',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setInsights([...sleepInsights, ...feedingInsights, ...diaperInsights]);
|
||||
|
||||
// Extract all patterns
|
||||
const allPatterns: Pattern[] = [];
|
||||
[...sleepInsights, ...feedingInsights, ...diaperInsights].forEach((insight) => {
|
||||
if (insight.patterns) {
|
||||
allPatterns.push(...insight.patterns);
|
||||
}
|
||||
});
|
||||
setPatterns(allPatterns);
|
||||
};
|
||||
|
||||
const getSeverityIcon = (severity: string) => {
|
||||
switch (severity) {
|
||||
case 'warning':
|
||||
return <Warning sx={{ color: 'warning.main' }} />;
|
||||
case 'success':
|
||||
return <CheckCircle sx={{ color: 'success.main' }} />;
|
||||
default:
|
||||
return <Lightbulb sx={{ color: 'info.main' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getTrendIcon = (trend: string) => {
|
||||
switch (trend) {
|
||||
case 'up':
|
||||
return <TrendingUp sx={{ color: 'success.main' }} />;
|
||||
case 'down':
|
||||
return <TrendingDown sx={{ color: 'warning.main' }} />;
|
||||
default:
|
||||
return <Schedule sx={{ color: 'info.main' }} />;
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
||||
<CircularProgress />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<Alert severity="error" sx={{ borderRadius: 2 }}>
|
||||
{error}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="h6" gutterBottom fontWeight="600">
|
||||
Pattern Insights & Recommendations
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
AI-powered insights based on your child's activity patterns
|
||||
</Typography>
|
||||
|
||||
{/* Insights Cards */}
|
||||
<Grid container spacing={2} sx={{ mb: 4 }}>
|
||||
{insights.map((insight, index) => (
|
||||
<Grid item xs={12} key={index}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.3, delay: index * 0.1 }}
|
||||
>
|
||||
<Card
|
||||
sx={{
|
||||
borderLeft: 4,
|
||||
borderColor:
|
||||
insight.severity === 'warning'
|
||||
? 'warning.main'
|
||||
: insight.severity === 'success'
|
||||
? 'success.main'
|
||||
: 'info.main',
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'flex-start' }}>
|
||||
<Box sx={{ mt: 0.5 }}>{getSeverityIcon(insight.severity)}</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
|
||||
<Typography variant="h6" fontWeight="600">
|
||||
{insight.title}
|
||||
</Typography>
|
||||
<Chip label={insight.category} size="small" />
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
|
||||
{insight.description}
|
||||
</Typography>
|
||||
|
||||
{/* Pattern Details */}
|
||||
{insight.patterns && insight.patterns.length > 0 && (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
{insight.patterns.map((pattern, pIndex) => (
|
||||
<Paper
|
||||
key={pIndex}
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 2,
|
||||
mb: 1,
|
||||
bgcolor: 'background.default',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getTrendIcon(pattern.trend)}
|
||||
<Typography variant="body2" fontWeight="600">
|
||||
{pattern.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Chip
|
||||
label={`${Math.round(pattern.confidence * 100)}% confidence`}
|
||||
size="small"
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
/>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={pattern.confidence * 100}
|
||||
sx={{ mb: 1, borderRadius: 1 }}
|
||||
/>
|
||||
{pattern.recommendations && pattern.recommendations.length > 0 && (
|
||||
<Box sx={{ mt: 1 }}>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Recommendations:
|
||||
</Typography>
|
||||
<ul style={{ margin: '4px 0', paddingLeft: '20px' }}>
|
||||
{pattern.recommendations.map((rec, rIndex) => (
|
||||
<li key={rIndex}>
|
||||
<Typography variant="caption">{rec}</Typography>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
{/* No Insights Message */}
|
||||
{insights.length === 0 && (
|
||||
<Alert severity="info" sx={{ borderRadius: 2 }}>
|
||||
Keep tracking activities to see personalized insights and patterns!
|
||||
</Alert>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user