class ChatManager { constructor() { this.sessionId = null; this.messages = []; this.isLoading = false; this.clarificationRound = 0; this.maxClarificationRounds = 6; this.pendingFiles = []; this.conversationHistories = []; this.messageCountForNaming = 0; this.messagesContainer = document.getElementById('chat-messages'); this.messageInput = document.getElementById('message-input'); this.sendBtn = document.getElementById('send-btn'); this.newChatBtn = document.getElementById('new-chat-btn'); this.clearChatBtn = document.getElementById('clear-chat-btn'); this.historyList = document.getElementById('history-list'); this.init(); } init() { if (!api.token && !localStorage.getItem('access_token') && !this.getCookie('access_token')) { const loginModal = new bootstrap.Modal(document.getElementById('loginModal')); loginModal.show(); } this.sendBtn.addEventListener('click', () => this.sendMessage()); this.messageInput.addEventListener('keydown', (e) => this.handleKeyDown(e)); this.newChatBtn?.addEventListener('click', () => this.newChat()); this.clearChatBtn?.addEventListener('click', () => this.clearChat()); this.messageInput.addEventListener('input', () => this.autoResize()); this.loadHistory(); } getCookie(name) { const matches = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^; ]*)')); return matches ? decodeURIComponent(matches[1]) : null; } handleKeyDown(e) { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); this.sendMessage(); } } autoResize() { this.messageInput.style.height = 'auto'; this.messageInput.style.height = Math.min(this.messageInput.style.scrollHeight, 150) + 'px'; } async sendMessage() { const message = this.messageInput.value.trim(); if (!message || this.isLoading) return; if (!api.token && !localStorage.getItem('access_token')) { const loginModal = new bootstrap.Modal(document.getElementById('loginModal')); loginModal.show(); return; } this.messageInput.value = ''; this.messageInput.style.height = 'auto'; this.addMessage(message, 'user'); this.showTyping(); this.updateWorkStatus('working'); this.isLoading = true; this.messageCountForNaming++; try { const res = await api.sendChat(message, this.sessionId); this.hideTyping(); if (res.data) { this.sessionId = res.data.session_id; const data = res.data; this.updateStatus({ model: data.model || 'qwen3:14b', elapsed: data.elapsed_seconds || 0, messageCount: this.messages.length }); if (data.type === 'clarification') { this.handleClarification(data); } else if (data.type === 'blocked') { this.addMessage(data.message, 'bot', { warning: true }); } else if (data.type === 'async_task') { this.addMessage(data.message, 'bot', { agents: data.agents || [], elapsed: data.elapsed_seconds || 0 }); } else { this.addMessage(data.message, 'bot', { model: data.model || 'qwen3:14b', agents: data.agents || [], elapsed: data.elapsed_seconds || 0 }); } if (data.tool_calls) { this.handleToolCalls(data.tool_calls); } if (this.messageCountForNaming >= 5 && !this.sessionNameGenerated) { this.generateSessionName(); } } } catch (error) { this.hideTyping(); this.addMessage(`抱歉,发生了错误:${error.message}`, 'bot', { error: true }); this.updateWorkStatus('error'); } finally { this.isLoading = false; this.updateWorkStatus('idle'); } } generateSessionName() { const userMessages = this.messages.filter(m => m.role === 'user'); if (userMessages.length === 0) return; const firstMsg = userMessages[0].content; const summary = firstMsg.length > 20 ? firstMsg.substring(0, 20) + '...' : firstMsg; this.sessionNameGenerated = true; const existingIndex = this.conversationHistories.findIndex(h => h.sessionId === this.sessionId); if (existingIndex >= 0) { this.conversationHistories[existingIndex].name = summary; this.conversationHistories[existingIndex].preview = userMessages[userMessages.length - 1].content.substring(0, 30); } this.saveHistory(); this.renderHistoryList(); } updateStatus({ model, elapsed, messageCount }) { const modelDisplay = model ? this.getModelDisplayName(model) : 'qwen3:14b'; document.getElementById('current-model-name').textContent = modelDisplay; document.getElementById('active-model').textContent = modelDisplay; if (elapsed) { document.getElementById('response-time').textContent = elapsed.toFixed(1) + 's'; } if (messageCount) { document.getElementById('message-count').textContent = Math.ceil(messageCount / 2); } } updateWorkStatus(status) { const statusEl = document.getElementById('work-status'); statusEl.className = 'status-value'; switch (status) { case 'working': statusEl.textContent = '处理中'; statusEl.classList.add('working'); break; case 'idle': statusEl.textContent = '空闲'; statusEl.classList.add('idle'); break; case 'error': statusEl.textContent = '错误'; statusEl.classList.add('error'); break; } } handleClarification(data) { this.clarificationRound = data.round || 1; this.maxClarificationRounds = data.max_rounds || 6; const questions = data.clarification_questions || []; const needsFileUpload = data.needs_file_upload || false; const suggestedTypes = data.suggested_file_types || []; const message = data.message || ''; let clarificationHtml = `
需要更多信息 (${this.clarificationRound}/${this.maxClarificationRounds})
`; if (message) { clarificationHtml += `
${this.formatContent(message)}
`; } if (questions.length > 0) { clarificationHtml += '
'; questions.forEach((q, i) => { clarificationHtml += `
${i + 1}. ${q}
`; }); clarificationHtml += '
'; } if (needsFileUpload || suggestedTypes.length > 0) { clarificationHtml += `
${suggestedTypes.length > 0 ? `支持: ${suggestedTypes.join(', ')}` : ''}
`; } clarificationHtml += '
'; const messageDiv = document.createElement('div'); messageDiv.className = 'message bot-message'; messageDiv.innerHTML = `
${clarificationHtml}
`; this.messagesContainer.appendChild(messageDiv); this.scrollToBottom(); } showFileUploadDialog() { const modal = document.createElement('div'); modal.className = 'modal fade'; modal.id = 'fileUploadModal'; modal.innerHTML = ` `; document.body.appendChild(modal); const fileInput = document.getElementById('fileInput'); fileInput.addEventListener('change', (e) => { const file = e.target.files[0]; if (file) { this.previewFile(file); } }); const bsModal = new bootstrap.Modal(modal); modal.addEventListener('hidden.bs.modal', () => modal.remove()); bsModal.show(); this.currentUploadModal = bsModal; } previewFile(file) { const preview = document.getElementById('filePreview'); const fileName = document.getElementById('fileName'); const fileSize = document.getElementById('fileSize'); const fileIcon = document.getElementById('fileIcon'); if (file.size > 10 * 1024 * 1024) { alert('文件大小超过10MB限制'); return; } fileName.textContent = file.name; fileSize.textContent = `(${(file.size / 1024).toFixed(1)} KB)`; const ext = file.name.split('.').pop().toLowerCase(); if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) { fileIcon.className = 'fas fa-image'; } else if (['pdf'].includes(ext)) { fileIcon.className = 'fas fa-file-pdf'; } else if (['doc', 'docx'].includes(ext)) { fileIcon.className = 'fas fa-file-word'; } else if (['xls', 'xlsx', 'csv'].includes(ext)) { fileIcon.className = 'fas fa-file-excel'; } else if (['py', 'js', 'java', 'cpp', 'c'].includes(ext)) { fileIcon.className = 'fas fa-file-code'; } else { fileIcon.className = 'fas fa-file'; } preview.style.display = 'block'; this.pendingFile = file; } async uploadFile() { if (!this.pendingFile) { alert('请选择文件'); return; } if (this.pendingFile.size > 10 * 1024 * 1024) { alert('文件大小超过10MB限制'); return; } const progress = document.getElementById('uploadProgress'); progress.style.display = 'block'; this.pendingFiles.push({ name: this.pendingFile.name, size: this.pendingFile.size, type: this.pendingFile.type }); const reader = new FileReader(); reader.onload = () => { this.pendingFiles[this.pendingFiles.length - 1].content = reader.result; const botMessage = document.createElement('div'); botMessage.className = 'message bot-message'; botMessage.innerHTML = `
已上传: ${this.pendingFile.name} (${(this.pendingFile.size / 1024).toFixed(1)} KB)

文件已收到,请继续描述您的需求。

`; this.messagesContainer.appendChild(botMessage); this.scrollToBottom(); if (this.currentUploadModal) { this.currentUploadModal.hide(); } }; reader.readAsDataURL(this.pendingFile); } addMessage(content, role, options = {}) { const messageDiv = document.createElement('div'); messageDiv.className = `message ${role}-message`; if (options.error) { messageDiv.classList.add('error'); } if (options.warning) { messageDiv.classList.add('warning'); } const avatar = role === 'bot' ? '' : ''; const formattedContent = this.formatContent(content); let agentInfo = ''; if (role === 'bot' && options.agents && options.agents.length > 0) { const agentNames = options.agents.map(a => `${a.name || a.key}`).join(' + '); const elapsed = options.elapsed ? `${options.elapsed.toFixed(1)}s` : ''; agentInfo = `
参与智能体: ${agentNames} ${elapsed}
`; } else if (role === 'bot' && options.model) { const modelDisplay = this.getModelDisplayName(options.model); agentInfo = ` ${modelDisplay}`; } messageDiv.innerHTML = `
${avatar}
${agentInfo} ${formattedContent}
`; this.messagesContainer.appendChild(messageDiv); this.scrollToBottom(); this.messages.push({ role, content }); } getModelDisplayName(model) { const modelMap = { 'qwen3:14b': 'Qwen3-14B', 'qwen2.5-coder:14b-instruct-q5_K_M': 'Qwen-Coder', 'qwen3.5:9b': 'Qwen3.5-9B', 'qwen3.5:27b': 'Qwen3.5-27B', 'glm-4.7-flash': 'GLM-4.7', 'gemma4:latest': 'Gemma-4', }; return modelMap[model] || model.split(':')[0]; } formatContent(content) { if (!content) return '

'; let formatted = content; formatted = formatted.replace(/```(\w*)\n?([\s\S]*?)```/g, '
$2
'); formatted = formatted.replace(/`([^`]+)`/g, '$1'); formatted = formatted.replace(/\*\*([^*]+)\*\*/g, '$1'); formatted = formatted.replace(/\*([^*]+)\*/g, '$1'); formatted = formatted.replace(/\n/g, '
'); return `

${formatted}

`; } showTyping() { const typingDiv = document.createElement('div'); typingDiv.className = 'message bot-message typing'; typingDiv.id = 'typing-indicator'; typingDiv.innerHTML = `

正在思考

`; this.messagesContainer.appendChild(typingDiv); this.scrollToBottom(); } hideTyping() { const typing = document.getElementById('typing-indicator'); if (typing) { typing.remove(); } } handleToolCalls(toolCalls) { if (!Array.isArray(toolCalls)) return; for (const call of toolCalls) { const toolName = call.function?.name || call.name; this.addSystemMessage(` 调用工具: ${toolName}`); } } addSystemMessage(content) { const messageDiv = document.createElement('div'); messageDiv.className = 'message system-message'; messageDiv.innerHTML = `

${content}

`; this.messagesContainer.appendChild(messageDiv); this.scrollToBottom(); } scrollToBottom() { this.messagesContainer.scrollTop = this.messagesContainer.scrollHeight; } loadHistory() { try { const saved = localStorage.getItem('ai-chat-histories'); if (saved) { this.conversationHistories = JSON.parse(saved); this.renderHistoryList(); } } catch (e) { console.error('Failed to load history:', e); } } saveHistory() { try { localStorage.setItem('ai-chat-histories', JSON.stringify(this.conversationHistories)); } catch (e) { console.error('Failed to save history:', e); } } renderHistoryList() { if (!this.historyList) return; if (this.conversationHistories.length === 0) { this.historyList.innerHTML = `

暂无对话记录

`; return; } this.historyList.innerHTML = this.conversationHistories.map(h => `
${h.name || '新对话'}
${h.messageCount || 0} 条消息
`).join(''); } async loadConversation(sessionId) { if (this.sessionId === sessionId) return; this.sessionId = sessionId; this.messages = []; this.messagesContainer.innerHTML = ''; this.messageCountForNaming = 0; this.sessionNameGenerated = false; const history = this.conversationHistories.find(h => h.sessionId === sessionId); if (history && history.messages) { this.messages = history.messages; this.messages.forEach(msg => { this.addMessage(msg.content, msg.role); }); this.messageCountForNaming = history.messages.filter(m => m.role === 'user').length; if (this.messageCountForNaming >= 5) { this.sessionNameGenerated = true; } } try { const res = await api.getChatHistory(sessionId); if (res.data && res.data.messages) { this.messages = res.data.messages; this.messagesContainer.innerHTML = ''; this.messages.forEach(msg => { this.addMessage(msg.content, msg.role); }); } } catch (e) { console.error('Failed to load chat history:', e); } this.renderHistoryList(); } newChat() { if (this.sessionId && this.messages.length > 0) { const historyEntry = this.conversationHistories.find(h => h.sessionId === this.sessionId); if (historyEntry) { historyEntry.messages = [...this.messages]; historyEntry.messageCount = Math.ceil(this.messages.length / 2); this.saveHistory(); } } this.sessionId = null; this.messages = []; this.clarificationRound = 0; this.pendingFiles = []; this.messageCountForNaming = 0; this.sessionNameGenerated = false; this.messagesContainer.innerHTML = `

您好!我是AI助手,有什么可以帮助您的吗?

`; document.getElementById('response-time').textContent = '-'; document.getElementById('message-count').textContent = '0'; this.renderHistoryList(); } clearChat() { if (confirm('确定要清空当前对话吗?')) { this.messagesContainer.innerHTML = `

您好!我是AI助手,有什么可以帮助您的吗?

`; this.messages = []; this.messageCountForNaming = 0; this.sessionNameGenerated = false; document.getElementById('message-count').textContent = '0'; } } } let chatManager; document.addEventListener('DOMContentLoaded', () => { chatManager = new ChatManager(); });