Skip to content

🗄️ Database Implementation

Building on: This route enhances your existing Summary Memory implementation. Make sure you have Summary Memory working before starting here.


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
// Perfect for production applications
const 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 Storage
const advantages = {
users: 'Multiple users with isolated conversations',
devices: 'Access from any device',
backup: 'Professional database backups',
search: 'Query conversations across users'
}

Add to your backend folder:

Terminal window
npm install pg prisma @prisma/client
npm install -D prisma
Terminal window
npx prisma init

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")
}

Update your .env file:

OPENAI_API_KEY=your_api_key_here
PORT=8000
DATABASE_URL="postgresql://username:password@localhost:5432/ai_chat_db"
Terminal window
# Generate Prisma client
npx prisma generate
# Create and apply migrations
npx prisma db push
# Optional: View data in Prisma Studio
npx prisma studio

Create db/operations.js in your backend:

import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
// 🆕 DATABASE: Simple database operations
export 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 storage
app.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 endpoints
app.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 summary
app.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

  1. Start your PostgreSQL database
  2. Start your enhanced backend (with database operations)
  3. Start your frontend
  4. Test the complete persistent database flow:
Phase 1: Create first conversation
You: "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

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

-- Users table
users: {
id: "user_1737644",
email: null,
name: "User",
created_at: "2024-01-15T10:30:00Z"
}
-- Conversations table
conversations: {
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 table
messages: {
id: "msg_xyz789",
conversation_id: "conv_abc123",
role: "user", // or "assistant"
content: "Hi! My name is Alex...",
created_at: "2024-01-15T10:30:00Z"
}
// Each user sees only their conversations
const userConversations = await getUserConversations(userId)
// Security: Users can only access their own data
const conversation = await getConversationWithMessages(conversationId, userId)

  • 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
// 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!

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
  • 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
  • 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!