"use client"; import React, { useEffect, useMemo, useState } from "react"; import { Box, Collapse, LinearProgress, Stack, Typography, alpha, useTheme, } from "@mui/material"; import AutoAwesome from "@mui/icons-material/AutoAwesome"; import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import ErrorOutlineRounded from "@mui/icons-material/ErrorOutlineRounded"; import ManageSearchRounded from "@mui/icons-material/ManageSearchRounded"; import BuildCircleRounded from "@mui/icons-material/BuildCircleRounded"; import TaskAltRounded from "@mui/icons-material/TaskAltRounded"; import PsychologyRounded from "@mui/icons-material/PsychologyRounded"; import SyncRounded from "@mui/icons-material/SyncRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import type { ChatProgress } from "./GlobalChatbox.types"; const formatDuration = (durationMs: number) => { if (!Number.isFinite(durationMs) || durationMs < 0) { return "0s"; } if (durationMs < 10_000) { return `${(durationMs / 1000).toFixed(1)}s`; } const totalSeconds = Math.round(durationMs / 1000); if (totalSeconds < 60) { return `${totalSeconds}s`; } const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; if (minutes < 60) { return `${minutes}m ${seconds.toString().padStart(2, "0")}s`; } const hours = Math.floor(minutes / 60); const remainMinutes = minutes % 60; return `${hours}h ${remainMinutes.toString().padStart(2, "0")}m`; }; const getProgressElapsedMs = (item: ChatProgress, nowMs: number) => { if (item.durationMs !== undefined) { return item.durationMs; } if (item.status === "running") { if (item.elapsedMs !== undefined && item.elapsedSnapshotAt !== undefined) { return Math.max(0, item.elapsedMs + (nowMs - item.elapsedSnapshotAt)); } if (item.startedAt !== undefined) { return Math.max(0, nowMs - item.startedAt); } return item.elapsedMs; } if (item.startedAt !== undefined && item.endedAt !== undefined) { return Math.max(0, item.endedAt - item.startedAt); } return item.elapsedMs; }; const phaseIcon = (phase: string, status: ChatProgress["status"]) => { const sx = { fontSize: 16 }; if (status === "completed") return ; if (status === "error") return ; if (phase === "planning") return ; if (phase === "tool") return ; if (phase === "complete") return ; if (phase === "session") return ; if (phase === "start") return ; return ; }; const formatToolTitle = (item: ChatProgress) => { const text = `${item.title} ${item.detail ?? ""}`; if (text.includes("tjwater_cli")) return "查询后端数据"; if (text.includes("show_chart")) return "生成图表"; if (text.includes("locate_features")) return "地图定位"; if (text.includes("view_history")) return "打开历史曲线"; if (text.includes("view_scada")) return "打开 SCADA 面板"; if (text.includes("render_junctions")) return "渲染节点"; return item.title; }; type AgentProgressTimelineProps = { progress: ChatProgress[]; isAborted?: boolean; }; const AgentProgressTimelineInner = ({ progress, isAborted }: AgentProgressTimelineProps) => { const theme = useTheme(); const [nowMs, setNowMs] = useState(() => Date.now()); // 判断是否最终完成(哪怕中间有报错,只要有完整的标记就算成功) const isOverallComplete = progress.some( (item) => item.phase === "complete" && item.status === "completed", ); // 修正状态判断:如果外部标记为中断,或者没有完成标记 const hasRunning = !isAborted && !isOverallComplete && progress.some((item) => item.status === "running"); const hasError = isAborted || progress.some((item) => item.status === "error"); useEffect(() => { if (!hasRunning) { return; } const timer = window.setInterval(() => { setNowMs(Date.now()); }, 500); return () => window.clearInterval(timer); }, [hasRunning]); // 展开状态逻辑:默认折叠,保持界面整洁 const [expanded, setExpanded] = useState(false); const summary = useMemo(() => { if (isAborted) return `已中断 (进行到第 ${progress.length} 步)`; if (isOverallComplete) { return hasError ? `已完成 (含 ${progress.length} 步探索)` : `已完成 (${progress.length} 步)`; } const runningItem = [...progress].reverse().find((item) => item.status === "running"); if (runningItem) return `${runningItem.title}...`; if (hasError) return "过程异常,尝试恢复中..."; return `已执行 ${progress.length} 步`; }, [isOverallComplete, hasError, progress, isAborted]); const totalDurationLabel = useMemo(() => { const requestProgress = progress.find((item) => item.id === "request-received"); const requestElapsed = requestProgress ? getProgressElapsedMs(requestProgress, nowMs) : undefined; if (requestElapsed !== undefined) { return formatDuration(requestElapsed); } const startedAtValues = progress .map((item) => item.startedAt) .filter((value): value is number => value !== undefined); if (startedAtValues.length === 0) { return undefined; } const minStartedAt = Math.min(...startedAtValues); const endedAtValues = progress .map((item) => item.endedAt) .filter((value): value is number => value !== undefined); const endAnchor = isOverallComplete ? endedAtValues.length > 0 ? Math.max(...endedAtValues) : nowMs : nowMs; return formatDuration(Math.max(0, endAnchor - minStartedAt)); }, [isOverallComplete, nowMs, progress]); // 根据整体状态决定顶部卡片的颜色主题 const statusColor = isOverallComplete ? "#4caf50" // Success Green : isAborted || (hasError && !hasRunning) ? theme.palette.error.main // Error Red : "#00acc1"; // Primary Cyan // 默认折叠:只显示最新的三条 const visibleCount = 3; const isCollapsible = progress.length > visibleCount; return ( setExpanded(!expanded)} sx={{ px: 2, py: 1.25, cursor: "pointer", userSelect: "none" }} > {isOverallComplete ? ( ) : hasRunning ? ( ) : hasError ? ( ) : ( )} Agent 过程: {summary} {totalDurationLabel ? ` · 耗时 ${totalDurationLabel}` : ""} {hasRunning && !expanded ? ( ) : null} {hasRunning ? ( ) : ( )} {progress.map((item, index) => { const isLast = index === progress.length - 1; const isHiddenWhenCollapsed = isCollapsible && index < progress.length - visibleCount; const stepElapsedMs = getProgressElapsedMs(item, nowMs); const itemColor = isAborted && isLast ? theme.palette.error.main : item.status === "error" ? theme.palette.error.main : item.status === "completed" ? "#4caf50" : "#00acc1"; const content = ( {!isLast ? ( ) : null} {phaseIcon( item.phase, isAborted && isLast ? "error" : isOverallComplete && item.status === "running" ? "completed" : item.status, )} {item.phase === "tool" ? formatToolTitle(item) : item.title} {stepElapsedMs !== undefined ? ( {formatDuration(stepElapsedMs)} ) : null} {item.detail && ( {item.detail} )} ); if (isHiddenWhenCollapsed) { return ( {content} ); } return content; })} ); }; export const AgentProgressTimeline = React.memo( AgentProgressTimelineInner, (prevProps, nextProps) => prevProps.progress === nextProps.progress && prevProps.isAborted === nextProps.isAborted, ); AgentProgressTimeline.displayName = "AgentProgressTimeline";