AgentEvalTool/frontend/web/src/pages/IntelligentEvals.tsx
sinohqb 1d2c37277c feat(intelligent-eval): open detail/report in right-side drawer
列表常驻,详情与报告改为右侧抽屉弹出,减少页面层级嵌套。
2026-08-06 00:29:52 +08:00

283 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
import {
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, 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,
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
} from '../api'
import { colors } from '../tokens'
import { formatDateTime } from '../utils/date'
type DrawerView = 'detail' | 'report'
interface CreateFormValues {
name: string
target_id: string
goal: string
intent?: string
role_description?: string
seeds_json?: string
time_window_hours: number
}
export default function IntelligentEvalsPage() {
const [drawerView, setDrawerView] = useState<DrawerView>('detail')
const [selectedId, setSelectedId] = useState<string | null>(null)
const [detailTick, setDetailTick] = useState(0)
const [createOpen, setCreateOpen] = useState(false)
const [submitting, setSubmitting] = useState(false)
const [form] = Form.useForm<CreateFormValues>()
const { data: evals, loading, reload } = useResource(
() => intelligentEvalsApi.list().then((r) => r.data.intelligent_evals),
{ tabPath: '/intelligent-evals' },
)
const { data: targets } = useResource(
() => targetsApi.list().then((r) => r.data),
{ tabPath: '/intelligent-evals' },
)
const { data: selected, reload: reloadSelected } = useResource(
() => (selectedId ? intelligentEvalsApi.get(selectedId).then((r) => r.data) : Promise.resolve(null)),
{ deps: [selectedId, detailTick] },
)
const pollActive = selected != null
&& (selected.status === 'planning' || selected.status === 'executing' || selected.status === 'pending_approval')
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)
setDrawerView('detail')
}
const openReport = (id: string) => {
setSelectedId(id)
setDrawerView('report')
}
const closeDrawer = () => {
setSelectedId(null)
setDrawerView('detail')
void reload()
}
const submitCreate = async () => {
const values = await form.validateFields()
let seeds: Record<string, unknown> = {}
const raw = (values.seeds_json ?? '').trim()
if (raw) {
try {
const parsed = JSON.parse(raw) as unknown
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
message.error('种子集必须是 JSON 对象')
return
}
seeds = parsed as Record<string, unknown>
} catch {
message.error('种子集不是合法 JSON')
return
}
}
const payload: CreateIntelligentEvalPayload = {
name: values.name,
target_id: values.target_id,
goal: values.goal,
seeds,
intent: values.intent ?? '',
role_description: values.role_description ?? '',
time_window_hours: values.time_window_hours,
}
setSubmitting(true)
try {
await intelligentEvalsApi.create(payload)
message.success('已创建OpenClaw 开始规划')
setCreateOpen(false)
form.resetFields()
void reload()
} finally {
setSubmitting(false)
}
}
const columns: ColumnsType<IntelligentEval> = [
{
title: '名称', dataIndex: 'name', key: 'name',
render: (name: string) => <span style={{ fontWeight: 500 }}>{name}</span>,
},
{
title: '评测对象', dataIndex: 'target_id', key: 'target', width: 180,
render: (id: string) => targetName(id),
},
{
title: '状态', dataIndex: 'status', key: 'status', width: 110,
render: (status: IntelligentEval['status']) => {
const meta = EVAL_STATUS[status] ?? { label: status, color: 'default' }
return <Tag color={meta.color}>{meta.label}</Tag>
},
},
{
title: '会话进度', key: 'progress', width: 120,
render: (_, ev) => `${ev.completed_sessions}/${ev.session_count}`,
},
{
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>
),
},
]
return (
<PageWrapper
title="智能评估"
description="描述评估目标,由 OpenClaw 自主规划、执行并产出结构化报告"
inline
fullHeight
extra={
<Space>
<Button icon={<ReloadOutlined />} onClick={() => reload()} />
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
</Button>
</Space>
}
>
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Table
rowKey="id"
loading={loading}
dataSource={evals ?? []}
columns={columns}
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)}
onBack={closeDrawer}
onOpenReport={() => setDrawerView('report')}
onChanged={() => setDetailTick((t) => t + 1)}
/>
)}
</Drawer>
<FormDrawer
title="新建智能评估"
open={createOpen}
onClose={() => setCreateOpen(false)}
onSubmit={submitCreate}
submitting={submitting}
okText="创建并开始规划"
>
<Form
form={form}
layout="vertical"
initialValues={{ time_window_hours: 24, seeds_json: '{}' }}
>
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
<Input placeholder="如:数字员工 24h 服务质量评估" />
</Form.Item>
<Form.Item name="target_id" label="评测对象" rules={[{ required: true, message: '请选择对象' }]}>
<Select
placeholder="选择评测对象"
showSearch
optionFilterProp="label"
options={(targets ?? []).map((t: Target) => ({ label: t.name, value: t.id }))}
/>
</Form.Item>
<Form.Item
name="goal"
label="评估目标"
tooltip="你希望这次评估回答什么问题OpenClaw 据此规划"
rules={[{ required: true, message: '请输入评估目标' }]}
>
<Input.TextArea rows={3} placeholder="如:评估数字员工在真实咨询场景下的服务态度与专业度" />
</Form.Item>
<Form.Item name="intent" label="考察意图">
<Input.TextArea rows={2} placeholder="(可选)重点关注的能力或风险" />
</Form.Item>
<Form.Item name="role_description" label="角色描述">
<Input.TextArea rows={2} placeholder="(可选)被评对象的角色设定,供 OpenClaw 规划参考" />
</Form.Item>
<Form.Item
name="seeds_json"
label="种子集JSON"
tooltip="提供给 OpenClaw 的种子数据如真实问题样例、SOP 摘录等;必须是 JSON 对象"
rules={[{
validator: async (_, v: string | undefined) => {
const raw = (v ?? '').trim()
if (!raw) return
let parsed: unknown
try { parsed = JSON.parse(raw) } catch { throw new Error('不是合法 JSON') }
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
throw new Error('必须是 JSON 对象')
}
},
}]}
>
<Input.TextArea rows={5} style={{ fontFamily: 'monospace', fontSize: 12 }} />
</Form.Item>
<Form.Item
name="time_window_hours"
label="时间窗口(小时)"
tooltip="评估覆盖的服务时间跨度"
rules={[{ required: true, message: '请输入时间窗口' }]}
>
<InputNumber min={1} max={720} style={{ width: 160 }} />
</Form.Item>
<div style={{
background: colors.bgSubtle, borderRadius: 8, padding: '8px 12px',
fontSize: 12, color: colors.textSecondary,
}}
>
OpenClaw OpenClaw
</div>
</Form>
</FormDrawer>
</PageWrapper>
)
}