import { useCallback, useEffect, useMemo, useState, type ComponentType, type ReactNode, } from "react"; import { Routes, Route, NavLink, Navigate, useLocation, useNavigate, } from "react-router-dom"; import { Activity, BarChart3, BookOpen, Clock, Code, Database, Download, Eye, FileText, Globe, Heart, KeyRound, Loader2, Menu, MessageSquare, Package, Puzzle, RotateCw, Settings, Shield, Sparkles, Star, Terminal, Wrench, X, Zap, } from "lucide-react"; import { SelectionSwitcher, Typography } from "@nous-research/ui"; import { cn } from "@/lib/utils"; import { Backdrop } from "@/components/Backdrop"; import { SidebarFooter } from "@/components/SidebarFooter"; import { SidebarStatusStrip } from "@/components/SidebarStatusStrip"; import { PageHeaderProvider } from "@/contexts/PageHeaderProvider"; import { useSystemActions } from "@/contexts/useSystemActions"; import type { SystemAction } from "@/contexts/system-actions-context"; import ConfigPage from "@/pages/ConfigPage"; import DocsPage from "@/pages/DocsPage"; import EnvPage from "@/pages/EnvPage"; import SessionsPage from "@/pages/SessionsPage"; import LogsPage from "@/pages/LogsPage"; import AnalyticsPage from "@/pages/AnalyticsPage"; import CronPage from "@/pages/CronPage"; import SkillsPage from "@/pages/SkillsPage"; import ChatPage from "@/pages/ChatPage"; import { LanguageSwitcher } from "@/components/LanguageSwitcher"; import { ThemeSwitcher } from "@/components/ThemeSwitcher"; import { useI18n } from "@/i18n"; import { PluginPage, PluginSlot, usePlugins } from "@/plugins"; import type { PluginManifest } from "@/plugins"; import { useTheme } from "@/themes"; function RootRedirect() { return ; } /** Built-in route → page component. Used for routing and for plugin `tab.path` / `tab.override` resolution. */ const BUILTIN_ROUTES: Record = { "/": RootRedirect, "/chat": ChatPage, "/sessions": SessionsPage, "/analytics": AnalyticsPage, "/logs": LogsPage, "/cron": CronPage, "/skills": SkillsPage, "/config": ConfigPage, "/env": EnvPage, "/docs": DocsPage, }; const BUILTIN_NAV: NavItem[] = [ { path: "/chat", labelKey: "chat", label: "Chat", icon: Terminal }, { path: "/sessions", labelKey: "sessions", label: "Sessions", icon: MessageSquare, }, { path: "/analytics", labelKey: "analytics", label: "Analytics", icon: BarChart3, }, { path: "/logs", labelKey: "logs", label: "Logs", icon: FileText }, { path: "/cron", labelKey: "cron", label: "Cron", icon: Clock }, { path: "/skills", labelKey: "skills", label: "Skills", icon: Package }, { path: "/config", labelKey: "config", label: "Config", icon: Settings }, { path: "/env", labelKey: "keys", label: "Keys", icon: KeyRound }, { path: "/docs", labelKey: "documentation", label: "Documentation", icon: BookOpen, }, ]; const ICON_MAP: Record> = { Activity, BarChart3, Clock, FileText, KeyRound, MessageSquare, Package, Settings, Puzzle, Sparkles, Terminal, Globe, Database, Shield, Wrench, Zap, Heart, Star, Code, Eye, }; function resolveIcon(name: string): ComponentType<{ className?: string }> { return ICON_MAP[name] ?? Puzzle; } function buildNavItems(builtIn: NavItem[], manifests: PluginManifest[]): NavItem[] { const items = [...builtIn]; for (const manifest of manifests) { if (manifest.tab.override) continue; if (manifest.tab.hidden) continue; const pluginItem: NavItem = { path: manifest.tab.path, label: manifest.label, icon: resolveIcon(manifest.icon), }; const pos = manifest.tab.position ?? "end"; if (pos === "end") { items.push(pluginItem); } else if (pos.startsWith("after:")) { const target = "/" + pos.slice(6); const idx = items.findIndex((i) => i.path === target); items.splice(idx >= 0 ? idx + 1 : items.length, 0, pluginItem); } else if (pos.startsWith("before:")) { const target = "/" + pos.slice(7); const idx = items.findIndex((i) => i.path === target); items.splice(idx >= 0 ? idx : items.length, 0, pluginItem); } else { items.push(pluginItem); } } return items; } function buildRoutes(manifests: PluginManifest[]): Array<{ key: string; path: string; element: ReactNode; }> { const byOverride = new Map(); const addons: PluginManifest[] = []; for (const m of manifests) { if (m.tab.override) { byOverride.set(m.tab.override, m); } else { addons.push(m); } } const routes: Array<{ key: string; path: string; element: ReactNode; }> = []; for (const [path, Component] of Object.entries(BUILTIN_ROUTES)) { const om = byOverride.get(path); if (om) { routes.push({ key: `override:${om.name}`, path, element: , }); } else { routes.push({ key: `builtin:${path}`, path, element: }); } } for (const m of addons) { if (m.tab.hidden) continue; if (BUILTIN_ROUTES[m.tab.path]) continue; routes.push({ key: `plugin:${m.name}`, path: m.tab.path, element: , }); } for (const m of manifests) { if (!m.tab.hidden) continue; if (BUILTIN_ROUTES[m.tab.path] || m.tab.override) continue; routes.push({ key: `plugin:hidden:${m.name}`, path: m.tab.path, element: , }); } return routes; } export default function App() { const { t } = useI18n(); const { pathname } = useLocation(); const { manifests } = usePlugins(); const { theme } = useTheme(); const [mobileOpen, setMobileOpen] = useState(false); const closeMobile = useCallback(() => setMobileOpen(false), []); const isDocsRoute = pathname === "/docs" || pathname === "/docs/"; const navItems = useMemo( () => buildNavItems(BUILTIN_NAV, manifests), [manifests], ); const routes = useMemo(() => buildRoutes(manifests), [manifests]); const pluginTabMeta = useMemo( () => manifests .filter((m) => !m.tab.hidden) .map((m) => ({ path: m.tab.override ?? m.tab.path, label: m.label, })), [manifests], ); const layoutVariant = theme.layoutVariant ?? "standard"; useEffect(() => { if (!mobileOpen) return; const onKey = (e: KeyboardEvent) => { if (e.key === "Escape") setMobileOpen(false); }; document.addEventListener("keydown", onKey); const prevOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; return () => { document.removeEventListener("keydown", onKey); document.body.style.overflow = prevOverflow; }; }, [mobileOpen]); useEffect(() => { const mql = window.matchMedia("(min-width: 1024px)"); const onChange = (e: MediaQueryListEvent) => { if (e.matches) setMobileOpen(false); }; mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange); }, []); return ( setMobileOpen(true)} aria-label={t.app.openNavigation} aria-expanded={mobileOpen} aria-controls="app-sidebar" className={cn( "inline-flex h-8 w-8 items-center justify-center", "text-midground/70 hover:text-midground transition-colors cursor-pointer", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground", )} > {t.app.brand} {mobileOpen && ( )} {routes.map(({ key, path, element }) => ( ))} } /> ); } function SidebarSystemActions({ onNavigate }: { onNavigate: () => void }) { const { t } = useI18n(); const navigate = useNavigate(); const { activeAction, isBusy, isRunning, pendingAction, runAction } = useSystemActions(); const items: SystemActionItem[] = [ { action: "restart", icon: RotateCw, label: t.status.restartGateway, runningLabel: t.status.restartingGateway, spin: true, }, { action: "update", icon: Download, label: t.status.updateHermes, runningLabel: t.status.updatingHermes, spin: false, }, ]; const handleClick = (action: SystemAction) => { if (isBusy) return; void runAction(action); navigate("/sessions"); onNavigate(); }; return ( {t.app.system} {items.map(({ action, icon: Icon, label, runningLabel, spin }) => { const isPending = pendingAction === action; const isActionRunning = activeAction === action && isRunning && !isPending; const busy = isPending || isActionRunning; const displayLabel = isActionRunning ? runningLabel : label; const disabled = isBusy && !busy; return ( handleClick(action)} disabled={disabled} aria-busy={busy} className={cn( "group relative flex w-full items-center gap-3", "px-5 py-1.5", "font-mondwest text-[0.75rem] tracking-[0.1em]", "text-left whitespace-nowrap transition-opacity cursor-pointer", "focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-midground", busy ? "text-midground opacity-100" : "opacity-60 hover:opacity-100", "disabled:cursor-not-allowed disabled:opacity-30", )} > {isPending ? ( ) : ( )} {displayLabel} {busy && ( )} ); })} ); } interface NavItem { icon: ComponentType<{ className?: string }>; label: string; labelKey?: string; path: string; } interface SystemActionItem { action: SystemAction; icon: ComponentType<{ className?: string }>; label: string; runningLabel: string; spin: boolean; }