重构聊天会话管理,支持会话历史和存储

This commit is contained in:
2026-04-30 15:02:08 +08:00
parent c5b0f43a0d
commit e0e78cd95a
11 changed files with 1247 additions and 221 deletions
+120 -107
View File
@@ -7,37 +7,32 @@ import {
Avatar,
Box,
IconButton,
ListItemIcon,
ListItemText,
Menu,
MenuItem,
Stack,
Tooltip,
Typography,
alpha,
useTheme,
} from "@mui/material";
import AddCommentRounded from "@mui/icons-material/AddCommentRounded";
import EditNoteRounded from "@mui/icons-material/EditNoteRounded";
import CloseRounded from "@mui/icons-material/CloseRounded";
import HistoryRounded from "@mui/icons-material/HistoryRounded";
type AgentHeaderProps = {
isStreaming: boolean;
menuAnchorEl: HTMLElement | null;
onMenuOpen: (event: React.MouseEvent<HTMLElement>) => void;
onMenuClose: () => void;
isHistoryOpen: boolean;
onHistoryToggle: () => void;
onNewConversation: () => void;
onClose: () => void;
};
export const AgentHeader = ({
isStreaming,
menuAnchorEl,
onMenuOpen,
onMenuClose,
isHistoryOpen,
onHistoryToggle,
onNewConversation,
onClose,
}: AgentHeaderProps) => {
const theme = useTheme();
const isMenuOpen = Boolean(menuAnchorEl);
return (
<Box
@@ -55,55 +50,46 @@ export const AgentHeader = ({
}}
>
<Stack direction="row" alignItems="center" spacing={2}>
<motion.div whileHover={{ rotate: 10, scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
onClick={onMenuOpen}
aria-label="打开 Agent 菜单"
aria-controls={isMenuOpen ? "global-chatbox-header-menu" : undefined}
aria-expanded={isMenuOpen ? "true" : undefined}
aria-haspopup="menu"
sx={{ p: 0, borderRadius: "50%" }}
>
<Box sx={{ position: "relative" }}>
<Avatar
sx={{
background: alpha("#ffffff", 0.9),
boxShadow: `0 8px 24px ${alpha("#00acc1", 0.4)}`,
width: 44,
height: 44,
border: `2px solid ${alpha("#fff", 0.8)}`,
p: 0.75,
}}
>
<Image
src="/ai-agent.svg"
alt="TJWater Agent"
width={30}
height={30}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
</Avatar>
<Box
sx={{
position: "absolute",
bottom: -2,
right: -2,
width: 14,
height: 14,
bgcolor: isStreaming ? "#ff9800" : "#00e676",
borderRadius: "50%",
border: "2.5px solid #fff",
boxShadow: `0 0 10px ${isStreaming ? "#ff9800" : "#00e676"}`,
animation: isStreaming ? "pulse 1.5s infinite" : "none",
"@keyframes pulse": {
"0%": { boxShadow: `0 0 0 0 ${alpha("#ff9800", 0.7)}` },
"70%": { boxShadow: `0 0 0 6px ${alpha("#ff9800", 0)}` },
"100%": { boxShadow: `0 0 0 0 ${alpha("#ff9800", 0)}` },
}
}}
<motion.div whileHover={{ rotate: 10, scale: 1.05 }} whileTap={{ scale: 0.95 }} style={{ display: "flex" }}>
<Box sx={{ position: "relative" }}>
<Avatar
sx={{
background: alpha("#ffffff", 0.9),
boxShadow: `0 8px 24px ${alpha("#00acc1", 0.4)}`,
width: 44,
height: 44,
border: `2px solid ${alpha("#fff", 0.8)}`,
p: 0.75,
}}
>
<Image
src="/ai-agent.svg"
alt="TJWater Agent"
width={30}
height={30}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
</Box>
</IconButton>
</Avatar>
<Box
sx={{
position: "absolute",
bottom: -2,
right: -2,
width: 14,
height: 14,
bgcolor: isStreaming ? "#ff9800" : "#00e676",
borderRadius: "50%",
border: "2.5px solid #fff",
boxShadow: `0 0 10px ${isStreaming ? "#ff9800" : "#00e676"}`,
animation: isStreaming ? "pulse 1.5s infinite" : "none",
"@keyframes pulse": {
"0%": { boxShadow: `0 0 0 0 ${alpha("#ff9800", 0.7)}` },
"70%": { boxShadow: `0 0 0 6px ${alpha("#ff9800", 0)}` },
"100%": { boxShadow: `0 0 0 0 ${alpha("#ff9800", 0)}` },
}
}}
/>
</Box>
</motion.div>
<Box>
<Typography
@@ -124,54 +110,81 @@ export const AgentHeader = ({
</Box>
</Stack>
<Menu
id="global-chatbox-header-menu"
anchorEl={menuAnchorEl}
open={isMenuOpen}
onClose={onMenuClose}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
slotProps={{
paper: {
elevation: 8,
sx: {
mt: 1,
minWidth: 180,
borderRadius: 3,
border: `1px solid ${alpha(theme.palette.divider, 0.12)}`,
backdropFilter: "blur(12px)",
bgcolor: alpha("#fff", 0.92),
},
},
}}
>
<MenuItem onClick={onNewConversation}>
<ListItemIcon>
<AddCommentRounded fontSize="small" />
</ListItemIcon>
<ListItemText
primary="新建对话"
secondary="清空当前会话"
primaryTypographyProps={{ sx: { fontSize: "0.95rem", fontWeight: 700 } }}
secondaryTypographyProps={{ sx: { fontSize: "0.8rem" } }}
/>
</MenuItem>
</Menu>
<Stack direction="row" spacing={1.25} alignItems="center">
<Tooltip title="新建对话">
<motion.div whileHover={{ scale: 1.08 }} whileTap={{ scale: 0.92 }} style={{ display: "flex" }}>
<IconButton
onClick={onNewConversation}
aria-label="新建对话"
sx={{
width: 36,
height: 36,
color: "text.primary",
bgcolor: alpha("#fff", 0.54),
border: `1px solid ${alpha("#fff", 0.4)}`,
boxShadow: `0 2px 8px ${alpha("#000", 0.02)}`,
"&:hover": {
bgcolor: "#fff",
color: "#00acc1",
borderColor: alpha("#fff", 0.8),
boxShadow: `0 4px 12px ${alpha("#000", 0.05)}`,
},
}}
>
<EditNoteRounded sx={{ fontSize: 22 }} />
</IconButton>
</motion.div>
</Tooltip>
<motion.div whileHover={{ scale: 1.08, rotate: 90 }} whileTap={{ scale: 0.92 }}>
<IconButton
onClick={onClose}
size="small"
aria-label="关闭 Agent"
sx={{
color: "text.primary",
bgcolor: alpha("#fff", 0.54),
"&:hover": { bgcolor: "#fff" },
}}
>
<CloseRounded />
</IconButton>
</motion.div>
<Tooltip title={isHistoryOpen ? "收起历史会话" : "打开历史会话"}>
<motion.div whileHover={{ scale: 1.08 }} whileTap={{ scale: 0.92 }} style={{ display: "flex" }}>
<IconButton
onClick={onHistoryToggle}
aria-label={isHistoryOpen ? "收起历史会话" : "打开历史会话"}
sx={{
width: 36,
height: 36,
color: isHistoryOpen ? "#00acc1" : "text.primary",
bgcolor: isHistoryOpen ? alpha("#00acc1", 0.12) : alpha("#fff", 0.54),
border: `1px solid ${isHistoryOpen ? alpha("#00acc1", 0.2) : alpha("#fff", 0.4)}`,
boxShadow: `0 2px 8px ${isHistoryOpen ? alpha("#00acc1", 0.05) : alpha("#000", 0.02)}`,
"&:hover": {
bgcolor: isHistoryOpen ? alpha("#00acc1", 0.16) : "#fff",
borderColor: isHistoryOpen ? alpha("#00acc1", 0.3) : alpha("#fff", 0.8),
boxShadow: `0 4px 12px ${isHistoryOpen ? alpha("#00acc1", 0.1) : alpha("#000", 0.05)}`,
},
}}
>
<HistoryRounded sx={{ fontSize: 20 }} />
</IconButton>
</motion.div>
</Tooltip>
<Tooltip title="关闭 Agent">
<motion.div whileHover={{ scale: 1.08, rotate: 90 }} whileTap={{ scale: 0.92 }} style={{ display: "flex" }}>
<IconButton
onClick={onClose}
aria-label="关闭 Agent"
sx={{
width: 36,
height: 36,
color: "text.primary",
bgcolor: alpha("#fff", 0.54),
border: `1px solid ${alpha("#fff", 0.4)}`,
boxShadow: `0 2px 8px ${alpha("#000", 0.02)}`,
"&:hover": {
bgcolor: "#fff",
color: "#e53935",
borderColor: alpha("#fff", 0.8),
boxShadow: `0 4px 12px ${alpha("#000", 0.05)}`,
},
}}
>
<CloseRounded sx={{ fontSize: 20 }} />
</IconButton>
</motion.div>
</Tooltip>
</Stack>
</Box>
);
};