AgentEvalTool/frontend/web/src/components/intelligent_eval/EvalOverview.tsx
sinohqb 61d8b8b4b8 feat(ui): single-column overview layout with end-of-content marker
- 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
2026-08-20 18:59:50 +08:00

254 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 {
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>
)
}