重构 Agent 聊天,支持分支管理与消息克隆
This commit is contained in:
@@ -1,19 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { Box, Paper, Stack, Typography, alpha, useTheme } from "@mui/material";
|
||||
import AutoAwesome from "@mui/icons-material/AutoAwesome";
|
||||
import { Box, Paper, Stack, Typography, alpha, useTheme, Grid } from "@mui/material";
|
||||
import WaterDropRounded from "@mui/icons-material/WaterDropRounded";
|
||||
import SensorsRounded from "@mui/icons-material/SensorsRounded";
|
||||
import TroubleshootRounded from "@mui/icons-material/TroubleshootRounded";
|
||||
import MapRounded from "@mui/icons-material/MapRounded";
|
||||
|
||||
import { AgentTurn } from "./AgentTurn";
|
||||
import { TypingIndicator } from "./GlobalChatbox.parts";
|
||||
import type { Message, SpeechState } from "./GlobalChatbox.types";
|
||||
import type {
|
||||
BranchGroup,
|
||||
BranchTransition,
|
||||
Message,
|
||||
SpeechState,
|
||||
} from "./GlobalChatbox.types";
|
||||
|
||||
type AgentWorkspaceProps = {
|
||||
messages: Message[];
|
||||
branchGroups: BranchGroup[];
|
||||
branchTransition: BranchTransition | null;
|
||||
isStreaming: boolean;
|
||||
bottomRef: React.RefObject<HTMLDivElement | null>;
|
||||
speakingMessageId: string | null;
|
||||
@@ -23,14 +31,18 @@ type AgentWorkspaceProps = {
|
||||
onResumeSpeech: () => void;
|
||||
onStopSpeech: () => void;
|
||||
isTtsSupported: boolean;
|
||||
onRegenerate: () => void;
|
||||
onEditResubmit: (messageId: string, newContent: string) => void;
|
||||
onCycleBranch: (rootMessageId: string, direction: -1 | 1) => void;
|
||||
};
|
||||
|
||||
const EmptyState = () => {
|
||||
const theme = useTheme();
|
||||
const capabilities = [
|
||||
{ icon: <WaterDropRounded sx={{ fontSize: 18 }} />, label: "水力瓶颈识别" },
|
||||
{ icon: <SensorsRounded sx={{ fontSize: 18 }} />, label: "SCADA 异常分析" },
|
||||
{ icon: <TroubleshootRounded sx={{ fontSize: 18 }} />, label: "改造与调度建议" },
|
||||
{ icon: <WaterDropRounded sx={{ fontSize: 20, color: "#00acc1" }} />, label: "水力瓶颈识别" },
|
||||
{ icon: <SensorsRounded sx={{ fontSize: 20, color: "#0288d1" }} />, label: "异常状态预警" },
|
||||
{ icon: <TroubleshootRounded sx={{ fontSize: 20, color: "#43a047" }} />, label: "调度与改造建议" },
|
||||
{ icon: <MapRounded sx={{ fontSize: 20, color: "#8e24aa" }} />, label: "GIS 地图联动" },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -38,62 +50,101 @@ const EmptyState = () => {
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||
style={{ margin: "auto", width: "100%" }}
|
||||
style={{ margin: "auto", width: "100%", maxWidth: 440, padding: 16 }}
|
||||
>
|
||||
<Paper
|
||||
elevation={0}
|
||||
sx={{
|
||||
p: 3,
|
||||
borderRadius: 5,
|
||||
bgcolor: alpha("#fff", 0.68),
|
||||
border: `1px solid ${alpha(theme.palette.divider, 0.12)}`,
|
||||
maxWidth: 380,
|
||||
mx: "auto",
|
||||
p: 4,
|
||||
borderRadius: 4,
|
||||
bgcolor: alpha("#ffffff", 0.4),
|
||||
border: `1px solid ${alpha("#fff", 0.8)}`,
|
||||
boxShadow: `0 16px 40px ${alpha("#000", 0.05)}`,
|
||||
textAlign: "center",
|
||||
backdropFilter: "blur(10px)",
|
||||
backdropFilter: "blur(24px)",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Box sx={{
|
||||
position: "absolute",
|
||||
top: -100,
|
||||
right: -100,
|
||||
width: 200,
|
||||
height: 200,
|
||||
background: "radial-gradient(circle, rgba(0, 172, 193, 0.15) 0%, rgba(255,255,255,0) 70%)",
|
||||
}} />
|
||||
<motion.div
|
||||
animate={{ y: [-5, 5, -5] }}
|
||||
transition={{ duration: 4, repeat: Infinity, ease: "easeInOut" }}
|
||||
animate={{
|
||||
y: [-6, 4, -6],
|
||||
scale: [1, 1.04, 1],
|
||||
rotate: [-3, 3, -3],
|
||||
}}
|
||||
transition={{ duration: 4.8, repeat: Infinity, ease: "easeInOut" }}
|
||||
style={{
|
||||
display: "inline-flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
width: 88,
|
||||
height: 88,
|
||||
marginBottom: 12,
|
||||
borderRadius: "50%",
|
||||
background: "radial-gradient(circle, rgba(255,255,255,0.92) 0%, rgba(255,255,255,0.45) 58%, rgba(255,255,255,0) 100%)",
|
||||
boxShadow: "0 10px 28px rgba(0, 131, 143, 0.12)",
|
||||
}}
|
||||
>
|
||||
<AutoAwesome
|
||||
sx={{
|
||||
fontSize: 54,
|
||||
color: "primary.main",
|
||||
mb: 1.6,
|
||||
filter: "drop-shadow(0 4px 8px rgba(0,0,0,0.1))",
|
||||
<Image
|
||||
src="/ai-agent.svg"
|
||||
alt="TJWater Agent"
|
||||
width={54}
|
||||
height={54}
|
||||
style={{
|
||||
objectFit: "contain",
|
||||
filter: "drop-shadow(0 4px 12px rgba(0, 131, 143, 0.2))",
|
||||
}}
|
||||
/>
|
||||
</motion.div>
|
||||
<Typography variant="h6" color="text.primary" fontWeight={900} gutterBottom>
|
||||
管网分析 Agent 已就绪
|
||||
<Typography variant="h6" color="text.primary" fontWeight={800} gutterBottom>
|
||||
我已就绪,请描述任务
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.65, mb: 2 }}>
|
||||
可以描述你的分析目标,我会展示规划、数据查询过程、地图动作和最终建议。
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.6, mb: 3 }}>
|
||||
你可以使用自然语言下达指令,我会自主规划决策执行、并在地图上呈现分析结果。
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={0.8} useFlexGap flexWrap="wrap" justifyContent="center">
|
||||
|
||||
<Grid container spacing={1.5}>
|
||||
{capabilities.map((item) => (
|
||||
<Stack
|
||||
key={item.label}
|
||||
direction="row"
|
||||
spacing={0.5}
|
||||
alignItems="center"
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.55,
|
||||
borderRadius: 99,
|
||||
bgcolor: alpha(theme.palette.primary.main, 0.07),
|
||||
color: "text.secondary",
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
<Typography variant="caption" fontWeight={700}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Grid item xs={6} key={item.label}>
|
||||
<motion.div whileHover={{ y: -2, scale: 1.02 }} transition={{ duration: 0.2 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
sx={{
|
||||
px: 1.5,
|
||||
py: 1.5,
|
||||
borderRadius: 3,
|
||||
bgcolor: alpha("#fff", 0.5),
|
||||
border: `1px solid ${alpha("#fff", 0.6)}`,
|
||||
boxShadow: `0 4px 12px ${alpha("#000", 0.03)}`,
|
||||
color: "text.primary",
|
||||
transition: "all 0.2s",
|
||||
"&:hover": {
|
||||
bgcolor: alpha("#fff", 0.8),
|
||||
borderColor: alpha("#00acc1", 0.4),
|
||||
boxShadow: `0 6px 16px ${alpha("#00acc1", 0.15)}`,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{item.icon}
|
||||
<Typography variant="caption" fontWeight={700}>
|
||||
{item.label}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</motion.div>
|
||||
</Grid>
|
||||
))}
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -101,6 +152,8 @@ const EmptyState = () => {
|
||||
|
||||
export const AgentWorkspace = ({
|
||||
messages,
|
||||
branchGroups,
|
||||
branchTransition,
|
||||
isStreaming,
|
||||
bottomRef,
|
||||
speakingMessageId,
|
||||
@@ -110,6 +163,9 @@ export const AgentWorkspace = ({
|
||||
onResumeSpeech,
|
||||
onStopSpeech,
|
||||
isTtsSupported,
|
||||
onRegenerate,
|
||||
onEditResubmit,
|
||||
onCycleBranch,
|
||||
}: AgentWorkspaceProps) => {
|
||||
const theme = useTheme();
|
||||
const latestAssistant = [...messages]
|
||||
@@ -120,6 +176,43 @@ export const AgentWorkspace = ({
|
||||
(!latestAssistant ||
|
||||
(latestAssistant.content.trim().length === 0 &&
|
||||
!(latestAssistant.artifacts?.length)));
|
||||
const stableMessages = branchTransition
|
||||
? messages.slice(0, branchTransition.parentCount)
|
||||
: messages;
|
||||
const transitionMessages = branchTransition
|
||||
? messages.slice(branchTransition.parentCount)
|
||||
: [];
|
||||
|
||||
const renderTurn = (message: Message) => {
|
||||
const rootMessageId = message.branchRootId ?? message.id;
|
||||
const branchGroup = branchGroups.find(
|
||||
(group) => group.rootMessageId === rootMessageId,
|
||||
);
|
||||
|
||||
return (
|
||||
<AgentTurn
|
||||
key={rootMessageId}
|
||||
message={message}
|
||||
branchState={
|
||||
branchGroup && branchGroup.branches.length > 1
|
||||
? {
|
||||
activeIndex: branchGroup.activeIndex,
|
||||
total: branchGroup.branches.length,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
||||
onSpeak={onSpeak}
|
||||
onPause={onPauseSpeech}
|
||||
onResume={onResumeSpeech}
|
||||
onStopSpeech={onStopSpeech}
|
||||
isTtsSupported={isTtsSupported}
|
||||
onRegenerate={onRegenerate}
|
||||
onEditResubmit={onEditResubmit}
|
||||
onCycleBranch={onCycleBranch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
@@ -130,26 +223,34 @@ export const AgentWorkspace = ({
|
||||
py: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: 2,
|
||||
zIndex: 5,
|
||||
}}
|
||||
>
|
||||
<AnimatePresence initial={false}>
|
||||
{messages.length === 0 ? <EmptyState /> : null}
|
||||
{messages.map((message) => (
|
||||
<AgentTurn
|
||||
key={message.id}
|
||||
message={message}
|
||||
messageSpeechState={speakingMessageId === message.id ? speechState : "idle"}
|
||||
onSpeak={onSpeak}
|
||||
onPause={onPauseSpeech}
|
||||
onResume={onResumeSpeech}
|
||||
onStopSpeech={onStopSpeech}
|
||||
isTtsSupported={isTtsSupported}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{messages.length > 0 ? (
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
|
||||
{stableMessages.map(renderTurn)}
|
||||
|
||||
{branchTransition ? (
|
||||
<AnimatePresence initial={false} mode="wait">
|
||||
<motion.div
|
||||
key={`${branchTransition.rootMessageId}:${branchTransition.activeBranchId}:${branchTransition.nonce}`}
|
||||
initial={{ opacity: 0, y: 8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -8 }}
|
||||
transition={{ duration: 0.18, ease: "easeOut" }}
|
||||
style={{ display: "flex", flexDirection: "column", gap: 16 }}
|
||||
>
|
||||
{transitionMessages.map(renderTurn)}
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
) : null}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
{showTypingIndicator ? (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10, scale: 0.94 }}
|
||||
|
||||
Reference in New Issue
Block a user