refactor(intelligent-eval): split list page into dedicated pages
All checks were successful
CI / test (push) Successful in 4m7s

评估列表页此前用 Drawer 嵌套承载详情/报告/任务队列/新建,功能页面过多。
按 keep-alive 多页模式拆为独立页面(静态路由 + intelligentEvalNav store 传递选中):
- 评估列表 (/intelligent-evals):只留列表 + 新建;详情/报告/任务队列改为导航
- 任务队列 (/intelligent-evals/tasks):TaskQueueMonitor 独立页,二级菜单项
- 评估详情 (/intelligent-evals/detail):新独立页,页内 Tabs 承载概览/决策过程/
  配置历史/报告(completed 才显示报告 tab),取代 Drawer 嵌套;审批/打回/取消
  提到页面头部统一管理
- EvalDetail 拆为纯展示的 EvalOverview;DecisionProcess/ConfigSnapshots/EvalReport
  的 onBack 改可选(tab 环境不显示返回按钮)
- index.css 加 intelligent-detail-tabs 高度链(绕开 Ant CSS-in-JS 高度覆盖)
tsc 0 错误, vitest 19 passed
This commit is contained in:
sinohqb 2026-08-17 16:39:53 +08:00
parent 2dd023fdd9
commit a69fa8a797
11 changed files with 487 additions and 329 deletions

View File

@ -17,6 +17,7 @@ import {
ExperimentOutlined,
SettingOutlined,
ToolOutlined,
UnorderedListOutlined,
} from '@ant-design/icons'
import TabBar from './components/TabBar'
import LoginPage from './pages/Login'
@ -38,6 +39,8 @@ 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'))
const IntelligentEvalDetailPage = lazy(() => import('./pages/IntelligentEvalDetail'))
function PageLoader({ children }: { children: ReactNode }) {
return (
@ -67,6 +70,8 @@ const routeConfigs: RouteConfig[] = [
{ 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: '/intelligent-evals/detail', name: '评估详情', icon: <FileTextOutlined />, component: () => <PageLoader><IntelligentEvalDetailPage /></PageLoader> },
{ path: '/models', name: '模型配置', icon: <CloudServerOutlined />, component: () => <PageLoader><ModelConfigsPage /></PageLoader> },
{ path: '/files', name: '原始文件', icon: <FolderOpenOutlined />, component: () => <PageLoader><FilesPage /></PageLoader> },
]
@ -97,6 +102,7 @@ const menuItems: MenuProps['items'] = [
label: '智能评估',
children: [
{ key: '/intelligent-evals', icon: <BulbOutlined />, label: '评估列表' },
{ key: '/intelligent-evals/tasks', icon: <UnorderedListOutlined />, label: '任务队列' },
],
},
{

View File

@ -16,7 +16,8 @@ const SNAPSHOT_TYPE_LABELS: Record<string, { label: string; color: string }> = {
interface ConfigSnapshotsProps {
evalId: string
onBack: () => void
/** 独立页内作为子视图 tab 使用时可不传tab 切换代替返回)。 */
onBack?: () => void
}
export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps) {
@ -240,7 +241,7 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>}
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<div style={{ flex: 1 }} />
<Button

View File

@ -16,7 +16,8 @@ const DECISION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
interface DecisionProcessProps {
evalId: string
onBack: () => void
/** 独立页内作为子视图 tab 使用时可不传tab 切换代替返回)。 */
onBack?: () => void
}
export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps) {
@ -105,7 +106,7 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>}
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<div style={{ flex: 1 }} />
<Select

View File

@ -1,267 +0,0 @@
import { useState } from 'react'
import {
Alert, Button, Card, Col, Descriptions, Empty, Input, Modal, Popconfirm, Progress, Row, Space, Spin, Tag, message,
} from 'antd'
import { FileTextOutlined, HistoryOutlined, NodeIndexOutlined, StopOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type IntelligentEval } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime, shortDateTime } from '../../utils/date'
import { EVAL_STATUS, SESSION_STATUS } from './status'
import ConfigSnapshots from './ConfigSnapshots'
import DecisionProcess from './DecisionProcess'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
function personaLabel(persona: Record<string, unknown>): string {
if (typeof persona.name === 'string' && persona.name) return persona.name
if (typeof persona.background === 'string' && persona.background) return persona.background
return JSON.stringify(persona)
}
function PlanView({ ev }: { ev: IntelligentEval }) {
const plan = ev.plan
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
return (
<div>
<Descriptions column={3} size="small" bordered style={{ marginBottom: 12 }}>
<Descriptions.Item label="预估会话数">{plan.estimated_sessions}</Descriptions.Item>
<Descriptions.Item label="单会话最大轮数">{plan.budget?.max_turns_per_session ?? '—'}</Descriptions.Item>
<Descriptions.Item label="总轮数上限">{plan.budget?.total_max_turns ?? '—'}</Descriptions.Item>
</Descriptions>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space size={6} wrap style={{ marginBottom: 12 }}>
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue">{d}</Tag>)}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.virtual_users ?? []).map((u, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · {u.goal}
</div>
))}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.time_distribution ?? []).map((t, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<Tag style={{ margin: 0 }}>{t.time_slot}</Tag> {t.sessions} · {t.scenario}
</div>
))}
</Space>
{plan.completion_criteria && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}></b>{plan.completion_criteria}
</div>
)}
</div>
)
}
interface EvalDetailProps {
ev: IntelligentEval
targetName: string
onOpenReport: () => void
onChanged: () => void
}
export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: EvalDetailProps) {
const [busy, setBusy] = useState(false)
const [rejectOpen, setRejectOpen] = useState(false)
const [feedback, setFeedback] = useState('')
const [showConfigHistory, setShowConfigHistory] = useState(false)
const [showDecisionProcess, setShowDecisionProcess] = useState(false)
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
const showSessions = ev.status === 'executing' || ev.status === 'completed'
const sessions = showSessions ? ev.sessions ?? [] : []
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
setBusy(true)
try {
await fn()
message.success(okMsg)
onChanged()
} finally {
setBusy(false)
}
}
const approve = () => runAction(() => intelligentEvalsApi.approve(ev.id), '已批准,进入执行')
const cancel = () => runAction(() => intelligentEvalsApi.cancel(ev.id), '已取消')
const submitReject = () => runAction(async () => {
await intelligentEvalsApi.reject(ev.id, feedback)
setRejectOpen(false)
setFeedback('')
}, '已打回,等待重新规划')
if (showConfigHistory) {
return <ConfigSnapshots evalId={ev.id} onBack={() => setShowConfigHistory(false)} />
}
if (showDecisionProcess) {
return <DecisionProcess evalId={ev.id} onBack={() => setShowDecisionProcess(false)} />
}
return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>{ev.name}</span>
<Tag color={meta.color}>{meta.label}</Tag>
<div style={{ flex: 1 }} />
<Space>
<Button icon={<HistoryOutlined />} onClick={() => setShowConfigHistory(true)}></Button>
<Button icon={<NodeIndexOutlined />} onClick={() => setShowDecisionProcess(true)}></Button>
{ev.status === 'completed' && (
<Button type="primary" icon={<FileTextOutlined />} onClick={onOpenReport}></Button>
)}
{(ev.status === 'pending_approval' || ev.status === 'executing') && (
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
<Button danger icon={<StopOutlined />} loading={busy}></Button>
</Popconfirm>
)}
</Space>
</div>
<Row gutter={16}>
<Col span={9}>
<Card size="small" title="基本信息" style={sectionCard}>
<Descriptions column={1} size="small">
<Descriptions.Item label="评测对象">{targetName}</Descriptions.Item>
<Descriptions.Item label="时间窗口">{ev.time_window_hours} </Descriptions.Item>
<Descriptions.Item label="创建时间">{formatDateTime(ev.created_at)}</Descriptions.Item>
<Descriptions.Item label="开始时间">{formatDateTime(ev.started_at)}</Descriptions.Item>
<Descriptions.Item label="完成时间">{formatDateTime(ev.completed_at)}</Descriptions.Item>
<Descriptions.Item label="会话进度">{ev.completed_sessions}/{ev.session_count}</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="用户输入" style={sectionCard}>
<Descriptions column={1} size="small">
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item>
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item>
<Descriptions.Item label="角色描述">
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span>
</Descriptions.Item>
<Descriptions.Item label="种子集">
{Object.keys(ev.seeds ?? {}).length === 0
? '—'
: (
<pre style={{
margin: 0, fontSize: 12, background: colors.bgSubtle,
padding: 8, borderRadius: 6, overflowX: 'auto',
}}
>
{JSON.stringify(ev.seeds, null, 2)}
</pre>
)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={15}>
{ev.status === 'planning' && (
ev.plan_feedback ? (
<Alert
style={sectionCard}
type="warning"
showIcon
message="计划已打回OpenClaw 正在根据反馈重新规划"
description={ev.plan_feedback}
/>
) : (
<Card size="small" style={sectionCard}>
<Spin size="small" />
<span style={{ marginLeft: 10, color: colors.textSecondary }}>
OpenClaw
</span>
</Card>
)
)}
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
<Card
size="small"
title="粗计划"
style={sectionCard}
extra={ev.status === 'pending_approval' && (
<Space>
<Button size="small" danger onClick={() => setRejectOpen(true)}></Button>
<Button size="small" type="primary" loading={busy} onClick={approve}></Button>
</Space>
)}
>
<PlanView ev={ev} />
</Card>
)}
{showSessions && (
<Card
size="small"
title={`会话进度(${ev.completed_sessions}/${ev.session_count}`}
style={sectionCard}
extra={(
<Progress
percent={ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0}
size="small"
style={{ width: 140 }}
/>
)}
>
{sessions.length === 0 && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
OpenClaw
</div>
)}
{sessions.map((s) => {
const sMeta = SESSION_STATUS[s.status]
return (
<div
key={s.id}
style={{
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
padding: '6px 0', borderBottom: `1px solid ${colors.border}`, fontSize: 13,
}}
>
<b style={{ color: colors.text }}>{personaLabel(s.persona)}</b>
<Tag color={sMeta.color} style={{ margin: 0 }}>{sMeta.label}</Tag>
{s.dimension && <Tag style={{ margin: 0 }}>{s.dimension}</Tag>}
<span style={{ color: colors.textSecondary, flex: 1, minWidth: 120 }}>{s.goal}</span>
<span style={{ color: colors.textMuted }}>{s.turn_count} </span>
{s.created_at && (
<span style={{ color: colors.textMuted }}>{shortDateTime(s.created_at)}</span>
)}
</div>
)
})}
{ev.status === 'executing' && (
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
OpenClaw 5
</div>
)}
</Card>
)}
</Col>
</Row>
<Modal
title="打回计划"
open={rejectOpen}
onCancel={() => setRejectOpen(false)}
onOk={submitReject}
okText="打回"
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
>
<Input.TextArea
rows={4}
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="请填写打回原因与改进方向OpenClaw 会据此重新规划"
/>
</Modal>
</div>
)
}

View File

@ -0,0 +1,190 @@
import {
Alert, Card, Col, Descriptions, Empty, Progress, Row, Space, Spin, Tag,
} from 'antd'
import type { IntelligentEval } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime, shortDateTime } from '../../utils/date'
import { EVAL_STATUS, SESSION_STATUS } from './status'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
function personaLabel(persona: Record<string, unknown>): string {
if (typeof persona.name === 'string' && persona.name) return persona.name
if (typeof persona.background === 'string' && persona.background) return persona.background
return JSON.stringify(persona)
}
function PlanView({ ev }: { ev: IntelligentEval }) {
const plan = ev.plan
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
return (
<div>
<Descriptions column={3} size="small" bordered style={{ marginBottom: 12 }}>
<Descriptions.Item label="预估会话数">{plan.estimated_sessions}</Descriptions.Item>
<Descriptions.Item label="单会话最大轮数">{plan.budget?.max_turns_per_session ?? '—'}</Descriptions.Item>
<Descriptions.Item label="总轮数上限">{plan.budget?.total_max_turns ?? '—'}</Descriptions.Item>
</Descriptions>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space size={6} wrap style={{ marginBottom: 12 }}>
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue">{d}</Tag>)}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.virtual_users ?? []).map((u, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · {u.goal}
</div>
))}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.time_distribution ?? []).map((t, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<Tag style={{ margin: 0 }}>{t.time_slot}</Tag> {t.sessions} · {t.scenario}
</div>
))}
</Space>
{plan.completion_criteria && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}></b>{plan.completion_criteria}
</div>
)}
</div>
)
}
interface EvalOverviewProps {
ev: IntelligentEval
targetName: string
}
/**
* "概览"
* /IntelligentEvalDetail
*/
export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
const showSessions = ev.status === 'executing' || ev.status === 'completed'
const sessions = showSessions ? ev.sessions ?? [] : []
return (
<Row gutter={16}>
<Col span={9}>
<Card size="small" title="基本信息" style={sectionCard}>
<Descriptions column={1} size="small">
<Descriptions.Item label="评测对象">{targetName}</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={meta.color}>{meta.label}</Tag>
</Descriptions.Item>
<Descriptions.Item label="时间窗口">{ev.time_window_hours} </Descriptions.Item>
<Descriptions.Item label="创建时间">{formatDateTime(ev.created_at)}</Descriptions.Item>
<Descriptions.Item label="开始时间">{formatDateTime(ev.started_at)}</Descriptions.Item>
<Descriptions.Item label="完成时间">{formatDateTime(ev.completed_at)}</Descriptions.Item>
<Descriptions.Item label="会话进度">{ev.completed_sessions}/{ev.session_count}</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="用户输入" style={sectionCard}>
<Descriptions column={1} size="small">
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item>
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item>
<Descriptions.Item label="角色描述">
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span>
</Descriptions.Item>
<Descriptions.Item label="种子集">
{Object.keys(ev.seeds ?? {}).length === 0
? '—'
: (
<pre style={{
margin: 0, fontSize: 12, background: colors.bgSubtle,
padding: 8, borderRadius: 6, overflowX: 'auto',
}}
>
{JSON.stringify(ev.seeds, null, 2)}
</pre>
)}
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={15}>
{ev.status === 'planning' && (
ev.plan_feedback ? (
<Alert
style={sectionCard}
type="warning"
showIcon
message="计划已打回OpenClaw 正在根据反馈重新规划"
description={ev.plan_feedback}
/>
) : (
<Card size="small" style={sectionCard}>
<Spin size="small" />
<span style={{ marginLeft: 10, color: colors.textSecondary }}>
OpenClaw
</span>
</Card>
)
)}
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
<Card size="small" title="粗计划" style={sectionCard}>
<PlanView ev={ev} />
</Card>
)}
{showSessions && (
<Card
size="small"
title={`会话进度(${ev.completed_sessions}/${ev.session_count}`}
style={sectionCard}
extra={(
<Progress
percent={ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0}
size="small"
style={{ width: 140 }}
/>
)}
>
{sessions.length === 0 && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
OpenClaw
</div>
)}
{sessions.map((s) => {
const sMeta = SESSION_STATUS[s.status]
return (
<div
key={s.id}
style={{
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
padding: '6px 0', borderBottom: `1px solid ${colors.border}`, fontSize: 13,
}}
>
<b style={{ color: colors.text }}>{personaLabel(s.persona)}</b>
<Tag color={sMeta.color} style={{ margin: 0 }}>{sMeta.label}</Tag>
{s.dimension && <Tag style={{ margin: 0 }}>{s.dimension}</Tag>}
<span style={{ color: colors.textSecondary, flex: 1, minWidth: 120 }}>{s.goal}</span>
<span style={{ color: colors.textMuted }}>{s.turn_count} </span>
{s.created_at && (
<span style={{ color: colors.textMuted }}>{shortDateTime(s.created_at)}</span>
)}
</div>
)
})}
{ev.status === 'executing' && (
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
OpenClaw 5
</div>
)}
</Card>
)}
</Col>
</Row>
)
}

View File

@ -106,7 +106,8 @@ function SessionMessages({ evalId, sessionId, verdict }: {
interface EvalReportProps {
ev: IntelligentEval
onBack: () => void
/** 独立页内作为子视图 tab 使用时可不传tab 切换代替返回)。 */
onBack?: () => void
}
export default function EvalReport({ ev, onBack }: EvalReportProps) {
@ -175,7 +176,7 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>}
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>
{ev.name} ·
</span>

View File

@ -64,6 +64,26 @@ body {
display: none !important;
}
/*
* intelligent-detail-tabs评估详情页的 Tabs 需承载内容面板概览/决策过程/
* 配置历史/报告建立高度链Tabs 撑满父容器content-holder 占剩余空间
* 面板内的子组件height:100% + overflowY:auto才能正确滚动
*/
.ant-tabs.intelligent-detail-tabs {
display: flex;
flex-direction: column;
height: 100%;
}
.ant-tabs.intelligent-detail-tabs .ant-tabs-content-holder {
flex: 1;
min-height: 0;
overflow: hidden;
}
.ant-tabs.intelligent-detail-tabs .ant-tabs-content,
.ant-tabs.intelligent-detail-tabs .ant-tabs-tabpane {
height: 100%;
}
/*
* flex-tabs保留兼容当前 Runs 页改用 nav-only-tabs
*/

View File

@ -0,0 +1,193 @@
import { useEffect, useState } from 'react'
import {
Button, Empty, Input, Modal, Popconfirm, Space, Spin, Tabs, Tag, message,
} from 'antd'
import type { TabsProps } from 'antd'
import { StopOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, targetsApi, type IntelligentEval } from '../api'
import PageWrapper from '../components/PageWrapper'
import EvalOverview from '../components/intelligent_eval/EvalOverview'
import DecisionProcess from '../components/intelligent_eval/DecisionProcess'
import ConfigSnapshots from '../components/intelligent_eval/ConfigSnapshots'
import EvalReport from '../components/intelligent_eval/EvalReport'
import { EVAL_STATUS } from '../components/intelligent_eval/status'
import { useResource } from '../hooks/useResource'
import { usePolling } from '../hooks/usePolling'
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
import { colors } from '../tokens'
const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing'])
function isActive(status: string | undefined): boolean {
return status != null && ACTIVE_STATUSES.has(status)
}
/**
* keep-alive tab
*
* "详情/报告" id intelligentEvalNav store
* Tabs ///
* Drawer
*/
export default function IntelligentEvalDetailPage() {
const evalId = useIntelligentEvalNav((s) => s.selectedEvalId)
const initialTab = useIntelligentEvalNav((s) => s.detailTab)
const [detail, setDetail] = useState<IntelligentEval | null>(null)
const [busy, setBusy] = useState(false)
const [rejectOpen, setRejectOpen] = useState(false)
const [feedback, setFeedback] = useState('')
const [activeTab, setActiveTab] = useState<EvalDetailTab>(initialTab)
const { data: targets } = useResource(
() => targetsApi.list().then((r) => r.data),
{ tabPath: '/intelligent-evals/detail' },
)
const load = async () => {
if (!evalId) return
try {
const res = await intelligentEvalsApi.get(evalId)
setDetail(res.data)
} catch {
message.error('加载评估详情失败')
}
}
useEffect(() => {
if (evalId) {
setActiveTab(initialTab)
void load()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [evalId, initialTab])
usePolling(() => { void load() }, 5000, evalId != null && isActive(detail?.status))
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
setBusy(true)
try {
await fn()
message.success(okMsg)
await load()
} finally {
setBusy(false)
}
}
const approve = () => runAction(() => intelligentEvalsApi.approve(evalId!), '已批准,进入执行')
const cancel = () => runAction(() => intelligentEvalsApi.cancel(evalId!), '已取消')
const submitReject = () => runAction(async () => {
await intelligentEvalsApi.reject(evalId!, feedback)
setRejectOpen(false)
setFeedback('')
}, '已打回,等待重新规划')
if (!evalId) {
return (
<PageWrapper title="评估详情" inline fullHeight>
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Empty description="从「智能评估 → 评估列表」点击某条评估进入详情" />
</div>
</PageWrapper>
)
}
if (!detail) {
return (
<PageWrapper title="评估详情" inline fullHeight>
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Spin />
</div>
</PageWrapper>
)
}
const meta = EVAL_STATUS[detail.status] ?? { label: detail.status, color: 'default' }
const targetName = (id: string) =>
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
const canApprove = detail.status === 'pending_approval'
const canCancel = detail.status === 'pending_approval' || detail.status === 'executing'
const items: TabsProps['items'] = [
{
key: 'overview',
label: '概览',
children: (
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<EvalOverview ev={detail} targetName={targetName(detail.target_id)} />
</div>
),
},
{
key: 'decision',
label: '决策过程',
children: <DecisionProcess evalId={evalId} />,
},
{
key: 'history',
label: '配置历史',
children: <ConfigSnapshots evalId={evalId} />,
},
]
if (detail.status === 'completed') {
items.push({
key: 'report',
label: '评估报告',
children: <EvalReport ev={detail} />,
})
}
return (
<PageWrapper title="评估详情" inline fullHeight>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div
style={{
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
padding: '8px 16px', background: colors.bgContainer, borderBottom: `1px solid ${colors.border}`,
}}
>
<span style={{ fontSize: 15, fontWeight: 600, color: colors.text }}>{detail.name}</span>
<Tag color={meta.color}>{meta.label}</Tag>
<div style={{ flex: 1 }} />
<Space>
{canApprove && (
<>
<Button danger loading={busy} onClick={() => setRejectOpen(true)}></Button>
<Button type="primary" loading={busy} onClick={approve}></Button>
</>
)}
{canCancel && (
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
<Button danger icon={<StopOutlined />} loading={busy}></Button>
</Popconfirm>
)}
</Space>
</div>
<div style={{ flex: 1, minHeight: 0, padding: '0 16px', background: colors.bgLayout }}>
<Tabs
className="intelligent-detail-tabs"
activeKey={activeTab}
onChange={(k) => setActiveTab(k as EvalDetailTab)}
items={items}
/>
</div>
</div>
<Modal
title="打回计划"
open={rejectOpen}
onCancel={() => setRejectOpen(false)}
onOk={submitReject}
okText="打回"
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
>
<Input.TextArea
rows={4}
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="请填写打回原因与改进方向OpenClaw 会据此重新规划"
/>
</Modal>
</PageWrapper>
)
}

View File

@ -0,0 +1,23 @@
import PageWrapper from '../components/PageWrapper'
import TaskQueueMonitor from '../components/intelligent_eval/TaskQueueMonitor'
/**
*
*
* scan loop 60s + worker
* + + 5s
*/
export default function IntelligentEvalTasksPage() {
return (
<PageWrapper
title="任务队列"
description="平台每 60 秒扫描 executing 评估入队,有任务时触发 OpenClaw worker 执行"
inline
fullHeight
>
<div style={{ height: '100%', padding: '0 16px 16px' }}>
<TaskQueueMonitor />
</div>
</PageWrapper>
)
}

View File

@ -1,6 +1,7 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Spin, Table, Tag, Tooltip, message,
Button, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, Tooltip, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import {
@ -8,21 +9,18 @@ import {
} from '@ant-design/icons'
import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import EvalDetail from '../components/intelligent_eval/EvalDetail'
import EvalReport from '../components/intelligent_eval/EvalReport'
import TaskQueueMonitor from '../components/intelligent_eval/TaskQueueMonitor'
import { EVAL_STATUS } from '../components/intelligent_eval/status'
import { useResource } from '../hooks/useResource'
import {
intelligentEvalsApi, targetsApi,
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
} from '../api'
import { useIntelligentEvalNav, type EvalDetailTab } from '../stores/intelligentEvalNav'
import { useTabStore, type TabItem } from '../stores/tabStore'
import { colors } from '../tokens'
import { formatDateTime } from '../utils/date'
import { useIntelligentEvalRead } from '../read/useIntelligentEvalRead'
type DrawerView = 'detail' | 'report'
interface CreateFormValues {
name: string
target_id: string
@ -33,17 +31,24 @@ interface CreateFormValues {
time_window_hours: number
}
const DETAIL_TAB: TabItem = {
key: '/intelligent-evals/detail', title: '评估详情', icon: <EyeOutlined />, closable: true,
}
const TASKS_TAB: TabItem = {
key: '/intelligent-evals/tasks', title: '任务队列', icon: <UnorderedListOutlined />, closable: true,
}
export default function IntelligentEvalsPage() {
const [drawerView, setDrawerView] = useState<DrawerView>('detail')
const [selectedId, setSelectedId] = useState<string | null>(null)
const navigate = useNavigate()
const openTab = useTabStore((s) => s.openTab)
const openDetailNav = useIntelligentEvalNav((s) => s.openDetail)
const [createOpen, setCreateOpen] = useState(false)
const [taskQueueOpen, setTaskQueueOpen] = useState(false)
const [page, setPage] = useState(1)
const [pageSize, setPageSize] = useState(20)
const [submitting, setSubmitting] = useState(false)
const [form] = Form.useForm<CreateFormValues>()
const { list, detail, reloadList, reloadDetail } = useIntelligentEvalRead(selectedId, undefined, page, pageSize)
const { list, reloadList } = useIntelligentEvalRead(null, undefined, page, pageSize)
const evals = list.value
const loading = list.phase === 'loading'
const { data: targets } = useResource(
@ -51,25 +56,20 @@ export default function IntelligentEvalsPage() {
{ tabPath: '/intelligent-evals' },
)
const selected = selectedId != null && detail.value?.id === selectedId ? detail.value : null
const targetName = (id: string) =>
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
const openDetail = (id: string) => {
setSelectedId(id)
setDrawerView('detail')
const openDetail = (id: string, tab: EvalDetailTab = 'overview') => {
openDetailNav(id, tab)
openTab(DETAIL_TAB)
navigate('/intelligent-evals/detail')
}
const openReport = (id: string) => {
setSelectedId(id)
setDrawerView('report')
}
const openReport = (id: string) => openDetail(id, 'report')
const closeDrawer = () => {
setSelectedId(null)
setDrawerView('detail')
void reloadList()
const openTasks = () => {
openTab(TASKS_TAB)
navigate('/intelligent-evals/tasks')
}
const submitCreate = async () => {
@ -160,7 +160,7 @@ export default function IntelligentEvalsPage() {
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={() => void reloadList()} />
<Button icon={<UnorderedListOutlined />} onClick={() => setTaskQueueOpen(true)}>
<Button icon={<UnorderedListOutlined />} onClick={openTasks}>
</Button>
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
@ -191,38 +191,6 @@ export default function IntelligentEvalsPage() {
/>
</div>
<Drawer
title={drawerView === 'report' ? '评估报告' : '评估详情'}
open={selectedId != null}
onClose={closeDrawer}
width={1040}
destroyOnClose
styles={{ body: { padding: 0 } }}
>
{selected == null ? (
<div style={{ padding: 48, textAlign: 'center' }}><Spin /></div>
) : drawerView === 'report' ? (
<EvalReport ev={selected} onBack={() => setDrawerView('detail')} />
) : (
<EvalDetail
ev={selected}
targetName={targetName(selected.target_id)}
onOpenReport={() => setDrawerView('report')}
onChanged={() => void reloadDetail()}
/>
)}
</Drawer>
<Drawer
title="任务队列(定时触发监控)"
open={taskQueueOpen}
onClose={() => setTaskQueueOpen(false)}
width={1000}
destroyOnClose
>
<TaskQueueMonitor />
</Drawer>
<FormDrawer
title="新建智能评估"
open={createOpen}

View File

@ -0,0 +1,22 @@
import { create } from 'zustand'
export type EvalDetailTab = 'overview' | 'decision' | 'history' | 'report'
interface IntelligentEvalNav {
/** 当前详情页展示的评估 id由评估列表页进入时设置。 */
selectedEvalId: string | null
/** 详情页初始激活的子视图 tab。 */
detailTab: EvalDetailTab
openDetail: (id: string, tab?: EvalDetailTab) => void
}
/**
* keep-alive
* tab/intelligent-evals/detail
* openDetail openTab + navigate
*/
export const useIntelligentEvalNav = create<IntelligentEvalNav>((set) => ({
selectedEvalId: null,
detailTab: 'overview',
openDetail: (id, tab = 'overview') => set({ selectedEvalId: id, detailTab: tab }),
}))