diff --git a/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx b/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx
index 9fb866a..0d46d88 100644
--- a/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx
+++ b/frontend/web/src/components/intelligent_eval/ExecutionProcess.test.tsx
@@ -3,10 +3,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { ExecutionProgress, IntelligentEval } from '../../api'
import ExecutionProcess from './ExecutionProcess'
-vi.mock('@ant-design/charts', () => ({
- Column: () =>
,
-}))
-
const getExecutionProgress = vi.fn()
const listDecisionLogs = vi.fn()
const listTasks = vi.fn()
@@ -93,11 +89,12 @@ afterEach(() => {
})
describe('ExecutionProcess', () => {
- it('renders lifecycle steps, next action and slot chart', async () => {
+ it('renders lifecycle timeline, next action and time distribution cards', async () => {
mockApi()
render()
- await waitFor(() => expect(screen.getByTestId('slot-chart')).toBeInTheDocument())
+ await waitFor(() => expect(screen.getByText('0-1h')).toBeInTheDocument())
+ expect(screen.getByText('1-2h')).toBeInTheDocument()
expect(screen.getByText('规划')).toBeInTheDocument()
expect(screen.getByText('审批')).toBeInTheDocument()
expect(screen.getByText(/下一步:等待平台触发 worker 补足当前欠账 2 个会话/)).toBeInTheDocument()
diff --git a/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx b/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx
index 8e5a778..586662b 100644
--- a/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx
+++ b/frontend/web/src/components/intelligent_eval/ExecutionProcess.tsx
@@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
-import { Alert, Card, Empty, Spin, Steps, Tag, Timeline } from 'antd'
-import { Column } from '@ant-design/charts'
+import { Alert, Card, Empty, Spin, Tag, Timeline } from 'antd'
+import {
+ CheckCircleOutlined,
+ ClockCircleOutlined,
+ EditOutlined,
+ FileTextOutlined,
+ PlayCircleOutlined,
+} from '@ant-design/icons'
import {
intelligentEvalsApi,
type DecisionLog,
@@ -15,11 +21,11 @@ import { usePolling } from '../../hooks/usePolling'
const ACTIVE_STATUSES = new Set(['planning', 'pending_approval', 'executing'])
-const STAGE_STEPS = [
- { key: 'planning', title: '规划' },
- { key: 'approval', title: '审批' },
- { key: 'executing', title: '执行' },
- { key: 'done', title: '完成' },
+const STAGE_CONFIG = [
+ { key: 'planning', title: '规划', icon: EditOutlined },
+ { key: 'approval', title: '审批', icon: FileTextOutlined },
+ { key: 'executing', title: '执行', icon: PlayCircleOutlined },
+ { key: 'done', title: '完成', icon: CheckCircleOutlined },
]
const DECISION_TYPE_LABELS: Record = {
@@ -91,6 +97,136 @@ function decisionEvents(logs: DecisionLog[]): ActivityEvent[] {
}))
}
+function LifecycleTimeline({ currentStage, abnormal }: { currentStage: string; abnormal: boolean }) {
+ const currentIndex = STAGE_CONFIG.findIndex((s) => s.key === currentStage)
+
+ return (
+
+ {STAGE_CONFIG.map((stage, idx) => {
+ const Icon = stage.icon
+ const isCompleted = idx < currentIndex
+ const isCurrent = idx === currentIndex
+ const isError = isCurrent && abnormal
+
+ const bgColor = isError
+ ? statusColors.failed
+ : isCompleted
+ ? statusColors.completed
+ : isCurrent
+ ? colors.primary
+ : colors.bgSubtle
+
+ const textColor = isCompleted || isCurrent ? '#fff' : colors.textSecondary
+
+ return (
+
+
+
+ {isCompleted ? : }
+
+
+ {stage.title}
+ {isCurrent &&
当前阶段
}
+
+
+ {idx < STAGE_CONFIG.length - 1 && (
+
+ )}
+
+ )
+ })}
+
+ )
+}
+
+function TimeDistributionCards({ slots }: { slots: ExecutionProgress['slots'] }) {
+ return (
+
+ {slots.map((slot) => {
+ const plannedPct = slot.planned > 0 ? 100 : 0
+ const createdPct = slot.planned > 0 ? (slot.created / slot.planned) * 100 : 0
+ const completedPct = slot.planned > 0 ? (slot.completed / slot.planned) * 100 : 0
+
+ return (
+
+
+
{slot.time_slot}
+ {slot.is_current && (
+
+
+ 当前
+
+ )}
+
+
+
+
+
+ 计划
+ {slot.planned}
+
+
+
+
+
+
+ 已创建
+ {slot.created}/{slot.planned}
+
+
+
+
+
+
+ 已完成
+ {slot.completed}/{slot.planned}
+
+
+
+
+
+ )
+ })}
+
+ )
+}
+
export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
const [progress, setProgress] = useState(null)
const [logs, setLogs] = useState([])
@@ -132,42 +268,12 @@ export default function ExecutionProcess({ ev }: { ev: IntelligentEval }) {
return
}
- const stageIndex = STAGE_STEPS.findIndex((s) => s.key === progress.current_stage)
const abnormal = progress.abnormal_outcome != null
- const slotData = progress.slots.flatMap((s) => {
- const label = s.is_current ? `${s.time_slot}(当前)` : s.time_slot
- return [
- { slot: label, kind: '计划', value: s.planned },
- { slot: label, kind: '已创建', value: s.created },
- { slot: label, kind: '已完成', value: s.completed },
- ]
- })
- const slotConfig = {
- data: slotData,
- xField: 'slot',
- yField: 'value',
- seriesField: 'kind',
- group: true,
- height: Math.max(200, progress.slots.length * 72),
- color: [colors.textMuted, colors.primary, statusColors.completed],
- legend: { position: 'top' },
- xAxis: { label: { autoRotate: true } },
- yAxis: { min: 0 },
- }
-
return (
- ({
- title: s.title,
- description: s.key === progress.current_stage ? '当前阶段' : undefined,
- }))}
- />
+
{progress.blocker != null && (
) : (
-
+
)}