From 00ad929d6892bfa7270110397f66b0e79313cd2d Mon Sep 17 00:00:00 2001 From: sinohqb Date: Mon, 17 Aug 2026 20:03:55 +0800 Subject: [PATCH] =?UTF-8?q?feat(intelligent-eval):=20v4.2=20UI=20=E2=80=94?= =?UTF-8?q?=20stat=20bar=20+=20single-line=20table=20+=20unified=20detail?= =?UTF-8?q?=20drawer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评估列表页(v4.2): - 顶部状态统计条(stat-chip 点击筛选,后端列表接口新增 stats 状态分布 + status 筛选参数) - 单行表格(名称/评测对象 ellipsis 省略)+ 会话进度迷你进度条 + 最后一列醒目查看按钮 + 整行可点击 - 详情/报告合一单层抽屉(1000px,Tabs:概览/决策过程/配置历史/报告), 审批/取消在抽屉头部;移除独立详情页 /intelligent-evals/detail 与 intelligentEvalNav store 任务队列页: - 与列表页统一 stat-chip 统计条 + 单行表格(评估状态小标、失败原因 tooltip) 清理:删除 IntelligentEvalDetail.tsx / intelligentEvalNav.ts;App.tsx 移除 detail 路由。任务队列保持独立菜单页。tsc 0 错误 vitest 19 passed 895 passed --- .../agenteval/intelligent_eval/lifecycle.py | 15 +- .../agenteval/intelligent_eval/repository.py | 28 ++- .../web/routers/intelligent_evals.py | 9 +- frontend/web/src/App.tsx | 2 - frontend/web/src/api.ts | 10 +- .../intelligent_eval/EvalOverview.tsx | 2 +- .../intelligent_eval/TaskQueueMonitor.tsx | 155 +++++++------ frontend/web/src/index.css | 33 +++ .../web/src/pages/IntelligentEvalDetail.tsx | 193 ---------------- frontend/web/src/pages/IntelligentEvals.tsx | 206 +++++++++++++----- frontend/web/src/read/intelligentEval.test.ts | 7 +- frontend/web/src/read/intelligentEval.ts | 26 ++- .../src/read/useIntelligentEvalRead.test.tsx | 14 +- .../web/src/read/useIntelligentEvalRead.ts | 7 +- frontend/web/src/stores/intelligentEvalNav.ts | 22 -- 15 files changed, 342 insertions(+), 387 deletions(-) delete mode 100644 frontend/web/src/pages/IntelligentEvalDetail.tsx delete mode 100644 frontend/web/src/stores/intelligentEvalNav.ts diff --git a/backend/agenteval/intelligent_eval/lifecycle.py b/backend/agenteval/intelligent_eval/lifecycle.py index 056036d..ee8db62 100644 --- a/backend/agenteval/intelligent_eval/lifecycle.py +++ b/backend/agenteval/intelligent_eval/lifecycle.py @@ -10,7 +10,7 @@ 非法转换抛 IntelligentEvalTransitionError,路由层映射为 409。 """ -from typing import Any +from typing import Any, Optional from sqlmodel import Session @@ -218,10 +218,17 @@ def list_evals(session: Session) -> list[IntelligentEval]: return IntelligentEvalRepository(session).list_all() -def list_evals_page(session: Session, offset: int, limit: int) -> tuple[list[IntelligentEval], int]: - """Return one page of evals (newest first) plus the total count.""" +def list_evals_page( + session: Session, offset: int, limit: int, status: Optional[str] = None +) -> tuple[list[IntelligentEval], int, dict[str, int]]: + """Return one page of evals (newest first), total count, and per-status counts.""" repo = IntelligentEvalRepository(session) - return repo.list_page(offset, limit), repo.count() + return repo.list_page(offset, limit, status), repo.count(), repo.count_by_status() + + +def eval_status_counts(session: Session) -> dict[str, int]: + """Count evaluations per status (for the list page stat bar).""" + return IntelligentEvalRepository(session).count_by_status() def _get_session_or_raise(repo: IntelligentEvalSessionRepository, session_id: str) -> IntelligentEvalSession: diff --git a/backend/agenteval/intelligent_eval/repository.py b/backend/agenteval/intelligent_eval/repository.py index 69ff4ce..7b28e71 100644 --- a/backend/agenteval/intelligent_eval/repository.py +++ b/backend/agenteval/intelligent_eval/repository.py @@ -101,20 +101,32 @@ class IntelligentEvalRepository: statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc()) return [self._from_db(r) for r in self.session.exec(statement).all()] - def list_page(self, offset: int, limit: int) -> list[IntelligentEval]: - """Return one page of evals (created_at desc) with the same ordering as ``list_all``.""" - statement = ( - select(IntelligentEvalDB) - .order_by(IntelligentEvalDB.created_at.desc()) - .offset(offset) - .limit(limit) - ) + def list_page( + self, offset: int, limit: int, status: Optional[str] = None + ) -> list[IntelligentEval]: + """Return one page of evals (created_at desc), optionally filtered by status.""" + statement = select(IntelligentEvalDB).order_by(IntelligentEvalDB.created_at.desc()) + if status: + statement = statement.where(IntelligentEvalDB.status == status) + statement = statement.offset(offset).limit(limit) return [self._from_db(r) for r in self.session.exec(statement).all()] def count(self) -> int: """Total number of evaluations (for pagination metadata).""" return self.session.exec(select(func.count()).select_from(IntelligentEvalDB)).one() + def count_by_status(self) -> dict[str, int]: + """Count evaluations per status (for the list page stat bar).""" + rows = self.session.exec( + select(IntelligentEvalDB.status, func.count(IntelligentEvalDB.id)).group_by( + IntelligentEvalDB.status + ) + ).all() + stats = {status.value: 0 for status in IntelligentEvalStatus} + for status, cnt in rows: + stats[status] = cnt + return stats + def get(self, eval_id: str) -> Optional[IntelligentEval]: db = self.session.get(IntelligentEvalDB, eval_id) return self._from_db(db) if db else None diff --git a/backend/agenteval/web/routers/intelligent_evals.py b/backend/agenteval/web/routers/intelligent_evals.py index 5f1ac14..180138f 100644 --- a/backend/agenteval/web/routers/intelligent_evals.py +++ b/backend/agenteval/web/routers/intelligent_evals.py @@ -97,12 +97,14 @@ async def create_eval(request: CreateEvalRequest, session: Session = Depends(get async def list_evals( page: int | None = None, page_size: int = 20, + status: str | None = None, session: Session = Depends(get_db), ) -> dict: - """List intelligent evaluations, optionally paginated. + """List intelligent evaluations, optionally paginated and filtered by status. 不传 ``page`` 时返回全部(向后兼容);传 ``page``(从 1 起)时按 - ``created_at`` 倒序分页,返回 ``total`` 供前端服务端分页。 + ``created_at`` 倒序分页,返回 ``total`` + ``stats``(各状态计数)供前端 + 服务端分页与状态统计条。 """ reader = IntelligentEvalReadModel(session) if page is None: @@ -110,10 +112,11 @@ async def list_evals( return {"intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)]} page_size = max(1, min(page_size, 100)) offset = (max(1, page) - 1) * page_size - evals, total = lifecycle.list_evals_page(session, offset, page_size) + evals, total, stats = lifecycle.list_evals_page(session, offset, page_size, status) return { "intelligent_evals": [item.model_dump(mode="json") for item in reader.list_items(evals)], "total": total, + "stats": stats, "page": page, "page_size": page_size, } diff --git a/frontend/web/src/App.tsx b/frontend/web/src/App.tsx index b86210a..9ccb85c 100644 --- a/frontend/web/src/App.tsx +++ b/frontend/web/src/App.tsx @@ -40,7 +40,6 @@ 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 ( @@ -71,7 +70,6 @@ const routeConfigs: RouteConfig[] = [ { path: '/reports', name: '评测报告', icon: , component: () => }, { path: '/intelligent-evals', name: '智能评估', icon: , component: () => }, { path: '/intelligent-evals/tasks', name: '任务队列', icon: , component: () => }, - { path: '/intelligent-evals/detail', name: '评估详情', icon: , component: () => }, { path: '/models', name: '模型配置', icon: , component: () => }, { path: '/files', name: '原始文件', icon: , component: () => }, ] diff --git a/frontend/web/src/api.ts b/frontend/web/src/api.ts index c4362e0..56b61b0 100644 --- a/frontend/web/src/api.ts +++ b/frontend/web/src/api.ts @@ -785,8 +785,14 @@ export interface TaskQueueList { } export const intelligentEvalsApi = { - list: (params?: { page?: number; page_size?: number }) => - api.get<{ intelligent_evals: IntelligentEval[]; total?: number; page?: number; page_size?: number }>( + list: (params?: { page?: number; page_size?: number; status?: string }) => + api.get<{ + intelligent_evals: IntelligentEval[] + total?: number + stats?: Record + page?: number + page_size?: number + }>( '/intelligent-evals', { params }, ), diff --git a/frontend/web/src/components/intelligent_eval/EvalOverview.tsx b/frontend/web/src/components/intelligent_eval/EvalOverview.tsx index ac6ffe0..1c46be7 100644 --- a/frontend/web/src/components/intelligent_eval/EvalOverview.tsx +++ b/frontend/web/src/components/intelligent_eval/EvalOverview.tsx @@ -64,7 +64,7 @@ interface EvalOverviewProps { /** * 评估详情"概览"子视图(纯展示):基本信息、用户输入、粗计划、会话进度。 - * 审批/取消等动作由详情页(IntelligentEvalDetail)统一管理。 + * 审批/取消等动作由评估列表页的详情抽屉头部统一管理。 */ export default function EvalOverview({ ev, targetName }: EvalOverviewProps) { const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' } diff --git a/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx b/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx index 3ca373d..9655dd4 100644 --- a/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx +++ b/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx @@ -1,44 +1,49 @@ import { useEffect, useState } from 'react' import { - Alert, Button, Empty, Select, Space, Statistic, Table, Tag, Tooltip, message, + Button, Empty, Space, Table, Tag, Tooltip, message, } from 'antd' import type { ColumnsType } from 'antd/es/table' import { ReloadOutlined } from '@ant-design/icons' import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api' import { usePolling } from '../../hooks/usePolling' -import { colors, statusColors } from '../../tokens' +import { colors } from '../../tokens' import { formatDateTime } from '../../utils/date' const STATUS_META: Record = { - pending: { label: '待处理', color: 'orange' }, + pending: { label: '待认领', color: 'orange' }, assigned: { label: '执行中', color: 'blue' }, completed: { label: '已完成', color: 'green' }, failed: { label: '失败', color: 'red' }, } -const STATUS_OPTIONS = (Object.keys(STATUS_META) as TaskQueueStatus[]).map((s) => ({ - value: s, - label: STATUS_META[s].label, -})) +const EVAL_STATUS_META: Record = { + executing: { label: '执行中', color: 'blue' }, + completed: { label: '已完成', color: 'green' }, + cancelled: { label: '已取消', color: 'default' }, + failed: { label: '失败', color: 'red' }, +} + +type FilterKey = 'all' | 'unresolved' | TaskQueueStatus /** - * 任务队列监控(方案③可视化)。 + * 任务队列监控(方案③可视化,与评估列表页统一 stat-chip + 单行表格)。 * - * 方案③的"定时触发"(scan loop 每 60s 扫描入队 + 触发 OpenClaw worker) - * 此前只有 Worker 消费端 API,无可查看的列表。这里展示任务队列明细与 - * 状态分布,5s 轮询,让平台侧的定时触发对用户可见。 + * 平台每 60s 扫描 executing 评估入队 + 触发 OpenClaw worker 执行。本组件 + * 展示任务明细与状态分布,5s 轮询;统计条点击筛选。 */ export default function TaskQueueMonitor() { const [tasks, setTasks] = useState(null) - const [stats, setStats] = useState<{ pending: number; assigned: number; completed: number; failed: number; unresolved: number } | null>(null) - const [statusFilter, setStatusFilter] = useState('all') + const [stats, setStats] = useState<{ + pending: number; assigned: number; completed: number; failed: number; unresolved: number + } | null>(null) + const [filter, setFilter] = useState('all') const [loading, setLoading] = useState(false) const loadData = async () => { setLoading(true) try { const res = await intelligentEvalsApi.listTasks( - statusFilter === 'all' ? undefined : { status: statusFilter }, + filter === 'all' || filter === 'unresolved' ? undefined : { status: filter }, ) setTasks(res.data.tasks) setStats(res.data.stats) @@ -49,111 +54,97 @@ export default function TaskQueueMonitor() { } } - // Initial fetch (usePolling owns the 5s interval + visibility pause). + // Initial fetch + reload on filter change (usePolling owns the 5s interval). useEffect(() => { void loadData() // eslint-disable-next-line react-hooks/exhaustive-deps - }, [statusFilter]) + }, [filter]) usePolling(() => { void loadData() }, 5000, true) + const chipMeta: { key: FilterKey; label: string; color?: string; get: () => number }[] = [ + { key: 'all', label: '全部任务', get: () => (stats ? Object.values(stats).reduce((a, b) => a + b, 0) : (tasks?.length ?? 0)) }, + { key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 }, + { key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 }, + { key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 }, + { key: 'completed', label: '已完成', color: '#52c41a', get: () => stats?.completed ?? 0 }, + { key: 'failed', label: '失败', color: '#ff4d4f', get: () => stats?.failed ?? 0 }, + ] + const columns: ColumnsType = [ { - title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170, + title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 165, render: (v: string | null) => (v ? formatDateTime(v) : '—'), }, { - title: '评估', dataIndex: 'eval_name', key: 'eval_name', - render: (name: string | null, t) => ( - - {name ?? t.eval_id.slice(0, 8)} - {t.eval_status && ( - {t.eval_status} + title: '评估', key: 'eval', width: 230, ellipsis: true, + render: (_, t) => ( + + {t.eval_name ?? t.eval_id.slice(0, 8)} + {t.eval_status && EVAL_STATUS_META[t.eval_status] && ( + + {EVAL_STATUS_META[t.eval_status].label} + )} ), }, { - title: '状态', dataIndex: 'status', key: 'status', width: 100, + title: '队列状态', dataIndex: 'status', key: 'status', width: 96, render: (s: TaskQueueStatus) => { const meta = STATUS_META[s] return {meta.label} }, }, { - title: '优先级', dataIndex: 'priority', key: 'priority', width: 80, + title: '优先级', dataIndex: 'priority', key: 'priority', width: 76, render: (p: number) => {p}, }, { - title: '原因', dataIndex: 'reason', key: 'reason', - render: (r: string) => ( - - - {r} - - - ), + title: '原因', dataIndex: 'reason', key: 'reason', ellipsis: true, + render: (r: string) => {r}, }, { - title: '认领方', dataIndex: 'assigned_cron_id', key: 'assigned_cron_id', width: 140, - render: (v: string | null, t) => { - if (!v) return - return ( - - {v} - - ) - }, - }, - { - title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 170, + title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 165, render: (v: string | null, t) => { if (t.status === 'failed' && t.error) { - return 失败 + return 失败 } - return v ? formatDateTime(v) : '—' + return v ? formatDateTime(v) : }, }, ] return ( -
-
- - - - - - - - -