重构 Agent 聊天,支持分支管理与消息克隆
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user