diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index a90845673..bb7ccedc9 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -91,6 +91,9 @@ export function AppSidebarProvider({ const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [chatToDelete, setChatToDelete] = useState<{ id: number; name: string } | null>(null); + const [showDeleteNoteDialog, setShowDeleteNoteDialog] = useState(false); + const [noteToDelete, setNoteToDelete] = useState<{ id: number; name: string; search_space_id: number } | null>(null); + const [isDeletingNote, setIsDeletingNote] = useState(false); const [isClient, setIsClient] = useState(false); // Set isClient to true when component mounts on the client @@ -105,25 +108,32 @@ export function AppSidebarProvider({ // Transform API response to the format expected by AppSidebar const recentChats = useMemo(() => { - return chats - ? chats.map((chat) => ({ - name: chat.title || `Chat ${chat.id}`, - url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`, - icon: "MessageCircleMore", - id: chat.id, - search_space_id: chat.search_space_id, - actions: [ - { - name: "Delete", - icon: "Trash2", - onClick: () => { - setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` }); - setShowDeleteDialog(true); - }, - }, - ], - })) - : []; + if (!chats) return []; + + // Sort chats by created_at (most recent first) + const sortedChats = [...chats].sort((a, b) => { + const dateA = new Date(a.created_at).getTime(); + const dateB = new Date(b.created_at).getTime(); + return dateB - dateA; // Descending order (most recent first) + }); + + return sortedChats.map((chat) => ({ + name: chat.title || `Chat ${chat.id}`, + url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`, + icon: "MessageCircleMore", + id: chat.id, + search_space_id: chat.search_space_id, + actions: [ + { + name: "Delete", + icon: "Trash2", + onClick: () => { + setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` }); + setShowDeleteDialog(true); + }, + }, + ], + })); }, [chats]); // Handle delete chat with better error handling @@ -141,6 +151,26 @@ export function AppSidebarProvider({ } }, [chatToDelete, deleteChat]); + // Handle delete note with confirmation + const handleDeleteNote = useCallback(async () => { + if (!noteToDelete) return; + + setIsDeletingNote(true); + try { + await notesApiService.deleteNote({ + search_space_id: noteToDelete.search_space_id, + note_id: noteToDelete.id, + }); + refetchNotes(); + } catch (error) { + console.error("Error deleting note:", error); + } finally { + setIsDeletingNote(false); + setShowDeleteNoteDialog(false); + setNoteToDelete(null); + } + }, [noteToDelete, refetchNotes]); + // Memoized fallback chats const fallbackChats = useMemo(() => { if (chatError) { @@ -162,19 +192,6 @@ export function AppSidebarProvider({ ]; } - if (!isLoadingChats && recentChats.length === 0) { - return [ - { - name: t("no_recent_chats"), - url: "#", - icon: "MessageCircleMore", - id: 0, - search_space_id: Number(searchSpaceId), - actions: [], - }, - ]; - } - return []; }, [chatError, isLoadingChats, recentChats.length, searchSpaceId, retryFetch, t, tCommon]); @@ -207,21 +224,14 @@ export function AppSidebarProvider({ { name: "Delete", icon: "Trash2", - onClick: async () => { - try { - await notesApiService.deleteNote({ - search_space_id: note.search_space_id, - note_id: note.id, - }); - refetchNotes(); - } catch (error) { - console.error("Error deleting note:", error); - } + onClick: () => { + setNoteToDelete({ id: note.id, name: note.title, search_space_id: note.search_space_id }); + setShowDeleteNoteDialog(true); }, }, ], })); - }, [notesData, refetchNotes]); + }, [notesData]); // Handle add note const handleAddNote = useCallback(() => { @@ -271,6 +281,7 @@ export function AppSidebarProvider({ navMain={navMain} RecentChats={[]} RecentNotes={[]} + onAddNote={handleAddNote} pageUsage={pageUsage} /> ); @@ -330,6 +341,49 @@ export function AppSidebarProvider({ + + {/* Delete Note Confirmation Dialog */} + + + + + + {t("delete_note")} + + + {t("delete_note_confirm")} {noteToDelete?.name}?{" "} + {t("action_cannot_undone")} + + + + + + + + ); } diff --git a/surfsense_web/components/sidebar/all-chats-sidebar.tsx b/surfsense_web/components/sidebar/all-chats-sidebar.tsx index 6b491cb24..b1a19e38a 100644 --- a/surfsense_web/components/sidebar/all-chats-sidebar.tsx +++ b/surfsense_web/components/sidebar/all-chats-sidebar.tsx @@ -1,6 +1,7 @@ "use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { Loader2, MessageCircleMore, MoreHorizontal, Search, Trash2, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -22,6 +23,7 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { chatsApiService } from "@/lib/apis/chats-api.service"; import { cn } from "@/lib/utils"; @@ -94,14 +96,22 @@ export function AllChatsSidebar({ setSearchQuery(""); }, []); - // Filter chats based on search query (client-side filtering) + // Filter and sort chats based on search query (client-side filtering) const chats = useMemo(() => { const allChats = chatsData ?? []; + + // Sort chats by created_at (most recent first) + const sortedChats = [...allChats].sort((a, b) => { + const dateA = new Date(a.created_at).getTime(); + const dateB = new Date(b.created_at).getTime(); + return dateB - dateA; // Descending order (most recent first) + }); + if (!debouncedSearchQuery) { - return allChats; + return sortedChats; } const query = debouncedSearchQuery.toLowerCase(); - return allChats.filter((chat) => + return sortedChats.filter((chat) => chat.title.toLowerCase().includes(query) ); }, [chatsData, debouncedSearchQuery]); @@ -111,7 +121,7 @@ export function AllChatsSidebar({ return ( - + {t("all_chats") || "All Chats"} {t("all_chats_description") || "Browse and manage all your chats"} @@ -135,7 +145,7 @@ export function AllChatsSidebar({ onClick={handleClearSearch} > - Clear search + {t("clear_search") || "Clear search"} )} @@ -167,15 +177,22 @@ export function AllChatsSidebar({ )} > {/* Main clickable area for navigation */} - + + + + + +

{t("created") || "Created"}: {format(new Date(chat.created_at), "MMM d, yyyy 'at' h:mm a")}

+
+
{/* Actions dropdown - separate from main click area */} @@ -195,7 +212,7 @@ export function AllChatsSidebar({ ) : ( )} - More options + {t("more_options") || "More options"} @@ -204,7 +221,7 @@ export function AllChatsSidebar({ className="text-destructive focus:text-destructive" > - Delete + {t("delete") || "Delete"} diff --git a/surfsense_web/components/sidebar/all-notes-sidebar.tsx b/surfsense_web/components/sidebar/all-notes-sidebar.tsx index 11c4f80ec..20070a580 100644 --- a/surfsense_web/components/sidebar/all-notes-sidebar.tsx +++ b/surfsense_web/components/sidebar/all-notes-sidebar.tsx @@ -1,6 +1,7 @@ "use client"; import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { format } from "date-fns"; import { FileText, Loader2, MoreHorizontal, Plus, Search, Trash2, X } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; @@ -21,6 +22,7 @@ import { SheetHeader, SheetTitle, } from "@/components/ui/sheet"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { useDebouncedValue } from "@/hooks/use-debounced-value"; import { documentsApiService } from "@/lib/apis/documents-api.service"; import { notesApiService } from "@/lib/apis/notes-api.service"; @@ -121,22 +123,38 @@ export function AllNotesSidebar({ const isLoading = isSearchMode ? isSearching : isLoadingNotes; const error = isSearchMode ? searchError : notesError; - // Transform notes data - handle both regular notes and search results + // Transform and sort notes data - handle both regular notes and search results const notes = useMemo(() => { + let notesList: { id: number; title: string; search_space_id: number; created_at: string; updated_at?: string | null }[]; + if (isSearchMode && searchData?.items) { - return searchData.items.map((doc) => ({ + notesList = searchData.items.map((doc) => ({ id: doc.id, title: doc.title, search_space_id: doc.search_space_id, + created_at: doc.created_at, + updated_at: doc.updated_at, })); + } else { + notesList = notesData?.items ?? []; } - return notesData?.items ?? []; + + // Sort notes by updated_at (most recent first), fallback to created_at + return [...notesList].sort((a, b) => { + const dateA = a.updated_at + ? new Date(a.updated_at).getTime() + : new Date(a.created_at).getTime(); + const dateB = b.updated_at + ? new Date(b.updated_at).getTime() + : new Date(b.created_at).getTime(); + return dateB - dateA; // Descending order (most recent first) + }); }, [isSearchMode, searchData, notesData]); return ( - + {t("all_notes") || "All Notes"} {t("all_notes_description") || "Browse and manage all your notes"} @@ -160,7 +178,7 @@ export function AllNotesSidebar({ onClick={handleClearSearch} > - Clear search + {t("clear_search") || "Clear search"} )} @@ -192,15 +210,27 @@ export function AllNotesSidebar({ )} > {/* Main clickable area for navigation */} - + + + + + +
+

{t("created") || "Created"}: {format(new Date(note.created_at), "MMM d, yyyy 'at' h:mm a")}

+ {note.updated_at && ( +

{t("updated") || "Updated"}: {format(new Date(note.updated_at), "MMM d, yyyy 'at' h:mm a")}

+ )} +
+
+
{/* Actions dropdown - separate from main click area */} @@ -220,7 +250,7 @@ export function AllNotesSidebar({ ) : ( )} - More options + {t("more_options") || "More options"} @@ -229,7 +259,7 @@ export function AllNotesSidebar({ className="text-destructive focus:text-destructive" > - Delete + {t("delete") || "Delete"} @@ -273,7 +303,7 @@ export function AllNotesSidebar({ {/* Footer with Add Note button */} {onAddNote && notes.length > 0 && ( -
+
@@ -121,10 +132,10 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP
- - - {chats.length > 0 ? ( - chats.map((chat) => { + {chats.length > 0 ? ( + + + {chats.map((chat) => { const isDeletingChat = isDeleting === chat.id; return ( @@ -163,7 +174,7 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP ) : ( )} - More options + {t("more_options") || "More options"} @@ -191,7 +202,7 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP {isDeletingChat && isDeleteAction - ? "Deleting..." + ? t("deleting") || "Deleting..." : action.name} @@ -203,17 +214,15 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsP )} ); - }) - ) : ( - - - - {t("no_recent_chats") || "No recent chats"} - - - )} - - + })} + + + ) : ( +
+ + {t("no_recent_chats") || "No recent chats"} +
+ )}
diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx index 274d77b33..606ab2680 100644 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ b/surfsense_web/components/sidebar/nav-main.tsx @@ -2,7 +2,7 @@ import { ChevronRight, type LucideIcon } from "lucide-react"; import { useTranslations } from "next-intl"; -import { useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { @@ -28,7 +28,12 @@ interface NavItem { }[]; } -export function NavMain({ items }: { items: NavItem[] }) { +interface NavMainProps { + items: NavItem[]; + onSourcesExpandedChange?: (expanded: boolean) => void; +} + +export function NavMain({ items, onSourcesExpandedChange }: NavMainProps) { const t = useTranslations("nav_menu"); // Translation function that handles both exact matches and fallback to original @@ -53,6 +58,29 @@ export function NavMain({ items }: { items: NavItem[] }) { // Memoize items to prevent unnecessary re-renders const memoizedItems = useMemo(() => items, [items]); + // Track expanded state for items with sub-menus (like Sources) + const [expandedItems, setExpandedItems] = useState>(() => { + const initial: Record = {}; + items.forEach((item) => { + if (item.items?.length) { + initial[item.title] = item.isActive ?? false; + } + }); + return initial; + }); + + // Handle collapsible state change + const handleOpenChange = useCallback( + (title: string, isOpen: boolean) => { + setExpandedItems((prev) => ({ ...prev, [title]: isOpen })); + // Notify parent when Sources is expanded/collapsed + if (title === "Sources" && onSourcesExpandedChange) { + onSourcesExpandedChange(isOpen); + } + }, + [onSourcesExpandedChange] + ); + return ( {translateTitle("Platform")} @@ -60,8 +88,15 @@ export function NavMain({ items }: { items: NavItem[] }) { {memoizedItems.map((item, index) => { const translatedTitle = translateTitle(item.title); const hasSub = !!item.items?.length; + const isItemOpen = expandedItems[item.title] ?? item.isActive ?? false; return ( - + handleOpenChange(item.title, open) : undefined} + defaultOpen={!hasSub ? item.isActive : undefined} + > {hasSub ? ( // When the item has children, make the whole row a collapsible trigger diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx index b14ecea77..383338d77 100644 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ b/surfsense_web/components/sidebar/nav-notes.tsx @@ -12,7 +12,7 @@ import { } from "lucide-react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { @@ -29,6 +29,7 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/sidebar"; +import { useIsMobile } from "@/hooks/use-mobile"; import { cn } from "@/lib/utils"; import { AllNotesSidebar } from "./all-notes-sidebar"; @@ -52,6 +53,7 @@ interface NavNotesProps { onAddNote?: () => void; defaultOpen?: boolean; searchSpaceId?: string; + isSourcesExpanded?: boolean; } // Map of icon names to their components @@ -61,13 +63,21 @@ const actionIconMap: Record = { MoreHorizontal, }; -export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) { +export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId, isSourcesExpanded = false }: NavNotesProps) { const t = useTranslations("sidebar"); const router = useRouter(); + const isMobile = useIsMobile(); const [isDeleting, setIsDeleting] = useState(null); const [isOpen, setIsOpen] = useState(defaultOpen); const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false); + // Auto-collapse on smaller screens when Sources is expanded + useEffect(() => { + if (isSourcesExpanded && isMobile) { + setIsOpen(false); + } + }, [isSourcesExpanded, isMobile]); + // Handle note deletion with loading state const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => { setIsDeleting(noteId); @@ -113,7 +123,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId } e.stopPropagation(); setIsAllNotesSidebarOpen(true); }} - aria-label="View all notes" + aria-label={t("view_all_notes") || "View all notes"} > @@ -127,7 +137,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId } e.stopPropagation(); onAddNote(); }} - aria-label="Add note" + aria-label={t("add_note") || "Add note"} > @@ -178,7 +188,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId } ) : ( )} - More options + {t("more_options") || "More options"} @@ -206,7 +216,7 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId } {isDeletingNote && isDeleteAction - ? "Deleting..." + ? t("deleting") || "Deleting..." : action.name} diff --git a/surfsense_web/components/ui/sidebar.tsx b/surfsense_web/components/ui/sidebar.tsx index edc846ba7..caafa6b6e 100644 --- a/surfsense_web/components/ui/sidebar.tsx +++ b/surfsense_web/components/ui/sidebar.tsx @@ -352,7 +352,7 @@ function SidebarContent({ className, ...props }: React.ComponentProps<"div">) { data-slot="sidebar-content" data-sidebar="content" className={cn( - "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden group-data-[collapsible=icon]:overflow-hidden", + "flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden", className )} {...props} diff --git a/surfsense_web/components/ui/tooltip.tsx b/surfsense_web/components/ui/tooltip.tsx index c065426ba..98420c858 100644 --- a/surfsense_web/components/ui/tooltip.tsx +++ b/surfsense_web/components/ui/tooltip.tsx @@ -42,13 +42,13 @@ function TooltipContent({ data-slot="tooltip-content" sideOffset={sideOffset} className={cn( - "bg-primary text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance", + "bg-popover text-popover-foreground border shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit rounded-md px-3 py-1.5 text-xs text-balance", className )} {...props} > {children} - + ); diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index 478264889..43ec2b0aa 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -98,6 +98,8 @@ "unknown_search_space": "Unknown Search Space", "delete_chat": "Delete Chat", "delete_chat_confirm": "Are you sure you want to delete", + "delete_note": "Delete Note", + "delete_note_confirm": "Are you sure you want to delete", "action_cannot_undone": "This action cannot be undone.", "deleting": "Deleting...", "surfsense_dashboard": "SurfSense Dashboard", @@ -641,7 +643,7 @@ "search_chats": "Search chats...", "no_chats_found": "No chats found", "no_recent_chats": "No recent chats", - "view_all_chats": "View All Chats", + "view_all_chats": "View all chats", "all_chats": "All Chats", "all_chats_description": "Browse and manage all your chats", "no_chats": "No chats yet", @@ -659,7 +661,15 @@ "no_notes": "No notes yet", "create_new_note": "Create a new note", "error_loading_notes": "Error loading notes", - "loading": "Loading..." + "loading": "Loading...", + "deleting": "Deleting...", + "delete": "Delete", + "created": "Created", + "updated": "Updated", + "more_options": "More options", + "clear_search": "Clear search", + "view_all_notes": "View all notes", + "add_note": "Add note" }, "errors": { "something_went_wrong": "Something went wrong", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 24b37c61f..d33354e06 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -98,6 +98,8 @@ "unknown_search_space": "未知搜索空间", "delete_chat": "删除对话", "delete_chat_confirm": "您确定要删除", + "delete_note": "删除笔记", + "delete_note_confirm": "您确定要删除", "action_cannot_undone": "此操作无法撤销。", "deleting": "删除中...", "surfsense_dashboard": "SurfSense 仪表盘", @@ -659,7 +661,15 @@ "no_notes": "暂无笔记", "create_new_note": "创建新笔记", "error_loading_notes": "加载笔记时出错", - "loading": "加载中..." + "loading": "加载中...", + "deleting": "删除中...", + "delete": "删除", + "created": "创建时间", + "updated": "更新时间", + "more_options": "更多选项", + "clear_search": "清除搜索", + "view_all_notes": "查看所有笔记", + "add_note": "添加笔记" }, "errors": { "something_went_wrong": "出错了",