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