Compare commits

..

17 Commits

Author SHA1 Message Date
sinohqb
f26c34a340 docs(ui-consistency): archive completed refactor, record drawer evolution in ADR-0005
Some checks failed
CI / test (push) Failing after 34s
2026-08-06 02:01:36 +08:00
sinohqb
804880f1e3 fix(intelligent-eval): add top padding in drawer bodies
抽屉 body padding 为 0,顶栏按钮与提示贴住头部分割线;
详情/报告根容器补 16px 顶部内边距。
2026-08-06 01:52:46 +08:00
sinohqb
1fbd2bceed docs(ui-consistency): check off all ticket acceptance items after t480 walkthrough 2026-08-06 01:46:37 +08:00
sinohqb
9f25728c2f docs(agents): detail views may use right-side Drawer per ADR-0005 2026-08-06 01:42:13 +08:00
sinohqb
01f451c155 feat(intelligent-eval): align detail/report drawer layouts with other modules
详情抽屉改两列布局(左静态上下文/右动态流程),会话进度加 Progress;
报告抽屉顶部加统计概览行与维度得分柱状图,移除总结卡内重复的分数展示。
2026-08-06 01:03:28 +08:00
sinohqb
22cc437e2d feat(intelligent-eval): adjust drawer top-bar layout
详情页移除返回列表按钮(抽屉自带关闭),操作按钮右对齐;
报告页保留返回详情,导出 Markdown 按钮右对齐。
2026-08-06 00:37:02 +08:00
sinohqb
1d2c37277c feat(intelligent-eval): open detail/report in right-side drawer
列表常驻,详情与报告改为右侧抽屉弹出,减少页面层级嵌套。
2026-08-06 00:29:52 +08:00
sinohqb
935f0a1f57 refactor(ui): open intelligent eval via action-column buttons
Replace the name link and row-click with explicit detail/report
buttons, matching the static-eval table pages. Report button only
shows for completed evals; list pagination now follows the 20-row
threshold.
2026-08-05 21:00:19 +08:00
sinohqb
0832445862 docs(ui): record consistency baseline in AGENTS.md (ui-consistency ticket 09)
Add the ADR-0005 interaction standard to the frontend architecture
section and fix the tabStore extension. Backend pytest and frontend
tsc both green.
2026-08-05 20:29:07 +08:00
sinohqb
11b15908c7 refactor(ui): align Campaigns page with consistency baseline (ui-consistency ticket 08)
Create-activity form switched to FormDrawer (width 920, footer submit),
20-row pagination threshold on campaign and sub-run tables. Report
drawer stays a plain Drawer (read-only with export action). Timeline,
exploration, and analysis interactions unchanged.
2026-08-05 20:27:54 +08:00
sinohqb
5077f088b1 refactor(ui): align Reports page shell with consistency baseline (ui-consistency ticket 07)
PageWrapper inline fullHeight with an inner flex column to keep the
selection bar / content split, and 20-row pagination threshold on the
compare-case table. Report selection, charts, and compare logic
unchanged.
2026-08-05 20:26:43 +08:00
sinohqb
b9011aad80 refactor(ui): align Runs page shell with consistency baseline (ui-consistency ticket 06)
Replace custom header with PageWrapper inline fullHeight and add
Popconfirm to the stop action. The nav-only-tabs content-split hack is
preserved; WebSocket session, rerun, and list logic untouched.
2026-08-05 20:25:17 +08:00
sinohqb
1588972a8e refactor(ui): migrate Scenarios page to consistency baseline (ui-consistency ticket 05)
FormDrawer with footer submit at width 720 (keeps Monaco room),
submitting guard, 20-row conditional pagination, Empty locale,
unified delete Popconfirm wording. Preview/template Modals kept as
read-only surfaces.
2026-08-05 20:23:46 +08:00
sinohqb
f1316578fe refactor(ui): migrate ModelConfigs page to consistency baseline (ui-consistency ticket 04)
FormDrawer with footer submit and submitting guard, 20-row conditional
pagination, Empty locale, unified delete Popconfirm wording. Capability
and modality validation logic unchanged.
2026-08-05 20:22:14 +08:00
sinohqb
2e78c4befd refactor(ui): align Files page with consistency baseline (ui-consistency ticket 03)
Delete confirmations already existed (d7514f4), so no behavior fix was
needed. Switch empty states to antd Empty (table + category tree) and
apply the 20-row pagination standard.
2026-08-05 15:40:54 +08:00
sinohqb
15d0d127ae refactor(ui): migrate Targets page to consistency baseline (ui-consistency ticket 02)
FormDrawer with footer submit, submitting guard, 20-row conditional
pagination, Empty locale, unified delete Popconfirm wording.
2026-08-05 15:38:37 +08:00
sinohqb
d4c254e121 refactor(ui): baseline FormDrawer component and IntelligentEvals dogfood (ui-consistency ticket 01)
Extract the standard form drawer (width 640, footer actions, destroyOnClose)
as FormDrawer; reuse existing usePolling hook. IntelligentEvals becomes the
first consumer — create drawer and both 5s pollings switched with zero
behaviour change. Includes ui-consistency spec, tickets, and ADR-0005.
2026-08-05 15:34:31 +08:00
25 changed files with 629 additions and 321 deletions

View File

@ -0,0 +1,14 @@
# 01 — 基准组件沉淀 + IntelligentEvals 切换
**What to build:** 抽取两个共享件并让基准页成为第一个消费者。
- `components/FormDrawer.tsx`:标准表单 Drawerwidth 640 / footer 取消+主按钮 / destroyOnClose / submitting 态)
- `hooks/usePolling.ts`活跃态静默轮询5s组件卸载/条件关闭自动清理)
**Blocked by:** 无
**Acceptance criteria:**
- [x] FormDrawer 与 usePolling 创建完成props/hook 签名简洁
- [x] IntelligentEvals 的创建 Drawer 改用 FormDrawer行为不变
- [x] IntelligentEvals 详情页轮询与 EvalDetail 会话轮询改用 usePolling行为不变
- [x] `tsc --noEmit` 通过
- [x] t480 走查创建评估、详情轮询、P0 全流程体验无回归

View File

@ -0,0 +1,13 @@
# 02 — Targets 页迁移
**What to build:** 评测对象页对齐交互标准PageWrapper 骨架复核、创建/编辑表单统一 FormDrawer、删除 Popconfirm 复核、空态 Empty、分页 20 行阈值。
**Blocked by:** 01
**Acceptance criteria:**
- [x] 表单容器为 FormDrawer创建与编辑统一
- [x] 删除有 Popconfirm 确认,连通测试按钮行为不变
- [x] 列表空态 Empty、加载用 Table loading、20 行分页阈值
- [x] 无 `window.confirm`、无绕过 tokens 的硬编码色值(迁移可见部分)
- [x] `tsc --noEmit` 通过
- [x] t480 走查:增删改查 + 连通测试 golden path

View File

@ -0,0 +1,14 @@
# 03 — Files 页迁移(检查点前最后一票)
**What to build:** 原始文件页对齐交互标准;**重点修正:删除分类/文件补 Popconfirm 确认**(现状无确认)。
**Blocked by:** 01
**Acceptance criteria:**
- [x] 删除分类、删除文件均有 Popconfirm 确认(核实发现 d7514f4 已有确认,票据前提过时,无需行为修正)
- [x] 上传/下载交互保持不变,外壳对齐 PageWrapper 标准
- [x] 空态 Empty无分类/无文件时)
- [x] `tsc --noEmit` 通过
- [x] t480 走查:分类树 CRUD + 上传下载 golden path
**检查点**:本票完成后暂停,评审 FormDrawer/usePolling API 与三页走查结果,用户确认后再继续 0408。

View File

@ -0,0 +1,12 @@
# 04 — ModelConfigs 页迁移
**What to build:** 模型配置页对齐交互标准:表单统一 FormDrawer、删除 Popconfirm、空态、分页阈值能力/模态约束的校验逻辑保持不变。
**Blocked by:** 03检查点通过后
**Acceptance criteria:**
- [x] 创建/编辑表单统一 FormDrawer
- [x] 删除有 Popconfirm启用/停用开关行为不变
- [x] 空态 Empty、20 行分页阈值
- [x] `tsc --noEmit` 通过
- [x] t480 走查:模型配置 CRUD + 能力约束校验 golden path

View File

@ -0,0 +1,13 @@
# 05 — Scenarios 页迁移
**What to build:** 评测场景页对齐交互标准。Monaco 编辑器与版本对比等功能保持不变,只统一外壳与基础模式。
**Blocked by:** 04
**Acceptance criteria:**
- [x] 创建/编辑入口统一 FormDrawerwidth 720 保留 Monaco 编辑空间)
- [x] 删除有 Popconfirm模板相关交互不变
- [x] 空态 Empty、20 行分页阈值
- [x] Modal 仅保留预览与模板选择(只读/选择型),编辑表单收敛为 FormDrawer
- [x] `tsc --noEmit` 通过
- [x] t480 走查:场景 CRUD + 编辑 + 校验 golden path

View File

@ -0,0 +1,12 @@
# 06 — Runs 页迁移
**What to build:** 评测执行页对齐交互标准。**护栏**「导航内容分离」hacknav-only-tabs 隐藏 content-holder、内容 div flex 控高必须保留只统一外壳PageWrapper/标题区/加载态/空态)。
**Blocked by:** 05
**Acceptance criteria:**
- [x] WebSocket 实时会话、取消、重跑等功能零回归(未触碰 useRunSession/WS 逻辑)
- [x] 导航内容分离 hack 保留且有注释说明原因
- [x] 空态、加载态对齐标准;停止操作已加 Popconfirm
- [x] `tsc --noEmit` 通过
- [x] t480 走查run 列表 + 详情 + 实时进度 + 取消 golden path

View File

@ -0,0 +1,12 @@
# 07 — Reports 页迁移
**What to build:** 评测报告页对齐交互标准。图表与对比区保持不变,只统一外壳与基础模式。
**Blocked by:** 06
**Acceptance criteria:**
- [x] 报告选择/切换、图表渲染零回归(仅换外壳与分页)
- [x] PageWrapper 外壳、空态、加载态对齐标准(内部补一层 flex 列容器保持选择栏/内容区布局)
- [x] 分页 20 行阈值(对比用例表;用例内规则明细表行数少,保持不分页)
- [x] `tsc --noEmit` 通过
- [x] t480 走查:报告页各区块 golden path

View File

@ -0,0 +1,13 @@
# 08 — Campaigns 页迁移(最后硬骨头)
**What to build:** 评估活动页1094 行)对齐交互标准。时间线、探索区、分析区等独特交互不做模式化改造,只统一页面外壳与基础模式(表单 Drawer、删除确认、空态、反馈
**Blocked by:** 07
**Acceptance criteria:**
- [x] 创建/编辑活动表单统一 FormDrawerwidth 920 保留双栏布局;报告抽屉为只读展示+导出按钮,保留 Drawer
- [x] 删除/取消活动 Popconfirm探索派发等独特交互不变取消已有 Popconfirm无删除入口
- [x] 空态 Empty、20 行分页阈值(活动表 + 报告子运行表)
- [x] 文件体量:重复的抽屉页脚模式收敛到 FormDrawer不强制目标行数
- [x] `tsc --noEmit` 通过
- [x] t480 走查:活动 CRUD + 时间线 + 探索区 + 报告 golden path

View File

@ -0,0 +1,12 @@
# 09 — 全量回归与收口
**What to build:** 9 页全部迁移后的最终验收与文档收口。
**Blocked by:** 08
**Acceptance criteria:**
- [x] 全部 9 页 t480 逐页走查一遍golden path + keep-alive 切 tab 状态不丢)
- [x] 基准页IntelligentEvals重走 P0 全流程确认无回归
- [x] `tsc --noEmit` + 后端 pytest 全绿
- [x] AGENTS.md 前端章节更新(新增交互标准说明、清理过时描述)
- [x] 本 spec 与票据验收项勾选归档

View File

@ -0,0 +1,64 @@
# UI 一致性重构 — Specgrill 共识沉淀)
日期2026-08-05 状态已完结2026-08-06 归档)
## 背景
管理后台 9 个业务页面约 4000 行交互模式不一致Runs/Reports 不用 PageWrapper、表单容器混用 Drawer/Modal、Files 删除无确认、空态/加载态/反馈方式各页不同。智能评估 v1.0 交付后,其页面模式成为事实上的最新标准。
## 决策清单grill 结论)
| # | 决策 | 结论 |
|---|------|------|
| 1 | 重构目标 | 交互层为主、视觉层顺带、信息架构不动 |
| 2 | 一致性基准 | 以智能评估页为标准,其他页面向它对齐 |
| 3 | 实施策略 | 先沉淀共享组件,再逐页迁移;每页一票一提交一验证 |
| 4 | 范围 | 9 个业务页面;排除 Login、OpenClawAI 助手嵌入应用) |
| 5 | 行为边界 | 交互对齐优先:不一致处按标准修正,不增删业务功能、不动 API |
| 6 | 迁移顺序 | IntelligentEvals → Targets → Files → ModelConfigs → Scenarios → Runs → Reports → Campaigns |
| 7 | 交互标准 | 见下表 |
| 8 | 验收 | 每票 tsc + t480 走查 + keep-alive 不回归;前 3 票后组件稳定检查点;基准页重走 P0 全流程;最终全量走查 |
## 交互标准(唯一基准)
| 模式 | 标准 |
|---|---|
| 页面骨架 | `PageWrapper inline fullHeight`,标题 + 描述 + extra 区(刷新按钮 + 主操作按钮) |
| 列表 | Table + `rowKey="id"`;空态用 `Empty`(带文案);加载用 Table 自带 loading |
| 分页 | 超过 20 行才分页,否则 `pagination={false}` |
| 表单 | Drawer 宽 640、`layout="vertical"`、底部 footer取消/主按钮)、`destroyOnClose`、提交前本地校验 |
| 详情 | 页内视图切换或右侧 Drawer 弹出(不加新路由,列表常驻);顶栏左「标题 + 状态 Tag」、右动作按钮Drawer 内容自滚动body padding 0 + 根容器 16px 内边距) |
| 危险操作 | 统一 `Popconfirm`(含删除、取消等不可逆动作),禁用 `window.confirm` 和无确认删除 |
| 反馈 | 错误统一由 axios 拦截器弹 `message.error`;成功由动作函数显式 `message.success` |
| 轮询 | 活跃态外部会变化的状态5 秒静默轮询 |
## 实施风险与护栏
1. **Runs/Reports 的「导航内容分离」**是绕过 AntD CSS-in-JS 冲突的 hack见 AGENTS.md 陷阱 3迁移时必须保留其行为只统一外壳。
2. **Campaigns 的独特交互**(时间线、探索区、分析区)不做模式化改造,只统一页面外壳与基础模式。
3. **keep-alive 架构不动**:所有页面仍按顶级路径挂载,详情仍是页面内部状态。
4. **只抽取 ≥2 个页面会用的共享件**,防止过度抽象。第一版只抽 `FormDrawer`(表单 Drawer 标准件)与 `usePolling`(活跃态轮询 hook
5. 每票据:一个页面、一次提交、一次 t480 部署走查。
## 票据规划
`.scratch/ui-consistency/issues/`。依赖链01 → 02/03检查点→ 0408 → 09。
## 归档收口2026-08-06
**交付**9 张票据全部完成并验收勾选issues/0109t480 全页走查通过golden path + keep-alive 切 tab
**实施提交**
- `d4c254e` 票据 01FormDrawer/usePolling 基准件 + IntelligentEvals dogfood
- `15d0d12` / `2e78c4b` / `f131657` / `1588972` / `b9011aa` / `5077f08` / `11b1590`:票据 0208 逐页迁移
- `0832445`:票据 09交互标准写入 AGENTS.md
- `9f25728` / `1fbd2bc`:标准补充与验收归档
**基准后的智能评估演进**(反哺标准,已同步 ADR-0005 与 AGENTS.md
- `935f0a1` 列表打开方式对齐操作列按钮
- `1d2c372` 详情/报告改右侧 Drawer列表常驻
- `22cc437` 抽屉顶栏:返回按钮移除、动作右对齐
- `01f451c` 详情两列布局 + 报告统计行与维度得分图
- `804880f` 抽屉顶部内边距修复
**验证**pytest 674 全绿、`tsc --noEmit` 通过;全部变更已部署 t480。

View File

@ -126,10 +126,18 @@ SPA 由 FastAPI 托管(`GET /{full_path:path}` → `index.html`)。`frontend
**路由采用 keep-alive 标签页模式**(非标准 `<Routes>`):所有页面同时挂载在 DOM 中,通过 `display: block/none` 切换可见性,保证页面状态在切 tab 时不丢失。路由配置集中在 `App.tsx``routeConfigs` 数组中Zustand `tabStore` 管理标签页的打开/关闭/激活状态,`react-router` 仅用于 URL 同步。
**三种状态管理策略**
- **Zustand**`stores/tabStore.ts`):全局标签页状态,唯一一个 store
- **Zustand**`stores/tabStore.tsx`):全局标签页状态,唯一一个 store
- **useReducer**`hooks/sessionReducer.ts`评测会话状态cases/turns/results/progress纯 reducer 无副作用hook 负责 WS/REST 副作用
- **useState**:各页面本地数据,无全局缓存(不使用 SWR/React Query
**UI 交互一致性标准**ADR-0005全部管理页必须遵守
- 页壳:`PageWrapper inline fullHeight`;详情/报告用页内 view 切换或右侧 Drawer 弹出(非新路由),列表常驻
- 表格:`Empty` 空态;超过 20 行才分页pageSize 20禁止 `scroll.y` calc hack外层 div `overflowY: auto`
- 表单:`components/FormDrawer.tsx`(宽 640、页脚取消/确定、`submitting` loading、`destroyOnClose`
- 危险操作(删除/取消/停止):`Popconfirm`
- 反馈axios 拦截器统一 message页面不重复弹错
- 活动状态轮询:`usePolling(fn, ms, enabled)` 5 秒静默轮询(`reload(true)`
**Vite 开发代理**dev server 端口 3000`/api`、`/ws`、`/openclaw` 全部代理到 `localhost:8000`;生产构建时 `dist/` 由 FastAPI 直接托管。
**Ant Design CSS-in-JS 冲突**Ant Design v5 的 emotion 运行时注入会覆盖静态 CSS 的 `flex`/`height` 规则。Runs 页采用"导航与内容分离"方案——Tabs 组件仅用于渲染导航头(`.nav-only-tabs` CSS 隐藏 `.ant-tabs-content-holder`),实际内容 div 由 flex 直接控制高度,绕过 Ant Design 内部 DOM。

View File

@ -0,0 +1,23 @@
# ADR-0005: 管理后台 UI 一致性重构以智能评估页为基准
## 状态
已接受2026-08-05
## 背景
管理后台 9 个业务页面交互模式不一致(表单容器 Drawer/Modal 混用、空态/加载态/删除确认各页不同、Runs/Reports 绕过 PageWrapper。智能评估 v1.0 的页面模式是最新打磨并被用户验证过的。
## 决策
- 以智能评估页模式为唯一基准,逐页迁移对齐(先沉淀共享件 FormDrawer/usePolling再按 IntelligentEvals → Targets → Files → ModelConfigs → Scenarios → Runs → Reports → Campaigns 顺序)。
- 重构边界:只统一交互层,不增删业务功能、不动 API、不动信息架构与 keep-alive 路由。
- 交互标准PageWrapper 骨架、Table+Empty、20 行分页阈值、Drawer 640 表单、详情用页内视图切换或右侧 Drawer 弹出列表常驻、Popconfirm 危险操作、message 反馈、活跃态 5s 轮询。
## 后果
- 正面:用户操作肌肉记忆可跨页面复用;新页面有明确模式可循;共享件减少重复代码。
- 负面迁移期间每页需人工走查验收Runs/Reports 的特殊 hack 需小心保留。
- 不做:视觉设计系统重做、导航重构、自动化视觉回归。
详细 spec 见 `.scratch/ui-consistency/spec.md`

View File

@ -0,0 +1,36 @@
import { Button, Drawer, Space } from 'antd'
import type { ReactNode } from 'react'
interface FormDrawerProps {
title: string
open: boolean
onClose: () => void
onSubmit: () => void
submitting?: boolean
okText?: string
width?: number
children: ReactNode
}
/** 标准表单抽屉:宽 640、底部取消/主按钮、destroyOnClose。 */
export default function FormDrawer({
title, open, onClose, onSubmit, submitting = false, okText = '确定', width = 640, children,
}: FormDrawerProps) {
return (
<Drawer
title={title}
open={open}
onClose={onClose}
width={width}
destroyOnClose
footer={
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={onClose}></Button>
<Button type="primary" loading={submitting} onClick={onSubmit}>{okText}</Button>
</Space>
}
>
{children}
</Drawer>
)
}

View File

@ -5,7 +5,7 @@ import {
FolderOutlined,
PlusOutlined,
} from '@ant-design/icons'
import { Button, Popconfirm, Tooltip, Tree } from 'antd'
import { Button, Empty, Popconfirm, Tooltip, Tree } from 'antd'
import type { DataNode } from 'antd/es/tree'
import type { FileCategory } from '../../api'
import { colors, fontSizes } from '../../tokens'
@ -109,6 +109,13 @@ export default function FileCategoryTree({
</Tooltip>
</div>
<div className="files-category-scroll">
{categories.length === 0 ? (
<Empty
description="暂无分类,点击右上角新建"
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ marginTop: 24 }}
/>
) : (
<Tree
defaultExpandAll
treeData={treeData}
@ -118,6 +125,7 @@ export default function FileCategoryTree({
onSelect(!key || key === ALL_FILES_KEY ? null : key)
}}
/>
)}
</div>
</aside>
)

View File

@ -3,11 +3,10 @@ import {
DownloadOutlined,
FileOutlined,
FolderOutlined,
InboxOutlined,
ReloadOutlined,
UploadOutlined,
} from '@ant-design/icons'
import { Button, Grid, Popconfirm, Space, Table, Tag, Tooltip } from 'antd'
import { Button, Empty, Grid, Popconfirm, Space, Table, Tag, Tooltip } from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { useMemo } from 'react'
import type { FileCategory, FileRecord } from '../../api'
@ -192,19 +191,12 @@ export default function FileTable({
rowKey="id"
loading={loading}
scroll={screens.md ? { x: 900 } : undefined}
pagination={files.length > 10 ? {
pageSize: 15,
showSizeChanger: true,
pageSizeOptions: ['15', '30', '50'],
pagination={files.length > 20 ? {
pageSize: 20,
showTotal: (total) => `${total} 个文件`,
} : false}
locale={{
emptyText: (
<div className="files-empty">
<InboxOutlined />
<span></span>
</div>
),
emptyText: <Empty description="暂无文件" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}}
/>
</div>

View File

@ -1,11 +1,12 @@
import { useEffect, useState } from 'react'
import { useCallback, useEffect, useState } from 'react'
import {
Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message,
Alert, Button, Card, Col, Descriptions, Empty, Input, Modal, Popconfirm, Progress, Row, Space, Spin, Tag, message,
} from 'antd'
import { ArrowLeftOutlined, FileTextOutlined, StopOutlined } from '@ant-design/icons'
import { FileTextOutlined, StopOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type IntelligentEval, type IntelligentEvalSession } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime, shortDateTime } from '../../utils/date'
import { usePolling } from '../../hooks/usePolling'
import { EVAL_STATUS, SESSION_STATUS } from './status'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
@ -62,12 +63,11 @@ function PlanView({ ev }: { ev: IntelligentEval }) {
interface EvalDetailProps {
ev: IntelligentEval
targetName: string
onBack: () => void
onOpenReport: () => void
onChanged: () => void
}
export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onChanged }: EvalDetailProps) {
export default function EvalDetail({ ev, targetName, onOpenReport, onChanged }: EvalDetailProps) {
const [busy, setBusy] = useState(false)
const [rejectOpen, setRejectOpen] = useState(false)
const [feedback, setFeedback] = useState('')
@ -75,17 +75,18 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
const showSessions = ev.status === 'executing' || ev.status === 'completed'
const loadSessions = useCallback(() => {
intelligentEvalsApi.listSessions(ev.id)
.then((res) => setSessions(res.data.sessions))
.catch(() => undefined)
}, [ev.id])
useEffect(() => {
if (!showSessions) { setSessions(null); return }
let cancelled = false
const load = () => intelligentEvalsApi.listSessions(ev.id)
.then((res) => { if (!cancelled) setSessions(res.data.sessions) })
.catch(() => undefined)
void load()
if (ev.status !== 'executing') return () => { cancelled = true }
const timer = setInterval(load, 5000)
return () => { cancelled = true; clearInterval(timer) }
}, [ev.id, ev.status, showSessions])
loadSessions()
}, [showSessions, loadSessions])
usePolling(loadSessions, 5000, showSessions && ev.status === 'executing')
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
setBusy(true)
@ -107,11 +108,12 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
}, '已打回,等待重新规划')
return (
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Space style={{ marginBottom: 12 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>{ev.name}</span>
<Tag color={meta.color}>{meta.label}</Tag>
<div style={{ flex: 1 }} />
<Space>
{ev.status === 'completed' && (
<Button type="primary" icon={<FileTextOutlined />} onClick={onOpenReport}></Button>
)}
@ -121,9 +123,12 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
</Popconfirm>
)}
</Space>
</div>
<Row gutter={16}>
<Col span={9}>
<Card size="small" title="基本信息" style={sectionCard}>
<Descriptions column={3} size="small">
<Descriptions column={1} size="small">
<Descriptions.Item label="评测对象">{targetName}</Descriptions.Item>
<Descriptions.Item label="时间窗口">{ev.time_window_hours} </Descriptions.Item>
<Descriptions.Item label="创建时间">{formatDateTime(ev.created_at)}</Descriptions.Item>
@ -155,7 +160,9 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
</Descriptions.Item>
</Descriptions>
</Card>
</Col>
<Col span={15}>
{ev.status === 'planning' && (
ev.plan_feedback ? (
<Alert
@ -192,7 +199,18 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
)}
{showSessions && (
<Card size="small" title={`会话进度(${ev.completed_sessions}/${ev.session_count}`} style={sectionCard}>
<Card
size="small"
title={`会话进度(${ev.completed_sessions}/${ev.session_count}`}
style={sectionCard}
extra={(
<Progress
percent={ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0}
size="small"
style={{ width: 140 }}
/>
)}
>
{sessions === null && <Spin size="small" />}
{sessions !== null && sessions.length === 0 && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
@ -227,6 +245,8 @@ export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onCha
)}
</Card>
)}
</Col>
</Row>
<Modal
title="打回计划"

View File

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react'
import {
Button, Card, Collapse, Empty, Space, Spin, Statistic, Tag, message,
Button, Card, Col, Collapse, Empty, Row, Space, Spin, Statistic, Tag, message,
} from 'antd'
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'
import { Bar } from '@ant-design/charts'
import ChatBubble from '../ChatBubble'
import {
intelligentEvalsApi,
@ -143,20 +144,38 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
const findings = [...(report?.findings ?? [])]
.sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order)
const scores = Object.entries(report?.scores ?? {})
const overall = scores.length > 0
? (scores.reduce((sum, [, v]) => sum + Number(v), 0) / scores.length).toFixed(1)
: null
const sevCounts = findings.reduce((acc, f) => {
acc[f.severity] = (acc[f.severity] ?? 0) + 1
return acc
}, {} as Record<string, number>)
const scoreData = scores.map(([dimension, score]) => ({ dimension, score: Number(score) }))
const scoreConfig = {
data: scoreData,
xField: 'dimension',
yField: 'score',
coordinate: { transform: [{ type: 'transpose' }] },
height: Math.max(160, scoreData.length * 44),
color: colors.primary,
}
return (
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Space style={{ marginBottom: 12 }}>
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>
{ev.name} ·
</span>
<div style={{ flex: 1 }} />
{report && (
<Button icon={<DownloadOutlined />} loading={exporting} onClick={exportMarkdown}>
Markdown
</Button>
)}
</Space>
</div>
{reportLoading && <Spin />}
@ -166,17 +185,36 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
{report && (
<>
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col span={6}>
<Card size="small"><Statistic title="综合评分" value={overall ?? '—'} /></Card>
</Col>
<Col span={6}>
<Card size="small">
<Statistic title="问题发现" value={findings.length} />
<div style={{ fontSize: 12, color: colors.textMuted }}>
{sevCounts.high ?? 0} · {sevCounts.medium ?? 0} · {sevCounts.low ?? 0}
</div>
</Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="亮点" value={report.highlights.length} /></Card>
</Col>
<Col span={6}>
<Card size="small"><Statistic title="会话数" value={ev.session_count} /></Card>
</Col>
</Row>
{scoreData.length > 0 && (
<Card size="small" title="维度得分" style={sectionCard}>
<Bar {...scoreConfig} />
</Card>
)}
<Card size="small" title="总结" style={sectionCard}>
<div style={{ whiteSpace: 'pre-wrap', fontSize: 13, lineHeight: 1.8 }}>
{report.summary}
</div>
{scores.length > 0 && (
<div style={{ display: 'flex', gap: 32, marginTop: 12, flexWrap: 'wrap' }}>
{scores.map(([dimension, score]) => (
<Statistic key={dimension} title={dimension} value={score} />
))}
</div>
)}
</Card>
<Card size="small" title={`问题发现(${findings.length}`} style={sectionCard}>

View File

@ -326,20 +326,6 @@ body {
display: none;
}
.files-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 40px;
color: #8c8c8c;
}
.files-empty .anticon {
font-size: 40px;
color: #bfbfbf;
}
.files-upload-category {
display: grid;
grid-template-columns: auto minmax(0, 1fr);

View File

@ -11,6 +11,7 @@ import {
RobotOutlined, SwapOutlined, CompassOutlined,
} from '@ant-design/icons'
import { Line, Bar } from '@ant-design/charts'
import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import StatCard from '../components/StatCard'
import {
@ -698,7 +699,7 @@ export default function CampaignsPage() {
loading={loading}
dataSource={campaigns}
columns={columns}
pagination={false}
pagination={campaigns.length > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
expandable={{
expandedRowKeys: expandedIds,
onExpand: onExpandRow,
@ -709,20 +710,14 @@ export default function CampaignsPage() {
</div>
{/* 创建活动 */}
<Drawer
<FormDrawer
title="新建评估活动"
open={createOpen}
onClose={() => setCreateOpen(false)}
onSubmit={submitCreate}
submitting={submitting}
okText="创建并开始调度"
width={920}
destroyOnClose
footer={
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => setCreateOpen(false)}></Button>
<Button type="primary" loading={submitting} onClick={submitCreate}>
</Button>
</Space>
}
>
<Form form={form} layout="vertical">
<Row gutter={32}>
@ -949,7 +944,7 @@ export default function CampaignsPage() {
</Col>
</Row>
</Form>
</Drawer>
</FormDrawer>
{/* 周期报告 */}
<Drawer
@ -1080,7 +1075,7 @@ export default function CampaignsPage() {
<Table
rowKey="id"
size="small"
pagination={false}
pagination={reportRuns.length > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
dataSource={reportRuns}
columns={runColumns}
locale={{ emptyText: <Empty description="暂无子运行" /> }}

View File

@ -1,13 +1,15 @@
import { useEffect, useState } from 'react'
import { useState } from 'react'
import {
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Spin, Table, Tag, Tooltip, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
import { PlusOutlined, ReloadOutlined, EyeOutlined, FileTextOutlined } 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 { EVAL_STATUS } from '../components/intelligent_eval/status'
import { usePolling } from '../hooks/usePolling'
import { useResource } from '../hooks/useResource'
import {
intelligentEvalsApi, targetsApi,
@ -16,7 +18,7 @@ import {
import { colors } from '../tokens'
import { formatDateTime } from '../utils/date'
type View = 'list' | 'detail' | 'report'
type DrawerView = 'detail' | 'report'
interface CreateFormValues {
name: string
@ -29,7 +31,7 @@ interface CreateFormValues {
}
export default function IntelligentEvalsPage() {
const [view, setView] = useState<View>('list')
const [drawerView, setDrawerView] = useState<DrawerView>('detail')
const [selectedId, setSelectedId] = useState<string | null>(null)
const [detailTick, setDetailTick] = useState(0)
const [createOpen, setCreateOpen] = useState(false)
@ -50,21 +52,28 @@ export default function IntelligentEvalsPage() {
{ deps: [selectedId, detailTick] },
)
const pollActive = view === 'detail' && selected != null
const pollActive = selected != null
&& (selected.status === 'planning' || selected.status === 'executing' || selected.status === 'pending_approval')
useEffect(() => {
if (!pollActive) return
const timer = setInterval(() => void reloadSelected(true), 5000)
return () => clearInterval(timer)
}, [pollActive, reloadSelected])
usePolling(() => void reloadSelected(true), 5000, pollActive)
const targetName = (id: string) =>
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
const openDetail = (id: string) => {
setSelectedId(id)
setView('detail')
setDrawerView('detail')
}
const openReport = (id: string) => {
setSelectedId(id)
setDrawerView('report')
}
const closeDrawer = () => {
setSelectedId(null)
setDrawerView('detail')
void reload()
}
const submitCreate = async () => {
@ -108,9 +117,7 @@ export default function IntelligentEvalsPage() {
const columns: ColumnsType<IntelligentEval> = [
{
title: '名称', dataIndex: 'name', key: 'name',
render: (name: string, ev) => (
<a onClick={() => openDetail(ev.id)}>{name}</a>
),
render: (name: string) => <span style={{ fontWeight: 500 }}>{name}</span>,
},
{
title: '评测对象', dataIndex: 'target_id', key: 'target', width: 180,
@ -131,37 +138,23 @@ export default function IntelligentEvalsPage() {
title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170,
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
},
{
title: '操作', key: 'action', width: 110, fixed: 'right' as const,
render: (_, ev) => (
<Space size={4}>
<Tooltip title="详情">
<Button size="small" type="text" icon={<EyeOutlined />} onClick={() => openDetail(ev.id)} />
</Tooltip>
{ev.status === 'completed' && (
<Tooltip title="查看报告">
<Button size="small" type="text" icon={<FileTextOutlined />} onClick={() => openReport(ev.id)} />
</Tooltip>
)}
</Space>
),
},
]
if (view === 'detail' && selectedId) {
if (!selected) {
return (
<PageWrapper title="智能评估" inline fullHeight>
<div style={{ padding: 16 }}></div>
</PageWrapper>
)
}
return (
<PageWrapper title="智能评估" inline fullHeight>
<EvalDetail
ev={selected}
targetName={targetName(selected.target_id)}
onBack={() => { setView('list'); void reload() }}
onOpenReport={() => setView('report')}
onChanged={() => setDetailTick((t) => t + 1)}
/>
</PageWrapper>
)
}
if (view === 'report' && selected) {
return (
<PageWrapper title="智能评估" inline fullHeight>
<EvalReport ev={selected} onBack={() => setView('detail')} />
</PageWrapper>
)
}
return (
<PageWrapper
title="智能评估"
@ -183,26 +176,40 @@ export default function IntelligentEvalsPage() {
loading={loading}
dataSource={evals ?? []}
columns={columns}
pagination={false}
onRow={(ev) => ({ onClick: () => openDetail(ev.id), style: { cursor: 'pointer' } })}
pagination={(evals?.length ?? 0) > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
locale={{ emptyText: <Empty description="还没有智能评估" /> }}
/>
</div>
<Drawer
title={drawerView === 'report' ? '评估报告' : '评估详情'}
open={selectedId != null}
onClose={closeDrawer}
width={1040}
destroyOnClose
styles={{ body: { padding: 0 } }}
>
{selected == null ? (
<div style={{ padding: 48, textAlign: 'center' }}><Spin /></div>
) : drawerView === 'report' ? (
<EvalReport ev={selected} onBack={() => setDrawerView('detail')} />
) : (
<EvalDetail
ev={selected}
targetName={targetName(selected.target_id)}
onOpenReport={() => setDrawerView('report')}
onChanged={() => setDetailTick((t) => t + 1)}
/>
)}
</Drawer>
<FormDrawer
title="新建智能评估"
open={createOpen}
onClose={() => setCreateOpen(false)}
width={640}
destroyOnClose
footer={
<Space style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => setCreateOpen(false)}></Button>
<Button type="primary" loading={submitting} onClick={submitCreate}>
</Button>
</Space>
}
onSubmit={submitCreate}
submitting={submitting}
okText="创建并开始规划"
>
<Form
form={form}
@ -268,7 +275,7 @@ export default function IntelligentEvalsPage() {
OpenClaw OpenClaw
</div>
</Form>
</Drawer>
</FormDrawer>
</PageWrapper>
)
}

View File

@ -1,15 +1,16 @@
import { useState } from 'react'
import {
Button, Divider, Drawer, Form, Input, InputNumber, message, Popconfirm, Select, Space, Switch, Table, Tag,
Button, Divider, Empty, Form, Input, InputNumber, message, Popconfirm, Select, Space, Switch, Table, Tag,
Tooltip,
} from 'antd'
import {
ApiOutlined, CheckCircleOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined,
ApiOutlined, DeleteOutlined, EditOutlined, PlusOutlined, ReloadOutlined,
} from '@ant-design/icons'
import {
modelConfigsApi, type ModelCapability, type ModelConfig, type ModelConfigPayload, type ModelModality,
type ModelProtocol,
} from '../api'
import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import { useResource } from '../hooks/useResource'
import { formatDateTime } from '../utils/date'
@ -85,6 +86,7 @@ const protocolOptions: Record<ModelProtocol, {
export default function ModelConfigsPage() {
const [drawerOpen, setDrawerOpen] = useState(false)
const [editing, setEditing] = useState<ModelConfig | null>(null)
const [submitting, setSubmitting] = useState(false)
const [testingId, setTestingId] = useState<string | null>(null)
const [capability, setCapability] = useState<ModelCapability | undefined>()
const [enabled, setEnabled] = useState<boolean | undefined>()
@ -170,11 +172,21 @@ export default function ModelConfigsPage() {
description: values.description || '',
}
if (editing) {
setSubmitting(true)
try {
await modelConfigsApi.update(editing.id, payload)
message.success('模型配置已更新')
} finally {
setSubmitting(false)
}
} else {
setSubmitting(true)
try {
await modelConfigsApi.create(payload)
message.success('模型配置已创建')
} finally {
setSubmitting(false)
}
}
notifyModelConfigsChanged()
setDrawerOpen(false)
@ -317,7 +329,7 @@ export default function ModelConfigsPage() {
<Tooltip title="编辑">
<Button type="text" size="small" icon={<EditOutlined />} onClick={() => openEdit(record)} />
</Tooltip>
<Popconfirm title="确认删除该模型配置?" onConfirm={() => remove(record)}>
<Popconfirm title="删除该模型配置?" onConfirm={() => remove(record)}>
<Tooltip title="删除">
<Button type="text" size="small" danger icon={<DeleteOutlined />} />
</Tooltip>
@ -356,25 +368,25 @@ export default function ModelConfigsPage() {
</Space>
}
>
<div style={{ height: '100%', minHeight: 0, overflow: 'hidden' }}>
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Table
dataSource={configs ?? []}
columns={columns}
rowKey="id"
loading={loading}
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (total) => `${total}` }}
scroll={{ y: 'calc(100vh - 200px)' }}
style={{ height: '100%' }}
pagination={(configs?.length ?? 0) > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
locale={{ emptyText: <Empty description="还没有模型配置" /> }}
tableLayout="auto"
/>
</div>
<Drawer
<FormDrawer
title={editing ? '编辑模型配置' : '新增模型配置'}
width="min(680px, 100vw)"
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
extra={<Button type="primary" icon={<CheckCircleOutlined />} onClick={submit}></Button>}
onSubmit={submit}
submitting={submitting}
okText="保存"
>
<Form form={form} layout="vertical">
<Divider orientation="left" plain style={{ marginTop: 0 }}></Divider>
@ -472,7 +484,7 @@ export default function ModelConfigsPage() {
</Form.Item>
</Space>
</Form>
</Drawer>
</FormDrawer>
</PageWrapper>
)
}

View File

@ -10,6 +10,7 @@ import {
DiffOutlined, FileMarkdownOutlined,
} from '@ant-design/icons'
import { reportsApi, runsApi, type Run } from '../api'
import PageWrapper from '../components/PageWrapper'
import { colors, triggerColors, triggerLabels } from '../tokens'
import { formatDateTime } from '../utils/date'
import { useResource } from '../hooks/useResource'
@ -222,17 +223,8 @@ export default function ReportsPage() {
(opt?.searchText ?? '').includes(input.toLowerCase())
return (
<PageWrapper title="评测报告" description="查看评测结果详情与统计分析" inline fullHeight>
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* 页头 */}
<div style={{
padding: '10px 16px 8px', flexShrink: 0,
display: 'flex', alignItems: 'center', gap: 12,
}}>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600, color: colors.text }}></h2>
<span style={{ width: 1, height: 18, background: '#d9d9d9', display: 'inline-block' }} />
<span style={{ fontSize: 13, color: colors.textSecondary }}></span>
</div>
{/* 选择栏:两行布局,避免对比模式下控件换行错乱 */}
<div style={{
padding: '8px 16px', flexShrink: 0,
@ -336,6 +328,7 @@ export default function ReportsPage() {
</Spin>
</div>
</div>
</PageWrapper>
)
}
@ -539,7 +532,7 @@ function CompareView({ result }: { result: CompareResult | null }) {
<Card title={`用例对比(${changed_cases} 个结果变化)`}>
<Table
size="small"
pagination={false}
pagination={cases.length > 20 ? { pageSize: 20, showTotal: (t) => `${t} 个用例` } : false}
dataSource={cases}
rowKey="case_id"
rowClassName={(r) => r.changed ? 'run-row' : ''}

View File

@ -1,7 +1,7 @@
import { useEffect, useMemo, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import {
Alert, Button, Empty, message, Progress, Select, Space, Tabs, Tooltip, Typography,
Alert, Button, Empty, message, Popconfirm, Progress, Select, Space, Tabs, Tooltip, Typography,
} from 'antd'
import {
FileTextOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined,
@ -11,6 +11,7 @@ import RunList from '../components/RunList'
import CaseBlock from '../components/CaseBlock'
import RuleOverview from '../components/RuleOverview'
import CaseDetail from '../components/CaseDetail'
import PageWrapper from '../components/PageWrapper'
import { useRunSession } from '../hooks/useRunSession'
import { useTicker } from '../hooks/useTicker'
import { useResource } from '../hooks/useResource'
@ -94,16 +95,9 @@ export default function RunsPage() {
}
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
{/* 页头 */}
<div style={{ padding: '10px 16px 8px', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 12 }}>
<h2 style={{ margin: 0, fontSize: 20, fontWeight: 600, color: colors.text }}></h2>
<span style={{ width: 1, height: 18, background: '#d9d9d9', display: 'inline-block' }} />
<span style={{ fontSize: 13, color: colors.textSecondary }}></span>
</div>
<PageWrapper title="评测执行" description="启动评测任务并查看实时执行详情" inline fullHeight>
{/* 主体 grid — 贴边铺满 */}
<div style={{ flex: 1, minHeight: 0 }}>
<div style={{ height: '100%' }}>
<div style={{
display: 'grid',
gridTemplateColumns: 'minmax(360px, 28vw) 1fr',
@ -225,7 +219,7 @@ export default function RunsPage() {
</div>
</div>
</div>
</div>
</PageWrapper>
)
}
@ -328,7 +322,9 @@ function DetailWorkspace({
</Typography.Text>
</Tooltip>
{isLive ? (
<Button danger size="small" icon={<StopOutlined />} onClick={onCancel}></Button>
<Popconfirm title="停止该评测任务?" onConfirm={onCancel} okButtonProps={{ danger: true }}>
<Button danger size="small" icon={<StopOutlined />}></Button>
</Popconfirm>
) : (
<Space size={4}>
<Button size="small" icon={<ReloadOutlined />} onClick={onRerun}></Button>

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import {
Button, Card, Drawer, Form, Input, message, Modal, Popconfirm,
Button, Card, Empty, Form, Input, message, Modal, Popconfirm,
Select, Space, Table, Tag, Tooltip,
} from 'antd'
import {
@ -9,6 +9,7 @@ import {
} from '@ant-design/icons'
import Editor from '@monaco-editor/react'
import { modelConfigsApi, scenariosApi, type Scenario } from '../api'
import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import { useResource } from '../hooks/useResource'
import { colors } from '../tokens'
@ -50,6 +51,7 @@ export default function ScenariosPage() {
const [previewOpen, setPreviewOpen] = useState(false)
const [previewData, setPreviewData] = useState<any>(null)
const [editingScenario, setEditingScenario] = useState<Scenario | null>(null)
const [submitting, setSubmitting] = useState(false)
const [casesJson, setCasesJson] = useState(YAML_TEMPLATE)
const [modelBindings, setModelBindings] = useState<Record<string, string>>({})
const [form] = Form.useForm()
@ -138,11 +140,21 @@ export default function ScenariosPage() {
}
if (editingScenario) {
setSubmitting(true)
try {
await scenariosApi.update(editingScenario.id, payload)
message.success('更新成功')
} finally {
setSubmitting(false)
}
} else {
setSubmitting(true)
try {
await scenariosApi.create(payload)
message.success('创建成功')
} finally {
setSubmitting(false)
}
}
setDrawerOpen(false)
reload()
@ -180,7 +192,7 @@ export default function ScenariosPage() {
<Tooltip title="编辑">
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
</Tooltip>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Popconfirm title="删除该评测场景" onConfirm={() => handleDelete(record.id)}>
<Tooltip title="删除">
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Tooltip>
@ -217,29 +229,25 @@ export default function ScenariosPage() {
</Space>
}
>
{/* 表格区 — 撑满剩余高度 */}
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Table
dataSource={scenarios ?? []}
columns={columns}
rowKey="id"
loading={loading}
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (t) => `${t}` }}
scroll={{ y: 'calc(100vh - 200px)' }}
style={{ height: '100%' }}
pagination={(scenarios?.length ?? 0) > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
locale={{ emptyText: <Empty description="还没有评测场景" /> }}
/>
</div>
<Drawer
<FormDrawer
title={editingScenario ? `编辑评测场景(当前 v${editingScenario.version ?? 1}` : '新增评测场景'}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
onSubmit={handleSubmit}
submitting={submitting}
okText={editingScenario ? '更新' : '创建'}
width={720}
extra={
<Button type="primary" onClick={handleSubmit}>
{editingScenario ? '更新' : '创建'}
</Button>
}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入场景名称' }]}>
@ -309,7 +317,7 @@ export default function ScenariosPage() {
}}
/>
</div>
</Drawer>
</FormDrawer>
<Modal
title={`场景预览: ${previewData?.name || ''}${previewData?.version != null ? `v${previewData.version}` : ''}`}

View File

@ -1,10 +1,11 @@
import { useState } from 'react'
import {
Button, Drawer, Form, Input, message, Popconfirm,
Button, Empty, Form, Input, message, Popconfirm,
Space, Table, Tag, Select, Tooltip,
} from 'antd'
import { PlusOutlined, ApiOutlined, EditOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons'
import { targetsApi, type Target } from '../api'
import FormDrawer from '../components/FormDrawer'
import PageWrapper from '../components/PageWrapper'
import { useResource } from '../hooks/useResource'
import { colors } from '../tokens'
@ -17,6 +18,7 @@ export default function TargetsPage() {
)
const [drawerOpen, setDrawerOpen] = useState(false)
const [editingTarget, setEditingTarget] = useState<Target | null>(null)
const [submitting, setSubmitting] = useState(false)
const [form] = Form.useForm()
const openCreate = () => {
@ -64,11 +66,21 @@ export default function TargetsPage() {
}
if (editingTarget) {
setSubmitting(true)
try {
await targetsApi.update(editingTarget.id, payload)
message.success('更新成功')
} finally {
setSubmitting(false)
}
} else {
setSubmitting(true)
try {
await targetsApi.create(payload)
message.success('创建成功')
} finally {
setSubmitting(false)
}
}
setDrawerOpen(false)
reload()
@ -122,7 +134,7 @@ export default function TargetsPage() {
<Tooltip title="编辑">
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => openEdit(record)} />
</Tooltip>
<Popconfirm title="确认删除?" onConfirm={() => handleDelete(record.id)}>
<Popconfirm title="删除该评测对象" onConfirm={() => handleDelete(record.id)}>
<Tooltip title="删除">
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
</Tooltip>
@ -149,29 +161,24 @@ export default function TargetsPage() {
</Space>
}
>
{/* 表格区 — 撑满剩余高度 */}
<div style={{ flex: 1, minHeight: 0, overflow: 'hidden' }}>
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Table
dataSource={targets ?? []}
columns={columns}
rowKey="id"
loading={loading}
pagination={{ pageSize: 15, showSizeChanger: true, showTotal: (t) => `${t}` }}
scroll={{ y: 'calc(100vh - 200px)' }}
style={{ height: '100%' }}
pagination={(targets?.length ?? 0) > 20 ? { pageSize: 20, showTotal: (t) => `${t}` } : false}
locale={{ emptyText: <Empty description="还没有评测对象" /> }}
/>
</div>
<Drawer
<FormDrawer
title={editingTarget ? '编辑评测对象' : '新增评测对象'}
open={drawerOpen}
onClose={() => setDrawerOpen(false)}
width={480}
extra={
<Button type="primary" onClick={handleSubmit}>
{editingTarget ? '更新' : '创建'}
</Button>
}
onSubmit={handleSubmit}
submitting={submitting}
okText={editingTarget ? '更新' : '创建'}
>
<Form form={form} layout="vertical">
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
@ -218,7 +225,7 @@ export default function TargetsPage() {
</Form.Item>
</div>
</Form>
</Drawer>
</FormDrawer>
</PageWrapper>
)
}