🎨 飞行侠完善 P1 功能:座位图 + @Agent + 会议纪要

新增功能:
- 座位可视化 - 圆形头像展示参会者
- @Agent 功能 - 定向消息给特定 Agent
- 会议纪要生成 - Web 界面一键生成
- 参会者列表 API

文件变更:
- meetings/views.py: mention_agent() 新接口
- templates/meeting_room.html:
  - 座位图 UI(圆形头像)
  - 生成纪要按钮
  - @Agent 按钮
- test_mention.py: @Agent 测试脚本

测试结果:
 完整功能测试 (7 项)
 会议纪要测试 (JSON + Markdown)
 @Agent 功能测试
This commit is contained in:
2026-04-04 11:43:41 +08:00
parent 53c3ac487a
commit d403583fb8
3 changed files with 298 additions and 2 deletions

View File

@@ -217,18 +217,38 @@
<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>
<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>
</div>
</div>
</div>
<!-- 座位图 -->
<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>
<!-- 消息列表 -->
<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 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>
</div>
</div>
</div>
@@ -499,6 +519,115 @@
}
}
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');
}
}
// 自动登录(如果记得凭证)
document.getElementById('username').value = 'test';
document.getElementById('password').value = 'test123';