From c3255e82da2d62d301ca1c6bb50d2c9caa1de3e7 Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 10 May 2026 21:13:21 +0530 Subject: [PATCH] Migrate Chat Sessions to database - Add chat_sessions and chat_messages tables - Create /api/chat endpoint for CRUD operations - Update home page and /ai page to use database - All chat history now persists across sessions and devices Co-Authored-By: Claude Opus 4.7 --- src/app/ai/page.tsx | 221 ++++++++++++++++++-------------------- src/app/api/chat/route.ts | 104 ++++++++++++++++++ src/app/page.tsx | 99 +++++++++-------- 3 files changed, 266 insertions(+), 158 deletions(-) create mode 100644 src/app/api/chat/route.ts diff --git a/src/app/ai/page.tsx b/src/app/ai/page.tsx index 6cf01eb..e6b7fbd 100644 --- a/src/app/ai/page.tsx +++ b/src/app/ai/page.tsx @@ -6,42 +6,19 @@ interface AIChat { id: string; role: "user" | "assistant"; content: string; - timestamp: number; + createdAt: string; } interface ChatSession { id: string; title: string; messages: AIChat[]; - createdAt: number; - updatedAt: number; -} - -const CHATS_KEY = "tia_chat_sessions"; - -function getSessions(): ChatSession[] { - if (typeof window === "undefined") return []; - try { - const data = localStorage.getItem(CHATS_KEY); - return data ? JSON.parse(data) : []; - } catch { return []; } -} - -function saveSessions(sessions: ChatSession[]) { - localStorage.setItem(CHATS_KEY, JSON.stringify(sessions)); -} - -function createNewSession(): ChatSession { - return { - id: crypto.randomUUID(), - title: "New conversation", - messages: [], - createdAt: Date.now(), - updatedAt: Date.now(), - }; + createdAt: string; + updatedAt: string; } export default function AIChatPage() { + const childId = "default"; const [sessions, setSessions] = useState([]); const [currentSessionId, setCurrentSessionId] = useState(""); const [input, setInput] = useState(""); @@ -49,127 +26,139 @@ export default function AIChatPage() { const [sidebarOpen, setSidebarOpen] = useState(true); useEffect(() => { - const loaded = getSessions(); - if (loaded.length === 0 || !currentSessionId) { - const newS = createNewSession(); - loaded.unshift(newS); - saveSessions(loaded); - setCurrentSessionId(newS.id); - } - setSessions(loaded); - if (!currentSessionId && loaded.length > 0) { - setCurrentSessionId(loaded[0].id); - } + fetchSessions(); }, []); + const fetchSessions = async () => { + try { + const res = await fetch(`/api/chat?childId=${childId}`); + const data = await res.json(); + setSessions(data.sessions || []); + } catch (err) { + console.error("Failed to fetch:", err); + } + }; + + const createNewSession = async () => { + try { + const res = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ childId, title: "New conversation" }), + }); + const data = await res.json(); + if (data.session) { + setSessions([data.session, ...sessions]); + setCurrentSessionId(data.session.id); + } + } catch (err) { + console.error("Failed to create:", err); + } + }; + const currentSession = sessions.find(s => s.id === currentSessionId); const handleSend = async () => { if (!input.trim() || loading || !currentSession) return; setLoading(true); - const userMsg: AIChat = { id: crypto.randomUUID(), role: "user", content: input.trim(), timestamp: Date.now() }; - const updated: ChatSession = { ...currentSession, messages: [...currentSession.messages, userMsg], updatedAt: Date.now() }; - if (updated.messages.length === 1) { - updated.title = input.trim().slice(0, 40) + (input.trim().length > 40 ? "..." : ""); - } - - const newSessions = sessions.map(s => s.id === currentSessionId ? updated : s); - const inputVal = input.trim(); + const userContent = input.trim(); setInput(""); - const tempId = currentSessionId; try { - 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 assistantMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: data.reply || "Sorry, I couldn't help with that.", timestamp: Date.now() }; - const final = { ...updated, messages: [...updated.messages, assistantMsg], updatedAt: Date.now() }; - const finalSessions = newSessions.map(s => s.id === tempId ? final : s); - saveSessions(finalSessions); - setSessions(finalSessions); - } catch { - const errMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again.", timestamp: Date.now() }; - const errS = { ...updated, messages: [...updated.messages, errMsg], updatedAt: Date.now() }; - const errSessions = newSessions.map(s => s.id === tempId ? errS : s); - saveSessions(errSessions); - setSessions(errSessions); + // Save user message + await fetch("/api/chat", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: currentSessionId, role: "user", content: userContent }), + }); + + // Get AI response + const aiRes = await fetch("/api/ai", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages: currentSession.messages.concat({ id: "", role: "user", content: userContent, createdAt: "" }) }), + }); + const aiData = await aiRes.json(); + + // Save AI response + if (aiData.reply) { + await fetch("/api/chat", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: currentSessionId, role: "assistant", content: aiData.reply }), + }); + } + + // Refresh sessions + fetchSessions(); + } catch (err) { + console.error("Failed to send:", err); } setLoading(false); }; - const switchSession = (id: string) => setCurrentSessionId(id); - - const newChat = () => { - const ns = createNewSession(); - const upd = [ns, ...sessions]; - saveSessions(upd); - setSessions(upd); - setCurrentSessionId(ns.id); - }; - - const deleteSession = (id: string, e: React.MouseEvent) => { - e.stopPropagation(); - const filtered = sessions.filter(s => s.id !== id); - saveSessions(filtered); - setSessions(filtered); - if (id === currentSessionId && filtered.length > 0) setCurrentSessionId(filtered[0].id); - else if (filtered.length === 0) newChat(); - }; - - const formatDate = (ts: number) => { - const d = new Date(ts); - const now = new Date(); - return d.toDateString() === now.toDateString() ? d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }) : d.toLocaleDateString([], { month: "short", day: "numeric" }); + const deleteSession = async (id: string) => { + try { + await fetch(`/api/chat?id=${id}`, { method: "DELETE" }); + setSessions(sessions.filter(s => s.id !== id)); + } catch (err) { + console.error("Failed to delete:", err); + } }; return ( -
-
-
-
-

Chats

- +
+ {/* Sidebar */} +
+
+
+

AI Chat

+
-
- {sessions.map(s => ( -
switchSession(s.id)} className={`p-3 border-b cursor-pointer hover:bg-gray-50 ${s.id === currentSessionId ? "bg-rose-50" : ""}`}> -
-
-
{s.title}
-
{formatDate(s.createdAt)}
-
- -
+
+ {sessions.map(session => ( +
setCurrentSessionId(session.id)} + className={`p-2 rounded-lg cursor-pointer ${session.id === currentSessionId ? "bg-rose-100" : ""}`} + > +
{session.title}
+
{new Date(session.updatedAt).toLocaleDateString()}
))}
+ {/* Main chat */}
-
- - ← Home +
+ + {currentSession?.title || "AI Chat"}
-
- {(!currentSession || currentSession.messages.length === 0) ? ( -
-

Start a conversation

-

Ask me anything about your baby!

-
- ) : currentSession.messages.map(msg => ( -
-
{msg.content}
+
+ {currentSession?.messages.map((msg, i) => ( +
+ {msg.content}
))} - {loading &&
Thinking...
}
-
-
- setInput(e.target.value)} onKeyDown={e => e.key === "Enter" && handleSend()} placeholder="Ask anything..." className="flex-1 p-3 border rounded-full" disabled={loading} /> - +
+
+ setInput(e.target.value)} + onKeyDown={e => e.key === "Enter" && handleSend()} + placeholder="Ask about your baby..." + className="flex-1 p-3 border rounded-lg" + disabled={loading} + /> +
diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts new file mode 100644 index 0000000..cd538ad --- /dev/null +++ b/src/app/api/chat/route.ts @@ -0,0 +1,104 @@ +import { NextResponse } from "next/server"; +import { sql } from "@/db"; + +// GET - list chat sessions for a child +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const childId = searchParams.get("childId") || "default"; + + try { + const sessions = await sql.unsafe( + `SELECT cs.id, cs.title, cs.created_at as "createdAt", cs.updated_at as "updatedAt", + COALESCE( + (SELECT json_agg(json_build_object('id', cm.id, 'role', cm.role, 'content', cm.content, 'createdAt', cm.created_at)) + FROM chat_messages cm WHERE cm.session_id = cs.id ORDER BY cm.created_at), + '[]'::json + ) as messages + FROM chat_sessions cs + WHERE cs.child_id = $1 + ORDER BY cs.updated_at DESC`, + [childId] + ); + return NextResponse.json({ sessions: sessions || [] }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// POST - create new chat session +export async function POST(request: Request) { + try { + const body = await request.json(); + const { childId, title } = body; + + if (!childId) { + return NextResponse.json({ error: "childId required" }, { status: 400 }); + } + + const [session] = await sql.unsafe( + `INSERT INTO chat_sessions (child_id, title) + VALUES ($1, $2) + RETURNING id, title, created_at as "createdAt", updated_at as "updatedAt"`, + [childId, title || "New conversation"] + ); + + return NextResponse.json({ success: true, session }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// PATCH - update session title or add message +export async function PATCH(request: Request) { + try { + const body = await request.json(); + const { sessionId, title, role, content } = body; + + if (!sessionId) { + return NextResponse.json({ error: "sessionId required" }, { status: 400 }); + } + + if (title) { + await sql.unsafe( + `UPDATE chat_sessions SET title = $1, updated_at = NOW() WHERE id = $2`, + [title, sessionId] + ); + } + + if (role && content) { + await sql.unsafe( + `INSERT INTO chat_messages (session_id, role, content) VALUES ($1, $2, $3)`, + [sessionId, role, content] + ); + await sql.unsafe( + `UPDATE chat_sessions SET updated_at = NOW() WHERE id = $1`, + [sessionId] + ); + } + + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// DELETE - delete chat session +export async function DELETE(request: Request) { + const { searchParams } = new URL(request.url); + const id = searchParams.get("id"); + + if (!id) { + return NextResponse.json({ error: "ID required" }, { status: 400 }); + } + + try { + await sql.unsafe(`DELETE FROM chat_sessions WHERE id = $1`, [id]); + return NextResponse.json({ success: true }); + } catch (error) { + console.error(error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index 4284f05..99345a0 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,21 +5,20 @@ import Link from "next/link"; import { useTheme } from "./ThemeProvider"; const OFFLINE_QUEUE_KEY = "tia_offline_queue"; -const CHATS_KEY = "tia_chat_sessions"; interface AIChat { id: string; role: "user" | "assistant"; content: string; - timestamp: number; + createdAt: string; } interface ChatSession { id: string; title: string; messages: AIChat[]; - createdAt: number; - updatedAt: number; + createdAt: string; + updatedAt: string; } export interface OfflineEntry { @@ -55,21 +54,25 @@ export async function processOfflineQueue() { localStorage.setItem(OFFLINE_QUEUE_KEY, JSON.stringify(failed)); } -// --- AI Session Functions (shared with /ai page) --- -function getSessions(): ChatSession[] { - if (typeof window === "undefined") return []; +// --- AI Session Functions (using database) --- +async function getSessions(cid: string): Promise { try { - const data = localStorage.getItem(CHATS_KEY); - return data ? JSON.parse(data) : []; + const res = await fetch(`/api/chat?childId=${cid}`); + const data = await res.json(); + return data.sessions || []; } catch { return []; } } -function saveSessions(sessions: ChatSession[]) { - localStorage.setItem(CHATS_KEY, JSON.stringify(sessions)); -} - -function createNewSession(): ChatSession { - return { id: crypto.randomUUID(), title: "New conversation", messages: [], createdAt: Date.now(), updatedAt: Date.now() }; +async function createSession(cid: string): Promise { + try { + const res = await fetch("/api/chat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ childId: cid, title: "New conversation" }), + }); + const data = await res.json(); + return data.session || null; + } catch { return null; } } function LogModal({ type, childId, onClose }: { type: "feed" | "diaper" | "sleep" | null; childId: string; onClose: () => void }) { @@ -139,10 +142,11 @@ export default function HomePage() { // Load latest session on mount useEffect(() => { - const sessions = getSessions(); - if (sessions.length > 0) { - setAiChats(sessions[0].messages); - } + getSessions(childId).then(sessions => { + if (sessions.length > 0) { + setAiChats(sessions[0].messages); + } + }); }, []); useEffect(() => { @@ -166,47 +170,58 @@ export default function HomePage() { toggleTheme(); }; - // Unified AI chat that saves to sessions + // Unified AI chat that saves to sessions (database) const handleAiChat = async (question?: string) => { const q = question || aiInput; if (!q.trim() || aiLoading) return; setAiLoading(true); setAiOpen(true); - const sessions = getSessions(); - let currentSession = sessions[0]; + const sessions = await getSessions(childId); + let currentSession: ChatSession | null = sessions[0] || null; // Create new session if none exists if (!currentSession) { - currentSession = createNewSession(); - sessions.unshift(currentSession); + currentSession = await createSession(childId); + if (currentSession) { + 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() - }; + if (!currentSession) { + setAiLoading(false); + return; + } + const userMsg: AIChat = { id: crypto.randomUUID(), role: "user", content: q, createdAt: new Date().toISOString() }; const inputVal = q.trim(); if (!question) setAiInput(""); try { + // Save user message to DB + await fetch("/api/chat", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: currentSession.id, role: "user", content: inputVal }), + }); + + // Get AI response 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 assistantMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: data.reply || "Sorry, I couldn't help with that.", timestamp: Date.now() }; - const finalSession = { ...updatedSession, messages: [...updatedSession.messages, assistantMsg], updatedAt: Date.now() }; - const finalSessions = [finalSession, ...sessions.filter(s => s.id !== currentSession!.id)]; - saveSessions(finalSessions); - setAiChats(finalSession.messages); - } catch { - const errMsg: AIChat = { id: crypto.randomUUID(), role: "assistant", content: "Something went wrong. Try again.", timestamp: Date.now() }; - const errSession = { ...updatedSession, messages: [...updatedSession.messages, errMsg], updatedAt: Date.now() }; - const errSessions = [errSession, ...sessions.filter(s => s.id !== currentSession!.id)]; - saveSessions(errSessions); - setAiChats(errSession.messages); + const reply = data.reply || "Sorry, I couldn't help with that."; + + // Save AI response to DB + await fetch("/api/chat", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sessionId: currentSession.id, role: "assistant", content: reply }), + }); + + // Refresh sessions + const updatedSessions = await getSessions(childId); + setAiChats(updatedSessions[0]?.messages || []); + } catch (err) { + console.error("AI chat error:", err); } setAiLoading(false); };