Agent 初版设计

This commit is contained in:
2026-04-29 17:15:49 +08:00
parent 2c1afdc97c
commit e5ca9e24aa
13 changed files with 1819 additions and 1255 deletions
@@ -0,0 +1,188 @@
"use client";
import React, { useMemo, useState } from "react";
import {
Box,
Button,
Chip,
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 type { ChatProgress } from "./GlobalChatbox.types";
const phaseIcon = (phase: string, status: ChatProgress["status"]) => {
const sx = { fontSize: 16 };
if (status === "completed") return <CheckCircleRounded sx={{ ...sx, color: "success.main" }} />;
if (status === "error") return <ErrorOutlineRounded sx={{ ...sx, color: "error.main" }} />;
if (phase === "planning") return <PsychologyRounded sx={{ ...sx, color: "primary.main" }} />;
if (phase === "tool") return <BuildCircleRounded sx={{ ...sx, color: "warning.main" }} />;
if (phase === "complete") return <TaskAltRounded sx={{ ...sx, color: "success.main" }} />;
if (phase === "session") return <SyncRounded sx={{ ...sx, color: "info.main" }} />;
if (phase === "start") return <ManageSearchRounded sx={{ ...sx, color: "primary.main" }} />;
return <AutoAwesome sx={{ ...sx, color: "primary.main" }} />;
};
const formatToolTitle = (item: ChatProgress) => {
const text = `${item.title} ${item.detail ?? ""}`;
if (text.includes("dynamic_http_call")) 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 面板";
return item.title;
};
export const AgentProgressTimeline = ({ progress }: { progress: ChatProgress[] }) => {
const theme = useTheme();
const hasComplete = progress.some(
(item) => item.phase === "complete" && item.status === "completed",
);
const hasRunning =
!hasComplete && progress.some((item) => item.status === "running");
const hasError = progress.some((item) => item.status === "error");
const [expanded, setExpanded] = useState(hasRunning);
const summary = useMemo(() => {
const completedCount = progress.filter((item) => item.status === "completed").length;
const runningItem = hasComplete
? undefined
: [...progress].reverse().find((item) => item.status === "running");
if (runningItem) return runningItem.title;
if (hasError) return "过程存在异常";
if (hasComplete) return `已完成 ${progress.length}`;
return `已完成 ${completedCount || progress.length}`;
}, [hasComplete, hasError, progress]);
return (
<Box
sx={{
borderRadius: 3,
bgcolor: alpha(theme.palette.primary.main, 0.045),
border: `1px solid ${alpha(theme.palette.primary.main, 0.14)}`,
overflow: "hidden",
}}
>
<Stack
direction="row"
spacing={1}
alignItems="center"
sx={{ px: 1.5, py: 1.1 }}
>
<AutoAwesome sx={{ fontSize: 17, color: "primary.main" }} />
<Typography variant="caption" fontWeight={800} color="text.primary">
Agent
</Typography>
<Chip
size="small"
label={summary}
color={hasError ? "error" : hasRunning ? "primary" : "success"}
variant="outlined"
sx={{ height: 22, fontSize: "0.68rem", maxWidth: 180 }}
/>
<Box sx={{ flex: 1 }} />
<Button
size="small"
onClick={() => setExpanded((value) => !value)}
sx={{ minWidth: 0, px: 0.75, fontSize: "0.72rem" }}
>
{expanded ? "收起" : "展开"}
</Button>
</Stack>
{hasRunning ? <LinearProgress sx={{ height: 3 }} /> : null}
<Collapse in={expanded} timeout="auto">
<Stack spacing={1} sx={{ px: 1.5, pb: 1.35 }}>
{progress.map((item, index) => (
<Stack key={item.id} direction="row" spacing={1} alignItems="stretch">
<Box
sx={{
position: "relative",
width: 18,
display: "flex",
justifyContent: "center",
flexShrink: 0,
pt: 0.1,
}}
>
{index < progress.length - 1 ? (
<Box
aria-hidden
sx={{
position: "absolute",
top: 18,
bottom: -10,
left: "50%",
width: 2,
transform: "translateX(-50%)",
borderRadius: 99,
bgcolor: alpha(
item.status === "error"
? theme.palette.error.main
: theme.palette.primary.main,
item.status === "completed" ? 0.22 : 0.36,
),
}}
/>
) : null}
<Box
sx={{
position: "relative",
zIndex: 1,
width: 18,
height: 18,
borderRadius: "50%",
bgcolor: alpha("#fff", 0.92),
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{phaseIcon(
item.phase,
hasComplete && item.status === "running"
? "completed"
: item.status,
)}
</Box>
</Box>
<Box sx={{ minWidth: 0, flex: 1 }}>
<Typography variant="caption" color="text.primary" fontWeight={700}>
{item.phase === "tool" ? formatToolTitle(item) : item.title}
</Typography>
{item.detail ? (
<Typography
variant="caption"
component="pre"
color="text.secondary"
sx={{
display: "block",
mt: 0.25,
m: 0,
whiteSpace: "pre-wrap",
fontFamily: "inherit",
fontSize: "0.7rem",
}}
>
{item.detail}
</Typography>
) : null}
</Box>
</Stack>
))}
</Stack>
</Collapse>
</Box>
);
};