// 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, // Tavily Search API tavilyApiUrl: 'https://api.tavily.com/search', tavilyApiKey: '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' }; // 数据结构 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 ? '
暂无对话记录
' : sortConversations().map(conv => `
${conv.is_pinned ? '📌' : ''}
${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 searchToggleBtn = document.getElementById('searchToggleBtn'); const searchBar = document.getElementById('searchBar'); const searchInput = document.getElementById('searchInput'); const searchCloseBtn = document.getElementById('searchCloseBtn'); const searchResults = document.getElementById('searchResults'); if (searchToggleBtn) { searchToggleBtn.addEventListener('click', () => { if (searchBar) { searchBar.classList.add('show'); if (searchInput) { searchInput.focus(); } } }); } if (searchCloseBtn) { searchCloseBtn.addEventListener('click', () => { hideSearchBar(); }); } if (searchInput) { searchInput.addEventListener('input', (e) => { const keyword = e.target.value.trim(); if (keyword) { searchConversations(keyword); } else { if (searchResults) searchResults.innerHTML = ''; } }); searchInput.addEventListener('keydown', (e) => { if (e.key === 'Escape') { hideSearchBar(); } }); } // 点击搜索结果 if (searchResults) { searchResults.addEventListener('click', (e) => { const item = e.target.closest('.search-result-item'); if (item) { const id = item.getAttribute('data-id'); hideSearchBar(); openConversation(id); } }); } function hideSearchBar() { if (searchBar) { searchBar.classList.remove('show'); } if (searchInput) { searchInput.value = ''; } if (searchResults) { searchResults.innerHTML = ''; } } const conversationList = document.getElementById('conversationList'); const actionMenu = document.getElementById('actionMenu'); let longPressTimer = null; let currentActionConvId = null; if (conversationList) { // 点击事件 conversationList.addEventListener('click', (e) => { const item = e.target.closest('.conversation-item'); if (item) { const id = item.getAttribute('data-id'); openConversation(id); } }); // 长按事件 conversationList.addEventListener('touchstart', (e) => { const item = e.target.closest('.conversation-item'); if (item) { longPressTimer = setTimeout(() => { currentActionConvId = item.getAttribute('data-id'); showActionMenu(currentActionConvId); }, 500); // 500ms长按 } }); conversationList.addEventListener('touchend', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); conversationList.addEventListener('touchmove', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); // 鼠标长按(PC端) conversationList.addEventListener('mousedown', (e) => { const item = e.target.closest('.conversation-item'); if (item) { longPressTimer = setTimeout(() => { currentActionConvId = item.getAttribute('data-id'); showActionMenu(currentActionConvId); }, 500); } }); conversationList.addEventListener('mouseup', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); conversationList.addEventListener('mouseleave', () => { if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; } }); } // 操作菜单事件 if (actionMenu) { actionMenu.addEventListener('click', (e) => { const item = e.target.closest('.action-menu-item'); if (item && currentActionConvId) { const action = item.getAttribute('data-action'); handleActionMenuAction(action, currentActionConvId); hideActionMenu(); } }); // 点击其他地方关闭菜单 document.addEventListener('click', (e) => { if (actionMenu.classList.contains('show') && !actionMenu.contains(e.target)) { hideActionMenu(); } }); } } // 排序对话(置顶在前) function sortConversations() { return [...conversations].sort((a, b) => { // 置顶优先 if (a.is_pinned && !b.is_pinned) return -1; if (!a.is_pinned && b.is_pinned) return 1; // 然后按更新时间 return b.updatedAt - a.updatedAt; }); } // 搜索对话 function searchConversations(keyword) { const searchResults = document.getElementById('searchResults'); if (!searchResults) return; keyword = keyword.toLowerCase(); // 搜索标题和消息内容 const results = conversations.filter(conv => { // 搜索标题 if (conv.title.toLowerCase().includes(keyword)) return true; // 搜索消息内容 if (conv.messages.some(m => m.content.toLowerCase().includes(keyword))) return true; return false; }); if (results.length === 0) { searchResults.innerHTML = '
未找到相关对话
'; return; } searchResults.innerHTML = results.map(conv => { // 找到匹配的消息片段 let matchSnippet = ''; const matchedMsg = conv.messages.find(m => m.content.toLowerCase().includes(keyword)); if (matchedMsg) { const content = matchedMsg.content; const idx = content.toLowerCase().indexOf(keyword); const start = Math.max(0, idx - 30); const end = Math.min(content.length, idx + keyword.length + 30); matchSnippet = (start > 0 ? '...' : '') + content.slice(start, end) + (end < content.length ? '...' : ''); } return `
${conv.is_pinned ? '📌' : ''}
${escapeHtml(conv.title)}
${matchSnippet ? `
${escapeHtml(matchSnippet)}
` : ''}
${conv.messages.length} 条消息 · ${formatTime(conv.updatedAt)}
`; }).join(''); } // 显示操作菜单 function showActionMenu(convId) { const actionMenu = document.getElementById('actionMenu'); const conv = conversations.find(c => c.id === convId); if (!actionMenu || !conv) return; // 更新置顶按钮文字 const pinText = document.getElementById('pinText'); if (pinText) { pinText.textContent = conv.is_pinned ? '取消置顶' : '置顶'; } // 显示菜单 actionMenu.classList.add('show'); } // 隐藏操作菜单 function hideActionMenu() { const actionMenu = document.getElementById('actionMenu'); if (actionMenu) { actionMenu.classList.remove('show'); } } // 处理操作菜单动作 function handleActionMenuAction(action, convId) { const conv = conversations.find(c => c.id === convId); if (!conv) return; switch (action) { case 'rename': renameConversation(convId); break; case 'share': shareConversation(convId); break; case 'pin': togglePinConversation(convId); break; case 'delete': deleteConversation(convId); break; } } // 重命名对话 function renameConversation(convId) { const conv = conversations.find(c => c.id === convId); if (!conv) return; const newTitle = prompt('请输入新的对话标题:', conv.title); if (newTitle && newTitle.trim() && newTitle !== conv.title) { conv.title = newTitle.trim(); conv.updatedAt = Date.now(); saveConversations(); showConversationList(); showToast('已重命名'); } } // 分享对话 function shareConversation(convId) { const conv = conversations.find(c => c.id === convId); if (!conv) return; // 构建分享内容 const shareContent = `【${conv.title}】\n\n${conv.messages.map(m => `${m.role === 'user' ? '👤 用户' : '🤖 AI'}: ${m.content}` ).join('\n\n')}`; // 复制到剪贴板 try { const textarea = document.createElement('textarea'); textarea.value = shareContent; textarea.style.position = 'fixed'; textarea.style.top = '0'; textarea.style.left = '0'; textarea.style.opacity = '0'; document.body.appendChild(textarea); textarea.select(); document.execCommand('copy'); document.body.removeChild(textarea); showToast('对话已复制到剪贴板'); } catch (err) { showToast('分享失败'); } } // 置顶/取消置顶对话 function togglePinConversation(convId) { const conv = conversations.find(c => c.id === convId); if (!conv) return; conv.is_pinned = !conv.is_pinned; conv.updatedAt = Date.now(); saveConversations(); showConversationList(); showToast(conv.is_pinned ? '已置顶' : '已取消置顶'); } // 创建新对话 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 (searchBtn) { searchBtn.addEventListener('click', () => { enableSearch = !enableSearch; searchBtn.classList.toggle('active', enableSearch); }); } // 绑定置顶置底按钮事件 const scrollTopBtn = document.getElementById('scrollTopBtn'); const scrollBottomBtn = document.getElementById('scrollBottomBtn'); if (scrollTopBtn) { scrollTopBtn.addEventListener('click', () => { if (messagesContainer) { messagesContainer.scrollTo({ top: 0, behavior: 'smooth' }); } }); } if (scrollBottomBtn) { scrollBottomBtn.addEventListener('click', () => { if (messagesContainer) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: 'smooth' }); } }); } // 绑定输入事件 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; const userMessage = currentConversation.messages[userMsgIndex]; // 如果开启联网搜索,先执行搜索 let searchResults = null; if (enableSearch && userMessage.role === 'user') { searchResults = await performSearch(userMessage.content); } // 只有开启深度思考时才添加 thinking 字段,开启搜索时添加 search_results 字段 currentConversation.messages.push({ role: 'assistant', content: '', ...(enableThinking ? { thinking: '' } : {}), ...(searchResults ? { search_results: searchResults } : {}) }); renderMessages(); const lastMessageEl = messagesDiv.lastElementChild; const contentEl = lastMessageEl.querySelector('.message-content'); const thinkingEl = lastMessageEl.querySelector('.thinking-content'); // 深度思考模式:思考块默认展开 if (enableThinking && thinkingEl) { const thinkingBlock = lastMessageEl.querySelector('.thinking-block'); if (thinkingBlock) thinkingBlock.classList.add('expanded'); thinkingEl.innerHTML = '思考中...'; } contentEl.innerHTML = ''; // 显示停止生成按钮 showStopGenerateBtn(); try { // 构建消息数组 let messagesToSend = currentConversation.messages.slice(0, aiMessageIndex).map(m => ({ role: m.role, content: m.content })); // 如果有搜索结果,将搜索内容添加到消息中 if (searchResults) { const searchContext = formatSearchResultsForLLM(searchResults); messagesToSend.push({ role: 'system', content: `以下是搜索结果,请根据这些信息回答用户问题:\n\n${searchContext}` }); } // 构建请求体 - 统一使用 glm-4.5-air,通过 thinking 参数控制 const requestBody = { model: CONFIG.model, messages: messagesToSend, max_tokens: CONFIG.maxTokens, stream: true, thinking: { type: enableThinking ? 'enabled' : 'disabled' } }; const response = await fetch(CONFIG.apiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.apiKey}` }, body: JSON.stringify(requestBody) }); if (!response.ok) { throw new Error(`API 错误: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; let thinkingOutputStarted = false; // 正式内容是否开始输出 let abortController = new AbortController(); // 用于中断流 // 绑定停止按钮事件 const stopBtn = document.getElementById('stopGenerateBtn'); if (stopBtn) { stopBtn.onclick = () => { abortController.abort(); isLoading = false; sendBtn.disabled = false; hideStopGenerateBtn(); // 更新最终内容 if (thinkingEl && enableThinking && currentConversation.messages[aiMessageIndex].thinking) { thinkingEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].thinking); } contentEl.innerHTML = renderMarkdown(currentConversation.messages[aiMessageIndex].content); currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); }; } while (true) { if (abortController.signal.aborted) break; // 检查是否已停止 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 (enableThinking && (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.contains('expanded')) { thinkingBlock.classList.add('expanded'); } scrollToBottom(); } // 处理正式回复内容 if (delta.content) { // 如果开启深度思考且开始输出正式内容,说明思考完成,立即折叠思考块 if (enableThinking && !thinkingOutputStarted && currentConversation.messages[aiMessageIndex].thinking) { thinkingOutputStarted = 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 && enableThinking && 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; hideStopGenerateBtn(); currentConversation.updatedAt = Date.now(); saveConversations(); renderMessages(); // 自动总结标题:第一次对话和每隔5次对话 const totalMessages = currentConversation.messages.length; if (totalMessages === 1 || totalMessages % 5 === 0) { await generateConversationTitle(); } } } // 执行 Tavily 搜索 async function performSearch(query) { try { const response = await fetch(CONFIG.tavilyApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${CONFIG.tavilyApiKey}` }, body: JSON.stringify({ query: query, max_results: 10, include_raw_content: false }) }); if (!response.ok) { console.error('搜索失败:', response.status); return null; } const data = await response.json(); return data.results || []; } catch (error) { console.error('搜索错误:', error); return null; } } // 格式化搜索结果给 LLM function formatSearchResultsForLLM(results) { if (!results || results.length === 0) return '无搜索结果'; return results.map((r, i) => `${i + 1}. 【${r.title}】\n来源: ${r.url}\n摘要: ${r.content || '无摘要'}\n` ).join('\n'); } // 显示停止生成按钮 function showStopGenerateBtn() { // 检查是否已存在 if (document.getElementById('stopGenerateBtn')) return; const stopBtn = document.createElement('button'); stopBtn.id = 'stopGenerateBtn'; stopBtn.className = 'stop-generate-btn'; stopBtn.innerHTML = ` 停止生成 `; // 插入到消息容器底部 if (messagesContainer) { messagesContainer.appendChild(stopBtn); } } // 隐藏停止生成按钮 function hideStopGenerateBtn() { const stopBtn = document.getElementById('stopGenerateBtn'); if (stopBtn) { stopBtn.remove(); } } // 生成对话标题 async function generateConversationTitle() { if (!currentConversation) return; // 构建对话摘要 const conversationText = currentConversation.messages.map(m => `${m.role === 'user' ? '用户' : 'AI'}: ${m.content.slice(0, 200)}` ).join('\n'); const titlePrompt = `请用不超过10个字总结以下对话的主题,只输出标题,不要其他内容: ${conversationText}`; 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: [{ role: 'user', content: titlePrompt }], max_tokens: 50 }) }); if (response.ok) { const data = await response.json(); const newTitle = data.choices?.[0]?.message?.content?.trim(); if (newTitle && newTitle.length > 0 && newTitle.length <= 15) { currentConversation.title = newTitle; currentConversation.updatedAt = Date.now(); saveConversations(); // 更新页面标题显示 const titleEl = document.querySelector('.header h1'); if (titleEl) { titleEl.textContent = newTitle; } } } } catch (error) { console.error('生成标题失败:', error); } } // 重新生成 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 && 'thinking' in msg) { // 判断是否是当前正在生成的消息(有thinking字段且正在加载) const isGenerating = index === currentConversation.messages.length - 1 && isLoading && enableThinking; // 思考进行中且没有正式内容时展开 const expandedClass = isGenerating && !msg.content ? 'expanded' : ''; thinkingHtml = `
思考过程
${renderMarkdown(msg.thinking || '思考中...')}
`; } // 搜索结果块(仅AI消息,放在思考块前面) let searchHtml = ''; if (!isUser && msg.search_results && msg.search_results.length > 0) { searchHtml = `
搜索结果 (${msg.search_results.length})
${msg.search_results.map((r, i) => ` `).join('')}
`; } const copyIcon = ``; const actions = isUser ? `
` : `
`; return `
${avatar}
${searchHtml} ${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'); } // 折叠/展开搜索结果 function toggleSearchResults(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(); } }