150 lines
4.3 KiB
JavaScript
150 lines
4.3 KiB
JavaScript
// src/components/ChatBot/MessageBubble.js
|
||
// 聊天消息气泡组件
|
||
|
||
import React from 'react';
|
||
import {
|
||
Box,
|
||
Flex,
|
||
Text,
|
||
Avatar,
|
||
useColorModeValue,
|
||
IconButton,
|
||
HStack,
|
||
Code,
|
||
Badge,
|
||
VStack,
|
||
} from '@chakra-ui/react';
|
||
import { FiCopy, FiThumbsUp, FiThumbsDown } from 'react-icons/fi';
|
||
import ReactMarkdown from 'react-markdown';
|
||
|
||
/**
|
||
* 消息气泡组件
|
||
* @param {Object} props
|
||
* @param {Object} props.message - 消息对象
|
||
* @param {boolean} props.isUser - 是否是用户消息
|
||
* @param {Function} props.onCopy - 复制消息回调
|
||
* @param {Function} props.onFeedback - 反馈回调
|
||
*/
|
||
export const MessageBubble = ({ message, isUser, onCopy, onFeedback }) => {
|
||
const userBg = useColorModeValue('blue.500', 'blue.600');
|
||
const botBg = useColorModeValue('gray.100', 'gray.700');
|
||
const userColor = 'white';
|
||
const botColor = useColorModeValue('gray.800', 'white');
|
||
|
||
const handleCopy = () => {
|
||
navigator.clipboard.writeText(message.content);
|
||
onCopy?.();
|
||
};
|
||
|
||
return (
|
||
<Flex
|
||
w="100%"
|
||
justify={isUser ? 'flex-end' : 'flex-start'}
|
||
mb={4}
|
||
>
|
||
<Flex
|
||
maxW="75%"
|
||
flexDirection={isUser ? 'row-reverse' : 'row'}
|
||
align="flex-start"
|
||
>
|
||
{/* 头像 */}
|
||
<Avatar
|
||
size="sm"
|
||
name={isUser ? '用户' : 'AI助手'}
|
||
bg={isUser ? 'blue.500' : 'green.500'}
|
||
color="white"
|
||
mx={3}
|
||
/>
|
||
|
||
{/* 消息内容 */}
|
||
<Box>
|
||
<Box
|
||
bg={isUser ? userBg : botBg}
|
||
color={isUser ? userColor : botColor}
|
||
px={4}
|
||
py={3}
|
||
borderRadius="lg"
|
||
boxShadow="sm"
|
||
>
|
||
{message.type === 'text' ? (
|
||
<Text fontSize="sm" whiteSpace="pre-wrap">
|
||
{message.content}
|
||
</Text>
|
||
) : message.type === 'markdown' ? (
|
||
<Box fontSize="sm" className="markdown-content">
|
||
<ReactMarkdown>{message.content}</ReactMarkdown>
|
||
</Box>
|
||
) : message.type === 'data' ? (
|
||
<VStack align="stretch" spacing={2}>
|
||
{message.data && Array.isArray(message.data) && message.data.slice(0, 5).map((item, idx) => (
|
||
<Box
|
||
key={idx}
|
||
p={3}
|
||
bg={useColorModeValue('white', 'gray.600')}
|
||
borderRadius="md"
|
||
fontSize="xs"
|
||
>
|
||
{Object.entries(item).map(([key, value]) => (
|
||
<Flex key={key} justify="space-between" mb={1}>
|
||
<Text fontWeight="bold" mr={2}>{key}:</Text>
|
||
<Text>{String(value)}</Text>
|
||
</Flex>
|
||
))}
|
||
</Box>
|
||
))}
|
||
{message.data && message.data.length > 5 && (
|
||
<Badge colorScheme="blue" alignSelf="center">
|
||
+{message.data.length - 5} 更多结果
|
||
</Badge>
|
||
)}
|
||
</VStack>
|
||
) : null}
|
||
</Box>
|
||
|
||
{/* 消息操作按钮(仅AI消息) */}
|
||
{!isUser && (
|
||
<HStack mt={2} spacing={2}>
|
||
<IconButton
|
||
icon={<FiCopy />}
|
||
size="xs"
|
||
variant="ghost"
|
||
aria-label="复制"
|
||
onClick={handleCopy}
|
||
/>
|
||
<IconButton
|
||
icon={<FiThumbsUp />}
|
||
size="xs"
|
||
variant="ghost"
|
||
aria-label="赞"
|
||
onClick={() => onFeedback?.('positive')}
|
||
/>
|
||
<IconButton
|
||
icon={<FiThumbsDown />}
|
||
size="xs"
|
||
variant="ghost"
|
||
aria-label="踩"
|
||
onClick={() => onFeedback?.('negative')}
|
||
/>
|
||
</HStack>
|
||
)}
|
||
|
||
{/* 时间戳 */}
|
||
<Text
|
||
fontSize="xs"
|
||
color="gray.500"
|
||
mt={1}
|
||
textAlign={isUser ? 'right' : 'left'}
|
||
>
|
||
{message.timestamp ? new Date(message.timestamp).toLocaleTimeString('zh-CN', {
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
}) : ''}
|
||
</Text>
|
||
</Box>
|
||
</Flex>
|
||
</Flex>
|
||
);
|
||
};
|
||
|
||
export default MessageBubble;
|