diff --git a/surfsense_backend/app/routes/chats_routes.py b/surfsense_backend/app/routes/chats_routes.py index e01b857..21af85c 100644 --- a/surfsense_backend/app/routes/chats_routes.py +++ b/surfsense_backend/app/routes/chats_routes.py @@ -6,7 +6,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select from app.db import Chat, SearchSpace, User, get_async_session -from app.schemas import AISDKChatRequest, ChatCreate, ChatRead, ChatUpdate +from app.schemas import ( + AISDKChatRequest, + ChatCreate, + ChatRead, + ChatReadWithoutMessages, + ChatUpdate, +) from app.tasks.stream_connector_search_results import stream_connector_search_results from app.users import current_active_user from app.utils.check_ownership import check_ownership @@ -112,7 +118,7 @@ async def create_chat( ) from None -@router.get("/chats/", response_model=list[ChatRead]) +@router.get("/chats/", response_model=list[ChatReadWithoutMessages]) async def read_chats( skip: int = 0, limit: int = 100, @@ -121,14 +127,26 @@ async def read_chats( user: User = Depends(current_active_user), ): try: - query = select(Chat).join(SearchSpace).filter(SearchSpace.user_id == user.id) + # Select specific fields excluding messages + query = ( + select( + Chat.id, + Chat.type, + Chat.title, + Chat.initial_connectors, + Chat.search_space_id, + Chat.created_at, + ) + .join(SearchSpace) + .filter(SearchSpace.user_id == user.id) + ) # Filter by search_space_id if provided if search_space_id is not None: query = query.filter(Chat.search_space_id == search_space_id) result = await session.execute(query.offset(skip).limit(limit)) - return result.scalars().all() + return result.all() except OperationalError: raise HTTPException( status_code=503, detail="Database operation failed. Please try again later." diff --git a/surfsense_backend/app/schemas/__init__.py b/surfsense_backend/app/schemas/__init__.py index e38d534..c038d9c 100644 --- a/surfsense_backend/app/schemas/__init__.py +++ b/surfsense_backend/app/schemas/__init__.py @@ -1,5 +1,12 @@ from .base import IDModel, TimestampModel -from .chats import AISDKChatRequest, ChatBase, ChatCreate, ChatRead, ChatUpdate +from .chats import ( + AISDKChatRequest, + ChatBase, + ChatCreate, + ChatRead, + ChatReadWithoutMessages, + ChatUpdate, +) from .chunks import ChunkBase, ChunkCreate, ChunkRead, ChunkUpdate from .documents import ( DocumentBase, @@ -37,6 +44,7 @@ __all__ = [ "ChatBase", "ChatCreate", "ChatRead", + "ChatReadWithoutMessages", "ChatUpdate", "ChunkBase", "ChunkCreate", diff --git a/surfsense_backend/app/schemas/chats.py b/surfsense_backend/app/schemas/chats.py index 1dfedef..59c274d 100644 --- a/surfsense_backend/app/schemas/chats.py +++ b/surfsense_backend/app/schemas/chats.py @@ -15,6 +15,12 @@ class ChatBase(BaseModel): search_space_id: int +class ChatBaseWithoutMessages(BaseModel): + type: ChatType + title: str + search_space_id: int + + class ClientAttachment(BaseModel): name: str content_type: str @@ -50,3 +56,7 @@ class ChatUpdate(ChatBase): class ChatRead(ChatBase, IDModel, TimestampModel): model_config = ConfigDict(from_attributes=True) + + +class ChatReadWithoutMessages(ChatBaseWithoutMessages, IDModel, TimestampModel): + model_config = ConfigDict(from_attributes=True) 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 index dbad14e..1222733 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/chats/chats-client.tsx @@ -19,14 +19,7 @@ import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; +import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, @@ -61,24 +54,16 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; +import { cn } from "@/lib/utils"; interface Chat { created_at: string; id: number; type: string; title: string; - messages: ChatMessage[]; search_space_id: number; } -interface ChatMessage { - id: string; - createdAt: string; - role: string; - content: string; - parts?: any; -} - interface ChatsPageClientProps { searchSpaceId: string; } @@ -580,12 +565,12 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) animate="animate" exit="exit" transition={{ duration: 0.2, delay: index * 0.05 }} - className={`overflow-hidden hover:shadow-md transition-shadow - ${ - selectionMode && selectedChats.includes(chat.id) - ? "ring-2 ring-primary ring-offset-2" - : "" - }`} + className={cn( + "overflow-hidden hover:shadow-md transition-shadow", + selectionMode && selectedChats.includes(chat.id) + ? "ring-2 ring-primary ring-offset-2" + : "" + )} onClick={(e) => { if (!selectionMode) return; // Ignore clicks coming from interactive elements @@ -672,24 +657,21 @@ export default function ChatsPageClient({ searchSpaceId }: ChatsPageClientProps) )} - - - {chat.messages && chat.messages.length > 0 - ? typeof chat.messages[0] === "string" - ? chat.messages[0] - : chat.messages[0]?.content || "No message content" - : "No messages in this chat."} - - - - - - {chat.messages?.length || 0} messages - + + {chat.type || "Unknown"} + + router.push(`/dashboard/${chat.search_space_id}/researcher/${chat.id}`) + } + > + + View Chat + ))} diff --git a/surfsense_web/components/sidebar/AppSidebarProvider.tsx b/surfsense_web/components/sidebar/AppSidebarProvider.tsx index 1a5920a..daff1af 100644 --- a/surfsense_web/components/sidebar/AppSidebarProvider.tsx +++ b/surfsense_web/components/sidebar/AppSidebarProvider.tsx @@ -1,7 +1,7 @@ "use client"; -import { Trash2 } from "lucide-react"; -import { useEffect, useState } from "react"; +import { AlertCircle, Trash2 } from "lucide-react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { AppSidebar } from "@/components/sidebar/app-sidebar"; import { Button } from "@/components/ui/button"; import { @@ -12,7 +12,7 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { apiClient } from "@/lib/api"; // Import the API client +import { apiClient } from "@/lib/api"; interface Chat { created_at: string; @@ -50,6 +50,26 @@ interface AppSidebarProviderProps { }[]; } +// Loading skeleton component +const LoadingSkeleton = () => ( + + {Array.from({ length: 3 }).map((_, i) => ( + + ))} + +); + +// Error state component +const ErrorState = ({ error, onRetry }: { error: string; onRetry: () => void }) => ( + + + {error} + + Retry + + +); + export function AppSidebarProvider({ searchSpaceId, navSecondary, @@ -80,66 +100,82 @@ export function AppSidebarProvider({ setIsClient(true); }, []); + // Memoized fetch function for chats + const fetchRecentChats = useCallback(async () => { + try { + // Only run on client-side + if (typeof window === "undefined") return; + + const chats: Chat[] = await apiClient.get( + `api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}` + ); + + // Sort chats by created_at in descending order (newest first) + const sortedChats = chats.sort( + (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() + ); + + // Transform API response to the format expected by AppSidebar + const formattedChats = 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); + }, + }, + ], + })); + + setRecentChats(formattedChats); + setChatError(null); + } catch (error) { + console.error("Error fetching chats:", error); + setChatError(error instanceof Error ? error.message : "Unknown error occurred"); + setRecentChats([]); + } finally { + setIsLoadingChats(false); + } + }, [searchSpaceId]); + + // Memoized fetch function for search space + const fetchSearchSpace = useCallback(async () => { + try { + // Only run on client-side + if (typeof window === "undefined") return; + + const data: SearchSpace = await apiClient.get( + `api/v1/searchspaces/${searchSpaceId}` + ); + setSearchSpace(data); + setSearchSpaceError(null); + } catch (error) { + console.error("Error fetching search space:", error); + setSearchSpaceError(error instanceof Error ? error.message : "Unknown error occurred"); + } finally { + setIsLoadingSearchSpace(false); + } + }, [searchSpaceId]); + + // Retry function + const retryFetch = useCallback(() => { + setChatError(null); + setSearchSpaceError(null); + setIsLoadingChats(true); + setIsLoadingSearchSpace(true); + fetchRecentChats(); + fetchSearchSpace(); + }, [fetchRecentChats, fetchSearchSpace]); + // Fetch recent chats useEffect(() => { - const fetchRecentChats = async () => { - try { - // Only run on client-side - if (typeof window === "undefined") return; - - try { - // Use the API client instead of direct fetch - filter by current search space ID - const chats: Chat[] = await apiClient.get( - `api/v1/chats/?limit=5&skip=0&search_space_id=${searchSpaceId}` - ); - - // Sort chats by created_at in descending order (newest first) - const sortedChats = chats.sort( - (a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime() - ); - // console.log("sortedChats", sortedChats); - // Transform API response to the format expected by AppSidebar - const formattedChats = sortedChats.map((chat) => ({ - name: chat.title || `Chat ${chat.id}`, // Fallback if title is empty - url: `/dashboard/${chat.search_space_id}/researcher/${chat.id}`, - icon: "MessageCircleMore", - id: chat.id, - search_space_id: chat.search_space_id, - actions: [ - { - name: "View Details", - icon: "ExternalLink", - onClick: () => { - window.location.href = `/dashboard/${chat.search_space_id}/researcher/${chat.id}`; - }, - }, - { - name: "Delete", - icon: "Trash2", - onClick: () => { - setChatToDelete({ id: chat.id, name: chat.title || `Chat ${chat.id}` }); - setShowDeleteDialog(true); - }, - }, - ], - })); - - setRecentChats(formattedChats); - setChatError(null); - } catch (error) { - console.error("Error fetching chats:", error); - setChatError(error instanceof Error ? error.message : "Unknown error occurred"); - // Provide empty array to ensure UI still renders - setRecentChats([]); - } finally { - setIsLoadingChats(false); - } - } catch (error) { - console.error("Error in fetchRecentChats:", error); - setIsLoadingChats(false); - } - }; - fetchRecentChats(); // Set up a refresh interval (every 5 minutes) @@ -147,144 +183,144 @@ export function AppSidebarProvider({ // Clean up interval on component unmount return () => clearInterval(intervalId); - }, [searchSpaceId]); + }, [fetchRecentChats]); - // Handle delete chat - const handleDeleteChat = async () => { + // Fetch search space details + useEffect(() => { + fetchSearchSpace(); + }, [fetchSearchSpace]); + + // Handle delete chat with better error handling + const handleDeleteChat = useCallback(async () => { if (!chatToDelete) return; try { setIsDeleting(true); - // Use the API client instead of direct fetch await apiClient.delete(`api/v1/chats/${chatToDelete.id}`); - // Close dialog and refresh chats - setRecentChats(recentChats.filter((chat) => chat.id !== chatToDelete.id)); + // Update local state + setRecentChats((prev) => prev.filter((chat) => chat.id !== chatToDelete.id)); } catch (error) { console.error("Error deleting chat:", error); + // You could show a toast notification here } finally { setIsDeleting(false); setShowDeleteDialog(false); setChatToDelete(null); } - }; + }, [chatToDelete]); - // Fetch search space details - useEffect(() => { - const fetchSearchSpace = async () => { - try { - // Only run on client-side - if (typeof window === "undefined") return; + // Memoized fallback chats + const fallbackChats = useMemo(() => { + if (chatError) { + return [ + { + name: "Error loading chats", + url: "#", + icon: "AlertCircle", + id: 0, + search_space_id: Number(searchSpaceId), + actions: [ + { + name: "Retry", + icon: "RefreshCw", + onClick: retryFetch, + }, + ], + }, + ]; + } - try { - // Use the API client instead of direct fetch - const data: SearchSpace = await apiClient.get( - `api/v1/searchspaces/${searchSpaceId}` - ); - setSearchSpace(data); - setSearchSpaceError(null); - } catch (error) { - console.error("Error fetching search space:", error); - setSearchSpaceError(error instanceof Error ? error.message : "Unknown error occurred"); - } finally { - setIsLoadingSearchSpace(false); - } - } catch (error) { - console.error("Error in fetchSearchSpace:", error); - setIsLoadingSearchSpace(false); - } - }; + if (!isLoadingChats && recentChats.length === 0) { + return [ + { + name: "No recent chats", + url: "#", + icon: "MessageCircleMore", + id: 0, + search_space_id: Number(searchSpaceId), + actions: [], + }, + ]; + } - fetchSearchSpace(); - }, [searchSpaceId]); - - // Create a fallback chat if there's an error or no chats - const fallbackChats = - chatError || (!isLoadingChats && recentChats.length === 0) - ? [ - { - name: chatError ? "Error loading chats" : "No recent chats", - url: "#", - icon: chatError ? "AlertCircle" : "MessageCircleMore", - id: 0, - search_space_id: Number(searchSpaceId), - actions: [], - }, - ] - : []; + return []; + }, [chatError, isLoadingChats, recentChats.length, searchSpaceId, retryFetch]); // Use fallback chats if there's an error or no chats const displayChats = recentChats.length > 0 ? recentChats : fallbackChats; - // Update the first item in navSecondary to show the search space name - const updatedNavSecondary = [...navSecondary]; - if (updatedNavSecondary.length > 0 && isClient) { - updatedNavSecondary[0] = { - ...updatedNavSecondary[0], - title: - searchSpace?.name || - (isLoadingSearchSpace - ? "Loading..." - : searchSpaceError - ? "Error loading search space" - : "Unknown Search Space"), - }; + // Memoized updated navSecondary + const updatedNavSecondary = useMemo(() => { + const updated = [...navSecondary]; + if (updated.length > 0 && isClient) { + updated[0] = { + ...updated[0], + title: + searchSpace?.name || + (isLoadingSearchSpace + ? "Loading..." + : searchSpaceError + ? "Error loading search space" + : "Unknown Search Space"), + }; + } + return updated; + }, [navSecondary, isClient, searchSpace?.name, isLoadingSearchSpace, searchSpaceError]); + + // Show loading state if not client-side + if (!isClient) { + return ; } return ( <> - + - {/* Delete Confirmation Dialog - Only render on client */} - {isClient && ( - - - - - - Delete Chat - - - Are you sure you want to delete{" "} - {chatToDelete?.name}? This action cannot be - undone. - - - - setShowDeleteDialog(false)} - disabled={isDeleting} - > - Cancel - - - {isDeleting ? ( - <> - - Deleting... - > - ) : ( - <> - - Delete - > - )} - - - - - )} + {/* Delete Confirmation Dialog */} + + + + + + Delete Chat + + + Are you sure you want to delete{" "} + {chatToDelete?.name}? This action cannot be + undone. + + + + setShowDeleteDialog(false)} + disabled={isDeleting} + > + Cancel + + + {isDeleting ? ( + <> + + Deleting... + > + ) : ( + <> + + Delete + > + )} + + + + > ); } diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 20d6736..5071de4 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -17,15 +17,17 @@ import { Trash2, Undo2, } from "lucide-react"; -import { useMemo } from "react"; +import { memo, useMemo } from "react"; import { Logo } from "@/components/Logo"; import { NavMain } from "@/components/sidebar/nav-main"; import { NavProjects } from "@/components/sidebar/nav-projects"; import { NavSecondary } from "@/components/sidebar/nav-secondary"; +import { NavUser } from "@/components/sidebar/nav-user"; import { Sidebar, SidebarContent, + SidebarFooter, SidebarHeader, SidebarMenu, SidebarMenuButton, @@ -64,7 +66,6 @@ const defaultData = { isActive: true, items: [], }, - { title: "Documents", url: "#", @@ -154,12 +155,12 @@ interface AppSidebarProps extends React.ComponentProps { navSecondary?: { title: string; url: string; - icon: string; // Changed to string (icon name) + icon: string; }[]; RecentChats?: { name: string; url: string; - icon: string; // Changed to string (icon name) + icon: string; id?: number; search_space_id?: number; actions?: { @@ -168,19 +169,26 @@ interface AppSidebarProps extends React.ComponentProps { onClick: () => void; }[]; }[]; + user?: { + name: string; + email: string; + avatar: string; + }; } -export function AppSidebar({ +// Memoized AppSidebar component for better performance +export const AppSidebar = memo(function AppSidebar({ navMain = defaultData.navMain, navSecondary = defaultData.navSecondary, RecentChats = defaultData.RecentChats, + user = defaultData.user, ...props }: AppSidebarProps) { // Process navMain to resolve icon names to components const processedNavMain = useMemo(() => { return navMain.map((item) => ({ ...item, - icon: iconMap[item.icon] || SquareTerminal, // Fallback to SquareTerminal if icon not found + icon: iconMap[item.icon] || SquareTerminal, })); }, [navMain]); @@ -188,7 +196,7 @@ export function AppSidebar({ const processedNavSecondary = useMemo(() => { return navSecondary.map((item) => ({ ...item, - icon: iconMap[item.icon] || Undo2, // Fallback to Undo2 if icon not found + icon: iconMap[item.icon] || Undo2, })); }, [navSecondary]); @@ -197,17 +205,17 @@ export function AppSidebar({ return ( RecentChats?.map((item) => ({ ...item, - icon: iconMap[item.icon] || MessageCircleMore, // Fallback to MessageCircleMore if icon not found + icon: iconMap[item.icon] || MessageCircleMore, })) || [] ); }, [RecentChats]); return ( - + - + @@ -221,11 +229,22 @@ export function AppSidebar({ - + + - {processedRecentChats.length > 0 && } - + + {processedRecentChats.length > 0 && ( + + + + )} + + + + {/* User Profile Section */} + + ); -} +}); diff --git a/surfsense_web/components/sidebar/nav-main.tsx b/surfsense_web/components/sidebar/nav-main.tsx index 41859b6..f768c7a 100644 --- a/surfsense_web/components/sidebar/nav-main.tsx +++ b/surfsense_web/components/sidebar/nav-main.tsx @@ -1,6 +1,7 @@ "use client"; import { ChevronRight, type LucideIcon } from "lucide-react"; +import { useMemo } from "react"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { @@ -15,46 +16,56 @@ import { SidebarMenuSubItem, } from "@/components/ui/sidebar"; -export function NavMain({ - items, -}: { - items: { +interface NavItem { + title: string; + url: string; + icon: LucideIcon; + isActive?: boolean; + items?: { title: string; url: string; - icon: LucideIcon; - isActive?: boolean; - items?: { - title: string; - url: string; - }[]; }[]; -}) { +} + +export function NavMain({ items }: { items: NavItem[] }) { + // Memoize items to prevent unnecessary re-renders + const memoizedItems = useMemo(() => items, [items]); + return ( Platform - {items.map((item, index) => ( + {memoizedItems.map((item, index) => ( - + {item.title} + {item.items?.length ? ( <> - + - Toggle + Toggle submenu - + {item.items?.map((subItem, subIndex) => ( - + {subItem.title} diff --git a/surfsense_web/components/sidebar/nav-projects.tsx b/surfsense_web/components/sidebar/nav-projects.tsx index 1ce323c..cd0245b 100644 --- a/surfsense_web/components/sidebar/nav-projects.tsx +++ b/surfsense_web/components/sidebar/nav-projects.tsx @@ -1,17 +1,27 @@ "use client"; -import { ExternalLink, Folder, type LucideIcon, MoreHorizontal, Share, Trash2 } from "lucide-react"; +import { + ExternalLink, + Folder, + type LucideIcon, + MoreHorizontal, + RefreshCw, + Search, + Share, + Trash2, +} from "lucide-react"; import { useRouter } from "next/navigation"; +import { useCallback, useMemo, useState } from "react"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, - DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { SidebarGroup, SidebarGroupLabel, + SidebarInput, SidebarMenu, SidebarMenuAction, SidebarMenuButton, @@ -26,6 +36,8 @@ const actionIconMap: Record = { Share, Trash2, MoreHorizontal, + Search, + RefreshCw, }; interface ChatAction { @@ -34,33 +46,57 @@ interface ChatAction { onClick: () => void; } -export function NavProjects({ - chats, -}: { - chats: { - name: string; - url: string; - icon: LucideIcon; - id?: number; - search_space_id?: number; - actions?: ChatAction[]; - }[]; -}) { +interface ChatItem { + name: string; + url: string; + icon: LucideIcon; + id?: number; + search_space_id?: number; + actions?: ChatAction[]; +} + +export function NavProjects({ chats }: { chats: ChatItem[] }) { const { isMobile } = useSidebar(); const router = useRouter(); + const [searchQuery, setSearchQuery] = useState(""); + const [isDeleting, setIsDeleting] = useState(null); const searchSpaceId = chats[0]?.search_space_id || ""; - return ( - - Recent Chats - - {chats.map((item, index) => ( - - - - {item.name} - + // 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 && ( @@ -73,44 +109,79 @@ export function NavProjects({ side={isMobile ? "bottom" : "right"} align={isMobile ? "end" : "start"} > - {item.actions ? ( - // Use the actions provided by the item - item.actions.map((action, actionIndex) => { - const ActionIcon = actionIconMap[action.icon] || Folder; - return ( - - - {action.name} - - ); - }) - ) : ( - // Default actions if none provided - <> - - - View Chat + {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} - - - - Delete Chat - - > - )} + ); + })} - - ))} - - router.push(`/dashboard/${searchSpaceId}/chats`)}> - - View All Chats - + )} + ); + }, + [isDeleting, router, isMobile, handleDeleteChat] + ); + + // Show search input if there are chats + const showSearch = chats.length > 0; + + return ( + + Recent Chats + + {/* Search Input */} + {showSearch && ( + + setSearchQuery(e.target.value)} + className="h-8" + /> + + )} + + + {/* Chat Items */} + {filteredChats.length > 0 ? ( + filteredChats.map((chat) => ) + ) : ( + /* No results state */ + + + + {searchQuery ? "No chats found" : "No recent chats"} + + + )} + + {/* View All Chats */} + {chats.length > 0 && ( + + router.push(`/dashboard/${searchSpaceId}/chats`)}> + + View All Chats + + + )} ); diff --git a/surfsense_web/components/sidebar/nav-secondary.tsx b/surfsense_web/components/sidebar/nav-secondary.tsx index f292ba7..ec2defa 100644 --- a/surfsense_web/components/sidebar/nav-secondary.tsx +++ b/surfsense_web/components/sidebar/nav-secondary.tsx @@ -2,6 +2,7 @@ import type { LucideIcon } from "lucide-react"; import type * as React from "react"; +import { useMemo } from "react"; import { SidebarGroup, @@ -11,23 +12,28 @@ import { SidebarMenuItem, } from "@/components/ui/sidebar"; +interface NavSecondaryItem { + title: string; + url: string; + icon: LucideIcon; +} + export function NavSecondary({ items, ...props }: { - items: { - title: string; - url: string; - icon: LucideIcon; - }[]; + items: NavSecondaryItem[]; } & React.ComponentPropsWithoutRef) { + // Memoize items to prevent unnecessary re-renders + const memoizedItems = useMemo(() => items, [items]); + return ( SearchSpace - {items.map((item, index) => ( + {memoizedItems.map((item, index) => ( - + {item.title} diff --git a/surfsense_web/components/sidebar/nav-user.tsx b/surfsense_web/components/sidebar/nav-user.tsx index 62229cb..664052d 100644 --- a/surfsense_web/components/sidebar/nav-user.tsx +++ b/surfsense_web/components/sidebar/nav-user.tsx @@ -1,7 +1,8 @@ "use client"; -import { BadgeCheck, ChevronsUpDown, LogOut, Settings } from "lucide-react"; +import { BadgeCheck, ChevronsUpDown, LogOut, Settings, User } from "lucide-react"; import { useParams, useRouter } from "next/navigation"; +import { memo, useCallback } from "react"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { DropdownMenu, @@ -13,90 +14,115 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { + SidebarGroup, SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar, } from "@/components/ui/sidebar"; -export function NavUser({ - user, -}: { - user: { - name: string; - email: string; - avatar: string; - }; -}) { +interface UserData { + name: string; + email: string; + avatar: string; +} + +// Memoized NavUser component for better performance +export const NavUser = memo(function NavUser({ user }: { user: UserData }) { const { isMobile } = useSidebar(); const router = useRouter(); const { search_space_id } = useParams(); - const handleLogout = () => { + // Memoized logout handler + const handleLogout = useCallback(() => { if (typeof window !== "undefined") { localStorage.removeItem("surfsense_bearer_token"); router.push("/"); } - }; + }, [router]); + + // Get user initials for avatar fallback + const userInitials = user.name + .split(" ") + .map((n) => n[0]) + .join("") + .toUpperCase() + .slice(0, 2); + return ( - - - - - - - - CN - - - {user.name} - {user.email} - - - - - - - + + + + + + - CN + + {userInitials || } + {user.name} - {user.email} + {user.email} - - - - + + + + + + + + + + {userInitials || } + + + + {user.name} + {user.email} + + + + + + router.push(`/dashboard/${search_space_id}/api-key`)} + aria-label="Manage API key" + > + + API Key + + + router.push(`/dashboard/${search_space_id}/api-key`)} + onClick={() => router.push(`/settings`)} + aria-label="Go to settings" > - - API Key + + Settings - - - router.push(`/settings`)}> - - Settings - - - - Log out - - - - - + + + Sign out + + + + + + ); -} +});
{error}