"use client"; import React, { useEffect, useMemo, useState } from "react"; import { Box, Collapse, IconButton, LinearProgress, Stack, Typography, alpha, useMediaQuery, useTheme, } from "@mui/material"; import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded"; import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import ErrorOutlineRounded from "@mui/icons-material/ErrorOutlineRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import RadioButtonUncheckedRounded from "@mui/icons-material/RadioButtonUncheckedRounded"; import StopCircleRounded from "@mui/icons-material/StopCircleRounded"; import type { AgentActivity, AgentActivityAction } from "@/lib/chatStream"; const activityAccent = "#0097a7"; type TimedActivityItem = { status: "running" | "completed" | "error" | "cancelled"; startedAt: number; endedAt?: number; elapsedMs?: number; elapsedSnapshotAt?: number; durationMs?: number; }; const formatDuration = (durationMs: number | undefined) => { if (durationMs === undefined || !Number.isFinite(durationMs)) return undefined; if (durationMs < 10_000) return `${(durationMs / 1000).toFixed(1)}s`; const seconds = Math.round(durationMs / 1000); return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`; }; const getElapsedMs = ( item: TimedActivityItem, now: 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 + now - item.elapsedSnapshotAt); } return Math.max(0, now - item.startedAt); } return item.endedAt ? Math.max(0, item.endedAt - item.startedAt) : undefined; }; const StatusIcon = ({ status, size = 18, }: { status: AgentActivity["status"]; size?: number; }) => { if (status === "completed") { return ; } if (status === "error") { return ; } if (status === "cancelled") { return ; } return ; }; const ActionStatusIcon = ({ status }: { status: AgentActivityAction["status"] }) => { if (status === "error") { return ; } if (status === "completed") { return ; } return ( ); }; const ActionRow = ({ action, now }: { action: AgentActivityAction; now: number }) => { const elapsed = getElapsedMs(action, now); return ( {action.title} {formatDuration(elapsed)} {action.target ? ( {action.target} ) : null} {action.error ? ( {action.error} ) : null} ); }; export const AgentActivityTimeline = ({ activities, }: { activities: AgentActivity[]; }) => { const theme = useTheme(); const reduceMotion = useMediaQuery("(prefers-reduced-motion: reduce)"); const hasRunning = activities.some((activity) => activity.status === "running"); const hasError = activities.some((activity) => activity.status === "error"); const hasCancelled = activities.some((activity) => activity.status === "cancelled"); const [expanded, setExpanded] = useState(false); const [now, setNow] = useState(() => Date.now()); useEffect(() => { if (!hasRunning) return; const timer = window.setInterval(() => setNow(Date.now()), 500); return () => window.clearInterval(timer); }, [hasRunning]); const current = [...activities] .reverse() .find((activity) => activity.status === "running") ?? activities.at(-1); const totalDuration = useMemo(() => { if (!activities.length) return undefined; const start = Math.min(...activities.map((activity) => activity.startedAt)); const end = hasRunning ? now : Math.max( ...activities.map((activity) => activity.endedAt ?? activity.startedAt), ); return formatDuration(Math.max(0, end - start)); }, [activities, hasRunning, now]); const overallStatus: AgentActivity["status"] = hasRunning ? "running" : hasError ? "error" : hasCancelled ? "cancelled" : "completed"; const statusLabel = { running: "进行中", completed: "已完成", error: "失败", cancelled: "已停止", }[overallStatus]; const statusColor = { running: activityAccent, completed: theme.palette.success.main, error: theme.palette.error.main, cancelled: theme.palette.text.secondary, }[overallStatus]; const summary = hasRunning ? (current?.title ?? "正在分析") : hasError ? "分析未完成" : hasCancelled ? "分析已停止" : `已完成 ${activities.length} 个阶段`; return ( 分析过程 {statusLabel} {summary} {totalDuration ? ` · ${totalDuration}` : ""} setExpanded((current) => !current)} sx={{ width: 28, height: 28, flex: "0 0 auto", color: "text.secondary", bgcolor: alpha("#000", 0.035), "&:hover": { bgcolor: alpha("#000", 0.07) }, }} > {expanded ? ( ) : ( )} {hasRunning ? ( ) : null} {activities.map((activity, index) => { const elapsed = formatDuration(getElapsedMs(activity, now)); return ( {index < activities.length - 1 ? ( ) : null} {activity.title} {elapsed} {activity.reason} {activity.actions.length ? ( {activity.actions.map((action) => ( ))} ) : null} ); })} ); };