2026-04-04 11:33:39 +08:00
|
|
|
|
<!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>
|
2026-04-04 11:43:41 +08:00
|
|
|
|
<button class="btn" onclick="agentReply()" style="width: 100%; margin-bottom: 10px;">💬 回复消息</button>
|
|
|
|
|
|
<button class="btn" onclick="mentionAgent()" style="width: 100%; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">📍 @Agent</button>
|
2026-04-04 11:33:39 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-04 11:43:41 +08:00
|
|
|
|
<!-- 座位图 -->
|
|
|
|
|
|
<div style="margin-top: 20px;">
|
|
|
|
|
|
<h3 style="margin-bottom: 15px;">🪑 座位图 <span id="participantCount" class="badge">0</span></h3>
|
|
|
|
|
|
<div class="messages" id="seatMap" style="min-height: 100px;">
|
|
|
|
|
|
<p style="text-align: center; color: #999; padding: 20px;">暂无参会者</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-04 11:33:39 +08:00
|
|
|
|
<!-- 消息列表 -->
|
|
|
|
|
|
<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>
|
2026-04-04 11:43:41 +08:00
|
|
|
|
<div style="margin-top: 15px; display: flex; gap: 10px;">
|
|
|
|
|
|
<button class="btn" onclick="loadMessages()">🔄 刷新</button>
|
|
|
|
|
|
<button class="btn" onclick="loadParticipants()" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">👥 刷新座位</button>
|
|
|
|
|
|
<button class="btn" onclick="generateMinutes()" style="background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);">📋 生成纪要</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 会议纪要显示区 -->
|
|
|
|
|
|
<div id="minutesDisplay" style="margin-top: 20px; display: none;">
|
|
|
|
|
|
<h3 style="margin-bottom: 15px;">📋 会议纪要</h3>
|
|
|
|
|
|
<div class="messages" id="minutesContent" style="white-space: pre-wrap; font-family: monospace;"></div>
|
|
|
|
|
|
<button class="btn" onclick="document.getElementById('minutesDisplay').style.display='none'" style="margin-top: 15px;">❌ 关闭</button>
|
2026-04-04 11:33:39 +08:00
|
|
|
|
</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');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 11:43:41 +08:00
|
|
|
|
async function loadParticipants() {
|
|
|
|
|
|
if (!currentMeetingId) {
|
|
|
|
|
|
showStatus('❌ 请先创建或加入会议', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/participants/`);
|
|
|
|
|
|
const participants = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById('participantCount').textContent = participants.length;
|
|
|
|
|
|
|
|
|
|
|
|
if (participants.length === 0) {
|
|
|
|
|
|
document.getElementById('seatMap').innerHTML =
|
|
|
|
|
|
'<p style="text-align: center; color: #999; padding: 20px;">暂无参会者</p>';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 圆桌座位布局
|
|
|
|
|
|
document.getElementById('seatMap').innerHTML = `
|
|
|
|
|
|
<div style="display: flex; flex-wrap: wrap; gap: 15px; justify-content: center; padding: 20px;">
|
|
|
|
|
|
${participants.map(p => `
|
|
|
|
|
|
<div style="
|
|
|
|
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 15px 20px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
width: 100px;
|
|
|
|
|
|
height: 100px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
box-shadow: 0 4px 15px rgba(0,0,0,0.2);
|
|
|
|
|
|
">
|
|
|
|
|
|
<div style="font-size: 24px; margin-bottom: 5px;">${p.agent_emoji || '👤'}</div>
|
|
|
|
|
|
<div style="font-weight: 600;">${p.nickname || '参会者'}</div>
|
|
|
|
|
|
${p.is_host ? '<div style="font-size: 10px; opacity: 0.8;">👑 主持</div>' : ''}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`).join('')}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showStatus(`❌ 加载座位失败:${e.message}`, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function generateMinutes() {
|
|
|
|
|
|
if (!currentMeetingId) {
|
|
|
|
|
|
showStatus('❌ 请先创建或加入会议', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`${API_BASE}/meetings/${currentMeetingId}/minutes/?output=markdown`);
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (res.ok && data.markdown) {
|
|
|
|
|
|
document.getElementById('minutesContent').textContent = data.markdown;
|
|
|
|
|
|
document.getElementById('minutesDisplay').style.display = 'block';
|
|
|
|
|
|
showStatus('✅ 会议纪要已生成!', 'success');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showStatus(`❌ 生成失败:${data.error || '未知错误'}`, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showStatus(`❌ 请求失败:${e.message}`, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function mentionAgent() {
|
|
|
|
|
|
const meetingId = currentMeetingId || prompt('请输入会议 ID:');
|
|
|
|
|
|
const targetAgentId = prompt('@哪个 Agent?输入 agent_id:');
|
|
|
|
|
|
const content = document.getElementById('messageContent').value;
|
|
|
|
|
|
const senderAgentId = document.getElementById('agentId').value || 'human';
|
|
|
|
|
|
const senderName = document.getElementById('agentName').value || '用户';
|
|
|
|
|
|
const senderEmoji = document.getElementById('agentEmoji').value || '👤';
|
|
|
|
|
|
|
|
|
|
|
|
if (!meetingId || !targetAgentId || !content) {
|
|
|
|
|
|
showStatus('❌ 请填写完整信息', 'error');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await fetch(`${API_BASE}/meetings/${meetingId}/mention_agent/`, {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
target_agent_id: targetAgentId,
|
|
|
|
|
|
content: content,
|
|
|
|
|
|
sender_agent_id: senderAgentId,
|
|
|
|
|
|
sender_name: senderName,
|
|
|
|
|
|
sender_emoji: senderEmoji
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
|
document.getElementById('messageContent').value = '';
|
|
|
|
|
|
showStatus(`✅ 已 @${targetAgentId}`, 'success');
|
|
|
|
|
|
loadMessages();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
showStatus(`❌ ${data.error || '发送失败'}`, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
showStatus(`❌ 请求失败:${e.message}`, 'error');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-04 11:33:39 +08:00
|
|
|
|
// 自动登录(如果记得凭证)
|
|
|
|
|
|
document.getElementById('username').value = 'test';
|
|
|
|
|
|
document.getElementById('password').value = 'test123';
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|