Fix AI chat performance and UX

- Single JOIN query instead of N+1 selects for sessions
- Auto-create session when sending without one
- Send button enabled when typing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Manohar Gupta 2026-05-11 01:49:36 +05:30
parent 881888ef10
commit 9e506279a7
2 changed files with 50 additions and 26 deletions

View file

@ -70,25 +70,53 @@ export default function AIChatPage() {
const currentSession = sessions.find(s => s.id === currentSessionId); const currentSession = sessions.find(s => s.id === currentSessionId);
const handleSend = async () => { const handleSend = async () => {
if (!input.trim() || loading || !currentSession) return; if (!input.trim() || loading) return;
setLoading(true); setLoading(true);
const userContent = input.trim(); const userContent = input.trim();
setInput(""); setInput("");
// Auto-create session if none selected
let sessionId = currentSessionId;
if (!sessionId && childId) {
try {
const res = await fetch("/api/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ childId, title: userContent.slice(0, 30) }),
});
const data = await res.json();
if (data.session) {
sessionId = data.session.id;
const newSession = { ...data.session, messages: [] };
setSessions([newSession, ...sessions]);
setCurrentSessionId(sessionId);
}
} catch (err) {
console.error("Failed to create session:", err);
setLoading(false);
return;
}
}
if (!sessionId) {
setLoading(false);
return;
}
try { try {
// Save user message // Save user message
await fetch("/api/chat", { await fetch("/api/chat", {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: currentSessionId, role: "user", content: userContent }), body: JSON.stringify({ sessionId, role: "user", content: userContent }),
}); });
// Get AI response // Get AI response
const aiRes = await fetch("/api/ai", { const aiRes = await fetch("/api/ai", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages: currentSession.messages.concat({ id: "", role: "user", content: userContent, createdAt: "" }) }), body: JSON.stringify({ messages: [{ role: "user", content: userContent }] }),
}); });
const aiData = await aiRes.json(); const aiData = await aiRes.json();
@ -97,7 +125,7 @@ export default function AIChatPage() {
await fetch("/api/chat", { await fetch("/api/chat", {
method: "PATCH", method: "PATCH",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sessionId: currentSessionId, role: "assistant", content: aiData.reply }), body: JSON.stringify({ sessionId, role: "assistant", content: aiData.reply }),
}); });
} }
@ -173,7 +201,7 @@ export default function AIChatPage() {
className="flex-1 p-3 border rounded-lg" className="flex-1 p-3 border rounded-lg"
disabled={loading} disabled={loading}
/> />
<button onClick={handleSend} disabled={loading || !currentSession} className={`px-4 py-2 text-white rounded-lg ${loading || !currentSession ? "bg-gray-300 cursor-not-allowed" : "bg-rose-400"}`}> <button onClick={handleSend} disabled={loading || !input.trim()} className={`px-4 py-2 text-white rounded-lg ${loading || !input.trim() ? "bg-gray-300 cursor-not-allowed" : "bg-rose-400"}`}>
{loading ? "..." : "Send"} {loading ? "..." : "Send"}
</button> </button>
</div> </div>

View file

@ -7,28 +7,24 @@ export async function GET(request: Request) {
const childId = searchParams.get("childId") || "default"; const childId = searchParams.get("childId") || "default";
try { try {
// Fetch sessions // Single query with JOIN for better performance
const sessions = await sql` const sessions = await sql.unsafe(`
SELECT id, title, created_at as "createdAt", updated_at as "updatedAt" SELECT
FROM chat_sessions cs.id, cs.title, cs.created_at as "createdAt", cs.updated_at as "updatedAt",
WHERE child_id = ${childId} COALESCE(
ORDER BY updated_at DESC json_agg(
`; json_build_object('id', cm.id, 'role', cm.role, 'content', cm.content, 'createdAt', cm.created_at ORDER BY cm.created_at)
) FILTER (WHERE cm.id IS NOT NULL),
'[]'::json
) as messages
FROM chat_sessions cs
LEFT JOIN chat_messages cm ON cm.session_id = cs.id
WHERE cs.child_id = $1
GROUP BY cs.id
ORDER BY cs.updated_at DESC
`, [childId]);
// Fetch messages for each session return NextResponse.json({ sessions: sessions || [] });
const sessionsWithMessages = await Promise.all(
(sessions || []).map(async (session: any) => {
const messages = await sql`
SELECT id, role, content, created_at as "createdAt"
FROM chat_messages
WHERE session_id = ${session.id}
ORDER BY created_at
`;
return { ...session, messages: messages || [] };
})
);
return NextResponse.json({ sessions: sessionsWithMessages || [] });
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });