Files
TJWaterFrontend_Refine/src/components/chat/AgentHeader.tsx
T
jiang 4bf99e8069
Build Push and Deploy / docker-image (push) Successful in 8s
Build Push and Deploy / deploy-fallback-log (push) Has been skipped
Refine chat session storage and title handling
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-21 17:33:48 +08:00

342 lines
12 KiB
TypeScript

"use client";
import Image from "next/image";
import React from "react";
import { motion } from "framer-motion";
import {
Avatar,
Box,
IconButton,
Stack,
TextField,
Tooltip,
Typography,
alpha,
useTheme,
} from "@mui/material";
import CheckRounded from "@mui/icons-material/CheckRounded";
import CloseRounded from "@mui/icons-material/CloseRounded";
import EditRounded from "@mui/icons-material/EditRounded";
import EditNoteRounded from "@mui/icons-material/EditNoteRounded";
import HistoryRounded from "@mui/icons-material/HistoryRounded";
type AgentHeaderProps = {
sessionTitle?: string;
canRenameSessionTitle?: boolean;
isHydrating?: boolean;
isStreaming: boolean;
isHistoryOpen: boolean;
onHistoryToggle: () => void;
onRenameSessionTitle?: (title: string) => void;
onNewConversation: () => void;
onClose: () => void;
};
export const AgentHeader = ({
sessionTitle,
canRenameSessionTitle = false,
isHydrating = false,
isStreaming,
isHistoryOpen,
onHistoryToggle,
onRenameSessionTitle,
onNewConversation,
onClose,
}: AgentHeaderProps) => {
const theme = useTheme();
const displayTitle = sessionTitle?.trim() || "新对话";
const [isEditingTitle, setIsEditingTitle] = React.useState(false);
const [draftTitle, setDraftTitle] = React.useState(sessionTitle?.trim() || "");
React.useEffect(() => {
if (!isEditingTitle) {
setDraftTitle(sessionTitle?.trim() || "");
}
}, [isEditingTitle, sessionTitle]);
const handleStartEditing = () => {
if (!canRenameSessionTitle || isHydrating || isStreaming) return;
setDraftTitle(sessionTitle?.trim() || "");
setIsEditingTitle(true);
};
const handleCancelEditing = () => {
setDraftTitle(sessionTitle?.trim() || "");
setIsEditingTitle(false);
};
const handleConfirmEditing = () => {
const normalizedTitle = draftTitle.trim();
if (!normalizedTitle) return;
onRenameSessionTitle?.(normalizedTitle);
setIsEditingTitle(false);
};
return (
<Box
sx={{
px: 3,
py: 2.5,
zIndex: 10,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
backdropFilter: "blur(20px)",
borderBottom: `1px solid ${alpha(theme.palette.divider, 0.1)}`,
background: `linear-gradient(to bottom, ${alpha("#fff", 0.4)}, ${alpha("#fff", 0.1)})`,
boxShadow: `0 1px 0 ${alpha("#fff", 0.6)} inset`,
}}
>
<Stack direction="row" alignItems="center" spacing={2} sx={{ minWidth: 0, flex: 1, mr: 2 }}>
<motion.div whileHover={{ rotate: 10, scale: 1.05 }} whileTap={{ scale: 0.95 }} style={{ display: "flex", flexShrink: 0 }}>
<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)}` },
},
}}
/>
</Box>
</motion.div>
<Box sx={{ minWidth: 0, flex: 1 }}>
{isEditingTitle ? (
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ width: "100%" }}>
<TextField
value={draftTitle}
onChange={(event) => setDraftTitle(event.target.value)}
size="small"
autoFocus
placeholder="请输入对话标题"
onKeyDown={(event) => {
if (event.key === "Enter") {
event.preventDefault();
handleConfirmEditing();
} else if (event.key === "Escape") {
event.preventDefault();
handleCancelEditing();
}
}}
sx={{
flex: 1,
minWidth: 0,
"& .MuiOutlinedInput-root": {
padding: "6px 8px",
bgcolor: "transparent",
borderRadius: 1.5,
transition: "all 0.2s ease-in-out",
"&.Mui-focused": {
bgcolor: alpha("#fff", 0.6),
boxShadow: `0 2px 10px ${alpha("#000", 0.05)}`,
},
"& fieldset": {
borderColor: "transparent",
},
"&:hover fieldset": {
borderColor: alpha(theme.palette.primary.main, 0.2),
},
"&.Mui-focused fieldset": {
borderColor: alpha(theme.palette.primary.main, 0.5),
borderWidth: "1px",
},
},
"& .MuiInputBase-input": {
padding: 0,
height: "auto",
fontSize: "1.25rem",
fontWeight: 800,
letterSpacing: -0.3,
lineHeight: "1.2",
background: `linear-gradient(90deg, #01579b, #00838f)`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
}
}}
/>
<IconButton
size="small"
aria-label="确认"
onClick={handleConfirmEditing}
disabled={!draftTitle.trim()}
sx={{
width: 30,
height: 30,
color: "success.main",
bgcolor: alpha(theme.palette.success.main, 0.1),
"&:hover": { bgcolor: alpha(theme.palette.success.main, 0.2) },
}}
>
<CheckRounded sx={{ fontSize: 18 }} />
</IconButton>
<IconButton
size="small"
aria-label="取消"
onClick={handleCancelEditing}
sx={{
width: 30,
height: 30,
color: "text.secondary",
bgcolor: alpha("#000", 0.05),
"&:hover": { bgcolor: alpha("#000", 0.1) },
}}
>
<CloseRounded sx={{ fontSize: 18 }} />
</IconButton>
</Stack>
) : (
<Stack direction="row" spacing={0.75} alignItems="center" sx={{ minWidth: 0 }}>
<Typography
variant="h6"
fontWeight={800}
sx={{
background: `linear-gradient(90deg, #01579b, #00838f)`,
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
letterSpacing: -0.3,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
px: "8px",
lineHeight: 1.2,
}}
>
{displayTitle}
</Typography>
{canRenameSessionTitle ? (
<Tooltip title="修改对话标题">
<span>
<IconButton
size="small"
aria-label="修改对话标题"
onClick={handleStartEditing}
disabled={isHydrating || isStreaming}
sx={{
width: 30,
height: 30,
color: "text.secondary",
bgcolor: alpha("#fff", 0.45),
"&:hover": {
color: "primary.main",
bgcolor: alpha(theme.palette.primary.main, 0.08),
},
}}
>
<EditRounded sx={{ fontSize: 18 }} />
</IconButton>
</span>
</Tooltip>
) : null}
</Stack>
)}
</Box>
</Stack>
<Stack direction="row" spacing={1.25} alignItems="center" sx={{ flexShrink: 0 }}>
<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>
<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>
);
};