- Overview: full-width stacked cards (basic info as two aligned rows, plan, user input with seeds in fixed-height right column, sessions) - Basic info labels fixed-width right-aligned for vertical alignment - Add "已显示全部内容" end marker and increase drawer tab bottom padding (16px → 24px) so content no longer looks cut off
254 lines
10 KiB
TypeScript
254 lines
10 KiB
TypeScript
import {
|
||
Alert, Card, Empty, Progress, Row, Col, Space, Spin, Table, Tag,
|
||
} from 'antd'
|
||
import type { IntelligentEval } from '../../api'
|
||
import { colors } from '../../tokens'
|
||
import { formatDateTime, shortDateTime } from '../../utils/date'
|
||
import { EVAL_STATUS, SESSION_STATUS } from './status'
|
||
|
||
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 InfoRow({ items }: { items: [string, React.ReactNode][] }) {
|
||
return (
|
||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px 32px', padding: '4px 0' }}>
|
||
{items.map(([k, v]) => (
|
||
<div key={k} style={{ display: 'flex', gap: 8, fontSize: 13, alignItems: 'center' }}>
|
||
<span style={{ color: colors.textSecondary, fontSize: 12, width: 60, textAlign: 'right', flexShrink: 0 }}>{k}</span>
|
||
<span>{v}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function FieldBlock({ label, content }: { label: string; content: string }) {
|
||
return (
|
||
<div>
|
||
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}>{label}</div>
|
||
<div style={{
|
||
fontSize: 13, lineHeight: 1.6, padding: '8px 12px', background: colors.bgSubtle,
|
||
borderRadius: 6, whiteSpace: 'pre-wrap',
|
||
}}>
|
||
{content || '—'}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function PlanView({ ev }: { ev: IntelligentEval }) {
|
||
const plan = ev.plan
|
||
if (!plan) return <Empty description="暂无粗计划" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||
return (
|
||
<div>
|
||
<Row gutter={12} style={{ marginBottom: 12 }}>
|
||
{[
|
||
['预估会话数', plan.estimated_sessions],
|
||
['单会话最大轮数', plan.budget?.max_turns_per_session ?? '—'],
|
||
['总轮数上限', plan.budget?.total_max_turns ?? '—'],
|
||
].map(([k, v]) => (
|
||
<Col span={8} key={k as string}>
|
||
<div style={{ background: colors.bgSubtle, borderRadius: 6, padding: '8px 12px' }}>
|
||
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 2 }}>{k}</div>
|
||
<div style={{ fontSize: 15, fontWeight: 600 }}>{v}</div>
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
|
||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 6 }}>评测维度</div>
|
||
<Space size={6} wrap style={{ marginBottom: 12 }}>
|
||
{(plan.dimensions ?? []).map((d) => <Tag key={d} color="blue" style={{ fontSize: 12 }}>{d}</Tag>)}
|
||
</Space>
|
||
|
||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 6 }}>虚拟用户</div>
|
||
<Row gutter={[12, 6]} style={{ marginBottom: 12 }}>
|
||
{(plan.virtual_users ?? []).map((u, i) => (
|
||
<Col span={12} key={i}>
|
||
<div style={{
|
||
fontSize: 13, color: colors.textSecondary, lineHeight: 1.5,
|
||
padding: '6px 10px', background: colors.bgSubtle, borderRadius: 6,
|
||
}}>
|
||
<b style={{ color: colors.text }}>{personaLabel(u.persona)}</b> · {u.goal}
|
||
</div>
|
||
</Col>
|
||
))}
|
||
</Row>
|
||
|
||
<div style={{ fontSize: 13, fontWeight: 500, marginBottom: 6 }}>时间分布</div>
|
||
<Table
|
||
size="small"
|
||
rowKey={(t) => t.time_slot}
|
||
dataSource={plan.time_distribution ?? []}
|
||
pagination={false}
|
||
style={{ marginBottom: 12 }}
|
||
columns={[
|
||
{
|
||
title: '时段', dataIndex: 'time_slot', width: 100,
|
||
render: (v: string) => <Tag style={{ margin: 0, fontSize: 12 }}>{v}</Tag>,
|
||
},
|
||
{ title: '会话数', dataIndex: 'sessions', width: 70 },
|
||
{ title: '场景说明', dataIndex: 'scenario', render: (v: string) => <span style={{ fontSize: 12, color: colors.textSecondary }}>{v}</span> },
|
||
]}
|
||
/>
|
||
|
||
{plan.completion_criteria && (
|
||
<div style={{ fontSize: 13, color: colors.textSecondary, lineHeight: 1.6 }}>
|
||
<b style={{ color: colors.text }}>完成标准:</b>{plan.completion_criteria}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
interface EvalOverviewProps {
|
||
ev: IntelligentEval
|
||
targetName: string
|
||
}
|
||
|
||
/**
|
||
* 评估详情"概览"子视图(纯展示,单列布局):
|
||
* 基本信息 → 粗计划 → 用户输入(种子集独立列)→ 会话进度。
|
||
* 审批/取消等动作由评估列表页的详情抽屉头部统一管理。
|
||
*/
|
||
export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
|
||
const meta = EVAL_STATUS[ev.status] ?? { label: ev.status, color: 'default' }
|
||
const showSessions = ev.status === 'executing' || ev.status === 'completed'
|
||
const sessions = showSessions ? ev.sessions ?? [] : []
|
||
const estimated = ev.plan?.estimated_sessions ?? ev.session_count
|
||
const hasSeeds = Object.keys(ev.seeds ?? {}).length > 0
|
||
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
{/* 基本信息:两行横向描述,label 定宽右对齐 */}
|
||
<Card size="small" title="基本信息">
|
||
<InfoRow items={[
|
||
['评测对象', targetName],
|
||
['状态', <Tag key="s" color={meta.color}>{meta.label}</Tag>],
|
||
['时间窗口', `${ev.time_window_hours} 小时`],
|
||
['会话进度', `${ev.completed_sessions}/${estimated}`],
|
||
]} />
|
||
<div style={{ borderTop: `1px dashed ${colors.border}`, marginTop: 4, paddingTop: 8 }}>
|
||
<InfoRow items={[
|
||
['创建时间', formatDateTime(ev.created_at)],
|
||
['开始时间', formatDateTime(ev.started_at)],
|
||
['完成时间', formatDateTime(ev.completed_at)],
|
||
]} />
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 粗计划 */}
|
||
{ev.status === 'planning' && (
|
||
ev.plan_feedback ? (
|
||
<Alert
|
||
type="warning"
|
||
showIcon
|
||
message="计划已打回,OpenClaw 正在根据反馈重新规划"
|
||
description={ev.plan_feedback}
|
||
/>
|
||
) : (
|
||
<Card size="small">
|
||
<Spin size="small" />
|
||
<span style={{ marginLeft: 10, fontSize: 13, color: colors.textSecondary }}>
|
||
OpenClaw 正在规划,产出粗计划后会自动进入待审批…
|
||
</span>
|
||
</Card>
|
||
)
|
||
)}
|
||
|
||
{(ev.status === 'pending_approval' || ev.status === 'executing' || ev.status === 'completed') && (
|
||
<Card size="small" title="粗计划">
|
||
<PlanView ev={ev} />
|
||
</Card>
|
||
)}
|
||
|
||
{/* 用户输入:左列目标/意图/角色堆叠,右列种子集固定高度 */}
|
||
<Card size="small" title="用户输入">
|
||
<Row gutter={16}>
|
||
<Col span={9}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
|
||
<FieldBlock label="评估目标" content={ev.goal} />
|
||
<FieldBlock label="考察意图" content={ev.intent} />
|
||
<FieldBlock label="角色描述" content={ev.role_description} />
|
||
</div>
|
||
</Col>
|
||
<Col span={15}>
|
||
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}>种子集</div>
|
||
{hasSeeds ? (
|
||
<pre style={{
|
||
margin: 0, fontSize: 12, lineHeight: 1.6, background: colors.bgSubtle,
|
||
padding: '12px 16px', borderRadius: 6, overflowX: 'auto',
|
||
height: 340, overflowY: 'auto',
|
||
}}>
|
||
{JSON.stringify(ev.seeds, null, 2)}
|
||
</pre>
|
||
) : (
|
||
<div style={{ fontSize: 13, padding: '8px 12px', background: colors.bgSubtle, borderRadius: 6 }}>—</div>
|
||
)}
|
||
</Col>
|
||
</Row>
|
||
</Card>
|
||
|
||
{/* 会话进度(全宽) */}
|
||
{showSessions && (
|
||
<Card
|
||
size="small"
|
||
title={`会话进度(${ev.completed_sessions}/${estimated})`}
|
||
extra={(
|
||
<Progress
|
||
percent={estimated ? Math.round((ev.completed_sessions / estimated) * 100) : 0}
|
||
size="small"
|
||
style={{ width: 160 }}
|
||
/>
|
||
)}
|
||
>
|
||
{sessions.length === 0 && (
|
||
<div style={{ fontSize: 13, color: colors.textSecondary }}>
|
||
等待 OpenClaw 按粗计划的时间分布创建会话…
|
||
</div>
|
||
)}
|
||
{sessions.map((s) => {
|
||
const sMeta = SESSION_STATUS[s.status]
|
||
return (
|
||
<div
|
||
key={s.id}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
|
||
padding: '8px 0', borderBottom: `1px solid ${colors.border}`,
|
||
}}
|
||
>
|
||
<b style={{ fontSize: 13, color: colors.text, minWidth: 80 }}>{personaLabel(s.persona)}</b>
|
||
<Tag color={sMeta.color} style={{ margin: 0, fontSize: 12 }}>{sMeta.label}</Tag>
|
||
{s.dimension && <Tag style={{ margin: 0, fontSize: 12 }}>{s.dimension}</Tag>}
|
||
<span style={{ fontSize: 13, color: colors.textSecondary, flex: 1, minWidth: 120 }}>{s.goal}</span>
|
||
<span style={{ fontSize: 12, color: colors.textMuted }}>{s.turn_count} 轮</span>
|
||
{s.created_at && (
|
||
<span style={{ fontSize: 12, color: colors.textMuted }}>{shortDateTime(s.created_at)}</span>
|
||
)}
|
||
</div>
|
||
)
|
||
})}
|
||
{ev.status === 'executing' && (
|
||
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
|
||
会话由 OpenClaw 按粗计划的时间分布自唤醒创建,本页每 5 秒自动刷新
|
||
</div>
|
||
)}
|
||
</Card>
|
||
)}
|
||
|
||
{/* 收尾标识:明确内容已到底,避免"看起来没显示完" */}
|
||
<div style={{
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 12,
|
||
fontSize: 12, color: colors.textMuted, padding: '4px 0 8px',
|
||
}}>
|
||
<span style={{ height: 1, width: 80, background: colors.border }} />
|
||
已显示全部内容
|
||
<span style={{ height: 1, width: 80, background: colors.border }} />
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|