update pay function

This commit is contained in:
2025-11-28 14:49:16 +08:00
parent 18f8f75116
commit 9b7a221315
11 changed files with 2917 additions and 46 deletions

View File

@@ -0,0 +1,267 @@
// src/views/AgentChat/components/MeetingRoom/MeetingMessageBubble.js
// 会议消息气泡组件
import React from 'react';
import { motion } from 'framer-motion';
import {
Box,
Flex,
HStack,
VStack,
Text,
Avatar,
Badge,
IconButton,
Tooltip,
Card,
CardBody,
} from '@chakra-ui/react';
import {
TrendingUp,
TrendingDown,
BarChart2,
Users,
Crown,
Copy,
ThumbsUp,
} from 'lucide-react';
import { getRoleConfig, MEETING_ROLES } from '../../constants/meetingRoles';
import { MarkdownWithCharts } from '@components/ChatBot/MarkdownWithCharts';
/**
* 获取角色图标
*/
const getRoleIcon = (roleType) => {
switch (roleType) {
case 'bull':
return <TrendingUp className="w-4 h-4" />;
case 'bear':
return <TrendingDown className="w-4 h-4" />;
case 'quant':
return <BarChart2 className="w-4 h-4" />;
case 'retail':
return <Users className="w-4 h-4" />;
case 'manager':
return <Crown className="w-4 h-4" />;
default:
return <Users className="w-4 h-4" />;
}
};
/**
* MeetingMessageBubble - 会议消息气泡组件
*
* @param {Object} props
* @param {Object} props.message - 消息对象
* @param {boolean} props.isLatest - 是否是最新消息
* @returns {JSX.Element}
*/
const MeetingMessageBubble = ({ message, isLatest }) => {
const roleConfig = getRoleConfig(message.role_id) || {
name: message.role_name,
nickname: message.nickname,
color: message.color,
roleType: 'retail',
};
const isUser = message.role_id === 'user';
const isManager = roleConfig.roleType === 'manager';
const isConclusion = message.is_conclusion;
// 复制到剪贴板
const handleCopy = () => {
navigator.clipboard.writeText(message.content);
};
return (
<Flex
direction="column"
align={isUser ? 'flex-end' : 'flex-start'}
w="100%"
>
{/* 消息头部:角色信息 */}
<HStack
spacing={2}
mb={2}
flexDirection={isUser ? 'row-reverse' : 'row'}
>
<motion.div
whileHover={{ scale: 1.1 }}
transition={{ type: 'spring', stiffness: 400 }}
>
<Avatar
size="sm"
icon={getRoleIcon(roleConfig.roleType)}
bg={roleConfig.color}
boxShadow={`0 0 12px ${roleConfig.color}40`}
/>
</motion.div>
<VStack spacing={0} align={isUser ? 'flex-end' : 'flex-start'}>
<HStack spacing={2}>
<Text
fontSize="sm"
fontWeight="bold"
color={roleConfig.color}
>
{roleConfig.name}
</Text>
{roleConfig.nickname !== roleConfig.name && (
<Text fontSize="xs" color="gray.500">
@{roleConfig.nickname}
</Text>
)}
{isManager && (
<Badge
colorScheme="purple"
size="sm"
variant="subtle"
>
主持人
</Badge>
)}
{isConclusion && (
<Badge
colorScheme="green"
size="sm"
variant="solid"
>
最终结论
</Badge>
)}
</HStack>
<Text fontSize="xs" color="gray.500">
{message.round_number} ·{' '}
{new Date(message.timestamp).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
})}
</Text>
</VStack>
</HStack>
{/* 消息内容卡片 */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.2 }}
style={{ maxWidth: isUser ? '70%' : '85%', width: '100%' }}
>
<Card
bg={
isUser
? `linear-gradient(135deg, ${roleConfig.color}20, ${roleConfig.color}10)`
: isConclusion
? 'linear-gradient(135deg, rgba(139, 92, 246, 0.15), rgba(139, 92, 246, 0.05))'
: 'rgba(255, 255, 255, 0.05)'
}
border="1px solid"
borderColor={
isConclusion
? 'purple.500'
: `${roleConfig.color}30`
}
borderRadius="xl"
overflow="hidden"
boxShadow={
isConclusion
? '0 0 20px rgba(139, 92, 246, 0.3)'
: isLatest
? `0 4px 20px ${roleConfig.color}20`
: 'none'
}
>
{/* 结论标题 */}
{isConclusion && (
<Box
bgGradient="linear(to-r, purple.600, violet.600)"
px={4}
py={2}
>
<HStack>
<Crown className="w-4 h-4" />
<Text fontWeight="bold" fontSize="sm" color="white">
基金经理投资建议
</Text>
</HStack>
</Box>
)}
<CardBody p={4}>
<Box
fontSize="sm"
color="gray.100"
lineHeight="tall"
sx={{
'& p': { mb: 2 },
'& h1, & h2, & h3': { color: 'gray.50', fontWeight: 'bold' },
'& ul, & ol': { pl: 4 },
'& li': { mb: 1 },
'& code': {
bg: 'rgba(255,255,255,0.1)',
px: 1,
borderRadius: 'sm',
},
'& blockquote': {
borderLeftWidth: '3px',
borderLeftColor: roleConfig.color,
pl: 3,
color: 'gray.300',
fontStyle: 'italic',
},
'& strong': { color: roleConfig.color },
}}
>
<MarkdownWithCharts content={message.content} variant="dark" />
</Box>
{/* 操作按钮 */}
<Flex mt={3} pt={3} borderTop="1px solid" borderColor="whiteAlpha.100">
<HStack spacing={2}>
<Tooltip label="复制">
<IconButton
size="xs"
variant="ghost"
icon={<Copy className="w-3 h-3" />}
onClick={handleCopy}
color="gray.500"
_hover={{ color: 'white', bg: 'whiteAlpha.100' }}
/>
</Tooltip>
<Tooltip label="有用">
<IconButton
size="xs"
variant="ghost"
icon={<ThumbsUp className="w-3 h-3" />}
color="gray.500"
_hover={{ color: 'green.400', bg: 'green.900' }}
/>
</Tooltip>
</HStack>
{/* 角色标签 */}
<Box ml="auto">
<Badge
bg={`${roleConfig.color}20`}
color={roleConfig.color}
fontSize="xs"
px={2}
py={0.5}
borderRadius="full"
>
{roleConfig.roleType === 'bull' && '📈 看多'}
{roleConfig.roleType === 'bear' && '📉 看空'}
{roleConfig.roleType === 'quant' && '📊 量化'}
{roleConfig.roleType === 'retail' && '🌱 散户'}
{roleConfig.roleType === 'manager' && '👔 决策'}
</Badge>
</Box>
</Flex>
</CardBody>
</Card>
</motion.div>
</Flex>
);
};
export default MeetingMessageBubble;

View File

@@ -0,0 +1,301 @@
// src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js
// 会议角色面板组件 - 显示所有参会角色状态
import React from 'react';
import { motion } from 'framer-motion';
import {
Box,
VStack,
HStack,
Text,
Avatar,
Badge,
Tooltip,
} from '@chakra-ui/react';
import {
TrendingUp,
TrendingDown,
BarChart2,
Users,
Crown,
Mic,
MicOff,
} from 'lucide-react';
import { MEETING_ROLES, MeetingStatus } from '../../constants/meetingRoles';
/**
* 获取角色图标
*/
const getRoleIcon = (roleType) => {
switch (roleType) {
case 'bull':
return <TrendingUp className="w-4 h-4" />;
case 'bear':
return <TrendingDown className="w-4 h-4" />;
case 'quant':
return <BarChart2 className="w-4 h-4" />;
case 'retail':
return <Users className="w-4 h-4" />;
case 'manager':
return <Crown className="w-4 h-4" />;
default:
return <Users className="w-4 h-4" />;
}
};
/**
* RoleCard - 单个角色卡片
*/
const RoleCard = ({ role, isSpeaking }) => {
return (
<Tooltip label={role.description} placement="right" hasArrow>
<motion.div
animate={
isSpeaking
? {
scale: [1, 1.02, 1],
boxShadow: [
`0 0 0px ${role.color}`,
`0 0 20px ${role.color}`,
`0 0 0px ${role.color}`,
],
}
: {}
}
transition={{
duration: 1.5,
repeat: isSpeaking ? Infinity : 0,
}}
>
<Box
p={3}
bg={isSpeaking ? `${role.color}15` : 'rgba(255, 255, 255, 0.03)'}
border="1px solid"
borderColor={isSpeaking ? role.color : 'rgba(255, 255, 255, 0.1)'}
borderRadius="lg"
cursor="pointer"
transition="all 0.2s"
_hover={{
bg: 'rgba(255, 255, 255, 0.05)',
borderColor: `${role.color}50`,
}}
>
<HStack spacing={3}>
{/* 头像 */}
<Box position="relative">
<Avatar
size="sm"
icon={getRoleIcon(role.roleType)}
bg={role.color}
boxShadow={isSpeaking ? `0 0 12px ${role.color}` : 'none'}
/>
{isSpeaking && (
<motion.div
animate={{ scale: [1, 1.2, 1] }}
transition={{ duration: 0.5, repeat: Infinity }}
style={{
position: 'absolute',
bottom: -2,
right: -2,
}}
>
<Box
bg="green.500"
borderRadius="full"
p={1}
boxShadow="0 0 8px rgba(34, 197, 94, 0.6)"
>
<Mic className="w-2 h-2" />
</Box>
</motion.div>
)}
</Box>
{/* 角色信息 */}
<VStack spacing={0} align="start" flex={1}>
<Text
fontSize="sm"
fontWeight="bold"
color={isSpeaking ? role.color : 'gray.200'}
>
{role.name}
</Text>
<Text fontSize="xs" color="gray.500">
@{role.nickname}
</Text>
</VStack>
{/* 状态指示 */}
<Badge
size="sm"
colorScheme={
role.roleType === 'bull'
? 'green'
: role.roleType === 'bear'
? 'red'
: role.roleType === 'quant'
? 'blue'
: role.roleType === 'manager'
? 'purple'
: 'yellow'
}
variant="subtle"
fontSize="10px"
>
{role.roleType === 'bull' && '多头'}
{role.roleType === 'bear' && '空头'}
{role.roleType === 'quant' && '量化'}
{role.roleType === 'retail' && '散户'}
{role.roleType === 'manager' && '主持'}
</Badge>
</HStack>
</Box>
</motion.div>
</Tooltip>
);
};
/**
* MeetingRolePanel - 会议角色面板
*
* @param {Object} props
* @param {string|null} props.speakingRoleId - 正在发言的角色 ID
* @param {MeetingStatus} props.status - 会议状态
* @returns {JSX.Element}
*/
const MeetingRolePanel = ({ speakingRoleId, status }) => {
// 将角色按类型分组
const analysts = Object.values(MEETING_ROLES).filter(
(r) => r.roleType !== 'manager'
);
const manager = MEETING_ROLES.fund_manager;
return (
<Box
w="220px"
bg="rgba(17, 24, 39, 0.8)"
borderRight="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
p={4}
overflowY="auto"
>
{/* 标题 */}
<Text
fontSize="xs"
fontWeight="bold"
color="gray.500"
textTransform="uppercase"
letterSpacing="wider"
mb={3}
>
参会成员
</Text>
<VStack spacing={2} align="stretch">
{/* 分析师组 */}
<Text fontSize="xs" color="gray.600" mt={2} mb={1}>
分析团队
</Text>
{analysts.map((role) => (
<RoleCard
key={role.id}
role={role}
isSpeaking={speakingRoleId === role.id}
/>
))}
{/* 分隔线 */}
<Box
h="1px"
bg="rgba(255, 255, 255, 0.1)"
my={2}
/>
{/* 基金经理 */}
<Text fontSize="xs" color="gray.600" mb={1}>
决策层
</Text>
<RoleCard
role={manager}
isSpeaking={speakingRoleId === 'fund_manager'}
/>
</VStack>
{/* 会议状态指示 */}
<Box
mt={4}
p={3}
bg="rgba(255, 255, 255, 0.03)"
borderRadius="lg"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
>
<Text fontSize="xs" color="gray.500" mb={2}>
会议状态
</Text>
<HStack spacing={2}>
<Box
w={2}
h={2}
borderRadius="full"
bg={
status === MeetingStatus.IDLE
? 'gray.500'
: status === MeetingStatus.CONCLUDED
? 'green.500'
: status === MeetingStatus.ERROR
? 'red.500'
: 'blue.500'
}
boxShadow={
status !== MeetingStatus.IDLE
? `0 0 8px ${
status === MeetingStatus.CONCLUDED
? 'rgba(34, 197, 94, 0.6)'
: status === MeetingStatus.ERROR
? 'rgba(239, 68, 68, 0.6)'
: 'rgba(59, 130, 246, 0.6)'
}`
: 'none'
}
/>
<Text fontSize="xs" color="gray.400">
{status === MeetingStatus.IDLE && '等待开始'}
{status === MeetingStatus.STARTING && '召集中...'}
{status === MeetingStatus.DISCUSSING && '讨论中'}
{status === MeetingStatus.SPEAKING && '发言中'}
{status === MeetingStatus.WAITING_INPUT && '等待输入'}
{status === MeetingStatus.CONCLUDED && '已结束'}
{status === MeetingStatus.ERROR && '异常'}
</Text>
</HStack>
</Box>
{/* 角色说明 */}
<Box mt={4}>
<Text fontSize="xs" color="gray.600" mb={2}>
💡 角色说明
</Text>
<VStack spacing={1} align="start">
<Text fontSize="10px" color="gray.500">
📈 多头挖掘利好因素
</Text>
<Text fontSize="10px" color="gray.500">
📉 空头发现风险隐患
</Text>
<Text fontSize="10px" color="gray.500">
📊 量化技术指标分析
</Text>
<Text fontSize="10px" color="gray.500">
🌱 散户反向指标参考
</Text>
<Text fontSize="10px" color="gray.500">
👔 主持综合判断决策
</Text>
</VStack>
</Box>
</Box>
);
};
export default MeetingRolePanel;

View File

@@ -0,0 +1,294 @@
// src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js
// 会议欢迎界面 - 显示议题建议
import React from 'react';
import { motion } from 'framer-motion';
import {
Box,
VStack,
HStack,
Text,
SimpleGrid,
Card,
CardBody,
Icon,
} from '@chakra-ui/react';
import {
TrendingUp,
FileText,
AlertTriangle,
LineChart,
Briefcase,
Building,
Zap,
Target,
} from 'lucide-react';
/**
* 议题建议列表
*/
const TOPIC_SUGGESTIONS = [
{
id: 1,
icon: FileText,
color: 'blue.400',
title: '财报分析',
example: '分析贵州茅台2024年三季报评估投资价值',
},
{
id: 2,
icon: AlertTriangle,
color: 'red.400',
title: '风险评估',
example: '分析宁德时代面临的主要风险和挑战',
},
{
id: 3,
icon: TrendingUp,
color: 'green.400',
title: '趋势判断',
example: '当前AI概念股还能不能追',
},
{
id: 4,
icon: LineChart,
color: 'purple.400',
title: '技术分析',
example: '从技术面分析上证指数短期走势',
},
{
id: 5,
icon: Building,
color: 'orange.400',
title: '行业研究',
example: '新能源汽车行业2025年投资机会分析',
},
{
id: 6,
icon: Target,
color: 'cyan.400',
title: '事件驱动',
example: '美联储降息对A股的影响分析',
},
];
/**
* TopicCard - 议题建议卡片
*/
const TopicCard = ({ suggestion, onClick }) => {
const IconComponent = suggestion.icon;
return (
<motion.div
whileHover={{ scale: 1.02, y: -4 }}
whileTap={{ scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 25 }}
>
<Card
bg="rgba(255, 255, 255, 0.03)"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
borderRadius="xl"
cursor="pointer"
onClick={() => onClick(suggestion.example)}
transition="all 0.2s"
_hover={{
bg: 'rgba(255, 255, 255, 0.05)',
borderColor: suggestion.color,
boxShadow: `0 8px 30px ${suggestion.color}20`,
}}
>
<CardBody p={4}>
<VStack align="start" spacing={3}>
<HStack spacing={3}>
<Box
p={2}
bg={`${suggestion.color}15`}
borderRadius="lg"
>
<Icon
as={IconComponent}
color={suggestion.color}
boxSize={5}
/>
</Box>
<Text
fontSize="sm"
fontWeight="bold"
color="gray.200"
>
{suggestion.title}
</Text>
</HStack>
<Text
fontSize="xs"
color="gray.400"
lineHeight="tall"
>
{suggestion.example}
</Text>
</VStack>
</CardBody>
</Card>
</motion.div>
);
};
/**
* MeetingWelcome - 会议欢迎界面
*
* @param {Object} props
* @param {Function} props.onTopicSelect - 选择议题回调
* @returns {JSX.Element}
*/
const MeetingWelcome = ({ onTopicSelect }) => {
return (
<Box
flex={1}
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
p={8}
>
<VStack spacing={8} maxW="800px" w="100%">
{/* 标题区域 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<VStack spacing={4}>
<motion.div
animate={{
rotate: [0, 5, -5, 0],
}}
transition={{
duration: 4,
repeat: Infinity,
ease: 'easeInOut',
}}
>
<Box
p={4}
bgGradient="linear(to-br, orange.400, red.500)"
borderRadius="2xl"
boxShadow="0 0 40px rgba(251, 146, 60, 0.4)"
>
<Briefcase className="w-10 h-10" />
</Box>
</motion.div>
<Text
fontSize="2xl"
fontWeight="bold"
bgGradient="linear(to-r, orange.300, red.300)"
bgClip="text"
textAlign="center"
>
欢迎来到投研会议室
</Text>
<Text
fontSize="sm"
color="gray.400"
textAlign="center"
maxW="500px"
>
多位 AI 分析师将从不同角度分析您的投资议题
包括多头空头量化分析师和散户视角
最终由基金经理给出投资建议
</Text>
</VStack>
</motion.div>
{/* 特点说明 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.3 }}
>
<HStack
spacing={6}
flexWrap="wrap"
justify="center"
>
{[
{ icon: '📈', text: '多空对决' },
{ icon: '📊', text: '量化分析' },
{ icon: '🎯', text: '投资建议' },
{ icon: '💬', text: '实时参与' },
].map((item, index) => (
<HStack
key={index}
spacing={2}
bg="rgba(255, 255, 255, 0.03)"
px={4}
py={2}
borderRadius="full"
border="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
>
<Text fontSize="lg">{item.icon}</Text>
<Text fontSize="sm" color="gray.300">
{item.text}
</Text>
</HStack>
))}
</HStack>
</motion.div>
{/* 议题建议 */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5 }}
style={{ width: '100%' }}
>
<VStack spacing={4} w="100%">
<HStack spacing={2}>
<Zap className="w-4 h-4" style={{ color: '#F59E0B' }} />
<Text fontSize="sm" color="gray.400">
点击下方议题快速开始或输入自定义议题
</Text>
</HStack>
<SimpleGrid columns={{ base: 1, md: 2, lg: 3 }} spacing={4} w="100%">
{TOPIC_SUGGESTIONS.map((suggestion) => (
<TopicCard
key={suggestion.id}
suggestion={suggestion}
onClick={onTopicSelect}
/>
))}
</SimpleGrid>
</VStack>
</motion.div>
{/* 使用提示 */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.7 }}
>
<Box
bg="rgba(251, 146, 60, 0.1)"
border="1px solid"
borderColor="rgba(251, 146, 60, 0.2)"
borderRadius="lg"
p={4}
maxW="500px"
>
<Text fontSize="xs" color="orange.300">
💡 提示会议进行中您可以随时插话参与讨论
您的观点会影响分析师的判断
讨论结束后基金经理会给出最终的投资建议
</Text>
</Box>
</motion.div>
</VStack>
</Box>
);
};
export default MeetingWelcome;

View File

@@ -0,0 +1,442 @@
// src/views/AgentChat/components/MeetingRoom/index.js
// 投研会议室主组件
import React, { useRef, useEffect } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
Box,
Flex,
VStack,
HStack,
Text,
Input,
IconButton,
Avatar,
Badge,
Spinner,
Tooltip,
Kbd,
useColorModeValue,
} from '@chakra-ui/react';
import {
Send,
Users,
RefreshCw,
MessageCircle,
CheckCircle,
AlertCircle,
} from 'lucide-react';
import {
MEETING_ROLES,
MeetingStatus,
getRoleConfig,
} from '../../constants/meetingRoles';
import { useInvestmentMeeting } from '../../hooks/useInvestmentMeeting';
import MeetingMessageBubble from './MeetingMessageBubble';
import MeetingRolePanel from './MeetingRolePanel';
import MeetingWelcome from './MeetingWelcome';
/**
* MeetingRoom - 投研会议室主组件
*
* @param {Object} props
* @param {Object} props.user - 当前用户信息
* @param {Function} props.onToast - Toast 通知函数
* @returns {JSX.Element}
*/
const MeetingRoom = ({ user, onToast }) => {
const inputRef = useRef(null);
const messagesEndRef = useRef(null);
// 使用投研会议 Hook
const {
messages,
status,
speakingRoleId,
currentRound,
isConcluded,
conclusion,
inputValue,
setInputValue,
startMeeting,
continueMeeting,
sendUserMessage,
resetMeeting,
currentTopic,
isLoading,
} = useInvestmentMeeting({
userId: user?.id ? String(user.id) : 'anonymous',
userNickname: user?.nickname || '匿名用户',
onToast,
});
// 自动滚动到底部
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages]);
// 处理键盘事件
const handleKeyPress = (e) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
// 处理发送
const handleSend = () => {
if (!inputValue.trim()) return;
if (status === MeetingStatus.IDLE) {
// 启动新会议
startMeeting(inputValue.trim());
setInputValue('');
} else if (
status === MeetingStatus.WAITING_INPUT ||
status === MeetingStatus.CONCLUDED
) {
// 用户插话或开始新话题
if (isConcluded) {
// 如果已结论,开始新会议
resetMeeting();
startMeeting(inputValue.trim());
} else {
sendUserMessage(inputValue.trim());
}
setInputValue('');
}
};
// 获取状态提示文字
const getStatusText = () => {
switch (status) {
case MeetingStatus.IDLE:
return '请输入投研议题,开始会议讨论';
case MeetingStatus.STARTING:
return '正在召集会议成员...';
case MeetingStatus.DISCUSSING:
return `${currentRound} 轮讨论进行中...`;
case MeetingStatus.SPEAKING:
const role = getRoleConfig(speakingRoleId);
return `${role?.name || '成员'} 正在发言...`;
case MeetingStatus.WAITING_INPUT:
return '讨论暂停,您可以插话或等待继续';
case MeetingStatus.CONCLUDED:
return '会议已结束,已得出投资建议';
case MeetingStatus.ERROR:
return '会议出现异常,请重试';
default:
return '';
}
};
// 获取输入框占位符
const getPlaceholder = () => {
if (status === MeetingStatus.IDLE) {
return '输入投研议题,如:分析茅台最新财报...';
} else if (status === MeetingStatus.WAITING_INPUT) {
return '输入您的观点参与讨论,或等待继续...';
} else if (status === MeetingStatus.CONCLUDED) {
return '会议已结束,输入新议题开始新会议...';
}
return '会议进行中...';
};
return (
<Flex h="100%" direction="column" bg="gray.900">
{/* 顶部标题栏 */}
<Box
bg="rgba(17, 24, 39, 0.9)"
backdropFilter="blur(20px)"
borderBottom="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
px={6}
py={4}
>
<Flex align="center" justify="space-between">
<HStack spacing={4}>
<motion.div
animate={{ rotate: isLoading ? 360 : 0 }}
transition={{
duration: 2,
repeat: isLoading ? Infinity : 0,
ease: 'linear',
}}
>
<Avatar
icon={<Users className="w-6 h-6" />}
bgGradient="linear(to-br, orange.400, red.500)"
boxShadow="0 0 20px rgba(251, 146, 60, 0.5)"
/>
</motion.div>
<Box>
<Text
fontSize="xl"
fontWeight="bold"
bgGradient="linear(to-r, orange.400, red.400)"
bgClip="text"
letterSpacing="tight"
>
投研会议室
</Text>
<HStack spacing={2} mt={1}>
<Badge
bg={
status === MeetingStatus.CONCLUDED
? 'green.500'
: status === MeetingStatus.ERROR
? 'red.500'
: 'blue.500'
}
color="white"
px={2}
py={1}
borderRadius="md"
display="flex"
alignItems="center"
gap={1}
>
{status === MeetingStatus.CONCLUDED ? (
<CheckCircle className="w-3 h-3" />
) : status === MeetingStatus.ERROR ? (
<AlertCircle className="w-3 h-3" />
) : (
<MessageCircle className="w-3 h-3" />
)}
{getStatusText()}
</Badge>
{currentRound > 0 && (
<Badge
bgGradient="linear(to-r, purple.500, pink.500)"
color="white"
px={2}
py={1}
borderRadius="md"
>
{currentRound}
</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={resetMeeting}
bg="rgba(255, 255, 255, 0.05)"
color="gray.400"
_hover={{
bg: 'rgba(255, 255, 255, 0.1)',
color: 'white',
}}
/>
</motion.div>
</Tooltip>
</HStack>
</Flex>
</Box>
{/* 主内容区 */}
<Flex flex={1} overflow="hidden">
{/* 角色面板(左侧) */}
<MeetingRolePanel
speakingRoleId={speakingRoleId}
status={status}
/>
{/* 消息区域(中间) */}
<Box
flex={1}
overflowY="auto"
bg="rgba(17, 24, 39, 0.5)"
p={4}
>
{messages.length === 0 && status === MeetingStatus.IDLE ? (
<MeetingWelcome
onTopicSelect={(topic) => {
setInputValue(topic);
inputRef.current?.focus();
}}
/>
) : (
<VStack spacing={4} align="stretch" maxW="800px" mx="auto">
{/* 当前议题展示 */}
{currentTopic && (
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
>
<Box
bg="rgba(251, 146, 60, 0.1)"
border="1px solid"
borderColor="rgba(251, 146, 60, 0.3)"
borderRadius="lg"
p={4}
mb={4}
>
<Text fontSize="sm" color="orange.300" fontWeight="medium">
📋 本次议题
</Text>
<Text color="white" mt={1}>
{currentTopic}
</Text>
</Box>
</motion.div>
)}
{/* 消息列表 */}
<AnimatePresence mode="popLayout">
{messages.map((message, index) => (
<motion.div
key={message.id || index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<MeetingMessageBubble
message={message}
isLatest={index === messages.length - 1}
/>
</motion.div>
))}
</AnimatePresence>
{/* 正在发言指示器 */}
{speakingRoleId && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
<HStack
spacing={3}
p={4}
bg="rgba(255, 255, 255, 0.05)"
borderRadius="lg"
>
<Spinner size="sm" color="purple.400" />
<Text color="gray.400" fontSize="sm">
{getRoleConfig(speakingRoleId)?.name} 正在思考...
</Text>
</HStack>
</motion.div>
)}
<div ref={messagesEndRef} />
</VStack>
)}
</Box>
</Flex>
{/* 输入栏 */}
<Box
bg="rgba(17, 24, 39, 0.9)"
backdropFilter="blur(20px)"
borderTop="1px solid"
borderColor="rgba(255, 255, 255, 0.1)"
px={6}
py={4}
>
<Box maxW="800px" mx="auto">
<HStack spacing={3}>
<Input
ref={inputRef}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyPress}
placeholder={getPlaceholder()}
isDisabled={
isLoading ||
status === MeetingStatus.DISCUSSING ||
status === MeetingStatus.SPEAKING
}
size="lg"
bg="rgba(255, 255, 255, 0.05)"
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: 'orange.400',
boxShadow: '0 0 0 1px var(--chakra-colors-orange-400)',
}}
/>
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<IconButton
size="lg"
icon={isLoading ? <Spinner size="sm" /> : <Send className="w-5 h-5" />}
onClick={handleSend}
isDisabled={
!inputValue.trim() ||
isLoading ||
status === MeetingStatus.DISCUSSING ||
status === MeetingStatus.SPEAKING
}
bgGradient="linear(to-r, orange.400, red.500)"
color="white"
_hover={{
bgGradient: 'linear(to-r, orange.500, red.600)',
boxShadow: '0 8px 20px rgba(251, 146, 60, 0.4)',
}}
/>
</motion.div>
{/* 继续讨论按钮 */}
{status === MeetingStatus.WAITING_INPUT && !isConcluded && (
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<Tooltip label="继续下一轮讨论">
<IconButton
size="lg"
icon={<MessageCircle className="w-5 h-5" />}
onClick={() => continueMeeting()}
isDisabled={isLoading}
bgGradient="linear(to-r, purple.400, blue.500)"
color="white"
_hover={{
bgGradient: 'linear(to-r, purple.500, blue.600)',
}}
/>
</Tooltip>
</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">
Enter
</Kbd>
<Text>
{status === MeetingStatus.IDLE ? '开始会议' : '发送消息'}
</Text>
</HStack>
{status === MeetingStatus.WAITING_INPUT && (
<Text color="orange.400">
💡 您可以插话参与讨论或点击继续按钮进行下一轮
</Text>
)}
</HStack>
</Box>
</Box>
</Flex>
);
};
export default MeetingRoom;