Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 57 additions & 8 deletions app/src/components/custom/AlgoBot.tsx
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;
Comment on lines +18 to +23

Copy link
Copy Markdown
Contributor

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 localStorage is accepted as ChatMessage[]. A stale/corrupt entry like { content: null } will make renderContent(msg.content) throw on first render, and malformed items also get forwarded to /api/chat. Filter by the runtime ChatMessage shape here and apply slice(-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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
function loadHistory(): ChatMessage[] | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const parsed = JSON.parse(raw);
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;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/custom/AlgoBot.tsx` around lines 18 - 23, The restored
chat history in loadHistory() is currently accepted as any non-empty array,
which can let malformed items through and cause renderContent(msg.content) or
/api/chat usage to fail. Update loadHistory() in AlgoBot.tsx to validate each
item against the runtime ChatMessage shape before returning it, drop invalid
entries, and cap the recovered list with slice(-MAX_MESSAGES) so stale oversized
history is trimmed on load.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 saveHistory() always calls setItem, clearing the chat removes the key and then immediately recreates it as [] when the effect reruns. The UI resets correctly, but the persisted history is not actually cleared.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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));
function saveHistory(messages: ChatMessage[]) {
try {
// Skip the welcome message (index 0) — only persist conversation
const toStore = messages.slice(1).slice(-MAX_MESSAGES);
if (toStore.length === 0) {
localStorage.removeItem(STORAGE_KEY);
return;
}
localStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
} catch {
// Private browsing or quota exceeded — silently ignore
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/src/components/custom/AlgoBot.tsx` around lines 30 - 34, The
saveHistory() flow in AlgoBot is re-persisting an empty array after “Clear Chat”
because it always writes to localStorage even when there are no conversation
messages to save. Update saveHistory(messages) so it skips setItem when the
sliced toStore list is empty (and ideally removes the STORAGE_KEY instead),
keeping the clear action truly empty. Use the existing saveHistory, STORAGE_KEY,
and localStorage call site to make the fix without affecting normal message
persistence.

} catch {
// Private browsing or quota exceeded — silently ignore
}
}

/* ─── Sparkle Star Icon (Leonardo AI style) ─── */
function SparkleIcon({ size = 40 }: { size?: number }) {
return (
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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' });
};
Expand Down Expand Up @@ -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"
Expand Down
Loading