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
185 lines
6.1 KiB
TypeScript
185 lines
6.1 KiB
TypeScript
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>
|
||
)
|
||
}
|