From 79e552520a57f399730186ad4dd9e391a5b6487e Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 03:53:40 +0530 Subject: [PATCH 1/6] refactor: remove old chat components and implement new sidebar structure - Deleted `chats-client.tsx` and `page.tsx` as part of the chat management overhaul. - Introduced `AllChatsSidebar` and `NavChats` components for improved chat navigation and management. - Updated sidebar to integrate new chat components and removed the deprecated `NavProjects`. - Enhanced chat deletion handling and added loading states for better user experience. - Added new translation keys for chat-related UI strings. --- .../[search_space_id]/chats/chats-client.tsx | 458 ------------------ .../[search_space_id]/chats/page.tsx | 25 - .../researcher/[[...chat_id]]/page.tsx | 6 +- .../atoms/chats/chat-mutation.atoms.ts | 4 +- .../components/sidebar/all-chats-sidebar.tsx | 241 +++++++++ .../components/sidebar/app-sidebar.tsx | 13 +- .../components/sidebar/nav-chats.tsx | 230 +++++++++ .../components/sidebar/nav-projects.tsx | 177 ------- surfsense_web/hooks/use-chat.ts | 4 +- surfsense_web/messages/en.json | 7 + surfsense_web/messages/zh.json | 7 + 11 files changed, 500 insertions(+), 672 deletions(-) delete mode 100644 surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx delete mode 100644 surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx create mode 100644 surfsense_web/components/sidebar/all-chats-sidebar.tsx create mode 100644 surfsense_web/components/sidebar/nav-chats.tsx delete mode 100644 surfsense_web/components/sidebar/nav-projects.tsx diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx deleted file mode 100644 index 1d00ef01a..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ /dev/null @@ -1,458 +0,0 @@ -"use client"; - -import { format } from "date-fns"; -import { useAtom, useAtomValue } from "jotai"; -import { - Calendar, - ExternalLink, - MessageCircleMore, - MoreHorizontal, - Search, - Tag, - Trash2, -} from "lucide-react"; -import { AnimatePresence, motion, type Variants } from "motion/react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useEffect, useState } from "react"; -import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; -import { chatsAtom } from "@/atoms/chats/chat-query.atoms"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { Input } from "@/components/ui/input"; -import { - Pagination, - PaginationContent, - PaginationItem, - PaginationLink, - PaginationNext, - PaginationPrevious, -} from "@/components/ui/pagination"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; - -export interface Chat { - created_at: string; - id: number; - type: "QNA"; - title: string; - search_space_id: number; - state_version: number; -} - -export interface ChatDetails { - type: "QNA"; - title: string; - initial_connectors: string[]; - messages: any[]; - created_at: string; - id: number; - search_space_id: number; - state_version: number; -} - -interface ChatsPageClientProps { - searchSpaceId: string; -} - -const pageVariants: Variants = { - initial: { opacity: 0 }, - enter: { opacity: 1, transition: { duration: 0.3, ease: "easeInOut" } }, - exit: { opacity: 0, transition: { duration: 0.3, ease: "easeInOut" } }, -}; - -const chatCardVariants: Variants = { - initial: { y: 20, opacity: 0 }, - animate: { y: 0, opacity: 1 }, - exit: { y: -20, opacity: 0 }, -}; - -const MotionCard = motion(Card); - -export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) { - const router = useRouter(); - const [filteredChats, setFilteredChats] = useState([]); - const [searchQuery, setSearchQuery] = useState(""); - const [currentPage, setCurrentPage] = useState(1); - const [totalPages, setTotalPages] = useState(1); - const [selectedType, setSelectedType] = useState("all"); - const [sortOrder, setSortOrder] = useState("newest"); - const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); - const [chatToDelete, setChatToDelete] = useState<{ - id: number; - title: string; - } | null>(null); - const { isFetching: isFetchingChats, data: chats, error: fetchError } = useAtomValue(chatsAtom); - const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = - useAtom(deleteChatMutationAtom); - - const chatsPerPage = 9; - const searchParams = useSearchParams(); - - // Get initial page from URL params if it exists - useEffect(() => { - const pageParam = searchParams.get("page"); - if (pageParam) { - const pageNumber = parseInt(pageParam, 10); - if (!Number.isNaN(pageNumber) && pageNumber > 0) { - setCurrentPage(pageNumber); - } - } - }, [searchParams]); - - // Filter and sort chats based on search query, type, and sort order - useEffect(() => { - let result = [...(chats || [])]; - - // Filter by search term - if (searchQuery) { - const query = searchQuery.toLowerCase(); - result = result.filter((chat) => chat.title.toLowerCase().includes(query)); - } - - // Filter by type - if (selectedType !== "all") { - result = result.filter((chat) => chat.type === selectedType); - } - - // Sort chats - result.sort((a, b) => { - const dateA = new Date(a.created_at).getTime(); - const dateB = new Date(b.created_at).getTime(); - - return sortOrder === "newest" ? dateB - dateA : dateA - dateB; - }); - - setFilteredChats(result); - setTotalPages(Math.max(1, Math.ceil(result.length / chatsPerPage))); - - // Reset to first page when filters change - if (currentPage !== 1 && (searchQuery || selectedType !== "all" || sortOrder !== "newest")) { - setCurrentPage(1); - } - }, [chats, searchQuery, selectedType, sortOrder, currentPage]); - - // Function to handle chat deletion - const handleDeleteChat = async () => { - if (!chatToDelete) return; - - await deleteChat({ id: chatToDelete.id }); - - setDeleteDialogOpen(false); - setChatToDelete(null); - }; - - // Calculate pagination - const indexOfLastChat = currentPage * chatsPerPage; // Index of last chat in the current page - const indexOfFirstChat = indexOfLastChat - chatsPerPage; // Index of first chat in the current page - const currentChats = filteredChats.slice(indexOfFirstChat, indexOfLastChat); - - // Get unique chat types for filter dropdown - const chatTypes = chats ? ["all", ...Array.from(new Set(chats.map((chat) => chat.type)))] : []; - - return ( - -
-
-

All Chats

-

View, search, and manage all your chats.

-
- - {/* Filter and Search Bar */} -
-
-
- - setSearchQuery(e.target.value)} - /> -
- - -
- -
- -
-
- - {/* Status Messages */} - {isFetchingChats && ( -
-
-
-

Loading chats...

-
-
- )} - - {fetchError && !isFetchingChats && ( -
-

Error loading chats

-

{fetchError.message}

-
- )} - - {!isFetchingChats && !fetchError && filteredChats.length === 0 && ( -
- -

No chats found

-

- {searchQuery || selectedType !== "all" - ? "Try adjusting your search filters" - : "Start a new chat to get started"} -

-
- )} - - {/* Chat Grid */} - {!isFetchingChats && !fetchError && filteredChats.length > 0 && ( - -
- {currentChats.map((chat, index) => ( - - -
-
- - {chat.title || `Chat ${chat.id}`} - - - - - {format(new Date(chat.created_at), "MMM d, yyyy")} - - -
- - - - - - - router.push( - `/dashboard/${chat.search_space_id}/researcher/${chat.id}` - ) - } - > - - View Chat - - - { - e.stopPropagation(); - setChatToDelete({ - id: chat.id, - title: chat.title || `Chat ${chat.id}`, - }); - setDeleteDialogOpen(true); - }} - > - - Delete Chat - - - -
-
- - - - - {chat.type || "Unknown"} - - - -
- ))} -
-
- )} - - {/* Pagination */} - {!isFetchingChats && !fetchError && totalPages > 1 && ( - - - - { - e.preventDefault(); - if (currentPage > 1) setCurrentPage(currentPage - 1); - }} - className={currentPage <= 1 ? "pointer-events-none opacity-50" : ""} - /> - - - {Array.from({ length: totalPages }).map((_, index) => { - const pageNumber = index + 1; - const isVisible = - pageNumber === 1 || - pageNumber === totalPages || - (pageNumber >= currentPage - 1 && pageNumber <= currentPage + 1); - - if (!isVisible) { - // Show ellipsis at appropriate positions - if (pageNumber === 2 || pageNumber === totalPages - 1) { - return ( - - ... - - ); - } - return null; - } - - return ( - - { - e.preventDefault(); - setCurrentPage(pageNumber); - }} - isActive={pageNumber === currentPage} - > - {pageNumber} - - - ); - })} - - - { - e.preventDefault(); - if (currentPage < totalPages) setCurrentPage(currentPage + 1); - }} - className={currentPage >= totalPages ? "pointer-events-none opacity-50" : ""} - /> - - - - )} -
- - {/* Delete Confirmation Dialog */} - - - - - - Delete Chat - - - Are you sure you want to delete{" "} - {chatToDelete?.title}? This action cannot be - undone. - - - - - - - - -
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx deleted file mode 100644 index 9a2bb0f48..000000000 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Suspense } from "react"; -import ChatsPageClient from "./chats-client"; - -interface PageProps { - params: { - search_space_id: string; - }; -} - -export default async function ChatsPage({ params }: PageProps) { - // Get search space ID from the route parameter - const { search_space_id: searchSpaceId } = await Promise.resolve(params); - - return ( - -
- - } - > - -
- ); -} diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx index 196f565a9..70b9505e9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx @@ -134,9 +134,13 @@ export default function ResearcherPage() { message: Message | CreateMessage, chatRequestOptions?: { data?: any } ) => { + // Use the first message content as the chat title (truncated to 100 chars) + const messageContent = typeof message.content === 'string' ? message.content : ''; + const chatTitle = messageContent.slice(0, 100) || "Untitled Chat"; + const newChat = await createChat({ type: researchMode, - title: "Untitled Chat", + title: chatTitle, initial_connectors: selectedConnectors, messages: [ { diff --git a/surfsense_web/atoms/chats/chat-mutation.atoms.ts b/surfsense_web/atoms/chats/chat-mutation.atoms.ts index a08dcd21f..798bfcd2f 100644 --- a/surfsense_web/atoms/chats/chat-mutation.atoms.ts +++ b/surfsense_web/atoms/chats/chat-mutation.atoms.ts @@ -1,7 +1,7 @@ import { atomWithMutation } from "jotai-tanstack-query"; import { toast } from "sonner"; -import type { Chat } from "@/app/dashboard/[search_space_id]/chats/chats-client"; import type { + ChatSummary, CreateChatRequest, DeleteChatRequest, UpdateChatRequest, @@ -29,7 +29,7 @@ export const deleteChatMutationAtom = atomWithMutation((get) => { toast.success("Chat deleted successfully"); queryClient.setQueryData( cacheKeys.chats.globalQueryParams(chatsQueryParams), - (oldData: Chat[]) => { + (oldData: ChatSummary[]) => { return oldData.filter((chat) => chat.id !== request.id); } ); diff --git a/surfsense_web/components/sidebar/all-chats-sidebar.tsx b/surfsense_web/components/sidebar/all-chats-sidebar.tsx new file mode 100644 index 000000000..6b491cb24 --- /dev/null +++ b/surfsense_web/components/sidebar/all-chats-sidebar.tsx @@ -0,0 +1,241 @@ +"use client"; + +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { Loader2, MessageCircleMore, MoreHorizontal, Search, Trash2, X } from "lucide-react"; +import { useRouter } from "next/navigation"; +import { useTranslations } from "next-intl"; +import { useCallback, useMemo, useState } from "react"; +import { toast } from "sonner"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, +} from "@/components/ui/sheet"; +import { useDebouncedValue } from "@/hooks/use-debounced-value"; +import { chatsApiService } from "@/lib/apis/chats-api.service"; +import { cn } from "@/lib/utils"; + +interface AllChatsSidebarProps { + open: boolean; + onOpenChange: (open: boolean) => void; + searchSpaceId: string; +} + +export function AllChatsSidebar({ + open, + onOpenChange, + searchSpaceId, +}: AllChatsSidebarProps) { + const t = useTranslations("sidebar"); + const router = useRouter(); + const queryClient = useQueryClient(); + const [deletingChatId, setDeletingChatId] = useState(null); + const [searchQuery, setSearchQuery] = useState(""); + const debouncedSearchQuery = useDebouncedValue(searchQuery, 300); + + // Fetch all chats + const { + data: chatsData, + error: chatsError, + isLoading: isLoadingChats, + } = useQuery({ + queryKey: ["all-chats", searchSpaceId], + queryFn: () => + chatsApiService.getChats({ + queryParams: { + search_space_id: Number(searchSpaceId), + }, + }), + enabled: !!searchSpaceId && open, + }); + + // Handle chat navigation + const handleChatClick = useCallback( + (chatId: number, chatSearchSpaceId: number) => { + router.push(`/dashboard/${chatSearchSpaceId}/researcher/${chatId}`); + onOpenChange(false); + }, + [router, onOpenChange] + ); + + // Handle chat deletion + const handleDeleteChat = useCallback( + async (chatId: number) => { + setDeletingChatId(chatId); + try { + await chatsApiService.deleteChat({ id: chatId }); + toast.success(t("chat_deleted") || "Chat deleted successfully"); + // Invalidate queries to refresh the list + queryClient.invalidateQueries({ queryKey: ["all-chats", searchSpaceId] }); + queryClient.invalidateQueries({ queryKey: ["chats"] }); + } catch (error) { + console.error("Error deleting chat:", error); + toast.error(t("error_deleting_chat") || "Failed to delete chat"); + } finally { + setDeletingChatId(null); + } + }, + [queryClient, searchSpaceId, t] + ); + + // Clear search + const handleClearSearch = useCallback(() => { + setSearchQuery(""); + }, []); + + // Filter chats based on search query (client-side filtering) + const chats = useMemo(() => { + const allChats = chatsData ?? []; + if (!debouncedSearchQuery) { + return allChats; + } + const query = debouncedSearchQuery.toLowerCase(); + return allChats.filter((chat) => + chat.title.toLowerCase().includes(query) + ); + }, [chatsData, debouncedSearchQuery]); + + const isSearchMode = !!debouncedSearchQuery; + + return ( + + + + {t("all_chats") || "All Chats"} + + {t("all_chats_description") || "Browse and manage all your chats"} + + + {/* Search Input */} +
+ + setSearchQuery(e.target.value)} + className="pl-9 pr-8 h-9" + /> + {searchQuery && ( + + )} +
+
+ + +
+ {isLoadingChats ? ( +
+ +
+ ) : chatsError ? ( +
+ {t("error_loading_chats") || "Error loading chats"} +
+ ) : chats.length > 0 ? ( +
+ {chats.map((chat) => { + const isDeleting = deletingChatId === chat.id; + + return ( +
+ {/* Main clickable area for navigation */} + + + {/* Actions dropdown - separate from main click area */} + + + + + + handleDeleteChat(chat.id)} + className="text-destructive focus:text-destructive" + > + + Delete + + + +
+ ); + })} +
+ ) : isSearchMode ? ( +
+ +

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

+

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

+
+ ) : ( +
+ +

+ {t("no_chats") || "No chats yet"} +

+

+ {t("start_new_chat_hint") || "Start a new chat from the researcher"} +

+
+ )} +
+
+
+
+ ); +} diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 48e7c35b8..78864da2d 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -113,9 +113,9 @@ function UserAvatar({ email, size = 32 }: { email: string; size?: number }) { ); } +import { NavChats } from "@/components/sidebar/nav-chats"; import { NavMain } from "@/components/sidebar/nav-main"; import { NavNotes } from "@/components/sidebar/nav-notes"; -import { NavProjects } from "@/components/sidebar/nav-projects"; import { NavSecondary } from "@/components/sidebar/nav-secondary"; import { PageUsageDisplay } from "@/components/sidebar/page-usage-display"; import { @@ -446,11 +446,12 @@ export const AppSidebar = memo(function AppSidebar({ - {processedRecentChats.length > 0 && ( -
- -
- )} +
+ +
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, +}; + +export function NavChats({ chats, defaultOpen = true, searchSpaceId }: NavChatsProps) { + const t = useTranslations("sidebar"); + const router = useRouter(); + 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; + + return ( + + {/* Main navigation button */} + handleChatClick(chat.url)} + disabled={isDeletingChat} + className={cn( + "pr-8", // Make room for the action button + 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 + ? "Deleting..." + : action.name} + + + ); + })} + + +
+ )} +
+ ); + }) + ) : ( + + + + {t("no_recent_chats") || "No recent chats"} + + + )} +
+
+
+
+ + {/* All Chats Sheet */} + {searchSpaceId && ( + + )} +
+ ); +} diff --git a/surfsense_web/components/sidebar/nav-projects.tsx b/surfsense_web/components/sidebar/nav-projects.tsx deleted file mode 100644 index 3862ce75d..000000000 --- a/surfsense_web/components/sidebar/nav-projects.tsx +++ /dev/null @@ -1,177 +0,0 @@ -"use client"; - -import { - ExternalLink, - Folder, - type LucideIcon, - MoreHorizontal, - RefreshCw, - Search, - Share, - Trash2, -} from "lucide-react"; -import { useRouter } from "next/navigation"; -import { useTranslations } from "next-intl"; -import { useCallback, useMemo, useState } from "react"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - SidebarGroup, - SidebarGroupLabel, - SidebarInput, - SidebarMenu, - SidebarMenuAction, - SidebarMenuButton, - SidebarMenuItem, - useSidebar, -} from "@/components/ui/sidebar"; - -// Map of icon names to their components -const actionIconMap: Record = { - ExternalLink, - Folder, - Share, - Trash2, - MoreHorizontal, - Search, - RefreshCw, -}; - -interface ChatAction { - name: string; - icon: string; - onClick: () => void; -} - -interface ChatItem { - name: string; - url: string; - icon: LucideIcon; - id?: number; - search_space_id?: number; - actions?: ChatAction[]; -} - -export function NavProjects({ chats }: { chats: ChatItem[] }) { - const t = useTranslations("sidebar"); - const { isMobile } = useSidebar(); - const router = useRouter(); - const [searchQuery, setSearchQuery] = useState(""); - const [isDeleting, setIsDeleting] = useState(null); - - const searchSpaceId = chats[0]?.search_space_id || ""; - - // Memoized filtered chats - const filteredChats = useMemo(() => { - if (!searchQuery.trim()) return chats; - - return chats.filter((chat) => chat.name.toLowerCase().includes(searchQuery.toLowerCase())); - }, [chats, searchQuery]); - - // Handle chat deletion with loading state - const handleDeleteChat = useCallback(async (chatId: number, deleteAction: () => void) => { - setIsDeleting(chatId); - try { - await deleteAction(); - } finally { - setIsDeleting(null); - } - }, []); - - // Enhanced chat item component - const ChatItemComponent = useCallback( - ({ chat }: { chat: ChatItem }) => { - const isDeletingChat = isDeleting === chat.id; - - return ( - - router.push(chat.url)} - disabled={isDeletingChat} - className={isDeletingChat ? "opacity-50" : ""} - > - - {chat.name} - - - {chat.actions && chat.actions.length > 0 && ( - - - - - More - - - - {chat.actions.map((action, actionIndex) => { - const ActionIcon = actionIconMap[action.icon] || Folder; - 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" : ""} - > - - {isDeletingChat && isDeleteAction ? "Deleting..." : action.name} - - ); - })} - - - )} - - ); - }, - [isDeleting, router, isMobile, handleDeleteChat] - ); - - // Show search input if there are chats - const showSearch = chats.length > 0; - - return ( - - {t("recent_chats")} - - {/* Chat Items */} - {filteredChats.length > 0 ? ( - filteredChats.map((chat) => ) - ) : ( - /* No results state */ - - - - {searchQuery ? t("no_chats_found") : t("no_recent_chats")} - - - )} - - {/* View All Chats */} - {chats.length > 0 && ( - - router.push(`/dashboard/${searchSpaceId}/chats`)}> - - {t("view_all_chats")} - - - )} - - - ); -} diff --git a/surfsense_web/hooks/use-chat.ts b/surfsense_web/hooks/use-chat.ts index 108165bbf..c31097e11 100644 --- a/surfsense_web/hooks/use-chat.ts +++ b/surfsense_web/hooks/use-chat.ts @@ -1,6 +1,4 @@ -import type { Message } from "@ai-sdk/react"; -import { useCallback, useEffect, useState } from "react"; -import type { ChatDetails } from "@/app/dashboard/[search_space_id]/chats/chats-client"; +import { useEffect, useState } from "react"; import type { ResearchMode } from "@/components/chat"; import type { Document } from "@/contracts/types/document.types"; import { getBearerToken } from "@/lib/auth-utils"; diff --git a/surfsense_web/messages/en.json b/surfsense_web/messages/en.json index e2cf89b5f..478264889 100644 --- a/surfsense_web/messages/en.json +++ b/surfsense_web/messages/en.json @@ -642,6 +642,13 @@ "no_chats_found": "No chats found", "no_recent_chats": "No recent 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", + "start_new_chat_hint": "Start a new chat from the researcher", + "error_loading_chats": "Error loading chats", + "chat_deleted": "Chat deleted successfully", + "error_deleting_chat": "Failed to delete chat", "search_space": "Search Space", "notes": "Notes", "all_notes": "All Notes", diff --git a/surfsense_web/messages/zh.json b/surfsense_web/messages/zh.json index 38546bb87..24b37c61f 100644 --- a/surfsense_web/messages/zh.json +++ b/surfsense_web/messages/zh.json @@ -642,6 +642,13 @@ "no_chats_found": "未找到对话", "no_recent_chats": "暂无最近对话", "view_all_chats": "查看所有对话", + "all_chats": "所有对话", + "all_chats_description": "浏览和管理您的所有对话", + "no_chats": "暂无对话", + "start_new_chat_hint": "从研究员开始新对话", + "error_loading_chats": "加载对话时出错", + "chat_deleted": "对话删除成功", + "error_deleting_chat": "删除对话失败", "search_space": "搜索空间", "notes": "笔记", "all_notes": "所有笔记", From 60d57305a77791a86a885bcc60cb4bb869d83895 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:36:10 +0530 Subject: [PATCH 2/6] refactor: streamline sidebar layout and adjust note/chat limits - Updated `AppSidebar` to simplify the layout by removing unnecessary div wrappers around `NavChats` and `NavNotes`. - Adjusted the number of notes and chats displayed in the sidebar from 5 to 4 for a more compact design. - Modified `SidebarContent` and `SidebarGroup` styles for improved spacing and layout consistency. --- .../components/sidebar/AppSidebarProvider.tsx | 8 +++--- .../components/sidebar/app-sidebar.tsx | 28 ++++++++----------- surfsense_web/components/ui/sidebar.tsx | 4 +-- 3 files changed, 18 insertions(+), 22 deletions(-) diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index ca05b0e3f..a90845673 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -57,7 +57,7 @@ export function AppSidebarProvider({ useAtom(deleteChatMutationAtom); useEffect(() => { - setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 5 })); + setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 4 })); }, [searchSpaceId]); const { @@ -84,7 +84,7 @@ export function AppSidebarProvider({ queryFn: () => notesApiService.getNotes({ search_space_id: Number(searchSpaceId), - page_size: 5, // Get 5 notes (changed from 10) + page_size: 4, // Get 4 notes for compact sidebar }), enabled: !!searchSpaceId, }); @@ -196,8 +196,8 @@ export function AppSidebarProvider({ return dateB - dateA; // Descending order (most recent first) }); - // Limit to 5 notes - return sortedNotes.slice(0, 5).map((note) => ({ + // 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", diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 78864da2d..d4a127a66 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -443,24 +443,20 @@ export const AppSidebar = memo(function AppSidebar({ - - + + -
- -
+ -
- -
-
+ +
{pageUsage && ( diff --git a/surfsense_web/components/ui/sidebar.tsx b/surfsense_web/components/ui/sidebar.tsx index 3170148eb..edc846ba7 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-auto group-data-[collapsible=icon]:overflow-hidden", + "flex min-h-0 flex-1 flex-col gap-2 overflow-hidden group-data-[collapsible=icon]:overflow-hidden", className )} {...props} @@ -365,7 +365,7 @@ function SidebarGroup({ className, ...props }: React.ComponentProps<"div">) {
); From bb971f89ba99a0f1f56e8bc5a6270c35a4439405 Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 21:40:40 +0530 Subject: [PATCH 3/6] feat: enhance sidebar functionality with tooltips and improved sorting - Added tooltips to chat and note items in the sidebars, displaying creation and update timestamps. - Implemented sorting of chats and notes by their creation or update dates for better organization. - Updated translation keys for new UI strings related to deletion and timestamps. - Adjusted sidebar layout for improved user experience on mobile devices. --- .../components/sidebar/AppSidebarProvider.tsx | 140 ++++++++++++------ .../components/sidebar/all-chats-sidebar.tsx | 49 ++++-- .../components/sidebar/all-notes-sidebar.tsx | 64 +++++--- .../components/sidebar/app-sidebar.tsx | 7 +- .../components/sidebar/nav-chats.tsx | 51 ++++--- surfsense_web/components/sidebar/nav-main.tsx | 41 ++++- .../components/sidebar/nav-notes.tsx | 22 ++- surfsense_web/components/ui/sidebar.tsx | 2 +- surfsense_web/components/ui/tooltip.tsx | 4 +- surfsense_web/messages/en.json | 14 +- surfsense_web/messages/zh.json | 12 +- 11 files changed, 293 insertions(+), 113 deletions(-) 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": "出错了", From ee46a43afc55cc076ea1657f674298458b8441be Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:37:04 +0530 Subject: [PATCH 4/6] feat: enhance chat query handling and sidebar updates - Updated chat query to include ordering by creation date for better organization. - Improved optimistic updates in chat deletion and creation mutations to ensure UI consistency. - Invalidate chat queries after mutations to refresh sidebar and components effectively. - Adjusted cache key structure for chat queries to include additional parameters for better cache management. --- surfsense_backend/app/routes/chats_routes.py | 23 +++++++++++-------- .../atoms/chats/chat-mutation.atoms.ts | 19 +++++++++++++-- .../components/sidebar/AppSidebarProvider.tsx | 2 +- surfsense_web/lib/query-client/cache-keys.ts | 9 +++++++- 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/surfsense_backend/app/routes/chats_routes.py b/surfsense_backend/app/routes/chats_routes.py index 2813201e7..26af68149 100644 --- a/surfsense_backend/app/routes/chats_routes.py +++ b/surfsense_backend/app/routes/chats_routes.py @@ -319,15 +319,19 @@ async def read_chats( "You don't have permission to read chats in this search space", ) # Select specific fields excluding messages - query = select( - Chat.id, - Chat.type, - Chat.title, - Chat.initial_connectors, - Chat.search_space_id, - Chat.created_at, - Chat.state_version, - ).filter(Chat.search_space_id == search_space_id) + query = ( + select( + Chat.id, + Chat.type, + Chat.title, + Chat.initial_connectors, + Chat.search_space_id, + Chat.created_at, + Chat.state_version, + ) + .filter(Chat.search_space_id == search_space_id) + .order_by(Chat.created_at.desc()) + ) else: # Get chats from all search spaces user has membership in query = ( @@ -343,6 +347,7 @@ async def read_chats( .join(SearchSpace) .join(SearchSpaceMembership) .filter(SearchSpaceMembership.user_id == user.id) + .order_by(Chat.created_at.desc()) ) result = await session.execute(query.offset(skip).limit(limit)) diff --git a/surfsense_web/atoms/chats/chat-mutation.atoms.ts b/surfsense_web/atoms/chats/chat-mutation.atoms.ts index 798bfcd2f..c761a706f 100644 --- a/surfsense_web/atoms/chats/chat-mutation.atoms.ts +++ b/surfsense_web/atoms/chats/chat-mutation.atoms.ts @@ -27,12 +27,21 @@ export const deleteChatMutationAtom = atomWithMutation((get) => { onSuccess: (_, request: DeleteChatRequest) => { toast.success("Chat deleted successfully"); + // Optimistically update the current query queryClient.setQueryData( cacheKeys.chats.globalQueryParams(chatsQueryParams), (oldData: ChatSummary[]) => { - return oldData.filter((chat) => chat.id !== request.id); + return oldData?.filter((chat) => chat.id !== request.id) ?? []; } ); + // Invalidate all chat queries to ensure consistency across components + queryClient.invalidateQueries({ + queryKey: ["chats"], + }); + // Also invalidate the "all-chats" query used by AllChatsSidebar + queryClient.invalidateQueries({ + queryKey: ["all-chats"], + }); }, }; }); @@ -50,8 +59,14 @@ export const createChatMutationAtom = atomWithMutation((get) => { }, onSuccess: () => { + // Invalidate ALL chat queries to ensure sidebar and other components refresh + // Using a partial key match to avoid stale closure issues with specific query params queryClient.invalidateQueries({ - queryKey: cacheKeys.chats.globalQueryParams(chatsQueryParams), + queryKey: ["chats"], + }); + // Also invalidate the "all-chats" query used by AllChatsSidebar + queryClient.invalidateQueries({ + queryKey: ["all-chats"], }); }, }; diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index bb7ccedc9..393d86b74 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -58,7 +58,7 @@ export function AppSidebarProvider({ useEffect(() => { setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 4 })); - }, [searchSpaceId]); + }, [searchSpaceId, setChatsQueryParams]); const { data: searchSpace, diff --git a/surfsense_web/lib/query-client/cache-keys.ts b/surfsense_web/lib/query-client/cache-keys.ts index a8eecb605..7eae21e05 100644 --- a/surfsense_web/lib/query-client/cache-keys.ts +++ b/surfsense_web/lib/query-client/cache-keys.ts @@ -10,7 +10,14 @@ export const cacheKeys = { chats: { activeChat: (chatId: string) => ["active-chat", chatId] as const, globalQueryParams: (queries: GetChatsRequest["queryParams"]) => - ["chats", ...(queries ? Object.values(queries) : [])] as const, + [ + "chats", + queries?.search_space_id, + queries?.limit, + queries?.skip, + queries?.page, + queries?.page_size, + ] as const, }, podcasts: { globalQueryParams: (queries: GetPodcastsRequest["queryParams"]) => From b53b19170e0a7e174d3d70f42d2c4851754b642d Mon Sep 17 00:00:00 2001 From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:59:42 +0530 Subject: [PATCH 5/6] feat: implement unsaved changes handling in editor and sidebar - Introduced global state management for unsaved changes and pending navigation using Jotai atoms. - Updated the editor component to sync local unsaved changes with global state and handle navigation prompts. - Enhanced sidebar functionality to check for unsaved changes before navigating to a new note. - Added cleanup logic for global state on component unmount to prevent stale data. --- .../editor/[documentId]/page.tsx | 67 ++++++++++++++++++- surfsense_web/atoms/editor/ui.atoms.ts | 27 ++++++++ .../components/sidebar/AppSidebarProvider.tsx | 19 +++++- surfsense_web/contracts/types/chat.types.ts | 2 +- 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 surfsense_web/atoms/editor/ui.atoms.ts diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index 4888faceb..e0984801e 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -1,11 +1,13 @@ "use client"; import { useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; import { AlertCircle, ArrowLeft, FileText, Loader2, Save } from "lucide-react"; import { motion } from "motion/react"; import { useParams, useRouter } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; +import { hasUnsavedEditorChangesAtom, pendingEditorNavigationAtom } from "@/atoms/editor/ui.atoms"; import { BlockNoteEditor } from "@/components/DynamicBlockNoteEditor"; import { AlertDialog, @@ -76,6 +78,46 @@ export default function EditorPage() { const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false); const [showUnsavedDialog, setShowUnsavedDialog] = useState(false); + // Global state for cross-component communication + const [, setGlobalHasUnsavedChanges] = useAtom(hasUnsavedEditorChangesAtom); + const [pendingNavigation, setPendingNavigation] = useAtom(pendingEditorNavigationAtom); + + // Sync local unsaved changes state with global atom + useEffect(() => { + setGlobalHasUnsavedChanges(hasUnsavedChanges); + }, [hasUnsavedChanges, setGlobalHasUnsavedChanges]); + + // Cleanup global state when component unmounts + useEffect(() => { + return () => { + setGlobalHasUnsavedChanges(false); + setPendingNavigation(null); + }; + }, [setGlobalHasUnsavedChanges, setPendingNavigation]); + + // Handle pending navigation from sidebar (e.g., when user clicks "+" to create new note) + useEffect(() => { + if (pendingNavigation) { + if (hasUnsavedChanges) { + // Show dialog to confirm navigation + setShowUnsavedDialog(true); + } else { + // No unsaved changes, navigate immediately + router.push(pendingNavigation); + setPendingNavigation(null); + } + } + }, [pendingNavigation, hasUnsavedChanges, router, setPendingNavigation]); + + // Reset state when documentId changes (e.g., navigating from existing note to new note) + useEffect(() => { + setDocument(null); + setEditorContent(null); + setError(null); + setHasUnsavedChanges(false); + setLoading(true); + }, [documentId]); + // Fetch document content - DIRECT CALL TO FASTAPI // Skip fetching if this is a new note useEffect(() => { @@ -287,7 +329,23 @@ export default function EditorPage() { const handleConfirmLeave = () => { setShowUnsavedDialog(false); - router.push(`/dashboard/${searchSpaceId}/researcher`); + // Clear global unsaved state + setGlobalHasUnsavedChanges(false); + setHasUnsavedChanges(false); + + // If there's a pending navigation (from sidebar), use that; otherwise go back to researcher + if (pendingNavigation) { + router.push(pendingNavigation); + setPendingNavigation(null); + } else { + router.push(`/dashboard/${searchSpaceId}/researcher`); + } + }; + + const handleCancelLeave = () => { + setShowUnsavedDialog(false); + // Clear pending navigation if user cancels + setPendingNavigation(null); }; if (loading) { @@ -402,6 +460,7 @@ export default function EditorPage() { )}
{/* Unsaved Changes Dialog */} - + { + if (!open) handleCancelLeave(); + }}> Unsaved Changes @@ -420,7 +481,7 @@ export default function EditorPage() { - Cancel + Cancel OK diff --git a/surfsense_web/atoms/editor/ui.atoms.ts b/surfsense_web/atoms/editor/ui.atoms.ts new file mode 100644 index 000000000..81a89a945 --- /dev/null +++ b/surfsense_web/atoms/editor/ui.atoms.ts @@ -0,0 +1,27 @@ +import { atom } from "jotai"; + +interface EditorUIState { + hasUnsavedChanges: boolean; + pendingNavigation: string | null; // URL to navigate to after user confirms +} + +export const editorUIAtom = atom({ + hasUnsavedChanges: false, + pendingNavigation: null, +}); + +// Derived atom for just the unsaved changes state +export const hasUnsavedEditorChangesAtom = atom( + (get) => get(editorUIAtom).hasUnsavedChanges, + (get, set, value: boolean) => { + set(editorUIAtom, { ...get(editorUIAtom), hasUnsavedChanges: value }); + } +); + +// Derived atom for pending navigation +export const pendingEditorNavigationAtom = atom( + (get) => get(editorUIAtom).pendingNavigation, + (get, set, value: string | null) => { + set(editorUIAtom, { ...get(editorUIAtom), pendingNavigation: value }); + } +); diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 393d86b74..0e4ca0460 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -9,6 +9,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { deleteChatMutationAtom } from "@/atoms/chats/chat-mutation.atoms"; import { chatsAtom } from "@/atoms/chats/chat-query.atoms"; import { globalChatsQueryParamsAtom } from "@/atoms/chats/ui.atoms"; +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"; @@ -55,6 +56,10 @@ export function AppSidebarProvider({ const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom); const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = useAtom(deleteChatMutationAtom); + + // Editor state for handling unsaved changes + const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom); + const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom); useEffect(() => { setChatsQueryParams((prev) => ({ ...prev, search_space_id: searchSpaceId, skip: 0, limit: 4 })); @@ -233,10 +238,18 @@ export function AppSidebarProvider({ })); }, [notesData]); - // Handle add note + // Handle add note - check for unsaved changes first const handleAddNote = useCallback(() => { - router.push(`/dashboard/${searchSpaceId}/editor/new`); - }, [router, searchSpaceId]); + 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(() => { diff --git a/surfsense_web/contracts/types/chat.types.ts b/surfsense_web/contracts/types/chat.types.ts index 0b8b5975c..a4e5b8f5b 100644 --- a/surfsense_web/contracts/types/chat.types.ts +++ b/surfsense_web/contracts/types/chat.types.ts @@ -14,7 +14,7 @@ export const chatSummary = z.object({ }); export const chatDetails = chatSummary.extend({ - initial_connectors: z.array(z.string()), + initial_connectors: z.array(z.string()).nullable().optional(), messages: z.array(z.any()), }); From b5a6321eb446a05797f172017d7a5cc17450900e Mon Sep 17 00:00:00 2001 From: "DESKTOP-RTLN3BA\\$punk" Date: Fri, 19 Dec 2025 15:38:39 -0800 Subject: [PATCH 6/6] chore: biome lint --- .../editor/[documentId]/page.tsx | 11 +++++--- .../researcher/[[...chat_id]]/page.tsx | 4 +-- .../components/sidebar/AppSidebarProvider.tsx | 16 ++++++++--- .../components/sidebar/all-chats-sidebar.tsx | 21 ++++++-------- .../components/sidebar/all-notes-sidebar.tsx | 20 ++++++++++--- .../components/sidebar/app-sidebar.tsx | 28 +++++++++---------- .../components/sidebar/nav-chats.tsx | 13 +++++++-- .../components/sidebar/nav-notes.tsx | 12 ++++++-- 8 files changed, 79 insertions(+), 46 deletions(-) diff --git a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx index e0984801e..8fc2fb825 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/editor/[documentId]/page.tsx @@ -332,7 +332,7 @@ export default function EditorPage() { // Clear global unsaved state setGlobalHasUnsavedChanges(false); setHasUnsavedChanges(false); - + // If there's a pending navigation (from sidebar), use that; otherwise go back to researcher if (pendingNavigation) { router.push(pendingNavigation); @@ -470,9 +470,12 @@ export default function EditorPage() {
{/* Unsaved Changes Dialog */} - { - if (!open) handleCancelLeave(); - }}> + { + if (!open) handleCancelLeave(); + }} + > Unsaved Changes diff --git a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx index 70b9505e9..60980f034 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/researcher/[[...chat_id]]/page.tsx @@ -135,9 +135,9 @@ export default function ResearcherPage() { chatRequestOptions?: { data?: any } ) => { // Use the first message content as the chat title (truncated to 100 chars) - const messageContent = typeof message.content === 'string' ? message.content : ''; + const messageContent = typeof message.content === "string" ? message.content : ""; const chatTitle = messageContent.slice(0, 100) || "Untitled Chat"; - + const newChat = await createChat({ type: researchMode, title: chatTitle, diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 0e4ca0460..aa1478be6 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -56,7 +56,7 @@ export function AppSidebarProvider({ const { data: chats, error: chatError, isLoading: isLoadingChats } = useAtomValue(chatsAtom); const [{ isPending: isDeletingChat, mutateAsync: deleteChat, error: deleteError }] = useAtom(deleteChatMutationAtom); - + // Editor state for handling unsaved changes const hasUnsavedEditorChanges = useAtomValue(hasUnsavedEditorChangesAtom); const setPendingNavigation = useSetAtom(pendingEditorNavigationAtom); @@ -97,7 +97,11 @@ 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 [noteToDelete, setNoteToDelete] = useState<{ + id: number; + name: string; + search_space_id: number; + } | null>(null); const [isDeletingNote, setIsDeletingNote] = useState(false); const [isClient, setIsClient] = useState(false); @@ -230,7 +234,11 @@ export function AppSidebarProvider({ name: "Delete", icon: "Trash2", onClick: () => { - setNoteToDelete({ id: note.id, name: note.title, search_space_id: note.search_space_id }); + setNoteToDelete({ + id: note.id, + name: note.title, + search_space_id: note.search_space_id, + }); setShowDeleteNoteDialog(true); }, }, @@ -241,7 +249,7 @@ export function AppSidebarProvider({ // 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); diff --git a/surfsense_web/components/sidebar/all-chats-sidebar.tsx b/surfsense_web/components/sidebar/all-chats-sidebar.tsx index b1a19e38a..5feb4ce3d 100644 --- a/surfsense_web/components/sidebar/all-chats-sidebar.tsx +++ b/surfsense_web/components/sidebar/all-chats-sidebar.tsx @@ -34,11 +34,7 @@ interface AllChatsSidebarProps { searchSpaceId: string; } -export function AllChatsSidebar({ - open, - onOpenChange, - searchSpaceId, -}: AllChatsSidebarProps) { +export function AllChatsSidebar({ open, onOpenChange, searchSpaceId }: AllChatsSidebarProps) { const t = useTranslations("sidebar"); const router = useRouter(); const queryClient = useQueryClient(); @@ -99,7 +95,7 @@ export function AllChatsSidebar({ // 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(); @@ -111,9 +107,7 @@ export function AllChatsSidebar({ return sortedChats; } const query = debouncedSearchQuery.toLowerCase(); - return sortedChats.filter((chat) => - chat.title.toLowerCase().includes(query) - ); + return sortedChats.filter((chat) => chat.title.toLowerCase().includes(query)); }, [chatsData, debouncedSearchQuery]); const isSearchMode = !!debouncedSearchQuery; @@ -190,7 +184,10 @@ export function AllChatsSidebar({ -

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

+

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

@@ -242,9 +239,7 @@ export function AllChatsSidebar({ ) : (
-

- {t("no_chats") || "No chats yet"} -

+

{t("no_chats") || "No chats yet"}

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

diff --git a/surfsense_web/components/sidebar/all-notes-sidebar.tsx b/surfsense_web/components/sidebar/all-notes-sidebar.tsx index 20070a580..20622c13e 100644 --- a/surfsense_web/components/sidebar/all-notes-sidebar.tsx +++ b/surfsense_web/components/sidebar/all-notes-sidebar.tsx @@ -125,8 +125,14 @@ export function AllNotesSidebar({ // 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 }[]; - + 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, @@ -224,9 +230,15 @@ export function AllNotesSidebar({
-

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

+

+ {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")}

+

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

)}
diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index ede8999fd..63101f2de 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -446,22 +446,22 @@ export const AppSidebar = memo(function AppSidebar({ - - + + - + - - + + {pageUsage && ( diff --git a/surfsense_web/components/sidebar/nav-chats.tsx b/surfsense_web/components/sidebar/nav-chats.tsx index dafe2fd9c..1165d6057 100644 --- a/surfsense_web/components/sidebar/nav-chats.tsx +++ b/surfsense_web/components/sidebar/nav-chats.tsx @@ -3,8 +3,8 @@ import { ChevronRight, FolderOpen, - type LucideIcon, Loader2, + type LucideIcon, MessageCircleMore, MoreHorizontal, RefreshCw, @@ -63,7 +63,12 @@ const actionIconMap: Record = { RefreshCw, }; -export function NavChats({ chats, defaultOpen = true, searchSpaceId, isSourcesExpanded = false }: NavChatsProps) { +export function NavChats({ + chats, + defaultOpen = true, + searchSpaceId, + isSourcesExpanded = false, +}: NavChatsProps) { const t = useTranslations("sidebar"); const router = useRouter(); const isMobile = useIsMobile(); @@ -174,7 +179,9 @@ export function NavChats({ chats, defaultOpen = true, searchSpaceId, isSourcesEx ) : ( )} - {t("more_options") || "More options"} + + {t("more_options") || "More options"} + diff --git a/surfsense_web/components/sidebar/nav-notes.tsx b/surfsense_web/components/sidebar/nav-notes.tsx index 383338d77..f634c2b72 100644 --- a/surfsense_web/components/sidebar/nav-notes.tsx +++ b/surfsense_web/components/sidebar/nav-notes.tsx @@ -63,7 +63,13 @@ const actionIconMap: Record = { MoreHorizontal, }; -export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId, isSourcesExpanded = false }: NavNotesProps) { +export function NavNotes({ + notes, + onAddNote, + defaultOpen = true, + searchSpaceId, + isSourcesExpanded = false, +}: NavNotesProps) { const t = useTranslations("sidebar"); const router = useRouter(); const isMobile = useIsMobile(); @@ -188,7 +194,9 @@ export function NavNotes({ notes, onAddNote, defaultOpen = true, searchSpaceId, ) : ( )} - {t("more_options") || "More options"} + + {t("more_options") || "More options"} +