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:
parent
6ffa2dd875
commit
967e00c4fa
3 changed files with 285 additions and 0 deletions
|
|
@ -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 ? (
|
||||
|
|
|
|||
112
src/app/api/history/route.ts
Normal file
112
src/app/api/history/route.ts
Normal 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
87
src/lib/guidelines.ts
Normal 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));
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue