Add AI chat card with quick questions
This commit is contained in:
parent
a599116118
commit
8bac2a3a7d
1 changed files with 85 additions and 174 deletions
233
src/app/page.tsx
233
src/app/page.tsx
|
|
@ -32,32 +32,19 @@ export function getOfflineQueue(): OfflineEntry[] {
|
|||
|
||||
export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) {
|
||||
const queue = getOfflineQueue();
|
||||
queue.push({
|
||||
...entry,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
queue.push({ ...entry, id: crypto.randomUUID(), timestamp: Date.now() });
|
||||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(queue));
|
||||
}
|
||||
|
||||
export async function processOfflineQueue() {
|
||||
const queue = getOfflineQueue();
|
||||
if (queue.length === 0) return;
|
||||
|
||||
const failed: OfflineEntry[] = [];
|
||||
|
||||
for (const entry of queue) {
|
||||
try {
|
||||
await fetch("/api/logs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(entry.data),
|
||||
});
|
||||
} catch {
|
||||
failed.push(entry);
|
||||
await fetch("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(entry.data) });
|
||||
} catch { failed.push(entry); }
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
|
||||
}
|
||||
|
||||
|
|
@ -77,29 +64,12 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
|
||||
const handleSubmit = async () => {
|
||||
setLoading(true);
|
||||
const data = {
|
||||
type,
|
||||
childId,
|
||||
subType,
|
||||
amountMl: amountMl ? Number(amountMl) : undefined,
|
||||
notes: notes || undefined,
|
||||
};
|
||||
|
||||
const data = { type, childId, subType, amountMl: amountMl ? Number(amountMl) : undefined, notes: notes || undefined };
|
||||
try {
|
||||
const res = await fetch("/api/logs", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (!res.ok && !navigator.onLine) {
|
||||
addToOfflineQueue({ type: type as any, data });
|
||||
}
|
||||
const res = await fetch("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data) });
|
||||
if (!res.ok && !navigator.onLine) { addToOfflineQueue({ type: type as any, data }); }
|
||||
onClose();
|
||||
} catch {
|
||||
addToOfflineQueue({ type: type as any, data });
|
||||
onClose();
|
||||
}
|
||||
} catch { addToOfflineQueue({ type: type as any, data }); onClose(); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
|
|
@ -111,41 +81,29 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
|
|||
{type === "diaper" && "Log Diaper"}
|
||||
{type === "sleep" && "Log Sleep"}
|
||||
</h2>
|
||||
|
||||
{type === "feed" && (
|
||||
<>
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="breast_milk">Breast Milk</option>
|
||||
<option value="formula">Formula</option>
|
||||
<option value="solid">Solid Food</option>
|
||||
<option value="water">Water</option>
|
||||
<option value="breast_milk">Breast Milk</option><option value="formula">Formula</option>
|
||||
<option value="solid">Solid Food</option><option value="water">Water</option>
|
||||
</select>
|
||||
<input type="number" placeholder="Amount (ml)" value={amountMl} onChange={(e) => setAmountMl(e.target.value)} className="w-full p-3 border rounded-xl mb-3" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "diaper" && (
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="wet">Wet</option>
|
||||
<option value="dirty">Dirty</option>
|
||||
<option value="both">Both</option>
|
||||
<option value="wet">Wet</option><option value="dirty">Dirty</option><option value="both">Both</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
{type === "sleep" && (
|
||||
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3">
|
||||
<option value="nap">Nap</option>
|
||||
<option value="night">Night Sleep</option>
|
||||
<option value="nap">Nap</option><option value="night">Night Sleep</option>
|
||||
</select>
|
||||
)}
|
||||
|
||||
<input type="text" placeholder="Notes (optional)" value={notes} onChange={(e) => setNotes(e.target.value)} className="w-full p-3 border rounded-xl mb-4" />
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button onClick={onClose} className="flex-1 p-3 border rounded-xl">Cancel</button>
|
||||
<button onClick={handleSubmit} disabled={loading} className="flex-1 p-3 bg-rose-400 text-white rounded-xl">
|
||||
{loading ? "Saving..." : "Save"}
|
||||
</button>
|
||||
<button onClick={handleSubmit} disabled={loading} className="flex-1 p-3 bg-rose-400 text-white rounded-xl">{loading ? "Saving..." : "Save"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -158,7 +116,6 @@ function calculateAge(birthDate: string) {
|
|||
const days = Math.floor((now.getTime() - birth.getTime()) / (1000 * 60 * 60 * 24));
|
||||
const months = Math.floor(days / 30);
|
||||
const years = Math.floor(months / 12);
|
||||
|
||||
if (years > 0) return `${years} year${years > 1 ? "s" : ""} old`;
|
||||
if (months > 0) return `${months} month${months > 1 ? "s" : ""} old`;
|
||||
return `${days} day${days > 1 ? "s" : ""} old`;
|
||||
|
|
@ -171,6 +128,16 @@ function getGreeting() {
|
|||
return "Good evening";
|
||||
}
|
||||
|
||||
// Quick questions for mama
|
||||
const QUICK_QUESTIONS = [
|
||||
"How much should my baby eat?",
|
||||
"When should baby sleep?",
|
||||
"Is fever normal?",
|
||||
"How to increase milk supply?",
|
||||
"Baby won't sleep 😴",
|
||||
"Starting solids?",
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
|
||||
const [aiOpen, setAiOpen] = useState(false);
|
||||
|
|
@ -186,21 +153,15 @@ export default function HomePage() {
|
|||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem("tia_theme");
|
||||
if (saved === "dark") {
|
||||
setDarkMode(true);
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
if (saved === "dark") { setDarkMode(true); document.documentElement.classList.add("dark"); }
|
||||
}, []);
|
||||
|
||||
const toggleDarkMode = () => {
|
||||
const next = !darkMode;
|
||||
setDarkMode(next);
|
||||
localStorage.setItem("tia_theme", next ? "dark" : "light");
|
||||
if (next) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
if (next) { document.documentElement.classList.add("dark"); }
|
||||
else { document.documentElement.classList.remove("dark"); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -208,9 +169,7 @@ export default function HomePage() {
|
|||
setPendingCount(queue.length);
|
||||
const handleOnline = () => processOfflineQueue();
|
||||
window.addEventListener("online", handleOnline);
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/sw.js").catch(console.error);
|
||||
}
|
||||
if ("serviceWorker" in navigator) { navigator.serviceWorker.register("/sw.js").catch(console.error); }
|
||||
return () => window.removeEventListener("online", handleOnline);
|
||||
}, []);
|
||||
|
||||
|
|
@ -224,37 +183,24 @@ export default function HomePage() {
|
|||
}).catch(() => {});
|
||||
}, [childId]);
|
||||
|
||||
const handleAiChat = async () => {
|
||||
if (!aiInput.trim() || aiLoading) return;
|
||||
const handleAiChat = async (question?: string) => {
|
||||
const q = question || aiInput;
|
||||
if (!q.trim() || aiLoading) return;
|
||||
setAiLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/ai", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ messages: [{ role: "user", content: aiInput }] }),
|
||||
});
|
||||
const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: q }] }) });
|
||||
const data = await res.json();
|
||||
setAiReply(data.reply || "Sorry, I couldn't help with that.");
|
||||
} catch {
|
||||
setAiReply("Something went wrong. Try again.");
|
||||
}
|
||||
} catch { setAiReply("Something went wrong. Try again."); }
|
||||
setAiLoading(false);
|
||||
setAiInput("");
|
||||
if (!question) setAiInput("");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-rose-50 to-amber-50 dark:from-gray-900 dark:to-gray-800">
|
||||
<div className="p-4 flex justify-between items-center">
|
||||
<button className="p-2">
|
||||
<Link href="/menu">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</Link>
|
||||
</button>
|
||||
<button onClick={toggleDarkMode} className="p-2">
|
||||
{darkMode ? "☀️" : "🌙"}
|
||||
</button>
|
||||
<button className="p-2"><Link href="/menu"><svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /></svg></Link></button>
|
||||
<button onClick={toggleDarkMode} className="p-2">{darkMode ? "☀️" : "🌙"}</button>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pb-4">
|
||||
|
|
@ -265,105 +211,70 @@ export default function HomePage() {
|
|||
<div className="mx-4 mb-4 p-4 bg-white rounded-2xl shadow-md">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 bg-rose-100 rounded-full flex items-center justify-center text-2xl">👶</div>
|
||||
<div>
|
||||
<div className="text-lg font-semibold">{child.name}</div>
|
||||
<div className="text-gray-500">{calculateAge(child.birthDate)}</div>
|
||||
</div>
|
||||
<div><div className="text-lg font-semibold">{child.name}</div><div className="text-gray-500">{calculateAge(child.birthDate)}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{pendingCount > 0 && (
|
||||
<div className="mx-4 mb-4 bg-amber-100 text-amber-800 px-4 py-2 rounded-xl text-center">
|
||||
{pendingCount} pending log{pendingCount > 1 ? "s" : ""} (will sync when online)
|
||||
</div>
|
||||
)}
|
||||
{pendingCount > 0 && <div className="mx-4 mb-4 bg-amber-100 text-amber-800 px-4 py-2 rounded-xl text-center">{pendingCount} pending log{pendingCount > 1 ? "s" : ""}</div>}
|
||||
|
||||
{/* Quick Log - Keep as is */}
|
||||
<div className="px-4 mb-4">
|
||||
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">🍼</span>
|
||||
<span className="text-xs mt-1">Feed</span>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-4 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-3xl">🍼</span><span className="text-sm mt-1">Feed</span>
|
||||
</button>
|
||||
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">😴</span>
|
||||
<span className="text-xs mt-1">Sleep</span>
|
||||
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-4 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-3xl">😴</span><span className="text-sm mt-1">Sleep</span>
|
||||
</button>
|
||||
<button onClick={() => setModalType("diaper")} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">👶</span>
|
||||
<span className="text-xs mt-1">Diaper</span>
|
||||
</button>
|
||||
<button onClick={() => setAiOpen(true)} className="flex flex-col items-center p-3 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-2xl">🤖</span>
|
||||
<span className="text-xs mt-1">AI</span>
|
||||
<button onClick={() => setModalType("diaper")} className="flex flex-col items-center p-4 bg-white rounded-xl shadow-sm">
|
||||
<span className="text-3xl">👶</span><span className="text-sm mt-1">Diaper</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI Chat Card */}
|
||||
<div className="px-4">
|
||||
<h2 className="font-semibold mb-3 ml-1">🤖 Ask Tia AI</h2>
|
||||
<div className="p-4 bg-white rounded-2xl shadow-md">
|
||||
<p className="text-sm text-gray-600 mb-3">Quick questions:</p>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{QUICK_QUESTIONS.map((q, i) => (
|
||||
<button key={i} onClick={() => { setAiOpen(true); handleAiChat(q); }} className="px-3 py-1 bg-rose-50 text-rose-600 rounded-full text-sm">
|
||||
{q}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<input type="text" value={aiInput} onChange={(e) => setAiInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleAiChat()} placeholder="Ask anything..." className="flex-1 p-2 border rounded-xl text-sm" disabled={aiLoading} />
|
||||
<button onClick={() => { setAiOpen(true); handleAiChat(); }} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recent Activity */}
|
||||
<div className="px-4 mt-4">
|
||||
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
|
||||
<div className="space-y-2">
|
||||
{lastLogs.length === 0 ? (
|
||||
<p className="text-gray-400 text-sm">No logs yet today</p>
|
||||
) : (
|
||||
lastLogs.filter(Boolean).map((log: any, i: number) => (
|
||||
{lastLogs.length === 0 ? <p className="text-gray-400 text-sm">No logs yet today</p> : lastLogs.filter(Boolean).map((log: any, i: number) => (
|
||||
<div key={i} className="flex items-center justify-between p-3 bg-white rounded-xl">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl">
|
||||
{log.type === "feed" && "🍼"}
|
||||
{log.type === "sleep" && "😴"}
|
||||
{log.type === "diaper" && "👶"}
|
||||
</span>
|
||||
<div>
|
||||
<div className="font-medium capitalize">{log.type}</div>
|
||||
<div className="text-xs text-gray-500">
|
||||
{new Date(log.logged_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xl">{log.type === "feed" && "🍼"}{log.type === "sleep" && "😴"}{log.type === "diaper" && "👶"}</span>
|
||||
<div><div className="font-medium capitalize">{log.type}</div><div className="text-xs text-gray-500">{new Date(log.logged_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}</div></div>
|
||||
</div>
|
||||
{log.amount_ml && <span className="text-sm text-gray-500">{log.amount_ml}ml</span>}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} />
|
||||
|
||||
{aiOpen && (
|
||||
{aiOpen && aiReply && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setAiOpen(false)}>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 w-full max-w-sm mx-4 max-h-[80vh] flex flex-col" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h2 className="font-bold">🤖 Ask Tia AI</h2>
|
||||
<button onClick={() => setAiOpen(false)} className="text-gray-500">✕</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto mb-3 min-h-[200px]">
|
||||
{aiReply && (
|
||||
<div className="bg-rose-50 dark:bg-gray-700 p-3 rounded-xl text-sm">
|
||||
{aiReply}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={aiInput}
|
||||
onChange={(e) => setAiInput(e.target.value)}
|
||||
onKeyDown={(e) => e.key === "Enter" && handleAiChat()}
|
||||
placeholder="Ask a quick question..."
|
||||
className="flex-1 p-2 border rounded-xl text-sm"
|
||||
disabled={aiLoading}
|
||||
/>
|
||||
<button
|
||||
onClick={handleAiChat}
|
||||
disabled={aiLoading || !aiInput.trim()}
|
||||
className="px-3 bg-rose-400 text-white rounded-xl text-sm"
|
||||
>
|
||||
{aiLoading ? "..." : "Send"}
|
||||
</button>
|
||||
</div>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 w-full max-w-sm mx-4" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex justify-between items-center mb-3"><h2 className="font-bold">🤖 Tia says:</h2><button onClick={() => setAiOpen(false)}>✕</button></div>
|
||||
<div className="text-sm">{aiReply}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue