// ===== index.js (Redis 存储版) ===== const chatArea = document.getElementById('chatArea'); const messageInput = document.getElementById('messageInput'); const submitBtn = document.getElementById('submitBtn'); const newChatBtn = document.getElementById('newChatBtn'); const chatList = document.getElementById('chatList'); const welcomeMessage = document.getElementById('welcomeMessage'); const toggleSidebarBtn = document.getElementById('toggleSidebar'); const sidebar = document.getElementById('sidebar'); // RAG 相关 DOM const ragSelect = document.getElementById('ragSelect'); const deleteRagBtn = document.getElementById('deleteRagBtn'); const refreshRagBtn = document.getElementById('refreshRagBtn'); let currentEventSource = null; let currentChatId = localStorage.getItem('currentChatId') || null; // ==================== Redis 存储相关函数 ==================== /** * 获取所有聊天会话及其标题 */ async function getAllChats() { try { const response = await fetch('http://localhost:8095/api/v1/chat'); const chatIds = await response.json(); return Promise.all(chatIds.map(async chatId => { const nameResponse = await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`); const name = await nameResponse.text(); return { chatId, name }; })); } catch (error) { console.error('获取聊天列表失败:', error); return []; } } /** * 创建新聊天会话 */ async function createNewChat() { const chatId = Date.now().toString(); try { // 设置默认名称 await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: '新聊天' }) }); currentChatId = chatId; localStorage.setItem('currentChatId', chatId); updateChatList(); clearChatArea(); return chatId; } catch (error) { console.error('创建聊天失败:', error); throw error; } } /** * 删除聊天会话 */ async function deleteChat(chatId) { if (!confirm('确定要删除这个聊天记录吗?')) return; try { const response = await fetch(`http://localhost:8095/api/v1/chat/${chatId}`, { method: 'DELETE' }); if (response.ok) { if (currentChatId === chatId) { await createNewChat(); } updateChatList(); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error('删除聊天失败:', error); alert('删除失败: ' + error.message); } } /** * 重命名聊天会话 */ async function renameChat(chatId) { const currentName = await (await fetch(`http://localhost:8095/api/v1/chat/${chatId}/name`)).text(); const newName = prompt('请输入新的聊天名称', currentName); if (newName) { try { await fetch(`http://localhost:8095/api/v1/chat/${chatId}/rename`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newName }) }); updateChatList(); } catch (error) { console.error('重命名失败:', error); alert('重命名失败: ' + error.message); } } } /** * 加载聊天历史 */ async function loadChat(chatId) { try { const response = await fetch(`http://localhost:8095/chat/${chatId}`); const messages = await response.json(); currentChatId = chatId; localStorage.setItem('currentChatId', chatId); clearChatArea(); messages.forEach(msg => { appendMessage(msg.content, msg.role === 'assistant', false); }); } catch (error) { console.error('加载聊天失败:', error); alert('加载聊天失败: ' + error.message); } } /** * 保存消息到 Redis */ async function saveMessage(chatId, content, isAssistant) { const msg = { role: isAssistant ? "assistant" : "user", content: content, ts: Date.now() }; try { await fetch(`http://localhost:8095/api/v1/chat/${chatId}/message`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(msg) }); } catch (error) { console.error('保存消息失败:', error); } } /** * 更新聊天列表 UI */ async function updateChatList() { chatList.innerHTML = ''; const chats = await getAllChats(); // 当前聊天置顶 chats.sort((a, b) => { if (a.chatId === currentChatId) return -1; if (b.chatId === currentChatId) return 1; return parseInt(b.chatId) - parseInt(a.chatId); // 按时间倒序 }); chats.forEach(chat => { const li = document.createElement('li'); li.className = `chat-item flex items-center justify-between p-2 hover:bg-gray-100 rounded-lg cursor-pointer transition-colors ${ chat.chatId === currentChatId ? 'bg-blue-50' : '' }`; const date = new Date(parseInt(chat.chatId)).toLocaleDateString('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit' }); li.innerHTML = `
${chat.name}
${date}
`; // 事件绑定 li.querySelectorAll('button')[0].addEventListener('click', (e) => { e.stopPropagation(); renameChat(chat.chatId); }); li.querySelectorAll('button')[1].addEventListener('click', (e) => { e.stopPropagation(); deleteChat(chat.chatId); }); li.addEventListener('click', () => loadChat(chat.chatId)); li.addEventListener('mouseenter', () => { li.querySelector('.chat-actions').classList.remove('opacity-0'); }); li.addEventListener('mouseleave', () => { li.querySelector('.chat-actions').classList.add('opacity-0'); }); chatList.appendChild(li); }); } // ==================== 消息显示相关函数 ==================== function clearChatArea() { chatArea.innerHTML = ''; welcomeMessage.style.display = 'flex'; } function appendMessage(content, isAssistant = false, saveToStorage = true) { welcomeMessage.style.display = 'none'; const messageDiv = document.createElement('div'); messageDiv.className = `max-w-4xl mx-auto mb-4 p-4 rounded-lg ${ isAssistant ? 'bg-gray-100' : 'bg-white border' } markdown-body relative`; const renderedContent = DOMPurify.sanitize(marked.parse(content)); messageDiv.innerHTML = renderedContent; const copyBtn = document.createElement('button'); copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs'; copyBtn.textContent = '复制'; copyBtn.onclick = () => { navigator.clipboard.writeText(content).then(() => { copyBtn.textContent = '已复制'; setTimeout(() => (copyBtn.textContent = '复制'), 2000); }); }; messageDiv.appendChild(copyBtn); chatArea.appendChild(messageDiv); chatArea.scrollTop = chatArea.scrollHeight; if (saveToStorage && currentChatId) { saveMessage(currentChatId, content, isAssistant); } } // ==================== 事件流处理 ==================== function startEventStream(message) { if (currentEventSource) currentEventSource.close(); const ragTag = ragSelect.value; const aiModelSelect = document.getElementById('aiModel'); const aiModelValue = aiModelSelect.value; const aiModelModel = aiModelSelect.options[aiModelSelect.selectedIndex].getAttribute('model'); let url; const base = `http://localhost:8095/api/v1/${aiModelValue}`; const params = new URLSearchParams({ message, model: aiModelModel }); if (ragTag) { params.append('ragTag', ragTag); url = `${base}/generate_stream_rag?${params.toString()}`; } else { url = `${base}/generate_stream?${params.toString()}`; } currentEventSource = new EventSource(url); let accumulatedContent = ''; let tempMessageDiv = null; currentEventSource.onmessage = function(event) { try { const data = JSON.parse(event.data); const content = data.result?.output?.content || data.content; if (content) { accumulatedContent += content; if (!tempMessageDiv) { tempMessageDiv = document.createElement('div'); tempMessageDiv.className = 'max-w-4xl mx-auto mb-4 p-4 rounded-lg bg-gray-100 markdown-body relative'; chatArea.appendChild(tempMessageDiv); welcomeMessage.style.display = 'none'; } tempMessageDiv.textContent = accumulatedContent; chatArea.scrollTop = chatArea.scrollHeight; } if (data.result?.output?.properties?.finishReason === 'STOP') { currentEventSource.close(); if (tempMessageDiv) { tempMessageDiv.innerHTML = DOMPurify.sanitize(marked.parse(accumulatedContent)); const copyBtn = document.createElement('button'); copyBtn.className = 'absolute top-2 right-2 p-1 bg-gray-200 rounded-md text-xs'; copyBtn.textContent = '复制'; copyBtn.onclick = () => { navigator.clipboard.writeText(accumulatedContent).then(() => { copyBtn.textContent = '已复制'; setTimeout(() => (copyBtn.textContent = '复制'), 2000); }); }; tempMessageDiv.appendChild(copyBtn); } if (currentChatId) { saveMessage(currentChatId, accumulatedContent, true); } } } catch (e) { console.error('解析事件数据失败:', e); } }; currentEventSource.onerror = function(error) { console.error('EventSource 错误:', error); currentEventSource.close(); }; } // ==================== 事件绑定 ==================== submitBtn.addEventListener('click', async () => { const message = messageInput.value.trim(); if (!message) return; if (!currentChatId) { await createNewChat(); } appendMessage(message, false); messageInput.value = ''; startEventStream(message); }); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submitBtn.click(); } }); newChatBtn.addEventListener('click', createNewChat); toggleSidebarBtn.addEventListener('click', () => { sidebar.classList.toggle('-translate-x-full'); updateSidebarIcon(); }); function updateSidebarIcon() { const iconPath = document.getElementById('sidebarIconPath'); if (sidebar.classList.contains('-translate-x-full')) { iconPath.setAttribute('d', 'M4 6h16M4 12h4m12 0h-4M4 18h16'); } else { iconPath.setAttribute('d', 'M4 6h16M4 12h16M4 18h16'); } } // ==================== 初始化 ==================== // 初始化聊天列表 updateChatList(); // 加载当前聊天 if (currentChatId) { loadChat(currentChatId); } // 响应式处理 window.addEventListener('resize', () => { if (window.innerWidth > 768) { sidebar.classList.remove('-translate-x-full'); } else { sidebar.classList.add('-translate-x-full'); } }); if (window.innerWidth <= 768) { sidebar.classList.add('-translate-x-full'); } updateSidebarIcon(); /** ------------------------- * 上传知识下拉菜单(保持你现有 HTML 结构) * - 点击按钮展开/收起菜单 * - 点击外部区域/按下 Esc 关闭 * - 点击菜单项后自动关闭 * ------------------------- */ (function initUploadMenu() { const uploadMenuButton = document.getElementById('uploadMenuButton'); const uploadMenu = document.getElementById('uploadMenu'); if (!uploadMenuButton || !uploadMenu) return; // 切换菜单显示/隐藏 const toggleMenu = () => { uploadMenu.classList.toggle('hidden'); }; // 显示菜单 const openMenu = () => { uploadMenu.classList.remove('hidden'); }; // 隐藏菜单 const closeMenu = () => { uploadMenu.classList.add('hidden'); }; // 点击按钮展开/收起 uploadMenuButton.addEventListener('click', (e) => { e.stopPropagation(); toggleMenu(); }); // 键盘辅助:Enter/Space 展开,Esc 关闭 uploadMenuButton.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openMenu(); } else if (e.key === 'Escape') { closeMenu(); } }); // 点击菜单项后自动关闭 uploadMenu.querySelectorAll('a').forEach(a => { a.addEventListener('click', () => closeMenu()); }); // 点击外部任意区域关闭 document.addEventListener('click', (e) => { if (!uploadMenu.classList.contains('hidden')) { if (!uploadMenu.contains(e.target) && e.target !== uploadMenuButton) { closeMenu(); } } }); // Esc 关闭(全局) document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeMenu(); }); })();