const chatLog = document.getElementById('chatLog'); const userInput = document.getElementById('userInput'); const sendBtn = document.getElementById('sendBtn'); const statusIndicator = document.getElementById('statusIndicator'); const conversationList = document.getElementById('conversationList'); const newChatBtn = document.getElementById('newChat'); const toggleSidebarBtn = document.getElementById('toggleSidebar'); let currentConversationId = null; let isStreaming = false; function setStatus(text) { statusIndicator.textContent = text; } function clearChat() { chatLog.innerHTML = ''; } const loaderHTML = ` . . . `; const doneHTML = `搜索完成`; const escapeHTML = (s) => s.replace(/&/g, '&').replace(//g, '>'); const withBr = (s) => escapeHTML(s).replace(/\n/g, '
'); function normalizeText(text) { if (!text) return ''; // 去掉首尾空白,压缩多余空行 let t = text.replace(/^\s+/, '').replace(/\s+$/, ''); t = t.replace(/\n{3,}/g, '\n\n'); // 连续3行以上压缩为1个空行 return t; } function setAssistantText(bubble, text, position = 'before') { const cls = position === 'after' ? '.assistant-after' : '.assistant-before'; const el = bubble.querySelector(cls); if (el) el.innerHTML = withBr(normalizeText(text || '')); } function setToolState(bubble, state) { const el = bubble.querySelector('.assistant-tool'); if (!el) return; if (state === 'searching') { el.innerHTML = `
${loaderHTML}
`; } else if (state === 'done') { el.innerHTML = `
${doneHTML}
`; } else { el.innerHTML = ''; } } function createBubble(role, content = '') { const div = document.createElement('div'); div.className = `message ${role}`; if (role === 'assistant') { div.innerHTML = '
'; setAssistantText(div, content, 'before'); } else { div.textContent = content; } chatLog.appendChild(div); chatLog.scrollTop = chatLog.scrollHeight; return div; } async function loadConversations() { const res = await fetch('/api/conversations'); const data = await res.json(); conversationList.innerHTML = ''; data.forEach(item => { const li = document.createElement('li'); li.textContent = item.title; li.dataset.id = item.id; if (item.id === currentConversationId) li.classList.add('active'); li.onclick = () => loadConversation(item.id); conversationList.appendChild(li); }); } async function loadConversation(id) { const res = await fetch(`/api/conversations/${id}`); if (!res.ok) return; const data = await res.json(); currentConversationId = data.id; clearChat(); const msgs = data.messages || []; for (let i = 0; i < msgs.length; i++) { const msg = msgs[i]; if (msg.role === 'tool') continue; // 处理带 tool_calls 的助手消息:组合“前置文字 + 搜索状态 + 完成后文字” if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length) { let beforeText = msg.content || ''; let afterText = ''; // 查找后续的 tool 结果和下一条 assistant 回复 let j = i + 1; while (j < msgs.length && msgs[j].role === 'tool') j++; if (j < msgs.length && msgs[j].role === 'assistant' && !msgs[j].tool_calls) { afterText = msgs[j].content || ''; // 跳过已消费的助手消息 i = j; } const bubble = createBubble('assistant', beforeText); setToolState(bubble, 'done'); setAssistantText(bubble, afterText, 'after'); continue; } createBubble(msg.role, msg.content || ''); } await loadConversations(); } async function sendMessage() { const text = userInput.value.trim(); if (!text || isStreaming) return; isStreaming = true; sendBtn.disabled = true; userInput.value = ''; createBubble('user', text); const assistantBubble = createBubble('assistant', ''); setStatus('生成中'); try { const response = await fetch('/api/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_id: currentConversationId, message: text }) }); if (!response.ok || !response.body) throw new Error('请求失败'); const newId = response.headers.get('X-Conversation-Id'); if (newId) currentConversationId = newId; const reader = response.body.getReader(); const decoder = new TextDecoder(); let beforeText = ''; let afterText = ''; let lineBuffer = ''; let toolStarted = false; let toolFinished = false; while (true) { const { value, done } = await reader.read(); if (done) break; const chunk = decoder.decode(value, { stream: true }); lineBuffer += chunk; while (true) { const nl = lineBuffer.indexOf('\n'); if (nl === -1) break; const line = lineBuffer.slice(0, nl); lineBuffer = lineBuffer.slice(nl + 1); if (!line.trim()) continue; let evt; try { evt = JSON.parse(line); } catch { continue; } if (evt.type === 'assistant_delta') { if (!toolStarted || !toolFinished) { beforeText += evt.delta || ''; setAssistantText(assistantBubble, beforeText, 'before'); } else { afterText += evt.delta || ''; setAssistantText(assistantBubble, afterText, 'after'); } } else if (evt.type === 'tool_call_start') { toolStarted = true; setToolState(assistantBubble, 'searching'); } else if (evt.type === 'tool_result') { toolFinished = true; setToolState(assistantBubble, 'done'); } } chatLog.scrollTop = chatLog.scrollHeight; } } catch (err) { setAssistantText(assistantBubble, '出错了,请重试'); setToolState(assistantBubble, null); console.error(err); } finally { isStreaming = false; sendBtn.disabled = false; setStatus('空闲'); await loadConversations(); } } sendBtn.addEventListener('click', sendMessage); userInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(); } }); newChatBtn.addEventListener('click', () => { currentConversationId = null; clearChat(); setStatus('空闲'); userInput.focus(); Array.from(conversationList.children).forEach(li => li.classList.remove('active')); }); toggleSidebarBtn.addEventListener('click', () => { document.querySelector('.sidebar').classList.toggle('open'); }); // 初始加载 loadConversations();