Skip to content

📱 Local Storage Implementation

Transform your Summary Memory chat to remember conversations across browser sessions using localStorage. This route requires zero backend changes and gets you persistent memory in just 15 minutes.

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 browser storage after every message
  • Auto-restore chats when page loads, including summaries
  • Conversation sidebar with saved conversation list
  • Simple conversation management (new, load, clear)
// Perfect combination for learning and demos
const benefits = {
setup: '15 minutes, zero backend changes',
features: 'Smart summarization + persistent storage',
privacy: 'Data never leaves user device',
cost: 'Completely free'
}
// Understand the limitations
const limitations = {
devices: 'Single device/browser only',
storage: '5MB browser limit',
sharing: 'No multi-user support'
}

🛠️ Step 1: Add Simple Storage Functions

Section titled “🛠️ Step 1: Add Simple Storage Functions”

Add these functions to your Summary Memory StreamingChat.jsx component:

import { useState, useRef, useEffect } from 'react'
import { Send, Bot, User, Trash2, Plus, MessageSquare } from 'lucide-react'
// 🆕 LOCAL STORAGE: Simple storage functions
const STORAGE_KEY = 'ai-conversations'
const saveConversation = (conversationId, messages, summary, conversationType) => {
const conversations = getAllConversations()
const conversationData = {
id: conversationId,
messages: messages.filter(msg => !msg.isStreaming),
summary,
conversationType,
title: generateTitle(messages),
timestamp: new Date().toISOString(),
messageCount: messages.length
}
conversations[conversationId] = conversationData
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(conversations))
console.log('💾 Conversation saved to localStorage')
} catch (error) {
console.error('Failed to save:', error)
}
}
const getAllConversations = () => {
try {
const stored = localStorage.getItem(STORAGE_KEY)
return stored ? JSON.parse(stored) : {}
} catch (error) {
console.error('Failed to load conversations:', error)
return {}
}
}
const loadConversation = (conversationId) => {
const conversations = getAllConversations()
return conversations[conversationId] || null
}
const deleteConversation = (conversationId) => {
const conversations = getAllConversations()
delete conversations[conversationId]
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(conversations))
console.log('🗑️ Conversation deleted')
} catch (error) {
console.error('Failed to delete conversation:', error)
}
}
const generateTitle = (messages) => {
if (!messages || messages.length === 0) return 'New Conversation'
const firstUserMessage = messages.find(msg => msg.isUser)
if (firstUserMessage) {
return firstUserMessage.text.length > 30
? firstUserMessage.text.substring(0, 30) + '...'
: firstUserMessage.text
}
return 'New Conversation'
}
const generateId = () => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
}

🔄 Step 2: Add Storage to Your Summary Memory Component

Section titled “🔄 Step 2: Add Storage to Your Summary Memory Component”

Update your Summary Memory component to include localStorage and conversation management:

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')
// 🆕 LOCAL STORAGE: Conversation management
const [currentConversationId, setCurrentConversationId] = useState(null)
const [conversations, setConversations] = useState({})
const [showSidebar, setShowSidebar] = useState(true)
// 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('http://localhost:8000/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);
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 }
};
// 🆕 LOCAL STORAGE: Load conversations on startup
useEffect(() => {
const allConversations = getAllConversations()
setConversations(allConversations)
// Load the most recent conversation if exists
const conversationIds = Object.keys(allConversations)
if (conversationIds.length > 0) {
const mostRecent = conversationIds
.map(id => allConversations[id])
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))[0]
if (mostRecent) {
loadConversationById(mostRecent.id)
}
}
}, [])
// 🆕 LOCAL STORAGE: Auto-save when conversation changes
useEffect(() => {
if (currentConversationId && messages.length > 0) {
saveConversation(currentConversationId, messages, summary, conversationType)
// Refresh conversation list
setConversations(getAllConversations())
}
}, [messages, summary, conversationType, currentConversationId])
// 🆕 LOCAL STORAGE: Conversation management functions
const startNewConversation = () => {
setCurrentConversationId(generateId())
setMessages([])
setSummary(null)
setConversationType('general')
}
const loadConversationById = (conversationId) => {
const conversation = loadConversation(conversationId)
if (conversation) {
setCurrentConversationId(conversationId)
setMessages(conversation.messages || [])
setSummary(conversation.summary || null)
setConversationType(conversation.conversationType || 'general')
console.log(`✅ Loaded conversation: ${conversation.title}`)
}
}
const deleteConversationById = (conversationId) => {
deleteConversation(conversationId)
setConversations(getAllConversations())
if (currentConversationId === conversationId) {
startNewConversation()
}
}
// Keep your existing sendMessage function with Summary Memory logic
const sendMessage = async () => {
if (!input.trim() || isStreaming) return
// Create new conversation if none exists
if (!currentConversationId) {
setCurrentConversationId(generateId())
}
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()
const response = await fetch('http://localhost:8000/api/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: currentInput,
conversationHistory: conversationHistory,
summary: summary,
recentWindowSize: recentWindowSize
}),
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
)
)
} 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">
{/* 🆕 LOCAL STORAGE: Conversation Sidebar */}
{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>
</div>
{/* Conversation List */}
<div className="flex-1 overflow-y-auto">
{Object.values(conversations)
.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp))
.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}
</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.timestamp).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>
))
}
{Object.keys(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 + Local Storage</h1>
<p className="text-blue-100 text-sm">
Smart conversation memory with persistent 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">
💾 {Object.keys(conversations).length} saved
</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}) • Auto-saved
</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 + Local Storage in action!</p>
{Object.keys(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 + Local Storage

Section titled “🧪 Test Your Summary Memory + Local Storage”
  1. Start your Summary Memory backend and frontend
  2. Test the complete persistent conversation 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
Phase 2: Create second conversation
• Click "New Chat" button
• Start a different conversation about cooking
• Watch both conversations appear in sidebar
Phase 3: Test persistence
• Refresh the page
• All conversations should load in sidebar
• Click any conversation to load it perfectly
• Summaries and settings should restore
Phase 4: Test conversation management
• Delete a conversation 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 Local Storage Features:

  • Auto-save conversations with immediate visual feedback in sidebar
  • Conversation list with titles, message counts, and summary indicators
  • Load any conversation instantly with full state restoration
  • Simple management - create new, delete old conversations

// What gets saved to localStorage:
const conversations = {
"conv_123": {
id: "conv_123",
title: "Hi! My name is Alex and I'm a...",
messages: [...], // All messages
summary: "User Alex is a software engineer...",
conversationType: "technical",
messageCount: 25,
timestamp: "2024-01-15T10:30:00Z"
},
"conv_456": {
id: "conv_456",
title: "What's the best way to cook pasta?",
messages: [...],
summary: null, // No summary yet
conversationType: "general",
messageCount: 8,
timestamp: "2024-01-15T11:15:00Z"
}
}
// Every time messages change:
useEffect(() => {
if (currentConversationId && messages.length > 0) {
saveConversation(currentConversationId, messages, summary, conversationType)
setConversations(getAllConversations()) // Refresh sidebar
}
}, [messages, summary, conversationType])

Your Summary Memory + Local Storage 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
  • Auto-save conversations - Every message automatically saved
  • Conversation sidebar - Visual list of all saved conversations
  • Complete restoration - Messages, summaries, and settings preserved
  • Simple management - Create new, load existing, delete old conversations
  • Smart titles - Auto-generated from first user message
  • Visual indicators - Shows message count, summary status, conversation type
  • Responsive design - Collapsible sidebar for different screen sizes
  • Error handling - Graceful fallbacks for storage issues

This combines the best of both worlds: Summary Memory’s intelligent conversation management with simple, reliable local storage. Perfect for learning, demos, and applications where users want complete data control! 💾🧠✨

Ready for production? When you’re ready to scale to multi-user applications, the Database Storage route will build on these same principles with server-side storage and user accounts.