feat: 创建 ChatArea 组件(含 MessageRenderer、ExecutionStepsDisplay 子组件)
This commit is contained in:
117
src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js
Normal file
117
src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js
|
||||||
|
// 执行步骤显示组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionButton,
|
||||||
|
AccordionPanel,
|
||||||
|
AccordionIcon,
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Badge,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Activity } from 'lucide-react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExecutionStepsDisplay - 执行步骤显示组件
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.steps - 执行步骤列表
|
||||||
|
* @param {Object} props.plan - 执行计划(可选)
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
const ExecutionStepsDisplay = ({ steps, plan }) => {
|
||||||
|
return (
|
||||||
|
<Accordion allowToggle>
|
||||||
|
<AccordionItem
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
borderRadius="lg"
|
||||||
|
bg="rgba(255, 255, 255, 0.03)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.05)',
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AccordionButton px={4} py={2}>
|
||||||
|
<HStack flex={1} spacing={2}>
|
||||||
|
<Activity className="w-4 h-4" color="#C084FC" />
|
||||||
|
<Text color="gray.300" fontSize="sm">
|
||||||
|
执行详情
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, purple.500, pink.500)"
|
||||||
|
color="white"
|
||||||
|
variant="subtle"
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{steps.length} 步骤
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
<AccordionIcon color="gray.400" />
|
||||||
|
</AccordionButton>
|
||||||
|
<AccordionPanel pb={4}>
|
||||||
|
<VStack spacing={2} align="stretch">
|
||||||
|
{steps.map((result, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, x: -10 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ delay: idx * 0.05 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.03)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
>
|
||||||
|
<CardBody p={3}>
|
||||||
|
<Flex align="start" justify="space-between" gap={2}>
|
||||||
|
<Text fontSize="xs" fontWeight="medium" color="gray.300">
|
||||||
|
步骤 {idx + 1}: {result.tool_name}
|
||||||
|
</Text>
|
||||||
|
<Badge
|
||||||
|
bgGradient={
|
||||||
|
result.status === 'success'
|
||||||
|
? 'linear(to-r, green.500, teal.500)'
|
||||||
|
: 'linear(to-r, red.500, orange.500)'
|
||||||
|
}
|
||||||
|
color="white"
|
||||||
|
variant="subtle"
|
||||||
|
boxShadow={
|
||||||
|
result.status === 'success'
|
||||||
|
? '0 2px 8px rgba(16, 185, 129, 0.3)'
|
||||||
|
: '0 2px 8px rgba(239, 68, 68, 0.3)'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{result.status}
|
||||||
|
</Badge>
|
||||||
|
</Flex>
|
||||||
|
<Text fontSize="xs" color="gray.500" mt={1}>
|
||||||
|
{result.execution_time?.toFixed(2)}s
|
||||||
|
</Text>
|
||||||
|
{result.error && (
|
||||||
|
<Text fontSize="xs" color="red.400" mt={1}>
|
||||||
|
⚠️ {result.error}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</VStack>
|
||||||
|
</AccordionPanel>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExecutionStepsDisplay;
|
||||||
248
src/views/AgentChat/components/ChatArea/MessageRenderer.js
Normal file
248
src/views/AgentChat/components/ChatArea/MessageRenderer.js
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/MessageRenderer.js
|
||||||
|
// 消息渲染器组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardBody,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
HStack,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
Box,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { Cpu, User, Copy, ThumbsUp, ThumbsDown, File } from 'lucide-react';
|
||||||
|
import { MessageTypes } from '../../constants/messageTypes';
|
||||||
|
import ExecutionStepsDisplay from './ExecutionStepsDisplay';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MessageRenderer - 消息渲染器组件
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Object} props.message - 消息对象
|
||||||
|
* @param {string} props.userAvatar - 用户头像 URL
|
||||||
|
* @returns {JSX.Element|null}
|
||||||
|
*/
|
||||||
|
const MessageRenderer = ({ message, userAvatar }) => {
|
||||||
|
switch (message.type) {
|
||||||
|
case MessageTypes.USER:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-end">
|
||||||
|
<HStack align="start" spacing={3} maxW="75%">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: 20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bgGradient="linear(to-br, blue.500, purple.600)"
|
||||||
|
boxShadow="0 8px 20px rgba(139, 92, 246, 0.4)"
|
||||||
|
>
|
||||||
|
<CardBody px={5} py={3}>
|
||||||
|
<Text fontSize="sm" color="white" whiteSpace="pre-wrap">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
{message.files && message.files.length > 0 && (
|
||||||
|
<HStack mt={2} flexWrap="wrap" spacing={2}>
|
||||||
|
{message.files.map((file, idx) => (
|
||||||
|
<Badge
|
||||||
|
key={idx}
|
||||||
|
bg="rgba(255, 255, 255, 0.2)"
|
||||||
|
color="white"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={1}
|
||||||
|
>
|
||||||
|
<File className="w-3 h-3" />
|
||||||
|
{file.name}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
<Avatar
|
||||||
|
src={userAvatar}
|
||||||
|
icon={<User className="w-4 h-4" />}
|
||||||
|
size="sm"
|
||||||
|
bgGradient="linear(to-br, blue.500, purple.600)"
|
||||||
|
boxShadow="0 0 12px rgba(139, 92, 246, 0.4)"
|
||||||
|
/>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_THINKING:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="start" spacing={3} maxW="75%">
|
||||||
|
<Avatar
|
||||||
|
icon={<Cpu className="w-4 h-4" />}
|
||||||
|
size="sm"
|
||||||
|
bgGradient="linear(to-br, purple.500, pink.500)"
|
||||||
|
boxShadow="0 0 12px rgba(236, 72, 153, 0.4)"
|
||||||
|
/>
|
||||||
|
<motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }}>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(16px) saturate(180%)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody px={5} py={3}>
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Spinner
|
||||||
|
size="sm"
|
||||||
|
color="purple.500"
|
||||||
|
emptyColor="gray.700"
|
||||||
|
thickness="3px"
|
||||||
|
speed="0.65s"
|
||||||
|
/>
|
||||||
|
<Text fontSize="sm" color="gray.300">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.AGENT_RESPONSE:
|
||||||
|
return (
|
||||||
|
<Flex justify="flex-start">
|
||||||
|
<HStack align="start" spacing={3} maxW="75%">
|
||||||
|
<Avatar
|
||||||
|
icon={<Cpu className="w-4 h-4" />}
|
||||||
|
size="sm"
|
||||||
|
bgGradient="linear(to-br, purple.500, pink.500)"
|
||||||
|
boxShadow="0 0 12px rgba(236, 72, 153, 0.4)"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, x: -20 }}
|
||||||
|
animate={{ opacity: 1, x: 0 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(16px) saturate(180%)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody px={5} py={3}>
|
||||||
|
<Text fontSize="sm" color="gray.100" whiteSpace="pre-wrap" lineHeight="relaxed">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{message.stepResults && message.stepResults.length > 0 && (
|
||||||
|
<Box mt={3}>
|
||||||
|
<ExecutionStepsDisplay steps={message.stepResults} plan={message.plan} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Flex
|
||||||
|
align="center"
|
||||||
|
gap={2}
|
||||||
|
mt={3}
|
||||||
|
pt={3}
|
||||||
|
borderTop="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
>
|
||||||
|
<Tooltip label="复制">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Copy className="w-4 h-4" />}
|
||||||
|
onClick={() => navigator.clipboard.writeText(message.content)}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{
|
||||||
|
color: 'white',
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="点赞">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ThumbsUp className="w-4 h-4" />}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{
|
||||||
|
color: 'green.400',
|
||||||
|
bg: 'rgba(16, 185, 129, 0.1)',
|
||||||
|
boxShadow: '0 0 12px rgba(16, 185, 129, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip label="点踩">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<ThumbsDown className="w-4 h-4" />}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
_hover={{
|
||||||
|
color: 'red.400',
|
||||||
|
bg: 'rgba(239, 68, 68, 0.1)',
|
||||||
|
boxShadow: '0 0 12px rgba(239, 68, 68, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
<Text fontSize="xs" color="gray.500" ml="auto">
|
||||||
|
{new Date(message.timestamp).toLocaleTimeString('zh-CN', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
})}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
case MessageTypes.ERROR:
|
||||||
|
return (
|
||||||
|
<Flex justify="center">
|
||||||
|
<motion.div initial={{ opacity: 0, scale: 0.9 }} animate={{ opacity: 1, scale: 1 }}>
|
||||||
|
<Card
|
||||||
|
bg="rgba(239, 68, 68, 0.1)"
|
||||||
|
backdropFilter="blur(16px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(239, 68, 68, 0.5)"
|
||||||
|
boxShadow="0 8px 32px 0 rgba(239, 68, 68, 0.37)"
|
||||||
|
>
|
||||||
|
<CardBody px={5} py={3}>
|
||||||
|
<Text fontSize="sm" color="red.400">
|
||||||
|
{message.content}
|
||||||
|
</Text>
|
||||||
|
</CardBody>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MessageRenderer;
|
||||||
474
src/views/AgentChat/components/ChatArea/index.js
Normal file
474
src/views/AgentChat/components/ChatArea/index.js
Normal file
@@ -0,0 +1,474 @@
|
|||||||
|
// src/views/AgentChat/components/ChatArea/index.js
|
||||||
|
// 中间聊天区域组件
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Tooltip,
|
||||||
|
IconButton,
|
||||||
|
Kbd,
|
||||||
|
HStack,
|
||||||
|
VStack,
|
||||||
|
Flex,
|
||||||
|
Text,
|
||||||
|
Tag,
|
||||||
|
TagLabel,
|
||||||
|
TagCloseButton,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import {
|
||||||
|
Send,
|
||||||
|
Menu,
|
||||||
|
RefreshCw,
|
||||||
|
Settings,
|
||||||
|
Cpu,
|
||||||
|
Zap,
|
||||||
|
Sparkles,
|
||||||
|
Paperclip,
|
||||||
|
Image as ImageIcon,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { AVAILABLE_MODELS } from '../../constants/models';
|
||||||
|
import { quickQuestions } from '../../constants/quickQuestions';
|
||||||
|
import { animations } from '../../constants/animations';
|
||||||
|
import MessageRenderer from './MessageRenderer';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ChatArea - 中间聊天区域组件
|
||||||
|
*
|
||||||
|
* @param {Object} props
|
||||||
|
* @param {Array} props.messages - 消息列表
|
||||||
|
* @param {string} props.inputValue - 输入框内容
|
||||||
|
* @param {Function} props.onInputChange - 输入框变化回调
|
||||||
|
* @param {boolean} props.isProcessing - 处理中状态
|
||||||
|
* @param {Function} props.onSendMessage - 发送消息回调
|
||||||
|
* @param {Function} props.onKeyPress - 键盘事件回调
|
||||||
|
* @param {Array} props.uploadedFiles - 已上传文件列表
|
||||||
|
* @param {Function} props.onFileSelect - 文件选择回调
|
||||||
|
* @param {Function} props.onFileRemove - 文件删除回调
|
||||||
|
* @param {string} props.selectedModel - 当前选中的模型 ID
|
||||||
|
* @param {boolean} props.isLeftSidebarOpen - 左侧栏是否展开
|
||||||
|
* @param {boolean} props.isRightSidebarOpen - 右侧栏是否展开
|
||||||
|
* @param {Function} props.onToggleLeftSidebar - 切换左侧栏回调
|
||||||
|
* @param {Function} props.onToggleRightSidebar - 切换右侧栏回调
|
||||||
|
* @param {Function} props.onNewSession - 新建会话回调
|
||||||
|
* @param {string} props.userAvatar - 用户头像 URL
|
||||||
|
* @param {RefObject} props.messagesEndRef - 消息列表底部引用
|
||||||
|
* @param {RefObject} props.inputRef - 输入框引用
|
||||||
|
* @param {RefObject} props.fileInputRef - 文件上传输入引用
|
||||||
|
* @returns {JSX.Element}
|
||||||
|
*/
|
||||||
|
const ChatArea = ({
|
||||||
|
messages,
|
||||||
|
inputValue,
|
||||||
|
onInputChange,
|
||||||
|
isProcessing,
|
||||||
|
onSendMessage,
|
||||||
|
onKeyPress,
|
||||||
|
uploadedFiles,
|
||||||
|
onFileSelect,
|
||||||
|
onFileRemove,
|
||||||
|
selectedModel,
|
||||||
|
isLeftSidebarOpen,
|
||||||
|
isRightSidebarOpen,
|
||||||
|
onToggleLeftSidebar,
|
||||||
|
onToggleRightSidebar,
|
||||||
|
onNewSession,
|
||||||
|
userAvatar,
|
||||||
|
messagesEndRef,
|
||||||
|
inputRef,
|
||||||
|
fileInputRef,
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Flex flex={1} direction="column">
|
||||||
|
{/* 顶部标题栏 - 深色毛玻璃 */}
|
||||||
|
<Box
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderBottom="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
px={6}
|
||||||
|
py={4}
|
||||||
|
boxShadow="0 8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="space-between">
|
||||||
|
<HStack spacing={4}>
|
||||||
|
{!isLeftSidebarOpen && (
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Menu className="w-4 h-4" />}
|
||||||
|
onClick={onToggleLeftSidebar}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
animate={{ rotate: 360 }}
|
||||||
|
transition={{ duration: 3, repeat: Infinity, ease: 'linear' }}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
icon={<Cpu className="w-6 h-6" />}
|
||||||
|
bgGradient="linear(to-br, purple.500, pink.500)"
|
||||||
|
boxShadow="0 0 20px rgba(236, 72, 153, 0.5)"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text
|
||||||
|
fontSize="xl"
|
||||||
|
fontWeight="bold"
|
||||||
|
bgGradient="linear(to-r, blue.400, purple.400)"
|
||||||
|
bgClip="text"
|
||||||
|
letterSpacing="tight"
|
||||||
|
>
|
||||||
|
价小前投研 AI
|
||||||
|
</Text>
|
||||||
|
<HStack spacing={2} mt={1}>
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, green.500, teal.500)"
|
||||||
|
color="white"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius="md"
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
gap={1}
|
||||||
|
boxShadow="0 2px 8px rgba(16, 185, 129, 0.3)"
|
||||||
|
>
|
||||||
|
<Zap className="w-3 h-3" />
|
||||||
|
智能分析
|
||||||
|
</Badge>
|
||||||
|
<Badge
|
||||||
|
bgGradient="linear(to-r, purple.500, pink.500)"
|
||||||
|
color="white"
|
||||||
|
px={2}
|
||||||
|
py={1}
|
||||||
|
borderRadius="md"
|
||||||
|
boxShadow="0 2px 8px rgba(139, 92, 246, 0.3)"
|
||||||
|
>
|
||||||
|
{AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name}
|
||||||
|
</Badge>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<Tooltip label="清空对话">
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<RefreshCw className="w-4 h-4" />}
|
||||||
|
onClick={onNewSession}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
{!isRightSidebarOpen && (
|
||||||
|
<motion.div whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }}>
|
||||||
|
<IconButton
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
icon={<Settings className="w-4 h-4" />}
|
||||||
|
onClick={onToggleRightSidebar}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.400"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 消息列表 */}
|
||||||
|
<Box
|
||||||
|
flex={1}
|
||||||
|
p={6}
|
||||||
|
bgGradient="linear(to-b, rgba(17, 24, 39, 0.5), rgba(17, 24, 39, 0.3))"
|
||||||
|
overflowY="auto"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
style={{ maxWidth: '896px', margin: '0 auto' }}
|
||||||
|
variants={animations.staggerContainer}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
>
|
||||||
|
<VStack spacing={4} align="stretch">
|
||||||
|
<AnimatePresence mode="popLayout">
|
||||||
|
{messages.map((message) => (
|
||||||
|
<motion.div
|
||||||
|
key={message.id}
|
||||||
|
variants={animations.fadeInUp}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit={{ opacity: 0, y: -20 }}
|
||||||
|
layout
|
||||||
|
>
|
||||||
|
<MessageRenderer message={message} userAvatar={userAvatar} />
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</AnimatePresence>
|
||||||
|
<div ref={messagesEndRef} />
|
||||||
|
</VStack>
|
||||||
|
</motion.div>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* 快捷问题 */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{messages.length <= 2 && !isProcessing && (
|
||||||
|
<motion.div
|
||||||
|
variants={animations.fadeInUp}
|
||||||
|
initial="initial"
|
||||||
|
animate="animate"
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<Box px={6} py={3}>
|
||||||
|
<Box maxW="896px" mx="auto">
|
||||||
|
<HStack fontSize="xs" color="gray.500" mb={2} fontWeight="medium" spacing={1}>
|
||||||
|
<Sparkles className="w-3 h-3" />
|
||||||
|
<Text>快速开始</Text>
|
||||||
|
</HStack>
|
||||||
|
<Box display="grid" gridTemplateColumns="repeat(2, 1fr)" gap={2}>
|
||||||
|
{quickQuestions.map((question, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
whileHover={{ scale: 1.02, y: -2 }}
|
||||||
|
whileTap={{ scale: 0.98 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
w="full"
|
||||||
|
justifyContent="flex-start"
|
||||||
|
h="auto"
|
||||||
|
py={3}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(12px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="gray.300"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(59, 130, 246, 0.15)',
|
||||||
|
borderColor: 'blue.400',
|
||||||
|
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
|
||||||
|
color: 'white',
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
onInputChange(question.text);
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text mr={2}>{question.emoji}</Text>
|
||||||
|
<Text>{question.text}</Text>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* 输入栏 - 深色毛玻璃 */}
|
||||||
|
<Box
|
||||||
|
bg="rgba(17, 24, 39, 0.8)"
|
||||||
|
backdropFilter="blur(20px) saturate(180%)"
|
||||||
|
borderTop="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
px={6}
|
||||||
|
py={4}
|
||||||
|
boxShadow="0 -8px 32px 0 rgba(31, 38, 135, 0.37)"
|
||||||
|
>
|
||||||
|
<Box maxW="896px" mx="auto">
|
||||||
|
{/* 已上传文件预览 */}
|
||||||
|
{uploadedFiles.length > 0 && (
|
||||||
|
<HStack mb={3} flexWrap="wrap" spacing={2}>
|
||||||
|
{uploadedFiles.map((file, idx) => (
|
||||||
|
<motion.div
|
||||||
|
key={idx}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
exit={{ opacity: 0, scale: 0.8 }}
|
||||||
|
>
|
||||||
|
<Tag
|
||||||
|
size="md"
|
||||||
|
variant="subtle"
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
borderWidth={1}
|
||||||
|
>
|
||||||
|
<TagLabel color="gray.300">{file.name}</TagLabel>
|
||||||
|
<TagCloseButton onClick={() => onFileRemove(idx)} color="gray.400" />
|
||||||
|
</Tag>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</HStack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<HStack spacing={2}>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*,.pdf,.doc,.docx,.txt"
|
||||||
|
onChange={onFileSelect}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Tooltip label="上传文件">
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
icon={<Paperclip className="w-5 h-5" />}
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Tooltip label="上传图片">
|
||||||
|
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
|
||||||
|
<IconButton
|
||||||
|
variant="ghost"
|
||||||
|
size="lg"
|
||||||
|
icon={<ImageIcon className="w-5 h-5" />}
|
||||||
|
onClick={() => {
|
||||||
|
fileInputRef.current?.setAttribute('accept', 'image/*');
|
||||||
|
fileInputRef.current?.click();
|
||||||
|
}}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
color="gray.300"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
_hover={{
|
||||||
|
bg: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
color: 'white',
|
||||||
|
boxShadow: '0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => onInputChange(e.target.value)}
|
||||||
|
onKeyDown={onKeyPress}
|
||||||
|
placeholder="输入你的问题... (Enter 发送, Shift+Enter 换行)"
|
||||||
|
isDisabled={isProcessing}
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
|
borderWidth={2}
|
||||||
|
bg="rgba(255, 255, 255, 0.05)"
|
||||||
|
backdropFilter="blur(10px)"
|
||||||
|
border="1px solid"
|
||||||
|
borderColor="rgba(255, 255, 255, 0.1)"
|
||||||
|
color="white"
|
||||||
|
_placeholder={{ color: 'gray.500' }}
|
||||||
|
_hover={{
|
||||||
|
borderColor: 'rgba(255, 255, 255, 0.2)',
|
||||||
|
}}
|
||||||
|
_focus={{
|
||||||
|
borderColor: 'purple.400',
|
||||||
|
boxShadow:
|
||||||
|
'0 0 0 1px var(--chakra-colors-purple-400), 0 0 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
bg: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
size="lg"
|
||||||
|
icon={!isProcessing && <Send className="w-5 h-5" />}
|
||||||
|
onClick={onSendMessage}
|
||||||
|
isLoading={isProcessing}
|
||||||
|
isDisabled={!inputValue.trim() || isProcessing}
|
||||||
|
bgGradient="linear(to-r, blue.500, purple.600)"
|
||||||
|
color="white"
|
||||||
|
_hover={{
|
||||||
|
bgGradient: 'linear(to-r, blue.600, purple.700)',
|
||||||
|
boxShadow: '0 8px 20px rgba(139, 92, 246, 0.4)',
|
||||||
|
}}
|
||||||
|
_active={{
|
||||||
|
transform: 'translateY(0)',
|
||||||
|
boxShadow: '0 4px 12px rgba(139, 92, 246, 0.3)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<HStack spacing={4} mt={2} fontSize="xs" color="gray.500">
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Enter
|
||||||
|
</Kbd>
|
||||||
|
<Text>发送</Text>
|
||||||
|
</HStack>
|
||||||
|
<HStack spacing={1}>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Shift
|
||||||
|
</Kbd>
|
||||||
|
<Text>+</Text>
|
||||||
|
<Kbd bg="rgba(255, 255, 255, 0.05)" color="gray.400" borderColor="rgba(255, 255, 255, 0.1)">
|
||||||
|
Enter
|
||||||
|
</Kbd>
|
||||||
|
<Text>换行</Text>
|
||||||
|
</HStack>
|
||||||
|
</HStack>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatArea;
|
||||||
Reference in New Issue
Block a user