import React, { useMemo, useState, useCallback } from 'react'; import { NavLink, Outlet, useLocation } from 'react-router-dom'; import { ErrorBoundary } from './ErrorBoundary'; import { OrgSwitcher } from '../cloud/OrgSwitcher'; import { ProjectSwitcher } from './ProjectSwitcher'; import { useFeatures } from '../hooks/useFeatures'; import { useAuth } from '../context/AuthContext'; interface NavItem { to: string; label: string; icon: React.ReactNode; } interface NavSection { header: string; items: NavItem[]; } const navSections: NavSection[] = [ { header: 'CORE', items: [ { to: '/', label: 'Overview', icon: ( ), }, { to: '/sessions', label: 'Sessions', icon: ( ), }, { to: '/agents', label: 'Agents', icon: ( ), }, { to: '/memories', label: 'Memories', icon: ๐Ÿง , }, { to: '/delegations', label: 'Delegations', icon: ๐Ÿ“Š, }, ], }, { header: 'ANALYTICS', items: [ { to: '/analytics', label: 'Analytics', icon: ( ), }, { to: '/users', label: 'Users', icon: ๐Ÿ‘ค, }, { to: '/benchmarks', label: 'Benchmarks', icon: ( ), }, ], }, { header: 'CONFIGURE', items: [ { to: '/prompts', label: 'Prompts', icon: ๐Ÿ“, }, { to: '/playground', label: 'Playground', icon: ๐ŸŽฎ, }, { to: '/guardrails', label: 'Guardrails', icon: ๐Ÿ›ก๏ธ, }, { to: '/budgets', label: 'Budgets', icon: ๐Ÿ’ฐ, }, { to: '/compliance', label: 'Compliance', icon: ๐Ÿ“‹, }, { to: '/eval/evaluators', label: 'Evaluators', icon: โš–๏ธ, }, { to: '/review', label: 'Review', icon: โœ๏ธ, }, { to: '/alerts', label: 'Alerts', icon: ( ), }, { to: '/settings', label: 'Settings', icon: ( ), }, ], }, ]; function linkClass({ isActive }: { isActive: boolean }): string { const base = 'flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors'; return isActive ? `${base} bg-brand-50 text-brand-700` : `${base} text-gray-600 hover:bg-gray-100 hover:text-gray-900`; } const LORE_NAV_PATHS = new Set(['/memories']); const MESH_NAV_PATHS = new Set(['/delegations']); const STORAGE_KEY = 'agentlens:sidebar-collapsed'; function loadCollapsedState(): Record { try { const raw = localStorage.getItem(STORAGE_KEY); return raw ? JSON.parse(raw) : {}; } catch { return {}; } } function saveCollapsedState(state: Record): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch { // ignore storage errors } } const ROUTE_TITLES: Record = { '/': 'Overview', '/sessions': 'Sessions', '/agents': 'Agents', '/memories': 'Memories', '/delegations': 'Delegations', '/analytics': 'Analytics', '/benchmarks': 'Benchmarks', '/prompts': 'Prompts', '/guardrails': 'Guardrails', '/budgets': 'Budgets', '/compliance': 'Compliance', '/eval/evaluators': 'Evaluator Catalog', '/alerts': 'Alerts', '/settings': 'Settings', }; export const Layout = React.memo(function Layout(): React.ReactElement { const [sidebarOpen, setSidebarOpen] = useState(false); const [collapsedGroups, setCollapsedGroups] = useState>(loadCollapsedState); const { lore, mesh } = useFeatures(); const { user, logout, authMode } = useAuth(); const location = useLocation(); const toggleGroup = useCallback((header: string) => { setCollapsedGroups((prev) => { const next = { ...prev, [header]: !prev[header] }; saveCollapsedState(next); return next; }); }, []); const pageTitle = useMemo(() => { const path = location.pathname; if (ROUTE_TITLES[path]) return ROUTE_TITLES[path]; // Match first segment for sub-routes like /sessions/:id const base = '/' + path.split('/').filter(Boolean)[0]; return ROUTE_TITLES[base] ?? 'Dashboard'; }, [location.pathname]); const visibleSections = useMemo( () => navSections.map((section) => ({ ...section, items: section.items.filter((item) => { if (LORE_NAV_PATHS.has(item.to) && !lore) return false; if (MESH_NAV_PATHS.has(item.to) && !mesh) return false; return true; }), })).filter((section) => section.items.length > 0), [lore, mesh] ); return (
{/* Mobile overlay */} {sidebarOpen && (
setSidebarOpen(false)} aria-hidden="true" /> )} {/* Sidebar */} {/* Main content */}
{/* Top bar */}

{pageTitle}

{user && authMode === 'dual' && ( <> {user.name || user.email} )}
{/* Page content. overflow-x-hidden (not the default visibleโ†’auto that overflow-y-auto forces) so wide tables scroll only in their own overflow-x-auto wrapper โ€” otherwise main and the wrapper both show a horizontal scrollbar. min-w-0 lets this flex child shrink below its content so the inner wrapper, not main, takes the scroll. */}
); });