// chatbot-ui.js /** * 全局函数,强制聊天机器人界面弹出(或切换到)模型选择器。 * * 主要逻辑: * 1. 设置 `window.isModelSelectorOpen = true`。 * 2. 调用 `ChatbotUI.updateChatbotUI()` 刷新界面以显示模型选择器。 */ window.showModelSelectorForChatbot = function() { window.isModelSelectorOpen = true; if (typeof window.ChatbotUI === 'object' && typeof window.ChatbotUI.updateChatbotUI === 'function') { window.ChatbotUI.updateChatbotUI(); } }; // 全局状态变量 window.isChatbotPositionedLeft = localStorage.getItem('chatbotPosition') === 'left' || false; window.isPresetQuestionsCollapsed = false; // 预设问题默认展开 window.presetAutoCollapseTriggeredForDoc = {}; // 记录文档是否已触发自动收起 // 全屏和宽度管理状态 window.isChatbotFullscreen = localStorage.getItem('chatbotFullscreen') === 'true' || false; window.forceChatbotWidthReset = false; // 是否强制重置聊天窗口宽度 window.lastIsChunkCompareActive = undefined; // 上一次 Chunk Compare 标签页的激活状态 window.chatbotInitialLoad = true; // 是否为首次加载 // 浮动模式状态 window.isChatbotFloating = localStorage.getItem('chatbotFloating') === 'true' || false; window.chatbotFloatingPosition = JSON.parse(localStorage.getItem('chatbotFloatingPosition') || '{"x": 100, "y": 100}'); window.chatbotFloatingSize = JSON.parse(localStorage.getItem('chatbotFloatingSize') || '{"width": 420, "height": 580}'); // 高级聊天功能选项 window.chatbotActiveOptions = { useContext: true, // 是否使用上下文 useReActMode: false, // 是否启用ReAct框架(推理+工具调用) enableSemanticFeatures: true, // 是否启用意群和向量搜索功能(默认开启) multiHopRetrieval: false, // 是否启用多轮取材(先选片段再回答) contentLengthStrategy: 'default', // 内容长度策略: 'default', 'segmented' summarySource: 'ocr', // 总结来源: 'ocr', 'none', 'translation' interestPointsActive: false, // 兴趣点功能 (占位) memoryManagementActive: false // 记忆管理功能 (占位) }; /** * 处理暂停对话按钮的点击事件。 * * 主要逻辑: * 1. 调用中止控制器来停止正在进行的请求。 * 2. 更新UI状态。 */ function handleChatbotStop() { if (window.chatbotAbortController) { window.chatbotAbortController.abort(); console.log('[Chatbot] 用户中止了对话'); } } /** * 处理聊天机器人发送按钮的点击事件。 * * 主要逻辑: * 1. 获取输入框内容和已选图片。 * 2. 如果文本和图片均为空,则不发送。 * 3. 构造消息内容 (支持文本和图片混合)。 * 4. 若使用 PromptConstructor,则增强用户输入。 * 5. 清空输入框和已选图片预览。 * 6. 调用 `ChatbotCore.sendChatbotMessage` 发送消息。 */ function handleChatbotSend() { const input = document.getElementById('chatbot-input'); if (!input) return; let val = input.value.trim(); const selectedImages = window.ChatbotImageUtils.selectedChatbotImages || []; if (!val && selectedImages.length === 0) return; let messageContent = []; let displayMessageContent = []; // 用于UI显示,可能包含缩略图 if (val) { messageContent.push({ type: 'text', text: val }); displayMessageContent.push({ type: 'text', text: val }); } selectedImages.forEach(img => { if (img.fullBase64) { messageContent.push({ type: 'image_url', image_url: { url: img.fullBase64 } }); displayMessageContent.push({ type: 'image_url', image_url: { url: img.thumbnailBase64 || img.fullBase64, // 优先用缩略图显示 fullUrl: img.fullBase64, // 点击放大用原图 originalSrc: img.originalSrc } }); } }); // 兼容旧的单模态模型,如果只有一个文本部分,则直接发送文本字符串 let sendVal = messageContent.length === 1 && messageContent[0].type === 'text' ? messageContent[0].text : messageContent; let displayVal = displayMessageContent.length === 1 && displayMessageContent[0].type === 'text' ? displayMessageContent[0].text : displayMessageContent; if (window.PromptConstructor && typeof window.PromptConstructor.enhanceUserPrompt === 'function') { sendVal = window.PromptConstructor.enhanceUserPrompt(sendVal); } input.value = ''; window.ChatbotImageUtils.selectedChatbotImages = []; window.ChatbotImageUtils.updateSelectedImagesPreview(); window.ChatbotCore.sendChatbotMessage(sendVal, updateChatbotUI, null, displayVal); } /** * 处理预设问题的点击事件 (UI层面)。 * * 主要逻辑: * 1. 将预设问题填充到输入框。 * 2. 调用 `handleChatbotSend` 发送。 * * @param {string} q - 预设问题文本。 */ // handlePresetQuestion 已在 ChatbotPreset 中定义(包含 Mermaid 和其他 prompt 注入逻辑) // 不再在此重复定义,避免覆盖 /** * 更新聊天机器人界面的核心函数。 * * 主要逻辑: * 1. **显隐控制**:根据 `isChatbotOpen` 控制 modal 和 fab。 * 2. **宽度/全屏管理**:根据 `isChatbotFullscreen` 和 `forceChatbotWidthReset` 等状态调整窗口大小和样式。 * 3. **模型信息获取**:从 `ChatbotCore` 获取模型配置。 * 4. **模型选择器模式** (`isModelSelectorOpen`): * - 若为自定义模型且 `isModelSelectorOpen` 为 true,则调用 `ChatbotModelSelectorUI.render` 显示模型选择器。 * - 否则,确保模型选择器被移除,聊天区和预设问题区可见。 * 5. **预设问题区渲染**:调用 `ChatbotPresetQuestionsUI.render`。 * 6. **聊天消息渲染**:调用 `ChatbotMessageRenderer` 模块渲染历史消息和加载指示器。 * 7. **滚动行为**:智能滚动聊天区域,确保新消息可见或保持用户当前视口。 * 8. **Mermaid图渲染**:调用 `ChatbotRenderingUtils.renderAllMermaidBlocks`。 * 9. **输入框与发送按钮状态更新**:根据加载状态启用/禁用。 * 10. **免责声明与清空历史按钮更新**。 * 11. **浮动高级选项按钮更新**:调用 `_updateFloatingOptionsDisplay`。 */ function updateChatbotUI() { const modal = document.getElementById('chatbot-modal'); const fab = document.getElementById('chatbot-fab'); if (!modal || !fab) return; // 检测沉浸式模式 const inImmersive = !!(window.ImmersiveLayout && typeof window.ImmersiveLayout.isActive === 'function' && window.ImmersiveLayout.isActive()); // 在沉浸式模式下强制禁用浮动模式 if (inImmersive && window.isChatbotFloating) { window.isChatbotFloating = false; } const currentDocId = window.ChatbotCore && typeof window.ChatbotCore.getCurrentDocId === 'function' ? window.ChatbotCore.getCurrentDocId() : 'default_doc'; const fullscreenButton = document.getElementById('chatbot-fullscreen-toggle-btn'); // --- 宽度重置逻辑 --- if (window.chatbotInitialLoad) { window.forceChatbotWidthReset = true; window.chatbotInitialLoad = false; } const chatbotWindowForCheck = modal.querySelector('.chatbot-window'); const isOnHistoryDetailForCheck = window.location.pathname.includes('history_detail.html'); const chunkCompareTabElementForCheck = document.getElementById('tab-chunk-compare'); const currentChunkCompareActive = isOnHistoryDetailForCheck && chunkCompareTabElementForCheck && chunkCompareTabElementForCheck.classList.contains('active'); if (window.lastIsChunkCompareActive !== undefined && window.lastIsChunkCompareActive !== currentChunkCompareActive) { window.forceChatbotWidthReset = true; } // --- 聊天窗口显隐与样式调整 --- if (window.isChatbotOpen) { fab.style.display = 'none'; const chatbotWindow = modal.querySelector('.chatbot-window'); if (chatbotWindow) { if (window.isChatbotFullscreen) { // 全屏样式 modal.style.display = 'block'; modal.style.position = 'fixed'; modal.style.width = '100vw'; modal.style.height = '100vh'; modal.style.top = '0px'; modal.style.left = '0px'; modal.style.bottom = '0px'; modal.style.right = '0px'; modal.style.padding = '0px'; modal.style.margin = '0px'; modal.style.border = 'none'; modal.style.background = 'var(--chat-bg,#ffffff)'; modal.style.pointerEvents = 'auto'; modal.style.zIndex = '100000'; chatbotWindow.style.position = 'absolute'; chatbotWindow.style.width = '100%'; chatbotWindow.style.height = '100%'; chatbotWindow.style.minWidth = '100%'; chatbotWindow.style.minHeight = '100%'; chatbotWindow.style.maxWidth = '100%'; chatbotWindow.style.maxHeight = '100%'; chatbotWindow.style.top = '0px'; chatbotWindow.style.left = '0px'; chatbotWindow.style.right = '0px'; chatbotWindow.style.bottom = '0px'; chatbotWindow.style.borderRadius = '0px'; chatbotWindow.style.padding = '0px'; chatbotWindow.style.margin = '0px'; chatbotWindow.style.border = 'none'; chatbotWindow.style.boxSizing = 'border-box'; chatbotWindow.style.overflow = 'hidden'; chatbotWindow.style.resize = 'none'; if (fullscreenButton) { fullscreenButton.innerHTML = ``; fullscreenButton.title = "退出全屏"; } } else { // 非全屏样式 modal.style.display = 'flex'; modal.style.position = 'fixed'; modal.style.width = 'auto'; modal.style.height = 'auto'; modal.style.top = '0'; modal.style.left = '0'; modal.style.bottom = '0'; modal.style.right = '0'; modal.style.padding = '0px'; modal.style.margin = '0px'; modal.style.border = 'none'; modal.style.background = 'transparent'; modal.style.pointerEvents = 'none'; if (window.isChatbotFloating) { // 浮动模式 chatbotWindow.style.position = 'fixed'; chatbotWindow.style.left = window.chatbotFloatingPosition.x + 'px'; chatbotWindow.style.top = window.chatbotFloatingPosition.y + 'px'; chatbotWindow.style.right = 'auto'; chatbotWindow.style.bottom = 'auto'; chatbotWindow.style.width = window.chatbotFloatingSize.width + 'px'; chatbotWindow.style.height = window.chatbotFloatingSize.height + 'px'; chatbotWindow.style.maxWidth = '90vw'; chatbotWindow.style.maxHeight = '90vh'; chatbotWindow.style.minWidth = '320px'; chatbotWindow.style.minHeight = '400px'; // 样式由 CSS .chatbot-window.floating-mode 控制 chatbotWindow.style.boxShadow = ''; chatbotWindow.style.zIndex = '100001'; chatbotWindow.classList.add('floating-mode'); } else { // 固定位置模式 chatbotWindow.classList.remove('floating-mode'); chatbotWindow.style.position = 'absolute'; let newMaxWidth = '720px'; let newWidth = '92vw'; let newMinHeight = 'calc(520px * 1.1)'; let newMaxHeight = 'calc(85vh * 1.1)'; const isOnHistoryDetail = window.location.pathname.includes('history_detail.html'); const chunkCompareTabElement = document.getElementById('tab-chunk-compare'); const isChunkCompareActive = isOnHistoryDetail && chunkCompareTabElement && chunkCompareTabElement.classList.contains('active'); if (isChunkCompareActive) { newMinHeight = 'calc(520px * 1.25)'; newMaxHeight = '99vh'; newMaxWidth = 'calc(720px * 0.90)'; newWidth = 'calc(92vw * 0.90)'; } if (window.forceChatbotWidthReset || !chatbotWindow.style.width.endsWith('px')) { chatbotWindow.style.width = newWidth; } chatbotWindow.style.maxWidth = newMaxWidth; chatbotWindow.style.minWidth = `calc(${newWidth} * 0.32)`; chatbotWindow.style.minHeight = newMinHeight; chatbotWindow.style.maxHeight = newMaxHeight; if (window.forceChatbotWidthReset || !chatbotWindow.style.height.endsWith('px')) { chatbotWindow.style.height = ''; } if (window.isChatbotPositionedLeft) { chatbotWindow.style.left = '44px'; chatbotWindow.style.right = 'auto'; } else { chatbotWindow.style.right = '44px'; chatbotWindow.style.left = 'auto'; } chatbotWindow.style.top = 'auto'; chatbotWindow.style.bottom = '44px'; // 样式由 CSS .chatbot-window 控制 chatbotWindow.style.borderRadius = ''; chatbotWindow.style.boxShadow = ''; } chatbotWindow.style.padding = ''; chatbotWindow.style.margin = ''; chatbotWindow.style.border = ''; chatbotWindow.style.boxSizing = 'border-box'; chatbotWindow.style.overflow = 'auto'; chatbotWindow.style.resize = 'none'; // 禁用默认resize,使用自定义拖拽 if (fullscreenButton) { fullscreenButton.innerHTML = ``; fullscreenButton.title = "全屏模式"; } } } } else { modal.style.display = 'none'; // 在沉浸式模式下不显示 FAB,以免与沉浸布局冲突 fab.style.display = inImmersive ? 'none' : 'block'; } window.forceChatbotWidthReset = false; window.lastIsChunkCompareActive = currentChunkCompareActive; const posToggleBtn = document.getElementById('chatbot-position-toggle-btn'); if (posToggleBtn) { if (window.isChatbotFloating) { // 浮动模式下隐藏位置切换按钮 posToggleBtn.style.display = 'none'; } else { posToggleBtn.style.display = 'flex'; if (window.isChatbotPositionedLeft) { posToggleBtn.innerHTML = ``; posToggleBtn.title = "切换到右下角"; } else { posToggleBtn.innerHTML = ``; posToggleBtn.title = "切换到左下角"; } } } const floatToggleBtn = document.getElementById('chatbot-float-toggle-btn'); if (floatToggleBtn) { // 在沉浸式模式隐藏浮动切换按钮 if (inImmersive) { floatToggleBtn.style.display = 'none'; } else { floatToggleBtn.style.display = 'flex'; } if (window.isChatbotFloating) { floatToggleBtn.innerHTML = ``; floatToggleBtn.title = "固定模式"; } else { floatToggleBtn.innerHTML = ``; floatToggleBtn.title = "浮动模式"; } } const chatBody = document.getElementById('chatbot-body'); const chatbotPresetHeader = document.getElementById('chatbot-preset-header'); const chatbotPresetBody = document.getElementById('chatbot-preset-body'); let modelSelectorDiv = document.getElementById('chatbot-model-selector'); const existingPresetContainer = document.getElementById('chatbot-preset-container'); if (existingPresetContainer) existingPresetContainer.remove(); const chatbotWindow = modal.querySelector('.chatbot-window'); if (!chatbotWindow) { console.error("Chatbot UI: .chatbot-window not found for preset container."); return; } let isCustomModel = false; let availableModels = []; let currentSettings = {}; try { // 缓存配置,避免流式更新时频繁加载 // 只在模型选择器打开或缓存失效时重新获取 if (!window._cachedChatbotConfig || window.isModelSelectorOpen) { window._cachedChatbotConfig = window.ChatbotCore.getChatbotConfig(); } const config = window._cachedChatbotConfig; currentSettings = config.settings || {}; isCustomModel = config.model === 'custom' || (typeof config.model === 'string' && config.model.startsWith('custom_source_')); availableModels = config.siteSpecificAvailableModels || []; } catch (e) { console.error("Error getting chatbot config for UI:", e); } const presetContainer = window.ChatbotPresetQuestionsUI.render( chatbotWindow, isCustomModel, currentDocId, updateChatbotUI, window.ChatbotPreset?.handlePresetQuestion || window.handlePresetQuestion ); chatbotWindow.appendChild(presetContainer); let userMessageCount = 0; if (window.ChatbotCore && window.ChatbotCore.chatHistory) { userMessageCount = window.ChatbotCore.chatHistory.filter(m => m.role === 'user').length; } if (userMessageCount >= 3 && !window.presetAutoCollapseTriggeredForDoc[currentDocId] && !window.isPresetQuestionsCollapsed && !window.isModelSelectorOpen) { window.isPresetQuestionsCollapsed = true; window.presetAutoCollapseTriggeredForDoc[currentDocId] = true; const presetToggleBtn = presetContainer.querySelector('#chatbot-preset-toggle-btn'); if (presetToggleBtn) { presetToggleBtn.innerHTML = ''; presetToggleBtn.title = "展开快捷指令"; } } // 获取 mainContentArea 元素 const mainContentArea = document.getElementById('chatbot-main-content-area'); if (mainContentArea) { let current_padding_top_for_main_content_area = 12; if (presetContainer && presetContainer.style.display !== 'none' && presetContainer.offsetHeight) { current_padding_top_for_main_content_area = presetContainer.offsetHeight; } mainContentArea.style.paddingTop = current_padding_top_for_main_content_area + 'px'; // 仅在“固定模式”下根据内容自适应高度;浮动/全屏不改动用户设置的尺寸 if (!window.isChatbotFloating && !window.isChatbotFullscreen) { const titleBar = document.getElementById('chatbot-title-bar'); const inputContainer = document.getElementById('chatbot-input-container'); if (titleBar && inputContainer && chatbotWindow) { const h_title_bar = titleBar.offsetHeight; const h_input_container = inputContainer.offsetHeight; const h_chat_body_target = 250; const desired_window_height = h_title_bar + current_padding_top_for_main_content_area + h_chat_body_target + h_input_container; const min_win_h_px = parseFloat(getComputedStyle(chatbotWindow).minHeight) || 520; const max_win_h_px = parseFloat(getComputedStyle(chatbotWindow).maxHeight) || (0.85 * window.innerHeight); chatbotWindow.style.height = Math.max(min_win_h_px, Math.min(max_win_h_px, desired_window_height)) + 'px'; } } } if (isCustomModel && window.isModelSelectorOpen) { if (window.ChatbotModelSelectorUI && typeof window.ChatbotModelSelectorUI.render === 'function') { window.ChatbotModelSelectorUI.render(mainContentArea, chatBody, availableModels, currentSettings, updateChatbotUI); } else { console.error("ChatbotModelSelectorUI.render is not available."); } return; } else { const existingModelSelectorDiv = document.getElementById('chatbot-model-selector'); if (existingModelSelectorDiv) existingModelSelectorDiv.remove(); if (presetContainer) presetContainer.style.display = ''; if (chatBody) chatBody.style.display = ''; } if (chatBody) { const oldScrollTop = chatBody.scrollTop; const oldScrollHeight = chatBody.scrollHeight; const oldClientHeight = chatBody.clientHeight; // Phase 3.5 提前定义消息计数(用于后续滚动逻辑) // 使用统一的状态管理对象,避免全局变量污染 if (!window.ChatbotRenderState) { window.ChatbotRenderState = { lastRenderedMessageCount: 0 }; } const currentMessageCount = window.ChatbotCore.chatHistory.length; const lastRenderedCount = window.ChatbotRenderState.lastRenderedMessageCount || 0; let docName = 'unknown_doc'; let docId = 'unknown_doc'; // 完整的 docId,用于 draw.io 等功能 let dataForMindmap = { images: [], ocr: '', translation: '' }; if (window.ChatbotCore && typeof window.ChatbotCore.getCurrentDocContent === 'function') { const currentDoc = window.ChatbotCore.getCurrentDocContent(); if (currentDoc) { docName = currentDoc.name || 'unknown_doc'; dataForMindmap = { images: currentDoc.images || [], ocr: currentDoc.ocr || '', translation: currentDoc.translation || '' }; } } // 获取完整的 docId(包含文档名、图片数量、OCR长度、翻译长度) if (window.ChatbotCore && typeof window.ChatbotCore.getCurrentDocId === 'function') { docId = window.ChatbotCore.getCurrentDocId(); } if (window.ChatbotMessageRenderer) { // Phase 3.5 增量渲染: 只渲染变化的消息,避免重新渲染整个历史 // 如果是流式更新(消息数量没变),只更新最后一条消息 if (window.ChatbotCore.isChatbotLoading && currentMessageCount === lastRenderedCount && currentMessageCount > 0) { const lastMessage = window.ChatbotCore.chatHistory[currentMessageCount - 1]; const lastMessageContainer = chatBody.querySelector(`.assistant-message[data-message-index="${currentMessageCount - 1}"]`); // 检查是否需要完整渲染(reasoning 第一次出现) let needFullRender = false; if (lastMessage.reasoningContent && lastMessageContainer) { const reasoningBlockId = `reasoning-block-${currentMessageCount - 1}`; const reasoningBlock = lastMessageContainer.querySelector(`#${reasoningBlockId}`); if (!reasoningBlock) { needFullRender = true; // reasoning 块不存在,需要完整渲染 } } // 如果需要完整渲染,跳过增量更新 if (!needFullRender && lastMessageContainer && lastMessage.role === 'assistant') { // 0. 更新 ReAct 可视化 (reactLog) - 增量追加 if (lastMessage.reactLog && lastMessage.reactLog.length > 0) { const vizId = `react-viz-${currentMessageCount - 1}`; let vizContainer = lastMessageContainer.querySelector(`#${vizId}`); // 如果容器不存在,说明是第一次出现 ReAct 日志,需要完整渲染 if (!vizContainer) { needFullRender = true; } else { // 增量更新步骤 const stepsContainer = vizContainer.querySelector('.react-steps-container'); if (stepsContainer) { const currentStepsCount = stepsContainer.children.length; const newStepsCount = lastMessage.reactLog.length; if (newStepsCount > currentStepsCount) { // 追加新步骤 const stepsToAdd = lastMessage.reactLog.slice(currentStepsCount); let newStepsHtml = ''; stepsToAdd.forEach((step, i) => { let icon = ''; let title = ''; let typeClass = ''; let content = ''; const stepIndex = currentStepsCount + i + 1; if (step.type === 'thought') { icon = 'carbon:idea'; title = `Thought ${step.iteration || stepIndex}`; typeClass = 'step-thought'; content = step.content; } else if (step.type === 'action') { icon = 'carbon:tools'; title = `Action ${step.iteration || stepIndex}`; typeClass = 'step-action'; content = `Tool: ${step.tool}\nInput: ${JSON.stringify(step.params, null, 2)}`; } else if (step.type === 'observation') { icon = 'carbon:view'; title = `Observation ${step.iteration || stepIndex}`; typeClass = 'step-observation'; content = typeof step.result === 'string' ? step.result : JSON.stringify(step.result, null, 2); if (content.length > 500) content = content.slice(0, 500) + '... (truncated)'; } if (content) { content = window.ChatbotUtils.escapeHtml(content); // 添加 slideIn 动画 newStepsHtml += `
${title}
${content}
`; } }); if (newStepsHtml) { stepsContainer.insertAdjacentHTML('beforeend', newStepsHtml); stepsContainer.scrollTop = stepsContainer.scrollHeight; // 更新状态徽章 const statusBadge = vizContainer.querySelector('.react-status-badge'); if (statusBadge) { const lastStep = lastMessage.reactLog[lastMessage.reactLog.length - 1]; if (lastStep.type === 'action') { statusBadge.textContent = 'Executing...'; statusBadge.className = 'react-status-badge react-status-executing'; } else if (lastStep.type === 'thought') { statusBadge.textContent = 'Thinking...'; statusBadge.className = 'react-status-badge react-status-thinking'; } } } } } } } // 如果在上述检查中发现需要完整渲染,则跳出 if (needFullRender) { // Fall through to full render logic below } else { // 1. 更新思考过程 (reasoning) if (lastMessage.reasoningContent) { const reasoningBlockId = `reasoning-block-${currentMessageCount - 1}`; let reasoningBlock = lastMessageContainer.querySelector(`#${reasoningBlockId}`); // 更新 reasoning 内容 // Phase 4.x 修复:使用更快的选择器(优先使用类名,回退到子元素选择器) const reasoningContentDiv = reasoningBlock ? (reasoningBlock.querySelector('.reasoning-content') || reasoningBlock.querySelector(':scope > div:last-child')) : null; if (reasoningContentDiv) { const newReasoningLength = lastMessage.reasoningContent.length; const lastReasoningLength = parseInt(reasoningBlock.dataset.lastReasoningLength || '0', 10); if (newReasoningLength !== lastReasoningLength) { try { if (typeof renderWithKatexStreaming === 'function') { reasoningContentDiv.innerHTML = renderWithKatexStreaming(lastMessage.reasoningContent); } else { reasoningContentDiv.innerHTML = lastMessage.reasoningContent.replace(/\n/g, '
'); } reasoningBlock.dataset.lastReasoningLength = newReasoningLength.toString(); } catch (e) { if (window.PerfLogger) { window.PerfLogger.error('Reasoning 增量渲染失败:', e); } reasoningContentDiv.textContent = lastMessage.reasoningContent; } } } } // 2. 更新主内容 (content) const contentDiv = lastMessageContainer.querySelector('.markdown-content'); if (contentDiv && lastMessage.content) { // 使用内容长度与内容本身判断是否有变化 const newContent = String(lastMessage.content); const newContentLength = newContent.length; const lastContentLength = parseInt(contentDiv.dataset.lastLength || '0', 10); const lastContent = contentDiv.dataset.lastContent || ''; if (newContentLength !== lastContentLength) { try { const isPureExtension = newContentLength > lastContentLength && lastContent && newContent.indexOf(lastContent) === 0; const appendedText = isPureExtension ? newContent.slice(lastContent.length) : ''; const canUseIncrementalAppend = isPureExtension && !lastMessage.isRawHtml && // 纯 HTML 内容不使用增量渲染 isChatbotSafePlainAppend(lastContent, appendedText); let didIncrementalUpdate = false; if (canUseIncrementalAppend && typeof renderWithKatexStreaming === 'function') { // Phase 4.2: 简单增量渲染(纯文本追加场景) const appendedHtml = renderWithKatexStreaming(appendedText); const temp = document.createElement('div'); temp.innerHTML = appendedHtml; while (temp.firstChild) { contentDiv.appendChild(temp.firstChild); } didIncrementalUpdate = true; } else if ( isPureExtension && !lastMessage.isRawHtml && // 纯 HTML 内容不使用增量渲染 appendedText && window.ChatbotMathStreaming && typeof window.ChatbotMathStreaming.renderIncremental === 'function' ) { // Phase 4.2(原型): 长公式增量渲染,仅在扩展场景下启用 let prevState = null; const stateRaw = contentDiv.dataset.mathStreamingState || ''; if (stateRaw) { try { prevState = JSON.parse(stateRaw); } catch (e) { prevState = null; } } const result = window.ChatbotMathStreaming.renderIncremental(prevState, appendedText); if (result && typeof result.html === 'string' && result.html) { const temp = document.createElement('div'); temp.innerHTML = result.html; while (temp.firstChild) { contentDiv.appendChild(temp.firstChild); } if (result.state) { try { contentDiv.dataset.mathStreamingState = JSON.stringify(result.state); } catch (e) { contentDiv.dataset.mathStreamingState = ''; } } didIncrementalUpdate = true; } } if (!didIncrementalUpdate) { // 回退:完整重渲染 let contentToRender = newContent; // 🔧 检测并修复历史数据中被转义的 HTML(向后兼容) if (!lastMessage.isRawHtml && (contentToRender.includes('<div') || contentToRender.includes('<button')) && (contentToRender.includes('配图 XML') || contentToRender.includes('手动修复'))) { console.log('[UI] 检测到被转义的 HTML,自动反转义'); // 创建临时元素进行反转义 const tempDiv = document.createElement('div'); tempDiv.innerHTML = contentToRender; contentToRender = tempDiv.innerHTML; // 使用 innerHTML 而不是 textContent lastMessage.isRawHtml = true; // 标记为纯 HTML } // 检查是否为纯 HTML 内容(不需要 Markdown 解析) if (lastMessage.isRawHtml) { contentDiv.innerHTML = contentToRender; } else if (typeof renderWithKatexStreaming === 'function') { contentDiv.innerHTML = renderWithKatexStreaming(contentToRender); } else if (typeof marked !== 'undefined') { contentDiv.innerHTML = marked.parse(contentToRender); } else { contentDiv.textContent = contentToRender; } // 重渲染后清理流式状态,避免状态与内容不一致 delete contentDiv.dataset.mathStreamingState; } contentDiv.dataset.lastLength = newContentLength.toString(); contentDiv.dataset.lastContent = newContent; } catch (e) { if (window.PerfLogger) { window.PerfLogger.error('增量渲染失败:', e); } contentDiv.textContent = newContent; } } } } // End of else block for needFullRender check // 3. 更新工具调用块 (toolCallHtml) - 支持流式多轮取材实时更新 if (lastMessage.toolCallHtml) { const toolCallBlockContainer = lastMessageContainer.querySelector('.tool-thinking-block'); const newToolCallHtml = String(lastMessage.toolCallHtml); // 检查是否需要更新(比较HTML内容) if (toolCallBlockContainer) { const currentHtml = toolCallBlockContainer.outerHTML; if (currentHtml !== newToolCallHtml) { // 更新工具调用块HTML const tempDiv = document.createElement('div'); tempDiv.innerHTML = newToolCallHtml; const newToolCallBlock = tempDiv.firstElementChild; if (newToolCallBlock) { toolCallBlockContainer.replaceWith(newToolCallBlock); } } } else if (newToolCallHtml) { // 工具调用块不存在,插入新的块(在主内容之前) const contentDiv = lastMessageContainer.querySelector('.markdown-content'); if (contentDiv && contentDiv.parentNode) { const tempDiv = document.createElement('div'); tempDiv.innerHTML = newToolCallHtml; const newToolCallBlock = tempDiv.firstElementChild; if (newToolCallBlock) { contentDiv.parentNode.insertBefore(newToolCallBlock, contentDiv); } } } } } // Phase 3.5 智能滚动:流式更新时保持用户阅读位置 // 只有在用户主动停留在底部附近时才自动滚动 if (!needFullRender) { const scrollThreshold = window.PerformanceConfig?.SCROLL?.BOTTOM_THRESHOLD || 50; const isUserAtBottom = oldScrollHeight - oldClientHeight <= oldScrollTop + scrollThreshold; if (isUserAtBottom) { chatBody.scrollTop = chatBody.scrollHeight; } // 如果用户正在查看上方内容,不做任何滚动操作 return; // 跳过完整重新渲染 } // needFullRender = true 时,继续执行完整渲染(不 return) } // 完整渲染:新消息或非流式更新 let messagesHtml = window.ChatbotCore.chatHistory.map((m, index) => { if (m.role === 'segment-summary') { return ''; } if (m.role === 'final-summary') { return window.ChatbotMessageRenderer.renderFinalSummaryMessage(m); } if (m.role === 'user') { return window.ChatbotMessageRenderer.renderUserMessage(m, index); } return window.ChatbotMessageRenderer.renderAssistantMessage(m, index, docName, dataForMindmap, docId); }).join(''); // Prevent double typing indicators: // Only show the standalone typing indicator if the last message is NOT from the assistant. // If the last message IS from the assistant, it means the response has started (even if empty/reasoning), // so the assistant message itself will render the appropriate state (Logo, Reasoning, or Content). const lastMsg = window.ChatbotCore.chatHistory[window.ChatbotCore.chatHistory.length - 1]; const isLastMsgAssistant = lastMsg && lastMsg.role === 'assistant'; if (window.ChatbotCore.isChatbotLoading && !isLastMsgAssistant) { messagesHtml += window.ChatbotMessageRenderer.renderTypingIndicator(); } chatBody.innerHTML = messagesHtml + window.ChatbotMessageRenderer.getMarkdownStyles(); window.ChatbotRenderState.lastRenderedMessageCount = currentMessageCount; if (window.PerfLogger) { window.PerfLogger.debug(`增量渲染: 完整渲染 ${currentMessageCount} 条消息`); } } else { console.error("ChatbotMessageRenderer is not loaded!"); chatBody.innerHTML = "

错误:消息渲染模块加载失败。

"; } setTimeout(() => { if (!window.ChatbotCore.isChatbotLoading) { chatBody.querySelectorAll('code.language-mermaid, pre code.language-mermaid').forEach(block => { block.setAttribute('data-mermaid-final', 'true'); }); } }, 0); // Phase 3.5 智能滚动:完整渲染时保持用户阅读位置 const isUserAtBottom = oldScrollHeight - oldClientHeight <= oldScrollTop + 50; // 增加容差到 50px const isNewMessageArrival = currentMessageCount > lastRenderedCount; // 检测是否有新消息到达 // 自动滚动到底部的条件: // 1. 有新消息到达(用户发送消息或助手开始回复) // 2. 或者用户已经停留在底部附近 if (isNewMessageArrival || isUserAtBottom) { chatBody.scrollTop = chatBody.scrollHeight; } // 否则保持用户当前的阅读位置(即使正在加载也不强制滚动) if (window.ChatbotRenderingUtils && typeof window.ChatbotRenderingUtils.renderAllMermaidBlocks === 'function') { if (window.mermaidLoaded && typeof window.mermaid !== 'undefined') { window.ChatbotRenderingUtils.renderAllMermaidBlocks(chatBody); } else { setTimeout(() => { if (window.ChatbotRenderingUtils && typeof window.ChatbotRenderingUtils.renderAllMermaidBlocks === 'function') { window.ChatbotRenderingUtils.renderAllMermaidBlocks(chatBody); } }, 600); setTimeout(() => { if (window.ChatbotRenderingUtils && typeof window.ChatbotRenderingUtils.renderAllMermaidBlocks === 'function') { window.ChatbotRenderingUtils.renderAllMermaidBlocks(chatBody); } }, 1200); } } else { console.warn('ChatbotUI: ChatbotRenderingUtils.renderAllMermaidBlocks is not available.'); } // Phase 4.1: 表格滚动性能优化 - 使用 IntersectionObserver 优先,回退到 requestAnimationFrame 节流 setupChatbotTableScrollHints(chatBody); } const input = document.getElementById('chatbot-input'); const sendBtn = document.getElementById('chatbot-send-btn'); const stopBtn = document.getElementById('chatbot-stop-btn'); if (input && sendBtn) { input.disabled = window.ChatbotCore.isChatbotLoading; sendBtn.disabled = window.ChatbotCore.isChatbotLoading; if (window.ChatbotCore.isChatbotLoading) { sendBtn.style.display = 'none'; if (stopBtn) stopBtn.style.display = 'flex'; } else { sendBtn.style.display = 'flex'; if (stopBtn) stopBtn.style.display = 'none'; } } const disclaimerDiv = document.querySelector('.chatbot-disclaimer'); if (disclaimerDiv) { const currentChatHistory = window.ChatbotCore && window.ChatbotCore.chatHistory ? window.ChatbotCore.chatHistory : []; if (currentChatHistory.length > 0) { disclaimerDiv.innerHTML = 'AI助手可能会犯错。请核实重要信息。删除对话记录'; const clearBtn = document.getElementById('chatbot-clear-history-btn'); if (clearBtn) { clearBtn.onclick = function() { if (confirm('确定要删除当前对话的所有记录吗?')) { if (window.ChatbotCore && typeof window.ChatbotCore.clearCurrentDocChatHistory === 'function') { const docIdToClear = window.ChatbotCore.getCurrentDocId ? window.ChatbotCore.getCurrentDocId() : 'default_doc'; window.ChatbotCore.clearCurrentDocChatHistory(updateChatbotUI); window.isPresetQuestionsCollapsed = false; if (window.presetAutoCollapseTriggeredForDoc) { delete window.presetAutoCollapseTriggeredForDoc[docIdToClear]; } } else { console.error("clearCurrentDocChatHistory function not found on ChatbotCore"); } } }; } } else { disclaimerDiv.innerHTML = '

AI助手可能会犯错。请核实重要信息。

'; } } // 将调用 _updateFloatingOptionsDisplay() 改为调用独立模块 if (window.ChatbotFloatingOptionsUI && typeof window.ChatbotFloatingOptionsUI.updateDisplay === 'function') { window.ChatbotFloatingOptionsUI.updateDisplay(); } } /** * Phase 4.1 - 表格滚动提示性能优化 * 目标:在保持现有视觉行为(滚动到最右侧隐藏渐变阴影)的前提下,减少滚动事件压力。 * * 策略: * 1. 优先使用 IntersectionObserver 观察表格中“最右侧单元格”是否完全进入视口。 * - root: table,自身作为滚动容器。 * - threshold: 1.0,仅当最后单元格完全可见时认为滚动到末尾。 * 2. 当浏览器不支持 IntersectionObserver 时,回退到 requestAnimationFrame 节流的 scroll 监听。 * 3. 每次调用都会清理旧的监听器和观察器,避免重复绑定和内存泄漏。 */ function setupChatbotTableScrollHints(chatBody) { if (!chatBody) return; const tables = chatBody.querySelectorAll('.markdown-content table'); tables.forEach(function(table) { // 清理旧的 IntersectionObserver(如有) if (table._scrollObserver && typeof table._scrollObserver.disconnect === 'function') { try { table._scrollObserver.disconnect(); } catch (e) { if (window.PerfLogger) { window.PerfLogger.warn('ChatbotUI: disconnect table._scrollObserver failed', e); } } } table._scrollObserver = null; // 清理旧的 scroll 监听器(如有) if (table._scrollListener) { table.removeEventListener('scroll', table._scrollListener); } table._scrollListener = null; // 如果浏览器支持 IntersectionObserver,则优先使用 if (window.IntersectionObserver) { let lastCell = table.querySelector('tr:last-child td:last-child'); if (!lastCell) { lastCell = table.querySelector('td:last-child, th:last-child'); } // 如果找不到可观察的单元格,则直接视为已滚动到底部(不显示渐变阴影) if (!lastCell) { table.classList.add('scrolled-to-end'); return; } const observer = new IntersectionObserver(function(entries) { entries.forEach(function(entry) { // 当最后一个单元格完全进入视口时,认为滚动到了最右端 const isEnd = entry.isIntersecting && entry.intersectionRatio >= 1; if (isEnd) { table.classList.add('scrolled-to-end'); } else { table.classList.remove('scrolled-to-end'); } }); }, { root: table, threshold: 1.0 }); observer.observe(lastCell); table._scrollObserver = observer; return; } // 回退方案:使用 requestAnimationFrame 节流 scroll 事件 let scrollRAF = null; const scrollListener = function() { if (scrollRAF) return; // 若浏览器不支持 requestAnimationFrame,则直接同步执行 if (typeof window.requestAnimationFrame !== 'function') { const isScrolledToEnd = table.scrollLeft >= (table.scrollWidth - table.clientWidth - 5); if (isScrolledToEnd) { table.classList.add('scrolled-to-end'); } else { table.classList.remove('scrolled-to-end'); } return; } scrollRAF = window.requestAnimationFrame(function() { const isScrolledToEnd = table.scrollLeft >= (table.scrollWidth - table.clientWidth - 5); if (isScrolledToEnd) { table.classList.add('scrolled-to-end'); } else { table.classList.remove('scrolled-to-end'); } scrollRAF = null; }); }; table._scrollListener = scrollListener; table.addEventListener('scroll', scrollListener); // 初始检查(表格首次渲染时) scrollListener(); }); } /** * Phase 4.2 - 简单增量追加判定(纯文本场景) * 仅在追加内容较为“安全”时启用增量渲染: * - newContent 以 oldContent 为前缀(纯追加,而非回退/编辑) * - 追加部分不包含明显的结构/公式标记(如 ```、$$、\[…\] 等) * 复杂 Markdown / LaTeX 场景仍回退到完整重渲染,避免破坏结构。 */ function isChatbotSafePlainAppend(oldContent, appendedText) { if (!appendedText) return false; // 边界检查 1:若旧内容末尾处位于未闭合的块级结构中(代码块/公式等),则不做增量。 // 这里采用“保守策略”:一旦存在疑似未闭合结构,就直接回退完整渲染,以换取更高的渲染正确性。 try { if (isChatbotInsideUnclosedBlock(oldContent)) { return false; } } catch (e) { // 检测异常时同样回退完整渲染 if (window.PerfLogger) { window.PerfLogger.warn('ChatbotUI: isChatbotInsideUnclosedBlock failed, fallback to full render.', e); } return false; } // 若追加部分包含明显的代码块/公式/复杂结构标记,则不做增量 const riskyPatterns = [ /```/, // 代码块 /\$\$/, // 块级公式 /\\\[/, // \[ ... \] /\\\(/, // \( ... \) /<\/?[a-zA-Z]/, // HTML 标签 /^#{1,6}\s/m, // 标题 /^\s{0,3}[-*+]\s/m, // 无序列表 /^\s{0,3}\d+\.\s/m, // 有序列表 /^\s{0,3}>\s/m // 引用 ]; for (let i = 0; i < riskyPatterns.length; i++) { if (riskyPatterns[i].test(appendedText)) { return false; } } return true; } /** * Phase 4.2.2 - Markdown 结构边界检测 * 检测 oldContent 末尾是否位于未闭合的 Markdown 块级结构中: * - ``` fenced code block * - $$ 块级公式 * - \[ \] / \( \) LaTeX 公式包裹 * * 为保证性能: * - 优先仅分析结尾一段文本(TAIL_WINDOW),减少在极长消息上的全量扫描开销; * - 对 fenced code / $$ 使用“奇偶计数”策略;对 \[ / \] 与 \( / \) 使用“数量差值”近似判断; * - 一旦存在“不确定”或“可能未闭合”的结构,则视为不安全,回退完整渲染。 */ function isChatbotInsideUnclosedBlock(oldContent) { if (!oldContent || typeof oldContent !== 'string') return false; // 限制分析窗口,避免在超长内容上多次全量扫描 const TAIL_WINDOW = 4000; const text = oldContent.length > TAIL_WINDOW ? oldContent.slice(-TAIL_WINDOW) : oldContent; // 辅助函数:统计匹配次数 function countMatches(pattern) { const re = new RegExp(pattern, 'g'); let count = 0; while (re.exec(text) !== null) { count++; } return count; } // 1) Fenced code block: ``` ... ``` // 使用出现次数的奇偶性近似判断是否在未闭合的代码块中。 const codeFenceCount = countMatches('```'); if (codeFenceCount % 2 === 1) { return true; } // 2) Block math: $$ ... $$ const blockMathCount = countMatches('\\$\\$'); if (blockMathCount % 2 === 1) { return true; } // 3) LaTeX-style delimiters: \[ ... \], \( ... \) const openBracket = countMatches('\\\\\\['); const closeBracket = countMatches('\\\\\\]'); if (openBracket > closeBracket) { return true; } const openParen = countMatches('\\\\\\('); const closeParen = countMatches('\\\\\\)'); if (openParen > closeParen) { return true; } return false; } /** * 初始化聊天机器人浮动按钮 (FAB) 和主弹窗 (Modal) 的 UI。 * * 主要步骤: * 1. **FAB 初始化**:创建 FAB,设置样式和点击事件(打开聊天弹窗)。 * 2. **Modal 初始化**:创建 Modal,包含头部、预设区、聊天内容区、输入区等。 * - 头部:标题、全屏/位置切换/关闭按钮。 * - 主内容区:承载预设区和聊天内容区。 * - 输入区:图片添加、文本输入、发送按钮、免责声明。 * - 注入基础 CSS (滚动条、响应式、暗黑模式等)。 * 3. **浮动高级选项初始化**:调用 `_createFloatingOptionsBar` 创建选项按钮并插入到输入区。 * 4. **事件绑定**:为全屏、位置切换、关闭按钮绑定事件。 * 5. **初始UI更新**:调用 `updateChatbotUI`。 */ function initChatbotUI() { // --- FAB (浮动操作按钮) 初始化 --- let fab = document.getElementById('chatbot-fab'); if (!fab) { fab = document.createElement('div'); fab.id = 'chatbot-fab'; // 设置 FAB 的固定定位、初始位置(根据 isChatbotPositionedLeft 决定左右)和层级 fab.style.position = 'fixed'; fab.style.bottom = '32px'; if (window.isChatbotPositionedLeft) { fab.style.left = '32px'; fab.style.right = 'auto'; } else { fab.style.right = '32px'; fab.style.left = 'auto'; } fab.style.zIndex = '99999'; // FAB 内部的按钮 HTML,使用响应式尺寸和CSS变量 fab.innerHTML = ` `; document.body.appendChild(fab); } // FAB 点击事件:打开聊天窗口,默认展开预设问题,并强制重置窗口宽度 fab.onclick = function() { window.isChatbotOpen = true; window.isPresetQuestionsCollapsed = false; window.forceChatbotWidthReset = true; updateChatbotUI(); }; // --- Modal (主聊天窗口) 初始化 --- let modal = document.getElementById('chatbot-modal'); if (!modal) { modal = document.createElement('div'); modal.id = 'chatbot-modal'; // Modal 作为聊天窗口的容器,初始隐藏,通过 flex 布局控制 chatbot-window 的居中(非全屏时) modal.style.position = 'fixed'; modal.style.inset = '0'; // 等同于 top:0, left:0, bottom:0, right:0 modal.style.zIndex = '100000'; modal.style.background = 'transparent'; // 背景透明,依赖内部 chatbot-window 的背景 modal.style.display = 'none'; // 初始隐藏 modal.style.pointerEvents = 'none'; // 自身不接收鼠标事件,允许穿透 // Modal 内部的 HTML 结构 modal.innerHTML = `
AI 智能助手
{/* 图片预览由 ChatbotImageUtils.updateSelectedImagesPreview 更新 */}

AI助手可能会犯错。请核实重要信息。

`; document.body.appendChild(modal); } // --- 浮动高级选项初始化 --- const inputContainerElement = document.getElementById('chatbot-input-container'); if (window.ChatbotFloatingOptionsUI && typeof window.ChatbotFloatingOptionsUI.createBar === 'function') { window.ChatbotFloatingOptionsUI.createBar(inputContainerElement, updateChatbotUI); } // --- 核心控制按钮事件绑定 --- // 浮动模式切换按钮点击事件 document.getElementById('chatbot-float-toggle-btn').onclick = function() { // 沉浸式模式下禁用浮动模式切换 if (window.ImmersiveLayout && typeof window.ImmersiveLayout.isActive === 'function' && window.ImmersiveLayout.isActive()) { return; // 直接忽略 } window.isChatbotFloating = !window.isChatbotFloating; localStorage.setItem('chatbotFloating', String(window.isChatbotFloating)); if (window.isChatbotFloating) { // 切换到浮动模式时,记录当前位置和大小 const chatbotWindow = modal.querySelector('.chatbot-window'); if (chatbotWindow) { const rect = chatbotWindow.getBoundingClientRect(); window.chatbotFloatingPosition = { x: rect.left, y: rect.top }; window.chatbotFloatingSize = { width: rect.width, height: rect.height }; localStorage.setItem('chatbotFloatingPosition', JSON.stringify(window.chatbotFloatingPosition)); localStorage.setItem('chatbotFloatingSize', JSON.stringify(window.chatbotFloatingSize)); } } updateChatbotUI(); }; // 全屏切换按钮点击事件 document.getElementById('chatbot-fullscreen-toggle-btn').onclick = function() { const wasFullscreen = window.isChatbotFullscreen; window.isChatbotFullscreen = !window.isChatbotFullscreen; // 切换全屏状态 localStorage.setItem('chatbotFullscreen', String(window.isChatbotFullscreen)); // 保存到localStorage if (wasFullscreen && !window.isChatbotFullscreen) { // 如果是从全屏退出 window.forceChatbotWidthReset = true; // 强制重置宽度 } updateChatbotUI(); }; // 位置切换按钮点击事件 document.getElementById('chatbot-position-toggle-btn').onclick = function() { window.isChatbotPositionedLeft = !window.isChatbotPositionedLeft; // 切换左右位置状态 localStorage.setItem('chatbotPosition', window.isChatbotPositionedLeft ? 'left' : 'right'); // 保存到localStorage window.forceChatbotWidthReset = true; // 位置变化也应重置宽度 updateChatbotUI(); }; // 关闭按钮点击事件 document.getElementById('chatbot-close-btn').onclick = function() { window.isChatbotOpen = false; // 关闭聊天窗口 updateChatbotUI(); }; // --- 拖拽和调整大小功能初始化 --- initChatbotDragAndResize(); updateChatbotUI(); // 首次完整渲染或更新UI } /** * 初始化聊天机器人的拖拽移动和调整大小功能 */ function initChatbotDragAndResize() { const modal = document.getElementById('chatbot-modal'); if (!modal) return; let isDragging = false; let isResizing = false; let dragStartX, dragStartY; let initialX, initialY, initialWidth, initialHeight; let resizeDirection = ''; // 拖拽移动功能 (仅在浮动模式下生效) function handleDragStart(e) { if (!window.isChatbotFloating || window.isChatbotFullscreen) return; const chatbotWindow = modal.querySelector('.chatbot-window'); if (!chatbotWindow) return; isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY; const rect = chatbotWindow.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; document.body.classList.add('chatbot-dragging'); e.preventDefault(); } function handleDragMove(e) { if (!isDragging || !window.isChatbotFloating) return; const deltaX = e.clientX - dragStartX; const deltaY = e.clientY - dragStartY; const newX = Math.max(0, Math.min(window.innerWidth - 320, initialX + deltaX)); const newY = Math.max(0, Math.min(window.innerHeight - 200, initialY + deltaY)); window.chatbotFloatingPosition = { x: newX, y: newY }; localStorage.setItem('chatbotFloatingPosition', JSON.stringify(window.chatbotFloatingPosition)); const chatbotWindow = modal.querySelector('.chatbot-window'); if (chatbotWindow) { chatbotWindow.style.left = newX + 'px'; chatbotWindow.style.top = newY + 'px'; } } function handleDragEnd() { if (isDragging) { isDragging = false; document.body.classList.remove('chatbot-dragging'); } } // 调整大小功能 function handleResizeStart(e) { if (window.isChatbotFullscreen || !window.isChatbotFloating) return; const chatbotWindow = modal.querySelector('.chatbot-window'); if (!chatbotWindow) return; isResizing = true; resizeDirection = e.target.dataset.direction; dragStartX = e.clientX; dragStartY = e.clientY; const rect = chatbotWindow.getBoundingClientRect(); initialX = rect.left; initialY = rect.top; initialWidth = rect.width; initialHeight = rect.height; document.body.classList.add('chatbot-dragging'); e.preventDefault(); e.stopPropagation(); } function handleResizeMove(e) { if (!isResizing || !window.isChatbotFloating) return; const deltaX = e.clientX - dragStartX; const deltaY = e.clientY - dragStartY; let newX = initialX; let newY = initialY; let newWidth = initialWidth; let newHeight = initialHeight; // 根据拖拽方向调整大小和位置 if (resizeDirection.includes('n')) { newY = initialY + deltaY; newHeight = initialHeight - deltaY; } if (resizeDirection.includes('s')) { newHeight = initialHeight + deltaY; } if (resizeDirection.includes('w')) { newX = initialX + deltaX; newWidth = initialWidth - deltaX; } if (resizeDirection.includes('e')) { newWidth = initialWidth + deltaX; } // 应用最小和最大尺寸限制 newWidth = Math.max(320, Math.min(window.innerWidth * 0.9, newWidth)); newHeight = Math.max(400, Math.min(window.innerHeight * 0.9, newHeight)); // 确保窗口不会超出屏幕边界 newX = Math.max(0, Math.min(window.innerWidth - newWidth, newX)); newY = Math.max(0, Math.min(window.innerHeight - newHeight, newY)); // 更新全局状态 window.chatbotFloatingPosition = { x: newX, y: newY }; window.chatbotFloatingSize = { width: newWidth, height: newHeight }; localStorage.setItem('chatbotFloatingPosition', JSON.stringify(window.chatbotFloatingPosition)); localStorage.setItem('chatbotFloatingSize', JSON.stringify(window.chatbotFloatingSize)); // 应用新的位置和大小 const chatbotWindow = modal.querySelector('.chatbot-window'); if (chatbotWindow) { chatbotWindow.style.left = newX + 'px'; chatbotWindow.style.top = newY + 'px'; chatbotWindow.style.width = newWidth + 'px'; chatbotWindow.style.height = newHeight + 'px'; } } function handleResizeEnd() { if (isResizing) { isResizing = false; resizeDirection = ''; document.body.classList.remove('chatbot-dragging'); } } // 绑定拖拽移动事件 (标题栏) modal.addEventListener('mousedown', function(e) { if (e.target.closest('.chatbot-draggable-header')) { handleDragStart(e); } else if (e.target.closest('.chatbot-resize-handle')) { handleResizeStart(e); } }); // 全局鼠标移动和释放事件 document.addEventListener('mousemove', function(e) { if (isDragging) { handleDragMove(e); } else if (isResizing) { handleResizeMove(e); } }); document.addEventListener('mouseup', function() { handleDragEnd(); handleResizeEnd(); }); // ========================================== // Phase 3: 初始化消息事件管理器(事件委托) // ========================================== if (window.ChatMessageEventManager) { try { window.chatMessageEventManager = new ChatMessageEventManager('#chatbot-body'); console.log('[ChatbotUI] ✅ Phase 3: 消息事件管理器已初始化(事件委托模式)'); } catch (error) { console.error('[ChatbotUI] ❌ Phase 3: 消息事件管理器初始化失败:', error); } } else { console.warn('[ChatbotUI] ⚠️ Phase 3: ChatMessageEventManager 类未加载,将使用内联事件(回滚模式)'); } } // 将核心函数挂载到 window 对象和 ChatbotUI 命名空间下,便于外部调用 window.handleChatbotSend = handleChatbotSend; window.handleChatbotStop = handleChatbotStop; // handlePresetQuestion 使用 ChatbotPreset 中的版本(包含完整的 prompt 注入逻辑) window.handlePresetQuestion = window.ChatbotPreset?.handlePresetQuestion || function(q) { // 降级方案:如果 ChatbotPreset 未加载,使用简单版本 const input = document.getElementById('chatbot-input'); if (!input) return; input.value = q; if (typeof window.handleChatbotSend === 'function') window.handleChatbotSend(); }; window.ChatbotUI = { updateChatbotUI, initChatbotUI }; // 当DOM内容加载完成后,执行初始化函数 // 这是确保所有需要的DOM元素都已存在后再进行操作的标准做法 if (document.readyState === 'loading') { // 如果文档仍在加载中,则等待 DOMContentLoaded 事件 document.addEventListener('DOMContentLoaded', initChatbotUI); } else { // 如果文档已经加载完毕,则直接执行初始化 initChatbotUI(); }