Agent 初版设计
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import React from "react";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Chip,
|
||||
Collapse,
|
||||
IconButton,
|
||||
Paper,
|
||||
Stack,
|
||||
TextField,
|
||||
Typography,
|
||||
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";
|
||||
|
||||
type AgentComposerProps = {
|
||||
input: string;
|
||||
inputRef: React.RefObject<HTMLInputElement | null>;
|
||||
isStreaming: boolean;
|
||||
isListening: boolean;
|
||||
isSttSupported: boolean;
|
||||
presets: string[];
|
||||
onInputChange: (value: string) => void;
|
||||
onSend: () => void;
|
||||
onAbort: () => void;
|
||||
onStartListening: () => void;
|
||||
onStopListening: () => void;
|
||||
onPresetSelect: (prompt: string) => void;
|
||||
};
|
||||
|
||||
export const AgentComposer = ({
|
||||
input,
|
||||
inputRef,
|
||||
isStreaming,
|
||||
isListening,
|
||||
isSttSupported,
|
||||
presets,
|
||||
onInputChange,
|
||||
onSend,
|
||||
onAbort,
|
||||
onStartListening,
|
||||
onStopListening,
|
||||
onPresetSelect,
|
||||
}: AgentComposerProps) => {
|
||||
const theme = useTheme();
|
||||
const canSend = input.trim().length > 0 && !isStreaming;
|
||||
const [isPresetOpen, setIsPresetOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Box sx={{ px: 3, pb: 3, pt: 1.5, zIndex: 10 }}>
|
||||
<Paper
|
||||
elevation={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)",
|
||||
}}
|
||||
>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<AutoAwesome sx={{ fontSize: 16, color: "primary.main" }} />
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={800}>
|
||||
常用管网任务
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1 }} />
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setIsPresetOpen((value) => !value)}
|
||||
aria-label={isPresetOpen ? "收起常用管网任务" : "展开常用管网任务"}
|
||||
sx={{ width: 26, height: 26, color: "text.secondary" }}
|
||||
>
|
||||
{isPresetOpen ? (
|
||||
<KeyboardArrowDownRounded fontSize="small" />
|
||||
) : (
|
||||
<KeyboardArrowUpRounded fontSize="small" />
|
||||
)}
|
||||
</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>
|
||||
</Collapse>
|
||||
</Paper>
|
||||
|
||||
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
component={Paper}
|
||||
elevation={12}
|
||||
sx={{
|
||||
p: "6px 8px",
|
||||
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)}`,
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
onChange={(event) => onInputChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
placeholder="描述你的管网分析目标..."
|
||||
fullWidth
|
||||
multiline
|
||||
maxRows={4}
|
||||
variant="standard"
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
sx: { px: 2, py: 1.35, fontSize: "0.98rem" },
|
||||
}}
|
||||
/>
|
||||
|
||||
{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,
|
||||
}}
|
||||
>
|
||||
<MicRounded />
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
) : (
|
||||
<IconButton
|
||||
onClick={onStartListening}
|
||||
disabled={isStreaming}
|
||||
aria-label="语音输入"
|
||||
sx={{ color: "text.secondary", width: 42, height: 42 }}
|
||||
>
|
||||
<MicRounded />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
) : null}
|
||||
|
||||
<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="停止生成"
|
||||
sx={{
|
||||
bgcolor: alpha(theme.palette.error.main, 0.1),
|
||||
color: "error.main",
|
||||
width: 42,
|
||||
height: 42,
|
||||
}}
|
||||
>
|
||||
<StopRounded />
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div key="send" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>
|
||||
<IconButton
|
||||
disabled={!canSend}
|
||||
onClick={onSend}
|
||||
aria-label="发送"
|
||||
sx={{
|
||||
bgcolor: canSend ? "primary.main" : "action.disabledBackground",
|
||||
color: "#fff",
|
||||
width: 42,
|
||||
height: 42,
|
||||
"&:hover": { bgcolor: canSend ? "primary.dark" : "action.disabledBackground" },
|
||||
}}
|
||||
>
|
||||
<SendRounded sx={{ ml: 0.35 }} />
|
||||
</IconButton>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Box>
|
||||
</Stack>
|
||||
</motion.div>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user