diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index f2f8bc0..19de612 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -715,6 +715,34 @@ export interface CreateIntelligentEvalPayload { time_window_hours: number } +export interface ConfigSnapshot { + id: string + eval_id: string + snapshot_type: 'created' | 'plan_submitted' | 'config_updated' + goal: string + seeds: Record + intent: string + role_description: string + time_window_hours: number + plan: IntelligentEvalPlan | null + created_at: string | null + created_by: string +} + +export interface ConfigSnapshotComparison { + snapshot_1: { + id: string + snapshot_type: string + created_at: string | null + } + snapshot_2: { + id: string + snapshot_type: string + created_at: string | null + } + differences: Record +} + export const intelligentEvalsApi = { list: () => api.get<{ intelligent_evals: IntelligentEval[] }>('/intelligent-evals'), get: (id: string) => api.get(`/intelligent-evals/${id}`), @@ -739,6 +767,16 @@ export const intelligentEvalsApi = { a.click() URL.revokeObjectURL(url) }, + // Config Snapshots + listConfigSnapshots: (id: string) => + api.get<{ snapshots: ConfigSnapshot[] }>(`/intelligent-evals/${id}/config-snapshots`), + getConfigSnapshot: (id: string, snapshotId: string) => + api.get(`/intelligent-evals/${id}/config-snapshots/${snapshotId}`), + compareConfigSnapshots: (id: string, snapshotId1: string, snapshotId2: string) => + api.post(`/intelligent-evals/${id}/config-snapshots/compare`, { + snapshot_id_1: snapshotId1, + snapshot_id_2: snapshotId2, + }), } // ── File Management ────────────────────────────────────────────── diff --git a/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx new file mode 100644 index 0000000..3adaf7c --- /dev/null +++ b/frontend/web/src/components/intelligent_eval/ConfigSnapshots.tsx @@ -0,0 +1,266 @@ +import { useState } from 'react' +import { + Button, Card, Descriptions, Empty, Space, Table, Tag, message, +} from 'antd' +import type { ColumnsType } from 'antd/es/table' +import { ArrowLeftOutlined, DiffOutlined } from '@ant-design/icons' +import { intelligentEvalsApi, type ConfigSnapshot, type ConfigSnapshotComparison } from '../../api' +import { colors } from '../../tokens' +import { formatDateTime } from '../../utils/date' + +const SNAPSHOT_TYPE_LABELS: Record = { + created: { label: '创建', color: 'green' }, + plan_submitted: { label: '计划提交', color: 'blue' }, + config_updated: { label: '配置更新', color: 'orange' }, +} + +interface ConfigSnapshotsProps { + evalId: string + onBack: () => void +} + +export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps) { + const [snapshots, setSnapshots] = useState(null) + const [loading, setLoading] = useState(false) + const [selectedSnapshot, setSelectedSnapshot] = useState(null) + const [compareMode, setCompareMode] = useState(false) + const [selectedForCompare, setSelectedForCompare] = useState([]) + const [comparison, setComparison] = useState(null) + + const loadSnapshots = async () => { + setLoading(true) + try { + const res = await intelligentEvalsApi.listConfigSnapshots(evalId) + setSnapshots(res.data.snapshots) + } catch { + message.error('加载配置快照失败') + } finally { + setLoading(false) + } + } + + useState(() => { + void loadSnapshots() + }) + + const handleViewDetail = async (snapshot: ConfigSnapshot) => { + try { + const res = await intelligentEvalsApi.getConfigSnapshot(evalId, snapshot.id) + setSelectedSnapshot(res.data) + } catch { + message.error('加载快照详情失败') + } + } + + const handleCompare = async () => { + if (selectedForCompare.length !== 2) { + message.warning('请选择两个快照进行对比') + return + } + + try { + const res = await intelligentEvalsApi.compareConfigSnapshots( + evalId, + selectedForCompare[0], + selectedForCompare[1], + ) + setComparison(res.data) + setCompareMode(true) + } catch { + message.error('对比快照失败') + } + } + + const handleExport = (snapshot: ConfigSnapshot) => { + const data = JSON.stringify(snapshot, 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 = `config-snapshot-${snapshot.id.slice(0, 8)}.json` + a.click() + URL.revokeObjectURL(url) + message.success('已导出快照') + } + + const columns: ColumnsType = [ + { + title: '时间', + dataIndex: 'created_at', + key: 'created_at', + render: (val: string | null) => formatDateTime(val), + }, + { + title: '类型', + dataIndex: 'snapshot_type', + key: 'snapshot_type', + render: (val: string) => { + const info = SNAPSHOT_TYPE_LABELS[val] ?? { label: val, color: 'default' } + return {info.label} + }, + }, + { + title: '创建者', + dataIndex: 'created_by', + key: 'created_by', + }, + { + title: '操作', + key: 'actions', + render: (_, record) => ( + + + + + ), + }, + ] + + const rowSelection = { + selectedRowKeys: selectedForCompare, + onChange: (keys: React.Key[]) => { + if (keys.length > 2) { + message.warning('最多选择两个快照进行对比') + return + } + setSelectedForCompare(keys as string[]) + }, + } + + if (selectedSnapshot) { + return ( +
+
+ + 快照详情 + + {SNAPSHOT_TYPE_LABELS[selectedSnapshot.snapshot_type]?.label ?? selectedSnapshot.snapshot_type} + +
+ + + + {selectedSnapshot.goal || '—'} + {selectedSnapshot.intent || '—'} + + {selectedSnapshot.role_description || '—'} + + {selectedSnapshot.time_window_hours} 小时 + + {Object.keys(selectedSnapshot.seeds ?? {}).length === 0 + ? '—' + : ( +
+                    {JSON.stringify(selectedSnapshot.seeds, null, 2)}
+                  
+ )} +
+
+
+ + {selectedSnapshot.plan && ( + +
+              {JSON.stringify(selectedSnapshot.plan, null, 2)}
+            
+
+ )} +
+ ) + } + + if (compareMode && comparison) { + return ( +
+
+ + 快照对比 +
+ + + + + {comparison.snapshot_1.snapshot_type} + {formatDateTime(comparison.snapshot_1.created_at)} + + + {comparison.snapshot_2.snapshot_type} + {formatDateTime(comparison.snapshot_2.created_at)} + + + + + + {Object.keys(comparison.differences).length === 0 ? ( + + ) : ( +
+ {Object.entries(comparison.differences).map(([field, diff]) => ( +
+
{field}
+
+
+
旧值
+
+                        {typeof diff.old === 'object' ? JSON.stringify(diff.old, null, 2) : String(diff.old ?? '—')}
+                      
+
+
+
新值
+
+                        {typeof diff.new === 'object' ? JSON.stringify(diff.new, null, 2) : String(diff.new ?? '—')}
+                      
+
+
+
+ ))} +
+ )} +
+
+ ) + } + + return ( +
+
+ + 配置历史 +
+ +
+ + }} + /> + + ) +} diff --git a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx index d929fd1..7731715 100644 --- a/frontend/web/src/components/intelligent_eval/EvalDetail.tsx +++ b/frontend/web/src/components/intelligent_eval/EvalDetail.tsx @@ -2,11 +2,12 @@ import { useState } from 'react' import { Alert, Button, Card, Col, Descriptions, Empty, Input, Modal, Popconfirm, Progress, Row, Space, Spin, Tag, message, } from 'antd' -import { FileTextOutlined, StopOutlined } from '@ant-design/icons' +import { FileTextOutlined, HistoryOutlined, 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' const sectionCard: React.CSSProperties = { marginBottom: 16 } @@ -70,6 +71,7 @@ export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: const [busy, setBusy] = useState(false) const [rejectOpen, setRejectOpen] = useState(false) const [feedback, setFeedback] = useState('') + const [showConfigHistory, setShowConfigHistory] = 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 ?? [] : [] @@ -93,6 +95,10 @@ export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: setFeedback('') }, '已打回,等待重新规划') + if (showConfigHistory) { + return setShowConfigHistory(false)} /> + } + return (
@@ -100,6 +106,7 @@ export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: {meta.label}
+ {ev.status === 'completed' && ( )}