diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 06402c75..6085132b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16000,9 +16000,9 @@ } }, "node_modules/typescript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", - "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", "license": "Apache-2.0", "peer": true, "bin": { @@ -16010,7 +16010,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=14.17" + "node": ">=4.2.0" } }, "node_modules/unbox-primitive": { diff --git a/frontend/src/App.js b/frontend/src/App.js index ab539aad..cf76a624 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -4,28 +4,24 @@ import axios from 'axios'; const API_BASE = 'http://localhost:8000/api/v1'; -// 配置 axios 默认头 axios.interceptors.request.use(config => { const token = localStorage.getItem('token'); - if (token) { - config.headers.Authorization = `Bearer ${token}`; - } + if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); // ============ 登录页面 ============ function LoginPage() { - const [username, setUsername] = useState(''); - const [password, setPassword] = useState(''); + const [username, setUsername] = useState('test'); + const [password, setPassword] = useState('test123'); const navigate = useNavigate(); const handleLogin = async (e) => { e.preventDefault(); try { - const response = await axios.post(`${API_BASE}/auth/login/`, { username, password }); - const token = response.data.token; - localStorage.setItem('token', token); - localStorage.setItem('user', JSON.stringify(response.data.user)); + const res = await axios.post(`${API_BASE}/auth/login/`, { username, password }); + localStorage.setItem('token', res.data.token); + localStorage.setItem('user', JSON.stringify(res.data.user)); navigate('/meetings'); } catch (error) { alert('登录失败:' + (error.response?.data?.detail || error.message)); @@ -33,35 +29,20 @@ function LoginPage() { }; return ( -
+

🏛️ 龙虾议事厅

- setUsername(e.target.value)} - style={styles.input} - required - /> - setPassword(e.target.value)} - style={styles.input} - required - /> - + setUsername(e.target.value)} style={styles.input} required /> + setPassword(e.target.value)} style={styles.input} required /> +
-

提示:首次使用请先注册超级用户

); } -// ============ 会议列表页面 ============ +// ============ 会议列表 ============ function MeetingList() { const [meetings, setMeetings] = useState([]); const [topic, setTopic] = useState(''); @@ -69,92 +50,50 @@ function MeetingList() { const token = localStorage.getItem('token'); useEffect(() => { - if (!token) { - navigate('/login'); - return; - } + if (!token) { navigate('/login'); return; } fetchMeetings(); }, []); const fetchMeetings = async () => { try { - const response = await axios.get(`${API_BASE}/meetings/`, { - headers: { Authorization: `Bearer ${token}` } - }); - setMeetings(response.data); - } catch (error) { - console.error('获取会议失败:', error); - } + const res = await axios.get(`${API_BASE}/meetings/`); + setMeetings(res.data); + } catch (error) { console.error(error); } }; const createMeeting = async (e) => { e.preventDefault(); try { - const response = await axios.post( - `${API_BASE}/meetings/`, - { topic }, - { headers: { Authorization: `Bearer ${token}` } } - ); - navigate(`/meeting/${response.data.id}`); + const res = await axios.post(`${API_BASE}/meetings/`, { topic }); + navigate(`/meeting/${res.data.id}`); } catch (error) { alert('创建失败:' + (error.response?.data?.detail || error.message)); } }; - const joinMeeting = async (meetingId) => { - const inviteCode = prompt('请输入邀请码:'); - if (!inviteCode) return; - - try { - await axios.post( - `${API_BASE}/meetings/${meetingId}/join/`, - { invite_code: inviteCode }, - { headers: { Authorization: `Bearer ${token}` } } - ); - navigate(`/meeting/${meetingId}`); - } catch (error) { - alert('加入失败:' + (error.response?.data?.error || error.message)); - } - }; - - const logout = () => { - localStorage.removeItem('token'); - navigate('/login'); - }; + const logout = () => { localStorage.removeItem('token'); navigate('/login'); }; return (
-

🏛️ 我的会议室

- +

📋 我的会议室

+
-
-

创建新会议

+

创建会议

- setTopic(e.target.value)} - style={styles.input} - required - /> - + setTopic(e.target.value)} style={styles.input} required /> +
- -
- {meetings.map(meeting => ( -
+
+ {meetings.map(m => ( +
-

{meeting.topic}

-

状态:{meeting.status} | 参会:{meeting.participant_count} | 邀请码:{meeting.invite_code}

-
-
- - +

{m.topic}

+

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

+
))}
@@ -162,58 +101,50 @@ function MeetingList() { ); } -// ============ 会议室页面 ============ +// ============ 会议室 ============ function MeetingRoom() { const { id } = useParams(); const [messages, setMessages] = useState([]); const [content, setContent] = useState(''); const [participants, setParticipants] = useState([]); - const [lastId, setLastId] = useState(0); + const [meeting, setMeeting] = useState(null); const token = localStorage.getItem('token'); useEffect(() => { if (!token) return; + fetchMeeting(); fetchParticipants(); fetchMessages(); - - // 1 秒轮询新消息 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 response = await axios.get(`${API_BASE}/meetings/${id}/participants/`, { - headers: { Authorization: `Bearer ${token}` } - }); - setParticipants(response.data); - } catch (error) { - console.error('获取参会者失败:', error); - } + const res = await axios.get(`${API_BASE}/meetings/${id}/participants/`); + setParticipants(res.data); + } catch (error) { console.error(error); } }; const fetchMessages = async () => { try { - const response = await axios.get(`${API_BASE}/meetings/${id}/messages/?last_id=${lastId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - if (response.data.messages.length > 0) { - setMessages(prev => [...prev, ...response.data.messages]); - setLastId(response.data.messages[response.data.messages.length - 1].id); - } - } catch (error) { - console.error('获取消息失败:', error); - } + 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 }, - { headers: { Authorization: `Bearer ${token}` } } - ); + await axios.post(`${API_BASE}/meetings/${id}/send_message/`, { content }); setContent(''); fetchMessages(); } catch (error) { @@ -221,48 +152,119 @@ function MeetingRoom() { } }; + const mentionAgent = async () => { + const targetAgentId = prompt('@哪个 Agent?输入 agent_id:'); + if (!targetAgentId || !content.trim()) return; + try { + await axios.post(`${API_BASE}/meetings/${id}/mention_agent/`, { + target_agent_id: targetAgentId, content, + sender_name: localStorage.getItem('user') ? JSON.parse(localStorage.getItem('user')).username : 'User' + }); + setContent(''); + fetchMessages(); + alert(`✅ 已 @${targetAgentId}`); + } 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 => ( - - {p.agent_emoji} {p.nickname} - +
+
{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.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} - required - /> - + setContent(e.target.value)} style={styles.input} /> + +
); } -// ============ 主应用 ============ +// ============ App ============ function App() { return ( @@ -278,126 +280,36 @@ function App() { // ============ 样式 ============ const styles = { - container: { - maxWidth: '1200px', - margin: '0 auto', - padding: '20px', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' - }, - header: { - display: 'flex', - alignItems: 'center', - gap: '20px', - marginBottom: '20px' - }, - title: { - margin: '0', - color: '#1a365d' - }, - card: { - background: 'white', - borderRadius: '12px', - padding: '20px', - marginBottom: '20px', - boxShadow: '0 4px 6px rgba(0,0,0,0.1)' - }, - form: { - display: 'flex', - gap: '10px', - marginBottom: '15px' - }, - input: { - flex: 1, - padding: '12px', - border: '2px solid #e2e8f0', - borderRadius: '8px', - fontSize: '1em' - }, - button: { - padding: '12px 24px', - background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', - color: 'white', - border: 'none', - borderRadius: '8px', - cursor: 'pointer', - fontSize: '1em', - fontWeight: '600' - }, - logoutBtn: { - marginLeft: 'auto', - padding: '8px 16px', - background: '#edf2f7', - border: 'none', - borderRadius: '6px', - cursor: 'pointer' - }, - meetingList: { - display: 'flex', - flexDirection: 'column', - gap: '15px' - }, - meetingCard: { - background: 'white', - borderRadius: '12px', - padding: '20px', - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - boxShadow: '0 4px 6px rgba(0,0,0,0.1)' - }, - smallBtn: { - padding: '8px 16px', - background: '#4299e1', - color: 'white', - border: 'none', - borderRadius: '6px', - cursor: 'pointer', - marginLeft: '10px' - }, - chatContainer: { - background: 'white', - borderRadius: '12px', - padding: '20px', - boxShadow: '0 4px 6px rgba(0,0,0,0.1)' - }, - messages: { - maxHeight: '500px', - overflowY: 'auto', - marginBottom: '20px' - }, - message: { - padding: '15px', - background: '#f7fafc', - borderRadius: '8px', - marginBottom: '10px' - }, - time: { - fontSize: '0.8em', - color: '#718096', - marginLeft: '10px' - }, - participants: { - display: 'flex', - gap: '10px', - marginLeft: 'auto' - }, - participant: { - background: '#edf2f7', - padding: '6px 12px', - borderRadius: '20px', - fontSize: '0.9em' - }, - backLink: { - color: '#4299e1', - textDecoration: 'none', - fontSize: '1.1em' - }, - hint: { - color: '#718096', - fontSize: '0.9em', - textAlign: 'center', - marginTop: '15px' - } + 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' }, + 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;