AgentEvalTool/frontend/web/src/App.tsx
sinohqb 00ad929d68
All checks were successful
CI / test (push) Successful in 3m58s
feat(intelligent-eval): v4.2 UI — stat bar + single-line table + unified detail drawer
评估列表页(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
2026-08-17 20:03:55 +08:00

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