AgentEvalTool/frontend/web/src/components/intelligent_eval/DecisionProcess.tsx
sinohqb a69fa8a797
All checks were successful
CI / test (push) Successful in 4m7s
refactor(intelligent-eval): split list page into dedicated pages
评估列表页此前用 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
2026-08-17 16:39:53 +08:00

185 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import {
Button, Card, Empty, Select, Table, Tag, Timeline, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type DecisionLog } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime } from '../../utils/date'
const DECISION_TYPE_LABELS: Record<string, { label: string; color: string }> = {
execute_session: { label: '执行会话', color: 'blue' },
wait: { label: '等待', color: 'default' },
start_analysis: { label: '开始分析', color: 'green' },
}
interface DecisionProcessProps {
evalId: string
/** 独立页内作为子视图 tab 使用时可不传tab 切换代替返回)。 */
onBack?: () => void
}
export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps) {
const [logs, setLogs] = useState<DecisionLog[] | null>(null)
const [loading, setLoading] = useState(false)
const [filterType, setFilterType] = useState<string | null>(null)
const [expandedLog, setExpandedLog] = useState<string | null>(null)
const loadLogs = async () => {
setLoading(true)
try {
const res = await intelligentEvalsApi.listDecisionLogs(evalId)
setLogs(res.data.logs)
} catch {
message.error('加载决策日志失败')
} finally {
setLoading(false)
}
}
useState(() => {
void loadLogs()
})
const handleExport = () => {
if (!logs) return
const data = JSON.stringify(logs, null, 2)
const blob = new Blob([data], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `decision-logs-${evalId.slice(0, 8)}.json`
a.click()
URL.revokeObjectURL(url)
message.success('已导出决策日志')
}
const filteredLogs = filterType
? logs?.filter((log) => log.decision_type === filterType) ?? []
: logs ?? []
const columns: ColumnsType<DecisionLog> = [
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180,
render: (val: string | null) => formatDateTime(val),
},
{
title: '决策类型',
dataIndex: 'decision_type',
key: 'decision_type',
width: 120,
render: (val: string) => {
const info = DECISION_TYPE_LABELS[val] ?? { label: val, color: 'default' }
return <Tag color={info.color}>{info.label}</Tag>
},
},
{
title: '原因',
dataIndex: 'reason',
key: 'reason',
ellipsis: true,
},
{
title: 'Cron ID',
dataIndex: 'cron_id',
key: 'cron_id',
width: 120,
render: (val: string) => <code style={{ fontSize: 11 }}>{val.slice(0, 8)}</code>,
},
{
title: '操作',
key: 'actions',
width: 80,
render: (_, record) => (
<Button size="small" onClick={() => setExpandedLog(expandedLog === record.id ? null : record.id)}>
{expandedLog === record.id ? '收起' : '详情'}
</Button>
),
},
]
return (
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>}
<span style={{ fontSize: 16, fontWeight: 600 }}></span>
<div style={{ flex: 1 }} />
<Select
placeholder="筛选决策类型"
allowClear
style={{ width: 160 }}
onChange={(val) => setFilterType(val ?? null)}
options={[
{ label: '执行会话', value: 'execute_session' },
{ label: '等待', value: 'wait' },
{ label: '开始分析', value: 'start_analysis' },
]}
/>
<Button icon={<DownloadOutlined />} onClick={handleExport}></Button>
</div>
<Card size="small" title="决策时间线" style={{ marginBottom: 16 }}>
{loading ? (
<Empty description="加载中..." />
) : filteredLogs.length === 0 ? (
<Empty description="暂无决策日志" />
) : (
<Timeline
items={filteredLogs.map((log) => ({
color: log.decision_type === 'execute_session' ? 'blue' : log.decision_type === 'start_analysis' ? 'green' : 'gray',
children: (
<div>
<div style={{ marginBottom: 4 }}>
<Tag color={DECISION_TYPE_LABELS[log.decision_type]?.color ?? 'default'}>
{DECISION_TYPE_LABELS[log.decision_type]?.label ?? log.decision_type}
</Tag>
<span style={{ fontSize: 12, color: colors.textSecondary }}>
{formatDateTime(log.created_at)}
</span>
</div>
<div style={{ fontSize: 13 }}>{log.reason}</div>
<div style={{ fontSize: 11, color: colors.textSecondary, marginTop: 4 }}>
Cron: <code>{log.cron_id.slice(0, 8)}</code>
</div>
</div>
),
}))}
/>
)}
</Card>
<Card size="small" title="决策日志列表">
<Table
rowKey="id"
loading={loading}
dataSource={filteredLogs}
columns={columns}
pagination={false}
expandable={{
expandedRowKeys: expandedLog ? [expandedLog] : [],
expandIcon: () => null,
expandedRowRender: (record) => (
<div style={{ padding: '8px 0' }}>
<div style={{ fontWeight: 500, marginBottom: 8 }}></div>
<pre style={{
margin: 0, fontSize: 12, background: colors.bgSubtle,
padding: 12, borderRadius: 6, overflowX: 'auto',
}}
>
{JSON.stringify(record.context, null, 2)}
</pre>
</div>
),
}}
locale={{ emptyText: <Empty description="暂无决策日志" /> }}
/>
</Card>
</div>
)
}