feat(ui): optimize intelligent eval drawer UI

- Show estimated session progress in list (completed/estimated with running count)
- Improve user input layout with scrollable sections for long text
- Remove 50-item limit on activity feed, show all events
- Enhance lifecycle steps and time distribution chart styling
- Add pagination to decision log table (20/50/100 per page)
- Unify font sizes across drawer tabs (12/13/14px scale)
This commit is contained in:
sinohqb 2026-08-20 17:44:05 +08:00
parent d0487b54b4
commit 60981b634d
5 changed files with 73 additions and 37 deletions

View File

@ -100,7 +100,7 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
dataIndex: 'cron_id', dataIndex: 'cron_id',
key: 'cron_id', key: 'cron_id',
width: 120, width: 120,
render: (val: string) => <code style={{ fontSize: 11 }}>{val.slice(0, 8)}</code>, render: (val: string) => <code style={{ fontSize: 12 }}>{val.slice(0, 8)}</code>,
}, },
{ {
title: '操作', title: '操作',
@ -157,8 +157,8 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
{formatDateTime(log.created_at)} {formatDateTime(log.created_at)}
</span> </span>
</div> </div>
<div style={{ fontSize: 13 }}>{log.reason}</div> <div style={{ fontSize: 13, lineHeight: 1.5 }}>{log.reason}</div>
<div style={{ fontSize: 11, color: colors.textSecondary, marginTop: 4 }}> <div style={{ fontSize: 12, color: colors.textSecondary, marginTop: 4 }}>
Cron: <code>{log.cron_id.slice(0, 8)}</code> Cron: <code>{log.cron_id.slice(0, 8)}</code>
</div> </div>
</div> </div>
@ -174,7 +174,12 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
loading={loading} loading={loading}
dataSource={filteredLogs} dataSource={filteredLogs}
columns={columns} columns={columns}
pagination={false} pagination={{
pageSize: 20,
showSizeChanger: true,
pageSizeOptions: [20, 50, 100],
showTotal: (total) => `${total}`,
}}
expandable={{ expandable={{
expandedRowKeys: expandedLog ? [expandedLog] : [], expandedRowKeys: expandedLog ? [expandedLog] : [],
expandIcon: () => null, expandIcon: () => null,

View File

@ -89,26 +89,49 @@ export default function EvalOverview({ ev, targetName }: EvalOverviewProps) {
</Card> </Card>
<Card size="small" title="用户输入" style={sectionCard}> <Card size="small" title="用户输入" style={sectionCard}>
<Descriptions column={1} size="small"> <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Descriptions.Item label="评估目标">{ev.goal || '—'}</Descriptions.Item> <div>
<Descriptions.Item label="考察意图">{ev.intent || '—'}</Descriptions.Item> <div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}></div>
<Descriptions.Item label="角色描述"> <div style={{
<span style={{ whiteSpace: 'pre-wrap' }}>{ev.role_description || '—'}</span> fontSize: 13, padding: '8px 12px', background: colors.bgSubtle,
</Descriptions.Item> borderRadius: 6, whiteSpace: 'pre-wrap', maxHeight: 120, overflowY: 'auto',
<Descriptions.Item label="种子集"> }}>
{Object.keys(ev.seeds ?? {}).length === 0 {ev.goal || '—'}
? '—' </div>
: ( </div>
<div>
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}></div>
<div style={{
fontSize: 13, padding: '8px 12px', background: colors.bgSubtle,
borderRadius: 6, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto',
}}>
{ev.intent || '—'}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}></div>
<div style={{
fontSize: 13, padding: '8px 12px', background: colors.bgSubtle,
borderRadius: 6, whiteSpace: 'pre-wrap', maxHeight: 100, overflowY: 'auto',
}}>
{ev.role_description || '—'}
</div>
</div>
<div>
<div style={{ fontSize: 12, color: colors.textSecondary, marginBottom: 4 }}></div>
{Object.keys(ev.seeds ?? {}).length === 0 ? (
<div style={{ fontSize: 13, padding: '8px 12px', background: colors.bgSubtle, borderRadius: 6 }}></div>
) : (
<pre style={{ <pre style={{
margin: 0, fontSize: 12, background: colors.bgSubtle, margin: 0, fontSize: 12, background: colors.bgSubtle,
padding: 8, borderRadius: 6, overflowX: 'auto', padding: '8px 12px', borderRadius: 6, overflowX: 'auto',
}} maxHeight: 160, overflowY: 'auto',
> }}>
{JSON.stringify(ev.seeds, null, 2)} {JSON.stringify(ev.seeds, null, 2)}
</pre> </pre>
)} )}
</Descriptions.Item> </div>
</Descriptions> </div>
</Card> </Card>
</Col> </Col>

View File

@ -110,13 +110,13 @@ describe('ExecutionProcess', () => {
await waitFor(() => expect(screen.getByText('评估已取消')).toBeInTheDocument()) await waitFor(() => expect(screen.getByText('评估已取消')).toBeInTheDocument())
}) })
it('caps the activity feed at 50 entries and hints at the decision tab', async () => { it('shows all activity entries with total count', async () => {
mockApi({}, 60) mockApi({}, 60)
render(<ExecutionProcess ev={makeEval()} />) render(<ExecutionProcess ev={makeEval()} />)
await waitFor(() => expect(screen.getByText(/仅展示最近 50 条/)).toBeInTheDocument()) await waitFor(() => expect(screen.getByText(/共 \d+ 条活动/)).toBeInTheDocument())
expect(screen.getByText(/决策·执行会话:时段欠账 59/)).toBeInTheDocument() expect(screen.getByText(/决策·执行会话:时段欠账 59/)).toBeInTheDocument()
expect(screen.queryByText(/决策·执行会话:时段欠账 0/)).not.toBeInTheDocument() expect(screen.getByText(/决策·执行会话:时段欠账 0/)).toBeInTheDocument()
}) })
it('scopes task loading to the current evaluation', async () => { it('scopes task loading to the current evaluation', async () => {

View File

@ -13,7 +13,6 @@ import { colors, statusColors } from '../../tokens'
import { formatDateTime } from '../../utils/date' import { formatDateTime } from '../../utils/date'
import { usePolling } from '../../hooks/usePolling' import { usePolling } from '../../hooks/usePolling'
const ACTIVITY_LIMIT = 50
const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing']) const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing'])
const STAGE_STEPS = [ const STAGE_STEPS = [
@ -123,7 +122,7 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
const activities = useMemo(() => { const activities = useMemo(() => {
const all = [...sessionEvents(ev), ...taskEvents(tasks), ...decisionEvents(logs)] const all = [...sessionEvents(ev), ...taskEvents(tasks), ...decisionEvents(logs)]
all.sort((a, b) => (a.time < b.time ? 1 : -1)) all.sort((a, b) => (a.time < b.time ? 1 : -1))
return all.slice(0, ACTIVITY_LIMIT) return all
}, [ev, tasks, logs]) }, [ev, tasks, logs])
if (loading && progress == null) { if (loading && progress == null) {
@ -151,7 +150,10 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
seriesField: 'kind', seriesField: 'kind',
group: true, group: true,
height: Math.max(200, progress.slots.length * 72), height: Math.max(200, progress.slots.length * 72),
color: [colors.textMuted, statusColors.running, statusColors.completed], color: [colors.textMuted, colors.primary, statusColors.completed],
legend: { position: 'top' },
xAxis: { label: { autoRotate: true } },
yAxis: { min: 0 },
} }
return ( return (
@ -160,9 +162,10 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
<Steps <Steps
current={stageIndex < 0 ? 0 : stageIndex} current={stageIndex < 0 ? 0 : stageIndex}
status={abnormal ? 'error' : progress.current_stage === 'done' ? 'finish' : 'process'} status={abnormal ? 'error' : progress.current_stage === 'done' ? 'finish' : 'process'}
size="small"
items={STAGE_STEPS.map((s) => ({ items={STAGE_STEPS.map((s) => ({
title: s.title, title: s.title,
status: undefined, description: s.key === progress.current_stage ? '当前阶段' : undefined,
}))} }))}
/> />
{progress.blocker != null && ( {progress.blocker != null && (
@ -201,14 +204,14 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
color: a.color, color: a.color,
children: ( children: (
<div> <div>
<div style={{ fontSize: 13 }}>{a.text}</div> <div style={{ fontSize: 13, lineHeight: 1.5 }}>{a.text}</div>
<div style={{ fontSize: 11, color: colors.textSecondary }}>{formatDateTime(a.time)}</div> <div style={{ fontSize: 12, color: colors.textSecondary }}>{formatDateTime(a.time)}</div>
</div> </div>
), ),
}))} }))}
/> />
<div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}> <div style={{ fontSize: 12, color: colors.textMuted, marginTop: 8 }}>
{`仅展示最近 ${ACTIVITY_LIMIT},完整决策历史见「决策过程」`} {`${activities.length} 条活动,完整决策历史见「决策过程」`}
</div> </div>
</> </>
)} )}

View File

@ -160,13 +160,18 @@ export default function IntelligentEvalsPage() {
}, },
}, },
{ {
title: '会话进度', key: 'progress', width: 170, title: '会话进度', key: 'progress', width: 200,
render: (_, ev) => { render: (_, ev) => {
const pct = ev.session_count ? Math.round((ev.completed_sessions / ev.session_count) * 100) : 0 const estimated = ev.plan?.estimated_sessions ?? ev.session_count
const running = (ev.sessions ?? []).filter((s) => s.status === 'running').length
const pct = estimated ? Math.round((ev.completed_sessions / estimated) * 100) : 0
return ( return (
<Space size={6}> <Space size={6}>
<Progress percent={pct} size="small" style={{ width: 90 }} /> <Progress percent={pct} size="small" style={{ width: 90 }} />
<span style={{ fontSize: 12, color: colors.textSecondary }}>{ev.completed_sessions}/{ev.session_count}</span> <span style={{ fontSize: 12, color: colors.textSecondary }}>
{ev.completed_sessions}/{estimated}
{running > 0 && <span style={{ color: statusColors.running, marginLeft: 4 }}>({running} )</span>}
</span>
</Space> </Space>
) )
}, },