Unify homepage AI card with session system

- Uses tia_chat_sessions localStorage (shared with /ai page)
- First question becomes session title
- All chats now saved for future reference in /ai

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-10 13:13:17 +05:30
parent 7aaab29baf
commit e7411ee31f

View file

@ -4,7 +4,22 @@ import { useState, useEffect } from "react";
import Link from "next/link"; import Link from "next/link";
const OFFLINE_QUEUE_KEY = "tia_offline_queue"; const OFFLINE_QUEUE_KEY = "tia_offline_queue";
const AI_CHAT_KEY = "tia_ai_chats"; const CHATS_KEY = "tia_chat_sessions";
interface AIChat {
id: string;
role: "user" | "assistant";
content: string;
timestamp: number;
}
interface ChatSession {
id: string;
title: string;
messages: AIChat[];
createdAt: number;
updatedAt: number;
}
export interface OfflineEntry { export interface OfflineEntry {
id: string; id: string;
@ -13,13 +28,6 @@ export interface OfflineEntry {
timestamp: number; timestamp: number;
} }
export interface AIChat {
id: string;
role: "user" | "assistant";
content: string;
timestamp: number;
}
export function getOfflineQueue(): OfflineEntry[] { export function getOfflineQueue(): OfflineEntry[] {
if (typeof window === "undefined") return []; if (typeof window === "undefined") return [];
try { try {
@ -46,30 +54,28 @@ export async function processOfflineQueue() {
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed)); localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
} }
function getAIChats(): AIChat[] { // --- AI Session Functions (shared with /ai page) ---
function getSessions(): ChatSession[] {
if (typeof window === "undefined") return []; if (typeof window === "undefined") return [];
try { try {
const data = localStorage.getItem(AI_CHAT_KEY); const data = localStorage.getItem(CHATS_KEY);
return data ? JSON.parse(data) : []; return data ? JSON.parse(data) : [];
} catch { return []; } } catch { return []; }
} }
function saveAIChats(chats: AIChat[]) { function saveSessions(sessions: ChatSession[]) {
localStorage.setItem(AI_CHAT_KEY, JSON.stringify(chats)); localStorage.setItem(CHATS_KEY, JSON.stringify(sessions));
} }
interface LogModalProps { function createNewSession(): ChatSession {
type: "feed" | "diaper" | "sleep" | null; return { id: crypto.randomUUID(), title: "New conversation", messages: [], createdAt: Date.now(), updatedAt: Date.now() };
childId: string;
onClose: () => void;
} }
function LogModal({ type, childId, onClose }: LogModalProps) { function LogModal({ type, childId, onClose }: { type: "feed" | "diaper" | "sleep" | null; childId: string; onClose: () => void }) {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [subType, setSubType] = useState("breast_milk"); const [subType, setSubType] = useState("breast_milk");
const [amountMl, setAmountMl] = useState(""); const [amountMl, setAmountMl] = useState("");
const [notes, setNotes] = useState(""); const [notes, setNotes] = useState("");
if (!type) return null; if (!type) return null;
const handleSubmit = async () => { const handleSubmit = async () => {
@ -77,7 +83,7 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
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 { try {
const res = await fetch("/api/logs", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(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 }); } if (!res.ok && !navigator.onLine) addToOfflineQueue({ type: type as any, data });
onClose(); onClose();
} catch { addToOfflineQueue({ type: type as any, data }); onClose(); } } catch { addToOfflineQueue({ type: type as any, data }); onClose(); }
setLoading(false); setLoading(false);
@ -86,33 +92,12 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
return ( return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"> <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white rounded-2xl p-6 w-full max-w-sm mx-4"> <div className="bg-white rounded-2xl p-6 w-full max-w-sm mx-4">
<h2 className="text-xl font-bold mb-4"> <h2 className="text-xl font-bold mb-4">{type === "feed" && "Log Feed"}{type === "diaper" && "Log Diaper"}{type === "sleep" && "Log Sleep"}</h2>
{type === "feed" && "Log Feed"}{type === "diaper" && "Log Diaper"}{type === "sleep" && "Log Sleep"} {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></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" /></>)}
</h2> {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></select>)}
{type === "feed" && ( {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></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" />
<select value={subType} onChange={(e) => setSubType(e.target.value)} className="w-full p-3 border rounded-xl mb-3"> <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></div>
<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>
</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>
</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>
</div>
</div> </div>
</div> </div>
); );
@ -136,14 +121,7 @@ function getGreeting() {
return "Good evening"; return "Good evening";
} }
const QUICK_QUESTIONS = [ 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?"];
"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() { export default function HomePage() {
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null); const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
@ -163,8 +141,12 @@ export default function HomePage() {
if (saved === "dark") { setDarkMode(true); document.documentElement.classList.add("dark"); } if (saved === "dark") { setDarkMode(true); document.documentElement.classList.add("dark"); }
}, []); }, []);
// Load latest session on mount
useEffect(() => { useEffect(() => {
setAiChats(getAIChats()); const sessions = getSessions();
if (sessions.length > 0) {
setAiChats(sessions[0].messages);
}
}, []); }, []);
useEffect(() => { useEffect(() => {
@ -172,7 +154,7 @@ export default function HomePage() {
setPendingCount(queue.length); setPendingCount(queue.length);
const handleOnline = () => processOfflineQueue(); const handleOnline = () => processOfflineQueue();
window.addEventListener("online", handleOnline); 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); return () => window.removeEventListener("online", handleOnline);
}, []); }, []);
@ -181,40 +163,57 @@ export default function HomePage() {
fetch(`/api/logs?type=feed&childId=${childId}&limit=1`).then(r => r.json()), fetch(`/api/logs?type=feed&childId=${childId}&limit=1`).then(r => r.json()),
fetch(`/api/logs?type=sleep&childId=${childId}&limit=1`).then(r => r.json()), fetch(`/api/logs?type=sleep&childId=${childId}&limit=1`).then(r => r.json()),
fetch(`/api/logs?type=diaper&childId=${childId}&limit=1`).then(r => r.json()), fetch(`/api/logs?type=diaper&childId=${childId}&limit=1`).then(r => r.json()),
]).then(([feed, sleep, diaper]) => { ]).then(([feed, sleep, diaper]) => setLastLogs([feed.entries?.[0], sleep.entries?.[0], diaper.entries?.[0]].filter(Boolean)));
setLastLogs([feed.entries?.[0], sleep.entries?.[0], diaper.entries?.[0]].filter(Boolean));
}).catch(() => {});
}, [childId]); }, [childId]);
const toggleDarkMode = () => { const toggleDarkMode = () => {
const next = !darkMode; const next = !darkMode;
setDarkMode(next); setDarkMode(next);
localStorage.setItem("tia_theme", next ? "dark" : "light"); localStorage.setItem("tia_theme", next ? "dark" : "light");
if (next) { document.documentElement.classList.add("dark"); } next ? document.documentElement.classList.add("dark") : document.documentElement.classList.remove("dark");
else { document.documentElement.classList.remove("dark"); }
}; };
// Unified AI chat that saves to sessions
const handleAiChat = async (question?: string) => { const handleAiChat = async (question?: string) => {
const q = question || aiInput; const q = question || aiInput;
if (!q.trim() || aiLoading) return; if (!q.trim() || aiLoading) return;
setAiLoading(true); setAiLoading(true);
setAiOpen(true); setAiOpen(true);
const newChats: AIChat[] = [...aiChats, { id: crypto.randomUUID(), role: "user", content: q, timestamp: Date.now() }]; const sessions = getSessions();
setAiChats(newChats); let currentSession = sessions[0];
setAiInput("");
// Create new session if none exists
if (!currentSession) {
currentSession = createNewSession();
sessions.unshift(currentSession);
}
const userMsg: AIChat = { id: crypto.randomUUID(), role: "user", content: q, timestamp: Date.now() };
const updatedSession: ChatSession = {
...currentSession,
messages: [...currentSession.messages, userMsg],
title: currentSession.messages.length === 0 ? q.slice(0, 40) + (q.length > 40 ? "..." : "") : currentSession.title,
updatedAt: Date.now()
};
const inputVal = q.trim();
if (!question) setAiInput("");
try { try {
const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: q }] }) }); const res = await fetch("/api/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: inputVal }] }) });
const data = await res.json(); const data = await res.json();
const reply = data.reply || "Sorry, I couldn't help with that."; const assistantMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: data.reply || "Sorry, I couldn't help with that.", timestamp: Date.now() };
const updatedChats: AIChat[] = [...newChats, { id: crypto.randomUUID(), role: "assistant", content: reply, timestamp: Date.now() }]; const finalSession = { ...updatedSession, messages: [...updatedSession.messages, assistantMsg], updatedAt: Date.now() };
setAiChats(updatedChats); const finalSessions = [finalSession, ...sessions.filter(s => s.id !== currentSession!.id)];
saveAIChats(updatedChats); saveSessions(finalSessions);
setAiChats(finalSession.messages);
} catch { } catch {
const errorChats: AIChat[] = [...newChats, { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again.", timestamp: Date.now() }]; const errMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again.", timestamp: Date.now() };
setAiChats(errorChats); const errSession = { ...updatedSession, messages: [...updatedSession.messages, errMsg], updatedAt: Date.now() };
saveAIChats(errorChats); const errSessions = [errSession, ...sessions.filter(s => s.id !== currentSession!.id)];
saveSessions(errSessions);
setAiChats(errSession.messages);
} }
setAiLoading(false); setAiLoading(false);
}; };
@ -243,22 +242,13 @@ export default function HomePage() {
<div className="px-4 mb-4"> <div className="px-4 mb-4">
<h2 className="font-semibold mb-3 ml-1">Quick Log</h2> <h2 className="font-semibold mb-3 ml-1">Quick Log</h2>
<div className="grid grid-cols-4 gap-2"> <div className="grid grid-cols-4 gap-2">
<button onClick={() => setModalType("feed")} className="flex flex-col items-center p-4 bg-white rounded-xl shadow-sm"> <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>
<span className="text-3xl">🍼</span><span className="text-sm mt-1">Feed</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> <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>
<button onClick={() => setModalType("sleep")} className="flex flex-col items-center p-4 bg-white rounded-xl shadow-sm"> <Link href="/medical" 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">Medical</span></Link>
<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-4 bg-white rounded-xl shadow-sm">
<span className="text-3xl">👶</span><span className="text-sm mt-1">Diaper</span>
</button>
<Link href="/medical" 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">Medical</span>
</Link>
</div> </div>
</div> </div>
{/* AI Chat Card */}
<div className="px-4"> <div className="px-4">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center mb-3">
<h2 className="font-semibold ml-1">Ask AI</h2> <h2 className="font-semibold ml-1">Ask AI</h2>
@ -266,13 +256,11 @@ export default function HomePage() {
</div> </div>
<div className="p-4 bg-white rounded-2xl shadow-md"> <div className="p-4 bg-white rounded-2xl shadow-md">
<div className="flex gap-2 mb-3"> <div className="flex gap-2 mb-3">
<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} /> <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={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button> <button onClick={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{QUICK_QUESTIONS.map((q, i) => ( {QUICK_QUESTIONS.map((q, i) => <button key={i} onClick={() => handleAiChat(q)} className="px-3 py-1 bg-rose-50 text-rose-600 rounded-full text-sm">{q}</button>)}
<button key={i} onClick={() => handleAiChat(q)} className="px-3 py-1 bg-rose-50 text-rose-600 rounded-full text-sm">{q}</button>
))}
</div> </div>
</div> </div>
</div> </div>
@ -294,24 +282,16 @@ export default function HomePage() {
<LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} /> <LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} />
{/* AI Chat Popup */} {aiOpen && aiChats.length > 0 && (
{aiOpen && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50" onClick={() => setAiOpen(false)}> <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="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"> <div className="flex justify-between items-center mb-3"><h2 className="font-bold">Ask AI</h2><button onClick={() => setAiOpen(false)}></button></div>
<h2 className="font-bold">Ask AI</h2>
<button onClick={() => setAiOpen(false)}></button>
</div>
<div className="flex-1 overflow-y-auto space-y-3 mb-3 min-h-[250px]"> <div className="flex-1 overflow-y-auto space-y-3 mb-3 min-h-[250px]">
{aiChats.length === 0 ? <p className="text-gray-400 text-sm text-center mt-10">Ask a question to start chatting</p> : aiChats.map((chat) => ( {aiChats.map((chat) => (<div key={chat.id} className={`max-w-[85%] p-3 rounded-xl text-sm ${chat.role === "user" ? "ml-auto bg-rose-400 text-white" : "bg-rose-50 dark:bg-gray-700"}`}>{chat.content}</div>))}
<div key={chat.id} className={`max-w-[85%] p-3 rounded-xl text-sm ${chat.role === "user" ? "ml-auto bg-rose-400 text-white" : "bg-rose-50 dark:bg-gray-700"}`}>
{chat.content}
</div>
))}
{aiLoading && <div className="bg-rose-50 dark:bg-gray-700 p-3 rounded-xl text-sm animate-pulse">Thinking...</div>} {aiLoading && <div className="bg-rose-50 dark:bg-gray-700 p-3 rounded-xl text-sm animate-pulse">Thinking...</div>}
</div> </div>
<div className="flex gap-2"> <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 question..." className="flex-1 p-2 border rounded-xl text-sm" disabled={aiLoading} /> <input type="text" value={aiInput} onChange={e => setAiInput(e.target.value)} onKeyDown={e => e.key === "Enter" && handleAiChat()} placeholder="Ask a 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"></button> <button onClick={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-3 bg-rose-400 text-white rounded-xl"></button>
</div> </div>
</div> </div>