All checks were successful
CI / test (push) Successful in 3m58s
评估列表页(v4.2): - 顶部状态统计条(stat-chip 点击筛选,后端列表接口新增 stats 状态分布 + status 筛选参数) - 单行表格(名称/评测对象 ellipsis 省略)+ 会话进度迷你进度条 + 最后一列醒目查看按钮 + 整行可点击 - 详情/报告合一单层抽屉(1000px,Tabs:概览/决策过程/配置历史/报告), 审批/取消在抽屉头部;移除独立详情页 /intelligent-evals/detail 与 intelligentEvalNav store 任务队列页: - 与列表页统一 stat-chip 统计条 + 单行表格(评估状态小标、失败原因 tooltip) 清理:删除 IntelligentEvalDetail.tsx / intelligentEvalNav.ts;App.tsx 移除 detail 路由。任务队列保持独立菜单页。tsc 0 错误 vitest 19 passed 895 passed
282 lines
9.5 KiB
TypeScript
282 lines
9.5 KiB
TypeScript
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 (
|
|
<Suspense fallback={
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
}>
|
|
{children}
|
|
</Suspense>
|
|
)
|
|
}
|
|
|
|
interface RouteConfig {
|
|
path: string
|
|
name: string
|
|
icon: ReactNode
|
|
component: () => ReactNode
|
|
}
|
|
|
|
const routeConfigs: RouteConfig[] = [
|
|
{ path: '/', name: '仪表盘', icon: <DashboardOutlined />, component: () => <PageLoader><HomePage /></PageLoader> },
|
|
{ path: '/openclaw', name: 'AI 助手', icon: <RobotOutlined />, component: () => <PageLoader><OpenClawPage /></PageLoader> },
|
|
{ path: '/targets', name: '评测对象', icon: <AimOutlined />, component: () => <PageLoader><TargetsPage /></PageLoader> },
|
|
{ path: '/scenarios', name: '评测场景', icon: <FileTextOutlined />, component: () => <PageLoader><ScenariosPage /></PageLoader> },
|
|
{ path: '/runs', name: '评测执行', icon: <PlayCircleOutlined />, component: () => <PageLoader><RunsPage /></PageLoader> },
|
|
{ path: '/campaigns', name: '评估活动', icon: <ScheduleOutlined />, component: () => <PageLoader><CampaignsPage /></PageLoader> },
|
|
{ path: '/reports', name: '评测报告', icon: <BarChartOutlined />, component: () => <PageLoader><ReportsPage /></PageLoader> },
|
|
{ path: '/intelligent-evals', name: '智能评估', icon: <BulbOutlined />, component: () => <PageLoader><IntelligentEvalsPage /></PageLoader> },
|
|
{ path: '/intelligent-evals/tasks', name: '任务队列', icon: <UnorderedListOutlined />, component: () => <PageLoader><IntelligentEvalTasksPage /></PageLoader> },
|
|
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
|
|
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
|
|
]
|
|
|
|
const componentMap: Record<string, () => ReactNode> = {}
|
|
routeConfigs.forEach((r) => { componentMap[r.path] = r.component })
|
|
|
|
const PINNED_TAB_PATHS = new Set(['/', '/openclaw'])
|
|
|
|
const menuItems: MenuProps['items'] = [
|
|
{ key: '/', icon: <DashboardOutlined />, label: '仪表盘' },
|
|
{ key: '/openclaw', icon: <RobotOutlined />, label: 'AI 助手' },
|
|
{ key: '/targets', icon: <AimOutlined />, label: '评测对象' },
|
|
{
|
|
key: 'group-static',
|
|
icon: <ExperimentOutlined />,
|
|
label: '静态评估',
|
|
children: [
|
|
{ key: '/scenarios', icon: <FileTextOutlined />, label: '评测场景' },
|
|
{ key: '/runs', icon: <PlayCircleOutlined />, label: '评测执行' },
|
|
{ key: '/campaigns', icon: <ScheduleOutlined />, label: '评估活动' },
|
|
{ key: '/reports', icon: <BarChartOutlined />, label: '评测报告' },
|
|
],
|
|
},
|
|
{
|
|
key: 'group-intelligent',
|
|
icon: <BulbOutlined />,
|
|
label: '智能评估',
|
|
children: [
|
|
{ key: '/intelligent-evals', icon: <BulbOutlined />, label: '评估列表' },
|
|
{ key: '/intelligent-evals/tasks', icon: <UnorderedListOutlined />, label: '任务队列' },
|
|
],
|
|
},
|
|
{
|
|
key: 'group-config',
|
|
icon: <SettingOutlined />,
|
|
label: '配置中心',
|
|
children: [
|
|
{ key: '/models', icon: <CloudServerOutlined />, label: '模型配置' },
|
|
],
|
|
},
|
|
{
|
|
key: 'group-system',
|
|
icon: <ToolOutlined />,
|
|
label: '系统',
|
|
children: [
|
|
{ key: '/files', icon: <FolderOpenOutlined />, label: '原始文件' },
|
|
],
|
|
},
|
|
]
|
|
|
|
type AuthState = 'checking' | 'login' | 'ready'
|
|
|
|
function App() {
|
|
const location = useLocation()
|
|
const navigate = useNavigate()
|
|
const { tabs, activeKey, openTab, setActiveTab } = useTabStore()
|
|
const [authState, setAuthState] = useState<AuthState>('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 (
|
|
<div style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (authState === 'login') {
|
|
return <LoginPage onSuccess={() => setAuthState('ready')} />
|
|
}
|
|
|
|
return (
|
|
<Layout style={{ height: '100vh', overflow: 'hidden' }}>
|
|
<Layout.Sider
|
|
width={220}
|
|
style={{
|
|
background: colors.sider,
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
overflow: 'hidden',
|
|
margin: 0,
|
|
padding: 0,
|
|
}}
|
|
>
|
|
<div
|
|
style={{
|
|
height: 56,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
padding: '0 12px 0 20px',
|
|
flexShrink: 0,
|
|
borderBottom: '1px solid rgba(255,255,255,0.06)',
|
|
}}
|
|
>
|
|
<span style={{ color: '#fff', fontWeight: 700, fontSize: 16, letterSpacing: '0.3px' }}>
|
|
AgentEvalTool
|
|
</span>
|
|
{authRequired && (
|
|
<Tooltip title="退出登录" placement="right">
|
|
<Button
|
|
type="text"
|
|
size="small"
|
|
icon={<LogoutOutlined />}
|
|
onClick={logout}
|
|
style={{ color: colors.siderText, marginLeft: 'auto' }}
|
|
/>
|
|
</Tooltip>
|
|
)}
|
|
</div>
|
|
<Menu
|
|
theme="dark"
|
|
mode="inline"
|
|
selectedKeys={[activeKey]}
|
|
defaultOpenKeys={['group-static', 'group-intelligent', 'group-config', 'group-system']}
|
|
items={menuItems}
|
|
onClick={handleMenuClick}
|
|
style={{
|
|
background: 'transparent',
|
|
border: 'none',
|
|
flex: 1,
|
|
overflow: 'auto',
|
|
paddingTop: 8,
|
|
paddingBottom: 0,
|
|
}}
|
|
/>
|
|
</Layout.Sider>
|
|
|
|
<Layout style={{ overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
|
<TabBar />
|
|
<Layout.Content
|
|
style={{
|
|
flex: 1,
|
|
minHeight: 0,
|
|
overflow: 'hidden',
|
|
background: colors.bgLayout,
|
|
position: 'relative',
|
|
}}
|
|
>
|
|
{tabs.map((tab) => {
|
|
const Component = componentMap[tab.key]
|
|
if (!Component) return null
|
|
const isFullBleed = tab.key === '/openclaw'
|
|
return (
|
|
<div
|
|
key={tab.key}
|
|
style={{
|
|
display: tab.key === activeKey ? 'block' : 'none',
|
|
...(isFullBleed
|
|
? { position: 'absolute', inset: 0 }
|
|
: { height: '100%', overflow: 'hidden' }),
|
|
}}
|
|
>
|
|
<Component />
|
|
</div>
|
|
)
|
|
})}
|
|
</Layout.Content>
|
|
</Layout>
|
|
</Layout>
|
|
)
|
|
}
|
|
|
|
export default App
|