"use client" import Link from "next/link" import { usePathname } from "next/navigation" import { Library, MessageSquare, Settings2, Plus, FileText } from "lucide-react" import { cn } from "@/lib/utils" import { useState, useEffect, useRef, useCallback } from "react" import { useChat } from "@/contexts/chat-context" import { EndpointType } from "@/contexts/chat-context" interface RawConversation { response_id: string title: string endpoint: string messages: Array<{ role: string content: string timestamp?: string response_id?: string }> created_at?: string last_activity?: string previous_response_id?: string total_messages: number [key: string]: unknown } interface ChatConversation { response_id: string title: string endpoint: EndpointType messages: Array<{ role: string content: string timestamp?: string response_id?: string }> created_at?: string last_activity?: string previous_response_id?: string total_messages: number [key: string]: unknown } export function Navigation() { const pathname = usePathname() const { endpoint, refreshTrigger, loadConversation, currentConversationId, setCurrentConversationId, startNewConversation, conversationDocs, addConversationDoc, refreshConversations, placeholderConversation, setPlaceholderConversation } = useChat() const [conversations, setConversations] = useState([]) const [loadingConversations, setLoadingConversations] = useState(false) const [previousConversationCount, setPreviousConversationCount] = useState(0) const fileInputRef = useRef(null) const handleNewConversation = () => { // Ensure current conversation appears in sidebar before starting a new one refreshConversations() // Use context helper to fully reset conversation state startNewConversation() // Notify chat view even if state was already 'new' if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('newConversation')) } } const handleFileUpload = async (file: File) => { console.log("Navigation file upload:", file.name) // Trigger loading start event for chat page window.dispatchEvent(new CustomEvent('fileUploadStart', { detail: { filename: file.name } })) try { const formData = new FormData() formData.append('file', file) formData.append('endpoint', endpoint) const response = await fetch('/api/upload_context', { method: 'POST', body: formData, }) if (!response.ok) { const errorText = await response.text() console.error("Upload failed:", errorText) // Trigger error event for chat page to handle window.dispatchEvent(new CustomEvent('fileUploadError', { detail: { filename: file.name, error: 'Failed to process document' } })) // Trigger loading end event window.dispatchEvent(new CustomEvent('fileUploadComplete')) return } const result = await response.json() console.log("Upload result:", result) // Add the file to conversation docs if (result.filename) { addConversationDoc(result.filename) } // Trigger file upload event for chat page to handle window.dispatchEvent(new CustomEvent('fileUploaded', { detail: { file, result } })) // Trigger loading end event window.dispatchEvent(new CustomEvent('fileUploadComplete')) } catch (error) { console.error('Upload failed:', error) // Trigger loading end event even on error window.dispatchEvent(new CustomEvent('fileUploadComplete')) // Trigger error event for chat page to handle window.dispatchEvent(new CustomEvent('fileUploadError', { detail: { filename: file.name, error: 'Failed to process document' } })) } } const handleFilePickerClick = () => { fileInputRef.current?.click() } const handleFilePickerChange = (e: React.ChangeEvent) => { const files = e.target.files if (files && files.length > 0) { handleFileUpload(files[0]) } // Reset the input so the same file can be selected again if (fileInputRef.current) { fileInputRef.current.value = '' } } const routes = [ { label: "Chat", icon: MessageSquare, href: "/chat", active: pathname === "/" || pathname === "/chat", }, { label: "Knowledge", icon: Library, href: "/knowledge", active: pathname === "/knowledge", }, { label: "Settings", icon: Settings2, href: "/settings", active: pathname === "/settings", }, ] const isOnChatPage = pathname === "/" || pathname === "/chat" const createDefaultPlaceholder = useCallback(() => { return { response_id: 'new-conversation-' + Date.now(), title: 'New conversation', endpoint: endpoint, messages: [{ role: 'assistant', content: 'How can I assist?', timestamp: new Date().toISOString() }], created_at: new Date().toISOString(), last_activity: new Date().toISOString(), total_messages: 1 } as ChatConversation }, [endpoint]) const fetchConversations = useCallback(async () => { setLoadingConversations(true) try { // Fetch from the selected endpoint only const apiEndpoint = endpoint === 'chat' ? '/api/chat/history' : '/api/langflow/history' const response = await fetch(apiEndpoint) if (response.ok) { const history = await response.json() const rawConversations = history.conversations || [] // Cast conversations to proper type and ensure endpoint is correct const conversations: ChatConversation[] = rawConversations.map((conv: RawConversation) => ({ ...conv, endpoint: conv.endpoint as EndpointType })) // Sort conversations by last activity (most recent first) conversations.sort((a: ChatConversation, b: ChatConversation) => { const aTime = new Date(a.last_activity || a.created_at || 0).getTime() const bTime = new Date(b.last_activity || b.created_at || 0).getTime() return bTime - aTime }) setConversations(conversations) // If no conversations exist and no placeholder is shown, create a default placeholder if (conversations.length === 0 && !placeholderConversation) { setPlaceholderConversation(createDefaultPlaceholder()) } } else { setConversations([]) // Also create placeholder when request fails and no conversations exist if (!placeholderConversation) { setPlaceholderConversation(createDefaultPlaceholder()) } } // Conversation documents are now managed in chat context } catch (error) { console.error(`Failed to fetch ${endpoint} conversations:`, error) setConversations([]) } finally { setLoadingConversations(false) } }, [endpoint, placeholderConversation, setPlaceholderConversation, createDefaultPlaceholder]) // Fetch chat conversations when on chat page, endpoint changes, or refresh is triggered useEffect(() => { if (isOnChatPage) { fetchConversations() } }, [isOnChatPage, endpoint, refreshTrigger, fetchConversations]) // Clear placeholder when conversation count increases (new conversation was created) useEffect(() => { const currentCount = conversations.length // If we had a placeholder and the conversation count increased, clear the placeholder and highlight the new conversation if (placeholderConversation && currentCount > previousConversationCount && conversations.length > 0) { setPlaceholderConversation(null) // Highlight the most recent conversation (first in sorted array) without loading its messages const newestConversation = conversations[0] if (newestConversation) { setCurrentConversationId(newestConversation.response_id) } } // Update the previous count setPreviousConversationCount(currentCount) }, [conversations.length, placeholderConversation, setPlaceholderConversation, previousConversationCount, conversations, setCurrentConversationId]) return (
{routes.map((route) => (
{route.label}
{route.label === "Settings" && (
)}
))}
{/* Chat Page Specific Sections */} {isOnChatPage && (
{/* Conversations Section */}

Conversations

{/* Conversations List - grows naturally, doesn't fill all space */}
{loadingConversations ? (
Loading...
) : ( <> {/* Show placeholder conversation if it exists */} {placeholderConversation && (
{ // Don't load placeholder as a real conversation, just focus the input if (typeof window !== 'undefined') { window.dispatchEvent(new CustomEvent('focusInput')) } }} >
{placeholderConversation.title}
Start typing to begin...
)} {/* Show regular conversations */} {conversations.length === 0 && !placeholderConversation ? (
No conversations yet
) : ( conversations.map((conversation) => (
{ loadConversation(conversation) }} >
{conversation.title}
{conversation.total_messages} messages
{conversation.last_activity && (
{new Date(conversation.last_activity).toLocaleDateString()}
)}
)) )} )}
{/* Conversation Knowledge Section - appears right after last conversation */}

Conversation knowledge

{conversationDocs.length === 0 ? (
No documents yet
) : ( conversationDocs.map((doc, index) => (
{doc.filename}
)) )}
)}
) }