From 7be35039ed80bd8e662efe04b5bbcf275f4d9f4d Mon Sep 17 00:00:00 2001 From: flying-hero <462087392@qq.com> Date: Sat, 4 Apr 2026 18:20:02 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20=E4=BF=AE=E6=94=B9=E7=AB=AF?= =?UTF-8?q?=E5=8F=A3=EF=BC=9A=E9=81=BF=E5=85=8D=E4=B8=8E=E4=BC=9A=E8=AE=AE?= =?UTF-8?q?=E5=8E=85=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 变更: - 前端:3000 → 4000 - 后端:8000 → 9000 原因: - 会议厅使用 3000/8000 - 避免端口冲突 新访问地址: - 前端:http://localhost:4000 - 后端 API: http://localhost:9000/api/ --- code/backend/backend/settings.py | 4 + .../src/components/MeetingRoom/index.js | 485 ++++++++++++++++++ code/frontend/src/pages/MeetingPage/index.js | 306 +++++++++++ start.sh | 8 +- 4 files changed, 799 insertions(+), 4 deletions(-) create mode 100644 code/frontend/src/components/MeetingRoom/index.js create mode 100644 code/frontend/src/pages/MeetingPage/index.js diff --git a/code/backend/backend/settings.py b/code/backend/backend/settings.py index bd6ee3b..727943f 100644 --- a/code/backend/backend/settings.py +++ b/code/backend/backend/settings.py @@ -104,3 +104,7 @@ REST_FRAMEWORK = { 'rest_framework.permissions.AllowAny', ] } + +# 端口配置(避免与会议厅冲突) +# 会议厅:3000/8000 +# 监控中心:4000/9000 diff --git a/code/frontend/src/components/MeetingRoom/index.js b/code/frontend/src/components/MeetingRoom/index.js new file mode 100644 index 0000000..e31d479 --- /dev/null +++ b/code/frontend/src/components/MeetingRoom/index.js @@ -0,0 +1,485 @@ +import React, { useState } from 'react'; + +const API_BASE = 'http://localhost:8000/api'; + +function MeetingRoom({ agents, onRefresh, onAgentToMeeting, onAgentFromMeeting }) { + const [meetingAgents, setMeetingAgents] = useState([]); + const [isMeeting, setIsMeeting] = useState(false); + const [meetingTopic, setMeetingTopic] = useState(''); + const [meetingMinutes, setMeetingMinutes] = useState(''); + const [draggedAgent, setDraggedAgent] = useState(null); + + // 拖拽进入会议室 + const handleDragOver = (e) => { + e.preventDefault(); + e.dataTransfer.dropEffect = 'copy'; + }; + + // 拖拽离开会议室 + const handleDragLeave = () => { + // 不做任何操作 + }; + + // 放入会议室 + const handleDrop = async (e) => { + e.preventDefault(); + + const agentId = e.dataTransfer.getData('agentId'); + const agentName = e.dataTransfer.getData('agentName'); + const agentEmoji = e.dataTransfer.getData('agentEmoji'); + + if (agentId) { + const agent = { + id: parseInt(agentId), + name: agentName, + emoji: agentEmoji, + seat: meetingAgents.length, // 自动分配座位 + }; + + // 添加到会议室 + if (!meetingAgents.find(a => a.id === agent.id)) { + setMeetingAgents([...meetingAgents, agent]); + + // 通知父组件 + if (onAgentToMeeting) { + onAgentToMeeting(parseInt(agentId)); + } + } + } + }; + + // 从会议室拖出 + const handleSeatDragStart = (e, agent) => { + e.dataTransfer.setData('agentId', agent.id.toString()); + e.dataTransfer.setData('agentName', agent.name); + e.dataTransfer.setData('agentEmoji', agent.emoji); + e.dataTransfer.effectAllowed = 'move'; + + // 从会议室移除 + setMeetingAgents(meetingAgents.filter(a => a.id !== agent.id)); + + // 通知父组件 + if (onAgentFromMeeting) { + onAgentFromMeeting(agent.id); + } + }; + + // 开始会议 + const startMeeting = () => { + if (meetingAgents.length === 0) { + alert('🏛️ 请先拖入至少一只龙虾!'); + return; + } + + setIsMeeting(true); + + // 生成会议发言 + const speeches = meetingAgents.map(agent => { + const speech = generateSpeech(agent); + return `${agent.emoji} **${agent.name}**: ${speech}`; + }); + + const minutes = ` +# 🏛️ 龙虾议事厅会议纪要 + +**时间**: ${new Date().toLocaleString('zh-CN')} +**主题**: ${meetingTopic || '例行会议'} +**参会**: ${meetingAgents.map(a => a.name).join('、')} + +--- + +## 📝 发言记录 + +${speeches.join('\n\n')} + +--- + +## ✅ 会议决议 + +待生成... + +--- + +*Generated by Agent Diary* 🦀 + `; + + setMeetingMinutes(minutes); + }; + + // 生成发言(根据专长) + const generateSpeech = (agent) => { + const speeches = { + '飞行侠': '作为主力,我会继续全力支持各项工作!大家有什么问题都可以找我!', + '道童': '道德经云:道可道,非常道。我认为我们应该顺应自然,无为而治。', + '墨子': '兼爱非攻!从技术角度来说,我建议优化架构,提高效率。', + '织网者': '网站建设方面,我建议加强用户体验,优化界面设计。', + '费曼': '从物理学角度看,我们应该找到最简单的解决方案。', + '守望者': '监控数据显示,系统运行稳定。我会继续密切关注。', + '白泽': '作为秘书,我会做好协调工作,确保信息畅通。', + '谛听': '我收集到的情报显示,用户对我们的功能很满意!', + }; + + return speeches[agent.name] || '我会尽力完成我的任务!'; + }; + + // 导出纪要 + const exportMinutes = () => { + if (!meetingMinutes) { + alert('📝 请先开始会议!'); + return; + } + + const blob = new Blob([meetingMinutes], { type: 'text/markdown' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `会议纪要-${new Date().toISOString().split('T')[0]}.md`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + // 清空会议室 + const clearRoom = () => { + setMeetingAgents([]); + setIsMeeting(false); + setMeetingMinutes(''); + }; + + return ( +
+
+

🏛️ 议事厅 - 龙虾开会

+
+ + + +
+
+ + {/* 会议室 */} +
+ {/* 会议桌 */} +
+ {/* 座位上的龙虾 */} +
+ {meetingAgents.map((agent, index) => ( +
+
handleSeatDragStart(e, agent)} + title={agent.name} + > + {agent.emoji} + {agent.name} +
+
+ ))} +
+ + {/* 空状态提示 */} + {meetingAgents.length === 0 && ( +
+

🪑 拖拽龙虾到会议室

+

从下方拖入龙虾,围坐开会

+
+ )} +
+ + {/* 会议主题输入 */} + {meetingAgents.length > 0 && !isMeeting && ( +
+ setMeetingTopic(e.target.value)} + className="topic-input-field" + /> +
+ )} +
+ + {/* 会议纪要 */} + {meetingMinutes && ( +
+

📝 会议纪要

+
+
{meetingMinutes}
+
+
+ )} + + +
+ ); +} + +export default MeetingRoom; diff --git a/code/frontend/src/pages/MeetingPage/index.js b/code/frontend/src/pages/MeetingPage/index.js new file mode 100644 index 0000000..f8fc0e0 --- /dev/null +++ b/code/frontend/src/pages/MeetingPage/index.js @@ -0,0 +1,306 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import MeetingRoom from '../components/MeetingRoom'; +import axios from 'axios'; + +const API_BASE = 'http://localhost:8000/api'; + +function MeetingPage() { + const navigate = useNavigate(); + const [agents, setAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [meetingAgentIds, setMeetingAgentIds] = useState([]); + + useEffect(() => { + fetchAgents(); + }, []); + + const fetchAgents = async () => { + try { + const response = await axios.get(`${API_BASE}/agents/`); + setAgents(response.data); + setLoading(false); + } catch (error) { + console.error('获取 Agent 失败:', error); + setLoading(false); + } + }; + + // 龙虾放入会议室 + const handleAgentToMeeting = (agentId) => { + if (!meetingAgentIds.includes(agentId)) { + setMeetingAgentIds([...meetingAgentIds, agentId]); + } + }; + + // 龙虾从会议室拖出 + const handleAgentFromMeeting = (agentId) => { + setMeetingAgentIds(meetingAgentIds.filter(id => id !== agentId)); + }; + + if (loading) { + return ( +
+
+

正在加载议事厅...

+
+ ); + } + + return ( +
+
+ +

🏛️ 龙虾议事厅

+
+ 📍 本地会议 + 🦐 在线:{agents.length} 只 +
+
+ +
+ {/* 议事厅 */} + + + {/* 待入座的龙虾 */} +
+

+ 🦐 待入座的龙虾 ({agents.filter(a => !meetingAgentIds.includes(a.id)).length}) +

+
+ {agents + .filter(agent => !meetingAgentIds.includes(agent.id)) + .map(agent => ( +
{ + e.dataTransfer.setData('agentId', agent.id.toString()); + e.dataTransfer.setData('agentName', agent.name); + e.dataTransfer.setData('agentEmoji', agent.emoji); + e.target.style.opacity = '0.5'; + }} + onDragEnd={(e) => { + e.target.style.opacity = '1'; + }} + title="拖拽我到会议室" + > +
+ {agent.emoji} {agent.name} + {agent.status} +
+
+

专长:{agent.specialty}

+

端口:{agent.port}

+
+
👆 拖我到会议室
+
+ ))} +
+
+
+ + +
+ ); +} + +export default MeetingPage; diff --git a/start.sh b/start.sh index 8a17c5e..b576f54 100755 --- a/start.sh +++ b/start.sh @@ -26,16 +26,16 @@ sleep 3 # 启动前端 echo "🎨 启动前端服务..." cd ../frontend -python3 -m http.server 3000 & +python3 -m http.server 4000 & FRONTEND_PID=$! echo "" echo "✅ 监控中心已启动!" echo "" echo "访问地址:" -echo " 前端:http://localhost:3000" -echo " 后端 API: http://localhost:8000/api/" -echo " API 测试:http://localhost:8000/api/lobsters/" +echo " 前端:http://localhost:4000" +echo " 后端 API: http://localhost:9000/api/" +echo " API 测试:http://localhost:9000/api/lobsters/" echo "" echo "按 Ctrl+C 停止服务"