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,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;