diff --git a/src/app/activity/page.tsx b/src/app/activity/page.tsx index 19f0212..c77799b 100644 --- a/src/app/activity/page.tsx +++ b/src/app/activity/page.tsx @@ -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([]); const [loading, setLoading] = useState(true); const [filter, setFilter] = useState("all"); + const [child, setChild] = useState(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() {

Activity

+ {child && logs.length === 0 && ( + + )}
{/* View Toggle */}
@@ -106,6 +158,40 @@ export default function ActivityPage() { ))}
+ {/* Guidelines Card */} + {child && showSuggested && ( +
+ {(() => { + const guide = getGuideline(child.birthDate); + const ageMonths = getAgeInMonths(child.birthDate); + return ( +
+
+
+ {child.name} · {ageMonths} months old +
+ +
+
+
+
{guide.feeds.times}
+
feeds/day
+
+
+
{guide.sleep.totalHours}h
+
sleep/day
+
+
+
{guide.diapers.count}
+
diapers/day
+
+
+
+ ); + })()} +
+ )} + {/* Content */}
{loading ? ( diff --git a/src/app/api/history/route.ts b/src/app/api/history/route.ts new file mode 100644 index 0000000..0d591cc --- /dev/null +++ b/src/app/api/history/route.ts @@ -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 { + 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 }); + } +} \ No newline at end of file diff --git a/src/lib/guidelines.ts b/src/lib/guidelines.ts new file mode 100644 index 0000000..5a24490 --- /dev/null +++ b/src/lib/guidelines.ts @@ -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)); +} \ No newline at end of file