Files
agent/app/components/Message.tsx
zch1234qq 4afe6529fb
Some checks failed
Docker Build and Deploy / build-and-push (push) Has been cancelled
Docker Build and Deploy / deploy-hk (push) Has been cancelled
qwe
2025-10-21 09:17:52 +08:00

157 lines
5.3 KiB
TypeScript

import React, { useRef } from 'react';
import { Box, Paper, Typography, Avatar, useMediaQuery, useTheme } from '@mui/material';
import ReactMarkdown from 'react-markdown';
// @ts-ignore // 临时忽略类型声明缺失,等待官方类型包或自定义声明
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
// @ts-ignore
import { dark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import type { Message as MessageType } from '../types/types';
interface MessageProps {
message: MessageType;
isLast?: boolean;
}
const Message: React.FC<MessageProps> = ({ message, isLast = false }) => {
const isUser = message.role === 'user';
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down('sm'));
const paperRef = useRef<HTMLDivElement>(null);
// 计算气泡的borderRadius
const borderRadius = isUser
? '18px 18px 4px 18px'
: '18px 18px 18px 4px';
return (
<Box
key={message.id}
display="flex"
marginBottom={2}
justifyContent={isUser ? "flex-end" : "flex-start"}
alignItems="flex-end"
>
<Box
display="flex"
alignItems="flex-end"
maxWidth={isMobile ? "85%" : "70%"}
gap={1.5}
>
{!isUser && (
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: theme.palette.primary.main,
boxShadow: theme.shadows[2],
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
}
}}
>
🤖
</Avatar>
)}
<Paper
ref={paperRef}
elevation={isLast ? 2 : 1}
sx={{
padding: 2,
backgroundColor: isUser
? theme.palette.primary.main
: theme.palette.background.default,
color: isUser ? 'white' : theme.palette.text.primary,
borderRadius: borderRadius,
position: 'relative',
transition: 'all 0.3s ease',
'&:hover': {
boxShadow: theme.shadows[isLast ? 4 : 2],
transform: 'translateY(-1px)',
}
}}
>
<ReactMarkdown
components={{
//@ts-ignore // 临时忽略类型声明缺失,等待官方类型包或自定义声明
code({ inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
style={dark}
language={match[1]}
PreTag="div"
{...props}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className} {...props}>
{children}
</code>
);
},
p: ({ ...props }) => <Typography variant="body1" paragraph sx={{ margin: '0.5em 0' }} {...props} />,
h1: ({ ...props }) => <Typography variant="h4" gutterBottom {...props} />,
h2: ({ ...props }) => <Typography variant="h5" gutterBottom {...props} />,
h3: ({ ...props }) => <Typography variant="h6" gutterBottom {...props} />,
ul: ({ ...props }) => <ul style={{ paddingLeft: '1.5em', margin: '0.5em 0' }} {...props} />,
ol: ({ ...props }) => <ol style={{ paddingLeft: '1.5em', margin: '0.5em 0' }} {...props} />,
li: ({ ...props }) => <li style={{ margin: '0.25em 0' }} {...props} />,
a: ({ ...props }) => <a style={{ color: theme.palette.primary.main, textDecoration: 'underline' }} {...props} />,
blockquote: ({ ...props }) => (
<blockquote style={{
borderLeft: `4px solid ${theme.palette.divider}`,
marginLeft: 0,
paddingLeft: '1em',
fontStyle: 'italic',
color: theme.palette.text.secondary
}} {...props} />
),
strong: ({ ...props }) => <strong style={{ fontWeight: 600 }} {...props} />,
em: ({ ...props }) => <em style={{ fontStyle: 'italic' }} {...props} />
}}
>
{message.content}
</ReactMarkdown>
<Typography
variant="caption"
sx={{
display: 'block',
marginTop: 1,
opacity: 0.7,
fontSize: '0.75rem',
textAlign: 'right'
}}
>
{message.timestamp.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})}
</Typography>
</Paper>
{isUser && (
<Avatar
sx={{
width: 36,
height: 36,
bgcolor: theme.palette.primary.dark,
boxShadow: theme.shadows[2],
transition: 'transform 0.2s',
'&:hover': {
transform: 'scale(1.05)',
}
}}
>
👤
</Avatar>
)}
</Box>
</Box>
);
};
export default Message;