feat: Unify insights and predictions in /insights page with tabs
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

Changes:
- Created UnifiedInsightsDashboard component with 2 tabs
- Tab 1: Insights - Shows existing charts, stats, and recent activities
- Tab 2: Predictions - Shows AI-powered predictions for next activities
- Growth Spurt Alert appears at the top when detected
- Child selector for families with multiple children
- Clean tab navigation with Timeline and TrendingUp icons

Features Now Accessible from /insights:
 Growth Spurt Detection (appears as alert banner)
 Pattern Analysis (feeding, sleep, diaper trends)
 AI Predictions (next feeding time, sleep duration, etc.)
 Charts and visualizations
 Recent activities timeline

User Experience:
- Single page access from bottom navigation (Insights icon)
- No need for separate /analytics page
- All smart AI features visible in one place
- Tab switching for different views

Files Changed:
- app/insights/page.tsx - Updated to use UnifiedInsightsDashboard
- components/features/analytics/UnifiedInsightsDashboard.tsx (new)
  * Manages state for both tabs
  * Loads insights and predictions data
  * Renders Growth Spurt Alert
  * Tab navigation UI

🎯 Result: Users can now easily see all AI insights and predictions
from the Insights menu item in bottom navigation!

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-04 12:01:44 +00:00
parent a0e0bbb002
commit 1be2b66372
3 changed files with 198 additions and 6 deletions

View File

@@ -5,10 +5,10 @@ import { AppShell } from '@/components/layouts/AppShell/AppShell';
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
import { LoadingFallback } from '@/components/common/LoadingFallback';
// Lazy load the insights dashboard component
const InsightsDashboard = lazy(() =>
import('@/components/features/analytics/InsightsDashboard').then((mod) => ({
default: mod.InsightsDashboard,
// Lazy load the unified insights dashboard component with tabs
const UnifiedInsightsDashboard = lazy(() =>
import('@/components/features/analytics/UnifiedInsightsDashboard').then((mod) => ({
default: mod.UnifiedInsightsDashboard,
}))
);
@@ -17,7 +17,7 @@ export default function InsightsPage() {
<ProtectedRoute>
<AppShell>
<Suspense fallback={<LoadingFallback variant="page" />}>
<InsightsDashboard />
<UnifiedInsightsDashboard />
</Suspense>
</AppShell>
</ProtectedRoute>

View File

@@ -0,0 +1,192 @@
'use client';
import { useState, useEffect } from 'react';
import {
Box,
Typography,
Tabs,
Tab,
Select,
MenuItem,
FormControl,
InputLabel,
CircularProgress,
Alert,
Grid,
} from '@mui/material';
import { Timeline, TrendingUp, Assessment } from '@mui/icons-material';
import { childrenApi, Child } from '@/lib/api/children';
import { analyticsApi, PatternInsights, PredictionInsights } from '@/lib/api/analytics';
import { InsightsDashboard } from './InsightsDashboard';
import PredictionsCard from './PredictionsCard';
import GrowthSpurtAlert from './GrowthSpurtAlert';
import { motion } from 'framer-motion';
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={`insights-tabpanel-${index}`}
aria-labelledby={`insights-tab-${index}`}
{...other}
>
{value === index && <Box sx={{ pt: 3 }}>{children}</Box>}
</div>
);
}
export function UnifiedInsightsDashboard() {
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 (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<CircularProgress />
</Box>
);
}
if (children.length === 0) {
return (
<Box sx={{ p: 3 }}>
<Alert severity="info">Add a child to your family to view insights and predictions.</Alert>
</Box>
);
}
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
{/* Header */}
<Box sx={{ mb: 3 }}>
<Typography variant="h4" gutterBottom fontWeight={600}>
Insights & Predictions
</Typography>
<Typography variant="body2" color="text.secondary">
AI-powered insights, patterns, and predictions for your child
</Typography>
</Box>
{/* Child Selector */}
{children.length > 1 && (
<Box sx={{ mb: 3 }}>
<FormControl sx={{ minWidth: 200 }}>
<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>
</Box>
)}
{/* 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="Insights" icon={<Timeline />} iconPosition="start" />
<Tab label="Predictions" icon={<TrendingUp />} iconPosition="start" />
</Tabs>
</Box>
{/* Tab Panels */}
<TabPanel value={tabValue} index={0}>
{/* Insights tab shows the existing InsightsDashboard */}
<InsightsDashboard />
</TabPanel>
<TabPanel value={tabValue} index={1}>
{/* Predictions tab */}
<Grid container spacing={3}>
<Grid item xs={12}>
<PredictionsCard predictions={predictions} loading={predictionsLoading} />
</Grid>
</Grid>
</TabPanel>
</Box>
</motion.div>
);
}

File diff suppressed because one or more lines are too long