Add smart onboarding to Activity page

- Pediatric guidelines data with age-based schedules
- Show child's age and benchmarks on Activity page
- AI history generation via /api/history
- Generate button to auto-populate past logs from birth

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-10 16:12:23 +05:30
parent 6ffa2dd875
commit 967e00c4fa
3 changed files with 285 additions and 0 deletions

View file

@ -15,6 +15,14 @@ interface Log {
loggedAt: string;
}
interface Child {
id: string;
name: string;
birthDate: string;
}
import { getGuideline, getAgeInMonths, guidelines } from "@/lib/guidelines";
interface DayLogs {
date: string;
logs: Log[];
@ -25,12 +33,28 @@ export default function ActivityPage() {
const [logs, setLogs] = useState<Log[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<LogType | "all">("all");
const [child, setChild] = useState<Child | null>(null);
const [showSuggested, setShowSuggested] = useState(true);
const [generating, setGenerating] = useState(false);
const childId = "default";
useEffect(() => {
fetchLogs();
fetchChild();
}, []);
const fetchChild = async () => {
try {
const res = await fetch("/api/children?familyId=default");
const data = await res.json();
if (data.children?.length > 0) {
setChild(data.children[0]);
}
} catch (err) {
console.error("Failed to fetch:", err);
}
};
const fetchLogs = async () => {
try {
const res = await fetch(`/api/logs?childId=${childId}&limit=100`);
@ -42,6 +66,25 @@ export default function ActivityPage() {
setLoading(false);
};
const generateHistory = async () => {
if (!child) return;
setGenerating(true);
try {
const res = await fetch("/api/history", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ childId: child.id, birthDate: child.birthDate }),
});
const data = await res.json();
if (data.success) {
fetchLogs();
}
} catch (err) {
console.error("Failed to generate:", err);
}
setGenerating(false);
};
const filteredLogs = filter === "all" ? logs : logs.filter((l) => l.type === filter);
const groupedByDay = filteredLogs.reduce((acc: DayLogs[], log) => {
@ -71,6 +114,15 @@ export default function ActivityPage() {
<div className="flex items-center gap-4">
<Link href="/menu" className="p-2"></Link>
<h1 className="text-xl font-bold">Activity</h1>
{child && logs.length === 0 && (
<button
onClick={generateHistory}
disabled={generating}
className="text-xs px-2 py-1 bg-rose-400 text-white rounded-full"
>
{generating ? "..." : "Generate History"}
</button>
)}
</div>
{/* View Toggle */}
<div className="flex bg-white dark:bg-gray-800 rounded-lg p-1">
@ -106,6 +158,40 @@ export default function ActivityPage() {
))}
</div>
{/* Guidelines Card */}
{child && showSuggested && (
<div className="px-4 mb-4">
{(() => {
const guide = getGuideline(child.birthDate);
const ageMonths = getAgeInMonths(child.birthDate);
return (
<div className="p-4 bg-gradient-to-r from-rose-100 to-amber-100 dark:from-rose-900 dark:to-amber-900 rounded-xl">
<div className="flex items-center justify-between mb-2">
<div className="font-medium text-rose-800 dark:text-rose-200">
{child.name} · {ageMonths} months old
</div>
<button onClick={() => setShowSuggested(false)} className="text-gray-400"></button>
</div>
<div className="grid grid-cols-3 gap-2 text-sm">
<div className="text-center">
<div className="text-lg font-bold text-rose-600 dark:text-rose-400">{guide.feeds.times}</div>
<div className="text-xs text-gray-600 dark:text-gray-400">feeds/day</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-amber-600 dark:text-amber-400">{guide.sleep.totalHours}h</div>
<div className="text-xs text-gray-600 dark:text-gray-400">sleep/day</div>
</div>
<div className="text-center">
<div className="text-lg font-bold text-blue-600 dark:text-blue-400">{guide.diapers.count}</div>
<div className="text-xs text-gray-600 dark:text-gray-400">diapers/day</div>
</div>
</div>
</div>
);
})()}
</div>
)}
{/* Content */}
<div className="px-4 pb-20">
{loading ? (

View file

@ -0,0 +1,112 @@
import { NextResponse } from "next/server";
import { sql } from "@/db";
import { getAgeInMonths, guidelines } from "@/lib/guidelines";
const LITELLM_URL = process.env.LITELLM_BASE_URL || "https://llm.manohargupta.com";
const LITELLM_KEY = process.env.LITELLM_API_KEY;
// Generate plausible daily logs for a child based on age
async function generateHistory(childId: string, birthDate: string): Promise<string> {
const ageMonths = getAgeInMonths(birthDate);
const guide = guidelines.find((g) => ageMonths >= g.minAgeMonths && ageMonths < g.maxAgeMonths)
|| guidelines[guidelines.length - 1];
const daysBack = Math.min(ageMonths * 30, 365); // Cap at 1 year
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
// Build prompt for AI to generate log entries
const prompt = `Generate ${daysBack} days of baby logs. For each day, create 3-5 entries with timestamps.
Child age: ${ageMonths} months
Typical routine: ${guide.feeds.times} feeds/day, ${guide.sleep.totalHours}h sleep, ${guide.diapers.count} diaper changes/day
Return as JSON array with objects containing:
- type: "feed", "sleep", or "diaper"
- subType: "breast_left", "breast_right", "bottle", "nap", "night_sleep", "wet", "dirty", "both"
- loggedAt: ISO timestamp
- amountMl: number (for bottles)
- notes: optional string
Make times realistic - feeds every 3-4 hours during day, sleep at night, diapers after feeds and naps.
Just return the JSON, no explanation.`;
try {
const response = await fetch(`${LITELLM_URL}/v1/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${LITELLM_KEY}`,
},
body: JSON.stringify({
model: "minimax-2.7",
messages: [{ role: "user", content: prompt }],
temperature: 0.7,
}),
});
if (!response.ok) {
throw new Error(`LiteLLM error: ${response.status}`);
}
const data = await response.json();
const content = data.choices?.[0]?.message?.content;
return content || "[]";
} catch (error) {
console.error("AI generation failed:", error);
return "[]";
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { childId, birthDate } = body;
if (!childId || !birthDate) {
return NextResponse.json({ error: "childId and birthDate required" }, { status: 400 });
}
const content = await generateHistory(childId, birthDate);
// Parse and insert logs
let logs: any[] = [];
try {
logs = JSON.parse(content);
} catch {
logs = [];
}
let inserted = 0;
for (const log of logs) {
try {
if (log.type === "feed") {
await sql.unsafe(
`INSERT INTO feeds (child_id, type, method, amount_ml, notes, logged_at) VALUES ($1, $2, $3, $4, $5, $6)`,
[childId, log.subType || "bottle", log.subType?.includes("breast") ? "breast_both" : "bottle", log.amountMl || null, log.notes || null, log.loggedAt]
);
inserted++;
} else if (log.type === "diaper") {
await sql.unsafe(
`INSERT INTO diapers_logs (child_id, type, notes, logged_at) VALUES ($1, $2, $3, $4)`,
[childId, log.subType || "wet", log.notes || null, log.loggedAt]
);
inserted++;
} else if (log.type === "sleep") {
await sql.unsafe(
`INSERT INTO sleeps (child_id, type, started_at, notes, logged_at) VALUES ($1, $2, $3, $4, $5)`,
[childId, log.subType || "nap", log.loggedAt, log.notes || null, log.loggedAt]
);
inserted++;
}
} catch (e) {
// Skip individual insert errors
}
}
return NextResponse.json({ success: true, generated: inserted });
} catch (error) {
console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 });
}
}

87
src/lib/guidelines.ts Normal file
View file

@ -0,0 +1,87 @@
// Pediatric guidelines for baby routines by age
// Based on AAP recommendations
export interface Guideline {
minAgeMonths: number;
maxAgeMonths: number;
label: string;
feeds: { times: number; notes: string };
sleep: { totalHours: number; notes: string };
diapers: { count: number; notes: string };
}
export const guidelines: Guideline[] = [
{
minAgeMonths: 0,
maxAgeMonths: 1,
label: "Newborn (0-1 mo)",
feeds: { times: 8, notes: "On demand, every 2-3 hours" },
sleep: { totalHours: 16, notes: "Short naps throughout day" },
diapers: { count: 8, notes: "Wet and dirty" },
},
{
minAgeMonths: 1,
maxAgeMonths: 3,
label: "Infant (1-3 mo)",
feeds: { times: 7, notes: "Every 3-4 hours" },
sleep: { totalHours: 15, notes: "3-4 naps/day" },
diapers: { count: 6, notes: "Dirty diapers reduce" },
},
{
minAgeMonths: 3,
maxAgeMonths: 6,
label: "Older Infant (3-6 mo)",
feeds: { times: 6, notes: "Ready for solids around 6 mo" },
sleep: { totalHours: 14, notes: "2-3 naps/day" },
diapers: { count: 4, notes: "May start solid foods" },
},
{
minAgeMonths: 6,
maxAgeMonths: 9,
label: "Baby (6-9 mo)",
feeds: { times: 4, notes: "3 meals + 2 snacks" },
sleep: { totalHours: 13, notes: "2 naps/day" },
diapers: { count: 4, notes: "Solid foods started" },
},
{
minAgeMonths: 9,
maxAgeMonths: 12,
label: "Toddler (9-12 mo)",
feeds: { times: 3, notes: "3 meals + 2 snacks" },
sleep: { totalHours: 12, notes: "1-2 naps/day" },
diapers: { count: 3, notes: "May start potty training" },
},
{
minAgeMonths: 12,
maxAgeMonths: 18,
label: "Toddler (12-18 mo)",
feeds: { times: 3, notes: "Family meals" },
sleep: { totalHours: 11, notes: "1 nap/day" },
diapers: { count: 3, notes: "Potty training begins" },
},
{
minAgeMonths: 18,
maxAgeMonths: 24,
label: "Toddler (18-24 mo)",
feeds: { times: 3, notes: "Family meals" },
sleep: { totalHours: 11, notes: "1 nap/day" },
diapers: { count: 2, notes: "Potty training" },
},
];
export function getGuideline(birthDate: string): Guideline {
const birth = new Date(birthDate);
const now = new Date();
const months = (now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24 * 30);
return (
guidelines.find((g) => months >= g.minAgeMonths && months < g.maxAgeMonths) ||
guidelines[guidelines.length - 1]
);
}
export function getAgeInMonths(birthDate: string): number {
const birth = new Date(birthDate);
const now = new Date();
return Math.floor((now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24 * 30));
}