Compare commits
3 Commits
00ad929d68
...
47804f2df3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47804f2df3 | ||
|
|
fa5e3b8d5d | ||
|
|
3626ab288b |
@ -9,13 +9,7 @@ import {
|
|||||||
} from '../api'
|
} from '../api'
|
||||||
import { colors } from '../tokens'
|
import { colors } from '../tokens'
|
||||||
import { shortDateTime } from '../utils/date'
|
import { shortDateTime } from '../utils/date'
|
||||||
|
import { SESSION_STATUS } from './intelligent_eval/status'
|
||||||
const SESSION_STATUS: Record<string, { label: string; color: string }> = {
|
|
||||||
running: { label: '进行中', color: 'processing' },
|
|
||||||
completed: { label: '已完成', color: 'success' },
|
|
||||||
failed: { label: '失败', color: 'error' },
|
|
||||||
expired: { label: '已过期', color: 'default' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const EMOTION_LABELS: Record<string, string> = {
|
const EMOTION_LABELS: Record<string, string> = {
|
||||||
positive: '满意',
|
positive: '满意',
|
||||||
@ -201,7 +195,7 @@ export default function ExplorationSection({ campaignId, summary }: {
|
|||||||
size="small"
|
size="small"
|
||||||
onChange={onExpand}
|
onChange={onExpand}
|
||||||
items={sessions.map((s) => {
|
items={sessions.map((s) => {
|
||||||
const meta = SESSION_STATUS[s.status] ?? SESSION_STATUS.running
|
const meta = SESSION_STATUS[s.status as keyof typeof SESSION_STATUS] ?? SESSION_STATUS.running
|
||||||
const personaName = typeof s.persona?.name === 'string' ? s.persona.name : s.id.slice(0, 8)
|
const personaName = typeof s.persona?.name === 'string' ? s.persona.name : s.id.slice(0, 8)
|
||||||
return {
|
return {
|
||||||
key: s.id,
|
key: s.id,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
type MetricDiff, type ModelConfig,
|
type MetricDiff, type ModelConfig,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { shortDateTime } from '../utils/date'
|
import { shortDateTime } from '../utils/date'
|
||||||
import { colors } from '../tokens'
|
import { colors, statusColors } from '../tokens'
|
||||||
|
|
||||||
const TREND_META: Record<string, { label: string; color: string }> = {
|
const TREND_META: Record<string, { label: string; color: string }> = {
|
||||||
improving: { label: '改善', color: 'green' },
|
improving: { label: '改善', color: 'green' },
|
||||||
@ -119,7 +119,7 @@ export default function PeriodComparisonSection({
|
|||||||
const good = metric.goodDirection === 'down' ? (pair.delta ?? 0) < 0 : (pair.delta ?? 0) > 0
|
const good = metric.goodDirection === 'down' ? (pair.delta ?? 0) < 0 : (pair.delta ?? 0) > 0
|
||||||
const deltaColor = pair.delta == null || pair.delta === 0
|
const deltaColor = pair.delta == null || pair.delta === 0
|
||||||
? colors.textMuted
|
? colors.textMuted
|
||||||
: good ? '#52c41a' : '#ff4d4f'
|
: good ? statusColors.completed : statusColors.failed
|
||||||
return (
|
return (
|
||||||
<Space size={4}>
|
<Space size={4}>
|
||||||
<span style={{ color: colors.textMuted }}>
|
<span style={{ color: colors.textMuted }}>
|
||||||
|
|||||||
@ -143,7 +143,7 @@ function ratioColor(pass: number, total: number): string {
|
|||||||
if (!total) return colors.textMuted
|
if (!total) return colors.textMuted
|
||||||
const r = pass / total
|
const r = pass / total
|
||||||
if (r >= 0.8) return statusColors.completed
|
if (r >= 0.8) return statusColors.completed
|
||||||
if (r >= 0.5) return '#faad14'
|
if (r >= 0.5) return colors.warning
|
||||||
return statusColors.failed
|
return statusColors.failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -233,7 +233,7 @@ function RunRow({ r, selected, targetName, scenarioName, onSelect, onOpenReport,
|
|||||||
const passRate = r.summary?.pass_rate
|
const passRate = r.summary?.pass_rate
|
||||||
if (passRate != null) {
|
if (passRate != null) {
|
||||||
const pct = Math.round(passRate * 100)
|
const pct = Math.round(passRate * 100)
|
||||||
const color = pct >= 80 ? statusColors.completed : pct >= 50 ? '#faad14' : statusColors.failed
|
const color = pct >= 80 ? statusColors.completed : pct >= 50 ? colors.warning : statusColors.failed
|
||||||
return { pct, color, label: `${pct}%`, active: false }
|
return { pct, color, label: `${pct}%`, active: false }
|
||||||
}
|
}
|
||||||
return { pct: 100, color: colors.textMuted, label: '完成', active: false }
|
return { pct: 100, color: colors.textMuted, label: '完成', active: false }
|
||||||
|
|||||||
29
frontend/web/src/components/SectionHeader.tsx
Normal file
29
frontend/web/src/components/SectionHeader.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import { Button } from 'antd'
|
||||||
|
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||||
|
import type { ReactNode } from 'react'
|
||||||
|
import { colors } from '../tokens'
|
||||||
|
|
||||||
|
interface SectionHeaderProps {
|
||||||
|
title: ReactNode
|
||||||
|
/** 提供时显示"返回"按钮(子视图/抽屉 tab 环境可省)。 */
|
||||||
|
onBack?: () => void
|
||||||
|
/** 返回按钮文案(默认"返回")。 */
|
||||||
|
backLabel?: string
|
||||||
|
/** 右侧操作区(筛选/刷新/导出等)。 */
|
||||||
|
actions?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 内容区子视图的标准横排头部:可选返回按钮 + 标题 + 右侧操作区。
|
||||||
|
* 供智能评估等 Drawer/Tabs 内容组件复用,消除各自手写 header 的重复。
|
||||||
|
*/
|
||||||
|
export default function SectionHeader({ title, onBack, backLabel = '返回', actions }: SectionHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||||
|
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>{backLabel}</Button>}
|
||||||
|
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>{title}</span>
|
||||||
|
<div style={{ flex: 1 }} />
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Button, Card, Descriptions, Empty, Space, Table, Tag, message,
|
Button, Card, Descriptions, Empty, Space, Table, Tag, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { ArrowLeftOutlined, DiffOutlined } from '@ant-design/icons'
|
import { ArrowLeftOutlined, DiffOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import SectionHeader from '../SectionHeader'
|
||||||
import { intelligentEvalsApi, type ConfigSnapshot, type ConfigSnapshotComparison } from '../../api'
|
import { intelligentEvalsApi, type ConfigSnapshot, type ConfigSnapshotComparison } from '../../api'
|
||||||
import { colors } from '../../tokens'
|
import { colors } from '../../tokens'
|
||||||
import { formatDateTime } from '../../utils/date'
|
import { formatDateTime } from '../../utils/date'
|
||||||
@ -40,9 +41,10 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useState(() => {
|
useEffect(() => {
|
||||||
void loadSnapshots()
|
if (evalId) void loadSnapshots()
|
||||||
})
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [evalId])
|
||||||
|
|
||||||
const handleViewDetail = async (snapshot: ConfigSnapshot) => {
|
const handleViewDetail = async (snapshot: ConfigSnapshot) => {
|
||||||
try {
|
try {
|
||||||
@ -181,10 +183,7 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
|
|||||||
if (compareMode && comparison) {
|
if (compareMode && comparison) {
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
<SectionHeader title="快照对比" onBack={() => setCompareMode(false)} />
|
||||||
<Button icon={<ArrowLeftOutlined />} onClick={() => setCompareMode(false)}>返回</Button>
|
|
||||||
<span style={{ fontSize: 16, fontWeight: 600 }}>快照对比</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card size="small" title="对比信息" style={{ marginBottom: 16 }}>
|
<Card size="small" title="对比信息" style={{ marginBottom: 16 }}>
|
||||||
<Descriptions column={2} size="small">
|
<Descriptions column={2} size="small">
|
||||||
@ -240,18 +239,22 @@ export default function ConfigSnapshots({ evalId, onBack }: ConfigSnapshotsProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
<SectionHeader
|
||||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>}
|
title="配置历史"
|
||||||
<span style={{ fontSize: 16, fontWeight: 600 }}>配置历史</span>
|
onBack={onBack}
|
||||||
<div style={{ flex: 1 }} />
|
actions={(
|
||||||
<Button
|
<>
|
||||||
icon={<DiffOutlined />}
|
<Button icon={<ReloadOutlined />} onClick={() => void loadSnapshots()}>刷新</Button>
|
||||||
disabled={selectedForCompare.length !== 2}
|
<Button
|
||||||
onClick={handleCompare}
|
icon={<DiffOutlined />}
|
||||||
>
|
disabled={selectedForCompare.length !== 2}
|
||||||
对比选中
|
onClick={handleCompare}
|
||||||
</Button>
|
>
|
||||||
</div>
|
对比选中
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import {
|
import {
|
||||||
Button, Card, Empty, Select, Table, Tag, Timeline, message,
|
Button, Card, Empty, Select, Table, Tag, Timeline, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
import { ArrowLeftOutlined, DownloadOutlined } from '@ant-design/icons'
|
import { DownloadOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||||
|
import SectionHeader from '../SectionHeader'
|
||||||
import { intelligentEvalsApi, type DecisionLog } from '../../api'
|
import { intelligentEvalsApi, type DecisionLog } from '../../api'
|
||||||
import { colors } from '../../tokens'
|
import { colors } from '../../tokens'
|
||||||
import { formatDateTime } from '../../utils/date'
|
import { formatDateTime } from '../../utils/date'
|
||||||
@ -38,9 +39,10 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useState(() => {
|
useEffect(() => {
|
||||||
void loadLogs()
|
if (evalId) void loadLogs()
|
||||||
})
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [evalId])
|
||||||
|
|
||||||
const handleExport = () => {
|
const handleExport = () => {
|
||||||
if (!logs) return
|
if (!logs) return
|
||||||
@ -105,23 +107,27 @@ export default function DecisionProcess({ evalId, onBack }: DecisionProcessProps
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
<SectionHeader
|
||||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回</Button>}
|
title="决策过程"
|
||||||
<span style={{ fontSize: 16, fontWeight: 600 }}>决策过程</span>
|
onBack={onBack}
|
||||||
<div style={{ flex: 1 }} />
|
actions={(
|
||||||
<Select
|
<>
|
||||||
placeholder="筛选决策类型"
|
<Select
|
||||||
allowClear
|
placeholder="筛选决策类型"
|
||||||
style={{ width: 160 }}
|
allowClear
|
||||||
onChange={(val) => setFilterType(val ?? null)}
|
style={{ width: 160 }}
|
||||||
options={[
|
onChange={(val) => setFilterType(val ?? null)}
|
||||||
{ label: '执行会话', value: 'execute_session' },
|
options={[
|
||||||
{ label: '等待', value: 'wait' },
|
{ label: '执行会话', value: 'execute_session' },
|
||||||
{ label: '开始分析', value: 'start_analysis' },
|
{ label: '等待', value: 'wait' },
|
||||||
]}
|
{ label: '开始分析', value: 'start_analysis' },
|
||||||
/>
|
]}
|
||||||
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
/>
|
||||||
</div>
|
<Button icon={<ReloadOutlined />} onClick={() => void loadLogs()}>刷新</Button>
|
||||||
|
<Button icon={<DownloadOutlined />} onClick={handleExport}>导出</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Card size="small" title="决策时间线" style={{ marginBottom: 16 }}>
|
<Card size="small" title="决策时间线" style={{ marginBottom: 16 }}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|||||||
@ -3,11 +3,11 @@ import {
|
|||||||
Button, Card, Col, Collapse, Empty, Row, Space, Spin, Tag, message,
|
Button, Card, Col, Collapse, Empty, Row, Space, Spin, Tag, message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
ArrowLeftOutlined, BulbOutlined, DownloadOutlined, MessageOutlined,
|
BulbOutlined, DownloadOutlined, MessageOutlined, StarOutlined, WarningOutlined,
|
||||||
StarOutlined, WarningOutlined,
|
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { Bar } from '@ant-design/charts'
|
import { Bar } from '@ant-design/charts'
|
||||||
import ChatBubble from '../ChatBubble'
|
import ChatBubble from '../ChatBubble'
|
||||||
|
import SectionHeader from '../SectionHeader'
|
||||||
import {
|
import {
|
||||||
intelligentEvalsApi,
|
intelligentEvalsApi,
|
||||||
type IntelligentEval,
|
type IntelligentEval,
|
||||||
@ -16,7 +16,7 @@ import {
|
|||||||
type IntelligentEvalSession,
|
type IntelligentEvalSession,
|
||||||
type ReportEvidence,
|
type ReportEvidence,
|
||||||
} from '../../api'
|
} from '../../api'
|
||||||
import { colors } from '../../tokens'
|
import { colors, statusColors } from '../../tokens'
|
||||||
import { shortDateTime } from '../../utils/date'
|
import { shortDateTime } from '../../utils/date'
|
||||||
import { SESSION_STATUS, severityOf } from './status'
|
import { SESSION_STATUS, severityOf } from './status'
|
||||||
|
|
||||||
@ -143,8 +143,8 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
|||||||
: null
|
: null
|
||||||
const overallNum = overall != null ? Number(overall) : null
|
const overallNum = overall != null ? Number(overall) : null
|
||||||
const overallColor = overallNum == null ? colors.textMuted
|
const overallColor = overallNum == null ? colors.textMuted
|
||||||
: overallNum >= 0.8 ? '#52c41a'
|
: overallNum >= 0.8 ? statusColors.completed
|
||||||
: overallNum >= 0.6 ? colors.warning : '#ff4d4f'
|
: overallNum >= 0.6 ? colors.warning : statusColors.failed
|
||||||
const sevCounts = findings.reduce((acc, f) => {
|
const sevCounts = findings.reduce((acc, f) => {
|
||||||
acc[f.severity] = (acc[f.severity] ?? 0) + 1
|
acc[f.severity] = (acc[f.severity] ?? 0) + 1
|
||||||
return acc
|
return acc
|
||||||
@ -175,18 +175,16 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
<SectionHeader
|
||||||
{onBack && <Button icon={<ArrowLeftOutlined />} onClick={onBack}>返回详情</Button>}
|
title={`${ev.name} · 评估报告`}
|
||||||
<span style={{ fontSize: 16, fontWeight: 600, color: colors.text }}>
|
onBack={onBack}
|
||||||
{ev.name} · 评估报告
|
backLabel="返回详情"
|
||||||
</span>
|
actions={report && (
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
{report && (
|
|
||||||
<Button icon={<DownloadOutlined />} loading={exporting} onClick={exportMarkdown}>
|
<Button icon={<DownloadOutlined />} loading={exporting} onClick={exportMarkdown}>
|
||||||
导出 Markdown
|
导出 Markdown
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
{reportLoading && <Spin />}
|
{reportLoading && <Spin />}
|
||||||
|
|
||||||
@ -210,7 +208,7 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) {
|
|||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} md={6}>
|
<Col xs={12} md={6}>
|
||||||
<div style={summaryCard}>
|
<div style={summaryCard}>
|
||||||
<div style={summaryIcon('#ff4d4f')}><WarningOutlined /></div>
|
<div style={summaryIcon(statusColors.failed)}><WarningOutlined /></div>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: 12, color: colors.textMuted }}>问题发现</div>
|
<div style={{ fontSize: 12, color: colors.textMuted }}>问题发现</div>
|
||||||
<div style={{ fontSize: 26, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
|
<div style={{ fontSize: 26, fontWeight: 700, color: colors.text, lineHeight: 1.2 }}>
|
||||||
|
|||||||
@ -6,8 +6,9 @@ import type { ColumnsType } from 'antd/es/table'
|
|||||||
import { ReloadOutlined } from '@ant-design/icons'
|
import { ReloadOutlined } from '@ant-design/icons'
|
||||||
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
|
import { intelligentEvalsApi, type TaskQueueItem, type TaskQueueStatus } from '../../api'
|
||||||
import { usePolling } from '../../hooks/usePolling'
|
import { usePolling } from '../../hooks/usePolling'
|
||||||
import { colors } from '../../tokens'
|
import { colors, statusColors } from '../../tokens'
|
||||||
import { formatDateTime } from '../../utils/date'
|
import { formatDateTime } from '../../utils/date'
|
||||||
|
import { EVAL_STATUS } from './status'
|
||||||
|
|
||||||
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
||||||
pending: { label: '待认领', color: 'orange' },
|
pending: { label: '待认领', color: 'orange' },
|
||||||
@ -16,13 +17,6 @@ const STATUS_META: Record<TaskQueueStatus, { label: string; color: string }> = {
|
|||||||
failed: { label: '失败', color: 'red' },
|
failed: { label: '失败', color: 'red' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const EVAL_STATUS_META: Record<string, { label: string; color: string }> = {
|
|
||||||
executing: { label: '执行中', color: 'blue' },
|
|
||||||
completed: { label: '已完成', color: 'green' },
|
|
||||||
cancelled: { label: '已取消', color: 'default' },
|
|
||||||
failed: { label: '失败', color: 'red' },
|
|
||||||
}
|
|
||||||
|
|
||||||
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
|
type FilterKey = 'all' | 'unresolved' | TaskQueueStatus
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -67,8 +61,8 @@ export default function TaskQueueMonitor() {
|
|||||||
{ key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 },
|
{ key: 'unresolved', label: '待处理', color: colors.warning, get: () => stats?.unresolved ?? 0 },
|
||||||
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
|
{ key: 'pending', label: '待认领', get: () => stats?.pending ?? 0 },
|
||||||
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
|
{ key: 'assigned', label: '执行中', color: colors.primary, get: () => stats?.assigned ?? 0 },
|
||||||
{ key: 'completed', label: '已完成', color: '#52c41a', get: () => stats?.completed ?? 0 },
|
{ key: 'completed', label: '已完成', color: statusColors.completed, get: () => stats?.completed ?? 0 },
|
||||||
{ key: 'failed', label: '失败', color: '#ff4d4f', get: () => stats?.failed ?? 0 },
|
{ key: 'failed', label: '失败', color: statusColors.failed, get: () => stats?.failed ?? 0 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const columns: ColumnsType<TaskQueueItem> = [
|
const columns: ColumnsType<TaskQueueItem> = [
|
||||||
@ -81,9 +75,9 @@ export default function TaskQueueMonitor() {
|
|||||||
render: (_, t) => (
|
render: (_, t) => (
|
||||||
<Space size={6}>
|
<Space size={6}>
|
||||||
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
|
<span style={{ fontWeight: 500 }}>{t.eval_name ?? t.eval_id.slice(0, 8)}</span>
|
||||||
{t.eval_status && EVAL_STATUS_META[t.eval_status] && (
|
{t.eval_status && EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS] && (
|
||||||
<Tag color={EVAL_STATUS_META[t.eval_status].color} style={{ margin: 0, fontSize: 11 }}>
|
<Tag color={EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].color} style={{ margin: 0, fontSize: 11 }}>
|
||||||
{EVAL_STATUS_META[t.eval_status].label}
|
{EVAL_STATUS[t.eval_status as keyof typeof EVAL_STATUS].label}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
@ -108,7 +102,7 @@ export default function TaskQueueMonitor() {
|
|||||||
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 165,
|
title: '完成时间', dataIndex: 'completed_at', key: 'completed_at', width: 165,
|
||||||
render: (v: string | null, t) => {
|
render: (v: string | null, t) => {
|
||||||
if (t.status === 'failed' && t.error) {
|
if (t.status === 'failed' && t.error) {
|
||||||
return <Tooltip title={t.error}><span style={{ color: '#ff4d4f' }}>失败</span></Tooltip>
|
return <Tooltip title={t.error}><span style={{ color: statusColors.failed }}>失败</span></Tooltip>
|
||||||
}
|
}
|
||||||
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}>—</span>
|
return v ? formatDateTime(v) : <span style={{ color: colors.textMuted }}>—</span>
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,13 +6,14 @@ import {
|
|||||||
type FileUploadConfig,
|
type FileUploadConfig,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { categoryContains, findCategory } from '../utils/fileTree'
|
import { categoryContains, findCategory } from '../utils/fileTree'
|
||||||
|
import { useOnTabActive } from './useOnTabActive'
|
||||||
|
|
||||||
const DEFAULT_CONFIG: FileUploadConfig = {
|
const DEFAULT_CONFIG: FileUploadConfig = {
|
||||||
allowed_extensions: [],
|
allowed_extensions: [],
|
||||||
max_upload_size_mb: 50,
|
max_upload_size_mb: 50,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useFiles() {
|
export function useFiles(tabPath?: string) {
|
||||||
const [categories, setCategories] = useState<FileCategory[]>([])
|
const [categories, setCategories] = useState<FileCategory[]>([])
|
||||||
const [files, setFiles] = useState<FileRecord[]>([])
|
const [files, setFiles] = useState<FileRecord[]>([])
|
||||||
const [config, setConfig] = useState<FileUploadConfig>(DEFAULT_CONFIG)
|
const [config, setConfig] = useState<FileUploadConfig>(DEFAULT_CONFIG)
|
||||||
@ -46,6 +47,11 @@ export function useFiles() {
|
|||||||
void Promise.allSettled([loadCategories(), loadFiles(), loadConfig()])
|
void Promise.allSettled([loadCategories(), loadFiles(), loadConfig()])
|
||||||
}, [loadCategories, loadConfig, loadFiles])
|
}, [loadCategories, loadConfig, loadFiles])
|
||||||
|
|
||||||
|
// keep-alive 下 tab 重新激活时刷新(跳走再回来数据不过期)
|
||||||
|
useOnTabActive(tabPath ?? '', () => {
|
||||||
|
if (tabPath) void refresh()
|
||||||
|
})
|
||||||
|
|
||||||
const selectCategory = useCallback((categoryId: string | null) => {
|
const selectCategory = useCallback((categoryId: string | null) => {
|
||||||
setSelectedCategoryId(categoryId)
|
setSelectedCategoryId(categoryId)
|
||||||
void loadFiles(categoryId)
|
void loadFiles(categoryId)
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import {
|
|||||||
sessionReducer,
|
sessionReducer,
|
||||||
type WsEvent,
|
type WsEvent,
|
||||||
} from './sessionReducer'
|
} from './sessionReducer'
|
||||||
|
import { usePolling } from './usePolling'
|
||||||
|
|
||||||
// WebSocket reconnect config
|
// WebSocket reconnect config
|
||||||
const WS_MAX_RETRIES = 5
|
const WS_MAX_RETRIES = 5
|
||||||
@ -81,18 +82,10 @@ export function useRunSession(): RunSession {
|
|||||||
const [state, dispatch] = useReducer(sessionReducer, initialSessionState)
|
const [state, dispatch] = useReducer(sessionReducer, initialSessionState)
|
||||||
|
|
||||||
const wsRef = useRef<WebSocket | null>(null)
|
const wsRef = useRef<WebSocket | null>(null)
|
||||||
const pollRef = useRef<number | null>(null)
|
|
||||||
const selectedIdRef = useRef<string | null>(null)
|
const selectedIdRef = useRef<string | null>(null)
|
||||||
const reconnectTimerRef = useRef<number | null>(null)
|
const reconnectTimerRef = useRef<number | null>(null)
|
||||||
const reconnectCountRef = useRef<number>(0)
|
const reconnectCountRef = useRef<number>(0)
|
||||||
|
|
||||||
const clearPolling = () => {
|
|
||||||
if (pollRef.current) {
|
|
||||||
window.clearInterval(pollRef.current)
|
|
||||||
pollRef.current = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearReconnect = () => {
|
const clearReconnect = () => {
|
||||||
if (reconnectTimerRef.current) {
|
if (reconnectTimerRef.current) {
|
||||||
window.clearTimeout(reconnectTimerRef.current)
|
window.clearTimeout(reconnectTimerRef.current)
|
||||||
@ -174,7 +167,6 @@ export function useRunSession(): RunSession {
|
|||||||
|
|
||||||
const select = useCallback((r: Run | null, opts?: { live?: boolean }) => {
|
const select = useCallback((r: Run | null, opts?: { live?: boolean }) => {
|
||||||
closeWs()
|
closeWs()
|
||||||
clearPolling()
|
|
||||||
dispatch({ type: 'RESET' })
|
dispatch({ type: 'RESET' })
|
||||||
setRun(r)
|
setRun(r)
|
||||||
selectedIdRef.current = r?.id ?? null
|
selectedIdRef.current = r?.id ?? null
|
||||||
@ -250,21 +242,17 @@ export function useRunSession(): RunSession {
|
|||||||
} catch { /* noop */ }
|
} catch { /* noop */ }
|
||||||
}, [run])
|
}, [run])
|
||||||
|
|
||||||
// Poll during live runs to keep the Run object fresh (status/summary/completed_at)
|
// Poll during live runs to keep the Run object fresh (status/summary/completed_at).
|
||||||
useEffect(() => {
|
// usePolling 统一轮询:pauseWhenHidden 默认开启,后台标签页暂停、回到前台立即刷新。
|
||||||
if (!state.isLive || !run) {
|
usePolling(() => {
|
||||||
clearPolling()
|
if (run && selectedIdRef.current === run.id) {
|
||||||
return
|
|
||||||
}
|
|
||||||
pollRef.current = window.setInterval(() => {
|
|
||||||
runsApi.get(run.id).then((res) => {
|
runsApi.get(run.id).then((res) => {
|
||||||
if (selectedIdRef.current === run.id) setRun(res.data)
|
if (selectedIdRef.current === run.id) setRun(res.data)
|
||||||
}).catch(() => { /* noop */ })
|
}).catch(() => { /* noop */ })
|
||||||
}, 3000)
|
}
|
||||||
return () => clearPolling()
|
}, 3000, state.isLive && run != null)
|
||||||
}, [state.isLive, run?.id])
|
|
||||||
|
|
||||||
useEffect(() => () => { closeWs(); clearPolling(); clearReconnect() }, [])
|
useEffect(() => () => { closeWs(); clearReconnect() }, [])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
run,
|
run,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import WindowTimeline, { type TimelineMarker } from '../components/WindowTimelin
|
|||||||
import CampaignRunTimeline from '../components/CampaignRunTimeline'
|
import CampaignRunTimeline from '../components/CampaignRunTimeline'
|
||||||
import PeriodComparisonSection from '../components/PeriodComparisonSection'
|
import PeriodComparisonSection from '../components/PeriodComparisonSection'
|
||||||
import ExplorationSection from '../components/ExplorationSection'
|
import ExplorationSection from '../components/ExplorationSection'
|
||||||
|
import { SEVERITY_META } from '../components/intelligent_eval/status'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
import { useCampaignReport } from '../hooks/useCampaignReport'
|
import { useCampaignReport } from '../hooks/useCampaignReport'
|
||||||
import { useTabStore } from '../stores/tabStore'
|
import { useTabStore } from '../stores/tabStore'
|
||||||
@ -39,12 +40,6 @@ const CAMPAIGN_STATUS: Record<string, { label: string; color: string }> = {
|
|||||||
failed: { label: '失败', color: 'error' },
|
failed: { label: '失败', color: 'error' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const SEVERITY_META: Record<string, { label: string; color: string }> = {
|
|
||||||
high: { label: '高', color: 'red' },
|
|
||||||
medium: { label: '中', color: 'orange' },
|
|
||||||
low: { label: '低', color: 'blue' },
|
|
||||||
}
|
|
||||||
|
|
||||||
const WINDOW_OPTIONS = [6, 12, 24, 48, 72].map((h) => ({ label: `${h} 小时`, value: h * 3600 }))
|
const WINDOW_OPTIONS = [6, 12, 24, 48, 72].map((h) => ({ label: `${h} 小时`, value: h * 3600 }))
|
||||||
|
|
||||||
const UNIT_OPTIONS = [
|
const UNIT_OPTIONS = [
|
||||||
|
|||||||
@ -1,126 +0,0 @@
|
|||||||
// @vitest-environment jsdom
|
|
||||||
import { act, cleanup, render } from '@testing-library/react'
|
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
||||||
|
|
||||||
vi.mock('../api', () => ({
|
|
||||||
openclawCronPoolApi: {
|
|
||||||
getStatus: vi.fn(),
|
|
||||||
getMetrics: vi.fn(),
|
|
||||||
getAlerts: vi.fn(),
|
|
||||||
scale: vi.fn(),
|
|
||||||
resolveAlert: vi.fn(),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
vi.mock('antd', async () => {
|
|
||||||
const actual = await vi.importActual<typeof import('antd')>('antd')
|
|
||||||
return {
|
|
||||||
...actual,
|
|
||||||
message: { error: vi.fn(), success: vi.fn(), warning: vi.fn(), info: vi.fn() },
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
import { openclawCronPoolApi } from '../api'
|
|
||||||
import CronPoolMonitor from './CronPoolMonitor'
|
|
||||||
|
|
||||||
const statusResp = {
|
|
||||||
data: { pool: { total: 5, idle: 3, busy: 2, stuck: 0, min_size: 5, max_size: 20 } },
|
|
||||||
}
|
|
||||||
const metricsResp = { data: { metrics: { pool_utilization: 0.4, task_backlog: 0, stuck_rate: 0 } } }
|
|
||||||
const alertsResp = { data: { alerts: [] } }
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
|
||||||
writable: true,
|
|
||||||
value: vi.fn().mockImplementation((query: string) => ({
|
|
||||||
matches: false,
|
|
||||||
media: query,
|
|
||||||
onchange: null,
|
|
||||||
addListener: vi.fn(),
|
|
||||||
removeListener: vi.fn(),
|
|
||||||
addEventListener: vi.fn(),
|
|
||||||
removeEventListener: vi.fn(),
|
|
||||||
dispatchEvent: vi.fn(),
|
|
||||||
})),
|
|
||||||
})
|
|
||||||
vi.useFakeTimers()
|
|
||||||
vi.mocked(openclawCronPoolApi.getStatus).mockResolvedValue(statusResp as never)
|
|
||||||
vi.mocked(openclawCronPoolApi.getMetrics).mockResolvedValue(metricsResp as never)
|
|
||||||
vi.mocked(openclawCronPoolApi.getAlerts).mockResolvedValue(alertsResp as never)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers()
|
|
||||||
cleanup()
|
|
||||||
})
|
|
||||||
|
|
||||||
async function settle() {
|
|
||||||
await act(async () => {
|
|
||||||
await Promise.resolve()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('CronPoolMonitor polling lifecycle', () => {
|
|
||||||
it('polls every 5 seconds while mounted', async () => {
|
|
||||||
render(<CronPoolMonitor />)
|
|
||||||
await settle()
|
|
||||||
expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(1)
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(5000)
|
|
||||||
})
|
|
||||||
await settle()
|
|
||||||
expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(2)
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(5000)
|
|
||||||
})
|
|
||||||
await settle()
|
|
||||||
expect(openclawCronPoolApi.getStatus).toHaveBeenCalledTimes(3)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('clears the polling interval on unmount', async () => {
|
|
||||||
const { unmount } = render(<CronPoolMonitor />)
|
|
||||||
await settle()
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(5000)
|
|
||||||
})
|
|
||||||
await settle()
|
|
||||||
const before = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
|
|
||||||
unmount()
|
|
||||||
await act(async () => {
|
|
||||||
await Promise.resolve()
|
|
||||||
await Promise.resolve()
|
|
||||||
})
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(30000)
|
|
||||||
})
|
|
||||||
await settle()
|
|
||||||
const after = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
|
|
||||||
expect(after).toBe(before)
|
|
||||||
})
|
|
||||||
it('pauses polling while the document is hidden and resumes on visible', async () => {
|
|
||||||
// Default starting state is visible.
|
|
||||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
|
|
||||||
|
|
||||||
render(<CronPoolMonitor />)
|
|
||||||
await settle()
|
|
||||||
const initial = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
|
|
||||||
expect(initial).toBeGreaterThanOrEqual(1)
|
|
||||||
|
|
||||||
// While hidden, advancing the clock must NOT trigger another poll.
|
|
||||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden')
|
|
||||||
document.dispatchEvent(new Event('visibilitychange'))
|
|
||||||
await act(async () => {
|
|
||||||
await vi.advanceTimersByTimeAsync(15000)
|
|
||||||
})
|
|
||||||
await settle()
|
|
||||||
const afterHidden = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
|
|
||||||
expect(afterHidden).toBe(initial)
|
|
||||||
|
|
||||||
// Returning to visible resumes polling and triggers an immediate reload.
|
|
||||||
vi.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible')
|
|
||||||
document.dispatchEvent(new Event('visibilitychange'))
|
|
||||||
await settle()
|
|
||||||
const afterVisible = vi.mocked(openclawCronPoolApi.getStatus).mock.calls.length
|
|
||||||
expect(afterVisible).toBeGreaterThan(initial)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@ -1,222 +0,0 @@
|
|||||||
// DEPRECATED (ADR-0009): 智能评估已改为触发式执行,cron 池不再使用(worker cron 已禁用)。
|
|
||||||
// 本页已从导航移除,遗留保留仅供回溯。监控职责由 TaskQueueMonitor(任务队列页)承担。
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import {
|
|
||||||
Alert, Button, Card, Descriptions, Empty, InputNumber, Space, Statistic, Table, Tag, message,
|
|
||||||
} from 'antd'
|
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
|
||||||
import { ReloadOutlined, WarningOutlined } from '@ant-design/icons'
|
|
||||||
import { openclawCronPoolApi, type CronPoolAlert, type CronPoolMetrics, type CronPoolStatus } from '../api'
|
|
||||||
import { usePolling } from '../hooks/usePolling'
|
|
||||||
import { colors } from '../tokens'
|
|
||||||
import { formatDateTime } from '../utils/date'
|
|
||||||
|
|
||||||
export default function CronPoolMonitor() {
|
|
||||||
const [status, setStatus] = useState<CronPoolStatus | null>(null)
|
|
||||||
const [metrics, setMetrics] = useState<CronPoolMetrics | null>(null)
|
|
||||||
const [alerts, setAlerts] = useState<CronPoolAlert[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [scaleTarget, setScaleTarget] = useState<number>(5)
|
|
||||||
const [scaleBusy, setScaleBusy] = useState(false)
|
|
||||||
|
|
||||||
const loadData = async () => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const [statusRes, metricsRes, alertsRes] = await Promise.all([
|
|
||||||
openclawCronPoolApi.getStatus(),
|
|
||||||
openclawCronPoolApi.getMetrics(),
|
|
||||||
openclawCronPoolApi.getAlerts(50),
|
|
||||||
])
|
|
||||||
setStatus(statusRes.data.pool)
|
|
||||||
setMetrics(metricsRes.data.metrics)
|
|
||||||
setAlerts(alertsRes.data.alerts)
|
|
||||||
} catch {
|
|
||||||
message.error('加载数据失败')
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial fetch (usePolling owns the 5s interval + visibility pause).
|
|
||||||
useEffect(() => {
|
|
||||||
void loadData()
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// S6: defer the recurring 5s poll to the shared `usePolling` hook. The
|
|
||||||
// default pauses the timer while the document is hidden (background tab /
|
|
||||||
// minimised window) and re-fetches once on the way back to `visible`.
|
|
||||||
usePolling(() => { void loadData() }, 5000, true)
|
|
||||||
|
|
||||||
const handleScale = async () => {
|
|
||||||
setScaleBusy(true)
|
|
||||||
try {
|
|
||||||
await openclawCronPoolApi.scale(scaleTarget)
|
|
||||||
message.success('扩缩容成功')
|
|
||||||
await loadData()
|
|
||||||
} catch {
|
|
||||||
message.error('扩缩容失败')
|
|
||||||
} finally {
|
|
||||||
setScaleBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleResolveAlert = async (alertId: string) => {
|
|
||||||
try {
|
|
||||||
await openclawCronPoolApi.resolveAlert(alertId)
|
|
||||||
message.success('已解决告警')
|
|
||||||
await loadData()
|
|
||||||
} catch {
|
|
||||||
message.error('解决告警失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const alertColumns: ColumnsType<CronPoolAlert> = [
|
|
||||||
{
|
|
||||||
title: '时间',
|
|
||||||
dataIndex: 'created_at',
|
|
||||||
key: 'created_at',
|
|
||||||
width: 180,
|
|
||||||
render: (val: string) => formatDateTime(val),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '级别',
|
|
||||||
dataIndex: 'severity',
|
|
||||||
key: 'severity',
|
|
||||||
width: 100,
|
|
||||||
render: (val: string) => (
|
|
||||||
<Tag color={val === 'critical' ? 'red' : val === 'warning' ? 'orange' : 'default'}>
|
|
||||||
{val === 'critical' ? '严重' : val === 'warning' ? '警告' : val}
|
|
||||||
</Tag>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '类型',
|
|
||||||
dataIndex: 'alert_type',
|
|
||||||
key: 'alert_type',
|
|
||||||
width: 150,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '消息',
|
|
||||||
dataIndex: 'message',
|
|
||||||
key: 'message',
|
|
||||||
ellipsis: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '状态',
|
|
||||||
key: 'status',
|
|
||||||
width: 100,
|
|
||||||
render: (_, record) => (
|
|
||||||
record.resolved_at ? (
|
|
||||||
<Tag color="green">已解决</Tag>
|
|
||||||
) : (
|
|
||||||
<Button size="small" type="primary" onClick={() => handleResolveAlert(record.id)}>
|
|
||||||
解决
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const unresolvedAlerts = alerts.filter((a) => !a.resolved_at)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ height: '100%', overflowY: 'auto', padding: 16 }}>
|
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 16 }}>
|
|
||||||
<span style={{ fontSize: 18, fontWeight: 600 }}>Cron 池监控</span>
|
|
||||||
<div style={{ flex: 1 }} />
|
|
||||||
<Button icon={<ReloadOutlined />} onClick={() => void loadData()} loading={loading}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{unresolvedAlerts.length > 0 && (
|
|
||||||
<Alert
|
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
type="warning"
|
|
||||||
showIcon
|
|
||||||
icon={<WarningOutlined />}
|
|
||||||
message={`有 ${unresolvedAlerts.length} 个未解决的告警`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Card size="small" title="池状态" style={{ marginBottom: 16 }}>
|
|
||||||
{status ? (
|
|
||||||
<div>
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 16, marginBottom: 16 }}>
|
|
||||||
<Statistic title="总数" value={status.total} />
|
|
||||||
<Statistic title="空闲" value={status.idle} valueStyle={{ color: '#52c41a' }} />
|
|
||||||
<Statistic title="忙碌" value={status.busy} valueStyle={{ color: colors.primary }} />
|
|
||||||
<Statistic title="卡死" value={status.stuck} valueStyle={{ color: status.stuck > 0 ? '#ff4d4f' : undefined }} />
|
|
||||||
</div>
|
|
||||||
<Descriptions size="small" column={2}>
|
|
||||||
<Descriptions.Item label="最小池大小">{status.min_size}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="最大池大小">{status.max_size}</Descriptions.Item>
|
|
||||||
</Descriptions>
|
|
||||||
<div style={{ marginTop: 16 }}>
|
|
||||||
<Space>
|
|
||||||
<InputNumber
|
|
||||||
min={status.min_size}
|
|
||||||
max={status.max_size}
|
|
||||||
value={scaleTarget}
|
|
||||||
onChange={(val) => val && setScaleTarget(val)}
|
|
||||||
/>
|
|
||||||
<Button type="primary" onClick={handleScale} loading={scaleBusy}>
|
|
||||||
手动扩缩容
|
|
||||||
</Button>
|
|
||||||
</Space>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Empty description="加载中..." />
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card size="small" title="监控指标" style={{ marginBottom: 16 }}>
|
|
||||||
{metrics ? (
|
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16 }}>
|
|
||||||
<Statistic
|
|
||||||
title="池使用率"
|
|
||||||
value={(metrics.pool_utilization * 100).toFixed(1)}
|
|
||||||
suffix="%"
|
|
||||||
valueStyle={{
|
|
||||||
color: metrics.pool_utilization > 0.9 ? '#ff4d4f' : metrics.pool_utilization > 0.7 ? colors.warning : undefined,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Statistic title="任务积压" value={metrics.task_backlog} />
|
|
||||||
<Statistic
|
|
||||||
title="卡死率"
|
|
||||||
value={(metrics.stuck_rate * 100).toFixed(1)}
|
|
||||||
suffix="%"
|
|
||||||
valueStyle={{ color: metrics.stuck_rate > 0.1 ? '#ff4d4f' : undefined }}
|
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title="平均处理时间"
|
|
||||||
value={metrics.avg_processing_time_seconds ? (metrics.avg_processing_time_seconds / 60).toFixed(1) : '—'}
|
|
||||||
suffix={metrics.avg_processing_time_seconds ? '分钟' : ''}
|
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title="评估完成率"
|
|
||||||
value={(metrics.eval_completion_rate * 100).toFixed(1)}
|
|
||||||
suffix="%"
|
|
||||||
/>
|
|
||||||
<Statistic title="更新时间" value={formatDateTime(metrics.timestamp)} />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Empty description="加载中..." />
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Card size="small" title="告警历史">
|
|
||||||
<Table
|
|
||||||
rowKey="id"
|
|
||||||
loading={loading}
|
|
||||||
dataSource={alerts}
|
|
||||||
columns={alertColumns}
|
|
||||||
pagination={false}
|
|
||||||
locale={{ emptyText: <Empty description="暂无告警" /> }}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -37,7 +37,7 @@ export default function FilesPage() {
|
|||||||
uploadFiles,
|
uploadFiles,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
downloadFile,
|
downloadFile,
|
||||||
} = useFiles()
|
} = useFiles('/files')
|
||||||
const [uploadOpen, setUploadOpen] = useState(false)
|
const [uploadOpen, setUploadOpen] = useState(false)
|
||||||
const [categoryDialog, setCategoryDialog] = useState<CategoryDialogState>(CLOSED_CATEGORY_DIALOG)
|
const [categoryDialog, setCategoryDialog] = useState<CategoryDialogState>(CLOSED_CATEGORY_DIALOG)
|
||||||
const [categorySaving, setCategorySaving] = useState(false)
|
const [categorySaving, setCategorySaving] = useState(false)
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function HomePage() {
|
|||||||
height: 240,
|
height: 240,
|
||||||
yAxis: { label: { formatter: (v: string) => `${v}%` }, min: 0, max: 100 },
|
yAxis: { label: { formatter: (v: string) => `${v}%` }, min: 0, max: 100 },
|
||||||
point: { size: 3 },
|
point: { size: 3 },
|
||||||
color: '#1677ff',
|
color: colors.primary,
|
||||||
tooltip: {
|
tooltip: {
|
||||||
formatter: (datum: TrendPoint) => ({
|
formatter: (datum: TrendPoint) => ({
|
||||||
name: '通过率',
|
name: '通过率',
|
||||||
@ -95,7 +95,7 @@ export default function HomePage() {
|
|||||||
color={(stats?.running_count ?? 0) > 0 ? 'blue' : 'green'}
|
color={(stats?.running_count ?? 0) > 0 ? 'blue' : 'green'}
|
||||||
title="运行中"
|
title="运行中"
|
||||||
value={stats?.running_count ?? 0}
|
value={stats?.running_count ?? 0}
|
||||||
valueStyle={(stats?.running_count ?? 0) > 0 ? { color: '#1677ff' } : undefined}
|
valueStyle={(stats?.running_count ?? 0) > 0 ? { color: colors.primary } : undefined}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={12} sm={8} xl={4}>
|
<Col xs={12} sm={8} xl={4}>
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
|
type CreateIntelligentEvalPayload, type IntelligentEval, type Target,
|
||||||
} from '../api'
|
} from '../api'
|
||||||
import { useTabStore, type TabItem } from '../stores/tabStore'
|
import { useTabStore, type TabItem } from '../stores/tabStore'
|
||||||
import { colors } from '../tokens'
|
import { colors, statusColors } from '../tokens'
|
||||||
import { formatDateTime } from '../utils/date'
|
import { formatDateTime } from '../utils/date'
|
||||||
import { useIntelligentEvalRead } from '../read/useIntelligentEvalRead'
|
import { useIntelligentEvalRead } from '../read/useIntelligentEvalRead'
|
||||||
|
|
||||||
@ -47,8 +47,8 @@ const STAT_CHIPS: { key: string; label: string; color?: string }[] = [
|
|||||||
{ key: 'planning', label: '规划中' },
|
{ key: 'planning', label: '规划中' },
|
||||||
{ key: 'pending_approval', label: '待审批' },
|
{ key: 'pending_approval', label: '待审批' },
|
||||||
{ key: 'executing', label: '执行中' },
|
{ key: 'executing', label: '执行中' },
|
||||||
{ key: 'completed', label: '已完成', color: '#52c41a' },
|
{ key: 'completed', label: '已完成', color: statusColors.completed },
|
||||||
{ key: 'failed', label: '失败', color: '#ff4d4f' },
|
{ key: 'failed', label: '失败', color: statusColors.failed },
|
||||||
]
|
]
|
||||||
|
|
||||||
export default function IntelligentEvalsPage() {
|
export default function IntelligentEvalsPage() {
|
||||||
|
|||||||
@ -11,8 +11,9 @@ import {
|
|||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import { reportsApi, runsApi, type Run } from '../api'
|
import { reportsApi, runsApi, type Run } from '../api'
|
||||||
import PageWrapper from '../components/PageWrapper'
|
import PageWrapper from '../components/PageWrapper'
|
||||||
import { colors, triggerColors, triggerLabels } from '../tokens'
|
import { colors, statusColors, triggerColors, triggerLabels } from '../tokens'
|
||||||
import { formatDateTime } from '../utils/date'
|
import { formatDateTime } from '../utils/date'
|
||||||
|
import { passRateColor } from '../utils/colors'
|
||||||
import { useResource } from '../hooks/useResource'
|
import { useResource } from '../hooks/useResource'
|
||||||
|
|
||||||
interface TurnData {
|
interface TurnData {
|
||||||
@ -196,7 +197,7 @@ export default function ReportsPage() {
|
|||||||
<span style={{ color: colors.textMuted }}>· {target}</span>
|
<span style={{ color: colors.textMuted }}>· {target}</span>
|
||||||
<span style={{ color: colors.textMuted, fontSize: 12 }}>{time}</span>
|
<span style={{ color: colors.textMuted, fontSize: 12 }}>{time}</span>
|
||||||
<span style={{
|
<span style={{
|
||||||
color: passRate != null && passRate >= 0.8 ? '#3f8600' : '#cf1322',
|
color: passRateColor(passRate ?? 0),
|
||||||
fontSize: 12, fontWeight: 600,
|
fontSize: 12, fontWeight: 600,
|
||||||
}}>{pct}</span>
|
}}>{pct}</span>
|
||||||
<Tag color={triggerColors[trigger] ?? 'default'} style={{ marginRight: 0, fontSize: 10, lineHeight: '16px', padding: '0 4px' }}>
|
<Tag color={triggerColors[trigger] ?? 'default'} style={{ marginRight: 0, fontSize: 10, lineHeight: '16px', padding: '0 4px' }}>
|
||||||
@ -352,19 +353,19 @@ function SingleReportView({ report }: { report: Report | null }) {
|
|||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic title="通过用例" value={report.summary.passed_cases}
|
<Statistic title="通过用例" value={report.summary.passed_cases}
|
||||||
valueStyle={{ color: '#3f8600' }} prefix={<CheckCircleOutlined />} />
|
valueStyle={{ color: statusColors.completed }} prefix={<CheckCircleOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic title="失败用例" value={report.summary.failed_cases}
|
<Statistic title="失败用例" value={report.summary.failed_cases}
|
||||||
valueStyle={{ color: '#cf1322' }} prefix={<CloseCircleOutlined />} />
|
valueStyle={{ color: statusColors.failed }} prefix={<CloseCircleOutlined />} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<Card>
|
<Card>
|
||||||
<Statistic title="通过率" value={report.summary.pass_rate * 100} precision={1} suffix="%"
|
<Statistic title="通过率" value={report.summary.pass_rate * 100} precision={1} suffix="%"
|
||||||
valueStyle={{ color: report.summary.pass_rate >= 0.8 ? '#3f8600' : '#cf1322' }} />
|
valueStyle={{ color: passRateColor(report.summary.pass_rate) }} />
|
||||||
</Card>
|
</Card>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
@ -475,7 +476,7 @@ function CompareView({ result }: { result: CompareResult | null }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { run_a, run_b, delta, cases, changed_cases } = result
|
const { run_a, run_b, delta, cases, changed_cases } = result
|
||||||
const deltaColor = (v: number) => v > 0 ? '#3f8600' : v < 0 ? '#cf1322' : colors.textMuted
|
const deltaColor = (v: number) => v > 0 ? statusColors.completed : v < 0 ? statusColors.failed : colors.textMuted
|
||||||
const deltaSign = (v: number) => v > 0 ? `+${v}` : String(v)
|
const deltaSign = (v: number) => v > 0 ? `+${v}` : String(v)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user