| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- {% extends "base.html" %}
- {% block content %}
- <div class="flex h-screen overflow-hidden" id="ai-view">
- <!-- Sidebar -->
- {% include 'partials/sidebar.html' %}
- <!-- Main Content -->
- <div class="flex-1 flex flex-col min-w-0 overflow-hidden bg-gray-900/80 relative">
- <!-- Top Header -->
- <header class="h-16 flex items-center justify-between px-6 z-20 bg-gray-900/80 backdrop-blur-md border-b border-blue-900/30">
- <button class="md:hidden text-gray-300 focus:outline-none" id="open-sidebar">
- <i class="fas fa-bars text-xl"></i>
- </button>
- <h1 class="text-lg md:text-2xl font-bold tech-title truncate ml-2">AI模型管理</h1>
- <div class="flex items-center space-x-4">
- <div class="flex items-center space-x-2">
- <div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center text-white font-bold border border-cyan-400 shadow-md">A</div>
- <span class="hidden md:inline text-sm text-gray-300">{{ current_user.username }}</span>
- </div>
- </div>
- </header>
- <!-- Main Scrollable Area -->
- <main class="flex-1 overflow-y-auto p-4 md:p-6 scroll-smooth">
- <!-- Toolbar -->
- <div class="mb-6 flex justify-end">
- <button onclick="openEditModal()" class="bg-blue-600 hover:bg-blue-500 text-white px-4 py-2 rounded-lg shadow-lg flex items-center gap-2 transition-all">
- <i class="fas fa-plus"></i> 接入新模型
- </button>
- </div>
- <!-- Model Grid -->
- <div id="model-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
- <!-- Cards injected here -->
- </div>
- </main>
- </div>
- </div>
- <!-- Edit/Add Modal -->
- <div id="edit-modal" class="fixed inset-0 z-50 hidden">
- <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" onclick="closeEditModal()"></div>
- <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-full max-w-lg p-6 animate-fade-in-up">
- <h3 class="text-xl font-bold text-white mb-4" id="modal-title">接入模型</h3>
- <form id="model-form" class="space-y-4">
- <input type="hidden" id="model-id">
- <div>
- <label class="block text-gray-400 text-sm mb-1">模型别名</label>
- <input type="text" id="model-name" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="例如:SiliconFlow Qwen">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">API接口地址 (Base URL)</label>
- <input type="text" id="api-base" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="https://api.siliconflow.cn/v1/">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">API密钥 (API Key)</label>
- <input type="password" id="api-key" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required>
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">模型ID (Model Name)</label>
- <input type="text" id="real-model-name" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" required placeholder="Qwen/Qwen3-Next-80B-A3B-Instruct">
- </div>
- <div>
- <label class="block text-gray-400 text-sm mb-1">系统提示词 (System Prompt)</label>
- <textarea id="system-prompt" rows="3" class="w-full bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" placeholder="You are a helpful assistant."></textarea>
- </div>
- <div class="flex justify-end gap-3 mt-6">
- <button type="button" onclick="closeEditModal()" class="px-4 py-2 rounded text-gray-400 hover:text-white hover:bg-gray-700 transition-colors">取消</button>
- <button type="submit" class="px-4 py-2 rounded bg-blue-600 text-white hover:bg-blue-700 transition-colors shadow-lg">保存</button>
- </div>
- </form>
- </div>
- </div>
- <!-- Test Chat Modal -->
- <div id="test-modal" class="fixed inset-0 z-50 hidden">
- <div class="absolute inset-0 bg-black/70 backdrop-blur-sm" onclick="closeTestModal()"></div>
- <div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-gray-800 rounded-xl shadow-2xl border border-gray-700 w-full max-w-2xl h-[600px] flex flex-col animate-fade-in-up">
- <div class="p-4 border-b border-gray-700 flex justify-between items-center bg-gray-800/50 rounded-t-xl">
- <div>
- <h3 class="text-lg font-bold text-white">模型测试</h3>
- <span class="text-xs text-green-400 flex items-center gap-1"><i class="fas fa-circle text-[8px]"></i> <span id="test-model-name">目标模型</span></span>
- </div>
- <button onclick="closeTestModal()" class="text-gray-400 hover:text-white"><i class="fas fa-times"></i></button>
- </div>
-
- <!-- Chat Area -->
- <div id="chat-box" class="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-900/50">
- <div class="flex justify-start">
- <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
- <p>你好!我是该模型的测试助手,请发送消息开始测试连接。</p>
- </div>
- </div>
- </div>
- <!-- Input Area -->
- <div class="p-4 bg-gray-800 border-t border-gray-700">
- <div class="flex gap-2">
- <input type="text" id="test-input" class="flex-1 bg-gray-700 text-white rounded p-2 border border-gray-600 focus:border-blue-500 focus:outline-none" placeholder="输入测试消息..." onkeypress="if(event.keyCode==13) sendTestMessage()">
- <button onclick="sendTestMessage()" id="send-btn" class="bg-blue-600 hover:bg-blue-500 text-white px-4 rounded transition-colors">
- <i class="fas fa-paper-plane"></i>
- </button>
- </div>
- <div class="mt-2 text-xs text-gray-500 flex justify-between">
- <span>本次消耗: <span id="last-tokens" class="text-blue-400">0</span> tokens</span>
- <span>总消耗: <span id="total-tokens-display" class="text-purple-400">0</span> tokens</span>
- </div>
- </div>
- </div>
- </div>
- <!-- Toast -->
- <div id="toast" class="fixed top-5 left-1/2 transform -translate-x-1/2 z-50 hidden transition-all duration-300">
- <div class="bg-gray-800 border-l-4 border-blue-500 text-white px-6 py-3 rounded shadow-lg flex items-center gap-3">
- <i class="fas fa-info-circle text-blue-400" id="toast-icon"></i>
- <span id="toast-message">系统通知</span>
- </div>
- </div>
- <script>
- let currentTestModelId = null;
- $(document).ready(function() {
- loadModels();
- $('#open-sidebar').click(function() {
- $('#sidebar').toggleClass('-translate-x-full');
- });
- $('#model-form').submit(function(e) {
- e.preventDefault();
- saveModel();
- });
- });
- function loadModels() {
- $.get('/ai/api/list', function(response) {
- const grid = $('#model-grid');
- grid.empty();
-
- if (response.items.length === 0) {
- grid.html('<div class="col-span-full text-center text-gray-500 py-10">暂无模型接入</div>');
- return;
- }
- response.items.forEach(item => {
- grid.append(`
- <div class="bg-gray-800 rounded-xl border border-gray-700 p-6 hover:border-blue-500/50 transition-all group relative overflow-hidden">
- <div class="absolute top-0 right-0 p-4 opacity-10 group-hover:opacity-20 transition-opacity">
- <i class="fas fa-brain text-6xl text-blue-500"></i>
- </div>
-
- <div class="flex justify-between items-start mb-4 relative z-10">
- <div>
- <h3 class="text-lg font-bold text-white mb-1">${item.name}</h3>
- <p class="text-xs text-gray-400 font-mono bg-gray-900/50 px-2 py-1 rounded inline-block">${item.model_name}</p>
- </div>
- <div class="flex items-center">
- <label class="relative inline-flex items-center cursor-pointer">
- <input type="checkbox" class="sr-only peer" onchange="toggleModel(${item.id})" ${item.is_active ? 'checked' : ''}>
- <div class="w-9 h-5 bg-gray-600 peer-focus:outline-none peer-focus:ring-2 peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div>
- </label>
- </div>
- </div>
- <div class="space-y-3 mb-6 relative z-10">
- <div class="flex items-center justify-between text-sm">
- <span class="text-gray-400">服务提供商</span>
- <span class="text-gray-300">${item.provider}</span>
- </div>
- <div class="flex items-center justify-between text-sm">
- <span class="text-gray-400">Token消耗</span>
- <span class="text-purple-400 font-mono font-bold">${item.total_tokens.toLocaleString()}</span>
- </div>
- <div class="flex items-center justify-between text-sm">
- <span class="text-gray-400">接口状态</span>
- <span class="text-green-400"><i class="fas fa-check-circle"></i> 已配置</span>
- </div>
- </div>
- <div class="flex gap-2 relative z-10 pt-4 border-t border-gray-700">
- <button onclick="openTestModal(${item.id}, '${item.name}', ${item.total_tokens})" class="flex-1 bg-blue-600/20 hover:bg-blue-600 text-blue-400 hover:text-white py-2 rounded transition-colors text-sm font-medium">
- <i class="fas fa-comments mr-1"></i> 测试对话
- </button>
- <button onclick='openEditModal(${JSON.stringify(item)})' class="bg-gray-700 hover:bg-gray-600 text-gray-300 py-2 px-3 rounded transition-colors" title="编辑">
- <i class="fas fa-edit"></i>
- </button>
- <button onclick="deleteModel(${item.id})" class="bg-red-900/20 hover:bg-red-600 text-red-400 hover:text-white py-2 px-3 rounded transition-colors" title="删除">
- <i class="fas fa-trash-alt"></i>
- </button>
- </div>
- </div>
- `);
- });
- });
- }
- function openEditModal(model = null) {
- if (model) {
- $('#modal-title').text('编辑模型');
- $('#model-id').val(model.id);
- $('#model-name').val(model.name);
- $('#api-base').val(model.api_base);
- $('#api-key').val(model.api_key); // Note: Should handle security better
- $('#real-model-name').val(model.model_name);
- $('#system-prompt').val(model.system_prompt);
- } else {
- $('#modal-title').text('接入新模型');
- $('#model-form')[0].reset();
- $('#model-id').val('');
- }
- $('#edit-modal').removeClass('hidden');
- }
- function closeEditModal() {
- $('#edit-modal').addClass('hidden');
- }
- function saveModel() {
- const data = {
- id: $('#model-id').val(),
- name: $('#model-name').val(),
- api_base: $('#api-base').val(),
- api_key: $('#api-key').val(),
- model_name: $('#real-model-name').val(),
- system_prompt: $('#system-prompt').val()
- };
- $.ajax({
- url: '/ai/api/save',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify(data),
- success: function() {
- showToast('保存成功', 'success');
- closeEditModal();
- loadModels();
- },
- error: function(xhr) {
- showToast('保存失败: ' + (xhr.responseJSON?.error || 'Unknown error'), 'error');
- }
- });
- }
- function deleteModel(id) {
- if (!confirm('确定要删除该模型配置吗?')) return;
- $.ajax({
- url: '/ai/api/delete',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({id: id}),
- success: function() {
- showToast('删除成功', 'success');
- loadModels();
- },
- error: function(xhr) {
- showToast('删除失败', 'error');
- }
- });
- }
- function toggleModel(id) {
- $.ajax({
- url: '/ai/api/toggle',
- method: 'POST',
- contentType: 'application/json',
- data: JSON.stringify({id: id}),
- success: function() {
- // Optionally show toast
- }
- });
- }
- // --- Test Chat Logic ---
- function openTestModal(id, name, totalTokens) {
- currentTestModelId = id;
- $('#test-model-name').text(name);
- $('#total-tokens-display').text(totalTokens.toLocaleString());
- $('#last-tokens').text('0');
- $('#chat-box').html(`
- <div class="flex justify-start">
- <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
- <p>你好!我是该模型的测试助手,请发送消息开始测试连接。</p>
- </div>
- </div>
- `);
- $('#test-modal').removeClass('hidden');
- $('#test-input').focus();
- }
- function closeTestModal() {
- $('#test-modal').addClass('hidden');
- currentTestModelId = null;
- }
- async function sendTestMessage() {
- const input = $('#test-input');
- const msg = input.val().trim();
- if (!msg) return;
- // Append User Message
- $('#chat-box').append(`
- <div class="flex justify-end animate-fade-in-up">
- <div class="bg-blue-600 text-white p-3 rounded-lg rounded-tr-none max-w-[80%]">
- <p>${escapeHtml(msg)}</p>
- </div>
- </div>
- `);
-
- input.val('');
- scrollToBottom();
- // Prepare Assistant Message Container
- const msgId = 'msg-' + Date.now();
- $('#chat-box').append(`
- <div class="flex justify-start animate-fade-in-up">
- <div class="bg-gray-700 text-gray-200 p-3 rounded-lg rounded-tl-none max-w-[80%]">
- <p id="${msgId}" class="whitespace-pre-wrap"><i class="fas fa-circle-notch fa-spin text-blue-400"></i></p>
- </div>
- </div>
- `);
- scrollToBottom();
- try {
- const response = await fetch('/ai/api/test', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- id: currentTestModelId,
- message: msg
- })
- });
- const reader = response.body.getReader();
- const decoder = new TextDecoder();
- let assistantText = '';
- let isFirstChunk = true;
- const $msgContent = $(`#${msgId}`);
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- const chunk = decoder.decode(value, { stream: true });
- const lines = chunk.split('\n');
-
- for (const line of lines) {
- if (line.startsWith('data: ')) {
- const dataStr = line.slice(6).trim();
- if (dataStr === '[DONE]') continue;
- if (!dataStr) continue;
-
- try {
- const data = JSON.parse(dataStr);
- if (data.error) {
- $msgContent.html(`<span class="text-red-400">Error: ${data.error}</span>`);
- return;
- }
- if (data.content) {
- if (isFirstChunk) {
- $msgContent.empty(); // Remove spinner
- isFirstChunk = false;
- }
- assistantText += data.content;
- $msgContent.text(assistantText);
- scrollToBottom();
- }
- if (data.usage) {
- $('#last-tokens').text(data.usage.total);
- $('#total-tokens-display').text(data.usage.total_aggregated.toLocaleString());
- loadModels();
- }
- } catch (e) {
- console.error('Error parsing SSE:', e);
- }
- }
- }
- }
- } catch (error) {
- $(`#${msgId}`).html(`<span class="text-red-400">Connection Error: ${error.message}</span>`);
- }
- }
- function scrollToBottom() {
- const chatBox = document.getElementById('chat-box');
- chatBox.scrollTop = chatBox.scrollHeight;
- }
- function escapeHtml(text) {
- const div = document.createElement('div');
- div.textContent = text;
- return div.innerHTML;
- }
- function showToast(msg, type = 'info') {
- const toast = $('#toast');
- const icon = $('#toast-icon');
- const text = $('#toast-message');
- const container = toast.find('div');
-
- text.text(msg);
- toast.removeClass('hidden').addClass('flex');
-
- container.removeClass('border-blue-500 border-red-500 border-green-500');
- icon.removeClass('text-blue-400 text-red-400 text-green-400 fa-info-circle fa-check-circle fa-exclamation-circle');
-
- if (type === 'success') {
- container.addClass('border-green-500');
- icon.addClass('text-green-400 fa-check-circle');
- } else if (type === 'error') {
- container.addClass('border-red-500');
- icon.addClass('text-red-400 fa-exclamation-circle');
- } else {
- container.addClass('border-blue-500');
- icon.addClass('text-blue-400 fa-info-circle');
- }
-
- setTimeout(() => {
- toast.addClass('hidden').removeClass('flex');
- }, 3000);
- }
- </script>
- {% endblock %}
|