279 lines
14 KiB
HTML
279 lines
14 KiB
HTML
{% 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 %}
|