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:
andupetcu
2025-09-30 21:38:45 +03:00
parent 37227369d3
commit b6ed413e0c
17 changed files with 7351 additions and 3 deletions

View File

@@ -0,0 +1,330 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import {
Box,
TextField,
IconButton,
Paper,
Typography,
Avatar,
CircularProgress,
Chip,
} from '@mui/material';
import { Send, SmartToy, Person, AutoAwesome } from '@mui/icons-material';
import { motion, AnimatePresence } from 'framer-motion';
import { useAuth } from '@/lib/auth/AuthContext';
import apiClient from '@/lib/api/client';
interface Message {
id: string;
role: 'user' | 'assistant';
content: string;
timestamp: Date;
}
const suggestedQuestions = [
'How much should my baby sleep at 3 months?',
'What are normal feeding patterns?',
'When should I introduce solid foods?',
'Tips for better sleep routine',
];
export default function AIAssistantPage() {
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { user } = useAuth();
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [messages]);
const handleSend = async (message?: string) => {
const messageText = message || input.trim();
if (!messageText || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: 'user',
content: messageText,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInput('');
setIsLoading(true);
try {
const response = await apiClient.post('/api/v1/ai/chat', {
message: messageText,
conversationId: null, // Will be managed by backend
});
const assistantMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: response.data.data.response,
timestamp: new Date(),
};
setMessages((prev) => [...prev, assistantMessage]);
} catch (error) {
console.error('AI chat error:', error);
const errorMessage: Message = {
id: (Date.now() + 1).toString(),
role: 'assistant',
content: 'Sorry, I encountered an error. Please try again.',
timestamp: new Date(),
};
setMessages((prev) => [...prev, errorMessage]);
} finally {
setIsLoading(false);
}
};
const handleSuggestedQuestion = (question: string) => {
handleSend(question);
};
return (
<Box
sx={{
height: 'calc(100vh - 64px)',
display: 'flex',
flexDirection: 'column',
background: 'linear-gradient(135deg, #FFF5F5 0%, #FFE4E1 100%)',
}}
>
{/* Header */}
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 0,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
borderBottom: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
<SmartToy />
</Avatar>
<Box>
<Typography variant="h6" fontWeight="600">
AI Parenting Assistant
</Typography>
<Typography variant="caption" color="text.secondary">
Ask me anything about parenting and childcare
</Typography>
</Box>
</Box>
</Paper>
{/* Messages Container */}
<Box
sx={{
flex: 1,
overflowY: 'auto',
p: 2,
display: 'flex',
flexDirection: 'column',
gap: 2,
}}
>
{messages.length === 0 && (
<Box
sx={{
flex: 1,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 3,
}}
>
<AutoAwesome sx={{ fontSize: 64, color: 'primary.main', opacity: 0.5 }} />
<Typography variant="h6" color="text.secondary" textAlign="center">
Hi {user?.name}! How can I help you today?
</Typography>
<Box
sx={{
display: 'flex',
flexWrap: 'wrap',
gap: 1,
justifyContent: 'center',
maxWidth: 600,
}}
>
{suggestedQuestions.map((question, index) => (
<Chip
key={index}
label={question}
onClick={() => handleSuggestedQuestion(question)}
sx={{
borderRadius: 3,
'&:hover': {
bgcolor: 'primary.light',
color: 'white',
},
}}
/>
))}
</Box>
</Box>
)}
<AnimatePresence>
{messages.map((message) => (
<motion.div
key={message.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.3 }}
>
<Box
sx={{
display: 'flex',
gap: 2,
justifyContent: message.role === 'user' ? 'flex-end' : 'flex-start',
}}
>
{message.role === 'assistant' && (
<Avatar sx={{ bgcolor: 'primary.main', mt: 1 }}>
<SmartToy />
</Avatar>
)}
<Paper
elevation={0}
sx={{
p: 2,
maxWidth: '70%',
borderRadius: 3,
bgcolor:
message.role === 'user'
? 'primary.main'
: 'rgba(255, 255, 255, 0.95)',
color: message.role === 'user' ? 'white' : 'text.primary',
backdropFilter: 'blur(10px)',
}}
>
<Typography variant="body1" sx={{ whiteSpace: 'pre-wrap' }}>
{message.content}
</Typography>
<Typography
variant="caption"
sx={{
mt: 1,
display: 'block',
opacity: 0.7,
}}
>
{message.timestamp.toLocaleTimeString()}
</Typography>
</Paper>
{message.role === 'user' && (
<Avatar sx={{ bgcolor: 'secondary.main', mt: 1 }}>
<Person />
</Avatar>
)}
</Box>
</motion.div>
))}
</AnimatePresence>
{isLoading && (
<Box sx={{ display: 'flex', gap: 2 }}>
<Avatar sx={{ bgcolor: 'primary.main' }}>
<SmartToy />
</Avatar>
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 3,
bgcolor: 'rgba(255, 255, 255, 0.95)',
display: 'flex',
alignItems: 'center',
gap: 1,
}}
>
<CircularProgress size={20} />
<Typography variant="body2" color="text.secondary">
Thinking...
</Typography>
</Paper>
</Box>
)}
<div ref={messagesEndRef} />
</Box>
{/* Input Area */}
<Paper
elevation={0}
sx={{
p: 2,
borderRadius: 0,
background: 'rgba(255, 255, 255, 0.95)',
backdropFilter: 'blur(10px)',
borderTop: '1px solid',
borderColor: 'divider',
}}
>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'flex-end' }}>
<TextField
fullWidth
multiline
maxRows={4}
placeholder="Ask me anything..."
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
}}
disabled={isLoading}
sx={{
'& .MuiOutlinedInput-root': {
borderRadius: 3,
},
}}
/>
<IconButton
color="primary"
onClick={() => handleSend()}
disabled={!input.trim() || isLoading}
sx={{
width: 48,
height: 48,
bgcolor: 'primary.main',
color: 'white',
'&:hover': {
bgcolor: 'primary.dark',
},
'&:disabled': {
bgcolor: 'action.disabledBackground',
},
}}
>
<Send />
</IconButton>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1, display: 'block' }}>
This AI assistant provides general information. Always consult healthcare professionals
for medical advice.
</Typography>
</Paper>
</Box>
);
}

View File

@@ -0,0 +1,263 @@
'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Paper,
Grid,
Card,
CardContent,
Button,
CircularProgress,
Tabs,
Tab,
Alert,
} from '@mui/material';
import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import {
TrendingUp,
Hotel,
Restaurant,
BabyChangingStation,
Download,
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import apiClient from '@/lib/api/client';
import WeeklySleepChart from '@/components/analytics/WeeklySleepChart';
import FeedingFrequencyGraph from '@/components/analytics/FeedingFrequencyGraph';
import GrowthCurve from '@/components/analytics/GrowthCurve';
import PatternInsights from '@/components/analytics/PatternInsights';
interface TabPanelProps {
children?: React.ReactNode;
index: number;
value: number;
}
function TabPanel(props: TabPanelProps) {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`analytics-tabpanel-${index}`}
aria-labelledby={`analytics-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
</div>
);
}
export default function AnalyticsPage() {
const [tabValue, setTabValue] = useState(0);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [insights, setInsights] = useState<any>(null);
useEffect(() => {
fetchAnalytics();
}, []);
const fetchAnalytics = async () => {
try {
setIsLoading(true);
const response = await apiClient.get('/api/v1/analytics/insights');
setInsights(response.data.data);
} catch (err: any) {
console.error('Failed to fetch analytics:', err);
setError(err.response?.data?.message || 'Failed to load analytics');
} finally {
setIsLoading(false);
}
};
const handleExportReport = async () => {
try {
const response = await apiClient.get('/api/v1/analytics/reports/weekly', {
responseType: 'blob',
});
const blob = new Blob([response.data], { type: 'application/pdf' });
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `weekly-report-${new Date().toISOString().split('T')[0]}.pdf`;
link.click();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error('Failed to export report:', err);
}
};
const handleTabChange = (event: React.SyntheticEvent, newValue: number) => {
setTabValue(newValue);
};
if (isLoading) {
return (
<ProtectedRoute>
<AppShell>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '60vh',
}}
>
<CircularProgress />
</Box>
</AppShell>
</ProtectedRoute>
);
}
return (
<ProtectedRoute>
<AppShell>
<Box>
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
{/* Header */}
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
mb: 3,
}}
>
<Box>
<Typography variant="h4" gutterBottom fontWeight="600">
Analytics & Insights 📊
</Typography>
<Typography variant="body1" color="text.secondary">
Track patterns and get personalized insights
</Typography>
</Box>
<Button
variant="contained"
startIcon={<Download />}
onClick={handleExportReport}
sx={{ borderRadius: 3 }}
>
Export Report
</Button>
</Box>
{error && (
<Alert severity="error" sx={{ mb: 3, borderRadius: 2 }}>
{error}
</Alert>
)}
{/* Summary Cards */}
<Grid container spacing={2} sx={{ mb: 4 }}>
<Grid item xs={12} sm={4}>
<Card
sx={{
background: 'linear-gradient(135deg, #B6D7FF 0%, #A5C9FF 100%)',
color: 'white',
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Hotel sx={{ fontSize: 32 }} />
<Typography variant="h5" fontWeight="600">
{insights?.sleep?.averageHours || '0'}h
</Typography>
</Box>
<Typography variant="body2">Avg Sleep (7 days)</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card
sx={{
background: 'linear-gradient(135deg, #FFB6C1 0%, #FFA5B0 100%)',
color: 'white',
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<Restaurant sx={{ fontSize: 32 }} />
<Typography variant="h5" fontWeight="600">
{insights?.feeding?.averagePerDay || '0'}
</Typography>
</Box>
<Typography variant="body2">Avg Feedings (7 days)</Typography>
</CardContent>
</Card>
</Grid>
<Grid item xs={12} sm={4}>
<Card
sx={{
background: 'linear-gradient(135deg, #FFE4B5 0%, #FFD9A0 100%)',
color: 'white',
}}
>
<CardContent>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 1 }}>
<BabyChangingStation sx={{ fontSize: 32 }} />
<Typography variant="h5" fontWeight="600">
{insights?.diaper?.averagePerDay || '0'}
</Typography>
</Box>
<Typography variant="body2">Avg Diapers (7 days)</Typography>
</CardContent>
</Card>
</Grid>
</Grid>
{/* Tabs */}
<Paper sx={{ borderRadius: 3, overflow: 'hidden' }}>
<Box sx={{ borderBottom: 1, borderColor: 'divider' }}>
<Tabs
value={tabValue}
onChange={handleTabChange}
aria-label="analytics tabs"
sx={{ px: 2 }}
>
<Tab label="Sleep Patterns" />
<Tab label="Feeding Patterns" />
<Tab label="Growth Curve" />
<Tab label="Insights" />
</Tabs>
</Box>
<TabPanel value={tabValue} index={0}>
<Box sx={{ p: 3 }}>
<WeeklySleepChart />
</Box>
</TabPanel>
<TabPanel value={tabValue} index={1}>
<Box sx={{ p: 3 }}>
<FeedingFrequencyGraph />
</Box>
</TabPanel>
<TabPanel value={tabValue} index={2}>
<Box sx={{ p: 3 }}>
<GrowthCurve />
</Box>
</TabPanel>
<TabPanel value={tabValue} index={3}>
<Box sx={{ p: 3 }}>
<PatternInsights insights={insights} />
</Box>
</TabPanel>
</Paper>
</motion.div>
</Box>
</AppShell>
</ProtectedRoute>
);
}

View File

@@ -8,6 +8,8 @@ import {
Hotel,
BabyChangingStation,
Insights,
SmartToy,
Analytics,
} from '@mui/icons-material';
import { motion } from 'framer-motion';
import { useAuth } from '@/lib/auth/AuthContext';
@@ -20,7 +22,8 @@ export default function HomePage() {
{ icon: <Restaurant />, label: 'Feeding', color: '#FFB6C1', path: '/track/feeding' },
{ icon: <Hotel />, label: 'Sleep', color: '#B6D7FF', path: '/track/sleep' },
{ icon: <BabyChangingStation />, label: 'Diaper', color: '#FFE4B5', path: '/track/diaper' },
{ icon: <Insights />, label: 'Insights', color: '#E6E6FA', path: '/insights' },
{ icon: <SmartToy />, label: 'AI Assistant', color: '#FFD3B6', path: '/ai-assistant' },
{ icon: <Analytics />, label: 'Analytics', color: '#D4B5FF', path: '/analytics' },
];
return (
@@ -45,7 +48,7 @@ export default function HomePage() {
</Typography>
<Grid container spacing={2} sx={{ mb: 4 }}>
{quickActions.map((action, index) => (
<Grid item xs={6} sm={3} key={action.label}>
<Grid item xs={6} sm={2.4} key={action.label}>
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}