document.addEventListener('DOMContentLoaded', () => { const messageInput = document.getElementById('message-input'); const sendButton = document.getElementById('send-button'); const chatMessages = document.getElementById('chat-messages'); const logMessages = document.getElementById('log-messages'); const chatHistory = document.getElementById('chat-history'); const rightPanel = document.getElementById('right-panel'); const togglePanel = document.getElementById('toggle-panel'); const newChatButton = document.getElementById('new-chat'); let chatId = uuidv4(); let currentConversation = null; let abortController = null; // 用于取消请求 // 创建取消按钮 const cancelButton = document.createElement('button'); cancelButton.id = 'cancel-button'; cancelButton.className = 'w-8 h-8 rounded-full bg-red-500 text-white flex items-center justify-center hover:bg-red-600 hidden absolute left-1/2 -translate-x-1/2 -top-10'; cancelButton.innerHTML = ''; messageInput.parentElement.style.position = 'relative'; messageInput.parentElement.appendChild(cancelButton); // 取消按钮点击事件 cancelButton.addEventListener('click', () => { if (abortController) { abortController.abort(); abortController = null; } // 隐藏取消按钮,显示发送按钮 cancelButton.classList.add('hidden'); sendButton.classList.remove('hidden'); // 重新启用输入框 messageInput.disabled = false; sendButton.disabled = false; sendButton.classList.remove('opacity-50'); }); // 配置 marked marked.setOptions({ highlight: function(code, language) { if (Prism.languages[language]) { return Prism.highlight(code, Prism.languages[language], language); } return code; }, breaks: true, gfm: true }); // 处理消息内容的函数 function processMessageContent(content) { return content; // 直接返回原始内容 } // 添加复制按钮到代码块 function addCopyButtons() { // 只选择包含 code 标签的 pre 元素 document.querySelectorAll('pre code').forEach(code => { const pre = code.parentElement; if (pre.classList.contains('copy-button-added')) { return; } pre.classList.add('copy-button-added'); const button = document.createElement('button'); button.className = 'copy-button'; button.textContent = 'Copy'; button.addEventListener('click', async () => { try { await navigator.clipboard.writeText(code.textContent); button.textContent = 'Copied!'; button.classList.add('copied'); setTimeout(() => { button.textContent = 'Copy'; button.classList.remove('copied'); }, 2000); } catch (err) { console.error('Failed to copy:', err); button.textContent = 'Failed'; setTimeout(() => { button.textContent = 'Copy'; }, 2000); } }); pre.insertBefore(button, pre.firstChild); }); } // 面板控制 document.querySelectorAll('.panel-toggle').forEach(button => { button.addEventListener('click', () => { const target = document.getElementById(button.dataset.target); const panel = button.closest('.panel'); const icon = button.querySelector('svg'); if (target.id === 'task-content') { // Task panel 使用高度控制 if (panel.style.flex === '0 1 48px') { panel.style.flex = '1 1 60%'; icon.style.transform = 'rotate(180deg)'; } else { panel.style.flex = '0 1 48px'; icon.style.transform = 'rotate(0deg)'; } } else { // Log panel 高度收缩 if (panel.style.flex === '0 1 48px') { panel.style.flex = '1 1 300px'; icon.style.transform = 'rotate(180deg)'; } else { panel.style.flex = '0 1 48px'; icon.style.transform = 'rotate(0deg)'; } } }); }); // 右侧面板切换 togglePanel.addEventListener('click', () => { rightPanel.classList.toggle('w-0'); rightPanel.classList.toggle('w-[500px]'); rightPanel.classList.toggle('opacity-0'); rightPanel.classList.toggle('opacity-100'); }); // 新建对话 newChatButton.addEventListener('click', () => { chatId = uuidv4(); currentConversation = null; chatMessages.innerHTML = ''; messageInput.value = ''; // 创建新的历史记录项 const historyItem = document.createElement('div'); historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start'; historyItem.dataset.chatId = chatId; historyItem.innerHTML = `
Empty
ID: ${chatId.substring(0, 8)}...
`; // 添加删除按钮事件 const deleteButton = historyItem.querySelector('.delete-chat'); deleteButton.addEventListener('click', (e) => { e.stopPropagation(); deleteConversation(chatId, historyItem); }); // 添加点击事件 historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(chatId)); // 将新对话添加到列表顶部 if (chatHistory.firstChild) { chatHistory.insertBefore(historyItem, chatHistory.firstChild); } else { chatHistory.appendChild(historyItem); } highlightCurrentChat(); }); // 设置 SSE 日志监听 let isAutoScrollLog = true; function connectLogStream() { console.log('Connecting to log stream...'); const logSource = new EventSource('/agent/api/log'); logSource.onmessage = (event) => { const logMessage = event.data; const wasAtBottom = isAutoScrollLog; // 创建新的日志行 const logLine = document.createElement('div'); logLine.className = 'log-line'; logLine.textContent = logMessage; logMessages.appendChild(logLine); // 保持最新的1000行日志 const maxLogLines = 1000; while (logMessages.children.length > maxLogLines) { logMessages.removeChild(logMessages.firstChild); } // 如果之前在底部,则自动滚动到新消息 if (wasAtBottom) { logMessages.scrollTop = logMessages.scrollHeight; } }; logSource.onerror = (error) => { console.error('Log SSE Error:', error); logSource.close(); // 3秒后尝试重连 console.log('Reconnecting in 3 seconds...'); setTimeout(connectLogStream, 3000); }; logSource.onopen = () => { console.log('Log stream connected'); }; } // 监听日志面板的滚动事件 logMessages.addEventListener('scroll', () => { const scrollBottom = logMessages.scrollHeight - logMessages.clientHeight; isAutoScrollLog = Math.abs(scrollBottom - logMessages.scrollTop) < 2; }); // 初始连接 connectLogStream(); function highlightCurrentChat() { document.querySelectorAll('.chat-item').forEach(item => { item.classList.remove('bg-blue-50'); }); const currentItem = document.querySelector(`[data-chat-id="${chatId}"]`); if (currentItem) { currentItem.classList.add('bg-blue-50'); } } // 删除对话 async function deleteConversation(id, element) { try { const response = await fetch(`/agent/api/history?id=${id}`, { method: 'DELETE' }); if (response.ok) { element.remove(); if (id === chatId) { chatId = uuidv4(); currentConversation = null; chatMessages.innerHTML = ''; } } else { console.error('Failed to delete conversation'); } } catch (error) { console.error('Error deleting conversation:', error); } } // 加载对话 async function loadConversation(id) { try { const response = await fetch(`/agent/api/history?id=${id}`); const data = await response.json(); if (data.conversation) { chatId = id; currentConversation = data.conversation; chatMessages.innerHTML = ''; data.conversation.messages.forEach(msg => { appendMessage(msg.content, msg.role === 'user', false); }); highlightCurrentChat(); } } catch (error) { console.error('Error loading conversation:', error); } } // 添加消息到聊天区域 function appendMessage(content, isUser, animate = true) { const processedContent = processMessageContent(content); const messageDiv = document.createElement('div'); messageDiv.className = 'flex items-start gap-3 mb-4'; // 添加头像 const avatar = document.createElement('div'); avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0'; avatar.textContent = isUser ? '🧑‍💻' : '🤖'; messageDiv.appendChild(avatar); // 消息内容 const contentDiv = document.createElement('div'); contentDiv.className = `message markdown-body rounded-lg p-4 ${isUser ? 'bg-gray-100' : 'bg-gray-50'}`; if (!animate || isUser) { contentDiv.innerHTML = marked.parse(processedContent); addCopyButtons(); } else { const typingDiv = document.createElement('div'); contentDiv.appendChild(typingDiv); new Typed(typingDiv, { strings: [marked.parse(processedContent)], typeSpeed: 20, showCursor: false, onComplete: () => addCopyButtons() }); } messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } async function sendMessage() { const message = messageInput.value.trim(); if (!message) return; // 如果是新对话的第一条消息,更新历史记录标题 const historyItem = document.querySelector(`[data-chat-id="${chatId}"]`); if (historyItem && historyItem.querySelector('.font-medium').textContent === 'Empty') { historyItem.querySelector('.font-medium').textContent = message; } appendMessage(message, true); messageInput.value = ''; // 禁用输入框和发送按钮,显示取消按钮 messageInput.disabled = true; sendButton.disabled = true; sendButton.classList.add('opacity-50'); sendButton.classList.add('hidden'); cancelButton.classList.remove('hidden'); try { console.log('Starting chat with ID:', chatId); let hasReceivedMessage = false; let currentMessageDiv = null; let contentDiv = null; let accumulatedContent = ''; let isFirstChunk = true; let lastRenderTime = 0; // 创建新的 AbortController abortController = new AbortController(); // 使用 fetch 替代 EventSource,添加 signal const response = await fetch(`/agent/api/chat?id=${chatId}&message=${encodeURIComponent(message)}`, { signal: abortController.signal }); // 检查响应状态 if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; // 用于存储不完整的 SSE 消息 try { while (true) { const {value, done} = await reader.read(); if (done) break; // 解码新的数据块并添加到缓冲区 buffer += decoder.decode(value, {stream: true}); // 按行分割并处理每一行 const lines = buffer.split(/\r\n|\r|\n/); // 保留最后一个可能不完整的行 buffer = lines.pop() || ''; for (const line of lines) { // 解析 SSE 格式的行 if (line.startsWith('data:')) { // 保留 data: 后的所有内容,包括前导空格 const rawData = line.slice(5); // 直接截取 'data:' 后的内容 // console.log(`Raw SSE data: |${rawData}|`); hasReceivedMessage = true; // 如果是第一个 chunk,创建新的消息框 if (isFirstChunk) { const messageDiv = document.createElement('div'); messageDiv.className = 'flex items-start gap-3 mb-4'; // 添加头像 const avatar = document.createElement('div'); avatar.className = 'w-8 h-8 flex items-center justify-center rounded-full bg-gray-100 flex-shrink-0'; avatar.textContent = '🤖'; messageDiv.appendChild(avatar); // 消息内容 contentDiv = document.createElement('div'); contentDiv.className = 'message markdown-body rounded-lg p-4 bg-gray-50'; messageDiv.appendChild(contentDiv); chatMessages.appendChild(messageDiv); currentMessageDiv = contentDiv; isFirstChunk = false; accumulatedContent = rawData; } else { if (rawData === '') { // 如果是空数据,添加换行符 accumulatedContent += '\n'; } else { // 否则直接拼接数据 accumulatedContent += rawData; } } // 限制渲染频率 const now = Date.now(); if (now - lastRenderTime >= 100) { renderContent(); } else { clearTimeout(window.renderTimeout); window.renderTimeout = setTimeout(renderContent, 100 - (now - lastRenderTime)); } } } } function renderContent() { currentMessageDiv.innerHTML = marked.parse(accumulatedContent); addCopyButtons(); chatMessages.scrollTop = chatMessages.scrollHeight; lastRenderTime = Date.now(); } // 请求完成后,隐藏取消按钮,显示发送按钮 cancelButton.classList.add('hidden'); sendButton.classList.remove('hidden'); abortController = null; } finally { // 确保读取器被正确关闭 reader.cancel(); } } catch (error) { console.error('Error sending message:', error); if (error.name === 'AbortError') { } else { appendMessage('Error: Failed to send message. Please try again.', false); } abortController = null; } finally { messageInput.disabled = false; sendButton.disabled = false; sendButton.classList.remove('opacity-50'); sendButton.classList.remove('hidden'); cancelButton.classList.add('hidden'); } } sendButton.addEventListener('click', sendMessage); messageInput.addEventListener('keypress', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); // 加载历史对话列表 fetch('/agent/api/history') .then(response => response.json()) .then(data => { if (data.ids && data.ids.length > 0) { chatHistory.innerHTML = ''; // 清空现有历史 let firstConversationId = null; const loadPromises = data.ids.map(id => fetch(`/agent/api/history?id=${id}`) .then(response => response.json()) .then(convData => { if (convData.conversation) { const firstMessage = convData.conversation.messages.length > 0 ? convData.conversation.messages[0].content : 'Empty'; const historyItem = document.createElement('div'); historyItem.className = 'chat-item p-3 hover:bg-gray-100 cursor-pointer rounded-lg mb-2 transition-colors flex justify-between items-start'; historyItem.dataset.chatId = id; historyItem.innerHTML = `
${firstMessage}
ID: ${id.substring(0, 8)}...
`; const deleteButton = historyItem.querySelector('.delete-chat'); deleteButton.addEventListener('click', (e) => { e.stopPropagation(); deleteConversation(id, historyItem); }); historyItem.querySelector('.flex-1').addEventListener('click', () => loadConversation(id)); chatHistory.appendChild(historyItem); // 记录第一个对话的ID if (firstConversationId === null) { firstConversationId = id; } } }) ); // 等待所有历史记录加载完成后,加载第一个对话 Promise.all(loadPromises).then(() => { if (firstConversationId) { loadConversation(firstConversationId); } }); } }) .catch(error => console.error('Error loading history:', error)); });