AgentEvalTool/frontend/web/src/components/intelligent_eval/TaskQueueMonitor.tsx
sinohqb 57951f7940
All checks were successful
CI / test (push) Successful in 3m58s
fix(intelligent-eval): task queue pagination + column widths align with eval list
任务队列页对齐评估列表:
- 分页:始终显示(pageSize 10 + showSizeChanger + showTotal),此前仅在
  >10 条时显示导致 t480(8 条)无分页器
- 列宽重新分配:评估列 230→300(长名称不截断)、原因列固定 240(ellipsis)、
  入队/完成时间 170、队列状态 100、优先级 80
- 字体格式对齐评估列表:去掉 size="middle" 改用默认(与评估列表一致)
tsc 0 错误 vitest 16 passed
2026-08-18 10:35:51 +08:00

150 lines
5.8 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 { useEffect, useState } from 'react'
import {
Button, Empty, Space, Table, Tag, Tooltip, message,
} from 'antd'
import type { ColumnsType } from 'antd/es/table'
import { ReloadOutlined } from '@ant-design/icons'
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
import { usePolling } from '../../hooks/usePolling'
import { colors, statusColors } from '../../tokens'
import { formatDateTime } from '../../utils/date'
import { EVAL_STATUS } from './status'
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
pending: { label: '待认领', color: 'orange' },
assigned: { label: '执行中', color: 'blue' },
completed: { label: '已完成', color: 'green' },
failed: { label: '失败', color: 'red' },
}
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
/**
* 任务队列监控(方案③可视化,与评估列表页统一 stat-chip + 单行表格)。
*
* 平台每 60s 扫描 executing 评估入队 + 触发 OpenClaw worker 执行。本组件
* 展示任务明细与状态分布5s 轮询;统计条点击筛选。
*/
export default function TaskQueueMonitor() {
const [tasks, setTasks] = useState<TaskQueueItem[] | null>(null)
const [stats, setStats] = useState<{
pending: number; assigned: number; completed: number; failed: number; unresolved: number
} | null>(null)
const [filter, setFilter] = useState<FilterKey>('all')
const [loading, setLoading] = useState(false)
const loadData = async () => {
setLoading(true)
try {
const res = await intelligentEvalsApi.listTasks(
filter === 'all' || filter === 'unresolved' ? undefined : { status: filter },
)
setTasks(res.data.tasks)
setStats(res.data.stats)
} catch {
message.error('加载任务队列失败')
} finally {
setLoading(false)
}
}
// Initial fetch + reload on filter change (usePolling owns the 5s interval).
useEffect(() => {
void loadData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter])
usePolling(() => { void loadData() }, 5000, true)
const chipMeta: { key: FilterKey; label: string; color?: string; get: () => number }[] = [
{ key: 'all', label: '全部任务', get: () => (stats ? Object.values(stats).reduce((a, b) => a + b, 0) : (tasks?.length ?? 0)) },
{ key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 },
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
{ key: 'completed', label: '已完成', color: statusColors.completed, get: () => stats?.completed ?? 0 },
{ key: 'failed', label: '失败', color: statusColors.failed, get: () => stats?.failed ?? 0 },
]
const columns: ColumnsType<TaskQueueItem> = [
{
title: '入队时间', dataIndex: 'created_at', key: 'created_at', width: 170,
render: (v: string | null) => (v ? formatDateTime(v) : '—'),
},
{
title: '评估', key: 'eval', width: 300, ellipsis: true,
render: (_, t) => (
<Space size={6}>
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
{t.eval_status && EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS] && (
<Tag color={EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].color} style={{ margin: 0, fontSize: 11 }}>
{EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].label}
</Tag>
)}
</Space>
),
},
{
title: '队列状态', dataIndex: 'status', key: 'status', width: 100,
render: (s: TaskQueueStatus) => {
const meta = STATUS_META[s]
return <Tag color={meta.color}>{meta.label}</Tag>
},
},
{
title: '优先级', dataIndex: 'priority', key: 'priority', width: 80,
render: (p: number) => <Tag>{p}</Tag>,
},
{
title: '原因', dataIndex: 'reason', key: 'reason', width: 240, ellipsis: true,
render: (r: string) => <Tooltip title={r}><span>{r}</span></Tooltip>,
},
{
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 170,
render: (v: string | null, t) => {
if (t.status === 'failed' && t.error) {
return <Tooltip title={t.error}><span style={{ color: statusColors.failed }}></span></Tooltip>
}
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}></span>
},
},
]
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div className="stat-bar">
{chipMeta.map((c) => (
<div
key={c.key}
className={`stat-chip ${filter === c.key ? 'active' : ''}`}
onClick={() => setFilter(c.key)}
>
<div className="n" style={{ color: c.color ?? colors.text }}>{c.get()}</div>
<div className="l">{c.label}</div>
</div>
))}
</div>
<div style={{ background: colors.bgContainer, borderRadius: 8, padding: 16, flex: 1, minHeight: 0, overflow: 'hidden' }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 12 }}>
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} />
</div>
<div style={{ height: 'calc(100% - 44px)', overflowY: 'auto' }}>
<Table
rowKey="id"
loading={loading}
dataSource={tasks ?? []}
columns={columns}
pagination={{
pageSize: 10,
showSizeChanger: true,
pageSizeOptions: [10, 20, 50],
showTotal: (t) => `${t}`,
}}
locale={{ emptyText: <Empty description="暂无任务" /> }}
/>
</div>
</div>
</div>
)
}