**AI Personalization Engine (Backend):** 1. **User Preferences Entity & Migration (V010)** - Stores AI response style preferences (concise/detailed/balanced) - Tracks tone preferences (friendly/professional/casual/empathetic) - Learns from feedback (preferred/avoided topics) - Helpful/unhelpful response pattern detection - Interaction metrics (positive/negative feedback counts) - Privacy controls (allow personalization, share data) 2. **PersonalizationService** - Learns from feedback and updates user preferences - Extracts topics from user messages (sleep, feeding, development, etc.) - Updates topic weights based on feedback (+/-0.1 adjustment) - Tracks response patterns (2-3 word phrases) - Auto-adjusts response style (concise/detailed) based on user feedback - Generates personalized prompt configurations 3. **Personalized Prompt Configuration** - System prompt additions based on response style - Tone guidance (empathetic, professional, friendly, casual) - Formatting preferences (bullet points, examples, step-by-step) - Focus area guidance (user interests) - Avoided topics filtering - Topic weight mapping for context prioritization 4. **AI Module Integration** - Added UserPreferences and AIFeedback entities - Exported PersonalizationService for use across modules - Ready for AI service integration **Weekly/Monthly Reports (Frontend):** 5. **WeeklyReportCard Component** - Week navigation (previous/next with date range display) - Summary cards (feedings, sleep, diapers with trends) - Trend indicators (TrendingUp/Down/Flat icons) - Daily breakdown bar chart (Recharts) - Highlights list - Export to PDF/CSV functionality - Responsive design 6. **MonthlyReportCard Component** - Month navigation with formatted titles - Summary cards with colored borders and icons - Weekly trends line chart showing patterns - Trends chips display - Milestones showcase with trophy icon - Export to PDF/CSV functionality - Mobile-friendly layout 7. **Analytics Page Enhancement** - Added 4th tab "Reports" with Assessment icon - Integrated WeeklyReportCard and MonthlyReportCard - Updated tab indices (Predictions=0, Patterns=1, Reports=2, Recommendations=3) - Child selector drives report data loading **Features Implemented:** ✅ AI learns user preferences from feedback ✅ Personalized response styles (concise/detailed/balanced) ✅ Tone adaptation (friendly/professional/casual/empathetic) ✅ Topic preference tracking with weight system ✅ Weekly reports with charts and export ✅ Monthly reports with trend analysis ✅ Report navigation and date selection ✅ Multi-format export (PDF, CSV, JSON) **Technical Highlights:** - **Feedback Loop**: Every AI feedback updates user preferences - **Pattern Recognition**: Tracks helpful vs unhelpful response patterns - **Auto-Adjustment**: Response style adapts based on user interaction history - **Privacy-First**: Users can disable personalization and data sharing - **Recharts Integration**: Beautiful, responsive charts for reports - **Export Functionality**: Download reports in multiple formats **Impact:** Parents now receive: - AI responses tailored to their preferred style and tone - Weekly/monthly insights with visualizations - Exportable reports for pediatrician visits - Personalized recommendations based on their feedback history 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
491 lines
18 KiB
TypeScript
491 lines
18 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Grid,
|
|
Card,
|
|
CardContent,
|
|
Select,
|
|
MenuItem,
|
|
FormControl,
|
|
InputLabel,
|
|
CircularProgress,
|
|
Alert,
|
|
Tab,
|
|
Tabs,
|
|
Chip,
|
|
List,
|
|
ListItem,
|
|
ListItemIcon,
|
|
ListItemText,
|
|
} from '@mui/material';
|
|
import {
|
|
TrendingUp,
|
|
Restaurant,
|
|
Hotel,
|
|
BabyChangingStation,
|
|
Warning,
|
|
CheckCircle,
|
|
Timeline,
|
|
Assessment,
|
|
} from '@mui/icons-material';
|
|
import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
|
import { childrenApi, Child } from '@/lib/api/children';
|
|
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
|
|
import PredictionsCard from '@/components/features/analytics/PredictionsCard';
|
|
import GrowthSpurtAlert from '@/components/features/analytics/GrowthSpurtAlert';
|
|
import WeeklyReportCard from '@/components/features/analytics/WeeklyReportCard';
|
|
import MonthlyReportCard from '@/components/features/analytics/MonthlyReportCard';
|
|
|
|
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={{ py: 3 }}>{children}</Box>}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function AnalyticsPage() {
|
|
const [children, setChildren] = useState<Child[]>([]);
|
|
const [selectedChildId, setSelectedChildId] = useState<string>('');
|
|
const [tabValue, setTabValue] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [insights, setInsights] = useState<PatternInsights | null>(null);
|
|
const [predictions, setPredictions] = useState<PredictionInsights | null>(null);
|
|
const [insightsLoading, setInsightsLoading] = useState(false);
|
|
const [predictionsLoading, setPredictionsLoading] = useState(false);
|
|
const [days, setDays] = useState<number>(7);
|
|
|
|
useEffect(() => {
|
|
loadChildren();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (selectedChildId) {
|
|
loadInsights();
|
|
loadPredictions();
|
|
}
|
|
}, [selectedChildId, days]);
|
|
|
|
const loadChildren = async () => {
|
|
try {
|
|
const data = await childrenApi.getChildren();
|
|
setChildren(data);
|
|
if (data.length > 0 && !selectedChildId) {
|
|
setSelectedChildId(data[0].id);
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load children:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadInsights = async () => {
|
|
if (!selectedChildId) return;
|
|
|
|
setInsightsLoading(true);
|
|
try {
|
|
const data = await analyticsApi.getInsights(selectedChildId, days);
|
|
setInsights(data);
|
|
} catch (error) {
|
|
console.error('Failed to load insights:', error);
|
|
} finally {
|
|
setInsightsLoading(false);
|
|
}
|
|
};
|
|
|
|
const loadPredictions = async () => {
|
|
if (!selectedChildId) return;
|
|
|
|
setPredictionsLoading(true);
|
|
try {
|
|
const data = await analyticsApi.getPredictions(selectedChildId);
|
|
setPredictions(data);
|
|
} catch (error) {
|
|
console.error('Failed to load predictions:', error);
|
|
} finally {
|
|
setPredictionsLoading(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
if (children.length === 0) {
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box sx={{ p: 3 }}>
|
|
<Alert severity="info">Add a child to your family to view analytics and predictions.</Alert>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ProtectedRoute>
|
|
<AppShell>
|
|
<Box sx={{ p: 3 }}>
|
|
{/* Header */}
|
|
<Box sx={{ mb: 3 }}>
|
|
<Typography variant="h4" gutterBottom>
|
|
Analytics & Predictions
|
|
</Typography>
|
|
<Typography variant="body2" color="text.secondary">
|
|
AI-powered insights and predictions for your child's patterns
|
|
</Typography>
|
|
</Box>
|
|
|
|
{/* Child Selector and Date Range */}
|
|
<Grid container spacing={2} sx={{ mb: 3 }}>
|
|
<Grid item xs={12} md={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Child</InputLabel>
|
|
<Select
|
|
value={selectedChildId}
|
|
label="Child"
|
|
onChange={(e) => setSelectedChildId(e.target.value)}
|
|
>
|
|
{children.map((child) => (
|
|
<MenuItem key={child.id} value={child.id}>
|
|
{child.name}
|
|
</MenuItem>
|
|
))}
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
<Grid item xs={12} md={6}>
|
|
<FormControl fullWidth>
|
|
<InputLabel>Date Range</InputLabel>
|
|
<Select value={days} label="Date Range" onChange={(e) => setDays(Number(e.target.value))}>
|
|
<MenuItem value={7}>Last 7 days</MenuItem>
|
|
<MenuItem value={14}>Last 14 days</MenuItem>
|
|
<MenuItem value={30}>Last 30 days</MenuItem>
|
|
</Select>
|
|
</FormControl>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{/* Growth Spurt Alert */}
|
|
{insights?.growthSpurt && <GrowthSpurtAlert growthSpurt={insights.growthSpurt} />}
|
|
|
|
{/* Tabs */}
|
|
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
|
|
<Tabs value={tabValue} onChange={(e, newValue) => setTabValue(newValue)}>
|
|
<Tab label="Predictions" icon={<TrendingUp />} iconPosition="start" />
|
|
<Tab label="Patterns" icon={<Timeline />} iconPosition="start" />
|
|
<Tab label="Reports" icon={<Assessment />} iconPosition="start" />
|
|
<Tab label="Recommendations" icon={<CheckCircle />} iconPosition="start" />
|
|
</Tabs>
|
|
</Box>
|
|
|
|
{/* Tab Panels */}
|
|
<TabPanel value={tabValue} index={0}>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12}>
|
|
<PredictionsCard predictions={predictions} loading={predictionsLoading} />
|
|
</Grid>
|
|
</Grid>
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={1}>
|
|
<Grid container spacing={3}>
|
|
{/* Sleep Patterns */}
|
|
{insights?.sleep && (
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Hotel sx={{ mr: 1, color: '#1976D2' }} />
|
|
<Typography variant="h6">Sleep Patterns</Typography>
|
|
<Chip
|
|
size="small"
|
|
label={insights.sleep.trend}
|
|
color={
|
|
insights.sleep.trend === 'improving'
|
|
? 'success'
|
|
: insights.sleep.trend === 'declining'
|
|
? 'error'
|
|
: 'default'
|
|
}
|
|
sx={{ ml: 'auto' }}
|
|
/>
|
|
</Box>
|
|
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Duration
|
|
</Typography>
|
|
<Typography variant="h6">{Math.round(insights.sleep.averageDuration)} min</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Night Wakings
|
|
</Typography>
|
|
<Typography variant="h6">{insights.sleep.nightWakings}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Bedtime
|
|
</Typography>
|
|
<Typography variant="h6">{insights.sleep.averageBedtime}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Wake Time
|
|
</Typography>
|
|
<Typography variant="h6">{insights.sleep.averageWakeTime}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Nap Count
|
|
</Typography>
|
|
<Typography variant="h6">{insights.sleep.napCount}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Consistency
|
|
</Typography>
|
|
<Typography variant="h6">{Math.round(insights.sleep.consistency * 100)}%</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Feeding Patterns */}
|
|
{insights?.feeding && (
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Restaurant sx={{ mr: 1, color: '#E91E63' }} />
|
|
<Typography variant="h6">Feeding Patterns</Typography>
|
|
<Chip
|
|
size="small"
|
|
label={insights.feeding.trend}
|
|
color={
|
|
insights.feeding.trend === 'increasing'
|
|
? 'success'
|
|
: insights.feeding.trend === 'decreasing'
|
|
? 'error'
|
|
: 'default'
|
|
}
|
|
sx={{ ml: 'auto' }}
|
|
/>
|
|
</Box>
|
|
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Total Feedings
|
|
</Typography>
|
|
<Typography variant="h6">{insights.feeding.totalFeedings}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Interval
|
|
</Typography>
|
|
<Typography variant="h6">{insights.feeding.averageInterval.toFixed(1)} hrs</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Duration
|
|
</Typography>
|
|
<Typography variant="h6">{Math.round(insights.feeding.averageDuration)} min</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Consistency
|
|
</Typography>
|
|
<Typography variant="h6">{Math.round(insights.feeding.consistency * 100)}%</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{Object.keys(insights.feeding.feedingMethod).length > 0 && (
|
|
<Box sx={{ mt: 2 }}>
|
|
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
Feeding Methods
|
|
</Typography>
|
|
{Object.entries(insights.feeding.feedingMethod).map(([method, count]) => (
|
|
<Chip
|
|
key={method}
|
|
label={`${method}: ${count}`}
|
|
size="small"
|
|
sx={{ mr: 0.5, mb: 0.5 }}
|
|
/>
|
|
))}
|
|
</Box>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Diaper Patterns */}
|
|
{insights?.diaper && (
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<BabyChangingStation sx={{ mr: 1, color: '#F57C00' }} />
|
|
<Typography variant="h6">Diaper Patterns</Typography>
|
|
<Chip
|
|
size="small"
|
|
label={insights.diaper.isHealthy ? 'Healthy' : 'Needs Attention'}
|
|
color={insights.diaper.isHealthy ? 'success' : 'warning'}
|
|
sx={{ ml: 'auto' }}
|
|
/>
|
|
</Box>
|
|
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Wet/Day
|
|
</Typography>
|
|
<Typography variant="h6">{insights.diaper.wetDiapersPerDay.toFixed(1)}</Typography>
|
|
</Grid>
|
|
<Grid item xs={6}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Dirty/Day
|
|
</Typography>
|
|
<Typography variant="h6">{insights.diaper.dirtyDiapersPerDay.toFixed(1)}</Typography>
|
|
</Grid>
|
|
<Grid item xs={12}>
|
|
<Typography variant="body2" color="text.secondary">
|
|
Avg Interval
|
|
</Typography>
|
|
<Typography variant="h6">{insights.diaper.averageInterval.toFixed(1)} hrs</Typography>
|
|
</Grid>
|
|
</Grid>
|
|
|
|
{insights.diaper.notes && (
|
|
<Alert severity="info" sx={{ mt: 2 }}>
|
|
{insights.diaper.notes}
|
|
</Alert>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
|
|
{insightsLoading && (
|
|
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
|
|
<CircularProgress />
|
|
</Box>
|
|
)}
|
|
|
|
{!insightsLoading && !insights && (
|
|
<Alert severity="info">Not enough data to analyze patterns yet. Track more activities!</Alert>
|
|
)}
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={2}>
|
|
<Grid container spacing={3}>
|
|
<Grid item xs={12}>
|
|
<WeeklyReportCard childId={selectedChildId} />
|
|
</Grid>
|
|
<Grid item xs={12}>
|
|
<MonthlyReportCard childId={selectedChildId} />
|
|
</Grid>
|
|
</Grid>
|
|
</TabPanel>
|
|
|
|
<TabPanel value={tabValue} index={3}>
|
|
<Grid container spacing={3}>
|
|
{/* Recommendations */}
|
|
{insights?.recommendations && insights.recommendations.length > 0 && (
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<CheckCircle sx={{ mr: 1, color: 'success.main' }} />
|
|
<Typography variant="h6">Recommendations</Typography>
|
|
</Box>
|
|
<List>
|
|
{insights.recommendations.map((rec, index) => (
|
|
<ListItem key={index}>
|
|
<ListItemIcon>
|
|
<CheckCircle color="success" />
|
|
</ListItemIcon>
|
|
<ListItemText primary={rec} />
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{/* Concerns */}
|
|
{insights?.concernsDetected && insights.concernsDetected.length > 0 && (
|
|
<Grid item xs={12} md={6}>
|
|
<Card>
|
|
<CardContent>
|
|
<Box sx={{ display: 'flex', alignItems: 'center', mb: 2 }}>
|
|
<Warning sx={{ mr: 1, color: 'warning.main' }} />
|
|
<Typography variant="h6">Concerns Detected</Typography>
|
|
</Box>
|
|
<List>
|
|
{insights.concernsDetected.map((concern, index) => (
|
|
<ListItem key={index}>
|
|
<ListItemIcon>
|
|
<Warning color="warning" />
|
|
</ListItemIcon>
|
|
<ListItemText primary={concern} />
|
|
</ListItem>
|
|
))}
|
|
</List>
|
|
<Alert severity="warning" sx={{ mt: 2 }}>
|
|
If you have concerns about your child's health, please consult with your pediatrician.
|
|
</Alert>
|
|
</CardContent>
|
|
</Card>
|
|
</Grid>
|
|
)}
|
|
|
|
{(!insights?.recommendations || insights.recommendations.length === 0) &&
|
|
(!insights?.concernsDetected || insights.concernsDetected.length === 0) && (
|
|
<Grid item xs={12}>
|
|
<Alert severity="info">No recommendations or concerns at this time. Keep tracking!</Alert>
|
|
</Grid>
|
|
)}
|
|
</Grid>
|
|
</TabPanel>
|
|
</Box>
|
|
</AppShell>
|
|
</ProtectedRoute>
|
|
);
|
|
}
|