"use client"; import { useChat } from "@/contexts/chat-context"; import { cn } from "@/lib/utils"; import { FileText, Library, MessageSquare, Plus, Settings2, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useCallback, useEffect, useRef, useState } from "react"; import { EndpointType } from "@/contexts/chat-context"; import { useLoadingStore } from "@/stores/loadingStore"; import { KnowledgeFilterList } from "./knowledge-filter-list"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-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 { loading } = useLoadingStore(); const [conversations, setConversations] = useState([]); const [loadingConversations, setLoadingConversations] = useState(false); const [loadingNewConversation, setLoadingNewConversation] = useState(false); const [previousConversationCount, setPreviousConversationCount] = useState(0); const fileInputRef = useRef(null); const { selectedFilter, setSelectedFilter } = useKnowledgeFilter(); const handleNewConversation = () => { setLoadingNewConversation(true); refreshConversations(); startNewConversation(); if (typeof window !== "undefined") { window.dispatchEvent(new CustomEvent("newConversation")); } // Clear loading state after a short delay to show the new conversation is created setTimeout(() => { setLoadingNewConversation(false); }, 300); }; 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 isOnKnowledgePage = pathname.startsWith("/knowledge"); 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" && (
)}
))}
{isOnKnowledgePage && ( )} {/* Chat Page Specific Sections */} {isOnChatPage && (
{/* Conversations Section */}

Conversations

{/* Conversations List - grows naturally, doesn't fill all space */}
{loadingNewConversation ? (
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) => (
{ if (loading) return; loadConversation(conversation); refreshConversations(); }} >
{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}
)) )}
)}
); }