🗄️ Database Implementation
Building on: This route enhances your existing Summary Memory implementation. Make sure you have Summary Memory working before starting here.
🎯 What You’ll Build
Section titled “🎯 What You’ll Build”Transform your Summary Memory chat to include:
- ✅ Auto-save conversations to PostgreSQL database after every message
- ✅ Auto-restore chats when page loads, including summaries
- ✅ Multi-user support with conversation isolation
- ✅ Conversation sidebar with saved conversation list (same as Local Storage)
- ✅ Cross-device sync - access conversations from any device
Why Database + Summary Memory?
Section titled “Why Database + Summary Memory?”// Perfect for production applicationsconst benefits = { setup: '45 minutes with PostgreSQL', features: 'Smart summarization + multi-user storage', scalability: 'Handles thousands of users', sync: 'Cross-device conversation access', reliability: 'Professional data persistence'}
// Additional capabilities over Local Storageconst advantages = { users: 'Multiple users with isolated conversations', devices: 'Access from any device', backup: 'Professional database backups', search: 'Query conversations across users'}
🛠️ Step 1: Database Setup
Section titled “🛠️ Step 1: Database Setup”Install Database Dependencies
Section titled “Install Database Dependencies”Add to your backend folder:
npm install pg prisma @prisma/clientnpm install -D prisma
Initialize Prisma
Section titled “Initialize Prisma”npx prisma init
Database Schema
Section titled “Database Schema”Update prisma/schema.prisma
:
generator client { provider = "prisma-client-js"}
datasource db { provider = "postgresql" url = env("DATABASE_URL")}
model User { id String @id @default(cuid()) email String? @unique name String? createdAt DateTime @default(now())
conversations Conversation[]
@@map("users")}
model Conversation { id String @id @default(cuid()) userId String title String? summary String? conversationType String @default("general") messageCount Int @default(0) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade) messages Message[]
@@map("conversations")}
model Message { id String @id @default(cuid()) conversationId String role String // 'user' or 'assistant' content String createdAt DateTime @default(now())
conversation Conversation @relation(fields: [conversationId], references: [id], onDelete: Cascade)
@@map("messages")}
Environment Configuration
Section titled “Environment Configuration”Update your .env
file:
OPENAI_API_KEY=your_api_key_herePORT=8000DATABASE_URL="postgresql://username:password@localhost:5432/ai_chat_db"
Setup Database
Section titled “Setup Database”# Generate Prisma clientnpx prisma generate
# Create and apply migrationsnpx prisma db push
# Optional: View data in Prisma Studionpx prisma studio
🔄 Step 2: Database Functions
Section titled “🔄 Step 2: Database Functions”Create db/operations.js
in your backend:
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// 🆕 DATABASE: Simple database operationsexport const createUser = async (email, name) => { return await prisma.user.create({ data: { email, name } })}
export const getOrCreateUser = async (userId) => { // For simplicity, create user if doesn't exist try { return await prisma.user.findUnique({ where: { id: userId } }) || await prisma.user.create({ data: { id: userId, name: 'User' } }) } catch (error) { return await prisma.user.create({ data: { id: userId, name: 'User' } }) }}
export const createConversation = async (userId, title = 'New Conversation') => { return await prisma.conversation.create({ data: { userId, title, }, include: { messages: true } })}
export const getUserConversations = async (userId) => { return await prisma.conversation.findMany({ where: { userId }, include: { messages: { orderBy: { createdAt: 'desc' }, take: 1 // Get last message for preview } }, orderBy: { updatedAt: 'desc' } })}
export const getConversationWithMessages = async (conversationId, userId) => { return await prisma.conversation.findFirst({ where: { id: conversationId, userId // Security: ensure user owns this conversation }, include: { messages: { orderBy: { createdAt: 'asc' } } } })}
export const addMessageToConversation = async (conversationId, role, content) => { const message = await prisma.message.create({ data: { conversationId, role, content } })
// Update conversation await prisma.conversation.update({ where: { id: conversationId }, data: { updatedAt: new Date(), messageCount: { increment: 1 } } })
return message}
export const updateConversationSummary = async (conversationId, summary, conversationType) => { return await prisma.conversation.update({ where: { id: conversationId }, data: { summary, conversationType, updatedAt: new Date() } })}
export const updateConversationTitle = async (conversationId, title) => { return await prisma.conversation.update({ where: { id: conversationId }, data: { title } })}
export const deleteConversation = async (conversationId, userId) => { return await prisma.conversation.deleteMany({ where: { id: conversationId, userId // Security: ensure user owns this conversation } })}
const generateTitle = (messages) => { if (!messages || messages.length === 0) return 'New Conversation'
const firstUserMessage = messages.find(msg => msg.role === 'user') if (firstUserMessage) { return firstUserMessage.content.length > 30 ? firstUserMessage.content.substring(0, 30) + '...' : firstUserMessage.content } return 'New Conversation'}
export { generateTitle }
🎛️ Step 3: Enhanced Backend with Database
Section titled “🎛️ Step 3: Enhanced Backend with Database”Update your backend to use the database. Add to your index.js
:
import express from 'express'import cors from 'cors'import OpenAI from 'openai'import { createConversation, getUserConversations, getConversationWithMessages, addMessageToConversation, updateConversationSummary, updateConversationTitle, deleteConversation, getOrCreateUser, generateTitle} from './db/operations.js'
const app = express()app.use(cors())app.use(express.json())
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY,})
// Keep your existing summarize endpoint (unchanged)app.post("/api/summarize", async (req, res) => { try { const { messages, conversationType = 'general' } = req.body;
if (!messages || messages.length === 0) { return res.status(400).json({ error: "Messages are required" }); }
const summaryInstructions = { technical: "Create a technical summary focusing on technologies discussed, decisions made, code examples covered, and implementation details. Preserve specific technical context.", creative: "Summarize the creative process including ideas generated, concepts explored, and creative directions chosen. Maintain the creative flow context.", support: "Summarize the support conversation including the user's issue, troubleshooting steps attempted, solutions provided, and current status.", general: "Create a conversational summary capturing key topics, decisions, and important context for continuing the discussion naturally." };
const instruction = summaryInstructions[conversationType] || summaryInstructions.general; let contextualMessage = `Please summarize this conversation:\n\n${messages.map(msg => `${msg.role}: ${msg.content}`).join('\n\n')}`; contextualMessage = `You are a conversation summarizer. ${instruction} Keep it concise but comprehensive enough to maintain conversation continuity.\n\n${contextualMessage}`;
console.log(`Creating summary for ${messages.length} messages`);
const response = await openai.responses.create({ model: "gpt-4o-mini", input: contextualMessage, });
res.json({ summary: response.output_text, messagesCount: messages.length, conversationType: conversationType, success: true, });
} catch (error) { console.error("Summarization Error:", error); res.status(500).json({ error: "Failed to create summary", success: false, }); }});
// 🆕 DATABASE: Enhanced chat endpoint with database storageapp.post("/api/chat/stream", async (req, res) => { try { const { message, conversationHistory = [], summary = null, recentWindowSize = 15, userId, conversationId = null } = req.body;
if (!message || !userId) { return res.status(400).json({ error: "Message and user ID are required" }); }
// Ensure user exists await getOrCreateUser(userId);
// Create new conversation if none provided let currentConversationId = conversationId; if (!currentConversationId) { const conversation = await createConversation(userId); currentConversationId = conversation.id; }
// Save user message to database await addMessageToConversation(currentConversationId, 'user', message);
// Set headers for streaming res.writeHead(200, { 'Content-Type': 'text/plain', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', });
// Build smart context with summary (same as Summary Memory) let contextualMessage = message;
if (summary && conversationHistory.length > 0) { const recentMessages = conversationHistory.slice(-recentWindowSize); const recentContext = recentMessages .map(msg => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`) .join('\n');
contextualMessage = `Previous conversation summary:\n${summary}\n\nRecent conversation:\n${recentContext}\n\nCurrent question: ${message}`; } else if (conversationHistory.length > 0) { const context = conversationHistory .map(msg => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`) .join('\n');
contextualMessage = `Previous conversation:\n${context}\n\nCurrent question: ${message}`; }
// Create streaming response const stream = await openai.responses.create({ model: "gpt-4o-mini", input: contextualMessage, stream: true, });
let aiResponse = '';
// Handle Response API events for await (const event of stream) { switch (event.type) { case "response.output_text.delta": if (event.delta) { let textChunk = typeof event.delta === "string" ? event.delta : event.delta.text || "";
if (textChunk) { aiResponse += textChunk; res.write(textChunk); res.flush?.(); } } break;
case "text_delta": if (event.text) { aiResponse += event.text; res.write(event.text); res.flush?.(); } break;
case "response.created": case "response.completed": case "response.output_item.added": case "response.content_part.added": case "response.content_part.done": case "response.output_item.done": case "response.output_text.done": break;
case "error": console.error("Stream error:", event.error); res.write("\n[Error during generation]"); break; } }
// Save AI response to database await addMessageToConversation(currentConversationId, 'assistant', aiResponse);
// Generate title if this is a new conversation if (!conversationId && conversationHistory.length === 0) { const title = generateTitle([{ role: 'user', content: message }]); await updateConversationTitle(currentConversationId, title); }
res.end();
} catch (error) { console.error("Streaming chat error:", error);
if (res.headersSent) { res.write("\n[Error occurred]"); res.end(); } else { res.status(500).json({ error: "Failed to stream AI response", success: false, }); } }});
// 🆕 DATABASE: Conversation management endpointsapp.post("/api/conversations", async (req, res) => { try { const { userId, title } = req.body;
if (!userId) { return res.status(400).json({ error: "User ID is required" }); }
await getOrCreateUser(userId); const conversation = await createConversation(userId, title);
res.json({ conversation, success: true }); } catch (error) { console.error("Create conversation error:", error); res.status(500).json({ error: "Failed to create conversation", success: false }); }});
app.get("/api/conversations/:userId", async (req, res) => { try { const { userId } = req.params; const conversations = await getUserConversations(userId);
res.json({ conversations, success: true }); } catch (error) { console.error("Get conversations error:", error); res.status(500).json({ error: "Failed to get conversations", success: false }); }});
app.get("/api/conversation/:id", async (req, res) => { try { const { id } = req.params; const { userId } = req.query;
if (!userId) { return res.status(400).json({ error: "User ID is required" }); }
const conversation = await getConversationWithMessages(id, userId);
if (!conversation) { return res.status(404).json({ error: "Conversation not found", success: false }); }
res.json({ conversation, success: true }); } catch (error) { console.error("Get conversation error:", error); res.status(500).json({ error: "Failed to get conversation", success: false }); }});
app.delete("/api/conversation/:id", async (req, res) => { try { const { id } = req.params; const { userId } = req.query;
if (!userId) { return res.status(400).json({ error: "User ID is required" }); }
await deleteConversation(id, userId);
res.json({ success: true }); } catch (error) { console.error("Delete conversation error:", error); res.status(500).json({ error: "Failed to delete conversation", success: false }); }});
// 🆕 DATABASE: Update conversation summaryapp.put("/api/conversation/:id/summary", async (req, res) => { try { const { id } = req.params; const { summary, conversationType, userId } = req.body;
if (!userId) { return res.status(400).json({ error: "User ID is required" }); }
// Verify user owns conversation const conversation = await getConversationWithMessages(id, userId); if (!conversation) { return res.status(404).json({ error: "Conversation not found", success: false }); }
await updateConversationSummary(id, summary, conversationType);
res.json({ success: true }); } catch (error) { console.error("Update summary error:", error); res.status(500).json({ error: "Failed to update summary", success: false }); }});
const PORT = process.env.PORT || 8000;app.listen(PORT, () => { console.log(`Server running on port ${PORT}`);});
🔄 Step 4: Enhanced Frontend with Database
Section titled “🔄 Step 4: Enhanced Frontend with Database”Update your Summary Memory frontend to use the database. The component structure stays the same but with database calls:
import { useState, useRef, useEffect } from 'react'import { Send, Bot, User, Trash2, Plus, MessageSquare } from 'lucide-react'
// 🆕 DATABASE: Simple database functions (replace localStorage functions)const BACKEND_URL = 'http://localhost:8000'
// Simple user ID generation (in production, use proper auth)const getUserId = () => { let userId = localStorage.getItem('userId') if (!userId) { userId = 'user_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9) localStorage.setItem('userId', userId) } return userId}
const createNewConversation = async (userId, title = 'New Conversation') => { try { const response = await fetch(`${BACKEND_URL}/api/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId, title }) }) const data = await response.json() return data.success ? data.conversation : null } catch (error) { console.error('Failed to create conversation:', error) return null }}
const getAllConversations = async (userId) => { try { const response = await fetch(`${BACKEND_URL}/api/conversations/${userId}`) const data = await response.json() return data.success ? data.conversations : [] } catch (error) { console.error('Failed to load conversations:', error) return [] }}
const loadConversation = async (conversationId, userId) => { try { const response = await fetch(`${BACKEND_URL}/api/conversation/${conversationId}?userId=${userId}`) const data = await response.json() return data.success ? data.conversation : null } catch (error) { console.error('Failed to load conversation:', error) return null }}
const deleteConversationDB = async (conversationId, userId) => { try { const response = await fetch(`${BACKEND_URL}/api/conversation/${conversationId}?userId=${userId}`, { method: 'DELETE' }) const data = await response.json() return data.success } catch (error) { console.error('Failed to delete conversation:', error) return false }}
const saveSummaryToDB = async (conversationId, summary, conversationType, userId) => { try { const response = await fetch(`${BACKEND_URL}/api/conversation/${conversationId}/summary`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ summary, conversationType, userId }) }) const data = await response.json() return data.success } catch (error) { console.error('Failed to save summary:', error) return false }}
function StreamingChat() { const [messages, setMessages] = useState([]) const [input, setInput] = useState('') const [isStreaming, setIsStreaming] = useState(false) const abortControllerRef = useRef(null)
// Summary Memory state (keep existing) const [summary, setSummary] = useState(null) const [recentWindowSize, setRecentWindowSize] = useState(15) const [summaryThreshold, setSummaryThreshold] = useState(25) const [isCreatingSummary, setIsCreatingSummary] = useState(false) const [conversationType, setConversationType] = useState('general')
// 🆕 DATABASE: Conversation management const [currentConversationId, setCurrentConversationId] = useState(null) const [conversations, setConversations] = useState([]) const [showSidebar, setShowSidebar] = useState(true) const [userId] = useState(getUserId())
// Keep ALL your existing Summary Memory functions const buildConversationHistory = (messages) => { return messages .filter(msg => !msg.isStreaming) .map(msg => ({ role: msg.isUser ? "user" : "assistant", content: msg.text })); };
const detectConversationType = (messages) => { const recentText = messages.slice(-10).map(m => m.text).join(' ').toLowerCase();
if (recentText.includes('function') || recentText.includes('code') || recentText.includes('api')) { return 'technical'; } else if (recentText.includes('create') || recentText.includes('idea') || recentText.includes('design')) { return 'creative'; } else if (recentText.includes('problem') || recentText.includes('error') || recentText.includes('help')) { return 'support'; } return 'general'; };
const createSummary = async (messagesToSummarize) => { if (isCreatingSummary) return;
try { setIsCreatingSummary(true);
const detectedType = detectConversationType(messages);
const response = await fetch(`${BACKEND_URL}/api/summarize`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: messagesToSummarize, conversationType: detectedType }), });
const data = await response.json();
if (data.success) { setSummary(data.summary); setConversationType(data.conversationType);
// 🆕 DATABASE: Save summary to database if (currentConversationId) { await saveSummaryToDB(currentConversationId, data.summary, data.conversationType, userId); }
console.log(`📋 Summary created: ${data.messagesCount} messages summarized as ${data.conversationType}`); } } catch (error) { console.error("Failed to create summary:", error); } finally { setIsCreatingSummary(false); } };
const shouldCreateSummary = (conversationHistory) => { return conversationHistory.length >= summaryThreshold && !summary; };
const shouldUpdateSummary = (conversationHistory) => { return conversationHistory.length >= summaryThreshold * 2 && summary; };
const isGoodTimeToSummarize = (conversationHistory) => { const recentMessages = conversationHistory.slice(-3);
const hasCodeDiscussion = recentMessages.some(msg => msg.content.includes('```') || msg.content.includes('function'));
const hasFollowUp = recentMessages.some(msg => msg.content.toLowerCase().includes('can you explain') || msg.content.toLowerCase().includes('tell me more') || msg.content.toLowerCase().includes('what about'));
return !hasCodeDiscussion && !hasFollowUp; };
const getMemoryStats = () => { const totalMessages = messages.filter(msg => !msg.isStreaming).length const recentMessages = Math.min(totalMessages, recentWindowSize) const summarizedMessages = Math.max(0, totalMessages - recentWindowSize)
return { totalMessages, recentMessages, summarizedMessages } };
// 🆕 DATABASE: Load conversations on startup useEffect(() => { const loadUserConversations = async () => { const userConversations = await getAllConversations(userId) setConversations(userConversations)
// Load the most recent conversation if exists if (userConversations.length > 0) { const mostRecent = userConversations[0] // Already sorted by updatedAt desc loadConversationById(mostRecent.id) } }
loadUserConversations() }, [userId])
// 🆕 DATABASE: Conversation management functions const startNewConversation = async () => { const conversation = await createNewConversation(userId) if (conversation) { setCurrentConversationId(conversation.id) setMessages([]) setSummary(null) setConversationType('general')
// Refresh conversation list const userConversations = await getAllConversations(userId) setConversations(userConversations) } }
const loadConversationById = async (conversationId) => { const conversation = await loadConversation(conversationId, userId) if (conversation) { setCurrentConversationId(conversationId)
// Convert database messages to frontend format const frontendMessages = conversation.messages.map(msg => ({ text: msg.content, isUser: msg.role === 'user', id: msg.id, isStreaming: false }))
setMessages(frontendMessages) setSummary(conversation.summary || null) setConversationType(conversation.conversationType || 'general') console.log(`✅ Loaded conversation: ${conversation.title}`) } }
const deleteConversationById = async (conversationId) => { const success = await deleteConversationDB(conversationId, userId) if (success) { // Refresh conversation list const userConversations = await getAllConversations(userId) setConversations(userConversations)
if (currentConversationId === conversationId) { if (userConversations.length > 0) { loadConversationById(userConversations[0].id) } else { startNewConversation() } } } }
// 🔄 ENHANCED: sendMessage with database storage const sendMessage = async () => { if (!input.trim() || isStreaming) return
const userMessage = { text: input, isUser: true, id: Date.now() } setMessages(prev => [...prev, userMessage])
const currentInput = input setInput('') setIsStreaming(true)
const aiMessageId = Date.now() + 1 const aiMessage = { text: '', isUser: false, id: aiMessageId, isStreaming: true } setMessages(prev => [...prev, aiMessage])
try { const conversationHistory = buildConversationHistory(messages)
// Summary Memory logic (keep exactly as is) if (shouldCreateSummary(conversationHistory) && isGoodTimeToSummarize(conversationHistory)) { const messagesToSummarize = conversationHistory.slice(0, -recentWindowSize); createSummary(messagesToSummarize); } else if (shouldUpdateSummary(conversationHistory) && isGoodTimeToSummarize(conversationHistory)) { const messagesToSummarize = conversationHistory.slice(0, -recentWindowSize); createSummary(messagesToSummarize); }
abortControllerRef.current = new AbortController()
// 🆕 DATABASE: Include userId and conversationId in request const response = await fetch(`${BACKEND_URL}/api/chat/stream`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: currentInput, conversationHistory: conversationHistory, summary: summary, recentWindowSize: recentWindowSize, userId: userId, conversationId: currentConversationId }), signal: abortControllerRef.current.signal, })
if (!response.ok) { throw new Error('Failed to get response') }
const reader = response.body.getReader() const decoder = new TextDecoder()
while (true) { const { done, value } = await reader.read() if (done) break
const chunk = decoder.decode(value, { stream: true })
setMessages(prev => prev.map(msg => msg.id === aiMessageId ? { ...msg, text: msg.text + chunk } : msg ) ) }
setMessages(prev => prev.map(msg => msg.id === aiMessageId ? { ...msg, isStreaming: false } : msg ) )
// 🆕 DATABASE: Refresh conversation list after sending message const userConversations = await getAllConversations(userId) setConversations(userConversations)
} catch (error) { if (error.name === 'AbortError') { console.log('Request was cancelled') } else { console.error('Streaming error:', error) setMessages(prev => prev.map(msg => msg.id === aiMessageId ? { ...msg, text: 'Sorry, something went wrong.', isStreaming: false } : msg ) ) } } finally { setIsStreaming(false) abortControllerRef.current = null } }
const handleKeyPress = (e) => { if (e.key === 'Enter' && !e.shiftKey && !isStreaming) { e.preventDefault() sendMessage() } }
const stopStreaming = () => { if (abortControllerRef.current) { abortControllerRef.current.abort() } }
return ( <div className="min-h-screen bg-gray-100 flex"> {/* 🆕 DATABASE: Conversation Sidebar (same as Local Storage) */} {showSidebar && ( <div className="w-80 bg-white border-r border-gray-200 flex flex-col"> {/* Sidebar Header */} <div className="p-4 border-b border-gray-200"> <div className="flex items-center justify-between mb-3"> <h2 className="text-lg font-semibold text-gray-800">Conversations</h2> <button onClick={() => setShowSidebar(false)} className="text-gray-400 hover:text-gray-600" > ✕ </button> </div> <button onClick={startNewConversation} className="w-full bg-blue-500 text-white px-4 py-2 rounded-lg hover:bg-blue-600 flex items-center justify-center space-x-2" > <Plus className="w-4 h-4" /> <span>New Chat</span> </button> <p className="text-xs text-gray-500 mt-2 text-center"> 🔗 Synced across devices • User: {userId.slice(-8)} </p> </div>
{/* Conversation List */} <div className="flex-1 overflow-y-auto"> {conversations.map(conversation => ( <div key={conversation.id} className={`p-3 border-b border-gray-100 cursor-pointer hover:bg-gray-50 ${ currentConversationId === conversation.id ? 'bg-blue-50 border-l-4 border-l-blue-500' : '' }`} onClick={() => loadConversationById(conversation.id)} > <div className="flex items-start justify-between"> <div className="flex-1 min-w-0"> <h3 className="text-sm font-medium text-gray-800 truncate"> {conversation.title || 'New Conversation'} </h3> <div className="flex items-center space-x-2 mt-1"> <span className="text-xs text-gray-500"> {conversation.messageCount} messages </span> {conversation.summary && ( <span className="text-xs bg-green-100 text-green-600 px-2 py-0.5 rounded"> 📋 Summary </span> )} <span className="text-xs bg-blue-100 text-blue-600 px-2 py-0.5 rounded"> {conversation.conversationType} </span> </div> <p className="text-xs text-gray-400 mt-1"> {new Date(conversation.updatedAt).toLocaleDateString()} </p> </div> <button onClick={(e) => { e.stopPropagation() deleteConversationById(conversation.id) }} className="text-gray-400 hover:text-red-500 ml-2" > <Trash2 className="w-4 h-4" /> </button> </div> </div> ))}
{conversations.length === 0 && ( <div className="p-6 text-center text-gray-500"> <MessageSquare className="w-8 h-8 mx-auto mb-2 text-gray-300" /> <p className="text-sm">No conversations yet</p> <p className="text-xs">Start a new chat to begin</p> </div> )} </div> </div> )}
{/* Main Chat Area */} <div className="flex-1 flex flex-col"> <div className="bg-white rounded-lg shadow-lg h-full flex flex-col"> {/* Chat Header */} <div className="bg-blue-500 text-white p-4 rounded-t-lg"> <div className="flex justify-between items-start"> <div className="flex items-center space-x-3"> {!showSidebar && ( <button onClick={() => setShowSidebar(true)} className="text-blue-100 hover:text-white" > <MessageSquare className="w-5 h-5" /> </button> )} <div> <h1 className="text-xl font-bold">Summary Memory + Database</h1> <p className="text-blue-100 text-sm"> Smart conversation memory with PostgreSQL storage </p> </div> </div>
<div className="text-right space-y-2"> <div> <label className="block text-xs text-blue-100">Recent: {recentWindowSize}</label> <input type="range" min="5" max="30" value={recentWindowSize} onChange={(e) => setRecentWindowSize(parseInt(e.target.value))} className="w-20" disabled={isStreaming} /> </div> <div> <label className="block text-xs text-blue-100">Summary at: {summaryThreshold}</label> <input type="range" min="15" max="50" value={summaryThreshold} onChange={(e) => setSummaryThreshold(parseInt(e.target.value))} className="w-20" disabled={isStreaming} /> </div> </div> </div> </div>
{/* Memory Status Dashboard */} <div className="bg-gray-50 px-4 py-3 border-b"> {(() => { const { totalMessages, recentMessages, summarizedMessages } = getMemoryStats();
return ( <div className="space-y-2"> <div className="flex justify-between items-center text-sm"> <div className="flex space-x-4 text-gray-600"> <span>📊 Total: {totalMessages}</span> <span>🔥 Recent: {recentMessages}</span> {summarizedMessages > 0 && ( <span>📝 Summarized: {summarizedMessages}</span> )} <span className="text-blue-600">🧠 {conversationType}</span> </div>
<div className="flex items-center space-x-2 text-xs"> {summary && ( <span className="text-green-600">✅ Summary Active</span> )} {isCreatingSummary && ( <span className="text-blue-600">🔄 Creating Summary...</span> )} <span className="text-purple-600"> 🗄️ {conversations.length} in DB </span> </div> </div>
<div className="w-full bg-gray-200 rounded-full h-2"> <div className="bg-blue-500 h-2 rounded-full transition-all duration-300" style={{ width: `${Math.min(100, (totalMessages / 50) * 100)}%` }} /> </div> </div> ); })()} </div>
{/* Active Summary Display */} {summary && ( <div className="bg-blue-50 border-l-4 border-blue-400 p-3 mx-4 mt-2 rounded"> <div className="flex items-start"> <span className="text-blue-600 mr-2">📋</span> <div className="flex-1"> <p className="text-xs font-medium text-blue-800 mb-1"> Active Summary ({conversationType}) • Saved to Database </p> <p className="text-xs text-blue-700 leading-relaxed"> {summary} </p> </div> </div> </div> )}
{/* Messages */} <div className="flex-1 overflow-y-auto p-4 space-y-4"> {messages.length === 0 && ( <div className="text-center text-gray-500 mt-20"> <Bot className="w-12 h-12 mx-auto mb-4 text-gray-400" /> <p>Send a message to see Summary Memory + Database in action!</p> {conversations.length > 0 && ( <p className="text-sm mt-2 text-blue-600"> 🗄️ Select a conversation from the sidebar or start a new one </p> )} </div> )}
{messages.map((message) => ( <div key={message.id} className={`flex items-start space-x-3 ${ message.isUser ? 'justify-end' : 'justify-start' }`} > {!message.isUser && ( <div className="bg-blue-500 p-2 rounded-full"> <Bot className="w-4 h-4 text-white" /> </div> )}
<div className={`max-w-xs lg:max-w-md px-4 py-2 rounded-lg ${ message.isUser ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-800' }`} > {message.text} {message.isStreaming && ( <span className="inline-block w-2 h-4 bg-blue-500 ml-1 animate-pulse" /> )} </div>
{message.isUser && ( <div className="bg-gray-500 p-2 rounded-full"> <User className="w-4 h-4 text-white" /> </div> )} </div> ))} </div>
{/* Input */} <div className="border-t p-4"> <div className="flex space-x-2"> <input type="text" value={input} onChange={(e) => setInput(e.target.value)} onKeyPress={handleKeyPress} placeholder="Type your message..." className="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" disabled={isStreaming} /> {isStreaming ? ( <button onClick={stopStreaming} className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg transition-colors" > Stop </button> ) : ( <button onClick={sendMessage} disabled={!input.trim()} className="bg-blue-500 hover:bg-blue-600 disabled:bg-gray-300 text-white p-2 rounded-lg transition-colors" > <Send className="w-5 h-5" /> </button> )} </div> </div> </div> </div> </div> )}
export default StreamingChat
🧪 Test Your Summary Memory + Database
Section titled “🧪 Test Your Summary Memory + Database”Step-by-Step Testing Guide
Section titled “Step-by-Step Testing Guide”- Start your PostgreSQL database
- Start your enhanced backend (with database operations)
- Start your frontend
- Test the complete persistent database flow:
Phase 1: Create first conversationYou: "Hi! My name is Alex and I'm a software engineer"AI: "Nice to meet you, Alex! Great to know you're in software engineering."
• Continue conversation until you have 10+ messages• Notice it auto-saves and appears in sidebar• Check Prisma Studio to see data in database
Phase 2: Test multi-user simulation• Open a new incognito window (new user)• Create conversations there• Notice each user has isolated conversations
Phase 3: Test cross-device sync• Refresh the page - conversations load from database• Each conversation should load perfectly with summaries• All Summary Memory features should work exactly as before
Phase 4: Test conversation management• Create multiple conversations• Delete conversations using trash icon• Load different conversations• See the active conversation highlighted
What to Watch For
Section titled “What to Watch For”Summary Memory Features (work exactly as before):
- ✅ Automatic summarization at conversation thresholds
- ✅ Memory optimization showing recent + summarized message counts
- ✅ Context retention - AI remembers early conversation details via summary
- ✅ Smart timing - summaries created at natural conversation breaks
New Database Features:
- ✅ Auto-save to database after every message
- ✅ Multi-user support - each user has isolated conversations
- ✅ Cross-device sync - access same conversations from any device
- ✅ Professional persistence - data survives server restarts
- ✅ Summary storage - summaries saved to database automatically
🗄️ Database Structure
Section titled “🗄️ Database Structure”What Gets Saved
Section titled “What Gets Saved”-- Users tableusers: { id: "user_1737644", email: null, name: "User", created_at: "2024-01-15T10:30:00Z"}
-- Conversations tableconversations: { id: "conv_abc123", user_id: "user_1737644", title: "Hi! My name is Alex and I'm a...", summary: "User Alex is a software engineer...", conversation_type: "technical", message_count: 25, created_at: "2024-01-15T10:30:00Z", updated_at: "2024-01-15T11:45:00Z"}
-- Messages tablemessages: { id: "msg_xyz789", conversation_id: "conv_abc123", role: "user", // or "assistant" content: "Hi! My name is Alex...", created_at: "2024-01-15T10:30:00Z"}
Multi-User Isolation
Section titled “Multi-User Isolation”// Each user sees only their conversationsconst userConversations = await getUserConversations(userId)
// Security: Users can only access their own dataconst conversation = await getConversationWithMessages(conversationId, userId)
💡 Key Differences from Local Storage
Section titled “💡 Key Differences from Local Storage”Advantages
Section titled “Advantages”- Multi-user support - Each user has isolated conversations
- Cross-device sync - Access from any device with same userId
- Scalability - Handles thousands of users and conversations
- Professional backup - Database backups and recovery
- Query capabilities - Search across conversations, analytics
Simple Migration Path
Section titled “Simple Migration Path”// If you built Local Storage first, easy to migrate:// 1. Keep the same UI and functionality// 2. Replace localStorage functions with API calls// 3. Add userId to all requests// 4. Everything else works the same!
✅ What You’ve Built
Section titled “✅ What You’ve Built”Your Summary Memory + Database system now provides:
Smart Memory Management (from Summary Memory)
Section titled “Smart Memory Management (from Summary Memory)”- ✅ Intelligent summarization - Automatic conversation compression
- ✅ Context retention - Never loses important conversation details
- ✅ Cost optimization - Up to 70% token savings in long conversations
- ✅ Background processing - Chat responses stay instant
Database Persistence (new features)
Section titled “Database Persistence (new features)”- ✅ Multi-user support - Isolated conversations per user
- ✅ Cross-device sync - Access conversations from any device
- ✅ Professional storage - PostgreSQL with proper relationships
- ✅ Automatic persistence - Messages and summaries saved automatically
- ✅ Conversation management - Create, load, delete with database operations
Production-Ready Features
Section titled “Production-Ready Features”- ✅ Security - Users can only access their own conversations
- ✅ Scalability - Handles large numbers of users and conversations
- ✅ Data integrity - ACID compliance and proper foreign keys
- ✅ Error handling - Graceful database error management
This combines three powerful systems: Summary Memory’s intelligent conversation management + PostgreSQL’s reliable persistence + multi-user architecture. Perfect for production applications that need to scale! 🗄️🧠✨
Ready for production? Add authentication (Auth0, Clerk, or custom) to replace the simple userId system, and you’ll have a complete multi-user conversational AI platform!