import React, { useState, useEffect } from 'react'; import { BrowserRouter as Router, Routes, Route, Link, useNavigate, useParams } from 'react-router-dom'; import axios from 'axios'; const API_BASE = 'http://localhost:8000/api/v1'; axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // ============ 登录页面 ============ function LoginPage() { const [username, setUsername] = useState('test'); const [password, setPassword] = useState('test123'); const [mode, setMode] = useState('solo'); const [agents, setAgents] = useState([]); const [selectedAgents, setSelectedAgents] = useState([]); const navigate = useNavigate(); // 扫描本机龙虾 useEffect(() => { scanAgents(); }, []); const scanAgents = async () => { try { const res = await axios.get(`${API_BASE}/user/scan-local-agents/`); setAgents(res.data.agents || []); } catch (error) { console.error('扫描龙虾失败:', error); } }; const handleLogin = async (e) => { e.preventDefault(); try { const payload = { username, password, mode }; if (mode !== 'solo' && selectedAgents.length > 0) { payload.agent_ids = selectedAgents; } const res = await axios.post(`${API_BASE}/auth/login/`, payload); localStorage.setItem('token', res.data.token); localStorage.setItem('user', JSON.stringify(res.data.user)); localStorage.setItem('sessions', JSON.stringify(res.data.sessions)); localStorage.setItem('mode', res.data.mode); navigate('/meetings'); } catch (error) { alert('登录失败:' + (error.response?.data?.detail || error.response?.data?.error || error.message)); } }; const toggleAgent = (agentId) => { setSelectedAgents(prev => prev.includes(agentId) ? prev.filter(id => id !== agentId) : [...prev, agentId] ); }; return (

🏛️ 龙虾议事厅

setUsername(e.target.value)} style={styles.input} required /> setPassword(e.target.value)} style={styles.input} required /> {/* 出战模式选择 */}
{/* 龙虾选择(组队或独当一面) */} {mode !== 'solo' && (
{agents.length === 0 ? (

未找到可用龙虾

) : ( agents.map(a => ( )) )} {selectedAgents.length > 0 && (

已选 {selectedAgents.length} 只龙虾队友 🦸

)}
)}
); } // ============ 会议列表 ============ function MeetingList() { const [meetings, setMeetings] = useState([]); const [topic, setTopic] = useState(''); const [autoAddAgents, setAutoAddAgents] = useState(true); const navigate = useNavigate(); const token = localStorage.getItem('token'); useEffect(() => { if (!token) { navigate('/login'); return; } fetchMeetings(); }, []); const fetchMeetings = async () => { try { const res = await axios.get(`${API_BASE}/meetings/`); setMeetings(res.data); } catch (error) { console.error(error); } }; const createMeeting = async (e) => { e.preventDefault(); try { // 获取当前登录的龙虾 const sessions = JSON.parse(localStorage.getItem('sessions') || '[]'); const agentIds = sessions.filter(s => s.session_type === 'agent').map(s => s.agent_id); const res = await axios.post(`${API_BASE}/meetings/`, { topic, auto_add_virtual_agents: !autoAddAgents ? false : agentIds.length === 0, // 如果没有龙虾才添加虚拟的 host_agent_id: agentIds.length > 0 ? agentIds[0] : null }); navigate(`/meeting/${res.data.id}`); } catch (error) { alert('创建失败:' + (error.response?.data?.detail || error.message)); } }; const logout = () => { localStorage.removeItem('token'); navigate('/login'); }; return (

📋 我的会议室

创建会议

setTopic(e.target.value)} style={styles.input} required />

💡 勾选"添加虚拟坐席"会自动创建 2 个虚拟龙虾参会者,方便测试 @ 功能

{meetings.map(m => (

{m.topic}

状态:{m.status} | 邀请码:{m.invite_code}

))}
); } // ============ 会议室 ============ function MeetingRoom() { const { id } = useParams(); const [messages, setMessages] = useState([]); const [content, setContent] = useState(''); const [participants, setParticipants] = useState([]); const [meeting, setMeeting] = useState(null); const [hoveredSeat, setHoveredSeat] = useState(null); const token = localStorage.getItem('token'); useEffect(() => { if (!token) return; fetchMeeting(); fetchParticipants(); fetchMessages(); const interval = setInterval(fetchMessages, 1000); return () => clearInterval(interval); }, [id]); const fetchMeeting = async () => { try { const res = await axios.get(`${API_BASE}/meetings/${id}/`); setMeeting(res.data); } catch (error) { console.error(error); } }; const fetchParticipants = async () => { try { const res = await axios.get(`${API_BASE}/meetings/${id}/participants/`); setParticipants(res.data); } catch (error) { console.error(error); } }; const fetchMessages = async () => { try { const res = await axios.get(`${API_BASE}/meetings/${id}/messages/?last_id=0`); setMessages(res.data.messages || []); } catch (error) { console.error(error); } }; const sendMessage = async (e) => { e.preventDefault(); if (!content.trim()) return; try { await axios.post(`${API_BASE}/meetings/${id}/send_message/`, { content }); setContent(''); fetchMessages(); } catch (error) { alert('发送失败:' + (error.response?.data?.detail || error.message)); } }; const mentionAgent = async (targetAgentId, agentName) => { const target = targetAgentId || prompt('@哪个 Agent?输入 agent_id:'); if (!target || !content.trim()) return; const name = agentName || target; try { await axios.post(`${API_BASE}/meetings/${id}/mention_agent/`, { target_agent_id: target, content, sender_name: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')).username : 'User' }); setContent(''); fetchMessages(); alert(`✅ 已 @${name}`); } catch (error) { alert('发送失败:' + (error.response?.data?.error || error.message)); } }; const startMeeting = async () => { try { await axios.post(`${API_BASE}/meetings/${id}/start/`); fetchMeeting(); alert('✅ 会议已开始'); } catch (error) { alert('开始失败:' + (error.response?.data?.error || error.message)); } }; const endMeeting = async () => { if (!confirm('确定结束会议?')) return; try { await axios.post(`${API_BASE}/meetings/${id}/end/`); fetchMeeting(); alert('✅ 会议已结束'); } catch (error) { alert('结束失败:' + (error.response?.data?.error || error.message)); } }; const generateMinutes = async () => { try { const res = await axios.get(`${API_BASE}/meetings/${id}/minutes/?output=markdown`); const blob = new Blob([res.data.markdown], { type: 'text/markdown' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `meeting-${id.slice(0, 8)}.md`; a.click(); URL.revokeObjectURL(url); alert('✅ 纪要已导出'); } catch (error) { alert('导出失败:' + error.message); } }; return (
← 返回

{meeting?.topic || '会议室'}

{meeting && {meeting.status}}
{meeting && (

ID: {meeting.id}

邀请码: {meeting.invite_code}

)} {/* 座位图 */}

🪑 座位图 {participants.length}

{participants.map(p => (
{ if (p.agent_id) { setContent(`@${p.nickname} `); document.querySelector('input[placeholder="输入消息..."]')?.focus(); } }} onMouseEnter={() => setHoveredSeat(p.id)} onMouseLeave={() => setHoveredSeat(null)} title={p.agent_id ? '点击 @ 此人' : ''} >
{p.agent_emoji || '👤'}
{p.nickname}
{p.is_host &&
👑
}
))}
{/* 聊天 */}

💬 聊天 {messages.length}

{messages.map(msg => (
{msg.sender_emoji} {msg.sender_name} {new Date(msg.created_at).toLocaleTimeString()}

{msg.content}

{msg.in_reply_to &&
↩️ 回复 #{msg.in_reply_to}
}
))}
setContent(e.target.value)} style={styles.input} />
); } // ============ App ============ function App() { return ( } /> } /> } /> } /> ); } // ============ 样式 ============ const styles = { center: { display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '100vh', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' }, container: { maxWidth: '900px', margin: '0 auto', padding: '20px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' }, header: { display: 'flex', alignItems: 'center', gap: '15px', marginBottom: '20px' }, card: { background: 'white', borderRadius: '12px', padding: '20px', marginBottom: '20px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }, infoCard: { background: '#e7f3ff', border: '1px solid #2196f3', borderRadius: '12px', padding: '15px', marginBottom: '20px' }, title: { margin: '0 0 20px', color: '#1a365d', textAlign: 'center' }, form: { display: 'flex', gap: '10px' }, input: { flex: 1, padding: '12px', border: '2px solid #e2e8f0', borderRadius: '8px', fontSize: '14px' }, btn: { padding: '12px 20px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', border: 'none', borderRadius: '8px', cursor: 'pointer', fontWeight: '600' }, btnGreen: { padding: '8px 16px', background: 'linear-gradient(135deg, #11998e 0%, #38ef7d 100%)', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', marginRight: '8px' }, btnRed: { padding: '8px 16px', background: 'linear-gradient(135deg, #eb3349 0%, #f45c43 100%)', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', marginRight: '8px' }, btnBlue: { padding: '8px 16px', background: 'linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }, btnPink: { padding: '8px 16px', background: 'linear-gradient(135deg, #f093fb 0%, #f5576c 100%)', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer' }, smallBtn: { padding: '8px 16px', background: '#edf2f7', border: 'none', borderRadius: '6px', cursor: 'pointer' }, list: { display: 'flex', flexDirection: 'column', gap: '15px' }, item: { background: 'white', borderRadius: '12px', padding: '20px', display: 'flex', justifyContent: 'space-between', alignItems: 'center', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }, link: { color: '#4299e1', textDecoration: 'none', fontSize: '16px' }, badge: { background: '#667eea', color: 'white', padding: '4px 10px', borderRadius: '20px', fontSize: '12px', fontWeight: '600' }, btnGroup: { display: 'flex', marginTop: '10px' }, seats: { display: 'flex', flexWrap: 'wrap', gap: '15px', justifyContent: 'center' }, seat: { background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', color: 'white', padding: '15px', borderRadius: '50%', width: '90px', height: '90px', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center', cursor: 'pointer', transition: 'transform 0.2s', ':hover': { transform: 'scale(1.1)' } }, seatHover: { transform: 'scale(1.05)' }, seatEmoji: { fontSize: '28px', marginBottom: '5px' }, seatName: { fontSize: '12px', fontWeight: '600' }, hostBadge: { fontSize: '10px', opacity: '0.8' }, messages: { maxHeight: '400px', overflowY: 'auto', marginBottom: '15px' }, msg: { padding: '12px', background: '#f7fafc', borderRadius: '8px', marginBottom: '10px' }, msgHeader: { display: 'flex', justifyContent: 'space-between', marginBottom: '5px' }, msgContent: { margin: '5px 0', color: '#4a5568' }, msgTime: { fontSize: '12px', color: '#a0aec0' }, replyTag: { fontSize: '11px', color: '#a0aec0', marginTop: '5px' } }; export default App;