feat(intelligent-eval): frontend list/detail/report pages (tickets 05-07)
Some checks failed
CI / test (push) Failing after 1m31s

List page with create drawer, detail panel with plan approval/rejection,
and structured report view with severity-sorted findings and session
chat drill-down. Views switch inside the /intelligent-evals keep-alive tab.
This commit is contained in:
sinohqb 2026-08-05 03:49:13 +08:00
parent da6ccc265d
commit cbee749da2
5 changed files with 927 additions and 9 deletions

View File

@ -603,6 +603,143 @@ export const campaignsApi = {
}, },
} }
// ── Intelligent Evaluation智能评估 v1.0 ────────────────────────
export type IntelligentEvalStatus =
| 'draft' | 'planning' | 'pending_approval'
| 'executing' | 'completed' | 'cancelled' | 'failed'
export type IntelligentEvalSessionStatus = 'running' | 'completed' | 'failed' | 'expired'
export interface IntelligentEvalMessage {
id: string
session_id: string
role: string
content: string
latency_ms: number | null
created_at: string | null
}
export interface IntelligentEvalSession {
id: string
eval_id: string
target_id: string
persona: Record<string, unknown>
goal: string
dimension: string | null
status: IntelligentEvalSessionStatus
verdict: Record<string, unknown> | null
turn_count: number
created_at: string | null
closed_at: string | null
}
export interface IntelligentEvalVirtualUser {
persona: Record<string, unknown>
goal: string
}
export interface IntelligentEvalTimeSlot {
time_slot: string
sessions: number
scenario: string
}
export interface IntelligentEvalPlan {
dimensions: string[]
virtual_users: IntelligentEvalVirtualUser[]
time_distribution: IntelligentEvalTimeSlot[]
estimated_sessions: number
budget: { max_turns_per_session: number; total_max_turns: number }
completion_criteria: string
}
export interface ReportEvidence {
session_id?: string
turn_index?: number | null
user_said?: string
assistant_replied?: string
}
export interface ReportFinding {
issue: string
severity: string
dimension: string
evidence: ReportEvidence[]
suggestion?: string | null
related_sop?: string | null
}
export interface ReportHighlight {
description: string
dimension?: string | null
}
export interface IntelligentEvalReport {
summary: string
scores: Record<string, number> | null
findings: ReportFinding[]
highlights: ReportHighlight[]
priority_recommendations: string[]
}
export interface IntelligentEval {
id: string
name: string
target_id: string
status: IntelligentEvalStatus
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
plan: IntelligentEvalPlan | null
plan_feedback: string | null
time_window_hours: number
report: IntelligentEvalReport | null
created_at: string | null
updated_at: string | null
started_at: string | null
completed_at: string | null
session_count: number
completed_sessions: number
}
export interface CreateIntelligentEvalPayload {
name: string
target_id: string
goal: string
seeds: Record<string, unknown>
intent: string
role_description: string
time_window_hours: number
}
export const intelligentEvalsApi = {
list: () => api.get<{ intelligent_evals: IntelligentEval[] }>('/intelligent-evals'),
get: (id: string) => api.get<IntelligentEval>(`/intelligent-evals/${id}`),
create: (data: CreateIntelligentEvalPayload) => api.post<IntelligentEval>('/intelligent-evals', data),
submitPlan: (id: string, plan: Record<string, unknown>) =>
api.put<IntelligentEval>(`/intelligent-evals/${id}/plan`, { plan }),
approve: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/approve`),
reject: (id: string, feedback: string) =>
api.post<IntelligentEval>(`/intelligent-evals/${id}/reject`, { feedback }),
cancel: (id: string) => api.post<IntelligentEval>(`/intelligent-evals/${id}/cancel`),
listSessions: (id: string) =>
api.get<{ sessions: IntelligentEvalSession[] }>(`/intelligent-evals/${id}/sessions`),
listMessages: (id: string, sessionId: string) =>
api.get<{ messages: IntelligentEvalMessage[] }>(`/intelligent-evals/${id}/sessions/${sessionId}/messages`),
getReport: (id: string) => api.get<IntelligentEvalReport>(`/intelligent-evals/${id}/report`),
downloadReportMarkdown: async (id: string) => {
const res = await api.get(`/intelligent-evals/${id}/report/markdown`, { responseType: 'blob' })
const url = URL.createObjectURL(res.data as Blob)
const a = document.createElement('a')
a.href = url
a.download = `intelligent-eval-report-${id.slice(0, 8)}.md`
a.click()
URL.revokeObjectURL(url)
},
}
// ── File Management ────────────────────────────────────────────── // ── File Management ──────────────────────────────────────────────
export interface FileCategory { export interface FileCategory {

View File

@ -0,0 +1,209 @@
import { useState } from 'react'
import {
Alert, Button, Card, Descriptions, Empty, Input, Modal, Popconfirm, Space, Spin, Tag, message,
} from 'antd'
import { ArrowLeftOutlined, FileTextOutlined, StopOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type IntelligentEval } from '../../api'
import { colors } from '../../tokens'
import { formatDateTime, shortDateTime } from '../../utils/date'
import { EVAL_STATUS } from './status'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
function personaLabel(persona: Record<string, unknown>): string {
if (typeof persona.name === 'string' && persona.name) return persona.name
if (typeof persona.background === 'string' && persona.background) return persona.background
return JSON.stringify(persona)
}
function PlanView({ ev }: { ev: IntelligentEval }) {
const plan = ev.plan
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
return (
<div>
<Descriptions column={3} size="small" bordered style={{ marginBottom: 12 }}>
<Descriptions.Item label="预估会话数">{plan.estimated_sessions}</Descriptions.Item>
<Descriptions.Item label="单会话最大轮数">{plan.budget?.max_turns_per_session ?? '—'}</Descriptions.Item>
<Descriptions.Item label="总轮数上限">{plan.budget?.total_max_turns ?? '—'}</Descriptions.Item>
</Descriptions>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space size={6} wrap style={{ marginBottom: 12 }}>
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue">{d}</Tag>)}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.virtual_users ?? []).map((u, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · {u.goal}
</div>
))}
</Space>
<div style={{ fontWeight: 500, marginBottom: 6 }}></div>
<Space direction="vertical" size={4} style={{ width: '100%', marginBottom: 12 }}>
{(plan.time_distribution ?? []).map((t, i) => (
<div key={i} style={{ fontSize: 13, color: colors.textSecondary }}>
<Tag style={{ margin: 0 }}>{t.time_slot}</Tag> {t.sessions} · {t.scenario}
</div>
))}
</Space>
{plan.completion_criteria && (
<div style={{ fontSize: 13, color: colors.textSecondary }}>
<b style={{ color: colors.text }}></b>{plan.completion_criteria}
</div>
)}
</div>
)
}
interface EvalDetailProps {
ev: IntelligentEval
targetName: string
onBack: () => void
onOpenReport: () => void
onChanged: () => void
}
export default function EvalDetail({ ev, targetName, onBack, onOpenReport, onChanged }: EvalDetailProps) {
const [busy, setBusy] = useState(false)
const [rejectOpen, setRejectOpen] = useState(false)
const [feedback, setFeedback] = useState('')
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
const runAction = async (fn: () => Promise<unknown>, okMsg: string) => {
setBusy(true)
try {
await fn()
message.success(okMsg)
onChanged()
} finally {
setBusy(false)
}
}
const approve = () => runAction(() => intelligentEvalsApi.approve(ev.id), '已批准,进入执行')
const cancel = () => runAction(() => intelligentEvalsApi.cancel(ev.id), '已取消')
const submitReject = () => runAction(async () => {
await intelligentEvalsApi.reject(ev.id, feedback)
setRejectOpen(false)
setFeedback('')
}, '已打回,等待重新规划')
return (
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Space style={{ marginBottom: 12 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>{ev.name}</span>
<Tag color={meta.color}>{meta.label}</Tag>
{ev.status === 'completed' && (
<Button type="primary" icon={<FileTextOutlined />} onClick={onOpenReport}></Button>
)}
{(ev.status === 'pending_approval' || ev.status === 'executing') && (
<Popconfirm title="取消该智能评估?" onConfirm={cancel}>
<Button danger icon={<StopOutlined />} loading={busy}></Button>
</Popconfirm>
)}
</Space>
<Card size="small" title="基本信息" style={sectionCard}>
<Descriptions column={3} 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>
<Descriptions.Item label="开始时间">{formatDateTime(ev.started_at)}</Descriptions.Item>
<Descriptions.Item label="完成时间">{formatDateTime(ev.completed_at)}</Descriptions.Item>
<Descriptions.Item label="会话进度">{ev.completed_sessions}/{ev.session_count}</Descriptions.Item>
</Descriptions>
</Card>
<Card size="small" title="用户输入" style={sectionCard}>
<Descriptions column={1} size="small">
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item>
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item>
<Descriptions.Item label="角色描述">
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span>
</Descriptions.Item>
<Descriptions.Item label="种子集">
{Object.keys(ev.seeds ?? {}).length === 0
? '—'
: (
<pre style={{
margin: 0, fontSize: 12, background: colors.bgSubtle,
padding: 8, borderRadius: 6, overflowX: 'auto',
}}
>
{JSON.stringify(ev.seeds, null, 2)}
</pre>
)}
</Descriptions.Item>
</Descriptions>
</Card>
{ev.status === 'planning' && (
ev.plan_feedback ? (
<Alert
style={sectionCard}
type="warning"
showIcon
message="计划已打回OpenClaw 正在根据反馈重新规划"
description={ev.plan_feedback}
/>
) : (
<Card size="small" style={sectionCard}>
<Spin size="small" />
<span style={{ marginLeft: 10, color: colors.textSecondary }}>
OpenClaw
</span>
</Card>
)
)}
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
<Card
size="small"
title="粗计划"
style={sectionCard}
extra={ev.status === 'pending_approval' && (
<Space>
<Button size="small" danger onClick={() => setRejectOpen(true)}></Button>
<Button size="small" type="primary" loading={busy} onClick={approve}></Button>
</Space>
)}
>
<PlanView ev={ev} />
</Card>
)}
{ev.status === 'executing' && (
<Card size="small" title="执行进度" style={sectionCard}>
<div style={{ fontSize: 13, color: colors.textSecondary, marginBottom: 8 }}>
{ev.completed_sessions} / {ev.session_count}
{ev.plan?.estimated_sessions ? ` / 预估 ${ev.plan.estimated_sessions}` : ''}
</div>
<div style={{ fontSize: 12, color: colors.textMuted }}>
OpenClaw {shortDateTime(ev.updated_at)}
</div>
</Card>
)}
<Modal
title="打回计划"
open={rejectOpen}
onCancel={() => setRejectOpen(false)}
onOk={submitReject}
okText="打回"
okButtonProps={{ danger: true, disabled: !feedback.trim(), loading: busy }}
>
<Input.TextArea
rows={4}
value={feedback}
onChange={(e) => setFeedback(e.target.value)}
placeholder="请填写打回原因与改进方向OpenClaw 会据此重新规划"
/>
</Modal>
</div>
)
}

View File

@ -0,0 +1,293 @@
import { useEffect, useState } from 'react'
import {
Button, Card, Collapse, Empty, Space, Spin, Statistic, Tag, message,
} from 'antd'
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'
import ChatBubble from '../ChatBubble'
import {
intelligentEvalsApi,
type IntelligentEval,
type IntelligentEvalMessage,
type IntelligentEvalReport,
type IntelligentEvalSession,
type ReportEvidence,
} from '../../api'
import { colors } from '../../tokens'
import { shortDateTime } from '../../utils/date'
import { SESSION_STATUS, severityOf } from './status'
const sectionCard: React.CSSProperties = { marginBottom: 16 }
function personaLabel(persona: Record<string, unknown>): string {
if (typeof persona.name === 'string' && persona.name) return persona.name
if (typeof persona.background === 'string' && persona.background) return persona.background
return persona.id != null ? String(persona.id) : '虚拟用户'
}
function EvidenceBlock({ evidence }: { evidence: ReportEvidence }) {
return (
<div style={{
background: colors.bgSubtle, borderLeft: `3px solid ${colors.border}`,
padding: '6px 10px', borderRadius: 4, fontSize: 12, lineHeight: 1.7,
}}
>
{evidence.session_id && (
<div style={{ color: colors.textMuted }}>
{evidence.session_id.slice(0, 8)}
{evidence.turn_index != null ? ` · 第 ${evidence.turn_index + 1}` : ''}
</div>
)}
{evidence.user_said && <div>{evidence.user_said}</div>}
{evidence.assistant_replied && <div>{evidence.assistant_replied}</div>}
</div>
)
}
function VerdictView({ verdict }: { verdict: Record<string, unknown> }) {
const entries = Object.entries(verdict)
if (entries.length === 0) return null
return (
<div style={{
marginTop: 8, border: `1px solid ${colors.border}`, borderRadius: 8,
background: colors.bgSubtle, padding: '8px 12px', fontSize: 12, lineHeight: 1.8,
}}
>
<div style={{ fontWeight: 500, marginBottom: 4 }}></div>
{entries.map(([k, v]) => (
<div key={k} style={{ color: colors.textSecondary }}>
<b style={{ color: colors.text }}>{k}</b>
{typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v)}
</div>
))}
</div>
)
}
function SessionMessages({ evalId, sessionId, verdict }: {
evalId: string
sessionId: string
verdict: Record<string, unknown> | null
}) {
const [messages, setMessages] = useState<IntelligentEvalMessage[] | null>(null)
useEffect(() => {
let cancelled = false
intelligentEvalsApi.listMessages(evalId, sessionId)
.then((res) => { if (!cancelled) setMessages(res.data.messages) })
.catch(() => undefined) // 拦截器已弹错;收起再展开可重试
return () => { cancelled = true }
}, [evalId, sessionId])
return (
<div>
{!messages && <Spin size="small" />}
{messages && messages.length === 0 && (
<Empty description="暂无对话记录" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
{messages?.map((m) => (
<ChatBubble
key={m.id}
role={m.role === 'user' ? 'user' : 'agent'}
mirrored
content={m.content}
meta={m.role !== 'user' && m.latency_ms != null ? (
<span style={{ color: colors.textMuted, fontSize: 11 }}>{m.latency_ms}ms</span>
) : undefined}
/>
))}
{verdict && <VerdictView verdict={verdict} />}
</div>
)
}
interface EvalReportProps {
ev: IntelligentEval
onBack: () => void
}
export default function EvalReport({ ev, onBack }: EvalReportProps) {
const [report, setReport] = useState<IntelligentEvalReport | null>(ev.report)
const [reportLoading, setReportLoading] = useState(!ev.report)
const [sessions, setSessions] = useState<IntelligentEvalSession[] | null>(null)
const [sessionsLoading, setSessionsLoading] = useState(true)
const [exporting, setExporting] = useState(false)
useEffect(() => {
let cancelled = false
setReport(ev.report)
setReportLoading(!ev.report)
if (!ev.report) {
intelligentEvalsApi.getReport(ev.id)
.then((res) => { if (!cancelled) setReport(res.data) })
.catch(() => undefined)
.finally(() => { if (!cancelled) setReportLoading(false) })
}
setSessionsLoading(true)
intelligentEvalsApi.listSessions(ev.id)
.then((res) => { if (!cancelled) setSessions(res.data.sessions) })
.catch(() => undefined)
.finally(() => { if (!cancelled) setSessionsLoading(false) })
return () => { cancelled = true }
}, [ev.id, ev.report])
const exportMarkdown = async () => {
setExporting(true)
try {
await intelligentEvalsApi.downloadReportMarkdown(ev.id)
message.success('已导出 Markdown 报告')
} finally {
setExporting(false)
}
}
const findings = [...(report?.findings ?? [])]
.sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order)
const scores = Object.entries(report?.scores ?? {})
return (
<div style={{ height: '100%', overflowY: 'auto', padding: '0 16px 16px' }}>
<Space style={{ marginBottom: 12 }}>
<Button icon={<ArrowLeftOutlined />} onClick={onBack}></Button>
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>
{ev.name} ·
</span>
{report && (
<Button icon={<DownloadOutlined />} loading={exporting} onClick={exportMarkdown}>
Markdown
</Button>
)}
</Space>
{reportLoading && <Spin />}
{!reportLoading && !report && (
<Empty description="暂无报告" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
{report && (
<>
<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}>
{findings.length === 0 && (
<div style={{ fontSize: 13, color: colors.textSecondary }}></div>
)}
<Collapse
size="small"
items={findings.map((f, i) => {
const sev = severityOf(f.severity)
return {
key: i,
label: (
<Space size={8} wrap>
<Tag color={sev.color} style={{ margin: 0 }}>{sev.label}</Tag>
<Tag style={{ margin: 0 }}>{f.dimension}</Tag>
<span style={{ fontWeight: 500 }}>{f.issue}</span>
</Space>
),
children: (
<div>
{f.evidence.length > 0 && (
<div style={{ marginBottom: 8 }}>
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 6 }}>
{f.evidence.length}
</div>
<Space direction="vertical" size={6} style={{ width: '100%' }}>
{f.evidence.map((e, j) => <EvidenceBlock key={j} evidence={e} />)}
</Space>
</div>
)}
{f.suggestion && (
<div style={{ fontSize: 13, marginBottom: 4 }}>
<b></b>{f.suggestion}
</div>
)}
{f.related_sop && (
<div style={{ fontSize: 12, color: colors.textMuted }}>
SOP{f.related_sop}
</div>
)}
</div>
),
}
})}
/>
</Card>
{report.highlights.length > 0 && (
<Card size="small" title="亮点" style={sectionCard}>
<Space direction="vertical" size={4} style={{ width: '100%' }}>
{report.highlights.map((h, i) => (
<div key={i} style={{ fontSize: 13 }}>
{h.dimension && <Tag color="green" style={{ margin: 0 }}>{h.dimension}</Tag>}
{h.dimension ? ' ' : ''}{h.description}
</div>
))}
</Space>
</Card>
)}
{report.priority_recommendations.length > 0 && (
<Card size="small" title="优先改进建议" style={sectionCard}>
{report.priority_recommendations.map((r, i) => (
<div key={i} style={{ fontSize: 13, marginBottom: 4 }}>
<Tag color="blue" style={{ margin: 0 }}>{i + 1}</Tag> {r}
</div>
))}
</Card>
)}
</>
)}
<Card size="small" title="会话记录" style={sectionCard}>
{sessionsLoading && <Spin size="small" />}
{!sessionsLoading && sessions && sessions.length === 0 && (
<Empty description="暂无会话" image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
{!sessionsLoading && sessions && sessions.length > 0 && (
<Collapse
size="small"
items={sessions.map((s) => {
const meta = SESSION_STATUS[s.status] ?? SESSION_STATUS.running
return {
key: s.id,
label: (
<Space size={8} wrap>
<span style={{ fontWeight: 500 }}>{personaLabel(s.persona)}</span>
<Tag color={meta.color}>{meta.label}</Tag>
<Tag>{s.turn_count} </Tag>
{s.dimension && <Tag>{s.dimension}</Tag>}
{s.created_at && (
<span style={{ fontSize: 12, color: colors.textMuted }}>
{shortDateTime(s.created_at)}
</span>
)}
</Space>
),
children: (
<div>
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 8 }}>
{s.goal}
</div>
<SessionMessages evalId={ev.id} sessionId={s.id} verdict={s.verdict} />
</div>
),
}
})}
/>
)}
</Card>
</div>
)
}

View File

@ -0,0 +1,28 @@
import type { IntelligentEvalSessionStatus, IntelligentEvalStatus } from '../../api'
export const EVAL_STATUS: Record<IntelligentEvalStatus, { label: string; color: string }> = {
draft: { label: '草稿', color: 'default' },
planning: { label: '规划中', color: 'processing' },
pending_approval: { label: '待审批', color: 'warning' },
executing: { label: '执行中', color: 'processing' },
completed: { label: '已完成', color: 'success' },
cancelled: { label: '已取消', color: 'default' },
failed: { label: '失败', color: 'error' },
}
export const SESSION_STATUS: Record<IntelligentEvalSessionStatus, { label: string; color: string }> = {
running: { label: '进行中', color: 'processing' },
completed: { label: '已完成', color: 'success' },
failed: { label: '失败', color: 'error' },
expired: { label: '已过期', color: 'default' },
}
export const SEVERITY_META: Record<string, { label: string; color: string; order: number }> = {
high: { label: '高', color: 'red', order: 0 },
medium: { label: '中', color: 'orange', order: 1 },
low: { label: '低', color: 'blue', order: 2 },
}
export function severityOf(severity: string) {
return SEVERITY_META[severity?.toLowerCase()] ?? { label: severity, color: 'default', order: 99 }
}

View File

@ -1,14 +1,265 @@
import { Result } from 'antd' import { useState } from 'react'
import { BulbOutlined } from '@ant-design/icons' import {
Button, Drawer, Empty, Form, Input, InputNumber, Select, Space, Table, Tag, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { PlusOutlined, ReloadOutlined } from '@ant-design/icons'
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 { 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 View = 'list' | '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() { export default function IntelligentEvalsPage() {
const [view, setView] = useState<View>('list')
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 } = useResource(
() => (selectedId ? intelligentEvalsApi.get(selectedId).then((r) => r.data) : Promise.resolve(null)),
{ deps: [selectedId, detailTick] },
)
const targetName = (id: string) =>
targets?.find((t) => t.id === id)?.name ?? id.slice(0, 8)
const openDetail = (id: string) => {
setSelectedId(id)
setView('detail')
}
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, ev) => (
<a onClick={() => openDetail(ev.id)}>{name}</a>
),
},
{
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) : '—'),
},
]
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 ( return (
<div style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <PageWrapper
<Result title="智能评估"
icon={<BulbOutlined style={{ color: '#8c8c8c' }} />} description="描述评估目标,由 OpenClaw 自主规划、执行并产出结构化报告"
title="智能评估" inline
subTitle="OpenClaw 驱动的独立评测体系(开发中)" fullHeight
/> extra={
</div> <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={false}
onRow={(ev) => ({ onClick: () => openDetail(ev.id), style: { cursor: 'pointer' } })}
locale={{ emptyText: <Empty description="还没有智能评估" /> }}
/>
</div>
<Drawer
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>
}
>
<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>
</Drawer>
</PageWrapper>
) )
} }