Spaces:
Sleeping
Sleeping
| <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://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.2.19/tailwind.min.css" rel="stylesheet"> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/atom-one-dark.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #3498db; | |
| --secondary-color: #2ecc71; | |
| --background-color: #f5f5f5; | |
| --chat-background: #ffffff; | |
| --text-color: #333333; | |
| --chat-bubble-user: #e8f5fe; | |
| --chat-bubble-ai: #f0f0f0; | |
| --sidebar-background: #ffffff; | |
| --input-background: #ffffff; | |
| --send-button-color: #4CAF50; | |
| --console-background: #f8f9fa; | |
| --console-text: #495057; | |
| --fold-background: #f8f9fa; | |
| --fold-text: #495057; | |
| --border-color: transparent; | |
| --search-result-background: #f8f9fa; | |
| } | |
| .dark-mode { | |
| --primary-color: #3498db; | |
| --secondary-color: #2ecc71; | |
| --background-color: #1e2124; | |
| --chat-background: #36393f; | |
| --text-color: #dcddde; | |
| --chat-bubble-user: #4e5d94; | |
| --chat-bubble-ai: #40444b; | |
| --sidebar-background: #2f3136; | |
| --input-background: #40444b; | |
| --send-button-color: #7289da; | |
| --console-background: #2f3136; | |
| --console-text: #dcddde; | |
| --fold-background: #2f3136; | |
| --fold-text: #dcddde; | |
| --border-color: #ffffff; | |
| --search-result-background: #2f3136; | |
| } | |
| body { | |
| background-color: var(--background-color); | |
| color: var(--text-color); | |
| transition: all 0.3s ease; | |
| } | |
| .chat-container { | |
| background-color: var(--chat-background); | |
| transition: all 0.3s ease; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| .chat-bubble { | |
| max-width: 80%; | |
| padding: 12px; | |
| border-radius: 12px; | |
| margin-bottom: 12px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .user-bubble { | |
| background-color: var(--chat-bubble-user); | |
| color: var(--text-color); | |
| margin-left: auto; | |
| border-bottom-right-radius: 4px; | |
| } | |
| .ai-bubble { | |
| background-color: var(--chat-bubble-ai); | |
| color: var(--text-color); | |
| margin-right: auto; | |
| border-bottom-left-radius: 4px; | |
| } | |
| .dark-mode .chat-bubble { | |
| border: 1px solid var(--border-color); | |
| } | |
| .status-indicator { | |
| width: 12px; | |
| height: 12px; | |
| border-radius: 50%; | |
| display: inline-block; | |
| margin-right: 8px; | |
| transition: all 0.3s ease; | |
| } | |
| .status-active { | |
| background-color: var(--secondary-color); | |
| } | |
| .status-inactive { | |
| background-color: #95a5a6; | |
| } | |
| .sidebar { | |
| background-color: var(--sidebar-background); | |
| transition: all 0.3s ease; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| #user-input { | |
| background-color: var(--input-background); | |
| color: var(--text-color); | |
| transition: all 0.3s ease; | |
| border-radius: 8px; | |
| padding-right: 40px; | |
| } | |
| #send-btn { | |
| position: absolute; | |
| right: 10px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| background: none; | |
| border: none; | |
| color: var(--send-button-color); | |
| font-size: 20px; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| #send-btn:hover { | |
| opacity: 0.8; | |
| } | |
| .console { | |
| background-color: var(--console-background); | |
| color: var(--console-text); | |
| transition: all 0.3s ease; | |
| border-radius: 8px; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| #status-log { | |
| background-color: var(--console-background); | |
| color: var(--console-text); | |
| transition: all 0.3s ease; | |
| border: 1px solid var(--border-color); | |
| } | |
| /* 滚动条样式 */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--chat-background); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--primary-color); | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--secondary-color); | |
| } | |
| /* 代码块样式 */ | |
| .hljs { | |
| background: var(--fold-background) ; | |
| color: var(--fold-text) ; | |
| } | |
| .search-results { | |
| background-color: var(--search-result-background); | |
| color: var(--text-color); | |
| border: 1px solid var(--border-color); | |
| } | |
| .search-results summary { | |
| cursor: pointer; | |
| font-weight: bold; | |
| } | |
| .search-results ul { | |
| list-style-type: none; | |
| padding-left: 0; | |
| } | |
| .search-results li { | |
| background-color: var(--chat-bubble-ai); | |
| border: 1px solid var(--border-color); | |
| } | |
| /* 控制台消息条目样式 */ | |
| #status-log div { | |
| background-color: var(--console-background); | |
| color: var(--console-text); | |
| border: 1px solid var(--border-color); | |
| } | |
| @media (max-width: 768px) { | |
| .sidebar { | |
| position: fixed; | |
| right: -300px; | |
| top: 0; | |
| bottom: 0; | |
| width: 300px; | |
| z-index: 1000; | |
| transition: right 0.3s ease-in-out; | |
| } | |
| .sidebar.open { | |
| right: 0; | |
| } | |
| .sidebar-overlay { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background-color: rgba(0, 0, 0, 0.5); | |
| z-index: 999; | |
| } | |
| .sidebar-overlay.show { | |
| display: block; | |
| } | |
| } | |
| </style> | |
| <body class="h-screen flex flex-col md:flex-row p-4"> | |
| <!-- 主聊天窗口 --> | |
| <div class="chat-container flex-grow md:w-3/4 p-4 flex flex-col h-screen mr-4"> | |
| <div id="chat-window" class="flex-grow overflow-y-auto mb-4 p-4 rounded-lg shadow-inner"></div> | |
| <div class="relative"> | |
| <input id="user-input" type="text" class="w-full border p-2" placeholder="输入您的问题..."> | |
| <button id="send-btn" class="text-2xl"> | |
| <i class="fas fa-paper-plane"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- 右侧控制台 --> | |
| <div class="sidebar console md:w-1/4 p-4 h-screen overflow-y-auto"> | |
| <h2 class="text-xl font-bold mb-4">运行状态</h2> | |
| <div class="mb-4"> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="main-model-status"></span> | |
| <span class="model-name">主模型</span> | |
| <span class="model-action" id="main-model-action"></span> | |
| </div> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="sub-model-1-status"></span> | |
| <span class="model-name">次级模型1</span> | |
| <span class="model-action" id="sub-model-1-action"></span> | |
| </div> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="sub-model-2-status"></span> | |
| <span class="model-name">次级模型2</span> | |
| <span class="model-action" id="sub-model-2-action"></span> | |
| </div> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="arbitration-model-status"></span> | |
| <span class="model-name">裁决模型</span> | |
| <span class="model-action" id="arbitration-model-action"></span> | |
| </div> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="search-status"></span> | |
| <span class="model-name">搜索</span> | |
| <span class="model-action" id="search-action"></span> | |
| </div> | |
| <div class="model-status"> | |
| <span class="status-indicator" id="email-status"></span> | |
| <span class="model-name">邮件发送</span> | |
| <span class="model-action" id="email-action"></span> | |
| </div> | |
| </div> | |
| <div id="status-log" class="p-2 rounded h-64 overflow-y-auto mb-4"></div> | |
| <button id="settings-btn" class="w-full bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-white px-4 py-2 rounded hover:bg-gray-400 dark:hover:bg-gray-500 transition duration-200"> | |
| <i class="fas fa-cog"></i> 设置 | |
| </button> | |
| </div> | |
| <!-- 移动端侧边栏切换按钮 --> | |
| <button id="sidebar-toggle" class="md:hidden fixed top-4 right-4 bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-white p-2 rounded-full shadow-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition duration-200 z-50"> | |
| <i class="fas fa-bars"></i> | |
| </button> | |
| <!-- 移动端侧边栏遮罩 --> | |
| <div id="sidebar-overlay" class="sidebar-overlay"></div> | |
| <!-- 暗色模式切换按钮 --> | |
| <button id="dark-mode-toggle" class="fixed top-4 left-4 bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-white p-2 rounded-full shadow-lg hover:bg-gray-400 dark:hover:bg-gray-500 transition duration-200"> | |
| <i class="fas fa-moon"></i> | |
| </button> | |
| <!-- 设置对话框 --> | |
| <div id="settings-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden flex items-center justify-center z-50"> | |
| <div class="bg-white dark:bg-gray-800 p-6 rounded-lg shadow-xl"> | |
| <h2 class="text-xl font-bold mb-4">设置</h2> | |
| <div class="mb-4"> | |
| <label for="max-history" class="block mb-2">保留对话轮数:</label> | |
| <input type="number" id="max-history" class="border rounded p-2 w-full dark:bg-gray-700 dark:text-white" min="1" max="50" value="10"> | |
| </div> | |
| <div class="flex justify-end"> | |
| <button id="save-settings" class="bg-blue-500 text-white px-4 py-2 rounded mr-2 hover:bg-blue-600 transition duration-200">保存</button> | |
| <button id="cancel-settings" class="bg-gray-300 dark:bg-gray-600 text-gray-800 dark:text-white px-4 py-2 rounded hover:bg-gray-400 dark:hover:bg-gray-500 transition duration-200">取消</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script> | |
| <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script> | |
| <script> | |
| const chatWindow = document.getElementById('chat-window'); | |
| const userInput = document.getElementById('user-input'); | |
| const sendBtn = document.getElementById('send-btn'); | |
| const statusLog = document.getElementById('status-log'); | |
| const settingsBtn = document.getElementById('settings-btn'); | |
| const settingsModal = document.getElementById('settings-modal'); | |
| const saveSettingsBtn = document.getElementById('save-settings'); | |
| const cancelSettingsBtn = document.getElementById('cancel-settings'); | |
| const maxHistoryInput = document.getElementById('max-history'); | |
| const darkModeToggle = document.getElementById('dark-mode-toggle'); | |
| const sidebarToggle = document.getElementById('sidebar-toggle'); | |
| const sidebar = document.querySelector('.sidebar'); | |
| const sidebarOverlay = document.getElementById('sidebar-overlay'); | |
| let conversationHistory = []; | |
| let maxHistory = 10; | |
| function addMessage(content, isUser) { | |
| const bubble = document.createElement('div'); | |
| bubble.className = `chat-bubble ${isUser ? 'user-bubble' : 'ai-bubble'}`; | |
| bubble.innerHTML = isUser ? content : marked.parse(content); | |
| chatWindow.appendChild(bubble); | |
| chatWindow.scrollTop = chatWindow.scrollHeight; | |
| bubble.querySelectorAll('pre code').forEach((block) => { | |
| hljs.highlightBlock(block); | |
| }); | |
| conversationHistory.push({ | |
| role: isUser ? 'user' : 'assistant', | |
| content: content | |
| }); | |
| if (conversationHistory.length > maxHistory * 2) { | |
| conversationHistory = conversationHistory.slice(-maxHistory * 2); | |
| } | |
| } | |
| function updateStatus(main, sub1, sub2, arbitration, search, email) { | |
| updateModelStatus('main-model', main); | |
| updateModelStatus('sub-model-1', sub1); | |
| updateModelStatus('sub-model-2', sub2); | |
| updateModelStatus('arbitration-model', arbitration); | |
| updateModelStatus('search', search); | |
| updateModelStatus('email', email); | |
| } | |
| function updateModelStatus(modelId, isActive, action = '') { | |
| const statusElement = document.getElementById(`${modelId}-status`); | |
| const actionElement = document.getElementById(`${modelId}-action`); | |
| statusElement.className = `status-indicator ${isActive ? 'status-active' : 'status-inactive'}`; | |
| actionElement.textContent = action; | |
| } | |
| function addStatusLog(message) { | |
| const logEntry = document.createElement('div'); | |
| logEntry.className = 'mb-2 p-2 bg-white dark:bg-gray-700 rounded shadow'; | |
| logEntry.textContent = message; | |
| statusLog.appendChild(logEntry); | |
| statusLog.scrollTop = statusLog.scrollHeight; | |
| } | |
| function displaySearchResults(results, type) { | |
| const searchResults = document.createElement('details'); | |
| searchResults.className = 'search-results mb-2 p-2 rounded'; | |
| const summary = document.createElement('summary'); | |
| summary.textContent = type === 'papers' ? '论文搜索结果' : '搜索结果'; | |
| searchResults.appendChild(summary); | |
| const resultsList = document.createElement('ul'); | |
| resultsList.className = 'mt-2'; | |
| results.forEach(result => { | |
| const li = document.createElement('li'); | |
| li.className = 'mb-2 p-2 rounded'; | |
| if (type === 'papers') { | |
| li.innerHTML = ` | |
| <strong>标题:</strong> ${result.标题}<br> | |
| <strong>作者:</strong> ${result.作者}<br> | |
| <strong>DOI:</strong> ${result.DOI}<br> | |
| <strong>ISBN:</strong> ${result.ISBN}<br> | |
| <strong>摘要:</strong> ${result.摘要} | |
| `; | |
| } else { | |
| li.innerHTML = `<strong>${result.title}</strong><br>${result.body}`; | |
| } | |
| resultsList.appendChild(li); | |
| }); | |
| searchResults.appendChild(resultsList); | |
| chatWindow.appendChild(searchResults); | |
| } | |
| function updateStatusFromLog(log) { | |
| if (log.includes("主模型:")) { | |
| updateModelStatus('main-model', true, log.split("主模型:")[1]); | |
| } else if (log.includes("次级模型1:")) { | |
| updateModelStatus('sub-model-1', true, log.split("次级模型1:")[1]); | |
| } else if (log.includes("次级模型2:")) { | |
| updateModelStatus('sub-model-2', true, log.split("次级模型2:")[1]); | |
| } else if (log.includes("裁决模型:")) { | |
| updateModelStatus('arbitration-model', true, log.split("裁决模型:")[1]); | |
| } else if (log.includes("搜索:")) { | |
| updateModelStatus('search', true, log.split("搜索:")[1]); | |
| } else if (log.includes("邮件:")) { | |
| updateModelStatus('email', true, log.split("邮件:")[1]); | |
| } | |
| } | |
| async function sendMessage() { | |
| const question = userInput.value.trim(); | |
| if (!question) return; | |
| addMessage(question, true); | |
| userInput.value = ''; | |
| updateStatus(false, false, false, false); | |
| statusLog.innerHTML = ''; | |
| addStatusLog('开始处理问题...'); | |
| try { | |
| const response = await fetch('/chat', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| question, | |
| history: conversationHistory.slice(-maxHistory * 2 + 1) | |
| }) | |
| }); | |
| const data = await response.json(); | |
| data.status_log.forEach(log => addStatusLog(log)); | |
| updateStatus( | |
| data.status_log.includes("主模型:回答生成完成"), | |
| data.status_log.some(log => log.includes("次级模型")), | |
| data.search_used, | |
| data.email_sent | |
| ); | |
| if (data.search_results) { | |
| if (Array.isArray(data.search_results) && data.search_results.length > 0 && 'DOI' in data.search_results[0]) { | |
| displaySearchResults(data.search_results, 'papers'); | |
| } else { | |
| displaySearchResults(data.search_results, 'web'); | |
| } | |
| } | |
| addMessage(data.response, false); | |
| addStatusLog('回答完成'); | |
| } catch (error) { | |
| console.error('Error:', error); | |
| addMessage('发生错误,请稍后再试。', false); | |
| updateStatus(false, false, false, false); | |
| addStatusLog('发生错误'); | |
| } | |
| } | |
| sendBtn.addEventListener('click', sendMessage); | |
| userInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| sendMessage(); | |
| } | |
| }); | |
| settingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.remove('hidden'); | |
| maxHistoryInput.value = maxHistory; | |
| }); | |
| saveSettingsBtn.addEventListener('click', async () => { | |
| const newMaxHistory = parseInt(maxHistoryInput.value); | |
| if (newMaxHistory >= 1 && newMaxHistory <= 50) { | |
| maxHistory = newMaxHistory; | |
| try { | |
| const response = await fetch('/settings', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ max_history: maxHistory }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| alert('设置已保存'); | |
| } else { | |
| alert('保存设置时出错'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert('保存设置时出错'); | |
| } | |
| settingsModal.classList.add('hidden'); | |
| } else { | |
| alert('请输入1到50之间的数字'); | |
| } | |
| }); | |
| cancelSettingsBtn.addEventListener('click', () => { | |
| settingsModal.classList.add('hidden'); | |
| }); | |
| darkModeToggle.addEventListener('click', () => { | |
| document.body.classList.toggle('dark-mode'); | |
| updateDarkModeIcon(); | |
| }); | |
| function updateDarkModeIcon() { | |
| const icon = darkModeToggle.querySelector('i'); | |
| if (document.body.classList.contains('dark-mode')) { | |
| icon.classList.remove('fa-moon'); | |
| icon.classList.add('fa-sun'); | |
| } else { | |
| icon.classList.remove('fa-sun'); | |
| icon.classList.add('fa-moon'); | |
| } | |
| } | |
| function initDarkMode() { | |
| if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { | |
| document.body.classList.add('dark-mode'); | |
| } | |
| updateDarkModeIcon(); | |
| } | |
| window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { | |
| if (e.matches) { | |
| document.body.classList.add('dark-mode'); | |
| } else { | |
| document.body.classList.remove('dark-mode'); | |
| } | |
| updateDarkModeIcon(); | |
| }); | |
| sidebarToggle.addEventListener('click', () => { | |
| sidebar.classList.toggle('open'); | |
| sidebarOverlay.classList.toggle('show'); | |
| }); | |
| sidebarOverlay.addEventListener('click', () => { | |
| sidebar.classList.remove('open'); | |
| sidebarOverlay.classList.remove('show'); | |
| }); | |
| window.addEventListener('load', () => { | |
| initDarkMode(); | |
| }); | |
| window.addEventListener('resize', () => { | |
| if (window.innerWidth >= 768) { | |
| sidebar.classList.remove('open'); | |
| sidebarOverlay.classList.remove('show'); | |
| } | |
| }); | |
| // 可以在这里添加任何其他需要的 JavaScript 功能 | |
| </script> | |
| </body> | |
| </html> |