diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index d7bd37be..4efd1403 100644 --- a/frontend/components/navigation.tsx +++ b/frontend/components/navigation.tsx @@ -1,13 +1,13 @@ "use client"; import { - EllipsisVertical, - FileText, - Library, - MessageSquare, - Plus, - Settings2, - Trash2, + EllipsisVertical, + FileText, + Library, + MessageSquare, + Plus, + Settings2, + Trash2, } from "lucide-react"; import Link from "next/link"; import { usePathname } from "next/navigation"; @@ -15,10 +15,10 @@ import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; import { useDeleteSessionMutation } from "@/app/api/queries/useDeleteSessionMutation"; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { type EndpointType, useChat } from "@/contexts/chat-context"; import { useKnowledgeFilter } from "@/contexts/knowledge-filter-context"; @@ -30,484 +30,484 @@ 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; + 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; + 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; + conversations?: ChatConversation[]; + isConversationsLoading?: boolean; + onNewConversation?: () => void; } export function Navigation({ - conversations = [], - isConversationsLoading = false, - onNewConversation, + conversations = [], + isConversationsLoading = false, + onNewConversation, }: NavigationProps = {}) { - const pathname = usePathname(); - const { - endpoint, - loadConversation, - currentConversationId, - setCurrentConversationId, - startNewConversation, - conversationDocs, - conversationData, - refreshConversations, - placeholderConversation, - setPlaceholderConversation, - conversationLoaded, - } = useChat(); + const pathname = usePathname(); + const { + endpoint, + loadConversation, + currentConversationId, + setCurrentConversationId, + startNewConversation, + conversationDocs, + conversationData, + refreshConversations, + placeholderConversation, + setPlaceholderConversation, + conversationLoaded, + } = useChat(); - const { loading } = useLoadingStore(); + const { loading } = useLoadingStore(); - const [previousConversationCount, setPreviousConversationCount] = useState(0); - const [deleteModalOpen, setDeleteModalOpen] = useState(false); - const [conversationToDelete, setConversationToDelete] = - useState(null); - const hasCompletedInitialLoad = useRef(false); - const mountTimeRef = useRef(null); + const [previousConversationCount, setPreviousConversationCount] = useState(0); + const [deleteModalOpen, setDeleteModalOpen] = useState(false); + const [conversationToDelete, setConversationToDelete] = + useState(null); + const hasCompletedInitialLoad = useRef(false); + const mountTimeRef = useRef(null); - const { selectedFilter, setSelectedFilter } = useKnowledgeFilter(); + const { selectedFilter, setSelectedFilter } = useKnowledgeFilter(); - // Delete session mutation - const deleteSessionMutation = useDeleteSessionMutation({ - onSuccess: () => { - toast.success("Conversation deleted successfully"); + // 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 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(); - } - } - } + 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}`); - }, - }); + setDeleteModalOpen(false); + setConversationToDelete(null); + }, + onError: (error) => { + toast.error(`Failed to delete conversation: ${error.message}`); + }, + }); - const handleNewConversation = () => { - // Use the prop callback if provided, otherwise use the context method - if (onNewConversation) { - onNewConversation(); - } else { - refreshConversations(); - startNewConversation(); - } + const handleNewConversation = () => { + // 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")); - } - }; + if (typeof window !== "undefined") { + window.dispatchEvent(new CustomEvent("newConversation")); + } + }; - const handleDeleteConversation = ( - conversation: ChatConversation, - event?: React.MouseEvent, - ) => { - if (event) { - event.preventDefault(); - event.stopPropagation(); - } - setConversationToDelete(conversation); - setDeleteModalOpen(true); - }; + 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 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 confirmDeleteConversation = () => { + if (conversationToDelete) { + deleteSessionMutation.mutate({ + sessionId: conversationToDelete.response_id, + endpoint: endpoint, + }); + } + }; - const routes = [ - { - label: "Chat", - icon: MessageSquare, - href: "/chat", - active: pathname === "/" || pathname.startsWith("/chat"), - }, - { - label: "Knowledge", - icon: Library, - href: "/knowledge", - active: pathname.startsWith("/knowledge"), - }, - { - label: "Settings", - icon: Settings2, - href: "/settings", - active: pathname.startsWith("/settings"), - }, - ]; + const routes = [ + { + label: "Chat", + icon: MessageSquare, + href: "/chat", + active: pathname === "/" || pathname.startsWith("/chat"), + }, + { + label: "Knowledge", + icon: Library, + href: "/knowledge", + active: pathname.startsWith("/knowledge"), + }, + { + label: "Settings", + icon: Settings2, + href: "/settings", + active: pathname.startsWith("/settings"), + }, + ]; - const isOnChatPage = pathname === "/" || pathname === "/chat"; - const isOnKnowledgePage = pathname.startsWith("/knowledge"); + const isOnChatPage = pathname === "/" || pathname === "/chat"; + const isOnKnowledgePage = pathname.startsWith("/knowledge"); - // Track mount time to prevent auto-selection right after component mounts (e.g., after onboarding) - useEffect(() => { - if (mountTimeRef.current === null) { - mountTimeRef.current = Date.now(); - } - }, []); + // Track mount time to prevent auto-selection right after component mounts (e.g., after onboarding) + useEffect(() => { + if (mountTimeRef.current === null) { + mountTimeRef.current = Date.now(); + } + }, []); - // Track when initial load completes - useEffect(() => { - if (!isConversationsLoading && !hasCompletedInitialLoad.current) { - hasCompletedInitialLoad.current = true; - // Set initial count after first load completes - setPreviousConversationCount(conversations.length); - } - }, [isConversationsLoading, conversations.length]); + // Track when initial load completes + useEffect(() => { + if (!isConversationsLoading && !hasCompletedInitialLoad.current) { + hasCompletedInitialLoad.current = true; + // Set initial count after first load completes + setPreviousConversationCount(conversations.length); + } + }, [isConversationsLoading, conversations.length]); - // Clear placeholder when conversation count increases (new conversation was created) - useEffect(() => { - const currentCount = conversations.length; - const timeSinceMount = mountTimeRef.current - ? Date.now() - mountTimeRef.current - : Infinity; - const MIN_TIME_AFTER_MOUNT = 2000; // 2 seconds - prevents selection right after onboarding + // Clear placeholder when conversation count increases (new conversation was created) + useEffect(() => { + const currentCount = conversations.length; + const timeSinceMount = mountTimeRef.current + ? Date.now() - mountTimeRef.current + : Infinity; + const MIN_TIME_AFTER_MOUNT = 2000; // 2 seconds - prevents selection right after onboarding - // Only select if: - // 1. We have a placeholder (new conversation was created) - // 2. Initial load has completed (prevents selection on browser refresh) - // 3. Count increased (new conversation appeared) - // 4. Not currently loading - // 5. Enough time has passed since mount (prevents selection after onboarding completes) - if ( - placeholderConversation && - hasCompletedInitialLoad.current && - currentCount > previousConversationCount && - conversations.length > 0 && - !isConversationsLoading && - timeSinceMount >= MIN_TIME_AFTER_MOUNT - ) { - 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); - } - } + // Only select if: + // 1. We have a placeholder (new conversation was created) + // 2. Initial load has completed (prevents selection on browser refresh) + // 3. Count increased (new conversation appeared) + // 4. Not currently loading + // 5. Enough time has passed since mount (prevents selection after onboarding completes) + if ( + placeholderConversation && + hasCompletedInitialLoad.current && + currentCount > previousConversationCount && + conversations.length > 0 && + !isConversationsLoading && + timeSinceMount >= MIN_TIME_AFTER_MOUNT + ) { + 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 only after initial load - if (hasCompletedInitialLoad.current) { - setPreviousConversationCount(currentCount); - } - }, [ - conversations.length, - placeholderConversation, - setPlaceholderConversation, - previousConversationCount, - conversations, - setCurrentConversationId, - isConversationsLoading, - ]); + // Update the previous count only after initial load + if (hasCompletedInitialLoad.current) { + setPreviousConversationCount(currentCount); + } + }, [ + conversations.length, + placeholderConversation, + setPlaceholderConversation, + previousConversationCount, + conversations, + setCurrentConversationId, + isConversationsLoading, + ]); - useEffect(() => { - let activeConvo; + useEffect(() => { + let activeConvo; - if (currentConversationId && conversations.length > 0) { - activeConvo = conversations.find( - (conv) => conv.response_id === currentConversationId, - ); - } + if (currentConversationId && conversations.length > 0) { + activeConvo = conversations.find( + (conv) => conv.response_id === currentConversationId, + ); + } - if (isOnChatPage && !isConversationsLoading) { - if (conversations.length === 0 && !placeholderConversation) { - handleNewConversation(); - } else if (activeConvo) { - loadConversation(activeConvo); - refreshConversations(); - } else if ( - conversations.length > 0 && - currentConversationId === null && - !placeholderConversation - ) { - handleNewConversation(); - } - } - }, [isOnChatPage, conversations, conversationLoaded]); + if (isOnChatPage && !isConversationsLoading) { + if (conversations.length === 0 && !placeholderConversation) { + handleNewConversation(); + } else if (activeConvo) { + loadConversation(activeConvo); + refreshConversations(); + } else if ( + conversations.length > 0 && + currentConversationId === null && + !placeholderConversation + ) { + handleNewConversation(); + } + } + }, [isOnChatPage, conversations, conversationLoaded]); - const newConversationFiles = conversationData?.messages - .filter( - (message) => - message.role === "user" && - (message.content.match(FILES_REGEX)?.[0] ?? null) !== null, - ) - .map((message) => message.content.match(FILES_REGEX)?.[0] ?? null) - .concat(conversationDocs.map((doc) => doc.filename)); + const newConversationFiles = conversationData?.messages + .filter( + (message) => + message.role === "user" && + (message.content.match(FILES_REGEX)?.[0] ?? null) !== null, + ) + .map((message) => message.content.match(FILES_REGEX)?.[0] ?? null) + .concat(conversationDocs.map((doc) => doc.filename)); - return ( -
-
-
- {routes.map((route) => ( -
- -
- - {route.label} -
- - {route.label === "Settings" && ( -
- )} -
- ))} -
-
+ return ( +
+
+
+ {routes.map((route) => ( +
+ +
+ + {route.label} +
+ + {route.label === "Settings" && ( +
+ )} +
+ ))} +
+
- {isOnKnowledgePage && ( - - )} + {isOnKnowledgePage && ( + + )} - {/* Chat Page Specific Sections */} - {isOnChatPage && ( -
- {/* Conversations Section */} -
-
-

- Conversations -

- -
-
+ {/* Chat Page Specific Sections */} + {isOnChatPage && ( +
+ {/* Conversations Section */} +
+
+

+ Conversations +

+ +
+
-
-
- {/* Show skeleton loaders when loading and no conversations exist */} - {isConversationsLoading && conversations.length === 0 ? ( - [0, 1].map((skeletonIndex) => ( -
-
-
- )) - ) : ( - <> - {/* Show regular conversations */} - {conversations.length === 0 && !isConversationsLoading ? ( -
- No conversations yet -
- ) : ( - conversations.map((conversation) => ( - - )) - )} - - )} -
- {(newConversationFiles?.length ?? 0) !== 0 && ( -
-
-

- Files -

-
-
- {newConversationFiles?.map((file, index) => ( -
-
- {file} -
-
- ))} -
-
- )} -
-
- )} +
+
+ {/* Show skeleton loaders when loading and no conversations exist */} + {isConversationsLoading && conversations.length === 0 ? ( + [0, 1].map((skeletonIndex) => ( +
+
+
+ )) + ) : ( + <> + {/* Show regular conversations */} + {conversations.length === 0 && !isConversationsLoading ? ( +
+ No conversations yet +
+ ) : ( + conversations.map((conversation) => ( + + )) + )} + + )} +
+ {(newConversationFiles?.length ?? 0) !== 0 && ( +
+
+

+ Files +

+
+
+ {newConversationFiles?.map((file, index) => ( +
+
+ {file} +
+
+ ))} +
+
+ )} +
+
+ )} - {/* Delete Session Modal */} - { - setDeleteModalOpen(false); - setConversationToDelete(null); - }} - onConfirm={confirmDeleteConversation} - sessionTitle={conversationToDelete?.title || ""} - isDeleting={deleteSessionMutation.isPending} - /> -
- ); + {/* Delete Session Modal */} + { + setDeleteModalOpen(false); + setConversationToDelete(null); + }} + onConfirm={confirmDeleteConversation} + sessionTitle={conversationToDelete?.title || ""} + isDeleting={deleteSessionMutation.isPending} + /> +
+ ); }