"use client"; import { EllipsisVertical, FileText, Library, MessageSquare, MoreHorizontal, Plus, Settings2, Trash2, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useDeleteSessionMutation } from "@/app/api/queries/useDeleteSessionMutation"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { type EndpointType, useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; import { cn } from "@/lib/utils"; import { useLoadingStore } from "@/stores/loadingStore"; import { DeleteSessionModal } from "./delete-session-modal"; import { KnowledgeFilterList } from "./knowledge-filter-list"; // Re-export the types for backward compatibility export 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; } export 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; } interface NavigationProps { conversations?: ChatConversation[]; isConversationsLoading?: boolean; onNewConversation?: () => void; } export function Navigation({ conversations = [], isConversationsLoading = false, onNewConversation, }: NavigationProps = {}) { const pathname = usePathname(); const { endpoint, loadConversation, currentConversationId, setCurrentConversationId, startNewConversation, conversationDocs, addConversationDoc, refreshConversations, placeholderConversation, setPlaceholderConversation, } = useChat(); const { loading } = useLoadingStore(); const [loadingNewConversation, setLoadingNewConversation] = useState(false); const [previousConversationCount, setPreviousConversationCount] = useState(0); const [deleteModalOpen, setDeleteModalOpen] = useState(false); const [conversationToDelete, setConversationToDelete] = useState(null); const fileInputRef = useRef(null); const { selectedFilter, setSelectedFilter } = useKnowledgeFilter(); // Delete session mutation const deleteSessionMutation = useDeleteSessionMutation({ onSuccess: () => { toast.success("Conversation deleted successfully"); // If we deleted the current conversation, select another one if ( conversationToDelete && currentConversationId === conversationToDelete.response_id ) { // Filter out the deleted conversation and find the next one const remainingConversations = conversations.filter( (conv) => conv.response_id !== conversationToDelete.response_id, ); if (remainingConversations.length > 0) { // Load the first available conversation (most recent) loadConversation(remainingConversations[0]); } else { // No conversations left, start a new one setCurrentConversationId(null); if (onNewConversation) { onNewConversation(); } else { refreshConversations(); startNewConversation(); } } } setDeleteModalOpen(false); setConversationToDelete(null); }, onError: (error) => { toast.error(`Failed to delete conversation: ${error.message}`); }, }); const handleNewConversation = () => { setLoadingNewConversation(true); // Use the prop callback if provided, otherwise use the context method if (onNewConversation) { onNewConversation(); } else { 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 handleDeleteConversation = ( conversation: ChatConversation, event?: React.MouseEvent, ) => { if (event) { event.preventDefault(); event.stopPropagation(); } setConversationToDelete(conversation); setDeleteModalOpen(true); }; const handleContextMenuAction = ( action: string, conversation: ChatConversation, ) => { switch (action) { case "delete": handleDeleteConversation(conversation); break; // Add more actions here in the future (rename, duplicate, etc.) default: break; } }; const confirmDeleteConversation = () => { if (conversationToDelete) { deleteSessionMutation.mutate({ sessionId: conversationToDelete.response_id, endpoint: endpoint, }); } }; 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"); // 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 || isConversationsLoading ? (
Loading...
) : ( <> {/* Show placeholder conversation if it exists */} {placeholderConversation && ( )} {/* Show regular conversations */} {conversations.length === 0 && !placeholderConversation ? (
No conversations yet
) : ( conversations.map((conversation) => ( e.stopPropagation()} > { e.stopPropagation(); handleContextMenuAction( "delete", conversation, ); }} className="cursor-pointer text-destructive focus:text-destructive" > Delete conversation
)) )} )}
{/* Conversation Knowledge Section - appears right after last conversation */}

Conversation knowledge

{conversationDocs.length === 0 ? (
No documents yet
) : ( conversationDocs.map((doc) => (
{doc.filename}
)) )}
)} {/* Delete Session Modal */} { setDeleteModalOpen(false); setConversationToDelete(null); }} onConfirm={confirmDeleteConversation} sessionTitle={conversationToDelete?.title || ""} isDeleting={deleteSessionMutation.isPending} />
); }