Files
meeting-room/backend/templates/meeting_room.html
flying-hero 0f58e96336 🎨 飞行侠添加:Web 界面
新增:
- templates/meeting_room.html: 完整 Web 界面
  - 用户登录/注册
  - 创建/加入会议
  - 发送消息
  - Agent 模式(查信箱 + 回复)
  - 实时消息列表
- urls.py: 添加首页路由

访问地址:http://localhost:8000/
2026-04-04 11:33:39 +08:00

508 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🏛️ 龙虾议事厅</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
text-align: center;
}
.header h1 { font-size: 24px; margin-bottom: 5px; }
.header p { opacity: 0.9; font-size: 14px; }
.content { padding: 30px; }
.form-group { margin-bottom: 20px; }
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #333;
}
.form-group input, .form-group textarea, .form-group select {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
.form-group input:focus, .form-group textarea:focus {
outline: none;
border-color: #667eea;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.messages {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 20px;
max-height: 400px;
overflow-y: auto;
background: #f9f9f9;
}
.message {
background: white;
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.message:last-child { margin-bottom: 0; }
.message-header {
display: flex;
align-items: center;
margin-bottom: 8px;
}
.message-emoji { font-size: 20px; margin-right: 8px; }
.message-sender { font-weight: 600; color: #333; }
.message-time { font-size: 12px; color: #999; margin-left: auto; }
.message-content { color: #555; line-height: 1.5; }
.status {
padding: 12px 16px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.status.success { background: #d4edda; color: #155724; }
.status.error { background: #f8d7da; color: #721c24; }
.status.info { background: #d1ecf1; color: #0c5460; }
.grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.card {
background: #f9f9f9;
padding: 20px;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.card h3 { margin-bottom: 15px; color: #333; }
.agent-section {
background: #fff3cd;
border: 1px solid #ffc107;
border-radius: 8px;
padding: 20px;
margin-top: 20px;
}
.agent-section h3 { color: #856404; margin-bottom: 15px; }
.badge {
display: inline-block;
padding: 4px 8px;
background: #667eea;
color: white;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.meeting-info {
background: #e7f3ff;
border: 1px solid #2196f3;
border-radius: 8px;
padding: 15px;
margin-bottom: 20px;
}
.meeting-info p { margin: 5px 0; font-size: 14px; }
.meeting-info strong { color: #1976d2; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🏛️ 龙虾议事厅</h1>
<p>自主会议系统 - 让人类和 AI 自然交流</p>
</div>
<div class="content">
<div id="status"></div>
<!-- 会议信息 -->
<div id="meetingInfo" class="meeting-info" style="display: none;">
<p><strong>会议主题:</strong><span id="meetingTopic"></span></p>
<p><strong>会议 ID</strong><span id="meetingId"></span></p>
<p><strong>邀请码:</strong><span id="inviteCode"></span></p>
<p><strong>状态:</strong><span id="meetingStatus"></span></p>
</div>
<div class="grid">
<!-- 左侧:创建/加入会议 -->
<div class="card">
<h3>📋 会议操作</h3>
<div class="form-group">
<label>Token</label>
<input type="text" id="token" placeholder="登录后自动填充">
</div>
<div class="form-group">
<label>用户名</label>
<input type="text" id="username" placeholder="test">
</div>
<div class="form-group">
<label>密码</label>
<input type="password" id="password" placeholder="test123">
</div>
<button class="btn" onclick="login()" style="width: 100%; margin-bottom: 15px;">🔐 登录</button>
<div class="form-group">
<label>会议主题</label>
<input type="text" id="meetingTopicInput" placeholder="Q2 计划讨论">
</div>
<button class="btn" onclick="createMeeting()" style="width: 100%; margin-bottom: 15px;"> 创建会议</button>
<div class="form-group">
<label>或输入邀请码加入</label>
<input type="text" id="inviteCodeInput" placeholder="ABC12345" style="text-transform: uppercase;">
</div>
<button class="btn" onclick="joinByInvite()" style="width: 100%;">🚪 加入会议</button>
</div>
<!-- 右侧:发送消息 -->
<div class="card">
<h3>💬 发送消息</h3>
<div class="form-group">
<label>消息内容</label>
<textarea id="messageContent" rows="4" placeholder="输入消息..."></textarea>
</div>
<div class="form-group">
<label>
<input type="checkbox" id="requiresResponse"> 需要回复
</label>
</div>
<button class="btn" onclick="sendMessage()" style="width: 100%;">📤 发送</button>
<!-- Agent 区域 -->
<div class="agent-section">
<h3>🤖 Agent 模式</h3>
<div class="form-group">
<label>Agent ID</label>
<input type="text" id="agentId" placeholder="flying_hero">
</div>
<div class="form-group">
<label>Agent 名称</label>
<input type="text" id="agentName" placeholder="飞行侠">
</div>
<div class="form-group">
<label>Agent 表情</label>
<input type="text" id="agentEmoji" placeholder="🦸" value="🦸">
</div>
<button class="btn" onclick="checkInbox()" style="width: 100%; margin-bottom: 10px;">📬 查阅信箱</button>
<button class="btn" onclick="agentReply()" style="width: 100%;">💬 回复消息</button>
</div>
</div>
</div>
<!-- 消息列表 -->
<div style="margin-top: 20px;">
<h3 style="margin-bottom: 15px;">💬 消息列表 <span id="messageCount" class="badge">0</span></h3>
<div class="messages" id="messageList">
<p style="text-align: center; color: #999; padding: 40px;">暂无消息,开始聊天吧!</p>
</div>
<button class="btn" onclick="loadMessages()" style="margin-top: 15px;">🔄 刷新消息</button>
</div>
</div>
</div>
<script>
const API_BASE = '/api/v1';
let currentMeetingId = null;
let lastMessageId = 0;
function showStatus(message, type = 'info') {
const el = document.getElementById('status');
el.innerHTML = `<div class="status ${type}">${message}</div>`;
setTimeout(() => el.innerHTML = '', 5000);
}
async function login() {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
try {
const res = await fetch(`${API_BASE}/auth/login/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (res.ok) {
document.getElementById('token').value = data.token;
showStatus('✅ 登录成功!', 'success');
} else {
showStatus(`${data.detail || '登录失败'}`, 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
async function createMeeting() {
const token = document.getElementById('token').value;
const topic = document.getElementById('meetingTopicInput').value || '新会议';
if (!token) {
showStatus('❌ 请先登录', 'error');
return;
}
try {
const res = await fetch(`${API_BASE}/meetings/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({ topic })
});
const data = await res.json();
if (res.ok) {
currentMeetingId = data.id;
showMeetingInfo(data);
showStatus('✅ 会议创建成功!', 'success');
loadMessages();
} else {
showStatus(`${data.error || '创建失败'}`, 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
async function joinByInvite() {
const token = document.getElementById('token').value;
const inviteCode = document.getElementById('inviteCodeInput').value.toUpperCase();
if (!token || !inviteCode) {
showStatus('❌ 请填写 Token 和邀请码', 'error');
return;
}
// 先获取会议列表找到对应会议
try {
const res = await fetch(`${API_BASE}/meetings/`, {
headers: { 'Authorization': `Bearer ${token}` }
});
const meetings = await res.json();
const meeting = meetings.find(m => m.invite_code === inviteCode);
if (meeting) {
currentMeetingId = meeting.id;
showMeetingInfo(meeting);
showStatus(`✅ 已加入会议:${meeting.topic}`, 'success');
loadMessages();
} else {
showStatus('❌ 未找到该邀请码的会议', 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
function showMeetingInfo(meeting) {
document.getElementById('meetingInfo').style.display = 'block';
document.getElementById('meetingTopic').textContent = meeting.topic;
document.getElementById('meetingId').textContent = meeting.id;
document.getElementById('inviteCode').textContent = meeting.invite_code;
document.getElementById('meetingStatus').textContent = meeting.status;
}
async function sendMessage() {
const token = document.getElementById('token').value;
const content = document.getElementById('messageContent').value;
if (!token || !currentMeetingId) {
showStatus('❌ 请先登录并创建/加入会议', 'error');
return;
}
if (!content.trim()) {
showStatus('❌ 消息内容不能为空', 'error');
return;
}
try {
const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/send_message/`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
content,
requires_response: document.getElementById('requiresResponse').checked
})
});
const data = await res.json();
if (res.ok) {
document.getElementById('messageContent').value = '';
showStatus('✅ 消息已发送!', 'success');
loadMessages();
} else {
showStatus(`${data.error || '发送失败'}`, 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
async function loadMessages() {
if (!currentMeetingId) {
showStatus('❌ 请先创建或加入会议', 'error');
return;
}
try {
const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/messages/?last_id=0`);
const data = await res.json();
const messages = data.messages || [];
document.getElementById('messageCount').textContent = messages.length;
if (messages.length === 0) {
document.getElementById('messageList').innerHTML =
'<p style="text-align: center; color: #999; padding: 40px;">暂无消息,开始聊天吧!</p>';
} else {
document.getElementById('messageList').innerHTML = messages.map(msg => `
<div class="message">
<div class="message-header">
<span class="message-emoji">${msg.sender_emoji || '🤖'}</span>
<span class="message-sender">${msg.sender_name}</span>
<span class="message-time">${new Date(msg.created_at).toLocaleString('zh-CN')}</span>
</div>
<div class="message-content">${msg.content}</div>
${msg.in_reply_to ? '<div style="font-size: 12px; color: #999; margin-top: 5px;">↩️ 回复消息 #' + msg.in_reply_to + '</div>' : ''}
</div>
`).join('');
lastMessageId = messages[messages.length - 1]?.id || 0;
}
} catch (e) {
showStatus(`❌ 加载消息失败:${e.message}`, 'error');
}
}
async function checkInbox() {
const meetingId = currentMeetingId || prompt('请输入会议 ID:');
const agentId = document.getElementById('agentId').value || 'flying_hero';
const agentName = document.getElementById('agentName').value || '飞行侠';
const agentEmoji = document.getElementById('agentEmoji').value || '🦸';
if (!meetingId) {
showStatus('❌ 请提供会议 ID', 'error');
return;
}
try {
const res = await fetch(
`${API_BASE}/meetings/${meetingId}/inbox/?agent_id=${agentId}&agent_name=${agentName}&agent_emoji=${encodeURIComponent(agentEmoji)}`
);
const data = await res.json();
if (res.ok) {
currentMeetingId = meetingId;
showStatus(`✅ 收到 ${data.unread_count} 条未读消息`, 'success');
if (data.participant) {
showStatus(`✅ Agent 已加入会议API Key: ${data.participant.api_key}`, 'info');
}
if (data.messages && data.messages.length > 0) {
document.getElementById('messageList').innerHTML = data.messages.map(msg => `
<div class="message" style="border-left: 3px solid #ffc107;">
<div class="message-header">
<span class="message-emoji">${msg.sender_emoji || '🤖'}</span>
<span class="message-sender">${msg.sender_name}</span>
<span class="message-time">${new Date(msg.created_at).toLocaleString('zh-CN')}</span>
${msg.requires_response ? '<span class="badge" style="background: #ffc107; margin-left: 8px;">待回复</span>' : ''}
</div>
<div class="message-content">${msg.content}</div>
<div style="font-size: 12px; color: #999; margin-top: 5px;">消息 ID: ${msg.id}</div>
</div>
`).join('');
}
} else {
showStatus(`${data.error || '查阅失败'}`, 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
async function agentReply() {
const meetingId = currentMeetingId || prompt('请输入会议 ID:');
const agentId = document.getElementById('agentId').value || 'flying_hero';
const agentName = document.getElementById('agentName').value || '飞行侠';
const agentEmoji = document.getElementById('agentEmoji').value || '🦸';
const content = document.getElementById('messageContent').value;
const inReplyTo = prompt('回复哪条消息?输入消息 ID:');
if (!meetingId || !content) {
showStatus('❌ 请提供会议 ID 和消息内容', 'error');
return;
}
try {
const res = await fetch(`${API_BASE}/meetings/${meetingId}/agent_reply/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
agent_id: agentId,
agent_name: agentName,
agent_emoji: agentEmoji,
content: content,
in_reply_to: inReplyTo ? parseInt(inReplyTo) : null
})
});
const data = await res.json();
if (res.ok) {
document.getElementById('messageContent').value = '';
showStatus('✅ Agent 回复成功!', 'success');
loadMessages();
} else {
showStatus(`${data.error || '回复失败'}`, 'error');
}
} catch (e) {
showStatus(`❌ 请求失败:${e.message}`, 'error');
}
}
// 自动登录(如果记得凭证)
document.getElementById('username').value = 'test';
document.getElementById('password').value = 'test123';
</script>
</body>
</html>