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 (
-
+
🏛️ 龙虾议事厅
-
提示:首次使用请先注册超级用户
);
}
-// ============ 会议列表页面 ============
+// ============ 会议列表 ============
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 (
-
🏛️ 我的会议室
-
+ 📋 我的会议室
+
-
-
创建新会议
+ 创建会议
-
-
- {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}
}
))}
-
);
}
-// ============ 主应用 ============
+// ============ 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;