Files
llm-proxy/admin/templates/chat.html
hubian 8086d76e93 feat: 添加浏览器标签图标 favicon
- 创建 SVG 格式 favicon(深色背景+大脑/网络节点设计)
- 在所有后台管理页面添加 favicon
2026-04-11 11:31:17 +08:00

416 lines
17 KiB
HTML
Raw Permalink 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>对话 - LLM Proxy</title>
<link rel="icon" href="/static/img/favicon.svg" type="image/svg+xml">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.gradient-bg { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
.chat-container { height: calc(100vh - 200px); }
.message-user { background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%); }
.message-assistant { background: #f3f4f6; }
.typing-indicator span { animation: blink 1.4s infinite both; }
.typing-indicator span:nth-child(2) { animation-delay: 0.2s; }
.typing-indicator span:nth-child(3) { animation-delay: 0.4s; }
@keyframes blink { 0%, 60%, 100% { opacity: 0.3; } 30% { opacity: 1; } }
</style>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: '#6366f1',
}
}
}
}
</script>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="flex">
<!-- 侧边栏 -->
<aside class="w-64 bg-slate-800 min-h-screen fixed left-0 top-0">
<div class="p-6">
<h1 class="text-white text-xl font-bold flex items-center gap-2">
<i class="ri-route-line text-2xl text-purple-400"></i>
LLM Proxy
</h1>
<p class="text-slate-400 text-sm mt-1">后台管理</p>
</div>
<nav class="mt-6">
<a href="/" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-dashboard-line"></i><span>仪表盘</span>
</a>
<a href="/providers" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-server-line"></i><span>提供商管理</span>
</a>
<a href="/models" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-cpu-line"></i><span>模型管理</span>
</a>
<a href="/auto-profiles" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-shuffle-line"></i><span>Auto配置</span>
</a>
<a href="/chat" class="flex items-center gap-3 px-6 py-3 bg-slate-700 text-white">
<i class="ri-chat-3-line"></i><span>对话</span>
</a>
<a href="/logs" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-file-list-line"></i><span>日志查看</span>
</a>
<a href="/config" class="flex items-center gap-3 px-6 py-3 text-slate-300 hover:bg-slate-700 hover:text-white">
<i class="ri-settings-3-line"></i><span>系统配置</span>
</a>
</nav>
</aside>
<!-- 主内容区 -->
<main class="ml-64 flex-1 flex">
<!-- 历史对话列表 -->
<div class="w-72 bg-white border-r flex flex-col">
<div class="p-4 border-b flex justify-between items-center">
<h2 class="font-semibold text-gray-800">历史对话</h2>
<button onclick="newChat()" class="p-2 text-primary hover:bg-indigo-50 rounded-lg transition" title="新建对话">
<i class="ri-add-line text-xl"></i>
</button>
</div>
<div id="chatList" class="flex-1 overflow-y-auto p-2 space-y-1">
<p class="text-gray-400 text-sm text-center py-4">加载中...</p>
</div>
</div>
<!-- 对话区域 -->
<div class="flex-1 flex flex-col">
<!-- 顶部工具栏 -->
<div class="bg-white border-b px-6 py-3 flex items-center justify-between">
<div class="flex items-center gap-4">
<select id="modelSelect" class="px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary focus:border-primary">
<option value="auto">auto (自动选择)</option>
</select>
<span id="chatTitle" class="text-gray-600">新对话</span>
</div>
<div class="flex items-center gap-2">
<button onclick="clearCurrentChat()" class="px-3 py-2 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition" title="清空对话">
<i class="ri-delete-bin-line"></i>
</button>
</div>
</div>
<!-- 消息区域 -->
<div id="messagesContainer" class="flex-1 overflow-y-auto p-6 space-y-4 bg-gray-50">
<div class="text-center text-gray-400 py-12">
<i class="ri-chat-3-line text-4xl"></i>
<p class="mt-2">开始新对话</p>
</div>
</div>
<!-- 输入区域 -->
<div class="bg-white border-t px-6 py-4">
<div class="flex gap-3">
<textarea id="messageInput"
class="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-primary focus:border-primary resize-none"
placeholder="输入消息... (Shift+Enter换行Enter发送)"
rows="1"
onkeydown="handleKeyDown(event)"></textarea>
<button onclick="sendMessage()" id="sendBtn" class="px-6 py-3 gradient-bg text-white rounded-xl hover:opacity-90 transition flex items-center gap-2">
<i class="ri-send-plane-fill"></i>
<span>发送</span>
</button>
</div>
</div>
</div>
</main>
</div>
<script>
let chats = [];
let currentChatId = null;
let models = [];
let isLoading = false;
// 初始化
async function init() {
await Promise.all([loadModels(), loadChats()]);
updateModelSelect();
// 如果有对话,加载第一个
if (chats.length > 0) {
loadChat(chats[0].id);
}
}
// 加载模型列表
async function loadModels() {
const res = await fetch('/api/chat/models');
models = await res.json();
}
// 更新模型选择器
function updateModelSelect() {
const select = document.getElementById('modelSelect');
select.innerHTML = models.map(m =>
`<option value="${m.id}">${m.id}${m.description ? ' - ' + m.description : ''}</option>`
).join('');
}
// 加载对话列表
async function loadChats() {
const res = await fetch('/api/chat/list');
chats = await res.json();
renderChatList();
}
// 渲染对话列表
function renderChatList() {
const container = document.getElementById('chatList');
if (chats.length === 0) {
container.innerHTML = `
<div class="text-center text-gray-400 py-8">
<i class="ri-chat-off-line text-2xl"></i>
<p class="text-sm mt-2">暂无对话</p>
</div>
`;
return;
}
container.innerHTML = chats.map(chat => `
<div onclick="loadChat('${chat.id}')"
class="p-3 rounded-lg cursor-pointer transition ${currentChatId === chat.id ? 'bg-indigo-100 border border-indigo-300' : 'hover:bg-gray-100'}">
<div class="flex justify-between items-start">
<div class="flex-1 min-w-0">
<div class="font-medium text-gray-800 truncate">${chat.title || '新对话'}</div>
<div class="text-xs text-gray-500 mt-1">${chat.message_count || 0} 条消息</div>
</div>
<button onclick="event.stopPropagation(); deleteChat('${chat.id}')"
class="p-1 text-gray-400 hover:text-red-500 hover:bg-red-50 rounded transition">
<i class="ri-delete-bin-line"></i>
</button>
</div>
<div class="text-xs text-gray-400 mt-1">${formatDate(chat.updated_at)}</div>
</div>
`).join('');
}
// 新建对话
function newChat() {
currentChatId = null;
document.getElementById('messagesContainer').innerHTML = `
<div class="text-center text-gray-400 py-12">
<i class="ri-chat-3-line text-4xl"></i>
<p class="mt-2">开始新对话</p>
</div>
`;
document.getElementById('chatTitle').textContent = '新对话';
renderChatList();
document.getElementById('messageInput').focus();
}
// 加载对话
async function loadChat(chatId) {
currentChatId = chatId;
renderChatList();
const res = await fetch(`/api/chat/${chatId}`);
const chat = await res.json();
document.getElementById('chatTitle').textContent = chat.title || '新对话';
document.getElementById('modelSelect').value = chat.model || 'auto';
// 渲染消息
const container = document.getElementById('messagesContainer');
if (!chat.messages || chat.messages.length === 0) {
container.innerHTML = `
<div class="text-center text-gray-400 py-12">
<i class="ri-chat-3-line text-4xl"></i>
<p class="mt-2">开始对话</p>
</div>
`;
return;
}
container.innerHTML = chat.messages.map(msg => renderMessage(msg)).join('');
scrollToBottom();
}
// 渲染单条消息
function renderMessage(msg) {
if (msg.role === 'user') {
return `
<div class="flex justify-end">
<div class="message-user text-white px-4 py-3 rounded-2xl rounded-br-md max-w-[80%]">
<div class="whitespace-pre-wrap">${escapeHtml(msg.content)}</div>
</div>
</div>
`;
} else {
return `
<div class="flex justify-start">
<div class="message-assistant px-4 py-3 rounded-2xl rounded-bl-md max-w-[80%]">
<div class="whitespace-pre-wrap text-gray-800">${escapeHtml(msg.content)}</div>
${msg.model ? `<div class="text-xs text-gray-400 mt-2">模型: ${msg.model}</div>` : ''}
</div>
</div>
`;
}
}
// 发送消息
async function sendMessage() {
const input = document.getElementById('messageInput');
const content = input.value.trim();
if (!content || isLoading) return;
const model = document.getElementById('modelSelect').value;
// 添加用户消息到界面
const container = document.getElementById('messagesContainer');
const userMsg = { role: 'user', content: content };
container.innerHTML += renderMessage(userMsg);
// 清空输入
input.value = '';
scrollToBottom();
// 显示加载状态
isLoading = true;
document.getElementById('sendBtn').disabled = true;
container.innerHTML += `
<div id="typingIndicator" class="flex justify-start">
<div class="message-assistant px-4 py-3 rounded-2xl rounded-bl-md">
<div class="typing-indicator flex gap-1">
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
<span class="w-2 h-2 bg-gray-400 rounded-full"></span>
</div>
</div>
</div>
`;
scrollToBottom();
try {
const res = await fetch('/api/chat/send', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
chat_id: currentChatId,
model: model,
message: content
})
});
const data = await res.json();
// 移除加载指示器
document.getElementById('typingIndicator')?.remove();
if (data.success) {
// 更新当前对话ID
currentChatId = data.chat_id;
// 添加助手回复
const assistantMsg = { role: 'assistant', content: data.response, model: data.model };
container.innerHTML += renderMessage(assistantMsg);
// 更新标题
document.getElementById('chatTitle').textContent = data.title || '新对话';
// 刷新对话列表
loadChats();
} else {
container.innerHTML += `
<div class="flex justify-start">
<div class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-2xl rounded-bl-md">
<i class="ri-error-warning-line mr-1"></i>
${data.error || '请求失败'}
</div>
</div>
`;
}
} catch (e) {
document.getElementById('typingIndicator')?.remove();
container.innerHTML += `
<div class="flex justify-start">
<div class="bg-red-50 border border-red-200 text-red-600 px-4 py-3 rounded-2xl rounded-bl-md">
<i class="ri-error-warning-line mr-1"></i>
网络错误: ${e.message}
</div>
</div>
`;
} finally {
isLoading = false;
document.getElementById('sendBtn').disabled = false;
scrollToBottom();
}
}
// 清空当前对话
async function clearCurrentChat() {
if (!currentChatId) {
newChat();
return;
}
if (!confirm('确定清空当前对话吗?')) return;
await fetch(`/api/chat/${currentChatId}/clear`, { method: 'POST' });
loadChat(currentChatId);
}
// 删除对话
async function deleteChat(chatId) {
if (!confirm('确定删除此对话吗?')) return;
await fetch(`/api/chat/${chatId}`, { method: 'DELETE' });
if (currentChatId === chatId) {
newChat();
}
loadChats();
}
// 处理键盘事件
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
// 滚动到底部
function scrollToBottom() {
const container = document.getElementById('messagesContainer');
container.scrollTop = container.scrollHeight;
}
// 格式化日期
function formatDate(dateStr) {
if (!dateStr) return '';
const date = new Date(dateStr);
const now = new Date();
const diff = now - date;
if (diff < 60000) return '刚刚';
if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
return date.toLocaleDateString();
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 启动
init();
</script>
</body>
</html>