"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";