-
} onClick={onBack}>返回详情
+ {onBack &&
} onClick={onBack}>返回详情}
{ev.name} · 评估报告
diff --git a/frontend/web/src/index.css b/frontend/web/src/index.css
index 24747d7..e4e2aac 100644
--- a/frontend/web/src/index.css
+++ b/frontend/web/src/index.css
@@ -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)
*/
diff --git a/frontend/web/src/pages/IntelligentEvalDetail.tsx b/frontend/web/src/pages/IntelligentEvalDetail.tsx
new file mode 100644
index 0000000..70e1b96
--- /dev/null
+++ b/frontend/web/src/pages/IntelligentEvalDetail.tsx
@@ -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
(null)
+ const [busy, setBusy] = useState(false)
+ const [rejectOpen, setRejectOpen] = useState(false)
+ const [feedback, setFeedback] = useState('')
+ const [activeTab, setActiveTab] = useState(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, 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 (
+
+
+
+
+
+ )
+ }
+
+ if (!detail) {
+ return (
+
+
+
+
+
+ )
+ }
+
+ 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: (
+
+
+
+ ),
+ },
+ {
+ key: 'decision',
+ label: '决策过程',
+ children: ,
+ },
+ {
+ key: 'history',
+ label: '配置历史',
+ children: ,
+ },
+ ]
+ if (detail.status === 'completed') {
+ items.push({
+ key: 'report',
+ label: '评估报告',
+ children: ,
+ })
+ }
+
+ return (
+
+
+
+
{detail.name}
+
{meta.label}
+
+
+ {canApprove && (
+ <>
+
+
+ >
+ )}
+ {canCancel && (
+
+ } loading={busy}>取消
+
+ )}
+
+
+
+
+ setActiveTab(k as EvalDetailTab)}
+ items={items}
+ />
+
+
+
+ setRejectOpen(false)}
+ onOk={submitReject}
+ okText="打回"
+ okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
+ >
+ setFeedback(e.target.value)}
+ placeholder="请填写打回原因与改进方向,OpenClaw 会据此重新规划"
+ />
+
+
+ )
+}
diff --git a/frontend/web/src/pages/IntelligentEvalTasks.tsx b/frontend/web/src/pages/IntelligentEvalTasks.tsx
new file mode 100644
index 0000000..d59e48d
--- /dev/null
+++ b/frontend/web/src/pages/IntelligentEvalTasks.tsx
@@ -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 (
+
+
+
+
+
+ )
+}
diff --git a/frontend/web/src/pages/IntelligentEvals.tsx b/frontend/web/src/pages/IntelligentEvals.tsx
index 3477706..43e4a9d 100644
--- a/frontend/web/src/pages/IntelligentEvals.tsx
+++ b/frontend/web/src/pages/IntelligentEvals.tsx
@@ -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: , closable: true,
+}
+const TASKS_TAB: TabItem = {
+ key: '/intelligent-evals/tasks', title: '任务队列', icon: , closable: true,
+}
+
export default function IntelligentEvalsPage() {
- const [drawerView, setDrawerView] = useState('detail')
- const [selectedId, setSelectedId] = useState(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()
- 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={
} onClick={() => void reloadList()} />
- } onClick={() => setTaskQueueOpen(true)}>
+ } onClick={openTasks}>
任务队列
} onClick={() => setCreateOpen(true)}>
@@ -191,38 +191,6 @@ export default function IntelligentEvalsPage() {
/>