update pay function
This commit is contained in:
@@ -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;
|
||||
301
src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js
Normal file
301
src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js
Normal 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;
|
||||
294
src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js
Normal file
294
src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js
Normal 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;
|
||||
442
src/views/AgentChat/components/MeetingRoom/index.js
Normal file
442
src/views/AgentChat/components/MeetingRoom/index.js
Normal 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;
|
||||
Reference in New Issue
Block a user