feat: Web UI 实现 - 暗黑主题 + 频道配置 + 连接测试 + 会话监控

This commit is contained in:
2026-03-14 22:26:00 +08:00
parent 0003e5f69f
commit 657a3440b9
15 changed files with 1816 additions and 0 deletions

View File

@@ -0,0 +1,278 @@
{% extends "base.html" %}
{% block title %}会话监控 - 智队中枢{% endblock %}
{% block content %}
<div class="space-y-6">
{# 页面标题 #}
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold dark:text-white">会话监控</h1>
<button onclick="loadSessions()" class="btn btn-secondary flex items-center gap-2">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
</svg>
刷新
</button>
</div>
{# 筛选 #}
<div class="card">
<div class="card-body">
<div class="flex flex-wrap items-end gap-4">
<div class="flex-1 min-w-[150px]">
<label class="block text-sm font-medium mb-1 dark:text-gray-300">Agent</label>
<select id="filter-agent" class="select" onchange="loadSessions()">
<option value="">全部</option>
{% for agent in agents %}
<option value="{{ agent.id }}">{{ agent.name }}</option>
{% endfor %}
</select>
</div>
<div class="flex-1 min-w-[150px]">
<label class="block text-sm font-medium mb-1 dark:text-gray-300">状态</label>
<select id="filter-status" class="select" onchange="loadSessions()">
<option value="">全部</option>
<option value="active">活跃</option>
<option value="paused">暂停</option>
<option value="closed">已关闭</option>
</select>
</div>
</div>
</div>
</div>
{# 会话列表 #}
<div class="card">
<div class="card-body p-0">
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>用户</th>
<th>Agent</th>
<th>消息数</th>
<th>状态</th>
<th>创建时间</th>
<th>最后活跃</th>
<th>操作</th>
</tr>
</thead>
<tbody id="sessions-body">
{% for session in sessions %}
<tr>
<td class="font-mono text-xs">{{ session.id[:8] }}...</td>
<td class="font-medium">{{ session.user.username }}</td>
<td>{{ session.primary_agent.name if session.primary_agent else '-' }}</td>
<td>{{ session.message_count }}</td>
<td>
{% if session.status == 'active' %}
<span class="badge badge-success">活跃</span>
{% elif session.status == 'paused' %}
<span class="badge badge-warning">暂停</span>
{% else %}
<span class="badge badge-danger">关闭</span>
{% endif %}
</td>
<td class="text-gray-500 dark:text-gray-400 text-sm">{{ session.created_at.strftime('%m-%d %H:%M') if session.created_at else '-' }}</td>
<td class="text-gray-500 dark:text-gray-400 text-sm">{{ session.last_active_at.strftime('%m-%d %H:%M') if session.last_active_at else '-' }}</td>
<td>
<button onclick="viewSession('{{ session.id }}')"
class="p-2 text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg transition-colors"
title="查看详情">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400">
暂无会话数据
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# 会话详情模态框 #}
<div id="session-modal" class="fixed inset-0 bg-black/50 hidden items-center justify-center z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-3xl mx-4 max-h-[90vh] overflow-hidden flex flex-col">
<div class="card-header flex items-center justify-between">
<h3 class="text-lg font-semibold dark:text-white">会话详情</h3>
<button onclick="hideModal('session-modal')" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="flex-1 overflow-auto">
{# 会话信息 #}
<div class="p-4 border-b border-gray-200 dark:border-gray-700">
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<div class="text-xs text-gray-500 dark:text-gray-400">用户</div>
<div id="session-user" class="font-medium">-</div>
</div>
<div>
<div class="text-xs text-gray-500 dark:text-gray-400">Agent</div>
<div id="session-agent" class="font-medium">-</div>
</div>
<div>
<div class="text-xs text-gray-500 dark:text-gray-400">状态</div>
<div id="session-status">-</div>
</div>
<div>
<div class="text-xs text-gray-500 dark:text-gray-400">创建时间</div>
<div id="session-created" class="text-sm">-</div>
</div>
</div>
</div>
{# 消息记录 #}
<div class="p-4">
<h4 class="font-medium mb-3 dark:text-white">消息记录</h4>
<div id="session-messages" class="space-y-3 max-h-96 overflow-auto">
{# 动态加载 #}
</div>
</div>
</div>
<div class="p-4 border-t border-gray-200 dark:border-gray-700 flex justify-between">
<button onclick="exportSession()" class="btn btn-secondary">导出记录</button>
<button onclick="closeSession()" class="btn btn-danger">关闭会话</button>
</div>
</div>
</div>
<script>
let currentSessionId = null;
function loadSessions() {
const agentId = document.getElementById('filter-agent').value;
const status = document.getElementById('filter-status').value;
const params = new URLSearchParams();
if (agentId) params.append('agent_id', agentId);
if (status) params.append('status', status);
fetch(`/api/web/sessions?${params}`)
.then(res => res.json())
.then(data => {
const tbody = document.getElementById('sessions-body');
if (data.sessions.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-center py-8 text-gray-500 dark:text-gray-400">暂无数据</td></tr>';
return;
}
tbody.innerHTML = data.sessions.map(s => `
<tr>
<td class="font-mono text-xs">${s.id.substring(0, 8)}...</td>
<td class="font-medium">${s.user?.username || '-'}</td>
<td>${s.primary_agent?.name || '-'}</td>
<td>${s.message_count}</td>
<td>
${s.status === 'active' ? '<span class="badge badge-success">活跃</span>' :
s.status === 'paused' ? '<span class="badge badge-warning">暂停</span>' :
'<span class="badge badge-danger">关闭</span>'}
</td>
<td class="text-gray-500 dark:text-gray-400 text-sm">${s.created_at ? new Date(s.created_at).toLocaleString() : '-'}</td>
<td class="text-gray-500 dark:text-gray-400 text-sm">${s.last_active_at ? new Date(s.last_active_at).toLocaleString() : '-'}</td>
<td>
<button onclick="viewSession('${s.id}')" class="p-2 text-primary-600 hover:bg-primary-50 dark:hover:bg-primary-900/30 rounded-lg">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</td>
</tr>
`).join('');
});
}
function viewSession(id) {
currentSessionId = id;
fetch(`/api/web/sessions/${id}`)
.then(res => res.json())
.then(data => {
const session = data.session;
document.getElementById('session-user').textContent = session.user?.username || '-';
document.getElementById('session-agent').textContent = session.primary_agent?.name || '-';
document.getElementById('session-status').innerHTML = session.status === 'active' ?
'<span class="badge badge-success">活跃</span>' :
session.status === 'paused' ?
'<span class="badge badge-warning">暂停</span>' :
'<span class="badge badge-danger">关闭</span>';
document.getElementById('session-created').textContent = session.created_at ?
new Date(session.created_at).toLocaleString() : '-';
// 加载消息
loadSessionMessages(id);
showModal('session-modal');
});
}
function loadSessionMessages(sessionId) {
fetch(`/api/web/sessions/${sessionId}/messages`)
.then(res => res.json())
.then(data => {
const container = document.getElementById('session-messages');
if (data.messages.length === 0) {
container.innerHTML = '<div class="text-center py-8 text-gray-500 dark:text-gray-400">暂无消息</div>';
return;
}
container.innerHTML = data.messages.map(m => `
<div class="flex ${m.sender_type === 'user' ? 'justify-start' : 'justify-end'}">
<div class="max-w-[70%] ${m.sender_type === 'user' ? 'bg-gray-100 dark:bg-gray-700' : 'bg-primary-100 dark:bg-primary-900/30'} rounded-lg px-4 py-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
${m.sender_type === 'user' ? (m.sender_id || '用户') : (m.sender_id || 'Agent')}
</div>
<div class="text-sm">${m.content}</div>
<div class="text-xs text-gray-400 mt-1">${new Date(m.created_at).toLocaleTimeString()}</div>
</div>
</div>
`).join('');
});
}
function closeSession() {
if (!currentSessionId) return;
if (!confirm('确定要关闭此会话吗?')) return;
fetch(`/api/web/sessions/${currentSessionId}/close`, { method: 'PUT' })
.then(res => {
if (res.ok) {
showToast('会话已关闭', 'success');
hideModal('session-modal');
loadSessions();
} else {
showToast('关闭失败', 'error');
}
});
}
function exportSession() {
if (!currentSessionId) return;
window.open(`/api/web/sessions/${currentSessionId}/export`, '_blank');
}
function showModal(id) {
document.getElementById(id).classList.remove('hidden');
document.getElementById(id).classList.add('flex');
}
function hideModal(id) {
document.getElementById(id).classList.add('hidden');
document.getElementById(id).classList.remove('flex');
}
</script>
{% endblock %}