Spaces:
Running
Running
| import React, { useState, useMemo } from "react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { ErrorBoundary } from "../shared/ErrorBoundary"; | |
| import { | |
| BarChart3, | |
| FileText, | |
| Upload, | |
| GitCompare, | |
| Link, | |
| Layers, | |
| Search, | |
| Bell, | |
| Settings, | |
| HelpCircle, | |
| User, | |
| ChevronDown, | |
| ChevronRight, | |
| PanelLeftClose, | |
| PanelLeftOpen, | |
| } from "lucide-react"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuSeparator, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { useNavigation } from "@/context/NavigationContext"; | |
| import { WelcomeGuideModal } from "@/components/shared/WelcomeGuideModal"; | |
| import { SettingsModal } from "@/components/shared/SettingsModal"; | |
| interface SidebarItem { | |
| id: string; | |
| label: string; | |
| icon: React.ComponentType<{ className?: string }>; | |
| onClick: () => void; | |
| badge?: string | number; | |
| } | |
| interface SidebarGroup { | |
| id: string; | |
| label: string; | |
| items: SidebarItem[]; | |
| isCollapsible?: boolean; | |
| } | |
| interface SidebarProps { | |
| isCollapsed: boolean; | |
| onToggleSidebar: () => void; | |
| } | |
| export function AppSidebar({ isCollapsed, onToggleSidebar }: SidebarProps) { | |
| const { actions } = useAgentGraph(); | |
| const navigation = useNavigation(); | |
| const [expandedGroups, setExpandedGroups] = useState<string[]>([ | |
| "main", | |
| "tools", | |
| ]); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); | |
| const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); | |
| const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); | |
| const handleDashboardClick = () => { | |
| actions.setActiveView("welcome"); | |
| }; | |
| // Memoize notification calculations | |
| const unreadNotificationsCount = useMemo(() => { | |
| return navigation.state.notifications.filter((n) => !n.read).length; | |
| }, [navigation.state.notifications]); | |
| const hasUnreadNotifications = unreadNotificationsCount > 0; | |
| const navigationGroups: SidebarGroup[] = [ | |
| { | |
| id: "main", | |
| label: "Main", | |
| isCollapsible: true, | |
| items: [ | |
| { | |
| id: "dashboard", | |
| label: "Dashboard", | |
| icon: BarChart3, | |
| onClick: () => actions.setActiveView("welcome"), | |
| }, | |
| { | |
| id: "traces", | |
| label: "My Traces", | |
| icon: FileText, | |
| onClick: () => actions.setActiveView("traces"), | |
| }, | |
| { | |
| id: "gallery", | |
| label: "Gallery", | |
| icon: Layers, | |
| onClick: () => actions.setActiveView("example-traces"), | |
| }, | |
| ], | |
| }, | |
| { | |
| id: "tools", | |
| label: "Tools", | |
| isCollapsible: true, | |
| items: [ | |
| { | |
| id: "upload", | |
| label: "Upload", | |
| icon: Upload, | |
| onClick: () => actions.setActiveView("upload"), | |
| }, | |
| { | |
| id: "compare", | |
| label: "Compare", | |
| icon: GitCompare, | |
| onClick: () => actions.setActiveView("graph-comparison"), | |
| }, | |
| { | |
| id: "connections", | |
| label: "Connections", | |
| icon: Link, | |
| onClick: () => actions.setActiveView("connections"), | |
| }, | |
| ], | |
| }, | |
| ]; | |
| const toggleGroup = (groupId: string) => { | |
| setExpandedGroups((prev) => | |
| prev.includes(groupId) | |
| ? prev.filter((id) => id !== groupId) | |
| : [...prev, groupId] | |
| ); | |
| }; | |
| return ( | |
| <> | |
| <aside | |
| className={`${ | |
| isCollapsed ? "w-16" : "w-64" | |
| } border-r border-slate-700 bg-gradient-to-b from-slate-800 to-slate-900 text-white transition-[width] duration-300 ease-in-out overflow-hidden flex flex-col h-full fixed left-0 top-0 z-40`} | |
| > | |
| {/* Header - Logo and Brand */} | |
| <div className="flex items-center justify-center p-3 border-b border-slate-700"> | |
| {!isCollapsed && ( | |
| <div | |
| className="flex items-center gap-2 cursor-pointer hover:opacity-80 transition-opacity" | |
| onClick={handleDashboardClick} | |
| > | |
| <div className="relative"> | |
| <div className="h-7 w-7 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center shadow-md"> | |
| <svg | |
| width="16" | |
| height="16" | |
| viewBox="0 0 20 20" | |
| fill="none" | |
| className="text-white" | |
| > | |
| <path | |
| d="M10 2L16 8L10 14L4 8L10 2Z" | |
| fill="currentColor" | |
| opacity="0.9" | |
| /> | |
| <circle cx="10" cy="6" r="1.5" fill="white" opacity="0.8" /> | |
| <circle cx="6" cy="10" r="1.5" fill="white" opacity="0.8" /> | |
| <circle | |
| cx="14" | |
| cy="10" | |
| r="1.5" | |
| fill="white" | |
| opacity="0.8" | |
| /> | |
| <path | |
| d="M10 16L18 8M10 16L2 8" | |
| stroke="currentColor" | |
| strokeWidth="1.5" | |
| opacity="0.6" | |
| strokeLinecap="round" | |
| /> | |
| </svg> | |
| </div> | |
| </div> | |
| <div> | |
| <h1 className="text-lg font-bold">AgentGraph</h1> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| {/* Search Bar and Toggle */} | |
| {!isCollapsed ? ( | |
| <div className="border-b border-slate-700"> | |
| <div className="p-3 flex items-center gap-2"> | |
| <div className="relative flex-1"> | |
| <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 text-slate-400 h-3.5 w-3.5" /> | |
| <Input | |
| type="text" | |
| placeholder="Search traces, graphs..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| className="pl-8 h-8 bg-slate-700/50 border-slate-600 text-white placeholder-slate-400 focus:bg-slate-700 text-xs" | |
| /> | |
| </div> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onToggleSidebar} | |
| className="h-7 w-7 p-0 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors flex-shrink-0" | |
| title="Collapse sidebar" | |
| > | |
| <PanelLeftClose className="h-3.5 w-3.5" /> | |
| </Button> | |
| </div> | |
| </div> | |
| ) : ( | |
| // Collapsed state - show expand button | |
| <div className="p-2 border-b border-slate-700"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={onToggleSidebar} | |
| className="h-7 w-7 p-0 hover:bg-slate-700/50 text-slate-400 hover:text-white transition-colors mx-auto block" | |
| title="Expand sidebar" | |
| > | |
| <PanelLeftOpen className="h-3.5 w-3.5" /> | |
| </Button> | |
| </div> | |
| )} | |
| {/* Navigation Groups */} | |
| <div className="flex-1 overflow-y-auto"> | |
| <nav className="p-2"> | |
| <ErrorBoundary> | |
| {navigationGroups.map((group) => ( | |
| <div key={group.id} className="mb-3"> | |
| {!isCollapsed && ( | |
| <div className="flex items-center justify-between px-2 py-1 mb-1"> | |
| <span className="text-xs font-semibold text-slate-400 uppercase tracking-wide"> | |
| {group.label} | |
| </span> | |
| {group.isCollapsible && ( | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => toggleGroup(group.id)} | |
| className="h-3 w-3 p-0 text-slate-400 hover:text-white" | |
| > | |
| <ChevronRight | |
| className={`h-2.5 w-2.5 transition-transform ${ | |
| expandedGroups.includes(group.id) | |
| ? "rotate-90" | |
| : "" | |
| }`} | |
| /> | |
| </Button> | |
| )} | |
| </div> | |
| )} | |
| <div | |
| className={`space-y-1 ${ | |
| !isCollapsed && !expandedGroups.includes(group.id) | |
| ? "hidden" | |
| : "" | |
| }`} | |
| > | |
| {group.items.map((item) => ( | |
| <Button | |
| key={item.id} | |
| onClick={item.onClick} | |
| className={`${ | |
| isCollapsed | |
| ? "w-10 h-10 p-0 justify-center mx-auto" | |
| : "w-full justify-start gap-2 h-8 px-2" | |
| } text-xs font-medium text-slate-200 hover:bg-slate-700/50 hover:text-white transition-colors duration-200 relative`} | |
| variant="ghost" | |
| title={isCollapsed ? item.label : undefined} | |
| > | |
| <item.icon className="h-3.5 w-3.5 flex-shrink-0" /> | |
| {!isCollapsed && ( | |
| <> | |
| <span className="truncate">{item.label}</span> | |
| {item.badge && ( | |
| <span className="ml-auto text-xs bg-slate-600/50 text-slate-200 px-1 py-0.5 rounded flex-shrink-0"> | |
| {item.badge} | |
| </span> | |
| )} | |
| </> | |
| )} | |
| </Button> | |
| ))} | |
| </div> | |
| </div> | |
| ))} | |
| </ErrorBoundary> | |
| </nav> | |
| </div> | |
| {/* Bottom Section */} | |
| <div className="border-t border-slate-700 p-2"> | |
| <div className={`${isCollapsed ? "space-y-1" : "space-y-1"}`}> | |
| {/* Notifications */} | |
| <DropdownMenu | |
| open={isNotificationsOpen} | |
| onOpenChange={setIsNotificationsOpen} | |
| > | |
| <DropdownMenuTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| className={`${ | |
| isCollapsed | |
| ? "w-10 h-10 p-0 justify-center mx-auto" | |
| : "w-full justify-start gap-2 h-8 px-2" | |
| } text-slate-300 hover:bg-slate-700/50 hover:text-yellow-400 transition-colors relative`} | |
| title={isCollapsed ? "Notifications" : undefined} | |
| > | |
| <Bell className="h-3.5 w-3.5 flex-shrink-0" /> | |
| {!isCollapsed && ( | |
| <span className="text-xs">Notifications</span> | |
| )} | |
| {hasUnreadNotifications && ( | |
| <Badge | |
| variant="destructive" | |
| className="absolute -top-1 -right-1 h-4 w-4 rounded-full p-0 text-xs animate-pulse flex items-center justify-center" | |
| > | |
| {unreadNotificationsCount} | |
| </Badge> | |
| )} | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="w-80"> | |
| <div className="px-4 py-3 border-b"> | |
| <h4 className="font-semibold">Notifications</h4> | |
| <p className="text-sm text-muted-foreground"> | |
| {unreadNotificationsCount} unread | |
| </p> | |
| </div> | |
| <div className="max-h-80 overflow-y-auto"> | |
| {navigation.state.notifications.length > 0 ? ( | |
| navigation.state.notifications | |
| .slice(0, 10) | |
| .map((notification) => ( | |
| <DropdownMenuItem | |
| key={notification.id} | |
| className="flex flex-col items-start p-4 hover:bg-muted/50" | |
| onClick={() => | |
| navigation.actions.markNotificationRead( | |
| notification.id | |
| ) | |
| } | |
| > | |
| <div className="flex justify-between w-full"> | |
| <span className="font-medium"> | |
| {notification.title} | |
| </span> | |
| <span className="text-xs text-muted-foreground"> | |
| {notification.timestamp.toLocaleDateString()} | |
| </span> | |
| </div> | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| {notification.message} | |
| </p> | |
| </DropdownMenuItem> | |
| )) | |
| ) : ( | |
| <div className="p-8 text-center text-muted-foreground"> | |
| <Bell className="h-8 w-8 mx-auto mb-2" /> | |
| <p className="text-sm">No notifications</p> | |
| </div> | |
| )} | |
| </div> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| {/* Help */} | |
| <Button | |
| variant="ghost" | |
| onClick={() => setIsHelpModalOpen(true)} | |
| className={`${ | |
| isCollapsed | |
| ? "w-10 h-10 p-0 justify-center mx-auto" | |
| : "w-full justify-start gap-2 h-8 px-2" | |
| } text-slate-300 hover:bg-slate-700/50 hover:text-white transition-colors`} | |
| title={isCollapsed ? "Help" : undefined} | |
| > | |
| <HelpCircle className="h-3.5 w-3.5 flex-shrink-0" /> | |
| {!isCollapsed && <span className="text-xs">Help</span>} | |
| </Button> | |
| {/* Settings */} | |
| <Button | |
| variant="ghost" | |
| onClick={() => setIsSettingsModalOpen(true)} | |
| className={`${ | |
| isCollapsed | |
| ? "w-10 h-10 p-0 justify-center mx-auto" | |
| : "w-full justify-start gap-2 h-8 px-2" | |
| } text-slate-300 hover:bg-slate-700/50 hover:text-white transition-colors`} | |
| title={isCollapsed ? "Settings" : undefined} | |
| > | |
| <Settings className="h-3.5 w-3.5 flex-shrink-0" /> | |
| {!isCollapsed && <span className="text-xs">Settings</span>} | |
| </Button> | |
| {/* User Menu */} | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| className={`${ | |
| isCollapsed | |
| ? "w-10 h-10 p-0 justify-center mx-auto" | |
| : "w-full justify-start gap-2 h-8 px-2" | |
| } text-slate-300 hover:bg-slate-700/50 hover:text-white transition-colors`} | |
| title={isCollapsed ? "User Menu" : undefined} | |
| > | |
| <div className="h-5 w-5 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center flex-shrink-0"> | |
| <User className="h-2.5 w-2.5 text-white" /> | |
| </div> | |
| {!isCollapsed && ( | |
| <> | |
| <span className="text-xs">User</span> | |
| <ChevronDown className="h-2.5 w-2.5 ml-auto" /> | |
| </> | |
| )} | |
| </Button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="w-56"> | |
| <DropdownMenuItem> | |
| <User className="mr-2 h-4 w-4" /> | |
| Profile | |
| </DropdownMenuItem> | |
| <DropdownMenuItem> | |
| <Settings className="mr-2 h-4 w-4" /> | |
| Settings | |
| </DropdownMenuItem> | |
| <DropdownMenuItem> | |
| <HelpCircle className="mr-2 h-4 w-4" /> | |
| Help & Support | |
| </DropdownMenuItem> | |
| <DropdownMenuSeparator /> | |
| <DropdownMenuItem>Log out</DropdownMenuItem> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| </div> | |
| </div> | |
| </aside> | |
| {/* Modals and Dialogs */} | |
| <WelcomeGuideModal | |
| open={isHelpModalOpen} | |
| onOpenChange={setIsHelpModalOpen} | |
| /> | |
| <SettingsModal | |
| open={isSettingsModalOpen} | |
| onOpenChange={setIsSettingsModalOpen} | |
| /> | |
| </> | |
| ); | |
| } | |