diff --git a/frontend/components/navigation.tsx b/frontend/components/navigation.tsx index 423172f5..4eb5d786 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,13 +15,14 @@ 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"; +import { FILES_REGEX } from "@/lib/constants"; import { cn } from "@/lib/utils"; import { useLoadingStore } from "@/stores/loadingStore"; import { DeleteSessionModal } from "./delete-session-modal"; @@ -29,486 +30,495 @@ 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, - addConversationDoc, - refreshConversations, - placeholderConversation, - setPlaceholderConversation, - } = useChat(); + const pathname = usePathname(); + const { + endpoint, + loadConversation, + currentConversationId, + setCurrentConversationId, + startNewConversation, + conversationDocs, + conversationData, + addConversationDoc, + refreshConversations, + placeholderConversation, + setPlaceholderConversation, + } = useChat(); - const { loading } = useLoadingStore(); + 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 [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(); + 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 = () => { - setLoadingNewConversation(true); + const handleNewConversation = () => { + setLoadingNewConversation(true); - // Use the prop callback if provided, otherwise use the context method - if (onNewConversation) { - onNewConversation(); - } else { - refreshConversations(); - startNewConversation(); - } + // 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); - }; + 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); + 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 }, - }) - ); + // 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); + try { + const formData = new FormData(); + formData.append("file", file); + formData.append("endpoint", endpoint); - const response = await fetch("/api/upload_context", { - method: "POST", - body: formData, - }); + const response = await fetch("/api/upload_context", { + method: "POST", + body: formData, + }); - if (!response.ok) { - const errorText = await response.text(); - console.error("Upload failed:", errorText); + 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 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; - } + // Trigger loading end event + window.dispatchEvent(new CustomEvent("fileUploadComplete")); + return; + } - const result = await response.json(); - console.log("Upload result:", result); + const result = await response.json(); + console.log("Upload result:", result); - // Add the file to conversation docs - if (result.filename) { - addConversationDoc(result.filename); - } + // 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 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 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" }, - }) - ); - } - }; + // 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 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 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 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 === "/chat", - }, - { - label: "Knowledge", - icon: Library, - href: "/knowledge", - active: pathname === "/knowledge", - }, - { - label: "Settings", - icon: Settings2, - href: "/settings", - active: pathname === "/settings", - }, - ]; + 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 isOnChatPage = pathname === "/" || pathname === "/chat"; + const isOnKnowledgePage = pathname.startsWith("/knowledge"); - // Clear placeholder when conversation count increases (new conversation was created) - useEffect(() => { - const currentCount = conversations.length; + // 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); - } - } + // 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, - ]); + // Update the previous count + setPreviousConversationCount(currentCount); + }, [ + conversations.length, + placeholderConversation, + setPlaceholderConversation, + previousConversationCount, + conversations, + setCurrentConversationId, + ]); - return ( -
-
-
- {routes.map(route => ( -
- -
- - {route.label} -
- - {route.label === "Settings" && ( -
- )} -
- ))} -
-
+ 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); - {isOnKnowledgePage && ( - - )} + return ( +
+
+
+ {routes.map((route) => ( +
+ +
+ + {route.label} +
+ + {route.label === "Settings" && ( +
+ )} +
+ ))} +
+
- {/* Chat Page Specific Sections */} - {isOnChatPage && ( -
- {/* Conversations Section */} -
-
-

- Conversations -

- -
-
+ {isOnKnowledgePage && ( + + )} -
- {/* Conversations List - grows naturally, doesn't fill all space */} -
- {loadingNewConversation || isConversationsLoading ? ( -
- Loading... -
- ) : ( - <> - {/* Show placeholder conversation if it exists */} - {placeholderConversation && ( - - )} + {/* Chat Page Specific Sections */} + {isOnChatPage && ( +
+ {/* Conversations Section */} +
+
+

+ Conversations +

+ +
+
- {/* Show regular conversations */} - {conversations.length === 0 && !placeholderConversation ? ( -
- No conversations yet -
- ) : ( - conversations.map(conversation => ( - - )) - )} - - )} -
+
+ {/* Conversations List - grows naturally, doesn't fill all space */} +
+ {loadingNewConversation || isConversationsLoading ? ( +
+ Loading... +
+ ) : ( + <> + {/* Show placeholder conversation if it exists */} + {placeholderConversation && ( + + )} - {/* Conversation Knowledge Section - appears right after last conversation */} + {/* Show regular conversations */} + {conversations.length === 0 && !placeholderConversation ? ( +
+ No conversations yet +
+ ) : ( + conversations.map((conversation) => ( + + )) + )} + + )} +
+ + {/* Conversation Knowledge Section - appears right after last conversation

@@ -551,22 +561,44 @@ export function Navigation({ )) )}

-
-
-
- )} +
*/} +
+
+

+ Files +

+
+
+ {newConversationFiles?.length === 0 ? ( +
+ No documents yet +
+ ) : ( + newConversationFiles?.map((file) => ( +
+
+ {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} + /> +
+ ); }