// 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', thinkingModel: 'glm-z1-flash', // 智谱思考模型 maxTokens: 2048 }; // 数据结构 let conversations = []; // 对话列表 let currentConversation = null; // 当前对话 let isLoading = false; // 功能开关 let enableThinking = false; // 深度思考 let enableSearch = false; // 联网搜索 // DOM 元素(初始为 null,在 openConversation 时重新获取) let appContainer = null; let messagesContainer = null; let messagesDiv = null; let userInput = null; let sendBtn = null; let welcome = null; let thinkingBtn = null; let searchBtn = 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'); thinkingBtn = document.getElementById('thinkingBtn'); searchBtn = document.getElementById('searchBtn'); // 绑定按钮事件 const backBtn = document.getElementById('backBtn'); if (backBtn) backBtn.addEventListener('click', showConversationList); const clearBtn = document.getElementById('clearBtn'); if (clearBtn) clearBtn.addEventListener('click', clearCurrentChat); // 绑定功能开关按钮事件 if (thinkingBtn) { thinkingBtn.addEventListener('click', () => { enableThinking = !enableThinking; thinkingBtn.classList.toggle('active', enableThinking); // 如果开启深度思考,关闭联网搜索(智谱思考模型不支持联网) if (enableThinking && enableSearch) { enableSearch = false; searchBtn.classList.remove('active'); } }); } if (searchBtn) { searchBtn.addEventListener('click', () => { enableSearch = !enableSearch; searchBtn.classList.toggle('active', enableSearch); // 如果开启联网搜索,关闭深度思考 if (enableSearch && enableThinking) { enableThinking = false; thinkingBtn.classList.remove('active'); } }); } // 绑定输入事件 userInput.addEventListener('keydown', handleKeyDown); userInput.addEventListener('input', (e) => autoResize(e.target)); sendBtn.addEventListener('click', sendMessage); // 绑定上传按钮事件 const attachBtn = document.getElementById('attachBtn'); const attachPanel = document.getElementById('attachPanel'); const imageInput = document.getElementById('imageInput'); const fileInput = document.getElementById('fileInput'); if (attachBtn) { attachBtn.addEventListener('click', (e) => { e.stopPropagation(); // 阻止冒泡到 document attachPanel.classList.toggle('show'); }); } // 点击其他地方关闭面板 document.addEventListener('click', (e) => { if (attachPanel && attachPanel.classList.contains('show') && !attachPanel.contains(e.target) && !attachBtn.contains(e.target)) { attachPanel.classList.remove('show'); } }); // 上传选项点击 attachPanel.querySelectorAll('.attach-item').forEach(item => { item.addEventListener('click', () => { const type = item.getAttribute('data-type'); attachPanel.classList.remove('show'); if (type === 'image') { imageInput.click(); } else if (type === 'file') { fileInput.click(); } }); }); // 图片上传处理 imageInput.addEventListener('change', handleImageUpload); // 文件上传处理 fileInput.addEventListener('change', handleFileUpload); // 绑定快捷按钮事件 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: '', thinking: '' // 思考内容 }); renderMessages(); const lastMessageEl = messagesDiv.lastElementChild; const contentEl = lastMessageEl.querySelector('.message-content'); const thinkingEl = lastMessageEl.querySelector('.thinking-content'); contentEl.innerHTML = ''; if (thinkingEl) thinkingEl.innerHTML = ''; try { // 根据开关选择模型 const model = enableThinking ? CONFIG.thinkingModel : CONFIG.model; const response = await fetch(CONFIG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify({ model: 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 = ''; let thinkingComplete = false; // 思考是否完成 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); const delta = data.choices?.[0]?.delta; if (delta) { // 处理思考内容 if (delta.reasoning_content || delta.thinking) { const thinkingChunk = delta.reasoning_content || delta.thinking; currentConversation.messages[aiMessageIndex].thinking += thinkingChunk; if (thinkingEl) { thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking) + ''; // 思考时展开 const thinkingBlock = lastMessageEl.querySelector('.thinking-block'); if (thinkingBlock) thinkingBlock.classList.add('expanded'); } scrollToBottom(); } // 处理正式回复内容 if (delta.content) { // 如果开始输出正式内容,说明思考完成 if (currentConversation.messages[aiMessageIndex].thinking && !thinkingComplete) { thinkingComplete = true; // 折叠思考内容 const thinkingBlock = lastMessageEl.querySelector('.thinking-block'); if (thinkingBlock) thinkingBlock.classList.remove('expanded'); if (thinkingEl) thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking); } currentConversation.messages[aiMessageIndex].content += delta.content; contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content) + ''; scrollToBottom(); } } } catch (e) {} } } } // 最终渲染 if (thinkingEl && currentConversation.messages[aiMessageIndex].thinking) { thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking); } 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 msg = currentConversation.messages[index]; // 如果是图片消息,复制图片描述或提示 let content = msg.content; if (msg.image && content === '[图片]') { content = '[图片: ' + (msg.imageName || '未命名') + ']'; } // HTTP 环境下 navigator.clipboard 不工作,优先使用 fallback try { const textarea = document.createElement('textarea'); textarea.value = content; textarea.style.position = 'fixed'; textarea.style.top = '0'; textarea.style.left = '0'; textarea.style.opacity = '0'; textarea.style.pointerEvents = 'none'; document.body.appendChild(textarea); textarea.focus(); textarea.select(); const success = document.execCommand('copy'); document.body.removeChild(textarea); if (success) { showToast('已复制到剪贴板'); } else { showToast('复制失败,请手动复制'); } } catch (err) { console.error('复制失败:', err); 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 ? '👤' : '🤖'; // 处理消息内容(支持图片) let contentHtml = ''; if (msg.image) { // 图片消息 contentHtml = `
${msg.imageName || '图片'}
`; if (msg.content && msg.content !== '[图片]') { contentHtml += `
${renderMarkdown(msg.content)}
`; } } else { contentHtml = renderMarkdown(msg.content); } // 思考内容块(仅AI消息) let thinkingHtml = ''; if (!isUser && msg.thinking) { thinkingHtml = `
思考过程
${renderMarkdown(msg.thinking)}
`; } const copyIcon = ``; const actions = isUser ? `
` : `
`; return `
${avatar}
${thinkingHtml}
${contentHtml}
${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(); } // 折叠/展开思考内容 function toggleThinking(block) { block.classList.toggle('expanded'); } // ==================== 工具函数 ==================== // 渲染 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(() => {}); } // ==================== 文件上传处理 ==================== // 处理图片上传 async function handleImageUpload(e) { const file = e.target.files[0]; if (!file) return; // 读取图片为base64 const reader = new FileReader(); reader.onload = async (event) => { const base64 = event.target.result; // 添加用户消息(显示图片) currentConversation.messages.push({ role: 'user', content: '[图片]', image: base64, imageName: file.name }); currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); // 隐藏欢迎界面 if (welcome) welcome.style.display = 'none'; // 调用AI生成 await streamGenerateWithImage(base64, file.name); }; reader.readAsDataURL(file); // 清空input以便再次选择同一文件 e.target.value = ''; } // 处理文件上传 async function handleFileUpload(e) { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = async (event) => { const content = event.target.result; const fileName = file.name; // 添加用户消息 currentConversation.messages.push({ role: 'user', content: `[文件: ${fileName}]\\n\\n${content.slice(0, 500)}${content.length > 500 ? '...' : ''}` }); currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); if (welcome) welcome.style.display = 'none'; // 调用AI生成 await streamGenerateWithFile(content, fileName); }; // 根据文件类型读取 if (file.name.endsWith('.pdf') || file.name.endsWith('.doc') || file.name.endsWith('.docx')) { // PDF/Word文件暂时只显示文件名 showToast('PDF/Word文件暂不支持解析,请上传文本文件'); e.target.value = ''; return; } reader.readAsText(file); e.target.value = ''; } // 带图片的流式生成 async function streamGenerateWithImage(base64, imageName) { 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 messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => { if (m.image) { return { role: m.role, content: [ { type: 'image_url', image_url: { url: m.image } }, { type: 'text', text: '请分析这张图片' } ] }; } return { role: m.role, content: m.content }; }); const response = await fetch(CONFIG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify({ model: 'glm-4v-flash', // 视觉模型 messages: messages, 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 (err) {} } } } contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } catch (error) { console.error('Error:', error); currentConversation.messages[aiMessageIndex].content = `抱歉,图片分析失败:${error.message}`; contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } finally { isLoading = false; sendBtn.disabled = false; currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); } } // 带文件的流式生成 async function streamGenerateWithFile(content, fileName) { 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 messages = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ role: m.role, content: m.content })); // 添加文件内容作为系统提示 messages.unshift({ role: 'system', content: `以下是用户上传的文件内容,请根据内容回答问题:\\n文件名:${fileName}\\n内容:\\n${content}` }); const response = await fetch(CONFIG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify({ model: CONFIG.model, messages: messages, 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 (err) {} } } } contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } catch (error) { console.error('Error:', error); currentConversation.messages[aiMessageIndex].content = `抱歉,文件处理失败:${error.message}`; contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); } finally { isLoading = false; sendBtn.disabled = false; currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); } }