From f536d68753cbc25daa9b7622b633c6d8b8133e99 Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 24 Nov 2025 15:18:32 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=88=9B=E5=BB=BA=20ChatArea=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=EF=BC=88=E5=90=AB=20MessageRenderer=E3=80=81?= =?UTF-8?q?ExecutionStepsDisplay=20=E5=AD=90=E7=BB=84=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ChatArea/ExecutionStepsDisplay.js | 117 +++++ .../components/ChatArea/MessageRenderer.js | 248 +++++++++ .../AgentChat/components/ChatArea/index.js | 474 ++++++++++++++++++ 3 files changed, 839 insertions(+) create mode 100644 src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js create mode 100644 src/views/AgentChat/components/ChatArea/MessageRenderer.js create mode 100644 src/views/AgentChat/components/ChatArea/index.js diff --git a/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js b/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js new file mode 100644 index 00000000..6b9321c2 --- /dev/null +++ b/src/views/AgentChat/components/ChatArea/ExecutionStepsDisplay.js @@ -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 ( + + + + + + + 执行详情 + + + {steps.length} 步骤 + + + + + + + {steps.map((result, idx) => ( + + + + + + 步骤 {idx + 1}: {result.tool_name} + + + {result.status} + + + + {result.execution_time?.toFixed(2)}s + + {result.error && ( + + ⚠️ {result.error} + + )} + + + + ))} + + + + + ); +}; + +export default ExecutionStepsDisplay; diff --git a/src/views/AgentChat/components/ChatArea/MessageRenderer.js b/src/views/AgentChat/components/ChatArea/MessageRenderer.js new file mode 100644 index 00000000..f056b9f3 --- /dev/null +++ b/src/views/AgentChat/components/ChatArea/MessageRenderer.js @@ -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 ( + + + + + + + {message.content} + + {message.files && message.files.length > 0 && ( + + {message.files.map((file, idx) => ( + + + {file.name} + + ))} + + )} + + + + } + size="sm" + bgGradient="linear(to-br, blue.500, purple.600)" + boxShadow="0 0 12px rgba(139, 92, 246, 0.4)" + /> + + + ); + + case MessageTypes.AGENT_THINKING: + return ( + + + } + size="sm" + bgGradient="linear(to-br, purple.500, pink.500)" + boxShadow="0 0 12px rgba(236, 72, 153, 0.4)" + /> + + + + + + + {message.content} + + + + + + + + ); + + case MessageTypes.AGENT_RESPONSE: + return ( + + + } + size="sm" + bgGradient="linear(to-br, purple.500, pink.500)" + boxShadow="0 0 12px rgba(236, 72, 153, 0.4)" + /> + + + + + {message.content} + + + {message.stepResults && message.stepResults.length > 0 && ( + + + + )} + + + + + } + 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)', + }} + /> + + + + + } + 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)', + }} + /> + + + + + } + 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)', + }} + /> + + + + {new Date(message.timestamp).toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + })} + + + + + + + + ); + + case MessageTypes.ERROR: + return ( + + + + + + {message.content} + + + + + + ); + + default: + return null; + } +}; + +export default MessageRenderer; diff --git a/src/views/AgentChat/components/ChatArea/index.js b/src/views/AgentChat/components/ChatArea/index.js new file mode 100644 index 00000000..4d44e8ce --- /dev/null +++ b/src/views/AgentChat/components/ChatArea/index.js @@ -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 ( + + {/* 顶部标题栏 - 深色毛玻璃 */} + + + + {!isLeftSidebarOpen && ( + + } + 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', + }} + /> + + )} + + + } + bgGradient="linear(to-br, purple.500, pink.500)" + boxShadow="0 0 20px rgba(236, 72, 153, 0.5)" + /> + + + + + 价小前投研 AI + + + + + 智能分析 + + + {AVAILABLE_MODELS.find((m) => m.id === selectedModel)?.name} + + + + + + + + + } + 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)', + }} + /> + + + {!isRightSidebarOpen && ( + + } + 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', + }} + /> + + )} + + + + + {/* 消息列表 */} + + + + + {messages.map((message) => ( + + + + ))} + +
+ + + + + {/* 快捷问题 */} + + {messages.length <= 2 && !isProcessing && ( + + + + + + 快速开始 + + + {quickQuestions.map((question, idx) => ( + + + + ))} + + + + + )} + + + {/* 输入栏 - 深色毛玻璃 */} + + + {/* 已上传文件预览 */} + {uploadedFiles.length > 0 && ( + + {uploadedFiles.map((file, idx) => ( + + + {file.name} + onFileRemove(idx)} color="gray.400" /> + + + ))} + + )} + + + + + + + } + 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)', + }} + /> + + + + + + } + 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)', + }} + /> + + + + 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)', + }} + /> + + + } + 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)', + }} + /> + + + + + + + Enter + + 发送 + + + + Shift + + + + + Enter + + 换行 + + + + + + ); +}; + +export default ChatArea;