feat(intelligent-eval): frontend list/detail/report pages (tickets 05-07)
Some checks failed
CI / test (push) Failing after 1m31s
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:
parent
da6ccc265d
commit
cbee749da2
@ -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 {
|
||||||
|
|||||||
209
frontend/web/src/components/intelligent_eval/EvalDetail.tsx
Normal file
209
frontend/web/src/components/intelligent_eval/EvalDetail.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
293
frontend/web/src/components/intelligent_eval/EvalReport.tsx
Normal file
293
frontend/web/src/components/intelligent_eval/EvalReport.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
28
frontend/web/src/components/intelligent_eval/status.ts
Normal file
28
frontend/web/src/components/intelligent_eval/status.ts
Normal 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 }
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user