diff --git a/mcp_server.py b/mcp_server.py
index a04bb43d..51814890 100644
--- a/mcp_server.py
+++ b/mcp_server.py
@@ -2337,6 +2337,637 @@ async def search_chat_history(user_id: str, query: str, top_k: int = 10):
raise HTTPException(status_code=500, detail=str(e))
+# ==================== 投研会议室系统 ====================
+
+# 投研会议室角色配置
+MEETING_ROLES = {
+ "buffett": {
+ "id": "buffett",
+ "name": "巴菲特",
+ "nickname": "唱多者",
+ "role_type": "bull", # 多头
+ "avatar": "/avatars/buffett.png",
+ "model": "kimi-k2-thinking",
+ "color": "#10B981", # 绿色(上涨)
+ "description": "主观多头,善于分析事件的潜在利好和长期价值",
+ "system_prompt": """你是"巴菲特",一位资深的价值投资者和主观多头分析师。
+
+你的特点:
+1. 善于发现事件和公司的潜在利好因素
+2. 关注长期价值,不被短期波动干扰
+3. 分析公司的护城河、竞争优势和管理层质量
+4. 对市场保持乐观但理性的态度
+
+分析风格:
+- 重点挖掘利好因素和投资机会
+- 从产业链、市场格局、政策支持等角度分析
+- 给出清晰的看多逻辑和目标预期
+- 语言风格:稳重、专业、富有洞察力
+
+注意:你的发言要简洁有力,每次发言控制在200字以内。直接表达观点,不要客套。"""
+ },
+ "big_short": {
+ "id": "big_short",
+ "name": "大空头",
+ "nickname": "大空头",
+ "role_type": "bear", # 空头
+ "avatar": "/avatars/big_short.png",
+ "model": "kimi-k2-thinking",
+ "color": "#EF4444", # 红色(下跌)
+ "description": "善于分析事件和财报中的风险因素,帮助投资者避雷",
+ "system_prompt": """你是"大空头",一位专业的风险分析师和空头研究员。
+
+你的特点:
+1. 善于发现被市场忽视的风险因素
+2. 擅长财报分析,发现财务造假和粉饰的迹象
+3. 关注行业天花板、竞争加剧、估值泡沫等问题
+4. 对市场保持警惕,帮助投资者避雷
+
+分析风格:
+- 重点挖掘风险因素和潜在隐患
+- 从财务数据、行业周期、估值水平等角度分析
+- 给出清晰的风险提示和规避建议
+- 语言风格:犀利、直接、善于质疑
+
+注意:你的发言要简洁有力,每次发言控制在200字以内。直接指出风险,不要绕弯子。"""
+ },
+ "simons": {
+ "id": "simons",
+ "name": "量化分析员",
+ "nickname": "西蒙斯",
+ "role_type": "quant", # 量化
+ "avatar": "/avatars/simons.png",
+ "model": "deepseek-v3",
+ "color": "#3B82F6", # 蓝色(中性)
+ "description": "中性立场,使用量化分析工具分析技术指标",
+ "system_prompt": """你是"量化分析员"(昵称:西蒙斯),一位专业的量化交易研究员。
+
+你的特点:
+1. 使用数据和技术指标说话,保持中性立场
+2. 擅长均线分析、量价关系、动能指标等技术分析
+3. 关注市场情绪、资金流向、筹码分布等量化因素
+4. 用概率思维看待市场,不做主观臆断
+
+分析风格:
+- 基于技术指标给出客观分析
+- 使用具体数据支撑观点(如:5日均线、MACD、RSI等)
+- 给出量化的买卖信号和风险评估
+- 语言风格:理性、客观、数据驱动
+
+注意:你的发言要简洁有力,每次发言控制在200字以内。多用数据说话,少发表主观意见。"""
+ },
+ "leek": {
+ "id": "leek",
+ "name": "韭菜",
+ "nickname": "牢大",
+ "role_type": "retail", # 散户
+ "avatar": "/avatars/leek.png",
+ "model": "deepmoney",
+ "color": "#F59E0B", # 黄色
+ "description": "贪婪又讨厌亏损,热爱追涨杀跌的典型散户",
+ "system_prompt": """你是"韭菜"(昵称:牢大),一个典型的散户投资者。
+
+你的特点:
+1. 贪婪但又害怕亏损,典型的追涨杀跌
+2. 容易被市场情绪影响,看到涨就想追,看到跌就想跑
+3. 喜欢听小道消息,容易被"内幕"吸引
+4. 短线思维,缺乏耐心,期望一夜暴富
+
+分析风格:
+- 用最朴素的散户思维来分析问题
+- 经常关注"这个能赚多少"、"会不会跌"
+- 容易情绪化,看涨时过度乐观,看跌时过度悲观
+- 语言风格:口语化、情绪化、接地气
+
+注意:你的发言要简洁直接,每次发言控制在150字以内。展现真实散户的心态,可以有些搞笑,但不要太出格。"""
+ },
+ "fund_manager": {
+ "id": "fund_manager",
+ "name": "基金经理",
+ "nickname": "决策者",
+ "role_type": "manager", # 管理者
+ "avatar": "/avatars/fund_manager.png",
+ "model": "kimi-k2-thinking",
+ "color": "#8B5CF6", # 紫色
+ "description": "总结其他人的发言做出最终决策",
+ "system_prompt": """你是"基金经理",投研会议的主持人和最终决策者。
+
+你的角色:
+1. 综合各方观点,做出理性判断
+2. 平衡多空观点,识别有价值的分析
+3. 特别注意:韭菜的观点通常是反向指标
+4. 给出专业、负责任的投资建议
+
+决策风格:
+- 综合考虑基本面、技术面、情绪面
+- 权衡风险与收益,给出明确的投资建议
+- 指出讨论中的关键洞察和需要注意的风险
+- 语言风格:权威、专业、全面
+
+决策输出格式:
+1. 综合评估:对讨论议题的整体判断
+2. 关键观点:各方有价值的观点总结
+3. 风险提示:需要注意的主要风险
+4. 操作建议:具体的投资建议(买入/持有/观望/卖出)
+5. 信心指数:对这个结论的信心程度(1-10分)
+
+注意:如果讨论还不够充分,你可以要求继续讨论。每次发言控制在300字以内。"""
+ }
+}
+
+# 投研会议室专用模型配置(扩展现有配置)
+MEETING_MODEL_CONFIGS = {
+ **MODEL_CONFIGS,
+ "deepseek-v3": {
+ "api_key": "sk-1cf3dfadf7244a8680cd0a60da6f1efd",
+ "base_url": "https://api.deepseek.com/v1",
+ "model": "deepseek-chat",
+ }
+}
+
+
+class MeetingRoleMessage(BaseModel):
+ """会议角色消息"""
+ role_id: str
+ role_name: str
+ nickname: str
+ avatar: str
+ color: str
+ content: str
+ timestamp: str
+ round_number: int # 第几轮讨论
+
+
+class MeetingRequest(BaseModel):
+ """投研会议请求"""
+ topic: str # 用户提出的议题
+ user_id: str = "anonymous"
+ user_nickname: str = "匿名用户"
+ session_id: Optional[str] = None
+ user_message: Optional[str] = None # 用户在讨论中的插话
+ conversation_history: List[Dict[str, Any]] = [] # 之前的讨论历史
+
+
+class MeetingResponse(BaseModel):
+ """投研会议响应"""
+ success: bool
+ session_id: str
+ messages: List[Dict[str, Any]] # 本轮所有角色的发言
+ round_number: int # 当前轮次
+ is_concluded: bool # 是否已得出结论
+ conclusion: Optional[Dict[str, Any]] = None # 基金经理的结论(如果有)
+
+
+async def call_role_llm(role_id: str, prompt: str, context: str = "") -> str:
+ """调用特定角色的LLM生成回复"""
+ role = MEETING_ROLES.get(role_id)
+ if not role:
+ raise ValueError(f"Unknown role: {role_id}")
+
+ model_name = role["model"]
+ model_config = MEETING_MODEL_CONFIGS.get(model_name, MODEL_CONFIGS["kimi-k2-thinking"])
+
+ try:
+ client = OpenAI(
+ api_key=model_config["api_key"],
+ base_url=model_config["base_url"]
+ )
+
+ messages = [
+ {"role": "system", "content": role["system_prompt"]},
+ ]
+
+ if context:
+ messages.append({"role": "user", "content": f"当前讨论背景:\n{context}"})
+
+ messages.append({"role": "user", "content": prompt})
+
+ response = client.chat.completions.create(
+ model=model_config["model"],
+ messages=messages,
+ temperature=0.7,
+ max_tokens=500,
+ )
+
+ return response.choices[0].message.content.strip()
+
+ except Exception as e:
+ logger.error(f"调用角色 {role_id} 的 LLM 失败: {e}")
+ return f"[{role['name']}暂时无法发言,请稍后重试]"
+
+
+async def determine_speaking_order(topic: str) -> List[str]:
+ """使用 K2 模型决定发言顺序"""
+ try:
+ client = OpenAI(
+ api_key=MODEL_CONFIGS["kimi-k2-thinking"]["api_key"],
+ base_url=MODEL_CONFIGS["kimi-k2-thinking"]["base_url"]
+ )
+
+ response = client.chat.completions.create(
+ model=MODEL_CONFIGS["kimi-k2-thinking"]["model"],
+ messages=[
+ {
+ "role": "system",
+ "content": """你是一个会议主持助手。根据用户提出的议题,决定投研会议中各角色的最佳发言顺序。
+
+可用角色(不包括基金经理,他最后总结):
+- buffett: 巴菲特(主观多头,分析利好)
+- big_short: 大空头(风险分析师)
+- simons: 量化分析员(技术分析)
+- leek: 韭菜(散户视角)
+
+根据议题性质,安排最合适的发言顺序。比如:
+- 如果是分析某公司/事件,建议先让多头分析利好,再让空头分析风险
+- 如果是技术走势问题,可以先让量化分析
+- 韭菜可以随时插入,提供散户视角
+
+只需要返回角色ID列表,用逗号分隔,例如:buffett,simons,big_short,leek"""
+ },
+ {"role": "user", "content": f"议题:{topic}"}
+ ],
+ temperature=0.3,
+ max_tokens=100,
+ )
+
+ order_str = response.choices[0].message.content.strip()
+ # 解析返回的顺序
+ order = [r.strip() for r in order_str.split(",") if r.strip() in MEETING_ROLES]
+
+ # 确保所有非管理者角色都在列表中
+ for role_id, role in MEETING_ROLES.items():
+ if role["role_type"] != "manager" and role_id not in order:
+ order.append(role_id)
+
+ return order
+
+ except Exception as e:
+ logger.error(f"决定发言顺序失败: {e}")
+ # 返回默认顺序
+ return ["buffett", "big_short", "simons", "leek"]
+
+
+async def check_conclusion_ready(discussion_history: str, topic: str) -> tuple[bool, str]:
+ """基金经理判断是否可以得出结论"""
+ try:
+ client = OpenAI(
+ api_key=MODEL_CONFIGS["kimi-k2-thinking"]["api_key"],
+ base_url=MODEL_CONFIGS["kimi-k2-thinking"]["base_url"]
+ )
+
+ response = client.chat.completions.create(
+ model=MODEL_CONFIGS["kimi-k2-thinking"]["model"],
+ messages=[
+ {
+ "role": "system",
+ "content": MEETING_ROLES["fund_manager"]["system_prompt"]
+ },
+ {
+ "role": "user",
+ "content": f"""议题:{topic}
+
+目前的讨论内容:
+{discussion_history}
+
+请判断:
+1. 目前的讨论是否足够充分,可以得出最终结论?
+2. 如果可以,请给出你的最终决策。
+3. 如果不可以,请说明还需要讨论什么,并要求继续讨论。
+
+请以JSON格式回复:
+{{
+ "can_conclude": true/false,
+ "reasoning": "判断理由",
+ "conclusion": "如果可以结论,这里是你的完整决策;如果不能,这里是需要继续讨论的方向"
+}}"""
+ }
+ ],
+ temperature=0.5,
+ max_tokens=800,
+ )
+
+ result = response.choices[0].message.content.strip()
+ # 尝试解析JSON
+ try:
+ # 处理可能的 markdown 代码块
+ if "```json" in result:
+ result = result.split("```json")[1].split("```")[0].strip()
+ elif "```" in result:
+ result = result.split("```")[1].split("```")[0].strip()
+
+ data = json.loads(result)
+ return data.get("can_conclude", False), data.get("conclusion", result)
+ except json.JSONDecodeError:
+ # 如果JSON解析失败,直接返回内容
+ return True, result
+
+ except Exception as e:
+ logger.error(f"检查结论状态失败: {e}")
+ return True, "基于目前的讨论,建议投资者谨慎对待,继续关注后续发展。"
+
+
+@app.post("/agent/meeting/start")
+async def start_investment_meeting(request: MeetingRequest):
+ """
+ 启动投研会议
+
+ 第一轮:所有角色(除基金经理外)依次发言
+ """
+ logger.info(f"启动投研会议: {request.topic} (user: {request.user_id})")
+
+ session_id = request.session_id or str(uuid.uuid4())
+ messages = []
+ round_number = 1
+
+ # 决定发言顺序
+ speaking_order = await determine_speaking_order(request.topic)
+ logger.info(f"发言顺序: {speaking_order}")
+
+ # 构建讨论上下文
+ context = f"议题:{request.topic}\n\n这是第一轮讨论,请针对议题发表你的观点。"
+
+ # 依次让每个角色发言
+ for role_id in speaking_order:
+ role = MEETING_ROLES[role_id]
+ if role["role_type"] == "manager":
+ continue # 基金经理不在第一轮发言
+
+ # 加入之前角色的发言作为上下文
+ prev_context = context
+ if messages:
+ prev_context += "\n\n其他人的观点:\n"
+ for msg in messages:
+ prev_context += f"- {msg['role_name']}:{msg['content']}\n"
+
+ # 调用LLM生成发言
+ content = await call_role_llm(role_id, request.topic, prev_context)
+
+ message = {
+ "role_id": role_id,
+ "role_name": role["name"],
+ "nickname": role["nickname"],
+ "avatar": role["avatar"],
+ "color": role["color"],
+ "content": content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number
+ }
+ messages.append(message)
+
+ # 第一轮结束后,基金经理判断是否可以得出结论
+ discussion_summary = "\n".join([
+ f"【{msg['role_name']}】:{msg['content']}"
+ for msg in messages
+ ])
+
+ can_conclude, conclusion_content = await check_conclusion_ready(discussion_summary, request.topic)
+
+ # 添加基金经理的发言
+ fund_manager = MEETING_ROLES["fund_manager"]
+ fund_manager_message = {
+ "role_id": "fund_manager",
+ "role_name": fund_manager["name"],
+ "nickname": fund_manager["nickname"],
+ "avatar": fund_manager["avatar"],
+ "color": fund_manager["color"],
+ "content": conclusion_content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number,
+ "is_conclusion": can_conclude
+ }
+ messages.append(fund_manager_message)
+
+ return {
+ "success": True,
+ "session_id": session_id,
+ "messages": messages,
+ "round_number": round_number,
+ "is_concluded": can_conclude,
+ "conclusion": fund_manager_message if can_conclude else None
+ }
+
+
+@app.post("/agent/meeting/continue")
+async def continue_investment_meeting(request: MeetingRequest):
+ """
+ 继续投研会议讨论
+
+ 根据之前的讨论历史,继续新一轮讨论
+ 支持用户在讨论中插话
+ """
+ logger.info(f"继续投研会议: {request.topic} (round: {len(request.conversation_history) // 5 + 1})")
+
+ session_id = request.session_id or str(uuid.uuid4())
+ messages = []
+ round_number = len(request.conversation_history) // 5 + 2 # 估算轮次
+
+ # 构建历史讨论上下文
+ history_context = "历史讨论:\n"
+ for msg in request.conversation_history:
+ history_context += f"【{msg.get('role_name', '未知')}】:{msg.get('content', '')}\n"
+
+ # 如果用户有插话,先处理用户消息
+ if request.user_message:
+ history_context += f"\n【用户】:{request.user_message}\n"
+ messages.append({
+ "role_id": "user",
+ "role_name": "用户",
+ "nickname": request.user_nickname,
+ "avatar": "",
+ "color": "#6366F1",
+ "content": request.user_message,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number
+ })
+
+ # 新一轮讨论的发言顺序
+ speaking_order = await determine_speaking_order(request.topic)
+
+ # 依次让每个角色发言
+ for role_id in speaking_order:
+ role = MEETING_ROLES[role_id]
+ if role["role_type"] == "manager":
+ continue
+
+ # 构建本次发言的上下文
+ current_context = f"议题:{request.topic}\n\n{history_context}"
+ if messages:
+ current_context += "\n本轮讨论:\n"
+ for msg in messages:
+ if msg["role_id"] != "user":
+ current_context += f"- {msg['role_name']}:{msg['content']}\n"
+
+ # 调用LLM
+ prompt = f"这是第{round_number}轮讨论,请根据之前的讨论内容,进一步阐述或补充你的观点。"
+ if request.user_message:
+ prompt += f"\n\n用户刚才说:{request.user_message}\n请也回应用户的观点。"
+
+ content = await call_role_llm(role_id, prompt, current_context)
+
+ message = {
+ "role_id": role_id,
+ "role_name": role["name"],
+ "nickname": role["nickname"],
+ "avatar": role["avatar"],
+ "color": role["color"],
+ "content": content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number
+ }
+ messages.append(message)
+
+ # 本轮结束后,基金经理再次判断
+ all_discussion = history_context + "\n本轮讨论:\n" + "\n".join([
+ f"【{msg['role_name']}】:{msg['content']}"
+ for msg in messages if msg["role_id"] != "user"
+ ])
+
+ can_conclude, conclusion_content = await check_conclusion_ready(all_discussion, request.topic)
+
+ # 添加基金经理的发言
+ fund_manager = MEETING_ROLES["fund_manager"]
+ fund_manager_message = {
+ "role_id": "fund_manager",
+ "role_name": fund_manager["name"],
+ "nickname": fund_manager["nickname"],
+ "avatar": fund_manager["avatar"],
+ "color": fund_manager["color"],
+ "content": conclusion_content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number,
+ "is_conclusion": can_conclude
+ }
+ messages.append(fund_manager_message)
+
+ return {
+ "success": True,
+ "session_id": session_id,
+ "messages": messages,
+ "round_number": round_number,
+ "is_concluded": can_conclude,
+ "conclusion": fund_manager_message if can_conclude else None
+ }
+
+
+@app.get("/agent/meeting/roles")
+async def get_meeting_roles():
+ """获取所有会议角色配置"""
+ return {
+ "success": True,
+ "roles": [
+ {
+ "id": role["id"],
+ "name": role["name"],
+ "nickname": role["nickname"],
+ "role_type": role["role_type"],
+ "avatar": role["avatar"],
+ "color": role["color"],
+ "description": role["description"],
+ }
+ for role in MEETING_ROLES.values()
+ ]
+ }
+
+
+@app.post("/agent/meeting/stream")
+async def stream_investment_meeting(request: MeetingRequest):
+ """
+ 流式投研会议
+
+ 以 SSE 方式逐个角色流式返回发言
+ """
+ logger.info(f"流式投研会议: {request.topic} (user: {request.user_id})")
+
+ async def generate_meeting_stream() -> AsyncGenerator[str, None]:
+ session_id = request.session_id or str(uuid.uuid4())
+ round_number = 1
+ all_messages = []
+
+ # 发送会话开始事件
+ yield f"data: {json.dumps({'type': 'session_start', 'session_id': session_id}, ensure_ascii=False)}\n\n"
+
+ # 决定发言顺序
+ speaking_order = await determine_speaking_order(request.topic)
+
+ yield f"data: {json.dumps({'type': 'order_decided', 'order': speaking_order}, ensure_ascii=False)}\n\n"
+
+ context = f"议题:{request.topic}\n\n这是第一轮讨论,请针对议题发表你的观点。"
+
+ # 依次让每个角色发言
+ for role_id in speaking_order:
+ role = MEETING_ROLES[role_id]
+ if role["role_type"] == "manager":
+ continue
+
+ # 发送"正在发言"状态
+ yield f"data: {json.dumps({'type': 'speaking_start', 'role_id': role_id, 'role_name': role['name']}, ensure_ascii=False)}\n\n"
+
+ # 构建上下文
+ prev_context = context
+ if all_messages:
+ prev_context += "\n\n其他人的观点:\n"
+ for msg in all_messages:
+ prev_context += f"- {msg['role_name']}:{msg['content']}\n"
+
+ # 调用LLM生成发言
+ content = await call_role_llm(role_id, request.topic, prev_context)
+
+ message = {
+ "role_id": role_id,
+ "role_name": role["name"],
+ "nickname": role["nickname"],
+ "avatar": role["avatar"],
+ "color": role["color"],
+ "content": content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number
+ }
+ all_messages.append(message)
+
+ # 发送完整发言
+ yield f"data: {json.dumps({'type': 'message', 'message': message}, ensure_ascii=False)}\n\n"
+
+ # 短暂延迟,让前端有时间处理
+ await asyncio.sleep(0.5)
+
+ # 基金经理总结
+ fund_manager = MEETING_ROLES["fund_manager"]
+ yield f"data: {json.dumps({'type': 'speaking_start', 'role_id': 'fund_manager', 'role_name': fund_manager['name']}, ensure_ascii=False)}\n\n"
+
+ discussion_summary = "\n".join([
+ f"【{msg['role_name']}】:{msg['content']}"
+ for msg in all_messages
+ ])
+ can_conclude, conclusion_content = await check_conclusion_ready(discussion_summary, request.topic)
+
+ fund_manager_message = {
+ "role_id": "fund_manager",
+ "role_name": fund_manager["name"],
+ "nickname": fund_manager["nickname"],
+ "avatar": fund_manager["avatar"],
+ "color": fund_manager["color"],
+ "content": conclusion_content,
+ "timestamp": datetime.now().isoformat(),
+ "round_number": round_number,
+ "is_conclusion": can_conclude
+ }
+
+ yield f"data: {json.dumps({'type': 'message', 'message': fund_manager_message}, ensure_ascii=False)}\n\n"
+
+ # 发送会议结束事件
+ yield f"data: {json.dumps({'type': 'meeting_end', 'is_concluded': can_conclude, 'round_number': round_number}, ensure_ascii=False)}\n\n"
+
+ return StreamingResponse(
+ generate_meeting_stream(),
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no",
+ },
+ )
+
+
# ==================== 健康检查 ====================
@app.get("/health")
diff --git a/src/mocks/handlers/agent.js b/src/mocks/handlers/agent.js
index 5bac63e1..2e1462cd 100644
--- a/src/mocks/handlers/agent.js
+++ b/src/mocks/handlers/agent.js
@@ -230,4 +230,208 @@ export const agentHandlers = [
count: history.length,
});
}),
+
+ // ==================== 投研会议室 API Handlers ====================
+
+ // GET /mcp/agent/meeting/roles - 获取会议角色配置
+ http.get('/mcp/agent/meeting/roles', async () => {
+ await delay(200);
+
+ return HttpResponse.json({
+ success: true,
+ roles: [
+ {
+ id: 'buffett',
+ name: '巴菲特',
+ nickname: '唱多者',
+ role_type: 'bull',
+ avatar: '/avatars/buffett.png',
+ color: '#10B981',
+ description: '主观多头,善于分析事件的潜在利好和长期价值',
+ },
+ {
+ id: 'big_short',
+ name: '大空头',
+ nickname: '大空头',
+ role_type: 'bear',
+ avatar: '/avatars/big_short.png',
+ color: '#EF4444',
+ description: '善于分析事件和财报中的风险因素,帮助投资者避雷',
+ },
+ {
+ id: 'simons',
+ name: '量化分析员',
+ nickname: '西蒙斯',
+ role_type: 'quant',
+ avatar: '/avatars/simons.png',
+ color: '#3B82F6',
+ description: '中性立场,使用量化分析工具分析技术指标',
+ },
+ {
+ id: 'leek',
+ name: '韭菜',
+ nickname: '牢大',
+ role_type: 'retail',
+ avatar: '/avatars/leek.png',
+ color: '#F59E0B',
+ description: '贪婪又讨厌亏损,热爱追涨杀跌的典型散户',
+ },
+ {
+ id: 'fund_manager',
+ name: '基金经理',
+ nickname: '决策者',
+ role_type: 'manager',
+ avatar: '/avatars/fund_manager.png',
+ color: '#8B5CF6',
+ description: '总结其他人的发言做出最终决策',
+ },
+ ],
+ });
+ }),
+
+ // POST /mcp/agent/meeting/start - 启动投研会议
+ http.post('/mcp/agent/meeting/start', async ({ request }) => {
+ await delay(2000); // 模拟多角色讨论耗时
+
+ const body = await request.json();
+ const { topic, user_id } = body;
+
+ const sessionId = `meeting-${Date.now()}`;
+ const timestamp = new Date().toISOString();
+
+ // 生成模拟的多角色讨论消息
+ const messages = [
+ {
+ role_id: 'buffett',
+ role_name: '巴菲特',
+ nickname: '唱多者',
+ avatar: '/avatars/buffett.png',
+ color: '#10B981',
+ content: `关于「${topic}」,我认为这里存在显著的投资机会。从价值投资的角度看,我们应该关注以下几点:\n\n1. **长期价值**:该标的具有较强的护城河\n2. **盈利能力**:ROE持续保持在较高水平\n3. **管理层质量**:管理团队稳定且执行力强\n\n我的观点是**看多**,建议逢低布局。`,
+ timestamp,
+ round_number: 1,
+ },
+ {
+ role_id: 'big_short',
+ role_name: '大空头',
+ nickname: '大空头',
+ avatar: '/avatars/big_short.png',
+ color: '#EF4444',
+ content: `等等,让我泼点冷水。关于「${topic}」,市场似乎过于乐观了:\n\n⚠️ **风险提示**:\n1. 当前估值处于历史高位,安全边际不足\n2. 行业竞争加剧,利润率面临压力\n3. 宏观环境不确定性增加\n\n建议投资者**保持谨慎**,不要追高。`,
+ timestamp: new Date(Date.now() + 1000).toISOString(),
+ round_number: 1,
+ },
+ {
+ role_id: 'simons',
+ role_name: '量化分析员',
+ nickname: '西蒙斯',
+ avatar: '/avatars/simons.png',
+ color: '#3B82F6',
+ content: `从量化角度分析「${topic}」:\n\n📊 **技术指标**:\n- MACD:金叉形态,动能向上\n- RSI:58,处于中性区域\n- 均线:5日>10日>20日,多头排列\n\n📈 **资金面**:\n- 主力资金:近5日净流入2.3亿\n- 北向资金:持续加仓\n\n**结论**:短期技术面偏多,但需关注60日均线支撑。`,
+ timestamp: new Date(Date.now() + 2000).toISOString(),
+ round_number: 1,
+ },
+ {
+ role_id: 'leek',
+ role_name: '韭菜',
+ nickname: '牢大',
+ avatar: '/avatars/leek.png',
+ color: '#F59E0B',
+ content: `哇!「${topic}」看起来要涨啊!\n\n🚀 我觉得必须满仓干!隔壁老王都赚翻了!\n\n不过话说回来...万一跌了怎么办?会不会套住?\n\n算了不管了,先冲一把再说!错过这村就没这店了!\n\n(内心OS:希望别当接盘侠...)`,
+ timestamp: new Date(Date.now() + 3000).toISOString(),
+ round_number: 1,
+ },
+ {
+ role_id: 'fund_manager',
+ role_name: '基金经理',
+ nickname: '决策者',
+ avatar: '/avatars/fund_manager.png',
+ color: '#8B5CF6',
+ content: `## 投资建议总结\n\n综合各方观点,对于「${topic}」,我的判断如下:\n\n### 综合评估\n多空双方都提出了有价值的观点。技术面短期偏多,但估值确实需要关注。\n\n### 关键观点\n- ✅ 基本面优质,长期价值明确\n- ⚠️ 短期估值偏高,需要耐心等待\n- 📊 技术面处于上升趋势\n\n### 风险提示\n注意仓位控制,避免追高\n\n### 操作建议\n**观望为主**,等待回调至支撑位再考虑建仓\n\n### 信心指数:7/10`,
+ timestamp: new Date(Date.now() + 4000).toISOString(),
+ round_number: 1,
+ is_conclusion: true,
+ },
+ ];
+
+ return HttpResponse.json({
+ success: true,
+ session_id: sessionId,
+ messages,
+ round_number: 1,
+ is_concluded: true,
+ conclusion: messages[messages.length - 1],
+ });
+ }),
+
+ // POST /mcp/agent/meeting/continue - 继续会议讨论
+ http.post('/mcp/agent/meeting/continue', async ({ request }) => {
+ await delay(1500);
+
+ const body = await request.json();
+ const { topic, user_message, conversation_history } = body;
+
+ const roundNumber = Math.floor(conversation_history.length / 5) + 2;
+ const timestamp = new Date().toISOString();
+
+ const messages = [];
+
+ // 如果用户有插话,添加用户消息
+ if (user_message) {
+ messages.push({
+ role_id: 'user',
+ role_name: '用户',
+ nickname: '你',
+ avatar: '',
+ color: '#6366F1',
+ content: user_message,
+ timestamp,
+ round_number: roundNumber,
+ });
+ }
+
+ // 生成新一轮讨论
+ messages.push(
+ {
+ role_id: 'buffett',
+ role_name: '巴菲特',
+ nickname: '唱多者',
+ avatar: '/avatars/buffett.png',
+ color: '#10B981',
+ content: `感谢用户的补充。${user_message ? `关于"${user_message}",` : ''}我依然坚持看多的观点。从更长远的角度看,短期波动不影响长期价值。`,
+ timestamp: new Date(Date.now() + 1000).toISOString(),
+ round_number: roundNumber,
+ },
+ {
+ role_id: 'big_short',
+ role_name: '大空头',
+ nickname: '大空头',
+ avatar: '/avatars/big_short.png',
+ color: '#EF4444',
+ content: `用户提出了很好的问题。我要再次强调风险控制的重要性。当前市场情绪过热,建议保持警惕。`,
+ timestamp: new Date(Date.now() + 2000).toISOString(),
+ round_number: roundNumber,
+ },
+ {
+ role_id: 'fund_manager',
+ role_name: '基金经理',
+ nickname: '决策者',
+ avatar: '/avatars/fund_manager.png',
+ color: '#8B5CF6',
+ content: `## 第${roundNumber}轮讨论总结\n\n经过进一步讨论,我维持之前的判断:\n\n- 短期观望为主\n- 中长期可以考虑分批建仓\n- 严格控制仓位,设好止损\n\n**信心指数:7.5/10**\n\n会议到此结束,感谢各位的参与!`,
+ timestamp: new Date(Date.now() + 3000).toISOString(),
+ round_number: roundNumber,
+ is_conclusion: true,
+ }
+ );
+
+ return HttpResponse.json({
+ success: true,
+ session_id: body.session_id,
+ messages,
+ round_number: roundNumber,
+ is_concluded: true,
+ conclusion: messages[messages.length - 1],
+ });
+ }),
];
diff --git a/src/views/AgentChat/components/MeetingRoom/MeetingMessageBubble.js b/src/views/AgentChat/components/MeetingRoom/MeetingMessageBubble.js
new file mode 100644
index 00000000..e4df9fc0
--- /dev/null
+++ b/src/views/AgentChat/components/MeetingRoom/MeetingMessageBubble.js
@@ -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 ;
+ case 'bear':
+ return ;
+ case 'quant':
+ return ;
+ case 'retail':
+ return ;
+ case 'manager':
+ return ;
+ default:
+ return ;
+ }
+};
+
+/**
+ * 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 (
+
+ {/* 消息头部:角色信息 */}
+
+
+
+
+
+
+
+
+ {roleConfig.name}
+
+ {roleConfig.nickname !== roleConfig.name && (
+
+ @{roleConfig.nickname}
+
+ )}
+ {isManager && (
+
+ 主持人
+
+ )}
+ {isConclusion && (
+
+ 最终结论
+
+ )}
+
+
+ 第 {message.round_number} 轮 ·{' '}
+ {new Date(message.timestamp).toLocaleTimeString('zh-CN', {
+ hour: '2-digit',
+ minute: '2-digit',
+ })}
+
+
+
+
+ {/* 消息内容卡片 */}
+
+
+ {/* 结论标题 */}
+ {isConclusion && (
+
+
+
+
+ 基金经理投资建议
+
+
+
+ )}
+
+
+
+
+
+
+ {/* 操作按钮 */}
+
+
+
+ }
+ onClick={handleCopy}
+ color="gray.500"
+ _hover={{ color: 'white', bg: 'whiteAlpha.100' }}
+ />
+
+
+ }
+ color="gray.500"
+ _hover={{ color: 'green.400', bg: 'green.900' }}
+ />
+
+
+
+ {/* 角色标签 */}
+
+
+ {roleConfig.roleType === 'bull' && '📈 看多'}
+ {roleConfig.roleType === 'bear' && '📉 看空'}
+ {roleConfig.roleType === 'quant' && '📊 量化'}
+ {roleConfig.roleType === 'retail' && '🌱 散户'}
+ {roleConfig.roleType === 'manager' && '👔 决策'}
+
+
+
+
+
+
+
+ );
+};
+
+export default MeetingMessageBubble;
diff --git a/src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js b/src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js
new file mode 100644
index 00000000..05975046
--- /dev/null
+++ b/src/views/AgentChat/components/MeetingRoom/MeetingRolePanel.js
@@ -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 ;
+ case 'bear':
+ return ;
+ case 'quant':
+ return ;
+ case 'retail':
+ return ;
+ case 'manager':
+ return ;
+ default:
+ return ;
+ }
+};
+
+/**
+ * RoleCard - 单个角色卡片
+ */
+const RoleCard = ({ role, isSpeaking }) => {
+ return (
+
+
+
+
+ {/* 头像 */}
+
+
+ {isSpeaking && (
+
+
+
+
+
+ )}
+
+
+ {/* 角色信息 */}
+
+
+ {role.name}
+
+
+ @{role.nickname}
+
+
+
+ {/* 状态指示 */}
+
+ {role.roleType === 'bull' && '多头'}
+ {role.roleType === 'bear' && '空头'}
+ {role.roleType === 'quant' && '量化'}
+ {role.roleType === 'retail' && '散户'}
+ {role.roleType === 'manager' && '主持'}
+
+
+
+
+
+ );
+};
+
+/**
+ * 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 (
+
+ {/* 标题 */}
+
+ 参会成员
+
+
+
+ {/* 分析师组 */}
+
+ 分析团队
+
+ {analysts.map((role) => (
+
+ ))}
+
+ {/* 分隔线 */}
+
+
+ {/* 基金经理 */}
+
+ 决策层
+
+
+
+
+ {/* 会议状态指示 */}
+
+
+ 会议状态
+
+
+
+
+ {status === MeetingStatus.IDLE && '等待开始'}
+ {status === MeetingStatus.STARTING && '召集中...'}
+ {status === MeetingStatus.DISCUSSING && '讨论中'}
+ {status === MeetingStatus.SPEAKING && '发言中'}
+ {status === MeetingStatus.WAITING_INPUT && '等待输入'}
+ {status === MeetingStatus.CONCLUDED && '已结束'}
+ {status === MeetingStatus.ERROR && '异常'}
+
+
+
+
+ {/* 角色说明 */}
+
+
+ 💡 角色说明
+
+
+
+ 📈 多头:挖掘利好因素
+
+
+ 📉 空头:发现风险隐患
+
+
+ 📊 量化:技术指标分析
+
+
+ 🌱 散户:反向指标参考
+
+
+ 👔 主持:综合判断决策
+
+
+
+
+ );
+};
+
+export default MeetingRolePanel;
diff --git a/src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js b/src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js
new file mode 100644
index 00000000..4e6fbea8
--- /dev/null
+++ b/src/views/AgentChat/components/MeetingRoom/MeetingWelcome.js
@@ -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 (
+
+ 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`,
+ }}
+ >
+
+
+
+
+
+
+
+ {suggestion.title}
+
+
+
+ {suggestion.example}
+
+
+
+
+
+ );
+};
+
+/**
+ * MeetingWelcome - 会议欢迎界面
+ *
+ * @param {Object} props
+ * @param {Function} props.onTopicSelect - 选择议题回调
+ * @returns {JSX.Element}
+ */
+const MeetingWelcome = ({ onTopicSelect }) => {
+ return (
+
+
+ {/* 标题区域 */}
+
+
+
+
+
+
+
+
+
+ 欢迎来到投研会议室
+
+
+
+ 多位 AI 分析师将从不同角度分析您的投资议题,
+ 包括多头、空头、量化分析师和散户视角,
+ 最终由基金经理给出投资建议。
+
+
+
+
+ {/* 特点说明 */}
+
+
+ {[
+ { icon: '📈', text: '多空对决' },
+ { icon: '📊', text: '量化分析' },
+ { icon: '🎯', text: '投资建议' },
+ { icon: '💬', text: '实时参与' },
+ ].map((item, index) => (
+
+ {item.icon}
+
+ {item.text}
+
+
+ ))}
+
+
+
+ {/* 议题建议 */}
+
+
+
+
+
+ 点击下方议题快速开始,或输入自定义议题
+
+
+
+
+ {TOPIC_SUGGESTIONS.map((suggestion) => (
+
+ ))}
+
+
+
+
+ {/* 使用提示 */}
+
+
+
+ 💡 提示:会议进行中您可以随时插话参与讨论,
+ 您的观点会影响分析师的判断。
+ 讨论结束后,基金经理会给出最终的投资建议。
+
+
+
+
+
+ );
+};
+
+export default MeetingWelcome;
diff --git a/src/views/AgentChat/components/MeetingRoom/index.js b/src/views/AgentChat/components/MeetingRoom/index.js
new file mode 100644
index 00000000..74405b4f
--- /dev/null
+++ b/src/views/AgentChat/components/MeetingRoom/index.js
@@ -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 (
+
+ {/* 顶部标题栏 */}
+
+
+
+
+ }
+ bgGradient="linear(to-br, orange.400, red.500)"
+ boxShadow="0 0 20px rgba(251, 146, 60, 0.5)"
+ />
+
+
+
+
+ 投研会议室
+
+
+
+ {status === MeetingStatus.CONCLUDED ? (
+
+ ) : status === MeetingStatus.ERROR ? (
+
+ ) : (
+
+ )}
+ {getStatusText()}
+
+ {currentRound > 0 && (
+
+ 第 {currentRound} 轮
+
+ )}
+
+
+
+
+
+
+
+ }
+ onClick={resetMeeting}
+ bg="rgba(255, 255, 255, 0.05)"
+ color="gray.400"
+ _hover={{
+ bg: 'rgba(255, 255, 255, 0.1)',
+ color: 'white',
+ }}
+ />
+
+
+
+
+
+
+ {/* 主内容区 */}
+
+ {/* 角色面板(左侧) */}
+
+
+ {/* 消息区域(中间) */}
+
+ {messages.length === 0 && status === MeetingStatus.IDLE ? (
+ {
+ setInputValue(topic);
+ inputRef.current?.focus();
+ }}
+ />
+ ) : (
+
+ {/* 当前议题展示 */}
+ {currentTopic && (
+
+
+
+ 📋 本次议题
+
+
+ {currentTopic}
+
+
+
+ )}
+
+ {/* 消息列表 */}
+
+ {messages.map((message, index) => (
+
+
+
+ ))}
+
+
+ {/* 正在发言指示器 */}
+ {speakingRoleId && (
+
+
+
+
+ {getRoleConfig(speakingRoleId)?.name} 正在思考...
+
+
+
+ )}
+
+
+
+ )}
+
+
+
+ {/* 输入栏 */}
+
+
+
+ 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)',
+ }}
+ />
+
+
+ : }
+ 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)',
+ }}
+ />
+
+
+ {/* 继续讨论按钮 */}
+ {status === MeetingStatus.WAITING_INPUT && !isConcluded && (
+
+
+ }
+ onClick={() => continueMeeting()}
+ isDisabled={isLoading}
+ bgGradient="linear(to-r, purple.400, blue.500)"
+ color="white"
+ _hover={{
+ bgGradient: 'linear(to-r, purple.500, blue.600)',
+ }}
+ />
+
+
+ )}
+
+
+
+
+
+ Enter
+
+
+ {status === MeetingStatus.IDLE ? '开始会议' : '发送消息'}
+
+
+ {status === MeetingStatus.WAITING_INPUT && (
+
+ 💡 您可以插话参与讨论,或点击继续按钮进行下一轮
+
+ )}
+
+
+
+
+ );
+};
+
+export default MeetingRoom;
diff --git a/src/views/AgentChat/constants/index.ts b/src/views/AgentChat/constants/index.ts
index 35c32e62..43bbf4fe 100644
--- a/src/views/AgentChat/constants/index.ts
+++ b/src/views/AgentChat/constants/index.ts
@@ -20,3 +20,4 @@ export * from './messageTypes';
export * from './models';
export * from './tools';
export * from './quickQuestions';
+export * from './meetingRoles';
diff --git a/src/views/AgentChat/constants/meetingRoles.ts b/src/views/AgentChat/constants/meetingRoles.ts
new file mode 100644
index 00000000..e6c7c8aa
--- /dev/null
+++ b/src/views/AgentChat/constants/meetingRoles.ts
@@ -0,0 +1,227 @@
+// src/views/AgentChat/constants/meetingRoles.ts
+// 投研会议室角色配置
+
+import * as React from 'react';
+import {
+ TrendingUp,
+ TrendingDown,
+ BarChart2,
+ Users,
+ Crown,
+} from 'lucide-react';
+
+/**
+ * 角色类型枚举
+ */
+export type MeetingRoleType = 'bull' | 'bear' | 'quant' | 'retail' | 'manager';
+
+/**
+ * 会议角色配置接口
+ */
+export interface MeetingRoleConfig {
+ /** 角色唯一标识 */
+ id: string;
+ /** 角色名称 */
+ name: string;
+ /** 角色昵称 */
+ nickname: string;
+ /** 角色类型 */
+ roleType: MeetingRoleType;
+ /** 头像路径 */
+ avatar: string;
+ /** 主题颜色 */
+ color: string;
+ /** 渐变背景 */
+ gradient: string;
+ /** 角色描述 */
+ description: string;
+ /** 图标 */
+ icon: React.ReactNode;
+}
+
+/**
+ * 会议消息接口
+ */
+export interface MeetingMessage {
+ /** 消息 ID */
+ id?: string | number;
+ /** 角色 ID */
+ role_id: string;
+ /** 角色名称 */
+ role_name: string;
+ /** 角色昵称 */
+ nickname: string;
+ /** 头像 */
+ avatar: string;
+ /** 颜色 */
+ color: string;
+ /** 消息内容 */
+ content: string;
+ /** 时间戳 */
+ timestamp: string;
+ /** 轮次 */
+ round_number: number;
+ /** 是否为结论 */
+ is_conclusion?: boolean;
+}
+
+/**
+ * 会议响应接口
+ */
+export interface MeetingResponse {
+ success: boolean;
+ session_id: string;
+ messages: MeetingMessage[];
+ round_number: number;
+ is_concluded: boolean;
+ conclusion?: MeetingMessage | null;
+}
+
+/**
+ * 会议请求接口
+ */
+export interface MeetingRequest {
+ topic: string;
+ user_id?: string;
+ user_nickname?: string;
+ session_id?: string;
+ user_message?: string;
+ conversation_history?: MeetingMessage[];
+}
+
+/**
+ * 投研会议室角色配置
+ */
+export const MEETING_ROLES: Record = {
+ buffett: {
+ id: 'buffett',
+ name: '巴菲特',
+ nickname: '唱多者',
+ roleType: 'bull',
+ avatar: '/avatars/buffett.png',
+ color: '#10B981',
+ gradient: 'linear(to-br, green.400, emerald.600)',
+ description: '主观多头,善于分析事件的潜在利好和长期价值',
+ icon: React.createElement(TrendingUp, { className: 'w-5 h-5' }),
+ },
+ big_short: {
+ id: 'big_short',
+ name: '大空头',
+ nickname: '大空头',
+ roleType: 'bear',
+ avatar: '/avatars/big_short.png',
+ color: '#EF4444',
+ gradient: 'linear(to-br, red.400, rose.600)',
+ description: '善于分析事件和财报中的风险因素,帮助投资者避雷',
+ icon: React.createElement(TrendingDown, { className: 'w-5 h-5' }),
+ },
+ simons: {
+ id: 'simons',
+ name: '量化分析员',
+ nickname: '西蒙斯',
+ roleType: 'quant',
+ avatar: '/avatars/simons.png',
+ color: '#3B82F6',
+ gradient: 'linear(to-br, blue.400, cyan.600)',
+ description: '中性立场,使用量化分析工具分析技术指标',
+ icon: React.createElement(BarChart2, { className: 'w-5 h-5' }),
+ },
+ leek: {
+ id: 'leek',
+ name: '韭菜',
+ nickname: '牢大',
+ roleType: 'retail',
+ avatar: '/avatars/leek.png',
+ color: '#F59E0B',
+ gradient: 'linear(to-br, amber.400, yellow.600)',
+ description: '贪婪又讨厌亏损,热爱追涨杀跌的典型散户',
+ icon: React.createElement(Users, { className: 'w-5 h-5' }),
+ },
+ fund_manager: {
+ id: 'fund_manager',
+ name: '基金经理',
+ nickname: '决策者',
+ roleType: 'manager',
+ avatar: '/avatars/fund_manager.png',
+ color: '#8B5CF6',
+ gradient: 'linear(to-br, purple.400, violet.600)',
+ description: '总结其他人的发言做出最终决策',
+ icon: React.createElement(Crown, { className: 'w-5 h-5' }),
+ },
+};
+
+/**
+ * 用户角色配置(用于用户插话)
+ */
+export const USER_ROLE: MeetingRoleConfig = {
+ id: 'user',
+ name: '用户',
+ nickname: '你',
+ roleType: 'retail',
+ avatar: '',
+ color: '#6366F1',
+ gradient: 'linear(to-br, indigo.400, purple.600)',
+ description: '参与讨论的用户',
+ icon: React.createElement(Users, { className: 'w-5 h-5' }),
+};
+
+/**
+ * 获取角色配置
+ */
+export const getRoleConfig = (roleId: string): MeetingRoleConfig | undefined => {
+ if (roleId === 'user') return USER_ROLE;
+ return MEETING_ROLES[roleId];
+};
+
+/**
+ * 获取所有非管理者角色(用于发言顺序)
+ */
+export const getSpeakingRoles = (): MeetingRoleConfig[] => {
+ return Object.values(MEETING_ROLES).filter(
+ (role) => role.roleType !== 'manager'
+ );
+};
+
+/**
+ * 会议状态枚举
+ */
+export enum MeetingStatus {
+ /** 空闲,等待用户输入议题 */
+ IDLE = 'idle',
+ /** 正在开始会议 */
+ STARTING = 'starting',
+ /** 正在讨论中 */
+ DISCUSSING = 'discussing',
+ /** 某个角色正在发言 */
+ SPEAKING = 'speaking',
+ /** 等待用户输入(可以插话或继续) */
+ WAITING_INPUT = 'waiting_input',
+ /** 会议已结束,得出结论 */
+ CONCLUDED = 'concluded',
+ /** 发生错误 */
+ ERROR = 'error',
+}
+
+/**
+ * SSE 事件类型
+ */
+export type MeetingEventType =
+ | 'session_start'
+ | 'order_decided'
+ | 'speaking_start'
+ | 'message'
+ | 'meeting_end';
+
+/**
+ * SSE 事件接口
+ */
+export interface MeetingEvent {
+ type: MeetingEventType;
+ session_id?: string;
+ order?: string[];
+ role_id?: string;
+ role_name?: string;
+ message?: MeetingMessage;
+ is_concluded?: boolean;
+ round_number?: number;
+}
diff --git a/src/views/AgentChat/hooks/index.ts b/src/views/AgentChat/hooks/index.ts
index bb28170e..b94d45bd 100644
--- a/src/views/AgentChat/hooks/index.ts
+++ b/src/views/AgentChat/hooks/index.ts
@@ -32,3 +32,9 @@ export type {
UseAgentChatParams,
UseAgentChatReturn,
} from './useAgentChat';
+
+export { useInvestmentMeeting } from './useInvestmentMeeting';
+export type {
+ UseInvestmentMeetingParams,
+ UseInvestmentMeetingReturn,
+} from './useInvestmentMeeting';
diff --git a/src/views/AgentChat/hooks/useInvestmentMeeting.ts b/src/views/AgentChat/hooks/useInvestmentMeeting.ts
new file mode 100644
index 00000000..e9a9d36f
--- /dev/null
+++ b/src/views/AgentChat/hooks/useInvestmentMeeting.ts
@@ -0,0 +1,403 @@
+// src/views/AgentChat/hooks/useInvestmentMeeting.ts
+// 投研会议室 Hook - 管理会议状态、发送消息、处理 SSE 流
+
+import { useState, useCallback, useRef } from 'react';
+import axios from 'axios';
+import {
+ MeetingMessage,
+ MeetingStatus,
+ MeetingEvent,
+ MeetingResponse,
+ getRoleConfig,
+} from '../constants/meetingRoles';
+
+/**
+ * useInvestmentMeeting Hook 参数
+ */
+export interface UseInvestmentMeetingParams {
+ /** 当前用户 ID */
+ userId?: string;
+ /** 当前用户昵称 */
+ userNickname?: string;
+ /** Toast 通知函数 */
+ onToast?: (options: {
+ title: string;
+ description?: string;
+ status: 'success' | 'error' | 'warning' | 'info';
+ }) => void;
+}
+
+/**
+ * useInvestmentMeeting Hook 返回值
+ */
+export interface UseInvestmentMeetingReturn {
+ /** 会议消息列表 */
+ messages: MeetingMessage[];
+ /** 会议状态 */
+ status: MeetingStatus;
+ /** 当前正在发言的角色 ID */
+ speakingRoleId: string | null;
+ /** 当前会话 ID */
+ sessionId: string | null;
+ /** 当前轮次 */
+ currentRound: number;
+ /** 是否已得出结论 */
+ isConcluded: boolean;
+ /** 结论消息 */
+ conclusion: MeetingMessage | null;
+ /** 输入框内容 */
+ inputValue: string;
+ /** 设置输入框内容 */
+ setInputValue: (value: string) => void;
+ /** 开始会议(用户提出议题) */
+ startMeeting: (topic: string) => Promise;
+ /** 继续会议(下一轮讨论) */
+ continueMeeting: (userMessage?: string) => Promise;
+ /** 用户插话 */
+ sendUserMessage: (message: string) => Promise;
+ /** 重置会议 */
+ resetMeeting: () => void;
+ /** 当前议题 */
+ currentTopic: string;
+ /** 是否正在加载 */
+ isLoading: boolean;
+}
+
+/**
+ * 投研会议室 Hook
+ *
+ * 管理投研会议的完整生命周期:
+ * 1. 启动会议(用户提出议题)
+ * 2. 处理角色发言(支持流式和非流式)
+ * 3. 用户插话
+ * 4. 继续讨论
+ * 5. 得出结论
+ */
+export const useInvestmentMeeting = ({
+ userId = 'anonymous',
+ userNickname = '匿名用户',
+ onToast,
+}: UseInvestmentMeetingParams = {}): UseInvestmentMeetingReturn => {
+ // 会议状态
+ const [messages, setMessages] = useState([]);
+ const [status, setStatus] = useState(MeetingStatus.IDLE);
+ const [speakingRoleId, setSpeakingRoleId] = useState(null);
+ const [sessionId, setSessionId] = useState(null);
+ const [currentRound, setCurrentRound] = useState(0);
+ const [isConcluded, setIsConcluded] = useState(false);
+ const [conclusion, setConclusion] = useState(null);
+ const [currentTopic, setCurrentTopic] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [inputValue, setInputValue] = useState('');
+
+ // 用于取消 SSE 连接
+ const eventSourceRef = useRef(null);
+
+ /**
+ * 添加消息到列表
+ */
+ const addMessage = useCallback((message: MeetingMessage) => {
+ setMessages((prev) => [
+ ...prev,
+ {
+ ...message,
+ id: message.id || Date.now() + Math.random(),
+ },
+ ]);
+ }, []);
+
+ /**
+ * 重置会议状态
+ */
+ const resetMeeting = useCallback(() => {
+ // 关闭 SSE 连接
+ if (eventSourceRef.current) {
+ eventSourceRef.current.close();
+ eventSourceRef.current = null;
+ }
+
+ setMessages([]);
+ setStatus(MeetingStatus.IDLE);
+ setSpeakingRoleId(null);
+ setSessionId(null);
+ setCurrentRound(0);
+ setIsConcluded(false);
+ setConclusion(null);
+ setCurrentTopic('');
+ setIsLoading(false);
+ setInputValue('');
+ }, []);
+
+ /**
+ * 启动会议(使用流式 SSE)
+ */
+ const startMeetingStream = useCallback(
+ async (topic: string) => {
+ setCurrentTopic(topic);
+ setStatus(MeetingStatus.STARTING);
+ setIsLoading(true);
+ setMessages([]);
+
+ try {
+ // 使用 EventSource 进行 SSE 连接
+ const params = new URLSearchParams({
+ topic,
+ user_id: userId,
+ user_nickname: userNickname,
+ });
+
+ const eventSource = new EventSource(
+ `/mcp/agent/meeting/stream?${params.toString()}`
+ );
+ eventSourceRef.current = eventSource;
+
+ eventSource.onmessage = (event) => {
+ try {
+ const data: MeetingEvent = JSON.parse(event.data);
+
+ switch (data.type) {
+ case 'session_start':
+ setSessionId(data.session_id || null);
+ setStatus(MeetingStatus.DISCUSSING);
+ break;
+
+ case 'order_decided':
+ // 发言顺序已决定
+ break;
+
+ case 'speaking_start':
+ setSpeakingRoleId(data.role_id || null);
+ setStatus(MeetingStatus.SPEAKING);
+ break;
+
+ case 'message':
+ if (data.message) {
+ addMessage(data.message);
+ setSpeakingRoleId(null);
+
+ // 检查是否是结论
+ if (data.message.is_conclusion) {
+ setConclusion(data.message);
+ setIsConcluded(true);
+ }
+ }
+ break;
+
+ case 'meeting_end':
+ setCurrentRound(data.round_number || 1);
+ setIsConcluded(data.is_concluded || false);
+ setStatus(
+ data.is_concluded
+ ? MeetingStatus.CONCLUDED
+ : MeetingStatus.WAITING_INPUT
+ );
+ setIsLoading(false);
+ eventSource.close();
+ break;
+ }
+ } catch (e) {
+ console.error('解析 SSE 事件失败:', e);
+ }
+ };
+
+ eventSource.onerror = (error) => {
+ console.error('SSE 连接错误:', error);
+ eventSource.close();
+ setStatus(MeetingStatus.ERROR);
+ setIsLoading(false);
+ onToast?.({
+ title: '连接失败',
+ description: '会议连接中断,请重试',
+ status: 'error',
+ });
+ };
+ } catch (error) {
+ console.error('启动会议失败:', error);
+ setStatus(MeetingStatus.ERROR);
+ setIsLoading(false);
+ onToast?.({
+ title: '启动会议失败',
+ description: '请稍后重试',
+ status: 'error',
+ });
+ }
+ },
+ [userId, userNickname, addMessage, onToast]
+ );
+
+ /**
+ * 启动会议(非流式,获取完整响应)
+ */
+ const startMeeting = useCallback(
+ async (topic: string) => {
+ setCurrentTopic(topic);
+ setStatus(MeetingStatus.STARTING);
+ setIsLoading(true);
+ setMessages([]);
+
+ try {
+ const response = await axios.post(
+ '/mcp/agent/meeting/start',
+ {
+ topic,
+ user_id: userId,
+ user_nickname: userNickname,
+ }
+ );
+
+ if (response.data.success) {
+ const data = response.data;
+
+ setSessionId(data.session_id);
+ setCurrentRound(data.round_number);
+ setIsConcluded(data.is_concluded);
+
+ // 添加所有消息
+ data.messages.forEach((msg) => {
+ addMessage(msg);
+ });
+
+ // 设置结论
+ if (data.conclusion) {
+ setConclusion(data.conclusion);
+ }
+
+ setStatus(
+ data.is_concluded
+ ? MeetingStatus.CONCLUDED
+ : MeetingStatus.WAITING_INPUT
+ );
+ } else {
+ throw new Error('会议启动失败');
+ }
+ } catch (error: any) {
+ console.error('启动会议失败:', error);
+ setStatus(MeetingStatus.ERROR);
+ onToast?.({
+ title: '启动会议失败',
+ description: error.response?.data?.detail || error.message,
+ status: 'error',
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [userId, userNickname, addMessage, onToast]
+ );
+
+ /**
+ * 继续会议讨论
+ */
+ const continueMeeting = useCallback(
+ async (userMessage?: string) => {
+ if (!currentTopic) {
+ onToast?.({
+ title: '无法继续',
+ description: '请先启动会议',
+ status: 'warning',
+ });
+ return;
+ }
+
+ setStatus(MeetingStatus.DISCUSSING);
+ setIsLoading(true);
+
+ try {
+ const response = await axios.post(
+ '/mcp/agent/meeting/continue',
+ {
+ topic: currentTopic,
+ user_id: userId,
+ user_nickname: userNickname,
+ session_id: sessionId,
+ user_message: userMessage,
+ conversation_history: messages,
+ }
+ );
+
+ if (response.data.success) {
+ const data = response.data;
+
+ setCurrentRound(data.round_number);
+ setIsConcluded(data.is_concluded);
+
+ // 添加新的消息
+ data.messages.forEach((msg) => {
+ addMessage(msg);
+ });
+
+ // 设置结论
+ if (data.conclusion) {
+ setConclusion(data.conclusion);
+ }
+
+ setStatus(
+ data.is_concluded
+ ? MeetingStatus.CONCLUDED
+ : MeetingStatus.WAITING_INPUT
+ );
+ }
+ } catch (error: any) {
+ console.error('继续会议失败:', error);
+ setStatus(MeetingStatus.ERROR);
+ onToast?.({
+ title: '继续会议失败',
+ description: error.response?.data?.detail || error.message,
+ status: 'error',
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ },
+ [currentTopic, userId, userNickname, sessionId, messages, addMessage, onToast]
+ );
+
+ /**
+ * 用户发送消息(插话)
+ */
+ const sendUserMessage = useCallback(
+ async (message: string) => {
+ if (!message.trim()) return;
+
+ // 先添加用户消息到列表
+ const userRole = getRoleConfig('user');
+ addMessage({
+ role_id: 'user',
+ role_name: '用户',
+ nickname: userNickname,
+ avatar: userRole?.avatar || '',
+ color: userRole?.color || '#6366F1',
+ content: message,
+ timestamp: new Date().toISOString(),
+ round_number: currentRound,
+ });
+
+ // 清空输入框
+ setInputValue('');
+
+ // 继续会议,带上用户消息
+ await continueMeeting(message);
+ },
+ [userNickname, currentRound, addMessage, continueMeeting]
+ );
+
+ return {
+ messages,
+ status,
+ speakingRoleId,
+ sessionId,
+ currentRound,
+ isConcluded,
+ conclusion,
+ inputValue,
+ setInputValue,
+ startMeeting,
+ continueMeeting,
+ sendUserMessage,
+ resetMeeting,
+ currentTopic,
+ isLoading,
+ };
+};
+
+export default useInvestmentMeeting;
diff --git a/src/views/AgentChat/index.js b/src/views/AgentChat/index.js
index 7651d6bd..ef7494f4 100644
--- a/src/views/AgentChat/index.js
+++ b/src/views/AgentChat/index.js
@@ -1,9 +1,12 @@
// src/views/AgentChat/index.js
// 超炫酷的 AI 投研助手 - HeroUI v3 现代深色主题版本
// 使用 Framer Motion 物理动画引擎 + 毛玻璃效果
+// 支持两种模式:单一聊天模式 & 投研会议室模式
import React, { useState } from 'react';
-import { Box, Flex, useToast } from '@chakra-ui/react';
+import { Box, Flex, useToast, HStack, Button, Tooltip } from '@chakra-ui/react';
+import { motion, AnimatePresence } from 'framer-motion';
+import { MessageSquare, Users } from 'lucide-react';
import { useAuth } from '@contexts/AuthContext';
// 常量配置 - 从 TypeScript 模块导入
@@ -14,10 +17,19 @@ 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 深色主题)
*
@@ -28,13 +40,19 @@ import { useAgentSessions, useAgentChat, useFileUpload } from './hooks';
*
* 主组件职责:
* 1. 组合各个自定义 Hooks
- * 2. 管理 UI 状态(侧边栏开关、模型选择、工具选择)
+ * 2. 管理 UI 状态(侧边栏开关、模型选择、工具选择、聊天模式)
* 3. 组合渲染子组件
+ *
+ * 新增功能(2024-11):
+ * - 投研会议室模式:多 AI 角色协作讨论投资议题
*/
const AgentChat = () => {
const { user } = useAuth();
const toast = useToast();
+ // ==================== 聊天模式状态 ====================
+ const [chatMode, setChatMode] = useState(ChatMode.SINGLE);
+
// ==================== UI 状态(主组件管理)====================
const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL_ID);
const [selectedTools, setSelectedTools] = useState(DEFAULT_SELECTED_TOOLS);
@@ -88,52 +106,129 @@ const AgentChat = () => {
// ==================== 渲染组件 ====================
return (
-
- {/* 左侧栏 */}
- setIsLeftSidebarOpen(false)}
- sessions={sessions}
- currentSessionId={currentSessionId}
- onSessionSwitch={switchSession}
- onNewSession={createNewSession}
- isLoadingSessions={isLoadingSessions}
- user={user}
- />
+
+ {/* 模式切换栏 */}
+
+
+
+
+ }
+ 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',
+ }}
+ >
+ 智能助手
+
+
+
- {/* 中间聊天区 */}
- setIsLeftSidebarOpen(true)}
- onToggleRightSidebar={() => setIsRightSidebarOpen(true)}
- onNewSession={createNewSession}
- userAvatar={user?.avatar}
- inputRef={inputRef}
- fileInputRef={fileInputRef}
- />
+
+
+ }
+ 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',
+ }}
+ >
+ 投研会议室
+
+
+
+
+
- {/* 右侧栏 */}
- setIsRightSidebarOpen(false)}
- selectedModel={selectedModel}
- onModelChange={setSelectedModel}
- selectedTools={selectedTools}
- onToolsChange={setSelectedTools}
- sessionsCount={sessions.length}
- messagesCount={messages.length}
- />
+ {/* 主内容区 */}
+
+
+ {chatMode === ChatMode.SINGLE ? (
+
+ {/* 左侧栏 */}
+ setIsLeftSidebarOpen(false)}
+ sessions={sessions}
+ currentSessionId={currentSessionId}
+ onSessionSwitch={switchSession}
+ onNewSession={createNewSession}
+ isLoadingSessions={isLoadingSessions}
+ user={user}
+ />
+
+ {/* 中间聊天区 */}
+ setIsLeftSidebarOpen(true)}
+ onToggleRightSidebar={() => setIsRightSidebarOpen(true)}
+ onNewSession={createNewSession}
+ userAvatar={user?.avatar}
+ inputRef={inputRef}
+ fileInputRef={fileInputRef}
+ />
+
+ {/* 右侧栏 */}
+ setIsRightSidebarOpen(false)}
+ selectedModel={selectedModel}
+ onModelChange={setSelectedModel}
+ selectedTools={selectedTools}
+ onToolsChange={setSelectedTools}
+ sessionsCount={sessions.length}
+ messagesCount={messages.length}
+ />
+
+ ) : (
+
+ {/* 投研会议室 */}
+
+
+ )}
+
+
);
};