-
Notifications
You must be signed in to change notification settings - Fork 13
feat: Persistent AlgoBot chat history with localStorage + clear button (closes #14) #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||
| import { useState, useRef, useEffect } from 'react'; | ||||||||||||||||||||||||||||||||||||||
| import { motion, AnimatePresence } from 'framer-motion'; | ||||||||||||||||||||||||||||||||||||||
| import { X, Send, LogIn, ChevronDown, Maximize2, Minimize2, Sparkles, Bot } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||
| import { X, Send, LogIn, ChevronDown, Maximize2, Minimize2, Sparkles, Bot, Trash2 } from 'lucide-react'; | ||||||||||||||||||||||||||||||||||||||
| import { useAuth } from '@/contexts/AuthContext'; | ||||||||||||||||||||||||||||||||||||||
| import { sendChatMessage } from '@/api/chat'; | ||||||||||||||||||||||||||||||||||||||
| import type { ChatMessage } from '@/api/chat'; | ||||||||||||||||||||||||||||||||||||||
| import { useAnimatedText } from '@/components/ui/animated-text'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const STORAGE_KEY = 'algobot_history'; | ||||||||||||||||||||||||||||||||||||||
| const MAX_MESSAGES = 20; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const WELCOME_MESSAGE: ChatMessage = { | ||||||||||||||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||||||||||||||
| content: | ||||||||||||||||||||||||||||||||||||||
| "Hey there! I'm **AlgoBot** — your personal DSA tutor 🤖\n\nI can:\n- 📖 **Teach** any DSA concept step-by-step\n- 🎯 **Suggest problems** from AlgoForge to practice\n- 🐛 **Help debug** your approach\n\nTry one of the quick actions below, or ask me anything!", | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function loadHistory(): ChatMessage[] | null { | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| const raw = localStorage.getItem(STORAGE_KEY); | ||||||||||||||||||||||||||||||||||||||
| if (!raw) return null; | ||||||||||||||||||||||||||||||||||||||
| const parsed = JSON.parse(raw); | ||||||||||||||||||||||||||||||||||||||
| if (Array.isArray(parsed) && parsed.length > 0) return parsed; | ||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| function saveHistory(messages: ChatMessage[]) { | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| // Skip the welcome message (index 0) — only persist conversation | ||||||||||||||||||||||||||||||||||||||
| const toStore = messages.slice(1).slice(-MAX_MESSAGES); | ||||||||||||||||||||||||||||||||||||||
| localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore)); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+30
to
+34
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Empty history gets written back right after “Clear Chat”. Because Suggested fix function saveHistory(messages: ChatMessage[]) {
try {
// Skip the welcome message (index 0) — only persist conversation
const toStore = messages.slice(1).slice(-MAX_MESSAGES);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
+ if (toStore.length === 0) {
+ localStorage.removeItem(STORAGE_KEY);
+ return;
+ }
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
} catch {
// Private browsing or quota exceeded — silently ignore
}
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||
| // Private browsing or quota exceeded — silently ignore | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| /* ─── Sparkle Star Icon (Leonardo AI style) ─── */ | ||||||||||||||||||||||||||||||||||||||
| function SparkleIcon({ size = 40 }: { size?: number }) { | ||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -42,13 +73,10 @@ export function AlgoBot({ onAuthClick }: { onAuthClick: (mode: 'login' | 'signup | |||||||||||||||||||||||||||||||||||||
| const { user } = useAuth(); | ||||||||||||||||||||||||||||||||||||||
| const [isOpen, setIsOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||
| const [isExpanded, setIsExpanded] = useState(false); | ||||||||||||||||||||||||||||||||||||||
| const [messages, setMessages] = useState<ChatMessage[]>([ | ||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||
| role: 'assistant', | ||||||||||||||||||||||||||||||||||||||
| content: | ||||||||||||||||||||||||||||||||||||||
| "Hey there! I'm **AlgoBot** — your personal DSA tutor 🤖\n\nI can:\n- 📖 **Teach** any DSA concept step-by-step\n- 🎯 **Suggest problems** from AlgoForge to practice\n- 🐛 **Help debug** your approach\n\nTry one of the quick actions below, or ask me anything!", | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| ]); | ||||||||||||||||||||||||||||||||||||||
| const [messages, setMessages] = useState<ChatMessage[]>(() => { | ||||||||||||||||||||||||||||||||||||||
| const history = loadHistory(); | ||||||||||||||||||||||||||||||||||||||
| return history ? [WELCOME_MESSAGE, ...history] : [WELCOME_MESSAGE]; | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
| const [input, setInput] = useState(''); | ||||||||||||||||||||||||||||||||||||||
| const [isTyping, setIsTyping] = useState(false); | ||||||||||||||||||||||||||||||||||||||
| const [showScrollDown, setShowScrollDown] = useState(false); | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -78,6 +106,17 @@ export function AlgoBot({ onAuthClick }: { onAuthClick: (mode: 'login' | 'signup | |||||||||||||||||||||||||||||||||||||
| return () => el.removeEventListener('scroll', handler); | ||||||||||||||||||||||||||||||||||||||
| }, [isOpen]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| // Persist conversation to localStorage whenever messages change | ||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||
| saveHistory(messages); | ||||||||||||||||||||||||||||||||||||||
| }, [messages]); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const handleClearChat = () => { | ||||||||||||||||||||||||||||||||||||||
| try { localStorage.removeItem(STORAGE_KEY); } catch { /* ignore */ } | ||||||||||||||||||||||||||||||||||||||
| setMessages([WELCOME_MESSAGE]); | ||||||||||||||||||||||||||||||||||||||
| setLatestAiIndex(0); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| const scrollToBottom = () => { | ||||||||||||||||||||||||||||||||||||||
| messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -271,6 +310,16 @@ export function AlgoBot({ onAuthClick }: { onAuthClick: (mode: 'login' | 'signup | |||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-0.5"> | ||||||||||||||||||||||||||||||||||||||
| {hasStartedConversation && ( | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| onClick={handleClearChat} | ||||||||||||||||||||||||||||||||||||||
| className="p-2 rounded-lg hover:bg-white/5 transition-colors group" | ||||||||||||||||||||||||||||||||||||||
| title="Clear Chat" | ||||||||||||||||||||||||||||||||||||||
| aria-label="Clear chat history" | ||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||
| <Trash2 className="w-4 h-4 text-white/30 group-hover:text-red-400/70 transition-colors" /> | ||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||||||||||
| <button | ||||||||||||||||||||||||||||||||||||||
| onClick={() => setIsExpanded(!isExpanded)} | ||||||||||||||||||||||||||||||||||||||
| className="p-2 rounded-lg hover:bg-white/5 transition-colors group" | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Validate and cap restored history before using it.
On Lines 22-23, any non-empty array from
localStorageis accepted asChatMessage[]. A stale/corrupt entry like{ content: null }will makerenderContent(msg.content)throw on first render, and malformed items also get forwarded to/api/chat. Filter by the runtimeChatMessageshape here and applyslice(-MAX_MESSAGES)on load too.Suggested fix
function loadHistory(): ChatMessage[] | null { try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw); - if (Array.isArray(parsed) && parsed.length > 0) return parsed; - return null; + if (!Array.isArray(parsed)) return null; + + const history = parsed + .filter( + (msg): msg is ChatMessage => + !!msg && + (msg.role === 'user' || msg.role === 'assistant') && + typeof msg.content === 'string' + ) + .slice(-MAX_MESSAGES); + + return history.length > 0 ? history : null; } catch { return null; } }📝 Committable suggestion
🤖 Prompt for AI Agents