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:
parent
d0487b54b4
commit
60981b634d
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user