Agent 初版设计

This commit is contained in:
2026-04-29 17:15:49 +08:00
parent 2c1afdc97c
commit e5ca9e24aa
13 changed files with 1819 additions and 1255 deletions
+241
View File
@@ -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>
);
};