Spaces:
Running
Running
| import React, { useState, useMemo, useCallback } from "react"; | |
| import { useAgentGraph } from "@/context/AgentGraphContext"; | |
| import { useNavigation } from "@/context/NavigationContext"; | |
| import { | |
| Search, | |
| Bell, | |
| Settings, | |
| HelpCircle, | |
| User, | |
| ChevronDown, | |
| FileText, | |
| GitBranch, | |
| } from "lucide-react"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Input } from "@/components/ui/input"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuSeparator, | |
| DropdownMenuTrigger, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { WelcomeGuideModal } from "@/components/shared/WelcomeGuideModal"; | |
| import { SettingsModal } from "@/components/shared/SettingsModal"; | |
| export function TopNavBar() { | |
| const { state, actions } = useAgentGraph(); | |
| const navigation = useNavigation(); | |
| const [searchQuery, setSearchQuery] = useState(""); | |
| const [isSearchOpen, setIsSearchOpen] = useState(false); | |
| const [isHelpModalOpen, setIsHelpModalOpen] = useState(false); | |
| const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); | |
| const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false); | |
| const handleDashboardClick = useCallback(() => { | |
| actions.setActiveView("welcome"); | |
| }, [actions]); | |
| const searchResults = useMemo(() => { | |
| if (!searchQuery.trim()) return { traces: [], graphs: [] }; | |
| const query = searchQuery.toLowerCase(); | |
| const traces = state.traces.filter( | |
| (trace) => | |
| trace.filename.toLowerCase().includes(query) || | |
| trace.title?.toLowerCase().includes(query) || | |
| trace.description?.toLowerCase().includes(query) | |
| ); | |
| const graphs = state.knowledgeGraphs.filter( | |
| (kg) => | |
| kg.filename.toLowerCase().includes(query) || | |
| kg.kg_id.toLowerCase().includes(query) | |
| ); | |
| return { traces: traces.slice(0, 5), graphs: graphs.slice(0, 5) }; | |
| }, [searchQuery, state.traces, state.knowledgeGraphs]); | |
| const handleSearchSelect = useCallback( | |
| (type: "trace" | "graph", id: string) => { | |
| if (type === "trace") { | |
| const trace = state.traces.find((t) => t.id.toString() === id); | |
| if (trace) { | |
| actions.setSelectedTrace(trace); | |
| actions.setActiveView("trace-kg"); | |
| } | |
| } else { | |
| const graph = state.knowledgeGraphs.find((kg) => kg.kg_id === id); | |
| if (graph) { | |
| actions.setSelectedKnowledgeGraph(graph); | |
| actions.setActiveView("kg-visualizer"); | |
| } | |
| } | |
| setIsSearchOpen(false); | |
| setSearchQuery(""); | |
| }, | |
| [state.traces, state.knowledgeGraphs, actions] | |
| ); | |
| // Close search when clicking outside | |
| React.useEffect(() => { | |
| const handleClickOutside = (event: MouseEvent) => { | |
| const target = event.target as Element; | |
| if (!target.closest(".search-container")) { | |
| setIsSearchOpen(false); | |
| } | |
| }; | |
| if (isSearchOpen) { | |
| document.addEventListener("mousedown", handleClickOutside); | |
| return () => | |
| document.removeEventListener("mousedown", handleClickOutside); | |
| } | |
| return undefined; | |
| }, [isSearchOpen]); | |
| const results = searchResults; | |
| const hasResults = results.traces.length > 0 || results.graphs.length > 0; | |
| // Memoize notification calculations to prevent infinite loops | |
| const unreadNotificationsCount = useMemo(() => { | |
| return navigation.state.notifications.filter((n) => !n.read).length; | |
| }, [navigation.state.notifications]); | |
| const hasUnreadNotifications = unreadNotificationsCount > 0; | |
| return ( | |
| <header className="h-16 border-b border-slate-700 bg-gradient-to-r from-slate-800 via-slate-850 to-slate-800 backdrop-blur supports-[backdrop-filter]:bg-slate-800/60 sticky top-0 z-50 shadow-lg"> | |
| <div className="flex items-center justify-between h-full px-6"> | |
| {/* Left Section - Logo and Brand */} | |
| <div className="flex items-center gap-6"> | |
| {/* Logo */} | |
| <div | |
| className="flex items-center gap-3 cursor-pointer hover:opacity-80 transition-opacity" | |
| onClick={handleDashboardClick} | |
| > | |
| <div className="relative"> | |
| <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center shadow-md"> | |
| {/* Simple, Modern AgentGraph Icon */} | |
| <svg | |
| width="18" | |
| height="18" | |
| viewBox="0 0 20 20" | |
| fill="none" | |
| className="text-white" | |
| > | |
| {/* Main diamond shape representing AI core */} | |
| <path | |
| d="M10 2L16 8L10 14L4 8L10 2Z" | |
| fill="currentColor" | |
| opacity="0.9" | |
| /> | |
| {/* Connection nodes */} | |
| <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" /> | |
| {/* Graph connections */} | |
| <path | |
| d="M10 16L18 8M10 16L2 8" | |
| stroke="currentColor" | |
| strokeWidth="1.5" | |
| opacity="0.6" | |
| strokeLinecap="round" | |
| /> | |
| </svg> | |
| </div> | |
| <div className="absolute -top-1 -right-1 h-3 w-3 rounded-full bg-green-400 border-2 border-slate-800"></div> | |
| </div> | |
| <div className="flex flex-col"> | |
| <span className="text-lg font-bold bg-gradient-to-r from-white to-slate-200 bg-clip-text text-transparent"> | |
| AgentGraph | |
| </span> | |
| </div> | |
| </div> | |
| {/* Navigation Divider */} | |
| <div className="h-8 w-px bg-gradient-to-b from-transparent via-slate-600 to-transparent" /> | |
| {/* Enhanced Search Bar */} | |
| <div className="flex-1 max-w-2xl mx-8"> | |
| <div className="relative search-container"> | |
| <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-slate-400" /> | |
| <Input | |
| placeholder="Search traces, graphs, and more..." | |
| value={searchQuery} | |
| onChange={(e) => setSearchQuery(e.target.value)} | |
| onFocus={() => setIsSearchOpen(true)} | |
| className="w-full pl-10 h-9 bg-slate-700/50 border-slate-600 text-white placeholder-slate-400 hover:bg-slate-700/70 focus:bg-slate-700 transition-colors" | |
| /> | |
| {/* Search Results Dropdown */} | |
| {isSearchOpen && ( | |
| <div className="absolute top-full left-0 right-0 mt-1 bg-popover border rounded-md shadow-lg z-50 max-h-80 overflow-y-auto"> | |
| {searchQuery.trim() && hasResults ? ( | |
| <div className="p-2"> | |
| {results.traces.length > 0 && ( | |
| <div className="mb-4"> | |
| <div className="px-2 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wide"> | |
| Traces | |
| </div> | |
| {results.traces.map((trace) => ( | |
| <Button | |
| key={trace.id} | |
| variant="ghost" | |
| className="w-full justify-start h-auto p-2 mb-1" | |
| onClick={() => | |
| handleSearchSelect("trace", trace.id.toString()) | |
| } | |
| > | |
| <FileText className="h-4 w-4 mr-2 flex-shrink-0" /> | |
| <div className="text-left"> | |
| <div className="font-medium truncate"> | |
| {trace.filename} | |
| </div> | |
| {trace.description && ( | |
| <div className="text-xs text-muted-foreground truncate"> | |
| {trace.description} | |
| </div> | |
| )} | |
| </div> | |
| </Button> | |
| ))} | |
| </div> | |
| )} | |
| {results.graphs.length > 0 && ( | |
| <div> | |
| <div className="px-2 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wide"> | |
| Agent Graphs | |
| </div> | |
| {results.graphs.map((graph) => ( | |
| <Button | |
| key={graph.kg_id} | |
| variant="ghost" | |
| className="w-full justify-start h-auto p-2 mb-1" | |
| onClick={() => | |
| handleSearchSelect("graph", graph.kg_id) | |
| } | |
| > | |
| <GitBranch className="h-4 w-4 mr-2 flex-shrink-0" /> | |
| <div className="text-left"> | |
| <div className="font-medium truncate"> | |
| {graph.filename} | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| {graph.entity_count} entities,{" "} | |
| {graph.relation_count} relations | |
| </div> | |
| </div> | |
| </Button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ) : searchQuery.trim() && !hasResults ? ( | |
| <div className="p-8 text-center text-muted-foreground"> | |
| <Search className="h-8 w-8 mx-auto mb-2" /> | |
| <p className="text-sm">No results found</p> | |
| </div> | |
| ) : ( | |
| <div className="p-8 text-center text-muted-foreground"> | |
| <Search className="h-8 w-8 mx-auto mb-2" /> | |
| <p className="text-sm">Start typing to search...</p> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| </div> | |
| {/* Right Section - User Actions with enhanced styling */} | |
| <div className="flex items-center gap-1"> | |
| {/* Notifications */} | |
| <DropdownMenu | |
| open={isNotificationsOpen} | |
| onOpenChange={setIsNotificationsOpen} | |
| > | |
| <DropdownMenuTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="relative text-slate-300 hover:bg-slate-700/50 hover:text-yellow-400 transition-colors" | |
| > | |
| <Bell className="h-4 w-4" /> | |
| {hasUnreadNotifications && ( | |
| <Badge | |
| variant="destructive" | |
| className="absolute -top-1 -right-1 h-5 w-5 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) => { | |
| // Get notification type colors | |
| const getNotificationColor = (type: string) => { | |
| switch (type) { | |
| case "success": | |
| return "border-l-green-500 bg-green-50 dark:bg-green-950/20"; | |
| case "error": | |
| return "border-l-red-500 bg-red-50 dark:bg-red-950/20"; | |
| case "warning": | |
| return "border-l-yellow-500 bg-yellow-50 dark:bg-yellow-950/20"; | |
| case "info": | |
| return "border-l-blue-500 bg-blue-50 dark:bg-blue-950/20"; | |
| default: | |
| return "border-l-gray-500 bg-gray-50 dark:bg-gray-950/20"; | |
| } | |
| }; | |
| return ( | |
| <DropdownMenuItem | |
| key={notification.id} | |
| className={`flex flex-col items-start p-4 hover:bg-muted/50 border-l-4 ${getNotificationColor( | |
| notification.type | |
| )}`} | |
| 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> | |
| {navigation.state.notifications.length > 0 && ( | |
| <div className="border-t p-2"> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="w-full" | |
| onClick={() => navigation.actions.clearAllNotifications()} | |
| > | |
| Clear all | |
| </Button> | |
| </div> | |
| )} | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| {/* Help */} | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-slate-300 hover:bg-slate-700/50 hover:text-green-400 transition-colors" | |
| onClick={() => setIsHelpModalOpen(true)} | |
| > | |
| <HelpCircle className="h-4 w-4" /> | |
| </Button> | |
| {/* Settings */} | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="text-slate-300 hover:bg-slate-700/50 hover:text-slate-100 transition-colors" | |
| onClick={() => setIsSettingsModalOpen(true)} | |
| > | |
| <Settings className="h-4 w-4" /> | |
| </Button> | |
| {/* Divider */} | |
| <div className="h-6 w-px bg-slate-600 mx-1" /> | |
| {/* User Menu with enhanced styling */} | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| className="flex items-center gap-2 text-slate-300 hover:bg-slate-700/50 hover:text-white transition-colors px-3" | |
| > | |
| <div className="h-7 w-7 rounded-full bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center"> | |
| <User className="h-4 w-4 text-white" /> | |
| </div> | |
| <span className="hidden sm:block">User</span> | |
| <ChevronDown className="h-3 w-3" /> | |
| </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> | |
| {/* Help Modal */} | |
| <WelcomeGuideModal | |
| open={isHelpModalOpen} | |
| onOpenChange={setIsHelpModalOpen} | |
| /> | |
| {/* Settings Modal */} | |
| <SettingsModal | |
| open={isSettingsModalOpen} | |
| onOpenChange={setIsSettingsModalOpen} | |
| /> | |
| </header> | |
| ); | |
| } | |