Files
vf_react/src/views/AgentChat/index.js
2025-12-11 14:48:00 +08:00

378 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/views/AgentChat/index.js
// 超炫酷的 AI 投研助手 - HeroUI v3 现代深色主题版本
// 使用 Framer Motion 物理动画引擎 + 毛玻璃效果
// 支持两种模式:单一聊天模式 & 投研会议室模式
import React, { useState } from 'react';
import { Box, Flex, useToast, HStack, Button, Tooltip, VStack, Text, Icon } from '@chakra-ui/react';
import { motion, AnimatePresence } from 'framer-motion';
import { MessageSquare, Users, Crown, Lock } from 'lucide-react';
import { useAuth } from '@contexts/AuthContext';
import { useSubscription } from '@hooks/useSubscription';
// 常量配置 - 从 TypeScript 模块导入
import { DEFAULT_MODEL_ID } from './constants/models';
import { DEFAULT_SELECTED_TOOLS } from './constants/tools';
// 拆分后的子组件
import LeftSidebar from './components/LeftSidebar';
import ChatArea from './components/ChatArea';
import RightSidebar from './components/RightSidebar';
import MeetingRoom from './components/MeetingRoom';
// 自定义 Hooks
import { useAgentSessions, useAgentChat, useFileUpload } from './hooks';
/**
* 聊天模式枚举
*/
const ChatMode = {
SINGLE: 'single', // 单一聊天模式
MEETING: 'meeting', // 投研会议室模式
};
/**
* Agent Chat - 主组件HeroUI v3 深色主题)
*
* 架构说明:
* - Phase 1: 常量配置已提取到 constants/ 目录TypeScript
* - Phase 2: UI 组件已拆分到 components/ 目录
* - Phase 3: 业务逻辑已提取到 hooks/ 目录TypeScript
*
* 主组件职责:
* 1. 组合各个自定义 Hooks
* 2. 管理 UI 状态(侧边栏开关、模型选择、工具选择、聊天模式)
* 3. 组合渲染子组件
*
* 新增功能2024-11
* - 投研会议室模式:多 AI 角色协作讨论投资议题
*/
const AgentChat = () => {
const { user, isAuthenticated } = useAuth();
const toast = useToast();
const { hasSubscriptionLevel, openSubscriptionModal } = useSubscription();
// ==================== MAX 权限检查 ====================
const isMaxUser = user?.subscription_type === 'max' || hasSubscriptionLevel('max');
// ==================== 聊天模式状态 ====================
const [chatMode, setChatMode] = useState(ChatMode.SINGLE);
// ==================== UI 状态(主组件管理)====================
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL_ID);
const [selectedTools, setSelectedTools] = useState(DEFAULT_SELECTED_TOOLS);
const [isLeftSidebarOpen, setIsLeftSidebarOpen] = useState(true);
const [isRightSidebarOpen, setIsRightSidebarOpen] = useState(true);
// ==================== 自定义 Hooks ====================
// 文件上传 Hook
const { uploadedFiles, fileInputRef, handleFileSelect, removeFile, clearFiles } = useFileUpload();
// 会话管理 Hook需要先创建 messages state
const [messages, setMessages] = useState([]);
const {
sessions,
currentSessionId,
setCurrentSessionId,
isLoadingSessions,
loadSessions,
switchSession,
createNewSession,
} = useAgentSessions({
user,
setMessages,
});
// 消息处理 Hook
const {
inputValue,
setInputValue,
isProcessing,
handleSendMessage,
handleKeyPress,
} = useAgentChat({
user,
currentSessionId,
setCurrentSessionId,
selectedModel,
selectedTools,
uploadedFiles,
clearFiles,
toast,
loadSessions,
messages,
setMessages,
});
// ==================== 输入框引用(保留在主组件)====================
const inputRef = React.useRef(null);
// ==================== 渲染组件 ====================
// 非 MAX 用户显示升级提示
if (!isMaxUser) {
return (
<Flex
h="100%"
bg="gray.900"
align="center"
justify="center"
direction="column"
position="relative"
overflow="hidden"
>
{/* 背景装饰 */}
<Box
position="absolute"
top="20%"
left="10%"
w="400px"
h="400px"
bg="purple.500"
filter="blur(150px)"
opacity="0.15"
borderRadius="full"
/>
<Box
position="absolute"
bottom="20%"
right="10%"
w="350px"
h="350px"
bg="pink.500"
filter="blur(150px)"
opacity="0.1"
borderRadius="full"
/>
{/* 升级提示卡片 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<VStack
spacing={6}
p={10}
bg="rgba(17, 24, 39, 0.8)"
backdropFilter="blur(20px)"
borderRadius="2xl"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
boxShadow="0 25px 50px -12px rgba(0, 0, 0, 0.5)"
maxW="500px"
textAlign="center"
>
{/* 图标 */}
<Box
p={4}
bg="linear-gradient(135deg, rgba(168, 85, 247, 0.2), rgba(236, 72, 153, 0.2))"
borderRadius="full"
border="1px solid"
borderColor="rgba(168, 85, 247, 0.3)"
>
<Icon as={Crown} boxSize={12} color="purple.400" />
</Box>
{/* 标题 */}
<VStack spacing={2}>
<Text
fontSize="2xl"
fontWeight="bold"
bgGradient="linear(to-r, purple.300, pink.300)"
bgClip="text"
>
价小前投研 AI
</Text>
<HStack spacing={2}>
<Icon as={Lock} boxSize={4} color="gray.400" />
<Text color="gray.400" fontSize="md">
Max 旗舰版专属功能
</Text>
</HStack>
</VStack>
{/* 描述 */}
<Text color="gray.300" fontSize="md" lineHeight="tall">
AI 投研助手是 Max 旗舰版用户的专属功能
提供智能股票分析投研会议室多模型对话等高级功能
升级到 Max 版即可解锁全部 AI 能力
</Text>
{/* 功能列表 */}
<VStack spacing={2} align="start" w="100%">
{[
'智能投研助手:一对一 AI 对话分析',
'投研会议室:多 AI 角色协作讨论',
'多模型选择DeepSeek、Kimi 等',
'专业工具:股票查询、新闻搜索、财务分析',
].map((feature, idx) => (
<HStack key={idx} spacing={3} color="gray.300">
<Box w={2} h={2} bg="purple.400" borderRadius="full" />
<Text fontSize="sm">{feature}</Text>
</HStack>
))}
</VStack>
{/* 升级按钮 */}
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} style={{ width: '100%' }}>
<Button
w="100%"
size="lg"
bgGradient="linear(to-r, purple.500, pink.500)"
color="white"
_hover={{
bgGradient: 'linear(to-r, purple.600, pink.600)',
boxShadow: '0 10px 40px rgba(168, 85, 247, 0.4)',
}}
leftIcon={<Crown className="w-5 h-5" />}
onClick={openSubscriptionModal}
>
升级到 Max 旗舰版
</Button>
</motion.div>
{/* 当前订阅状态 */}
{isAuthenticated && user && (
<Text color="gray.500" fontSize="sm">
当前订阅{user.subscription_type === 'pro' ? 'Pro 专业版' : '免费版'}
</Text>
)}
</VStack>
</motion.div>
</Flex>
);
}
return (
<Flex h="100%" position="relative" bg="gray.900" direction="column">
{/* 模式切换栏 */}
<Box
bg="rgba(17, 24, 39, 0.95)"
borderBottom="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
px={4}
py={2}
>
<HStack spacing={2} justify="center">
<Tooltip label="单一聊天模式:与 AI 助手一对一对话">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
size="sm"
leftIcon={<MessageSquare className="w-4 h-4" />}
variant={chatMode === ChatMode.SINGLE ? 'solid' : 'ghost'}
colorScheme={chatMode === ChatMode.SINGLE ? 'purple' : 'gray'}
onClick={() => setChatMode(ChatMode.SINGLE)}
bg={chatMode === ChatMode.SINGLE ? 'purple.500' : 'transparent'}
color={chatMode === ChatMode.SINGLE ? 'white' : 'gray.400'}
_hover={{
bg: chatMode === ChatMode.SINGLE ? 'purple.600' : 'whiteAlpha.100',
}}
>
智能助手
</Button>
</motion.div>
</Tooltip>
<Tooltip label="投研会议室:多位 AI 分析师协作讨论投资议题">
<motion.div whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
<Button
size="sm"
leftIcon={<Users className="w-4 h-4" />}
variant={chatMode === ChatMode.MEETING ? 'solid' : 'ghost'}
colorScheme={chatMode === ChatMode.MEETING ? 'orange' : 'gray'}
onClick={() => setChatMode(ChatMode.MEETING)}
bg={chatMode === ChatMode.MEETING ? 'orange.500' : 'transparent'}
color={chatMode === ChatMode.MEETING ? 'white' : 'gray.400'}
_hover={{
bg: chatMode === ChatMode.MEETING ? 'orange.600' : 'whiteAlpha.100',
}}
>
投研会议室
</Button>
</motion.div>
</Tooltip>
</HStack>
</Box>
{/* 主内容区 */}
<Flex flex={1} overflow="hidden">
<AnimatePresence mode="wait">
{chatMode === ChatMode.SINGLE ? (
<motion.div
key="single-chat"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 20 }}
transition={{ duration: 0.3 }}
style={{ display: 'flex', flex: 1, height: '100%' }}
>
{/* 左侧栏 */}
<LeftSidebar
isOpen={isLeftSidebarOpen}
onClose={() => setIsLeftSidebarOpen(false)}
sessions={sessions}
currentSessionId={currentSessionId}
onSessionSwitch={switchSession}
onNewSession={createNewSession}
isLoadingSessions={isLoadingSessions}
user={user}
/>
{/* 中间聊天区 */}
<ChatArea
messages={messages}
inputValue={inputValue}
onInputChange={setInputValue}
isProcessing={isProcessing}
onSendMessage={handleSendMessage}
onKeyPress={handleKeyPress}
uploadedFiles={uploadedFiles}
onFileSelect={handleFileSelect}
onFileRemove={removeFile}
selectedModel={selectedModel}
isLeftSidebarOpen={isLeftSidebarOpen}
isRightSidebarOpen={isRightSidebarOpen}
onToggleLeftSidebar={() => setIsLeftSidebarOpen(true)}
onToggleRightSidebar={() => setIsRightSidebarOpen(true)}
onNewSession={createNewSession}
userAvatar={user?.avatar}
inputRef={inputRef}
fileInputRef={fileInputRef}
/>
{/* 右侧栏 */}
<RightSidebar
isOpen={isRightSidebarOpen}
onClose={() => setIsRightSidebarOpen(false)}
selectedModel={selectedModel}
onModelChange={setSelectedModel}
selectedTools={selectedTools}
onToolsChange={setSelectedTools}
sessionsCount={sessions.length}
messagesCount={messages.length}
/>
</motion.div>
) : (
<motion.div
key="meeting-room"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.3 }}
style={{ flex: 1, height: '100%' }}
>
{/* 投研会议室 */}
<MeetingRoom user={user} onToast={toast} />
</motion.div>
)}
</AnimatePresence>
</Flex>
</Flex>
);
};
export default AgentChat;