wu981526092's picture
🚀 Deploy AgentGraph: Complete agent monitoring and knowledge graph system
c2ea5ed
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}
/>
</>
);
}