Compare commits
17 Commits
6cc2efafb6
...
f26c34a340
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f26c34a340 | ||
|
|
804880f1e3 | ||
|
|
1fbd2bceed | ||
|
|
9f25728c2f | ||
|
|
01f451c155 | ||
|
|
22cc437e2d | ||
|
|
1d2c37277c | ||
|
|
935f0a1f57 | ||
|
|
0832445862 | ||
|
|
11b15908c7 | ||
|
|
5077f088b1 | ||
|
|
b9011aad80 | ||
|
|
1588972a8e | ||
|
|
f1316578fe | ||
|
|
2e78c4befd | ||
|
|
15d0d127ae | ||
|
|
d4c254e121 |
14
.scratch/ui-consistency/issues/01-baseline-components.md
Normal file
14
.scratch/ui-consistency/issues/01-baseline-components.md
Normal file
@ -0,0 +1,14 @@
|
||||
# 01 — 基准组件沉淀 + IntelligentEvals 切换
|
||||
|
||||
**What to build:** 抽取两个共享件并让基准页成为第一个消费者。
|
||||
- `components/FormDrawer.tsx`:标准表单 Drawer(width 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 全流程体验无回归
|
||||
13
.scratch/ui-consistency/issues/02-targets.md
Normal file
13
.scratch/ui-consistency/issues/02-targets.md
Normal 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
|
||||
14
.scratch/ui-consistency/issues/03-files.md
Normal file
14
.scratch/ui-consistency/issues/03-files.md
Normal 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 与三页走查结果,用户确认后再继续 04–08。
|
||||
12
.scratch/ui-consistency/issues/04-model-configs.md
Normal file
12
.scratch/ui-consistency/issues/04-model-configs.md
Normal 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
|
||||
13
.scratch/ui-consistency/issues/05-scenarios.md
Normal file
13
.scratch/ui-consistency/issues/05-scenarios.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 05 — Scenarios 页迁移
|
||||
|
||||
**What to build:** 评测场景页对齐交互标准。Monaco 编辑器与版本对比等功能保持不变,只统一外壳与基础模式。
|
||||
|
||||
**Blocked by:** 04
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [x] 创建/编辑入口统一 FormDrawer(width 720 保留 Monaco 编辑空间)
|
||||
- [x] 删除有 Popconfirm;模板相关交互不变
|
||||
- [x] 空态 Empty、20 行分页阈值
|
||||
- [x] Modal 仅保留预览与模板选择(只读/选择型),编辑表单收敛为 FormDrawer
|
||||
- [x] `tsc --noEmit` 通过
|
||||
- [x] t480 走查:场景 CRUD + 编辑 + 校验 golden path
|
||||
12
.scratch/ui-consistency/issues/06-runs.md
Normal file
12
.scratch/ui-consistency/issues/06-runs.md
Normal file
@ -0,0 +1,12 @@
|
||||
# 06 — Runs 页迁移
|
||||
|
||||
**What to build:** 评测执行页对齐交互标准。**护栏**:「导航内容分离」hack(nav-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
|
||||
12
.scratch/ui-consistency/issues/07-reports.md
Normal file
12
.scratch/ui-consistency/issues/07-reports.md
Normal 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
|
||||
13
.scratch/ui-consistency/issues/08-campaigns.md
Normal file
13
.scratch/ui-consistency/issues/08-campaigns.md
Normal file
@ -0,0 +1,13 @@
|
||||
# 08 — Campaigns 页迁移(最后硬骨头)
|
||||
|
||||
**What to build:** 评估活动页(1094 行)对齐交互标准。时间线、探索区、分析区等独特交互不做模式化改造,只统一页面外壳与基础模式(表单 Drawer、删除确认、空态、反馈)。
|
||||
|
||||
**Blocked by:** 07
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [x] 创建/编辑活动表单统一 FormDrawer(width 920 保留双栏布局;报告抽屉为只读展示+导出按钮,保留 Drawer)
|
||||
- [x] 删除/取消活动 Popconfirm;探索派发等独特交互不变(取消已有 Popconfirm,无删除入口)
|
||||
- [x] 空态 Empty、20 行分页阈值(活动表 + 报告子运行表)
|
||||
- [x] 文件体量:重复的抽屉页脚模式收敛到 FormDrawer(不强制目标行数)
|
||||
- [x] `tsc --noEmit` 通过
|
||||
- [x] t480 走查:活动 CRUD + 时间线 + 探索区 + 报告 golden path
|
||||
12
.scratch/ui-consistency/issues/09-final-regression.md
Normal file
12
.scratch/ui-consistency/issues/09-final-regression.md
Normal 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 与票据验收项勾选归档
|
||||
64
.scratch/ui-consistency/spec.md
Normal file
64
.scratch/ui-consistency/spec.md
Normal file
@ -0,0 +1,64 @@
|
||||
# UI 一致性重构 — Spec(grill 共识沉淀)
|
||||
|
||||
日期:2026-08-05 | 状态:已完结(2026-08-06 归档)
|
||||
|
||||
## 背景
|
||||
|
||||
管理后台 9 个业务页面约 4000 行,交互模式不一致:Runs/Reports 不用 PageWrapper、表单容器混用 Drawer/Modal、Files 删除无确认、空态/加载态/反馈方式各页不同。智能评估 v1.0 交付后,其页面模式成为事实上的最新标准。
|
||||
|
||||
## 决策清单(grill 结论)
|
||||
|
||||
| # | 决策 | 结论 |
|
||||
|---|------|------|
|
||||
| 1 | 重构目标 | 交互层为主、视觉层顺带、信息架构不动 |
|
||||
| 2 | 一致性基准 | 以智能评估页为标准,其他页面向它对齐 |
|
||||
| 3 | 实施策略 | 先沉淀共享组件,再逐页迁移;每页一票一提交一验证 |
|
||||
| 4 | 范围 | 9 个业务页面;排除 Login、OpenClaw(AI 助手嵌入应用) |
|
||||
| 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(检查点)→ 04–08 → 09。
|
||||
|
||||
## 归档收口(2026-08-06)
|
||||
|
||||
**交付**:9 张票据全部完成并验收勾选(issues/01–09),t480 全页走查通过(golden path + keep-alive 切 tab)。
|
||||
|
||||
**实施提交**:
|
||||
- `d4c254e` 票据 01:FormDrawer/usePolling 基准件 + IntelligentEvals dogfood
|
||||
- `15d0d12` / `2e78c4b` / `f131657` / `1588972` / `b9011aa` / `5077f08` / `11b1590`:票据 02–08 逐页迁移
|
||||
- `0832445`:票据 09,交互标准写入 AGENTS.md
|
||||
- `9f25728` / `1fbd2bc`:标准补充与验收归档
|
||||
|
||||
**基准后的智能评估演进**(反哺标准,已同步 ADR-0005 与 AGENTS.md):
|
||||
- `935f0a1` 列表打开方式对齐操作列按钮
|
||||
- `1d2c372` 详情/报告改右侧 Drawer(列表常驻)
|
||||
- `22cc437` 抽屉顶栏:返回按钮移除、动作右对齐
|
||||
- `01f451c` 详情两列布局 + 报告统计行与维度得分图
|
||||
- `804880f` 抽屉顶部内边距修复
|
||||
|
||||
**验证**:pytest 674 全绿、`tsc --noEmit` 通过;全部变更已部署 t480。
|
||||
10
AGENTS.md
10
AGENTS.md
@ -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。
|
||||
|
||||
23
docs/adr/0005-ui-consistency-baseline.md
Normal file
23
docs/adr/0005-ui-consistency-baseline.md
Normal 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`。
|
||||
36
frontend/web/src/components/FormDrawer.tsx
Normal file
36
frontend/web/src/components/FormDrawer.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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="打回计划"
|
||||
|
||||
@ -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}>
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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="暂无子运行" /> }}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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' : ''}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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})` : ''}`}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user