Improve AI card - chat popup, saved chats, quick questions below

This commit is contained in:
Manohar Gupta 2026-05-10 12:34:41 +05:30
parent 415e92ada7
commit c1057830b1

View file

@ -4,19 +4,19 @@ 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";
export interface OfflineEntry { export interface OfflineEntry {
id: string; id: string;
type: "feed" | "diaper" | "sleep"; type: "feed" | "diaper" | "sleep";
data: { data: any;
type: "feed" | "diaper" | "sleep"; timestamp: number;
childId: string; }
subType: string;
amountMl?: number; export interface AIChat {
notes?: string; id: string;
startedAt?: string; role: "user" | "assistant";
endedAt?: string; content: string;
};
timestamp: number; timestamp: number;
} }
@ -25,9 +25,7 @@ export function getOfflineQueue(): OfflineEntry[] {
try { try {
const data = localStorage.getItem(OFFLINE_QUEUE_KEY); const data = localStorage.getItem(OFFLINE_QUEUE_KEY);
return data ? JSON.parse(data) : []; return data ? JSON.parse(data) : [];
} catch { } catch { return []; }
return [];
}
} }
export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) { export function addToOfflineQueue(entry: Omit<OfflineEntry, "id" | "timestamp">) {
@ -48,6 +46,18 @@ export async function processOfflineQueue() {
localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed)); localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed));
} }
function getAIChats(): AIChat[] {
if (typeof window === "undefined") return [];
try {
const data = localStorage.getItem(AI_CHAT_KEY);
return data ? JSON.parse(data) : [];
} catch { return []; }
}
function saveAIChats(chats: AIChat[]) {
localStorage.setItem(AI_CHAT_KEY, JSON.stringify(chats));
}
interface LogModalProps { interface LogModalProps {
type: "feed" | "diaper" | "sleep" | null; type: "feed" | "diaper" | "sleep" | null;
childId: string; childId: string;
@ -77,9 +87,7 @@ function LogModal({ type, childId, onClose }: LogModalProps) {
<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 === "feed" && "Log Feed"}{type === "diaper" && "Log Diaper"}{type === "sleep" && "Log Sleep"}
{type === "diaper" && "Log Diaper"}
{type === "sleep" && "Log Sleep"}
</h2> </h2>
{type === "feed" && ( {type === "feed" && (
<> <>
@ -128,13 +136,12 @@ function getGreeting() {
return "Good evening"; return "Good evening";
} }
// Quick questions for mama
const QUICK_QUESTIONS = [ const QUICK_QUESTIONS = [
"How much should my baby eat?", "How much should my baby eat?",
"When should baby sleep?", "When should baby sleep?",
"Is fever normal?", "Is fever normal?",
"How to increase milk supply?", "How to increase milk supply?",
"Baby won't sleep 😴", "Baby won't sleep",
"Starting solids?", "Starting solids?",
]; ];
@ -142,7 +149,7 @@ export default function HomePage() {
const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null); const [modalType, setModalType] = useState<"feed" | "diaper" | "sleep" | null>(null);
const [aiOpen, setAiOpen] = useState(false); const [aiOpen, setAiOpen] = useState(false);
const [aiInput, setAiInput] = useState(""); const [aiInput, setAiInput] = useState("");
const [aiReply, setAiReply] = useState(""); const [aiChats, setAiChats] = useState<AIChat[]>([]);
const [aiLoading, setAiLoading] = useState(false); const [aiLoading, setAiLoading] = useState(false);
const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a"); const [childId] = useState("5ad3b16a-1e0d-45ab-bc91-038397d75d0a");
const [pendingCount, setPendingCount] = useState(0); const [pendingCount, setPendingCount] = useState(0);
@ -156,13 +163,9 @@ export default function HomePage() {
if (saved === "dark") { setDarkMode(true); document.documentElement.classList.add("dark"); } if (saved === "dark") { setDarkMode(true); document.documentElement.classList.add("dark"); }
}, []); }, []);
const toggleDarkMode = () => { useEffect(() => {
const next = !darkMode; setAiChats(getAIChats());
setDarkMode(next); }, []);
localStorage.setItem("tia_theme", next ? "dark" : "light");
if (next) { document.documentElement.classList.add("dark"); }
else { document.documentElement.classList.remove("dark"); }
};
useEffect(() => { useEffect(() => {
const queue = getOfflineQueue(); const queue = getOfflineQueue();
@ -183,17 +186,37 @@ export default function HomePage() {
}).catch(() => {}); }).catch(() => {});
}, [childId]); }, [childId]);
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"); }
};
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);
const newChats: AIChat[] = [...aiChats, { id: crypto.randomUUID(), role: "user", content: q, timestamp: Date.now() }];
setAiChats(newChats);
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: q }] }) });
const data = await res.json(); const data = await res.json();
setAiReply(data.reply || "Sorry, I couldn't help with that."); const reply = data.reply || "Sorry, I couldn't help with that.";
} catch { setAiReply("Something went wrong. Try again."); } const updatedChats: AIChat[] = [...newChats, { id: crypto.randomUUID(), role: "assistant", content: reply, timestamp: Date.now() }];
setAiChats(updatedChats);
saveAIChats(updatedChats);
} catch {
const errorChats: AIChat[] = [...newChats, { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again.", timestamp: Date.now() }];
setAiChats(errorChats);
saveAIChats(errorChats);
}
setAiLoading(false); setAiLoading(false);
if (!question) setAiInput("");
}; };
return ( return (
@ -217,7 +240,6 @@ export default function HomePage() {
{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>} {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"> <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">
@ -238,24 +260,23 @@ export default function HomePage() {
{/* AI Chat Card */} {/* AI Chat Card */}
<div className="px-4"> <div className="px-4">
<h2 className="font-semibold mb-3 ml-1">🤖 Ask Tia AI</h2> <div className="flex justify-between items-center mb-3">
<h2 className="font-semibold ml-1">Ask AI</h2>
<Link href="/ai" className="text-rose-500 text-sm">View all chats </Link>
</div>
<div className="p-4 bg-white rounded-2xl shadow-md"> <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 gap-2 mb-3">
<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} /> <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> <button onClick={() => handleAiChat()} disabled={aiLoading || !aiInput.trim()} className="px-4 bg-rose-400 text-white rounded-xl text-sm">{aiLoading ? "..." : "Ask"}</button>
</div>
<div className="flex flex-wrap gap-2">
{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>
))}
</div> </div>
</div> </div>
</div> </div>
{/* Recent Activity */}
<div className="px-4 mt-4"> <div className="px-4 mt-4">
<h2 className="font-semibold mb-3 ml-1">Recent Activity</h2> <h2 className="font-semibold mb-3 ml-1">Recent Activity</h2>
<div className="space-y-2"> <div className="space-y-2">
@ -273,11 +294,26 @@ export default function HomePage() {
<LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} /> <LogModal type={modalType} childId={childId} onClose={() => setModalType(null)} />
{aiOpen && aiReply && ( {/* AI Chat Popup */}
{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" 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"><h2 className="font-bold">🤖 Tia says:</h2><button onClick={() => setAiOpen(false)}></button></div> <div className="flex justify-between items-center mb-3">
<div className="text-sm">{aiReply}</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]">
{aiChats.length === 0 ? <p className="text-gray-400 text-sm text-center mt-10">Ask a question to start chatting</p> : 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>
))}
{aiLoading && <div className="bg-rose-50 dark:bg-gray-700 p-3 rounded-xl text-sm animate-pulse">Thinking...</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 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>
</div>
</div> </div>
</div> </div>
)} )}