fix(chat): 调整输入区操作按钮布局

This commit is contained in:
2026-07-30 19:07:17 +08:00
parent 38d246eb7b
commit b57e58ff87
2 changed files with 98 additions and 46 deletions
@@ -0,0 +1,61 @@
/* eslint-disable @next/next/no-img-element */
import React from "react";
import { render, screen } from "@testing-library/react";
import { ThemeProvider, createTheme } from "@mui/material/styles";
import { AgentComposer } from "./AgentComposer";
jest.mock("next/image", () => ({
__esModule: true,
default: (props: React.ImgHTMLAttributes<HTMLImageElement>) => (
<img {...props} alt={props.alt ?? ""} />
),
}));
jest.mock("framer-motion", () => ({
AnimatePresence: ({ children }: { children: React.ReactNode }) => <>{children}</>,
motion: {
div: ({
children,
animate: _animate,
exit: _exit,
initial: _initial,
transition: _transition,
...props
}: React.HTMLAttributes<HTMLDivElement> & Record<string, unknown>) => (
<div {...props}>{children}</div>
),
},
}));
describe("AgentComposer", () => {
it("places voice input immediately before send without an attachment action", () => {
render(
<ThemeProvider theme={createTheme()}>
<AgentComposer
isStreaming={false}
isListening={false}
isSttSupported
presets={[]}
onSend={jest.fn()}
onAbort={jest.fn()}
onStartListening={jest.fn()}
onStopListening={jest.fn()}
modelOptions={[{ id: "test-model", label: "测试模型" }]}
selectedModel="test-model"
onModelChange={jest.fn()}
approvalMode="request"
onApprovalModeChange={jest.fn()}
/>
</ThemeProvider>,
);
const voiceButton = screen.getByRole("button", { name: "语音输入" });
const sendButton = screen.getByRole("button", { name: "发送" });
expect(screen.queryByRole("button", { name: "上传附件" })).not.toBeInTheDocument();
expect(screen.getByTitle("快捷指令图标")).toBeInTheDocument();
expect(screen.queryByAltText("TJWater Agent")).not.toBeInTheDocument();
expect(voiceButton.nextElementSibling?.contains(sendButton)).toBe(true);
});
});
+36 -45
View File
@@ -23,7 +23,6 @@ import StopRounded from "@mui/icons-material/StopRounded";
import MicRounded from "@mui/icons-material/MicRounded"; import MicRounded from "@mui/icons-material/MicRounded";
import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded";
import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded";
import AttachFileRounded from "@mui/icons-material/AttachFileRounded";
import BoltRounded from "@mui/icons-material/BoltRounded"; import BoltRounded from "@mui/icons-material/BoltRounded";
import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded"; import AutoAwesomeRounded from "@mui/icons-material/AutoAwesomeRounded";
import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded"; import VerifiedUserRounded from "@mui/icons-material/VerifiedUserRounded";
@@ -125,15 +124,9 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
}} }}
> >
<Stack direction="row" spacing={1} alignItems="center"> <Stack direction="row" spacing={1} alignItems="center">
<Image <AutoAwesomeRounded
src="/ai-agent.svg" titleAccess="快捷指令图标"
alt="TJWater Agent" sx={{ fontSize: 18, color: "#00acc1", flexShrink: 0 }}
width={18}
height={18}
style={{
objectFit: "contain",
flexShrink: 0,
}}
/> />
<Typography variant="caption" color="text.secondary" fontWeight={800} sx={{ letterSpacing: 0.5 }}> <Typography variant="caption" color="text.secondary" fontWeight={800} sx={{ letterSpacing: 0.5 }}>
@@ -230,41 +223,6 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
<Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mt: 2 }}> <Stack direction="row" alignItems="center" justifyContent="space-between" sx={{ mt: 2 }}>
<Stack direction="row" spacing={0.5} alignItems="center"> <Stack direction="row" spacing={0.5} alignItems="center">
<IconButton size="small" aria-label="上传附件" sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}>
<AttachFileRounded fontSize="small" />
</IconButton>
{isSttSupported ? (
isListening ? (
<motion.div
animate={{ scale: [1, 1.14, 1] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<IconButton
onClick={onStopListening}
aria-label="停止语音输入"
size="small"
sx={{
color: "error.main",
bgcolor: alpha(theme.palette.error.main, 0.15),
width: 36,
height: 36,
}}
>
<MicRounded fontSize="small" />
</IconButton>
</motion.div>
) : (
<IconButton
onClick={onStartListening}
disabled={isStreaming || isHydrating}
aria-label="语音输入"
size="small"
sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}
>
<MicRounded fontSize="small" />
</IconButton>
)
) : null}
<FormControl size="small" sx={{ minWidth: 96 }}> <FormControl size="small" sx={{ minWidth: 96 }}>
<Select <Select
value={approvalMode} value={approvalMode}
@@ -471,6 +429,39 @@ export const AgentComposer = React.forwardRef<AgentComposerHandle, AgentComposer
</Select> </Select>
</FormControl> </FormControl>
{isSttSupported ? (
isListening ? (
<motion.div
animate={{ scale: [1, 1.14, 1] }}
transition={{ duration: 1.5, repeat: Infinity, ease: "easeInOut" }}
>
<IconButton
onClick={onStopListening}
aria-label="停止语音输入"
size="small"
sx={{
color: "error.main",
bgcolor: alpha(theme.palette.error.main, 0.15),
width: 36,
height: 36,
}}
>
<MicRounded fontSize="small" />
</IconButton>
</motion.div>
) : (
<IconButton
onClick={onStartListening}
disabled={isStreaming || isHydrating}
aria-label="语音输入"
size="small"
sx={{ color: "text.secondary", width: 36, height: 36, bgcolor: alpha("#fff", 0.6) }}
>
<MicRounded fontSize="small" />
</IconButton>
)
) : null}
<AnimatePresence mode="wait"> <AnimatePresence mode="wait">
{isStreaming ? ( {isStreaming ? (
<motion.div key="stop" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}> <motion.div key="stop" initial={{ scale: 0 }} animate={{ scale: 1 }} exit={{ scale: 0 }}>