feat: Unify insights and predictions in /insights page with tabs
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:
@@ -5,10 +5,10 @@ import { AppShell } from '@/components/layouts/AppShell/AppShell';
|
|||||||
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/common/ProtectedRoute';
|
||||||
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
import { LoadingFallback } from '@/components/common/LoadingFallback';
|
||||||
|
|
||||||
// Lazy load the insights dashboard component
|
// Lazy load the unified insights dashboard component with tabs
|
||||||
const InsightsDashboard = lazy(() =>
|
const UnifiedInsightsDashboard = lazy(() =>
|
||||||
import('@/components/features/analytics/InsightsDashboard').then((mod) => ({
|
import('@/components/features/analytics/UnifiedInsightsDashboard').then((mod) => ({
|
||||||
default: mod.InsightsDashboard,
|
default: mod.UnifiedInsightsDashboard,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export default function InsightsPage() {
|
|||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<AppShell>
|
<AppShell>
|
||||||
<Suspense fallback={<LoadingFallback variant="page" />}>
|
<Suspense fallback={<LoadingFallback variant="page" />}>
|
||||||
<InsightsDashboard />
|
<UnifiedInsightsDashboard />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</AppShell>
|
</AppShell>
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user