342 lines
12 KiB
TypeScript
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>
|
|
);
|
|
};
|