283 lines
10 KiB
TypeScript
283 lines
10 KiB
TypeScript
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>
|
||
)
|
||
}
|