Files
ai-chat-system/templates/admin/index.html

983 lines
34 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>AI对话系统 - 后台管理</title>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: #f5f5f5;
min-height: 100vh;
}
.header {
background: #202123;
color: #fff;
padding: 16px 24px;
display: flex;
align-items: center;
justify-content: space-between;
}
.header h1 {
font-size: 20px;
}
.header-links {
display: flex;
gap: 16px;
}
.header-links a {
color: #fff;
text-decoration: none;
font-size: 14px;
}
.header-links a:hover {
text-decoration: underline;
}
.container {
max-width: 1200px;
margin: 24px auto;
padding: 0 24px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.stat-card h3 {
color: #666;
font-size: 14px;
margin-bottom: 8px;
}
.stat-card .value {
font-size: 32px;
font-weight: 600;
color: #333;
}
.tabs {
display: flex;
gap: 8px;
margin-bottom: 24px;
}
.tab-btn {
padding: 12px 24px;
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.tab-btn:hover {
background: #f0f0f0;
}
.tab-btn.active {
background: #10a37f;
color: #fff;
border-color: #10a37f;
}
.tab-content {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
}
.tab-panel {
display: none;
}
.tab-panel.active {
display: block;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
th {
font-weight: 600;
color: #666;
font-size: 14px;
}
td {
color: #333;
}
tr:hover {
background: #f9f9f9;
}
/* AI配置专用样式 */
.ai-config-section {
background: #f8f9fa;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.ai-config-section h3 {
font-size: 18px;
color: #333;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.ai-config-section h3 i {
color: #10a37f;
}
.ai-status {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: #fff;
border-radius: 8px;
margin-bottom: 16px;
}
.ai-status.ok {
border: 1px solid #10a37f;
}
.ai-status.error {
border: 1px solid #dc3545;
}
.ai-status-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.ai-status-dot.ok {
background: #10a37f;
}
.ai-status-dot.error {
background: #dc3545;
}
.ai-status-text {
flex: 1;
}
.ai-config-form {
display: grid;
gap: 16px;
}
.config-row {
display: grid;
grid-template-columns: 150px 1fr;
gap: 16px;
align-items: center;
}
.config-row label {
font-weight: 500;
color: #555;
}
.config-row input, .config-row select {
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
width: 100%;
}
.config-row input:focus, .config-row select:focus {
outline: none;
border-color: #10a37f;
}
/* 模型输入组合框 */
.model-input-wrapper {
display: flex;
position: relative;
}
.model-input-wrapper input {
flex: 1;
padding-right: 40px;
}
.btn-model-dropdown {
position: absolute;
right: 4px;
top: 50%;
transform: translateY(-50%);
width: 32px;
height: 32px;
background: #f0f0f0;
border: none;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #666;
transition: all 0.2s;
}
.btn-model-dropdown:hover {
background: #e0e0e0;
color: #333;
}
/* datalist样式提示 */
.model-input-wrapper input::-webkit-calendar-picker-indicator {
opacity: 0;
width: 32px;
height: 32px;
cursor: pointer;
}
.config-actions {
display: flex;
gap: 12px;
margin-top: 16px;
}
.btn {
padding: 12px 24px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
border: none;
transition: all 0.2s;
}
.btn-primary {
background: #10a37f;
color: #fff;
}
.btn-primary:hover {
background: #0d8c6d;
}
.btn-secondary {
background: #fff;
color: #333;
border: 1px solid #ddd;
}
.btn-secondary:hover {
background: #f0f0f0;
}
.btn-test {
background: #007bff;
color: #fff;
}
.btn-test:hover {
background: #0056b3;
}
.test-result {
padding: 16px;
border-radius: 8px;
margin-top: 16px;
}
.test-result.success {
background: #d4edda;
border: 1px solid #10a37f;
color: #155724;
}
.test-result.error {
background: #f8d7da;
border: 1px solid #dc3545;
color: #721c24;
}
/* 原有配置表单样式 */
.config-form {
display: flex;
gap: 16px;
margin-bottom: 24px;
}
.config-form input, .config-form textarea {
padding: 12px 16px;
border: 1px solid #ddd;
border-radius: 8px;
font-size: 14px;
}
.config-form input {
width: 200px;
}
.config-form textarea {
width: 300px;
resize: vertical;
min-height: 60px;
}
.config-list {
margin-top: 24px;
}
.config-item {
display: flex;
align-items: center;
padding: 16px;
background: #f9f9f9;
border-radius: 8px;
margin-bottom: 8px;
}
.config-item .key {
font-weight: 600;
width: 200px;
color: #333;
}
.config-item .value {
flex: 1;
color: #666;
overflow: hidden;
text-overflow: ellipsis;
}
.config-item .desc {
color: #999;
font-size: 12px;
width: 200px;
}
.badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.badge.web {
background: #e3f2fd;
color: #1976d2;
}
.badge.matrix {
background: #f3e5f5;
color: #7b1fa2;
}
.badge.active {
background: #e8f5e9;
color: #2e7d32;
}
.badge.inactive {
background: #ffebee;
color: #c62828;
}
.message-preview {
max-width: 300px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.loading {
opacity: 0.6;
pointer-events: none;
}
</style>
</head>
<body>
<div class="header">
<h1>🤖 AI对话系统 - 后台管理</h1>
<div class="header-links">
<a href="/">← 返回聊天</a>
</div>
</div>
<div class="container">
<div class="stats-grid" id="statsGrid">
<div class="stat-card">
<h3>总用户数</h3>
<div class="value" id="totalUsers">-</div>
</div>
<div class="stat-card">
<h3>总对话数</h3>
<div class="value" id="totalConversations">-</div>
</div>
<div class="stat-card">
<h3>总消息数</h3>
<div class="value" id="totalMessages">-</div>
</div>
</div>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab('ai')">🧠 AI配置</button>
<button class="tab-btn" onclick="switchTab('users')">👥 用户管理</button>
<button class="tab-btn" onclick="switchTab('conversations')">💬 对话记录</button>
<button class="tab-btn" onclick="switchTab('config')">⚙️ 其他配置</button>
</div>
<div class="tab-content">
<!-- AI配置 -->
<div class="tab-panel active" id="aiPanel">
<div class="ai-config-section">
<h3><i class="ri-robot-line"></i> 大模型配置</h3>
<div class="ai-status" id="aiStatus">
<div class="ai-status-dot" id="aiStatusDot"></div>
<div class="ai-status-text" id="aiStatusText">检测中...</div>
</div>
<div class="ai-config-form">
<div class="config-row">
<label>API地址</label>
<input type="text" id="aiApiBase" placeholder="http://192.168.2.17:19007/v1">
</div>
<div class="config-row">
<label>API密钥</label>
<input type="text" id="aiApiKey" placeholder="xxxx">
</div>
<div class="config-row">
<label>模型</label>
<div class="model-input-wrapper">
<input type="text" id="aiModel" list="modelList" placeholder="选择或输入模型名称">
<datalist id="modelList">
<option value="auto">auto (自动选择)</option>
<option value="qwen3.5-4b">qwen3.5-4b</option>
<option value="dsv32">dsv32</option>
<option value="glm-4">glm-4</option>
<option value="gpt-4o">gpt-4o</option>
<option value="claude-3-opus">claude-3-opus</option>
</datalist>
<button class="btn-model-dropdown" onclick="toggleModelDropdown()" title="显示预设模型">
<i class="ri-arrow-down-s-line"></i>
</button>
</div>
</div>
</div>
<div class="config-actions">
<button class="btn btn-primary" onclick="saveAIConfig()">
<i class="ri-save-line"></i> 保存配置
</button>
<button class="btn btn-test" onclick="testAIConnection()">
<i class="ri-link"></i> 测试连接
</button>
<button class="btn btn-secondary" onclick="refreshModels()">
<i class="ri-refresh-line"></i> 刷新模型列表
</button>
</div>
<div class="test-result" id="testResult" style="display: none;"></div>
</div>
<div class="ai-config-section">
<h3><i class="ri-information-line"></i> 当前状态</h3>
<table>
<tr>
<th>配置项</th>
<th>当前值</th>
<th>状态</th>
</tr>
<tr>
<td>API地址</td>
<td id="currentApiBase">-</td>
<td><span class="badge" id="apiBaseStatus">-</span></td>
</tr>
<tr>
<td>模型</td>
<td id="currentModel">-</td>
<td><span class="badge" id="modelStatus">-</span></td>
</tr>
<tr>
<td>连接状态</td>
<td id="connectionStatus">-</td>
<td><span class="badge" id="connectionBadge">检测中</span></td>
</tr>
</table>
</div>
</div>
<!-- 用户管理 -->
<div class="tab-panel" id="usersPanel">
<table>
<thead>
<tr>
<th>用户ID</th>
<th>显示名称</th>
<th>类型</th>
<th>创建时间</th>
<th>最后活跃</th>
<th>状态</th>
</tr>
</thead>
<tbody id="usersTableBody">
<tr><td colspan="6">加载中...</td></tr>
</tbody>
</table>
</div>
<!-- 对话记录 -->
<div class="tab-panel" id="conversationsPanel">
<table>
<thead>
<tr>
<th>会话ID</th>
<th>用户</th>
<th>标题</th>
<th>消息数</th>
<th>创建时间</th>
<th>最后更新</th>
<th>状态</th>
</tr>
</thead>
<tbody id="conversationsTableBody">
<tr><td colspan="7">加载中...</td></tr>
</tbody>
</table>
</div>
<!-- 其他配置 -->
<div class="tab-panel" id="configPanel">
<div class="config-form">
<input type="text" id="configKey" placeholder="配置键名">
<textarea id="configValue" placeholder="配置值"></textarea>
<input type="text" id="configDesc" placeholder="描述(可选)">
<button class="btn btn-primary" onclick="saveConfig()">保存</button>
</div>
<div class="config-list" id="configList">
<!-- 配置列表 -->
</div>
</div>
</div>
</div>
<script>
// 初始化
document.addEventListener('DOMContentLoaded', () => {
loadStats();
loadAIConfig();
loadUsers();
loadConversations();
loadConfig();
});
// 切换标签页
function switchTab(tabName) {
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.remove('active'));
document.querySelector(`.tab-btn[onclick="switchTab('${tabName}')"]`).classList.add('active');
document.getElementById(`${tabName}Panel`).classList.add('active');
}
// 切换模型下拉显示
function toggleModelDropdown() {
const input = document.getElementById('aiModel');
input.focus();
// 触发datalist显示
if (input.showPicker) {
input.showPicker();
} else {
// 兼容性处理:模拟点击
input.click();
}
}
// 加载AI配置
async function loadAIConfig() {
try {
const response = await fetch('/api/admin/ai-config');
const data = await response.json();
document.getElementById('aiApiBase').value = data.api_base || '';
document.getElementById('aiApiKey').value = data.api_key || '';
document.getElementById('aiModel').value = data.model || 'auto';
// 更新当前状态显示
document.getElementById('currentApiBase').textContent = data.api_base || '-';
document.getElementById('currentModel').textContent = data.model || '-';
// 更新状态指示
if (data.use_mock) {
document.getElementById('aiStatusDot').className = 'ai-status-dot error';
document.getElementById('aiStatusText').textContent = '当前使用Mock模式未连接真实API';
document.getElementById('aiStatus').className = 'ai-status error';
document.getElementById('connectionStatus').textContent = 'Mock模式';
document.getElementById('connectionBadge').className = 'badge inactive';
document.getElementById('connectionBadge').textContent = '未连接';
} else {
document.getElementById('aiStatusDot').className = 'ai-status-dot ok';
document.getElementById('aiStatusText').textContent = '已配置真实API';
document.getElementById('aiStatus').className = 'ai-status ok';
document.getElementById('connectionStatus').textContent = '已配置';
document.getElementById('connectionBadge').className = 'badge active';
document.getElementById('connectionBadge').textContent = '待测试';
}
document.getElementById('apiBaseStatus').className = 'badge active';
document.getElementById('apiBaseStatus').textContent = '已配置';
document.getElementById('modelStatus').className = 'badge active';
document.getElementById('modelStatus').textContent = data.model || 'auto';
} catch (error) {
console.error('加载AI配置失败:', error);
}
}
// 保存AI配置
async function saveAIConfig() {
const apiBase = document.getElementById('aiApiBase').value.trim();
const apiKey = document.getElementById('aiApiKey').value.trim();
const model = document.getElementById('aiModel').value.trim();
if (!apiBase) {
alert('请填写API地址');
return;
}
try {
const btn = event.target;
btn.classList.add('loading');
btn.textContent = '保存中...';
const response = await fetch('/api/admin/ai-config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({api_base: apiBase, api_key: apiKey, model: model})
});
const data = await response.json();
btn.classList.remove('loading');
btn.innerHTML = '<i class="ri-save-line"></i> 保存配置';
if (data.success) {
// 显示成功提示
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.className = 'test-result success';
resultDiv.innerHTML = `<i class="ri-check-line"></i> ${data.message}`;
// 重新加载配置
loadAIConfig();
// 3秒后隐藏提示
setTimeout(() => resultDiv.style.display = 'none', 3000);
} else {
alert('保存失败: ' + (data.message || '未知错误'));
}
} catch (error) {
console.error('保存AI配置失败:', error);
alert('保存失败: ' + error.message);
event.target.classList.remove('loading');
event.target.innerHTML = '<i class="ri-save-line"></i> 保存配置';
}
}
// 测试AI连接
async function testAIConnection() {
try {
const btn = event.target;
btn.classList.add('loading');
btn.innerHTML = '<i class="ri-loader-line"></i> 测试中...';
const response = await fetch('/api/admin/test-ai', {
method: 'POST'
});
const data = await response.json();
btn.classList.remove('loading');
btn.innerHTML = '<i class="ri-link"></i> 测试连接';
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
if (data.success) {
resultDiv.className = 'test-result success';
resultDiv.innerHTML = `<i class="ri-check-line"></i> <strong>连接成功!</strong><br>模型: ${data.model}<br>响应: ${data.message}`;
// 更新连接状态
document.getElementById('aiStatusDot').className = 'ai-status-dot ok';
document.getElementById('aiStatusText').textContent = '连接正常';
document.getElementById('aiStatus').className = 'ai-status ok';
document.getElementById('connectionStatus').textContent = '正常';
document.getElementById('connectionBadge').className = 'badge active';
document.getElementById('connectionBadge').textContent = '已连接';
} else {
resultDiv.className = 'test-result error';
resultDiv.innerHTML = `<i class="ri-close-line"></i> <strong>连接失败</strong><br>${data.message}`;
// 更新连接状态
document.getElementById('aiStatusDot').className = 'ai-status-dot error';
document.getElementById('aiStatusText').textContent = '连接失败';
document.getElementById('aiStatus').className = 'ai-status error';
document.getElementById('connectionStatus').textContent = '失败';
document.getElementById('connectionBadge').className = 'badge inactive';
document.getElementById('connectionBadge').textContent = '错误';
}
} catch (error) {
console.error('测试连接失败:', error);
event.target.classList.remove('loading');
event.target.innerHTML = '<i class="ri-link"></i> 测试连接';
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.className = 'test-result error';
resultDiv.innerHTML = `<i class="ri-close-line"></i> 测试失败: ${error.message}`;
}
}
// 刷新模型列表
async function refreshModels() {
try {
const btn = event.target;
btn.classList.add('loading');
btn.innerHTML = '<i class="ri-loader-line"></i> 刷新中...';
const response = await fetch('/api/admin/models');
const data = await response.json();
btn.classList.remove('loading');
btn.innerHTML = '<i class="ri-refresh-line"></i> 刷新模型列表';
// 更新datalist
const datalist = document.getElementById('modelList');
datalist.innerHTML = '';
for (const model of data.models) {
const option = document.createElement('option');
option.value = model.id;
option.textContent = model.name;
datalist.appendChild(option);
}
// 显示提示
const resultDiv = document.getElementById('testResult');
resultDiv.style.display = 'block';
resultDiv.className = 'test-result success';
resultDiv.innerHTML = `<i class="ri-check-line"></i> 获取到 ${data.models.length} 个模型,可在输入框中选择`;
if (!data.success) {
resultDiv.className = 'test-result error';
resultDiv.innerHTML = `<i class="ri-warning-line"></i> ${data.message || '使用默认模型列表'}`;
}
setTimeout(() => resultDiv.style.display = 'none', 3000);
} catch (error) {
console.error('刷新模型列表失败:', error);
event.target.classList.remove('loading');
event.target.innerHTML = '<i class="ri-refresh-line"></i> 刷新模型列表';
}
}
// 加载统计数据
async function loadStats() {
try {
const response = await fetch('/api/admin/stats');
const data = await response.json();
document.getElementById('totalUsers').textContent = data.total_users;
document.getElementById('totalConversations').textContent = data.total_conversations;
document.getElementById('totalMessages').textContent = data.total_messages;
} catch (error) {
console.error('加载统计失败:', error);
}
}
// 加载用户列表
async function loadUsers() {
try {
const response = await fetch('/api/admin/users');
const data = await response.json();
const tbody = document.getElementById('usersTableBody');
if (data.users.length === 0) {
tbody.innerHTML = '<tr><td colspan="6">暂无用户</td></tr>';
return;
}
tbody.innerHTML = data.users.map(user => `
<tr>
<td>${user.user_id}</td>
<td>${user.display_name || '-'}</td>
<td><span class="badge ${user.user_type}">${user.user_type}</span></td>
<td>${formatDate(user.created_at)}</td>
<td>${formatDate(user.last_active_at)}</td>
<td><span class="badge ${user.is_active ? 'active' : 'inactive'}">${user.is_active ? '活跃' : '禁用'}</span></td>
</tr>
`).join('');
} catch (error) {
console.error('加载用户失败:', error);
}
}
// 加载对话列表
async function loadConversations() {
try {
const response = await fetch('/api/admin/conversations');
const data = await response.json();
const tbody = document.getElementById('conversationsTableBody');
if (data.conversations.length === 0) {
tbody.innerHTML = '<tr><td colspan="7">暂无对话</td></tr>';
return;
}
tbody.innerHTML = data.conversations.map(conv => `
<tr>
<td>${conv.conversation_id}</td>
<td>${conv.user_id || '-'}</td>
<td class="message-preview">${conv.title || '新对话'}</td>
<td>${conv.message_count}</td>
<td>${formatDate(conv.created_at)}</td>
<td>${formatDate(conv.updated_at)}</td>
<td><span class="badge ${conv.is_active ? 'active' : 'inactive'}">${conv.is_active ? '活跃' : '已删除'}</span></td>
</tr>
`).join('');
} catch (error) {
console.error('加载对话失败:', error);
}
}
// 加载其他配置
async function loadConfig() {
try {
const response = await fetch('/api/admin/config');
const data = await response.json();
const container = document.getElementById('configList');
// 过滤掉AI配置在AI配置面板单独显示
const otherConfigs = data.configs.filter(c => !c.key.startsWith('ai_'));
if (otherConfigs.length === 0) {
container.innerHTML = '<p>暂无其他配置</p>';
return;
}
container.innerHTML = otherConfigs.map(config => `
<div class="config-item">
<div class="key">${config.key}</div>
<div class="value">${config.value}</div>
<div class="desc">${config.description || ''}</div>
</div>
`).join('');
} catch (error) {
console.error('加载配置失败:', error);
}
}
// 保存其他配置
async function saveConfig() {
const key = document.getElementById('configKey').value.trim();
const value = document.getElementById('configValue').value.trim();
const desc = document.getElementById('configDesc').value.trim();
if (!key || !value) {
alert('请填写配置键名和值');
return;
}
try {
const response = await fetch('/api/admin/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({key, value, description: desc})
});
if (response.ok) {
alert('配置已保存');
document.getElementById('configKey').value = '';
document.getElementById('configValue').value = '';
document.getElementById('configDesc').value = '';
loadConfig();
} else {
alert('保存失败');
}
} catch (error) {
console.error('保存配置失败:', error);
alert('保存失败');
}
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
// 定时刷新
setInterval(() => {
loadStats();
if (document.getElementById('aiPanel').classList.contains('active')) {
loadAIConfig();
}
if (document.getElementById('usersPanel').classList.contains('active')) {
loadUsers();
}
if (document.getElementById('conversationsPanel').classList.contains('active')) {
loadConversations();
}
}, 30000);
</script>
</body>
</html>