// AI助手 - 前端应用 // 使用智谱 GLM-4.5-Air 模型(流式输出 + 多对话管理) const CONFIG = { apiUrl: 'https://open.bigmodel.cn/api/paas/v4/chat/completions', apiKey: '2259e33a1357460abe17919aaf81e73d.K44a8LPQTmFM5PKm', model: 'glm-4.5-air', maxTokens: 2048 }; // 数据结构 let conversations = []; // 对话列表 let currentConversation = null; // 当前对话 let isLoading = false; // DOM 元素(初始为 null,在 openConversation 时重新获取) let appContainer = null; let messagesContainer = null; let messagesDiv = null; let userInput = null; let sendBtn = null; let welcome = null; // 初始化 document.addEventListener('DOMContentLoaded', () => { // 初始化 appContainer appContainer = document.getElementById('app'); // 从本地存储加载对话列表 const saved = localStorage.getItem('conversations'); if (saved) { conversations = JSON.parse(saved); } // 兼容旧数据格式(chat_history) const oldHistory = localStorage.getItem('chat_history'); if (oldHistory && conversations.length === 0) { const oldMessages = JSON.parse(oldHistory); if (oldMessages.length > 0) { // 转换旧数据为新格式 const convertedConv = { id: Date.now().toString(), title: oldMessages[0].content.slice(0, 30) + (oldMessages[0].content.length > 30 ? '...' : ''), messages: oldMessages, createdAt: Date.now(), updatedAt: Date.now() }; conversations.push(convertedConv); saveConversations(); localStorage.removeItem('chat_history'); // 清理旧数据 } } // 显示对话列表页面 showConversationList(); }); // ==================== 对话列表页面 ==================== function showConversationList() { currentConversation = null; // 渲染对话列表 const listHtml = `

AI助手

${conversations.length === 0 ? '
暂无对话记录
' : conversations.map(conv => `
${escapeHtml(conv.title)}
${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}
`).join('') }
`; appContainer.innerHTML = listHtml; // 绑定事件 const newChatBtn = document.getElementById('newChatBtn'); if (newChatBtn) { newChatBtn.addEventListener('click', createNewConversation); } const conversationList = document.getElementById('conversationList'); if (conversationList) { conversationList.addEventListener('click', (e) => { const item = e.target.closest('.conversation-item'); const deleteBtn = e.target.closest('.conv-delete-btn'); if (deleteBtn) { e.stopPropagation(); const id = deleteBtn.getAttribute('data-id'); deleteConversation(id); } else if (item) { const id = item.getAttribute('data-id'); openConversation(id); } }); } } // 创建新对话 function createNewConversation() { const newConv = { id: Date.now().toString(), title: '新对话', messages: [], createdAt: Date.now(), updatedAt: Date.now() }; conversations.unshift(newConv); saveConversations(); openConversation(newConv.id); } // 打开对话 function openConversation(id) { currentConversation = conversations.find(c => c.id === id); if (!currentConversation) { showConversationList(); return; } // 渲染对话页面 const chatHtml = `

${escapeHtml(currentConversation.title)}

👋

你好!我是AI助手

有什么可以帮助你的吗?

`; appContainer.innerHTML = chatHtml; // 重新获取 DOM 元素 messagesContainer = document.getElementById('messagesContainer'); messagesDiv = document.getElementById('messages'); userInput = document.getElementById('userInput'); sendBtn = document.getElementById('sendBtn'); welcome = document.getElementById('welcome'); // 绑定按钮事件 const backBtn = document.getElementById('backBtn'); if (backBtn) backBtn.addEventListener('click', showConversationList); const clearBtn = document.getElementById('clearBtn'); if (clearBtn) clearBtn.addEventListener('click', clearCurrentChat); // 绑定输入事件 userInput.addEventListener('keydown', handleKeyDown); userInput.addEventListener('input', (e) => autoResize(e.target)); sendBtn.addEventListener('click', sendMessage); // 绑定快捷按钮事件 document.querySelectorAll('.quick-btn').forEach(btn => { btn.addEventListener('click', () => { const text = btn.getAttribute('data-text'); userInput.value = text; sendMessage(); }); }); // 渲染消息 renderMessages(); userInput.focus(); } // 删除对话 function deleteConversation(id) { if (!confirm('确定要删除这个对话吗?')) return; conversations = conversations.filter(c => c.id !== id); saveConversations(); showConversationList(); } // ==================== 对话页面 ==================== // 自动调整输入框高度 function autoResize(textarea) { textarea.style.height = 'auto'; textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px'; } // 处理键盘事件 function handleKeyDown(event) { if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage(); } } // 发送快捷消息 function sendQuickMessage(text) { userInput.value = text; sendMessage(); } // 发送消息(流式输出) async function sendMessage() { if (!currentConversation) return; const text = userInput.value.trim(); if (!text || isLoading) return; // 隐藏欢迎界面 welcome.style.display = 'none'; // 添加用户消息 currentConversation.messages.push({ role: 'user', content: text }); // 更新对话标题(第一条用户消息) if (currentConversation.title === '新对话') { currentConversation.title = text.slice(0, 30) + (text.length > 30 ? '...' : ''); // 更新标题显示 const titleEl = document.querySelector('.header h1'); if (titleEl) { titleEl.textContent = currentConversation.title; } } currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); userInput.value = ''; autoResize(userInput); // 调用流式生成 await streamGenerate(currentConversation.messages.length - 1); } // 流式生成 AI 回复 async function streamGenerate(userMsgIndex) { isLoading = true; sendBtn.disabled = true; const aiMessageIndex = currentConversation.messages.length; currentConversation.messages.push({ role: 'assistant', content: '' }); renderMessages(); const lastMessageEl = messagesDiv.lastElementChild; const contentEl = lastMessageEl.querySelector('.message-content'); contentEl.innerHTML = ''; try { const response = await fetch(CONFIG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify({ model: CONFIG.model, messages: currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ role: m.role, content: m.content })), max_tokens: CONFIG.maxTokens, stream: true }) }); if (!response.ok) { throw new Error(`API 错误: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (line.startsWith('data: ')) { const jsonStr = line.slice(6).trim(); if (jsonStr === '[DONE]') continue; try { const data = JSON.parse(jsonStr); if (data.choices && data.choices[0]?.delta?.content) { currentConversation.messages[aiMessageIndex].content += data.choices[0].delta.content; contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + ''; scrollToBottom(); } } catch (e) {} } } } contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } catch (error) { console.error('Error:', error); currentConversation.messages[aiMessageIndex].content = `抱歉,出现了错误:${error.message}\n\n请检查网络连接后重试。`; contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } finally { isLoading = false; sendBtn.disabled = false; currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); } } // 重新生成 AI 回复 async function regenerate(index) { if (!currentConversation || isLoading || index < 1) return; const userMsgIndex = index - 1; if (currentConversation.messages[userMsgIndex].role !== 'user') return; currentConversation.messages.splice(index, 1); currentConversation.updatedAt = Date.now(); saveConversations(); await streamGenerate(userMsgIndex); } // 删除消息 function deleteMessage(index) { if (!currentConversation || isLoading) return; const msg = currentConversation.messages[index]; if (msg.role === 'assistant') { if (index > 0 && currentConversation.messages[index - 1].role === 'user') { currentConversation.messages.splice(index - 1, 2); } else { currentConversation.messages.splice(index, 1); } } else { if (index < currentConversation.messages.length - 1 && currentConversation.messages[index + 1].role === 'assistant') { currentConversation.messages.splice(index, 2); } else { currentConversation.messages.splice(index, 1); } } currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); if (currentConversation.messages.length === 0) { welcome.style.display = 'block'; } } // 复制消息(复制原文) function copyMessage(index) { if (!currentConversation) return; const content = currentConversation.messages[index].content; navigator.clipboard.writeText(content).then(() => { showToast('已复制到剪贴板'); }).catch(err => { const textarea = document.createElement('textarea'); textarea.value = content; textarea.style.position = 'fixed'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToast('已复制到剪贴板'); }); } // 清空当前对话 function clearCurrentChat() { if (!currentConversation) return; if (confirm('确定要清空当前对话吗?')) { currentConversation.messages = []; currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); welcome.style.display = 'block'; } } // 渲染消息 function renderMessages() { if (!currentConversation) return; // 根据消息数量显示/隐藏欢迎界面 if (welcome) { welcome.style.display = currentConversation.messages.length > 0 ? 'none' : 'block'; } messagesDiv.innerHTML = currentConversation.messages.map((msg, index) => { const isUser = msg.role === 'user'; const avatar = isUser ? '👤' : '🤖'; const content = renderMarkdown(msg.content); const copyIcon = ``; const actions = isUser ? `
` : `
`; return `
${avatar}
${content}
${actions}
`; }).join(''); // 绑定消息操作按钮事件(事件委托) messagesDiv.querySelectorAll('.copy-btn').forEach(btn => { btn.addEventListener('click', () => copyMessage(parseInt(btn.dataset.index))); }); messagesDiv.querySelectorAll('.regenerate-btn').forEach(btn => { btn.addEventListener('click', () => regenerate(parseInt(btn.dataset.index))); }); messagesDiv.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => deleteMessage(parseInt(btn.dataset.index))); }); scrollToBottom(); } // ==================== 工具函数 ==================== // 渲染 Markdown function renderMarkdown(text) { if (!text) return ''; marked.setOptions({ breaks: true, gfm: true }); return marked.parse(text); } // 滚动到底部 function scrollToBottom() { if (messagesContainer) { messagesContainer.scrollTop = messagesContainer.scrollHeight; } } // 保存对话列表 function saveConversations() { localStorage.setItem('conversations', JSON.stringify(conversations)); } // 显示提示 function showToast(message) { const toast = document.createElement('div'); toast.className = 'toast'; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.classList.add('show'), 10); setTimeout(() => { toast.classList.remove('show'); setTimeout(() => document.body.removeChild(toast), 300); }, 2000); } // HTML转义 function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // 格式化时间 function formatTime(timestamp) { const date = new Date(timestamp); const now = new Date(); const diff = now - date; if (diff < 60000) return '刚刚'; if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前'; if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前'; if (diff < 604800000) return Math.floor(diff / 86400000) + '天前'; return date.toLocaleDateString('zh-CN'); } // PWA 注册 if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js').catch(() => {}); }