diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx deleted file mode 100644 index f5146c427..000000000 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ /dev/null @@ -1,383 +0,0 @@ -"use client"; - -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useAtomValue, useSetAtom } from "jotai"; -import { Trash2 } from "lucide-react"; -import { useParams, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; -import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { AppSidebar } from "@/components/sidebar/app-sidebar"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { notesApiService } from "@/lib/apis/notes-api.service"; -import { searchSpacesApiService } from "@/lib/apis/search-spaces-api.service"; -import { deleteThread, fetchThreads } from "@/lib/chat/thread-persistence"; -import { cacheKeys } from "@/lib/query-client/cache-keys"; - -interface AppSidebarProviderProps { - searchSpaceId: string; - navSecondary: { - title: string; - url: string; - icon: string; - }[]; - navMain: { - title: string; - url: string; - icon: string; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; - }[]; -} - -export function AppSidebarProvider({ - searchSpaceId, - navSecondary, - navMain, -}: AppSidebarProviderProps) { - const t = useTranslations("dashboard"); - const tCommon = useTranslations("common"); - const router = useRouter(); - const params = useParams(); - const queryClient = useQueryClient(); - - // Get current chat ID from URL params - const currentChatId = params?.chat_id - ? Number(Array.isArray(params.chat_id) ? params.chat_id[0] : params.chat_id) - : null; - const [isDeletingThread, setIsDeletingThread] = useState(false); - - // Editor state for handling unsaved changes - const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom); - const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom); - - // Fetch new chat threads - const { - data: threadsData, - error: threadError, - refetch: refetchThreads, - } = useQuery({ - queryKey: ["threads", searchSpaceId], - queryFn: () => fetchThreads(Number(searchSpaceId), 4), - enabled: !!searchSpaceId, - }); - - const { - data: searchSpace, - isLoading: isLoadingSearchSpace, - error: searchSpaceError, - } = useQuery({ - queryKey: cacheKeys.searchSpaces.detail(searchSpaceId), - queryFn: () => searchSpacesApiService.getSearchSpace({ id: Number(searchSpaceId) }), - enabled: !!searchSpaceId, - }); - - const { data: user } = useAtomValue(currentUserAtom); - - // Fetch notes - const { data: notesData, refetch: refetchNotes } = useQuery({ - queryKey: ["notes", searchSpaceId], - queryFn: () => - notesApiService.getNotes({ - search_space_id: Number(searchSpaceId), - page_size: 4, // Get 4 notes for compact sidebar - }), - enabled: !!searchSpaceId, - }); - - const [showDeleteDialog, setShowDeleteDialog] = useState(false); - const [threadToDelete, setThreadToDelete] = 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); - - // Transform threads to the format expected by AppSidebar - const recentChats = useMemo(() => { - if (!threadsData?.threads) return []; - - // Threads are already sorted by updated_at desc from the API - return threadsData.threads.map((thread) => ({ - name: thread.title || `Chat ${thread.id}`, - url: `/dashboard/${searchSpaceId}/new-chat/${thread.id}`, - icon: "MessageCircleMore", - id: thread.id, - search_space_id: Number(searchSpaceId), - actions: [ - { - name: "Delete", - icon: "Trash2", - onClick: () => { - setThreadToDelete({ - id: thread.id, - name: thread.title || `Chat ${thread.id}`, - }); - setShowDeleteDialog(true); - }, - }, - ], - })); - }, [threadsData, searchSpaceId]); - - // Handle delete thread - const handleDeleteThread = useCallback(async () => { - if (!threadToDelete) return; - - setIsDeletingThread(true); - try { - await deleteThread(threadToDelete.id); - // Invalidate threads query to refresh the list - queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); - // Only navigate to new-chat if the deleted chat is currently open - if (currentChatId === threadToDelete.id) { - router.push(`/dashboard/${searchSpaceId}/new-chat`); - } - } catch (error) { - console.error("Error deleting thread:", error); - } finally { - setIsDeletingThread(false); - setShowDeleteDialog(false); - setThreadToDelete(null); - } - }, [threadToDelete, queryClient, searchSpaceId, router, currentChatId]); - - // 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 (threadError) { - return [ - { - name: t("error_loading_chats"), - url: "#", - icon: "AlertCircle", - id: 0, - search_space_id: Number(searchSpaceId), - actions: [ - { - name: tCommon("retry"), - icon: "RefreshCw", - onClick: () => refetchThreads(), - }, - ], - }, - ]; - } - - return []; - }, [threadError, searchSpaceId, refetchThreads, t, tCommon]); - - // Use fallback chats if there's an error or no chats - const displayChats = recentChats.length > 0 ? recentChats : fallbackChats; - - // Transform notes to the format expected by NavNotes - const recentNotes = useMemo(() => { - if (!notesData?.items) return []; - - // Sort notes by updated_at (most recent first), fallback to created_at if updated_at is null - const sortedNotes = [...notesData.items].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) - }); - - // Limit to 4 notes for compact sidebar - return sortedNotes.slice(0, 4).map((note) => ({ - name: note.title, - url: `/dashboard/${note.search_space_id}/editor/${note.id}`, - icon: "FileText", - id: note.id, - search_space_id: note.search_space_id, - actions: [ - { - name: "Delete", - icon: "Trash2", - onClick: () => { - setNoteToDelete({ - id: note.id, - name: note.title, - search_space_id: note.search_space_id, - }); - setShowDeleteNoteDialog(true); - }, - }, - ], - })); - }, [notesData]); - - // Handle add note - check for unsaved changes first - const handleAddNote = useCallback(() => { - const newNoteUrl = `/dashboard/${searchSpaceId}/editor/new`; - - if (hasUnsavedEditorChanges) { - // Set pending navigation - the editor will show the unsaved changes dialog - setPendingNavigation(newNoteUrl); - } else { - // No unsaved changes, navigate directly - router.push(newNoteUrl); - } - }, [router, searchSpaceId, hasUnsavedEditorChanges, setPendingNavigation]); - - // Memoized updated navSecondary - const updatedNavSecondary = useMemo(() => { - const updated = [...navSecondary]; - if (updated.length > 0) { - updated[0] = { - ...updated[0], - title: - searchSpace?.name || - (isLoadingSearchSpace - ? tCommon("loading") - : searchSpaceError - ? t("error_loading_space") - : t("unknown_search_space")), - }; - } - return updated; - }, [navSecondary, searchSpace?.name, isLoadingSearchSpace, searchSpaceError, t, tCommon]); - - // Prepare page usage data - const pageUsage = user - ? { - pagesUsed: user.pages_used, - pagesLimit: user.pages_limit, - } - : undefined; - - return ( - <> - - - {/* Delete Confirmation Dialog */} - - - - - - {t("delete_chat")} - - - {t("delete_chat_confirm")} {threadToDelete?.name} - ? {t("action_cannot_undone")} - - - - - - - - - - {/* 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 deleted file mode 100644 index 02459f2b9..000000000 --- a/surfsense_web/components/sidebar/all-chats-sidebar.tsx +++ /dev/null @@ -1,443 +0,0 @@ -"use client"; - -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { format } from "date-fns"; -import { - ArchiveIcon, - Loader2, - MessageCircleMore, - MoreHorizontal, - RotateCcwIcon, - Search, - Trash2, - X, -} from "lucide-react"; -import { AnimatePresence, motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useState } from "react"; -import { createPortal } from "react-dom"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; -import { useDebouncedValue } from "@/hooks/use-debounced-value"; -import { - deleteThread, - fetchThreads, - searchThreads, - type ThreadListItem, - updateThread, -} from "@/lib/chat/thread-persistence"; -import { cn } from "@/lib/utils"; - -interface AllChatsSidebarProps { - open: boolean; - onOpenChange: (open: boolean) => void; - searchSpaceId: string; - onCloseMobileSidebar?: () => void; -} - -export function AllChatsSidebar({ - open, - onOpenChange, - searchSpaceId, - onCloseMobileSidebar, -}: AllChatsSidebarProps) { - const t = useTranslations("sidebar"); - const router = useRouter(); - const params = useParams(); - const queryClient = useQueryClient(); - - // Get the current chat ID from URL to check if user is deleting the currently open chat - const currentChatId = Array.isArray(params.chat_id) - ? Number(params.chat_id[0]) - : params.chat_id - ? Number(params.chat_id) - : null; - const [deletingThreadId, setDeletingThreadId] = useState(null); - const [archivingThreadId, setArchivingThreadId] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [showArchived, setShowArchived] = useState(false); - const [mounted, setMounted] = useState(false); - const [openDropdownId, setOpenDropdownId] = useState(null); - const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); - - const isSearchMode = !!debouncedSearchQuery.trim(); - - // Handle mounting for portal - useEffect(() => { - setMounted(true); - }, []); - - // Handle escape key - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape" && open) { - onOpenChange(false); - } - }; - document.addEventListener("keydown", handleEscape); - return () => document.removeEventListener("keydown", handleEscape); - }, [open, onOpenChange]); - - // Lock body scroll when open - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - - // Fetch all threads (when not searching) - const { - data: threadsData, - error: threadsError, - isLoading: isLoadingThreads, - } = useQuery({ - queryKey: ["all-threads", searchSpaceId], - queryFn: () => fetchThreads(Number(searchSpaceId)), - enabled: !!searchSpaceId && open && !isSearchMode, - }); - - // Search threads (when searching) - const { - data: searchData, - error: searchError, - isLoading: isLoadingSearch, - } = useQuery({ - queryKey: ["search-threads", searchSpaceId, debouncedSearchQuery], - queryFn: () => searchThreads(Number(searchSpaceId), debouncedSearchQuery.trim()), - enabled: !!searchSpaceId && open && isSearchMode, - }); - - // Handle thread navigation - const handleThreadClick = useCallback( - (threadId: number) => { - router.push(`/dashboard/${searchSpaceId}/new-chat/${threadId}`); - onOpenChange(false); - // Also close the main sidebar on mobile - onCloseMobileSidebar?.(); - }, - [router, onOpenChange, searchSpaceId, onCloseMobileSidebar] - ); - - // Handle thread deletion - const handleDeleteThread = useCallback( - async (threadId: number) => { - setDeletingThreadId(threadId); - try { - await deleteThread(threadId); - toast.success(t("chat_deleted") || "Chat deleted successfully"); - queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); - - // If the deleted chat is currently open, close sidebar first then redirect - if (currentChatId === threadId) { - onOpenChange(false); - // Wait for sidebar close animation to complete before navigating - setTimeout(() => { - router.push(`/dashboard/${searchSpaceId}/new-chat`); - }, 250); - } - } catch (error) { - console.error("Error deleting thread:", error); - toast.error(t("error_deleting_chat") || "Failed to delete chat"); - } finally { - setDeletingThreadId(null); - } - }, - [queryClient, searchSpaceId, t, currentChatId, router, onOpenChange] - ); - - // Handle thread archive/unarchive - const handleToggleArchive = useCallback( - async (threadId: number, currentlyArchived: boolean) => { - setArchivingThreadId(threadId); - try { - await updateThread(threadId, { archived: !currentlyArchived }); - toast.success( - currentlyArchived - ? t("chat_unarchived") || "Chat restored" - : t("chat_archived") || "Chat archived" - ); - queryClient.invalidateQueries({ queryKey: ["all-threads", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["search-threads", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["threads", searchSpaceId] }); - } catch (error) { - console.error("Error archiving thread:", error); - toast.error(t("error_archiving_chat") || "Failed to archive chat"); - } finally { - setArchivingThreadId(null); - } - }, - [queryClient, searchSpaceId, t] - ); - - // Clear search - const handleClearSearch = useCallback(() => { - setSearchQuery(""); - }, []); - - // Determine which data source to use - let threads: ThreadListItem[] = []; - if (isSearchMode) { - threads = searchData ?? []; - } else if (threadsData) { - threads = showArchived ? threadsData.archived_threads : threadsData.threads; - } - - const isLoading = isSearchMode ? isLoadingSearch : isLoadingThreads; - const error = isSearchMode ? searchError : threadsError; - - // Get counts for tabs - const activeCount = threadsData?.threads.length ?? 0; - const archivedCount = threadsData?.archived_threads.length ?? 0; - - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - {/* Backdrop */} - onOpenChange(false)} - aria-hidden="true" - /> - - {/* Panel */} - - {/* Header */} -
-
-

{t("all_chats") || "All Chats"}

- -
- - {/* Search Input */} -
- - setSearchQuery(e.target.value)} - className="pl-9 pr-8 h-9" - /> - {searchQuery && ( - - )} -
-
- - {/* Tab toggle for active/archived (only show when not searching) */} - {!isSearchMode && ( -
- - -
- )} - - {/* Scrollable Content */} -
- {isLoading ? ( -
- -
- ) : error ? ( -
- {t("error_loading_chats") || "Error loading chats"} -
- ) : threads.length > 0 ? ( -
- {threads.map((thread) => { - const isDeleting = deletingThreadId === thread.id; - const isArchiving = archivingThreadId === thread.id; - const isBusy = isDeleting || isArchiving; - const isActive = currentChatId === thread.id; - - return ( -
- {/* Main clickable area for navigation */} - - - - - -

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

-
-
- - {/* Actions dropdown */} - setOpenDropdownId(isOpen ? thread.id : null)} - > - - - - - handleToggleArchive(thread.id, thread.archived)} - disabled={isArchiving} - > - {thread.archived ? ( - <> - - {t("unarchive") || "Restore"} - - ) : ( - <> - - {t("archive") || "Archive"} - - )} - - - handleDeleteThread(thread.id)} - className="text-destructive focus:text-destructive" - > - - {t("delete") || "Delete"} - - - -
- ); - })} -
- ) : isSearchMode ? ( -
- -

- {t("no_chats_found") || "No chats found"} -

-

- {t("try_different_search") || "Try a different search term"} -

-
- ) : ( -
- -

- {showArchived - ? t("no_archived_chats") || "No archived chats" - : t("no_chats") || "No chats yet"} -

- {!showArchived && ( -

- {t("start_new_chat_hint") || "Start a new chat from the chat page"} -

- )} -
- )} -
-
- - )} -
, - document.body - ); -} diff --git a/surfsense_web/components/sidebar/all-notes-sidebar.tsx b/surfsense_web/components/sidebar/all-notes-sidebar.tsx deleted file mode 100644 index 67d1b4ba6..000000000 --- a/surfsense_web/components/sidebar/all-notes-sidebar.tsx +++ /dev/null @@ -1,407 +0,0 @@ -"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 { AnimatePresence, motion } from "motion/react"; -import { useParams, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { createPortal } from "react-dom"; -import { Button } from "@/components/ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -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"; -import { cn } from "@/lib/utils"; - -interface AllNotesSidebarProps { - open: boolean; - onOpenChange: (open: boolean) => void; - searchSpaceId: string; - onAddNote?: () => void; - onCloseMobileSidebar?: () => void; -} - -export function AllNotesSidebar({ - open, - onOpenChange, - searchSpaceId, - onAddNote, - onCloseMobileSidebar, -}: AllNotesSidebarProps) { - const t = useTranslations("sidebar"); - const router = useRouter(); - const params = useParams(); - const queryClient = useQueryClient(); - - // Get the current note ID from URL to highlight the open note - const currentNoteId = params.note_id ? Number(params.note_id) : null; - const [deletingNoteId, setDeletingNoteId] = useState(null); - const [searchQuery, setSearchQuery] = useState(""); - const [mounted, setMounted] = useState(false); - const [openDropdownId, setOpenDropdownId] = useState(null); - const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); - - // Handle mounting for portal - useEffect(() => { - setMounted(true); - }, []); - - // Handle escape key - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key === "Escape" && open) { - onOpenChange(false); - } - }; - document.addEventListener("keydown", handleEscape); - return () => document.removeEventListener("keydown", handleEscape); - }, [open, onOpenChange]); - - // Lock body scroll when open - useEffect(() => { - if (open) { - document.body.style.overflow = "hidden"; - } else { - document.body.style.overflow = ""; - } - return () => { - document.body.style.overflow = ""; - }; - }, [open]); - - // Fetch all notes (when no search query) - const { - data: notesData, - error: notesError, - isLoading: isLoadingNotes, - } = useQuery({ - queryKey: ["all-notes", searchSpaceId], - queryFn: () => - notesApiService.getNotes({ - search_space_id: Number(searchSpaceId), - page_size: 1000, - }), - enabled: !!searchSpaceId && open && !debouncedSearchQuery, - }); - - // Search notes (when there's a search query) - const { - data: searchData, - error: searchError, - isLoading: isSearching, - } = useQuery({ - queryKey: ["search-notes", searchSpaceId, debouncedSearchQuery], - queryFn: () => - documentsApiService.searchDocuments({ - queryParams: { - search_space_id: Number(searchSpaceId), - document_types: ["NOTE"], - title: debouncedSearchQuery, - page_size: 100, - }, - }), - enabled: !!searchSpaceId && open && !!debouncedSearchQuery, - }); - - // Handle note navigation - const handleNoteClick = useCallback( - (noteId: number, noteSearchSpaceId: number) => { - router.push(`/dashboard/${noteSearchSpaceId}/editor/${noteId}`); - onOpenChange(false); - // Also close the main sidebar on mobile - onCloseMobileSidebar?.(); - }, - [router, onOpenChange, onCloseMobileSidebar] - ); - - // Handle note deletion - const handleDeleteNote = useCallback( - async (noteId: number, noteSearchSpaceId: number) => { - setDeletingNoteId(noteId); - try { - await notesApiService.deleteNote({ - search_space_id: noteSearchSpaceId, - note_id: noteId, - }); - queryClient.invalidateQueries({ queryKey: ["all-notes", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["notes", searchSpaceId] }); - queryClient.invalidateQueries({ queryKey: ["search-notes", searchSpaceId] }); - } catch (error) { - console.error("Error deleting note:", error); - } finally { - setDeletingNoteId(null); - } - }, - [queryClient, searchSpaceId] - ); - - // Clear search - const handleClearSearch = useCallback(() => { - setSearchQuery(""); - }, []); - - // Determine which data to show - const isSearchMode = !!debouncedSearchQuery; - const isLoading = isSearchMode ? isSearching : isLoadingNotes; - const error = isSearchMode ? searchError : notesError; - - // 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) { - 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 ?? []; - } - - // 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]); - - if (!mounted) return null; - - return createPortal( - - {open && ( - <> - {/* Backdrop */} - onOpenChange(false)} - aria-hidden="true" - /> - - {/* Panel */} - - {/* Header */} -
-
-

{t("all_notes") || "All Notes"}

- -
- - {/* Search Input */} -
- - setSearchQuery(e.target.value)} - className="pl-9 pr-8 h-9" - /> - {searchQuery && ( - - )} -
-
- - {/* Scrollable Content */} -
- {isLoading ? ( -
- -
- ) : error ? ( -
- {t("error_loading_notes") || "Error loading notes"} -
- ) : notes.length > 0 ? ( -
- {notes.map((note) => { - const isDeleting = deletingNoteId === note.id; - const isActive = currentNoteId === note.id; - - return ( -
- {/* 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 */} - setOpenDropdownId(isOpen ? note.id : null)} - > - - - - - handleDeleteNote(note.id, note.search_space_id)} - className="text-destructive focus:text-destructive" - > - - {t("delete") || "Delete"} - - - -
- ); - })} -
- ) : isSearchMode ? ( -
- -

- {t("no_results_found") || "No notes found"} -

-

- {t("try_different_search") || "Try a different search term"} -

-
- ) : ( -
- -

- {t("no_notes") || "No notes yet"} -

- {onAddNote && ( - - )} -
- )} -
- - {/* Footer with Add Note button */} - {onAddNote && notes.length > 0 && ( -
- -
- )} -
- - )} -
, - document.body - ); -} diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx deleted file mode 100644 index 8030cb9d2..000000000 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ /dev/null @@ -1,473 +0,0 @@ -"use client"; - -import { useAtomValue } from "jotai"; -import { - AlertCircle, - ArrowLeftRight, - BookOpen, - Cable, - ChevronsUpDown, - Database, - ExternalLink, - FileStack, - FileText, - Info, - LogOut, - Logs, - type LucideIcon, - MessageCircle, - MessageCircleMore, - MoonIcon, - Podcast, - RefreshCw, - Settings2, - SquareLibrary, - SquareTerminal, - SunIcon, - Trash2, - Undo2, - UserPlus, - Users, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useTheme } from "next-themes"; -import { memo, useEffect, useMemo, useState } from "react"; -import { currentUserAtom } from "@/atoms/user/user-query.atoms"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { resetUser, trackLogout } from "@/lib/posthog/events"; - -/** - * Generates a consistent color based on a string (email) - */ -function stringToColor(str: string): string { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - const colors = [ - "#6366f1", // indigo - "#8b5cf6", // violet - "#a855f7", // purple - "#d946ef", // fuchsia - "#ec4899", // pink - "#f43f5e", // rose - "#ef4444", // red - "#f97316", // orange - "#eab308", // yellow - "#84cc16", // lime - "#22c55e", // green - "#14b8a6", // teal - "#06b6d4", // cyan - "#0ea5e9", // sky - "#3b82f6", // blue - ]; - return colors[Math.abs(hash) % colors.length]; -} - -/** - * Gets initials from an email address - */ -function getInitials(email: string): string { - const name = email.split("@")[0]; - const parts = name.split(/[._-]/); - if (parts.length >= 2) { - return (parts[0][0] + parts[1][0]).toUpperCase(); - } - return name.slice(0, 2).toUpperCase(); -} - -/** - * Dynamic avatar component that generates an SVG based on email - */ -function UserAvatar({ email, size = 32 }: { email: string; size?: number }) { - const bgColor = stringToColor(email); - const initials = getInitials(email); - - return ( - - Avatar for {email} - - - {initials} - - - ); -} - -import { NavChats } from "@/components/sidebar/nav-chats"; -import { NavMain } from "@/components/sidebar/nav-main"; -import { NavNotes } from "@/components/sidebar/nav-notes"; -import { NavSecondary } from "@/components/sidebar/nav-secondary"; -import { PageUsageDisplay } from "@/components/sidebar/page-usage-display"; -import { - Sidebar, - SidebarContent, - SidebarFooter, - SidebarHeader, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; - -// Map of icon names to their components -export const iconMap: Record = { - BookOpen, - Cable, - Database, - FileStack, - Undo2, - MessageCircleMore, - Settings2, - SquareLibrary, - FileText, - SquareTerminal, - AlertCircle, - Info, - ExternalLink, - Trash2, - Podcast, - Users, - RefreshCw, - MessageCircle, - Logs, -}; - -const defaultData = { - user: { - name: "Surf", - email: "m@example.com", - avatar: "/icon-128.png", - }, - navMain: [ - { - title: "Chat", - url: "#", - icon: "SquareTerminal", - isActive: true, - items: [], - }, - { - title: "Sources", - url: "#", - icon: "Database", - items: [ - { - title: "Manage Documents", - url: "#", - }, - { - title: "Manage Connectors", - url: "#", - }, - ], - }, - ], - navSecondary: [ - { - title: "SEARCH SPACE", - url: "#", - icon: "LifeBuoy", - }, - ], - RecentChats: [ - { - name: "Design Engineering", - url: "#", - icon: "MessageCircleMore", - id: 1001, - }, - { - name: "Sales & Marketing", - url: "#", - icon: "MessageCircleMore", - id: 1002, - }, - { - name: "Travel", - url: "#", - icon: "MessageCircleMore", - id: 1003, - }, - ], - RecentNotes: [ - { - name: "Meeting Notes", - url: "#", - icon: "FileText", - id: 2001, - }, - { - name: "Project Ideas", - url: "#", - icon: "FileText", - id: 2002, - }, - ], -}; - -interface AppSidebarProps extends React.ComponentProps { - searchSpaceId?: string; - navMain?: { - title: string; - url: string; - icon: string; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; - }[]; - navSecondary?: { - title: string; - url: string; - icon: string; - }[]; - RecentChats?: { - name: string; - url: string; - icon: string; - id?: number; - search_space_id?: number; - actions?: { - name: string; - icon: string; - onClick: () => void; - }[]; - }[]; - RecentNotes?: { - name: string; - url: string; - icon: string; - id?: number; - search_space_id?: number; - actions?: { - name: string; - icon: string; - onClick: () => void; - }[]; - }[]; - user?: { - name: string; - email: string; - avatar: string; - }; - pageUsage?: { - pagesUsed: number; - pagesLimit: number; - }; - onAddNote?: () => void; -} - -// Memoized AppSidebar component for better performance -export const AppSidebar = memo(function AppSidebar({ - searchSpaceId, - navMain = defaultData.navMain, - navSecondary = defaultData.navSecondary, - RecentChats = defaultData.RecentChats, - RecentNotes = defaultData.RecentNotes, - pageUsage, - onAddNote, - ...props -}: AppSidebarProps) { - const router = useRouter(); - const { theme, setTheme } = useTheme(); - const { data: user, isPending: isLoadingUser } = useAtomValue(currentUserAtom); - const [isClient, setIsClient] = useState(false); - - useEffect(() => { - setIsClient(true); - }, []); - - // Process navMain to resolve icon names to components - const processedNavMain = useMemo(() => { - return navMain.map((item) => ({ - ...item, - icon: iconMap[item.icon] || SquareTerminal, - })); - }, [navMain]); - - // Process navSecondary to resolve icon names to components - const processedNavSecondary = useMemo(() => { - return navSecondary.map((item) => ({ - ...item, - icon: iconMap[item.icon] || Undo2, - })); - }, [navSecondary]); - - // Process RecentChats to resolve icon names to components - const processedRecentChats = useMemo(() => { - return ( - RecentChats?.map((item) => ({ - ...item, - icon: iconMap[item.icon] || MessageCircleMore, - })) || [] - ); - }, [RecentChats]); - - // Process RecentNotes to resolve icon names to components - const processedRecentNotes = useMemo(() => { - return ( - RecentNotes?.map((item) => ({ - ...item, - icon: iconMap[item.icon] || FileText, - })) || [] - ); - }, [RecentNotes]); - - // Get user display name from email - const userDisplayName = user?.email ? user.email.split("@")[0] : "User"; - const userEmail = user?.email || (isLoadingUser ? "Loading..." : "Unknown"); - - const handleLogout = () => { - try { - // Track logout event and reset PostHog identity - trackLogout(); - resetUser(); - - if (typeof window !== "undefined") { - localStorage.removeItem("surfsense_bearer_token"); - router.push("/"); - } - } catch (error) { - console.error("Error during logout:", error); - router.push("/"); - } - }; - - return ( - - - - - - - -
- {user?.email ? ( - - ) : ( -
- )} -
-
- {userDisplayName} - {userEmail} -
- - - - - -
-
- {user?.email ? ( - - ) : ( -
- )} -
-
- {userDisplayName} - {userEmail} -
-
- - - - {searchSpaceId && ( - <> - router.push(`/dashboard/${searchSpaceId}/settings`)} - > - - Settings - - router.push(`/dashboard/${searchSpaceId}/team`)} - > - - Invite members - - - )} - router.push("/dashboard")}> - - Switch workspace - - - - - {isClient && ( - setTheme(theme === "dark" ? "light" : "dark")}> - {theme === "dark" ? ( - - ) : ( - - )} - {theme === "dark" ? "Light mode" : "Dark mode"} - - )} - - - - - Logout - - - - - - - - - - - - - - - - {pageUsage && ( - - )} - - - - ); -}); diff --git a/surfsense_web/components/sidebar/nav-chats.tsx b/surfsense_web/components/sidebar/nav-chats.tsx deleted file mode 100644 index ba0004fc8..000000000 --- a/surfsense_web/components/sidebar/nav-chats.tsx +++ /dev/null @@ -1,237 +0,0 @@ -"use client"; - -import { - ChevronRight, - FolderOpen, - Loader2, - type LucideIcon, - MessageCircleMore, - MoreHorizontal, - RefreshCw, - Trash2, -} from "lucide-react"; -import { usePathname, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { cn } from "@/lib/utils"; -import { AllChatsSidebar } from "./all-chats-sidebar"; - -interface ChatAction { - name: string; - icon: string; - onClick: () => void; -} - -interface ChatItem { - name: string; - url: string; - icon: LucideIcon; - id?: number; - search_space_id?: number; - actions?: ChatAction[]; -} - -interface NavChatsProps { - chats: ChatItem[]; - defaultOpen?: boolean; - searchSpaceId?: string; -} - -// Map of icon names to their components -const actionIconMap: Record = { - MessageCircleMore, - Trash2, - MoreHorizontal, - RefreshCw, -}; - -export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) { - const t = useTranslations("sidebar"); - const router = useRouter(); - const pathname = usePathname(); - const { setOpenMobile } = useSidebar(); - const [isDeleting, setIsDeleting] = useState(null); - const [isOpen, setIsOpen] = useState(defaultOpen); - const [isAllChatsSidebarOpen, setIsAllChatsSidebarOpen] = useState(false); - - // Handle chat deletion with loading state - const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => { - setIsDeleting(chatId); - try { - await deleteAction(); - } finally { - setIsDeleting(null); - } - }, []); - - // Handle chat navigation - const handleChatClick = useCallback( - (url: string) => { - router.push(url); - }, - [router] - ); - - return ( - - -
- - - - {t("recent_chats") || "Recent Chats"} - - - - {/* Action buttons - always visible on hover */} -
- {searchSpaceId && chats.length > 0 && ( - - )} -
-
- - - {chats.length > 0 ? ( - - - {chats.map((chat) => { - const isDeletingChat = isDeleting === chat.id; - const isActive = pathname === chat.url; - - return ( - - {/* Main navigation button */} - handleChatClick(chat.url)} - disabled={isDeletingChat} - className={cn( - "pr-8", // Make room for the action button - isActive && "bg-sidebar-accent text-sidebar-accent-foreground", - isDeletingChat && "opacity-50" - )} - > - - {chat.name} - - - {/* Actions dropdown - positioned absolutely */} - {chat.actions && chat.actions.length > 0 && ( -
- - - - - - {chat.actions.map((action, actionIndex) => { - const ActionIcon = actionIconMap[action.icon] || MessageCircleMore; - const isDeleteAction = action.name.toLowerCase().includes("delete"); - - return ( - { - if (isDeleteAction) { - handleDeleteChat(chat.id || 0, action.onClick); - } else { - action.onClick(); - } - }} - disabled={isDeletingChat} - className={ - isDeleteAction - ? "text-destructive focus:text-destructive" - : "" - } - > - - - {isDeletingChat && isDeleteAction - ? t("deleting") || "Deleting..." - : action.name} - - - ); - })} - - -
- )} -
- ); - })} -
-
- ) : ( -
- - {t("no_recent_chats") || "No recent chats"} -
- )} -
-
- - {/* All Chats Sheet */} - {searchSpaceId && ( - setOpenMobile(false)} - /> - )} -
- ); -} diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx deleted file mode 100644 index a0dbe912f..000000000 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ /dev/null @@ -1,207 +0,0 @@ -"use client"; - -import { ChevronRight, type LucideIcon } from "lucide-react"; -import { usePathname } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; - -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - SidebarMenuSub, - SidebarMenuSubButton, - SidebarMenuSubItem, -} from "@/components/ui/sidebar"; - -interface NavItem { - title: string; - url: string; - icon: LucideIcon; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; -} - -interface NavMainProps { - items: NavItem[]; -} - -export function NavMain({ items }: NavMainProps) { - const t = useTranslations("nav_menu"); - const pathname = usePathname(); - - // Translation function that handles both exact matches and fallback to original - const translateTitle = (title: string): string => { - const titleMap: Record = { - Researcher: "researcher", - "Manage LLMs": "manage_llms", - Sources: "sources", - "Manage Documents": "manage_documents", - "Manage Connectors": "manage_connectors", - Podcasts: "podcasts", - Logs: "logs", - Platform: "platform", - Team: "team", - }; - - const key = titleMap[title]; - return key ? t(key) : title; - }; - - // Check if an item is active based on pathname - const isItemActive = useCallback( - (item: NavItem): boolean => { - if (!pathname) return false; - - // For items without sub-items, check if pathname matches or starts with the URL - if (!item.items?.length) { - // Chat item: active ONLY when on new-chat page without a specific chat ID - // (i.e., exactly /dashboard/{id}/new-chat, not /dashboard/{id}/new-chat/123) - if (item.url.includes("/new-chat")) { - // Match exactly the new-chat base URL (ends with /new-chat) - return pathname.endsWith("/new-chat"); - } - // Logs item: active when on logs page - if (item.url.includes("/logs")) { - return pathname.includes("/logs"); - } - // Check exact match or prefix match - return pathname === item.url || pathname.startsWith(`${item.url}/`); - } - - // For items with sub-items (like Sources), check if any sub-item URL matches - return item.items.some( - (subItem) => pathname === subItem.url || pathname.startsWith(subItem.url) - ); - }, - [pathname] - ); - - // 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 })); - }, []); - - return ( - - {translateTitle("Platform")} - - {memoizedItems.map((item, index) => { - const translatedTitle = translateTitle(item.title); - const hasSub = !!item.items?.length; - const isActive = isItemActive(item); - const isItemOpen = expandedItems[item.title] ?? isActive ?? false; - return ( - handleOpenChange(item.title, open) : undefined} - defaultOpen={!hasSub ? isActive : undefined} - > - - {hasSub ? ( - // When the item has children, make the whole row a collapsible trigger - <> - - - - - - - - - - Toggle submenu - - - - - - {item.items?.map((subItem, subIndex) => { - const translatedSubTitle = translateTitle(subItem.title); - const isDocumentsLink = - subItem.title === "Manage Documents" || - translatedSubTitle.toLowerCase().includes("documents"); - return ( - - - - {translatedSubTitle} - - - - ); - })} - - - - ) : ( - // Leaf item: treat as a normal link - - - - {translatedTitle} - - - )} - - - ); - })} - - - ); -} diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx deleted file mode 100644 index e9f94fe80..000000000 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ /dev/null @@ -1,287 +0,0 @@ -"use client"; - -import { - ChevronRight, - FileText, - FolderOpen, - Loader2, - type LucideIcon, - MoreHorizontal, - Plus, - Trash2, -} from "lucide-react"; -import { usePathname, useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; -import { Button } from "@/components/ui/button"; -import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; -import { useLogsSummary } from "@/hooks/use-logs"; -import { cn } from "@/lib/utils"; -import { AllNotesSidebar } from "./all-notes-sidebar"; - -interface NoteAction { - name: string; - icon: string; - onClick: () => void; -} - -interface NoteItem { - name: string; - url: string; - icon: LucideIcon; - id?: number; - search_space_id?: number; - actions?: NoteAction[]; -} - -interface NavNotesProps { - notes: NoteItem[]; - onAddNote?: () => void; - defaultOpen?: boolean; - searchSpaceId?: string; -} - -// Map of icon names to their components -const actionIconMap: Record = { - FileText, - Trash2, - MoreHorizontal, -}; - -export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId }: NavNotesProps) { - const t = useTranslations("sidebar"); - const router = useRouter(); - const pathname = usePathname(); - const { setOpenMobile } = useSidebar(); - const [isDeleting, setIsDeleting] = useState(null); - const [isOpen, setIsOpen] = useState(defaultOpen); - const [isAllNotesSidebarOpen, setIsAllNotesSidebarOpen] = useState(false); - - // Poll for active reindexing tasks to show inline loading indicators - // Smart polling: only polls when there are active tasks, stops when idle - const { summary } = useLogsSummary(searchSpaceId ? Number(searchSpaceId) : 0, 24, { - enablePolling: true, - refetchInterval: 5000, // Poll every 5 seconds when tasks are active - }); - - // Create a Set of document IDs that are currently being reindexed - const reindexingDocumentIds = useMemo(() => { - if (!summary?.active_tasks) return new Set(); - return new Set( - summary.active_tasks - .filter((task) => task.document_id != null) - .map((task) => task.document_id as number) - ); - }, [summary?.active_tasks]); - - // Handle note deletion with loading state - const handleDeleteNote = useCallback(async (noteId: number, deleteAction: () => void) => { - setIsDeleting(noteId); - try { - await deleteAction(); - } finally { - setIsDeleting(null); - } - }, []); - - // Handle note navigation - const handleNoteClick = useCallback( - (url: string) => { - router.push(url); - }, - [router] - ); - - return ( - - -
- - - - {t("notes") || "Notes"} - - - - {/* Action buttons - always visible on hover */} -
- {searchSpaceId && notes.length > 0 && ( - - )} - {onAddNote && ( - - )} -
-
- - - - - {notes.length > 0 ? ( - notes.map((note) => { - const isDeletingNote = isDeleting === note.id; - const isActive = pathname === note.url; - const isReindexing = note.id ? reindexingDocumentIds.has(note.id) : false; - - return ( - - {/* Main navigation button */} - handleNoteClick(note.url)} - disabled={isDeletingNote} - className={cn( - "pr-8", // Make room for the action button - isActive && "bg-sidebar-accent text-sidebar-accent-foreground", - isDeletingNote && "opacity-50" - )} - > - {isReindexing ? ( - - ) : ( - - )} - {note.name} - - - {/* Actions dropdown - positioned absolutely */} - {note.actions && note.actions.length > 0 && ( -
- - - - - - {note.actions.map((action, actionIndex) => { - const ActionIcon = actionIconMap[action.icon] || FileText; - const isDeleteAction = action.name.toLowerCase().includes("delete"); - - return ( - { - if (isDeleteAction) { - handleDeleteNote(note.id || 0, action.onClick); - } else { - action.onClick(); - } - }} - disabled={isDeletingNote} - className={ - isDeleteAction - ? "text-destructive focus:text-destructive" - : "" - } - > - - - {isDeletingNote && isDeleteAction - ? t("deleting") || "Deleting..." - : action.name} - - - ); - })} - - -
- )} -
- ); - }) - ) : ( - - {onAddNote ? ( - - - {t("create_new_note") || "Create a new note"} - - ) : ( - - - {t("no_notes") || "No notes yet"} - - )} - - )} -
-
-
-
- - {/* All Notes Sheet */} - {searchSpaceId && ( - setOpenMobile(false)} - /> - )} -
- ); -} diff --git a/surfsense_web/components/sidebar/nav-secondary.tsx b/surfsense_web/components/sidebar/nav-secondary.tsx deleted file mode 100644 index 23aeabc38..000000000 --- a/surfsense_web/components/sidebar/nav-secondary.tsx +++ /dev/null @@ -1,59 +0,0 @@ -"use client"; - -import type { LucideIcon } from "lucide-react"; -import { useTranslations } from "next-intl"; -import type * as React from "react"; -import { useMemo } from "react"; - -import { - SidebarGroup, - SidebarGroupLabel, - SidebarMenu, - SidebarMenuButton, - SidebarMenuItem, -} from "@/components/ui/sidebar"; - -interface NavSecondaryItem { - title: string; - url: string; - icon: LucideIcon; -} - -export function NavSecondary({ - items, - ...props -}: { - items: NavSecondaryItem[]; -} & React.ComponentPropsWithoutRef) { - const t = useTranslations("sidebar"); - - // Memoize items to prevent unnecessary re-renders - const memoizedItems = useMemo(() => items, [items]); - - return ( - - {t("search_space")} - - {memoizedItems.map((item, index) => ( - - {item.url === "#" ? ( - // Non-interactive display item (e.g., search space name) -
- - {item.title} -
- ) : ( - // Interactive link item - - - - {item.title} - - - )} -
- ))} -
-
- ); -} diff --git a/surfsense_web/components/sidebar/page-usage-display.tsx b/surfsense_web/components/sidebar/page-usage-display.tsx deleted file mode 100644 index 6c640c0aa..000000000 --- a/surfsense_web/components/sidebar/page-usage-display.tsx +++ /dev/null @@ -1,57 +0,0 @@ -"use client"; - -import { Mail } from "lucide-react"; -import { Progress } from "@/components/ui/progress"; -import { - SidebarGroup, - SidebarGroupContent, - SidebarGroupLabel, - useSidebar, -} from "@/components/ui/sidebar"; - -interface PageUsageDisplayProps { - pagesUsed: number; - pagesLimit: number; -} - -export function PageUsageDisplay({ pagesUsed, pagesLimit }: PageUsageDisplayProps) { - const { state } = useSidebar(); - const usagePercentage = (pagesUsed / pagesLimit) * 100; - const isCollapsed = state === "collapsed"; - - return ( - - - Page Usage - - -
- {isCollapsed ? ( - // Show only a compact progress indicator when collapsed -
- -
- ) : ( - // Show full details when expanded - <> -
- - {pagesUsed.toLocaleString()} / {pagesLimit.toLocaleString()} pages - - {usagePercentage.toFixed(0)}% -
- - - - Contact to increase limits - - - )} -
-
-
- ); -}