Files
maternal-app/maternal-web/app/analytics/page.tsx
Andrei a6891e9a53
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled
feat: AI Personalization Engine & Weekly/Monthly Reports Complete
**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>
2025-10-03 21:58:45 +00:00

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>
);
}