import { lazy, Suspense, useEffect, useState } from 'react' import { useLocation, useNavigate } from 'react-router-dom' import { Layout, Menu, Spin, Button, Tooltip } from 'antd' import type { MenuProps } from 'antd' import { DashboardOutlined, AimOutlined, FileTextOutlined, PlayCircleOutlined, BarChartOutlined, RobotOutlined, FolderOpenOutlined, CloudServerOutlined, ScheduleOutlined, LogoutOutlined, BulbOutlined, ExperimentOutlined, SettingOutlined, ToolOutlined, UnorderedListOutlined, } from '@ant-design/icons' import TabBar from './components/TabBar' import LoginPage from './pages/Login' import { authApi, authToken, setOnUnauthorized } from './api' import { useTabStore, type TabItem } from './stores/tabStore' import { colors } from './tokens' import type { ReactNode } from 'react' // Lazy-load pages to split heavy dependencies (@ant-design/charts, Monaco Editor) // into separate chunks. The keep-alive tab pattern still works — each page is // loaded on first access and then stays mounted. const HomePage = lazy(() => import('./pages/Home')) const TargetsPage = lazy(() => import('./pages/Targets')) const ScenariosPage = lazy(() => import('./pages/Scenarios')) const RunsPage = lazy(() => import('./pages/Runs')) const ReportsPage = lazy(() => import('./pages/Reports')) const CampaignsPage = lazy(() => import('./pages/Campaigns')) const OpenClawPage = lazy(() => import('./pages/OpenClaw')) const FilesPage = lazy(() => import('./pages/Files')) const ModelConfigsPage = lazy(() => import('./pages/ModelConfigs')) const IntelligentEvalsPage = lazy(() => import('./pages/IntelligentEvals')) const IntelligentEvalTasksPage = lazy(() => import('./pages/IntelligentEvalTasks')) function PageLoader({ children }: { children: ReactNode }) { return ( }> {children} ) } interface RouteConfig { path: string name: string icon: ReactNode component: () => ReactNode } const routeConfigs: RouteConfig[] = [ { path: '/', name: '仪表盘', icon: , component: () => }, { path: '/openclaw', name: 'AI 助手', icon: , component: () => }, { path: '/targets', name: '评测对象', icon: , component: () => }, { path: '/scenarios', name: '评测场景', icon: , component: () => }, { path: '/runs', name: '评测执行', icon: , component: () => }, { path: '/campaigns', name: '评估活动', icon: , component: () => }, { path: '/reports', name: '评测报告', icon: , component: () => }, { path: '/intelligent-evals', name: '智能评估', icon: , component: () => }, { path: '/intelligent-evals/tasks', name: '任务队列', icon: , component: () => }, { path: '/models', name: '模型配置', icon: , component: () => }, { path: '/files', name: '原始文件', icon: , component: () => }, ] const componentMap: Record ReactNode> = {} routeConfigs.forEach((r) => { componentMap[r.path] = r.component }) const PINNED_TAB_PATHS = new Set(['/', '/openclaw']) const menuItems: MenuProps['items'] = [ { key: '/', icon: , label: '仪表盘' }, { key: '/openclaw', icon: , label: 'AI 助手' }, { key: '/targets', icon: , label: '评测对象' }, { key: 'group-static', icon: , label: '静态评估', children: [ { key: '/scenarios', icon: , label: '评测场景' }, { key: '/runs', icon: , label: '评测执行' }, { key: '/campaigns', icon: , label: '评估活动' }, { key: '/reports', icon: , label: '评测报告' }, ], }, { key: 'group-intelligent', icon: , label: '智能评估', children: [ { key: '/intelligent-evals', icon: , label: '评估列表' }, { key: '/intelligent-evals/tasks', icon: , label: '任务队列' }, ], }, { key: 'group-config', icon: , label: '配置中心', children: [ { key: '/models', icon: , label: '模型配置' }, ], }, { key: 'group-system', icon: , label: '系统', children: [ { key: '/files', icon: , label: '原始文件' }, ], }, ] type AuthState = 'checking' | 'login' | 'ready' function App() { const location = useLocation() const navigate = useNavigate() const { tabs, activeKey, openTab, setActiveTab } = useTabStore() const [authState, setAuthState] = useState('checking') const [authRequired, setAuthRequired] = useState(false) useEffect(() => { setOnUnauthorized(() => setAuthState('login')) authApi.status() .then((res) => { setAuthRequired(res.data.auth_required) setAuthState(res.data.authenticated ? 'ready' : 'login') }) .catch(() => setAuthState('ready')) // 后端不可达时不锁死界面,由具体请求报错 return () => setOnUnauthorized(null) }, []) const logout = () => { authToken.clear() setAuthState('login') } useEffect(() => { const config = routeConfigs.find((r) => r.path === location.pathname) if (config) { const tab: TabItem = { key: config.path, title: config.name, icon: config.icon, closable: !PINNED_TAB_PATHS.has(config.path), } openTab(tab) } else { setActiveTab('/') navigate('/') } }, [location.pathname]) const handleMenuClick: MenuProps['onClick'] = ({ key }) => { const config = routeConfigs.find((r) => r.path === key) if (!config) return const tab: TabItem = { key: config.path, title: config.name, icon: config.icon, closable: !PINNED_TAB_PATHS.has(config.path), } openTab(tab) navigate(key) } if (authState === 'checking') { return (
) } if (authState === 'login') { return setAuthState('ready')} /> } return (
AgentEvalTool {authRequired && (
{tabs.map((tab) => { const Component = componentMap[tab.key] if (!Component) return null const isFullBleed = tab.key === '/openclaw' return (
) })}
) } export default App