重构 Agent 聊天,支持分支管理与消息克隆

This commit is contained in:
2026-04-30 13:05:45 +08:00
parent e5ca9e24aa
commit 36d1a8d6ea
20 changed files with 1722 additions and 586 deletions
+137 -108
View File
@@ -1,9 +1,9 @@
"use client";
import Image from "next/image";
import React from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
Avatar,
Box,
Chip,
Collapse,
@@ -15,12 +15,12 @@ import {
alpha,
useTheme,
} from "@mui/material";
import AutoAwesome from "@mui/icons-material/AutoAwesome";
import SendRounded from "@mui/icons-material/SendRounded";
import StopRounded from "@mui/icons-material/StopRounded";
import MicRounded from "@mui/icons-material/MicRounded";
import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded";
import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
import AttachFileRounded from "@mui/icons-material/AttachFileRounded";
type AgentComposerProps = {
input: string;
@@ -56,30 +56,41 @@ export const AgentComposer = ({
const [isPresetOpen, setIsPresetOpen] = React.useState(false);
return (
<Box sx={{ px: 3, pb: 3, pt: 1.5, zIndex: 10 }}>
<Box sx={{ px: 2, pb: 2, pt: 1, zIndex: 10 }}>
<Paper
elevation={0}
elevation={isPresetOpen ? 4 : 0}
sx={{
mb: isPresetOpen ? 1.25 : 0.8,
px: 1.2,
py: 0.85,
borderRadius: 3.5,
bgcolor: alpha("#fff", 0.72),
border: `1px solid ${alpha(theme.palette.divider, 0.12)}`,
backdropFilter: "blur(12px)",
mb: 1.5,
px: 1.5,
py: 1,
borderRadius: 4,
bgcolor: alpha("#fff", 0.6),
border: `1px solid ${alpha("#fff", 0.5)}`,
backdropFilter: "blur(24px)",
boxShadow: isPresetOpen ? `0 -8px 24px ${alpha("#00acc1", 0.1)}` : "none",
transition: "all 0.3s ease",
}}
>
<Stack direction="row" spacing={1} alignItems="center">
<AutoAwesome sx={{ fontSize: 16, color: "primary.main" }} />
<Typography variant="caption" color="text.secondary" fontWeight={800}>
</Typography>
>
<Stack direction="row" spacing={1} alignItems="center">
<Image
src="/ai-agent.svg"
alt="TJWater Agent"
width={18}
height={18}
style={{
objectFit: "contain",
flexShrink: 0,
}}
/>
<Typography variant="caption" color="text.secondary" fontWeight={800} sx={{ letterSpacing: 0.5 }}>
</Typography>
<Box sx={{ flex: 1 }} />
<IconButton
size="small"
onClick={() => setIsPresetOpen((value) => !value)}
aria-label={isPresetOpen ? "收起常用管网任务" : "展开常用管网任务"}
sx={{ width: 26, height: 26, color: "text.secondary" }}
sx={{ width: 28, height: 28, color: "text.secondary", bgcolor: alpha("#fff", 0.5) }}
>
{isPresetOpen ? (
<KeyboardArrowDownRounded fontSize="small" />
@@ -89,60 +100,56 @@ export const AgentComposer = ({
</IconButton>
</Stack>
<Collapse in={isPresetOpen} timeout="auto" unmountOnExit>
<Stack direction="row" spacing={0.8} useFlexGap flexWrap="wrap" sx={{ pt: 0.9 }}>
{presets.map((prompt) => (
<Chip
key={prompt}
label={prompt.replace(/[。.]$/, "")}
size="small"
clickable
onClick={() => {
onPresetSelect(prompt);
setIsPresetOpen(false);
}}
sx={{
maxWidth: "100%",
height: 28,
borderRadius: 2,
bgcolor: alpha(theme.palette.primary.main, 0.07),
color: "text.primary",
fontWeight: 600,
"& .MuiChip-label": {
overflow: "hidden",
textOverflow: "ellipsis",
},
}}
/>
))}
</Stack>
<Box sx={{ mt: 1.5, mb: 0.5, pb: 1 }}>
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
{presets.map((prompt) => (
<Chip
key={prompt}
label={prompt.replace(/[。.]$/, "")}
size="medium"
clickable
onClick={() => {
onPresetSelect(prompt);
setIsPresetOpen(false);
}}
sx={{
height: 32,
borderRadius: "16px",
bgcolor: alpha("#fff", 0.7),
border: `1px solid ${alpha("#00acc1", 0.15)}`,
color: "text.primary",
fontWeight: 600,
fontSize: '0.85rem',
boxShadow: `0 2px 6px ${alpha("#000", 0.03)}`,
backdropFilter: "blur(10px)",
"&:hover": {
bgcolor: alpha("#fff", 0.95),
boxShadow: `0 4px 10px ${alpha("#00acc1", 0.2)}`,
borderColor: alpha("#00acc1", 0.4),
color: "#00acc1"
}
}}
/>
))}
</Box>
</Box>
</Collapse>
</Paper>
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }}>
<Stack
direction="row"
alignItems="center"
component={Paper}
<Paper
elevation={12}
sx={{
p: "6px 8px",
display: "flex",
flexDirection: "column",
p: 1.5,
borderRadius: 5,
bgcolor: alpha("#fff", 0.92),
backdropFilter: "blur(10px)",
border: `1px solid ${alpha("#fff", 0.62)}`,
boxShadow: `0 12px 40px -8px ${alpha(theme.palette.primary.main, 0.15)}`,
bgcolor: alpha("#ffffff", 0.75),
backdropFilter: "blur(40px)",
border: `1px solid ${alpha("#ffffff", 0.9)}`,
boxShadow: `0 16px 40px ${alpha("#000", 0.1)}, 0 0 0 1px ${alpha("#00acc1", 0.05)} inset`,
}}
>
<Avatar
sx={{
width: 28,
height: 28,
ml: 0.5,
background: `linear-gradient(135deg, ${theme.palette.primary.light}, ${theme.palette.secondary.main})`,
}}
>
<AutoAwesome sx={{ fontSize: 16, color: "#fff" }} />
</Avatar>
<TextField
inputRef={inputRef}
value={input}
@@ -153,62 +160,70 @@ export const AgentComposer = ({
onSend();
}
}}
placeholder="描述你的管网分析目标..."
placeholder="描述你的分析目标,或点击上方指令库..."
fullWidth
multiline
maxRows={4}
maxRows={5}
variant="standard"
InputProps={{
disableUnderline: true,
sx: { px: 2, py: 1.35, fontSize: "0.98rem" },
sx: { px: 1, py: 0.5, fontSize: "1rem", lineHeight: 1.6, fontWeight: 500, color: "text.primary" },
}}
/>
{isSttSupported ? (
<Box sx={{ display: "flex", alignItems: "center", mr: 0.5 }}>
{isListening ? (
<motion.div
animate={{ scale: [1, 1.14, 1] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<IconButton
onClick={onStopListening}
aria-label="停止语音输入"
sx={{
color: "error.main",
bgcolor: alpha(theme.palette.error.main, 0.1),
width: 42,
height: 42,
}}
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mt: 2 }}>
<Stack direction="row" spacing={0.5} alignItems="center">
<IconButton size="small" aria-label="上传附件" sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}>
<AttachFileRounded fontSize="small" />
</IconButton>
{isSttSupported ? (
isListening ? (
<motion.div
animate={{ scale: [1, 1.14, 1] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<MicRounded />
<IconButton
onClick={onStopListening}
aria-label="停止语音输入"
size="small"
sx={{
color: "error.main",
bgcolor: alpha(theme.palette.error.main, 0.15),
width: 36,
height: 36,
}}
>
<MicRounded fontSize="small" />
</IconButton>
</motion.div>
) : (
<IconButton
onClick={onStartListening}
disabled={isStreaming}
aria-label="语音输入"
size="small"
sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}
>
<MicRounded fontSize="small" />
</IconButton>
</motion.div>
) : (
<IconButton
onClick={onStartListening}
disabled={isStreaming}
aria-label="语音输入"
sx={{ color: "text.secondary", width: 42, height: 42 }}
>
<MicRounded />
</IconButton>
)}
</Box>
) : null}
)
) : null}
</Stack>
<Box sx={{ pr: 0.5 }}>
<AnimatePresence mode="wait">
{isStreaming ? (
<motion.div key="stop" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
<IconButton
onClick={onAbort}
aria-label="停止生成"
size="small"
sx={{
bgcolor: alpha(theme.palette.error.main, 0.1),
color: "error.main",
width: 42,
height: 42,
bgcolor: "error.main",
color: "#fff",
width: 40,
height: 40,
boxShadow: `0 4px 12px ${alpha(theme.palette.error.main, 0.4)}`,
"&:hover": { bgcolor: "error.dark" },
}}
>
<StopRounded />
@@ -220,12 +235,14 @@ export const AgentComposer = ({
disabled={!canSend}
onClick={onSend}
aria-label="发送"
size="small"
sx={{
bgcolor: canSend ? "primary.main" : "action.disabledBackground",
color: "#fff",
width: 42,
height: 42,
"&:hover": { bgcolor: canSend ? "primary.dark" : "action.disabledBackground" },
bgcolor: canSend ? "#00acc1" : alpha("#fff", 0.5),
color: canSend ? "#fff" : "action.disabled",
width: 40,
height: 40,
boxShadow: canSend ? `0 6px 16px ${alpha("#00acc1", 0.4)}` : "none",
"&:hover": { bgcolor: canSend ? "#00838f" : alpha("#fff", 0.5) },
}}
>
<SendRounded sx={{ ml: 0.35 }} />
@@ -233,9 +250,21 @@ export const AgentComposer = ({
</motion.div>
)}
</AnimatePresence>
</Box>
</Stack>
</Stack>
</Paper>
</motion.div>
<Box sx={{ mt: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.5, opacity: 0.6 }}>
<Image
src="/deepseek-logo.svg"
alt="DeepSeek"
width={14}
height={14}
style={{ width: 14, height: 14 }}
/>
<Typography variant="caption" sx={{ fontSize: "0.65rem", color: "text.secondary", fontWeight: 500, letterSpacing: 0.5 }}>
Powered by DeepSeek V3 · TJWater Agent Intelligence
</Typography>
</Box>
</Box>
);
};