From 4e85fa4b17962de0c23bdaad55d67419475bd691 Mon Sep 17 00:00:00 2001 From: Sujan Kumar MV Date: Sun, 16 Nov 2025 13:46:19 +0530 Subject: [PATCH 1/3] Fix: closed issues #146 #82 #137 #135 #138 #77 #78 #79 #74 --- .vscode/settings.json | 10 + app/api/generate/route.ts | 2 +- components/dijkstra-gpt.tsx | 909 ++++++++++++++++++++---------------- package-lock.json | 7 +- package.json | 1 + 5 files changed, 533 insertions(+), 396 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..639aac5 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "cSpell.words": [ + "dataforge", + "genai", + "Leetcode", + "msword", + "officedocument", + "wordprocessingml" + ] +} \ No newline at end of file diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index a8f0a55..400cf4a 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -84,7 +84,7 @@ export async function POST(request: Request): Promise { const prompt = typeof bRec["prompt"] === "string" ? bRec["prompt"] : null; const model = typeof bRec["model"] === "string" ? bRec["model"] - : "gemini-2.0-flash-exp"; + : "gemini-2.0-flash-lite-preview"; // Validate prompt if (!prompt) { diff --git a/components/dijkstra-gpt.tsx b/components/dijkstra-gpt.tsx index 7f46d70..7e39213 100644 --- a/components/dijkstra-gpt.tsx +++ b/components/dijkstra-gpt.tsx @@ -1,7 +1,6 @@ -"use client"; + "use client"; -import React from "react"; -import { useState, useRef, useEffect } from "react"; +import React, { useState, useRef, useEffect } from "react"; // UI Components from shadcn/ui library import { Button } from "@/components/ui/button"; @@ -9,9 +8,19 @@ import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Card } from "@/components/ui/card"; -import { Separator } from "@/components/ui/separator"; -// Icon library (removed voice & enhance icons) +// Markdown rendering +import ReactMarkdown from "react-markdown"; +import remarkGfm from "remark-gfm"; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +// Icon library import { Paperclip, Image, @@ -21,14 +30,15 @@ import { Copy, RefreshCw, Share2, - MessageSquare, Trash2, ChevronLeft, - ChevronRight, Search, - Calendar, Check, Download, + MessageSquare, + Plus, + MoreVertical, + Edit3, } from "lucide-react"; // API client for Gemini AI communication @@ -40,7 +50,6 @@ import { toast } from "sonner"; // ============================================ // TYPE DEFINITIONS // ============================================ - type Message = { id: string; role: "user" | "assistant"; @@ -60,12 +69,10 @@ type ChatSession = { // ============================================ // MAIN COMPONENT // ============================================ - export default function DijkstraGPT() { - // ============================================ + // ========================================== // STATE MANAGEMENT - // ============================================ - + // ========================================== const [prompt, setPrompt] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -74,30 +81,31 @@ export default function DijkstraGPT() { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [copiedMessageId, setCopiedMessageId] = useState(null); - const [apiStatus, setApiStatus] = useState<'checking' | 'active' | 'inactive'>('checking'); + const [apiStatus, setApiStatus] = useState<"checking" | "active" | "inactive">( + "checking" + ); const [hasMessages, setHasMessages] = useState(false); + const [editingSessionId, setEditingSessionId] = useState(null); + const [editingTitle, setEditingTitle] = useState(""); - // ============================================ - // REFS FOR DOM ELEMENTS - // ============================================ - + // ========================================== + // REFS + // ========================================== const textareaRef = useRef(null); const fileInputRef = useRef(null); const messagesEndRef = useRef(null); - + const cancelGenerationRef = useRef(false); - // ============================================ - // COMPUTED VALUES - // ============================================ - - const currentSession = chatSessions.find((session) => session.id === currentSessionId); + // ========================================== + // COMPUTED + // ========================================== + const currentSession = chatSessions.find((s) => s.id === currentSessionId); const messages = currentSession?.messages || []; - // ============================================ + // ========================================== // EFFECTS - // ============================================ - - // Auto-resize textarea based on content + // ========================================== + // Auto-resize textarea useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; @@ -105,34 +113,30 @@ export default function DijkstraGPT() { } }, [prompt]); - // Auto-scroll to bottom when new messages arrive (scrolling parent container) + // Scroll to bottom when messages change useEffect(() => { if (messagesEndRef.current && hasMessages) { - // Find the scrollable parent container from page.tsx - let scrollContainer = messagesEndRef.current.closest('.overflow-y-auto'); + let scrollContainer = messagesEndRef.current.closest(".overflow-y-auto"); if (!scrollContainer) { - // Try to find the parent scroll container by traversing up let parent = messagesEndRef.current.parentElement; while (parent && parent !== document.body) { const style = window.getComputedStyle(parent); - if (style.overflowY === 'auto' || style.overflowY === 'scroll') { + if (style.overflowY === "auto" || style.overflowY === "scroll") { scrollContainer = parent; break; } parent = parent.parentElement; } } - + if (scrollContainer) { - // Use setTimeout to ensure DOM is updated setTimeout(() => { scrollContainer?.scrollTo({ top: scrollContainer.scrollHeight, - behavior: "smooth" + behavior: "smooth", }); }, 100); } else { - // Fallback to direct scrollIntoView setTimeout(() => { messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, 100); @@ -140,15 +144,14 @@ export default function DijkstraGPT() { } }, [messages, isLoading, hasMessages]); - // Track if there are messages to determine layout state - // Note: hasMessages is set immediately on submit, so we only update if it's false and messages exist + // Track if there are messages useEffect(() => { if (!hasMessages && messages.length > 0) { setHasMessages(true); } }, [messages, hasMessages]); - // Initialize with a default session on mount + // Create initial empty session on mount useEffect(() => { const initialSession: ChatSession = { id: Date.now().toString(), @@ -157,15 +160,18 @@ export default function DijkstraGPT() { createdAt: new Date(), updatedAt: new Date(), }; - setChatSessions([initialSession]); setCurrentSessionId(initialSession.id); - }, []); + }, []); - // ============================================ - // SESSION MANAGEMENT FUNCTIONS - // ============================================ + // Check API status on mount + useEffect(() => { + checkApiStatus(); + }, []); + // ========================================== + // SESSION MANAGEMENT + // ========================================== const createNewChat = (): void => { const newSession: ChatSession = { id: Date.now().toString(), @@ -179,6 +185,8 @@ export default function DijkstraGPT() { setCurrentSessionId(newSession.id); setPrompt(""); setUploadedFiles([]); + setIsLoading(false); + cancelGenerationRef.current = false; toast.success("New chat created"); }; @@ -188,7 +196,9 @@ export default function DijkstraGPT() { if (session.id === sessionId && session.title === "New Chat") { return { ...session, - title: firstMessage.slice(0, 50) + (firstMessage.length > 50 ? "..." : ""), + title: + firstMessage.slice(0, 50) + + (firstMessage.length > 50 ? "..." : ""), updatedAt: new Date(), }; } @@ -197,12 +207,9 @@ export default function DijkstraGPT() { ); }; - // Check API status on mount - useEffect(() => { - checkApiStatus(); - }, []); - const deleteSession = (sessionId: string): void => { + if (!window.confirm("Are you sure you want to delete this chat?")) return; + setChatSessions((prev) => prev.filter((s) => s.id !== sessionId)); if (currentSessionId === sessionId) { @@ -227,6 +234,53 @@ export default function DijkstraGPT() { toast.success("Chat downloaded"); }; + const renameSession = (sessionId: string): void => { + const session = chatSessions.find((s) => s.id === sessionId); + if (session) { + setEditingSessionId(sessionId); + setEditingTitle(session.title); + } + }; + + const saveRename = (): void => { + if (editingSessionId && editingTitle.trim()) { + setChatSessions((prev) => + prev.map((session) => + session.id === editingSessionId + ? { ...session, title: editingTitle.trim(), updatedAt: new Date() } + : session + ) + ); + toast.success("Chat renamed"); + } + setEditingSessionId(null); + setEditingTitle(""); + }; + + const shareSession = async (sessionId: string): Promise => { + const session = chatSessions.find((s) => s.id === sessionId); + if (!session) return; + + const shareText = `${session.title}\n\n${session.messages + .map((m) => `${m.role === "user" ? "You" : "Assistant"}: ${m.content}`) + .join("\n\n")}`; + + try { + if (navigator.share) { + await navigator.share({ + title: session.title, + text: shareText, + }); + toast.success("Chat shared"); + } else { + await navigator.clipboard.writeText(shareText); + toast.success("Chat copied to clipboard"); + } + } catch (error) { + console.log("Share cancelled or failed:", error); + } + }; + const addMessage = (message: Message): void => { setChatSessions((prev) => prev.map((session) => { @@ -241,30 +295,29 @@ export default function DijkstraGPT() { }) ); }; - // ============================================ - // API STATUS CHECK - // ============================================ + // ========================================== + // API STATUS CHECK + // ========================================== const checkApiStatus = async (): Promise => { try { const response = await callGemini("test"); if (response) { - setApiStatus('active'); - console.log('✅ API key is active'); + setApiStatus("active"); + console.log("✅ API key is active"); } else { - setApiStatus('inactive'); - console.log('❌ API key is not configured or invalid'); + setApiStatus("inactive"); + console.log("❌ API key is not configured or invalid"); } } catch (error) { - setApiStatus('inactive'); - console.error('❌ API check failed:', error); + setApiStatus("inactive"); + console.error("❌ API check failed:", error); } }; - // ============================================ - // FILE HANDLING FUNCTIONS - // ============================================ - + // ========================================== + // FILE HANDLING + // ========================================== const handleFileUpload = (event: React.ChangeEvent): void => { const files = Array.from(event.target.files || []); @@ -279,7 +332,9 @@ export default function DijkstraGPT() { ]; const validFiles = files.filter((file) => { - const isValid = validTypes.includes(file.type) || file.name.match(/\.(jpg|jpeg|png|pdf|txt|doc|docx)$/i); + const isValid = + validTypes.includes(file.type) || + file.name.match(/\.(jpg|jpeg|png|pdf|txt|doc|docx)$/i); if (!isValid) { toast.error(`File type not supported: ${file.name}`); @@ -298,10 +353,9 @@ export default function DijkstraGPT() { setUploadedFiles((prev) => prev.filter((_, i) => i !== index)); }; - // ============================================ - // MESSAGE ACTION FUNCTIONS - // ============================================ - + // ========================================== + // MESSAGE ACTIONS + // ========================================== const copyMessage = async (content: string, messageId: string): Promise => { try { await navigator.clipboard.writeText(content); @@ -314,28 +368,38 @@ export default function DijkstraGPT() { } }; - // Helper to update assistant message content progressively (streaming) - const updateAssistantContent = (sessionId: string | null, messageId: string, newContent: string) => { + const updateAssistantContent = ( + sessionId: string | null, + messageId: string, + newContent: string + ) => { if (!sessionId) return; setChatSessions((prev) => prev.map((session) => { if (session.id !== sessionId) return session; return { ...session, - messages: session.messages.map((m) => (m.id === messageId ? { ...m, content: newContent } : m)), + messages: session.messages.map((m) => + m.id === messageId ? { ...m, content: newContent } : m + ), updatedAt: new Date(), }; }) ); }; - // Try to handle streaming from callGemini. If callGemini returns a Response-like object with a body stream, - // we read it. Otherwise, if it returns a string, we simulate a typewriter streaming effect. - const streamGeminiResponse = async (promptText: string, sessionId: string, assistantMessageId: string) => { + // ========================================== + // STREAMING WITH CANCEL SUPPORT + // ========================================== + const streamGeminiResponse = async ( + promptText: string, + sessionId: string, + assistantMessageId: string + ) => { try { const res: any = await callGemini(promptText); - // If it's a fetch Response-like object with a ReadableStream body + // ReadableStream case if (res && res.body && typeof res.body.getReader === "function") { const reader = res.body.getReader(); const decoder = new TextDecoder(); @@ -343,7 +407,15 @@ export default function DijkstraGPT() { let accumulated = ""; while (!done) { - // eslint-disable-next-line no-await-in-loop + if (cancelGenerationRef.current) { + updateAssistantContent( + sessionId, + assistantMessageId, + accumulated || "⚠ Generation stopped by user." + ); + return accumulated; + } + const { value, done: d } = await reader.read(); if (value) { accumulated += decoder.decode(value, { stream: true }); @@ -352,34 +424,45 @@ export default function DijkstraGPT() { done = !!d; } - // finalize (in case any leftovers) updateAssistantContent(sessionId, assistantMessageId, accumulated); return accumulated; } - // If callGemini returned a string, simulate typing + // Plain string case if (typeof res === "string") { const full = res; let i = 0; - // A small delay between characters produces a smooth typing effect. Adjust as needed. while (i <= full.length) { - // eslint-disable-next-line no-await-in-loop + if (cancelGenerationRef.current) { + updateAssistantContent( + sessionId, + assistantMessageId, + full.slice(0, i) || "⚠ Generation stopped by user." + ); + return full.slice(0, i); + } await new Promise((r) => setTimeout(r, 12)); - i += Math.ceil(Math.random() * 3); // append a few chars at a time to feel natural + i += Math.ceil(Math.random() * 3); const chunk = full.slice(0, i); updateAssistantContent(sessionId, assistantMessageId, chunk); } - updateAssistantContent(sessionId, assistantMessageId, full); return full; } - // If the response shape is unknown but contains text field + // Object with .text if (res && typeof res === "object" && typeof res.text === "string") { const text = res.text; let i = 0; while (i <= text.length) { - // eslint-disable-next-line no-await-in-loop + if (cancelGenerationRef.current) { + updateAssistantContent( + sessionId, + assistantMessageId, + text.slice(0, i) || "⚠ Generation stopped by user." + ); + return text.slice(0, i); + } await new Promise((r) => setTimeout(r, 12)); i += Math.ceil(Math.random() * 4); updateAssistantContent(sessionId, assistantMessageId, text.slice(0, i)); @@ -388,36 +471,66 @@ export default function DijkstraGPT() { return text; } - // Unknown shape -> convert to string and show const fallback = String(res); updateAssistantContent(sessionId, assistantMessageId, fallback); return fallback; } catch (error) { + if (cancelGenerationRef.current) { + updateAssistantContent( + sessionId, + assistantMessageId, + "⚠ Generation stopped by user." + ); + return; + } const errStr = "⚠ Error: " + String(error); updateAssistantContent(sessionId, assistantMessageId, errStr); throw error; } }; - const regenerateMessage = async (userMessage: Message): Promise => { - setIsLoading(true); + // Regenerate: delete old assistant then stream new + const handleRegenerate = async (assistantMessageId: string): Promise => { + const session = chatSessions.find((s) => s.id === currentSessionId); + if (!session) return; - try { - // create placeholder assistant message - const assistantMessage: Message = { - id: (Date.now() + 2).toString(), - role: "assistant", - content: "", - timestamp: new Date(), - }; + const idx = session.messages.findIndex((m) => m.id === assistantMessageId); + if (idx <= 0) return; - addMessage(assistantMessage); + const userMsg = session.messages[idx - 1]; + if (!userMsg || userMsg.role !== "user") return; - await streamGeminiResponse(userMessage.content, currentSessionId!, assistantMessage.id); + // Remove old assistant message + setChatSessions((prev) => + prev.map((s) => + s.id === currentSessionId + ? { ...s, messages: s.messages.filter((m) => m.id !== assistantMessageId) } + : s + ) + ); + + cancelGenerationRef.current = false; + setIsLoading(true); + const newAssistantMessage: Message = { + id: (Date.now() + 2).toString(), + role: "assistant", + content: "", + timestamp: new Date(), + }; + addMessage(newAssistantMessage); + + try { + await streamGeminiResponse( + userMsg.content, + currentSessionId!, + newAssistantMessage.id + ); toast.success("Response regenerated"); } catch (error) { - toast.error("Failed to regenerate: " + String(error)); + if (!cancelGenerationRef.current) { + toast.error("Failed to regenerate: " + String(error)); + } } finally { setIsLoading(false); } @@ -440,15 +553,14 @@ export default function DijkstraGPT() { } }; - // ============================================ - // INPUT HANDLING FUNCTIONS - // ============================================ - + // ========================================== + // INPUT HANDLING + // ========================================== const handleSubmit = async (): Promise => { if (!prompt.trim() && uploadedFiles.length === 0) return; - // Trigger layout transition immediately setHasMessages(true); + cancelGenerationRef.current = false; if (!currentSessionId) { createNewChat(); @@ -475,28 +587,27 @@ export default function DijkstraGPT() { updateSessionTitle(currentSessionId!, currentPrompt); } - // Create an assistant placeholder message (will be updated progressively) const assistantMessage: Message = { id: (Date.now() + 1).toString(), role: "assistant", content: "", timestamp: new Date(), }; - addMessage(assistantMessage); try { await streamGeminiResponse(currentPrompt, currentSessionId!, assistantMessage.id); } catch (error) { - const errorMessage: Message = { - id: (Date.now() + 3).toString(), - role: "assistant", - content: "⚠ Error: " + String(error), - timestamp: new Date(), - }; - // replace the placeholder content with the error - updateAssistantContent(currentSessionId, assistantMessage.id, errorMessage.content); - toast.error("Failed to get response"); + if (!cancelGenerationRef.current) { + const errorMessage: Message = { + id: (Date.now() + 3).toString(), + role: "assistant", + content: "⚠ Error: " + String(error), + timestamp: new Date(), + }; + updateAssistantContent(currentSessionId, assistantMessage.id, errorMessage.content); + toast.error("Failed to get response"); + } } finally { setIsLoading(false); } @@ -509,14 +620,22 @@ export default function DijkstraGPT() { } }; - // ============================================ - // SEARCH & FILTER - // ============================================ + const handleStopGeneration = () => { + if (!isLoading) return; + cancelGenerationRef.current = true; + setIsLoading(false); + toast.info("Generation stopped"); + }; + // ========================================== + // SEARCH & GROUPING + // ========================================== const filteredSessions = chatSessions.filter( (session) => session.title.toLowerCase().includes(searchQuery.toLowerCase()) || - session.messages.some((msg) => msg.content.toLowerCase().includes(searchQuery.toLowerCase())) + session.messages.some((msg) => + msg.content.toLowerCase().includes(searchQuery.toLowerCase()) + ) ); const groupedSessions = filteredSessions.reduce((groups, session) => { @@ -528,19 +647,26 @@ export default function DijkstraGPT() { return groups; }, {} as Record); - // ============================================ - // RENDER MESSAGE FUNCTION - // ============================================ - + // ========================================== + // RENDER MESSAGE (Markdown + thinking text) + // ========================================== const renderMessage = (msg: Message, i: number): React.JSX.Element => { const isUser = msg.role === "user"; const isCopied = copiedMessageId === msg.id; - // Split content by code blocks (```) and preserve formatting - const parts = msg.content.split(/```/g); + const isLastMessage = i === messages.length - 1; + const shouldShowThinkingPlaceholder = + !isUser && isLastMessage && isLoading && msg.content.trim() === ""; + + const displayContent = shouldShowThinkingPlaceholder + ? "DijkstraGPT is thinking ..." + : msg.content || ""; return ( -
+
- {/* File attachments display for user messages */} + {/* File attachments for user messages */} {isUser && msg.files && msg.files.length > 0 && (
{msg.files.map((file, idx) => ( @@ -561,51 +687,28 @@ export default function DijkstraGPT() {
)} - {/* Message content with code block formatting */}
- {parts.map((part, idx) => - idx % 2 === 1 ? ( - // Code block (odd indices) -
-
{part.trim()}
-
- ) : ( - // Regular text with list formatting (even indices) -
- {part.split("\n").map((line, li) => { - if (line.trim().match(/^[-*]\s+/)) { - return ( -
  • - {line.replace(/^[-*]\s+/, "")} -
  • - ); - } else if (line.trim().match(/^\d+\.\s+/)) { - return ( -
  • - {line.replace(/^\d+\.\s+/, "")} -
  • - ); - } else if (line.trim()) { - return

    {line}

    ; - } - return
    ; - })} -
    - ) - )} +
    + + {displayContent} + +
    - {/* Timestamp */} -

    {msg.timestamp.toLocaleTimeString()}

    +

    + {msg.timestamp.toLocaleTimeString()} +

    - {/* Action buttons for assistant messages - ICONS ONLY */} + {/* Assistant toolbar: Copy / Regenerate / Share (icons only) */} {!isUser && ( -
    - {/* Copy button - Icon only */} +
    - {/* Regenerate button - Icon only */} - {/* Share button - Icon only */} - {/* Image upload button */}
    - {/* Right side - Send button */} - + {/* Right side: circular Send / Stop button */} + {isLoading ? ( + + ) : ( + + )}
    @@ -776,40 +886,49 @@ export default function DijkstraGPT() { )}
    ); - - // ============================================ + // ========================================== // MAIN COMPONENT RENDER - // ============================================ - + // ========================================== return (
    - {/* ==================== MAIN CHAT AREA ==================== */}
    -
    - {/* Messages Container - No internal scroll, uses parent scroll */} + {/* MAIN CHAT AREA */} +
    + + {/* Hero / examples when no messages */} {!hasMessages && !isLoading ? ( <> - {/* Header - matches original layout */}
    Dijkstra GPT logo -

    Your Personal CS Prep Assistant

    +

    + Your Personal CS Prep Assistant +

    - This model has been trained on a wide range of computer science topics, tips and tricks, resources, and more to help you on your journey towards becoming a Computer Science Engineer. It is also context aware of what you do within GitHub and Leetcode. Happy coding :) + This model has been trained on a wide range of computer science + topics, tips and tricks, resources, and more to help you on your + journey towards becoming a Computer Science Engineer. It is also + context aware of what you do within GitHub and Leetcode. Happy + coding :)

    - {/* Main Content - matches original layout */} -
    +
    - {/* Example Prompts - Below Input (shown in original layout) */}
    -

    Get started with these examples

    -

    Click any prompt to try it out

    +

    + Get started with these examples +

    +

    + Click any prompt to try it out +

    -
    {examplePrompts.map((example, index) => ( ))}
    @@ -834,86 +948,48 @@ export default function DijkstraGPT() {
    ) : ( -
    - {/* ==================== MESSAGES VIEW ==================== */} -
    - {/* Render all messages */} + // Messages view +
    +
    {messages.map((m, i) => renderMessage(m, i))} - - {/* Loading indicator */} - {isLoading && ( -
    -
    -
    -
    -
    -
    -
    -
    -

    Thinking...

    -
    -
    -
    - )} -
    )} - {/* ==================== INPUT AREA - SINGLE INSTANCE THAT MOVES ==================== */} + {/* INPUT AREA */} {hasMessages ? (
    -
    - {renderInputArea(false)} -
    +
    {renderInputArea(false)}
    ) : (
    -
    - {renderInputArea(true)} -
    +
    {renderInputArea(true)}
    )}
    {/* ==================== SIDEBAR - CHAT HISTORY ==================== */} -
    - {/* Sidebar content wrapper - controls visibility */} -
    + {isSidebarOpen && ( +
    + {/* Sidebar header */}
    - {/* Title */}

    - Chat History

    - {/* New chat button */} - @@ -931,133 +1007,183 @@ export default function DijkstraGPT() {
    - {/* Chat sessions list with ScrollArea */} - -
    - {/* Grouped by date */} - {Object.entries(groupedSessions).map(([date, sessions]) => ( -
    - {/* Date header */} -
    - - {date === new Date().toDateString() - ? "Today" - : date === new Date(Date.now() - 86400000).toDateString() - ? "Yesterday" - : new Date(date).toLocaleDateString()} -
    + {/* Chat sessions list */} + +
    + {Object.entries(groupedSessions).map(([date, sessions]) => ( +
    + {/* Date header */} +
    + {date === new Date().toDateString() + ? "Today" + : date === new Date(Date.now() - 86400000).toDateString() + ? "Yesterday" + : new Date(date).toLocaleDateString()} +
    - {/* Session items */} -
    - {sessions.map((session) => ( - setCurrentSessionId(session.id)} - role="button" - tabIndex={0} - aria-label={`Select chat: ${session.title}`} + {/* Session cards */} +
    + {sessions.map((session) => ( + setCurrentSessionId(session.id)} + role="button" + tabIndex={0} + aria-label={`Select chat: ${session.title}`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + setCurrentSessionId(session.id); + } + }} + > +
    +
    + {/* Session info */} +
    + {/* 🔥 Removed the icon box div here */} + +
    + {editingSessionId === session.id ? ( + + setEditingTitle(e.target.value) + } + onBlur={saveRename} onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - setCurrentSessionId(session.id); + if (e.key === "Enter") saveRename(); + if (e.key === "Escape") { + setEditingSessionId(null); + setEditingTitle(""); } }} - > -
    -
    - {/* Session info */} -
    -
    - -
    -
    -

    - {session.title} -

    -

    - {session.messages.length} messages • {session.updatedAt.toLocaleDateString()} -

    -
    -
    - - {/* Action buttons - Download and Delete */} -
    - {/* Download button */} - - - {/* Delete button */} - -
    -
    -
    - - ))} + onClick={(e) => e.stopPropagation()} + autoFocus + className="w-full text-sm bg-background border border-border/50 rounded px-2 py-1" + /> + ) : ( + <> +

    + {session.title} +

    +

    + {session.messages.length} messages •{" "} + {session.updatedAt.toLocaleDateString()} +

    + + )}
    - ))} - {/* Empty state */} - {filteredSessions.length === 0 && ( -
    -
    - -
    -

    No chat history yet

    -

    Start a conversation to see your chats here

    -
    - )} + {/* 3-dot menu */} + + + + + + { + e.stopPropagation(); + renameSession(session.id); + }} + > + Rename + + { + e.stopPropagation(); + shareSession(session.id); + }} + > + Share + + { + e.stopPropagation(); + downloadSession(session); + }} + > + Download + + { + e.stopPropagation(); + deleteSession(session.id); + }} + > + Delete + + + +
    - -
    +
    + ))}
    + ))} - {/* ==================== FLOATING SIDEBAR TOGGLE ==================== */} + {/* Empty state */} + {filteredSessions.length === 0 && ( +
    +
    + +
    +

    + No chat history yet +

    +

    + Start a conversation to see your chats here +

    +
    + )} +
    +
    + +
    + )} +
    + + {/* Floating sidebar toggle */} - {/* ==================== HIDDEN FILE INPUT ==================== */} + {/* Hidden file input */} -
    ); } diff --git a/package-lock.json b/package-lock.json index 35b76de..947f61b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -81,6 +81,7 @@ "react-markdown": "^10.1.0", "react-tooltip": "^5.29.1", "recharts": "^3.0.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", @@ -9004,9 +9005,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 80bba09..7c318f3 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "react-markdown": "^10.1.0", "react-tooltip": "^5.29.1", "recharts": "^3.0.0", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", From 2f09854f65954caca99471d7b67ccfb3494f86c9 Mon Sep 17 00:00:00 2001 From: Sujan Kumar MV <82932721+KRYSTALM7@users.noreply.github.com> Date: Tue, 18 Nov 2025 22:55:33 +0530 Subject: [PATCH 2/3] Refactor DijkstraGPT component structure and comments Need a little bit of help in cleaning up the UI for the DijkstraGPT sidebar. --- components/dijkstra-gpt.tsx | 478 +++++++++++++----------------------- 1 file changed, 173 insertions(+), 305 deletions(-) diff --git a/components/dijkstra-gpt.tsx b/components/dijkstra-gpt.tsx index 7e39213..61ccbfc 100644 --- a/components/dijkstra-gpt.tsx +++ b/components/dijkstra-gpt.tsx @@ -1,4 +1,4 @@ - "use client"; +"use client"; import React, { useState, useRef, useEffect } from "react"; @@ -7,7 +7,6 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; -import { Card } from "@/components/ui/card"; // Markdown rendering import ReactMarkdown from "react-markdown"; @@ -35,10 +34,8 @@ import { Search, Check, Download, - MessageSquare, - Plus, - MoreVertical, Edit3, + MoreVertical, } from "lucide-react"; // API client for Gemini AI communication @@ -70,9 +67,9 @@ type ChatSession = { // MAIN COMPONENT // ============================================ export default function DijkstraGPT() { - // ========================================== + // ============================================ // STATE MANAGEMENT - // ========================================== + // ============================================ const [prompt, setPrompt] = useState(""); const [uploadedFiles, setUploadedFiles] = useState([]); const [isLoading, setIsLoading] = useState(false); @@ -81,31 +78,27 @@ export default function DijkstraGPT() { const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [copiedMessageId, setCopiedMessageId] = useState(null); - const [apiStatus, setApiStatus] = useState<"checking" | "active" | "inactive">( - "checking" - ); - const [hasMessages, setHasMessages] = useState(false); + const [apiStatus, setApiStatus] = useState<"checking" | "active" | "inactive">("checking"); const [editingSessionId, setEditingSessionId] = useState(null); const [editingTitle, setEditingTitle] = useState(""); - // ========================================== - // REFS - // ========================================== + // refs const textareaRef = useRef(null); const fileInputRef = useRef(null); const messagesEndRef = useRef(null); + + // cancel flag for streaming const cancelGenerationRef = useRef(false); - // ========================================== - // COMPUTED - // ========================================== - const currentSession = chatSessions.find((s) => s.id === currentSessionId); + // computed + const currentSession = chatSessions.find((session) => session.id === currentSessionId); const messages = currentSession?.messages || []; + const hasMessages = messages.length > 0; - // ========================================== + // ============================================ // EFFECTS - // ========================================== - // Auto-resize textarea + // ============================================ + // Auto-resize textarea based on content useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; @@ -113,7 +106,7 @@ export default function DijkstraGPT() { } }, [prompt]); - // Scroll to bottom when messages change + // Auto-scroll to bottom when new messages arrive useEffect(() => { if (messagesEndRef.current && hasMessages) { let scrollContainer = messagesEndRef.current.closest(".overflow-y-auto"); @@ -128,7 +121,6 @@ export default function DijkstraGPT() { parent = parent.parentElement; } } - if (scrollContainer) { setTimeout(() => { scrollContainer?.scrollTo({ @@ -144,14 +136,7 @@ export default function DijkstraGPT() { } }, [messages, isLoading, hasMessages]); - // Track if there are messages - useEffect(() => { - if (!hasMessages && messages.length > 0) { - setHasMessages(true); - } - }, [messages, hasMessages]); - - // Create initial empty session on mount + // Initialize with a default session on mount useEffect(() => { const initialSession: ChatSession = { id: Date.now().toString(), @@ -169,9 +154,9 @@ export default function DijkstraGPT() { checkApiStatus(); }, []); - // ========================================== + // ============================================ // SESSION MANAGEMENT - // ========================================== + // ============================================ const createNewChat = (): void => { const newSession: ChatSession = { id: Date.now().toString(), @@ -180,7 +165,6 @@ export default function DijkstraGPT() { createdAt: new Date(), updatedAt: new Date(), }; - setChatSessions((prev) => [newSession, ...prev]); setCurrentSessionId(newSession.id); setPrompt(""); @@ -196,9 +180,7 @@ export default function DijkstraGPT() { if (session.id === sessionId && session.title === "New Chat") { return { ...session, - title: - firstMessage.slice(0, 50) + - (firstMessage.length > 50 ? "..." : ""), + title: firstMessage.slice(0, 50) + (firstMessage.length > 50 ? "..." : ""), updatedAt: new Date(), }; } @@ -208,28 +190,24 @@ export default function DijkstraGPT() { }; const deleteSession = (sessionId: string): void => { - if (!window.confirm("Are you sure you want to delete this chat?")) return; + if (!confirm("Are you sure you want to delete this chat?")) return; setChatSessions((prev) => prev.filter((s) => s.id !== sessionId)); - if (currentSessionId === sessionId) { const remaining = chatSessions.filter((s) => s.id !== sessionId); setCurrentSessionId(remaining.length > 0 ? remaining[0].id : null); } - toast.success("Chat deleted"); }; const downloadSession = (session: ChatSession): void => { const dataStr = JSON.stringify(session, null, 2); const dataBlob = new Blob([dataStr], { type: "application/json" }); - const url = URL.createObjectURL(dataBlob); const link = document.createElement("a"); link.href = url; link.download = `${session.title}_${Date.now()}.json`; link.click(); - URL.revokeObjectURL(url); toast.success("Chat downloaded"); }; @@ -296,9 +274,9 @@ export default function DijkstraGPT() { ); }; - // ========================================== + // ============================================ // API STATUS CHECK - // ========================================== + // ============================================ const checkApiStatus = async (): Promise => { try { const response = await callGemini("test"); @@ -315,12 +293,11 @@ export default function DijkstraGPT() { } }; - // ========================================== + // ============================================ // FILE HANDLING - // ========================================== + // ============================================ const handleFileUpload = (event: React.ChangeEvent): void => { const files = Array.from(event.target.files || []); - const validTypes = [ "application/pdf", "image/jpeg", @@ -333,9 +310,7 @@ export default function DijkstraGPT() { const validFiles = files.filter((file) => { const isValid = - validTypes.includes(file.type) || - file.name.match(/\.(jpg|jpeg|png|pdf|txt|doc|docx)$/i); - + validTypes.includes(file.type) || file.name.match(/\.(jpg|jpeg|png|pdf|txt|doc|docx)$/i); if (!isValid) { toast.error(`File type not supported: ${file.name}`); } @@ -343,7 +318,6 @@ export default function DijkstraGPT() { }); setUploadedFiles((prev) => [...prev, ...validFiles]); - if (validFiles.length > 0) { toast.success(`${validFiles.length} file(s) uploaded`); } @@ -353,15 +327,14 @@ export default function DijkstraGPT() { setUploadedFiles((prev) => prev.filter((_, i) => i !== index)); }; - // ========================================== + // ============================================ // MESSAGE ACTIONS - // ========================================== + // ============================================ const copyMessage = async (content: string, messageId: string): Promise => { try { await navigator.clipboard.writeText(content); setCopiedMessageId(messageId); toast.success("Copied to clipboard"); - setTimeout(() => setCopiedMessageId(null), 2000); } catch (error) { toast.error("Failed to copy"); @@ -388,9 +361,9 @@ export default function DijkstraGPT() { ); }; - // ========================================== - // STREAMING WITH CANCEL SUPPORT - // ========================================== + // ============================================ + // STREAMING + CANCEL + // ============================================ const streamGeminiResponse = async ( promptText: string, sessionId: string, @@ -399,7 +372,7 @@ export default function DijkstraGPT() { try { const res: any = await callGemini(promptText); - // ReadableStream case + // ReadableStream (true streaming) if (res && res.body && typeof res.body.getReader === "function") { const reader = res.body.getReader(); const decoder = new TextDecoder(); @@ -423,12 +396,11 @@ export default function DijkstraGPT() { } done = !!d; } - updateAssistantContent(sessionId, assistantMessageId, accumulated); return accumulated; } - // Plain string case + // Simple string if (typeof res === "string") { const full = res; let i = 0; @@ -450,7 +422,7 @@ export default function DijkstraGPT() { return full; } - // Object with .text + // Object with `.text` if (res && typeof res === "object" && typeof res.text === "string") { const text = res.text; let i = 0; @@ -476,11 +448,7 @@ export default function DijkstraGPT() { return fallback; } catch (error) { if (cancelGenerationRef.current) { - updateAssistantContent( - sessionId, - assistantMessageId, - "⚠ Generation stopped by user." - ); + updateAssistantContent(sessionId, assistantMessageId, "⚠ Generation stopped by user."); return; } const errStr = "⚠ Error: " + String(error); @@ -489,7 +457,9 @@ export default function DijkstraGPT() { } }; - // Regenerate: delete old assistant then stream new + // ============================================ + // REGENERATE (delete old assistant, new one) +// ============================================ const handleRegenerate = async (assistantMessageId: string): Promise => { const session = chatSessions.find((s) => s.id === currentSessionId); if (!session) return; @@ -512,6 +482,7 @@ export default function DijkstraGPT() { cancelGenerationRef.current = false; setIsLoading(true); + // New assistant bubble const newAssistantMessage: Message = { id: (Date.now() + 2).toString(), role: "assistant", @@ -521,11 +492,7 @@ export default function DijkstraGPT() { addMessage(newAssistantMessage); try { - await streamGeminiResponse( - userMsg.content, - currentSessionId!, - newAssistantMessage.id - ); + await streamGeminiResponse(userMsg.content, currentSessionId!, newAssistantMessage.id); toast.success("Response regenerated"); } catch (error) { if (!cancelGenerationRef.current) { @@ -553,13 +520,12 @@ export default function DijkstraGPT() { } }; - // ========================================== - // INPUT HANDLING - // ========================================== + // ============================================ + // INPUT HANDLERS + // ============================================ const handleSubmit = async (): Promise => { if (!prompt.trim() && uploadedFiles.length === 0) return; - setHasMessages(true); cancelGenerationRef.current = false; if (!currentSessionId) { @@ -578,7 +544,6 @@ export default function DijkstraGPT() { }; addMessage(userMessage); - setPrompt(""); setUploadedFiles([]); setIsLoading(true); @@ -627,9 +592,9 @@ export default function DijkstraGPT() { toast.info("Generation stopped"); }; - // ========================================== - // SEARCH & GROUPING - // ========================================== + // ============================================ + // SEARCH & FILTER + // ============================================ const filteredSessions = chatSessions.filter( (session) => session.title.toLowerCase().includes(searchQuery.toLowerCase()) || @@ -638,18 +603,9 @@ export default function DijkstraGPT() { ) ); - const groupedSessions = filteredSessions.reduce((groups, session) => { - const date = session.updatedAt.toDateString(); - if (!groups[date]) { - groups[date] = []; - } - groups[date].push(session); - return groups; - }, {} as Record); - - // ========================================== - // RENDER MESSAGE (Markdown + thinking text) - // ========================================== + // ============================================ + // MESSAGE RENDER (markdown + thinking in bubble) +// ============================================ const renderMessage = (msg: Message, i: number): React.JSX.Element => { const isUser = msg.role === "user"; const isCopied = copiedMessageId === msg.id; @@ -664,18 +620,15 @@ export default function DijkstraGPT() { return (
    - {/* File attachments for user messages */} {isUser && msg.files && msg.files.length > 0 && (
    {msg.files.map((file, idx) => ( @@ -690,23 +643,17 @@ export default function DijkstraGPT() {
    - - {displayContent} - + {displayContent}
    -

    - {msg.timestamp.toLocaleTimeString()} -

    +

    {msg.timestamp.toLocaleTimeString()}

    - {/* Assistant toolbar: Copy / Regenerate / Share (icons only) */} + {/* Assistant toolbar: Copy, Regenerate, Share ONLY */} {!isUser && (