class SnapSolver { constructor() { console.log('Creating SnapSolver instance...'); // 初始化属性 this.socket = null; this.socketId = null; this.isProcessing = false; this.cropper = null; this.autoScrollInterval = null; this.capturedImage = null; // 存储截图的base64数据 this.userThinkingExpanded = false; // 用户思考过程展开状态偏好 this.originalImage = null; this.croppedImage = null; this.extractedContent = ''; this.eventsSetup = false; // Cache DOM elements this.captureBtn = document.getElementById('captureBtn'); // 移除裁剪按钮引用 this.screenshotImg = document.getElementById('screenshotImg'); this.imagePreview = document.getElementById('imagePreview'); this.emptyState = document.getElementById('emptyState'); this.extractedText = document.getElementById('extractedText'); this.cropContainer = document.getElementById('cropContainer'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); this.extractTextBtn = document.getElementById('extractText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); this.claudePanel = document.getElementById('claudePanel'); this.responseContent = document.getElementById('responseContent'); this.thinkingContent = document.getElementById('thinkingContent'); this.thinkingSection = document.getElementById('thinkingSection'); this.thinkingToggle = document.getElementById('thinkingToggle'); this.connectionStatus = document.getElementById('connectionStatus'); this.statusLight = document.querySelector('.status-light'); this.progressLine = document.querySelector('.progress-line'); this.statusText = document.querySelector('.status-text'); this.analysisIndicator = document.querySelector('.analysis-indicator'); // Crop elements this.cropCancel = document.getElementById('cropCancel'); this.cropConfirm = document.getElementById('cropConfirm'); // 初始化应用 this.initialize(); } initializeElements() { // Main elements this.screenshotImg = document.getElementById('screenshotImg'); this.imagePreview = document.getElementById('imagePreview'); this.emptyState = document.getElementById('emptyState'); // 移除对裁剪按钮的引用 this.captureBtn = document.getElementById('captureBtn'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); this.extractTextBtn = document.getElementById('extractText'); this.extractedText = document.getElementById('extractedText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); this.cropContainer = document.getElementById('cropContainer'); this.manualTextInput = document.getElementById('manualTextInput'); this.claudePanel = document.getElementById('claudePanel'); this.responseContent = document.getElementById('responseContent'); this.thinkingSection = document.getElementById('thinkingSection'); this.thinkingContent = document.getElementById('thinkingContent'); this.thinkingToggle = document.getElementById('thinkingToggle'); this.connectionStatus = document.getElementById('connectionStatus'); this.statusLight = document.querySelector('.status-light'); this.progressLine = document.querySelector('.progress-line'); this.statusText = document.querySelector('.status-text'); this.analysisIndicator = document.querySelector('.analysis-indicator'); // Crop elements this.cropCancel = document.getElementById('cropCancel'); this.cropConfirm = document.getElementById('cropConfirm'); } initializeState() { this.socket = null; this.cropper = null; this.croppedImage = null; this.extractedContent = ''; // 确保裁剪容器和其他面板初始为隐藏状态 if (this.cropContainer) { this.cropContainer.classList.add('hidden'); } if (this.claudePanel) { this.claudePanel.classList.add('hidden'); } if (this.thinkingSection) { this.thinkingSection.classList.add('hidden'); } } setupAutoScroll() { // Create MutationObserver to watch for content changes const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if (mutation.type === 'characterData' || mutation.type === 'childList') { this.responseContent.scrollTo({ top: this.responseContent.scrollHeight, behavior: 'smooth' }); } }); }); // Start observing the response content observer.observe(this.responseContent, { childList: true, characterData: true, subtree: true }); } updateConnectionStatus(status, isConnected) { if (this.connectionStatus) { this.connectionStatus.textContent = status; if (isConnected) { this.connectionStatus.classList.remove('disconnected'); this.connectionStatus.classList.add('connected'); } else { this.connectionStatus.classList.remove('connected'); this.connectionStatus.classList.add('disconnected'); } } } updateStatusLight(status) { // 更新状态指示器 if (this.analysisIndicator) { this.analysisIndicator.className = 'analysis-indicator'; this.progressLine.className = 'progress-line'; switch (status) { case 'started': case 'streaming': this.analysisIndicator.classList.add('processing'); this.progressLine.classList.add('processing'); this.statusText.textContent = '处理中...'; break; case 'completed': this.analysisIndicator.classList.add('completed'); this.progressLine.classList.add('completed'); this.statusText.textContent = '已完成'; break; case 'error': this.analysisIndicator.classList.add('error'); this.progressLine.classList.add('error'); this.statusText.textContent = '错误'; break; default: // 重置为默认状态 this.statusText.textContent = '准备中'; break; } } else if (this.statusLight) { // 兼容旧版状态灯 this.statusLight.className = 'status-light'; switch (status) { case 'started': case 'streaming': this.statusLight.classList.add('processing'); break; case 'completed': this.statusLight.classList.add('completed'); break; case 'error': this.statusLight.classList.add('error'); break; default: // Reset to default state break; } } } initializeConnection() { try { // 添加日志以便调试 console.log('尝试连接WebSocket服务器...'); // 确保在Safari上使用完整的URL const socketUrl = window.location.protocol === 'https:' ? `${window.location.origin}` : window.location.origin; console.log('WebSocket连接URL:', socketUrl); this.socket = io(socketUrl, { reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000, reconnectionDelayMax: 5000, timeout: 20000, autoConnect: true, transports: ['websocket', 'polling'] // 明确指定传输方式,增加兼容性 }); this.socket.on('connect', () => { console.log('Connected to server'); this.updateConnectionStatus('已连接', true); this.captureBtn.disabled = false; }); this.socket.on('disconnect', () => { console.log('Disconnected from server'); this.updateConnectionStatus('已断开', false); }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); this.updateConnectionStatus('连接错误', false); }); this.socket.on('reconnect', (attemptNumber) => { console.log(`Reconnected after ${attemptNumber} attempts`); this.updateConnectionStatus('已重连', true); }); this.socket.on('reconnect_attempt', (attemptNumber) => { console.log(`Reconnection attempt: ${attemptNumber}`); }); this.socket.on('reconnect_error', (error) => { console.error('Reconnection error:', error); }); this.socket.on('reconnect_failed', () => { console.error('Failed to reconnect'); window.uiManager.showToast('Connection to server failed, please refresh the page and try again', 'error'); }); this.setupSocketEventHandlers(); } catch (error) { console.error('Connection error:', error); this.updateConnectionStatus('重连失败', false); // 移除setTimeout,让用户手动刷新页面重连 window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error'); } } setupSocketEventHandlers() { // 如果已经设置过事件处理器,先移除它们 if (this.hasOwnProperty('eventsSetup') && this.eventsSetup) { this.socket.off('screenshot_response'); this.socket.off('screenshot_complete'); this.socket.off('request_acknowledged'); this.socket.off('text_extracted'); this.socket.off('claude_response'); } // 标记事件处理器已设置 this.eventsSetup = true; // 旧版截图响应处理器 (保留兼容性) this.socket.on('screenshot_response', (data) => { if (data.success) { this.screenshotImg.src = `data:image/png;base64,${data.image}`; this.originalImage = `data:image/png;base64,${data.image}`; this.imagePreview.classList.remove('hidden'); this.emptyState.classList.add('hidden'); // 根据模型类型显示适当的按钮 this.updateImageActionButtons(); // 恢复按钮状态 this.captureBtn.disabled = false; this.captureBtn.innerHTML = ''; // 初始化裁剪器 this.initializeCropper(); window.uiManager.showToast('截图成功', 'success'); } else { this.captureBtn.disabled = false; this.captureBtn.innerHTML = ''; console.error('截图失败:', data.error); window.uiManager.showToast('截图失败: ' + data.error, 'error'); } }); // 新版截图响应处理器 this.socket.on('screenshot_complete', (data) => { this.captureBtn.disabled = false; this.captureBtn.innerHTML = ''; if (data.success) { // 显示截图预览 this.screenshotImg.src = 'data:image/png;base64,' + data.image; this.originalImage = 'data:image/png;base64,' + data.image; this.imagePreview.classList.remove('hidden'); this.emptyState.classList.add('hidden'); // 根据模型类型显示适当的按钮 this.updateImageActionButtons(); // 初始化裁剪工具 this.initializeCropper(); // 显示成功消息 window.uiManager.showToast('截图成功', 'success'); } else { // 显示错误消息 window.uiManager.showToast('截图失败: ' + (data.error || '未知错误'), 'error'); } }); // 确认请求处理 this.socket.on('request_acknowledged', (data) => { console.log('服务器确认收到请求:', data); }); // Text extraction response this.socket.on('text_extracted', (data) => { // 重新启用按钮 this.extractTextBtn.disabled = false; this.extractTextBtn.innerHTML = '提取文本'; if (this.extractedText) { this.extractedText.disabled = false; } // 检查是否有内容数据 if (data.content) { this.extractedText.value = data.content; this.extractedContent = data.content; this.extractedText.classList.remove('hidden'); this.sendExtractedTextBtn.classList.remove('hidden'); this.sendExtractedTextBtn.disabled = false; window.uiManager.showToast('文本提取成功', 'success'); } else if (data.error) { console.error('文本提取失败:', data.error); window.uiManager.showToast('文本提取失败: ' + data.error, 'error'); // 启用发送按钮以便用户可以手动输入文本 this.sendExtractedTextBtn.disabled = false; } else { // 未知响应格式 console.error('未知的文本提取响应格式:', data); window.uiManager.showToast('文本提取返回未知格式', 'error'); this.sendExtractedTextBtn.disabled = false; } }); this.socket.on('claude_response', (data) => { console.log('Received claude_response:', data); this.updateStatusLight(data.status); // 确保Claude面板可见 if (this.claudePanel && this.claudePanel.classList.contains('hidden')) { this.claudePanel.classList.remove('hidden'); } switch (data.status) { case 'started': console.log('Analysis started'); // 清空显示内容 if (this.responseContent) this.responseContent.innerHTML = ''; if (this.thinkingContent) this.thinkingContent.innerHTML = ''; if (this.thinkingSection) this.thinkingSection.classList.add('hidden'); // 禁用按钮防止重复点击 if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = true; if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = true; // 显示进行中状态 if (this.responseContent) { this.responseContent.innerHTML = '
分析进行中,请稍候...
'; this.responseContent.style.display = 'block'; } break; case 'thinking': // 处理思考内容 if (data.content && this.thinkingContent && this.thinkingSection) { console.log('Received thinking content'); this.thinkingSection.classList.remove('hidden'); // 显示动态省略号 this.showThinkingAnimation(true); // 直接设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } // 更新切换按钮图标 const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; case 'reasoning': // 处理推理内容 (QVQ-Max模型使用) if (data.content && this.thinkingContent && this.thinkingSection) { console.log('Received reasoning content'); this.thinkingSection.classList.remove('hidden'); // 显示动态省略号 this.showThinkingAnimation(true); // 直接设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } // 更新切换按钮图标 const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; case 'thinking_complete': // 完整的思考内容 if (data.content && this.thinkingContent && this.thinkingSection) { console.log('思考过程完成'); this.thinkingSection.classList.remove('hidden'); // 停止动态省略号 this.showThinkingAnimation(false); // 设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 移除打字动画 this.thinkingContent.classList.remove('thinking-typing'); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; case 'reasoning_complete': // 完整的推理内容 (QVQ-Max模型使用) if (data.content && this.thinkingContent && this.thinkingSection) { console.log('Reasoning complete'); this.thinkingSection.classList.remove('hidden'); // 停止动态省略号 this.showThinkingAnimation(false); // 设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 移除打字动画 this.thinkingContent.classList.remove('thinking-typing'); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } } break; case 'streaming': if (data.content && this.responseContent) { console.log('Received content chunk'); // 使用更安全的方式设置内容,避免HTML解析问题 this.setElementContent(this.responseContent, data.content); this.responseContent.style.display = 'block'; // 停止省略号动画 this.showThinkingAnimation(false); // 移除思考部分的打字动画 if (this.thinkingContent) { this.thinkingContent.classList.remove('thinking-typing'); } // 平滑滚动到最新内容 this.scrollToBottom(); } break; case 'completed': console.log('Analysis completed'); // 重新启用按钮 if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; // 恢复界面 this.updateStatusLight('completed'); // 只有在有思考内容时才显示思考组件 if (this.thinkingSection && this.thinkingContent) { // 检查思考内容是否为空 const hasThinkingContent = this.thinkingContent.textContent && this.thinkingContent.textContent.trim() !== ''; if (hasThinkingContent) { // 有思考内容,显示思考组件 this.thinkingSection.classList.remove('hidden'); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } const toggleBtn = document.querySelector('#thinkingToggle .toggle-btn i'); if (toggleBtn) { toggleBtn.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } // 简化提示信息 window.uiManager.showToast('分析完成', 'success'); } else { // 没有思考内容,隐藏思考组件 this.thinkingSection.classList.add('hidden'); window.uiManager.showToast('分析完成', 'success'); } } // 确保响应内容完整显示 if (data.content && data.content.trim() !== '' && this.responseContent) { this.setElementContent(this.responseContent, data.content); } // 确保结果内容可见 if (this.responseContent) { this.responseContent.style.display = 'block'; // 滚动到结果内容底部 this.scrollToBottom(); } break; case 'error': console.error('Analysis error:', data.error); const errorMessage = data.error || 'Unknown error occurred'; // 显示错误信息 if (errorMessage && this.responseContent) { const currentText = this.responseContent.textContent || ''; this.setElementContent(this.responseContent, currentText + '\nError: ' + errorMessage); } // 重新启用按钮 if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; window.uiManager.showToast('Analysis failed: ' + errorMessage, 'error'); break; default: console.warn('Unknown response status:', data.status); // 对于未知状态,尝试显示内容(如果有) if (data.content && this.responseContent) { this.setElementContent(this.responseContent, data.content); this.responseContent.style.display = 'block'; } // 确保按钮可用 if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; window.uiManager.showToast('Unknown error occurred', 'error'); break; } }); // 接收到thinking数据时 this.socket.on('thinking', (data) => { console.log('收到思考过程数据'); // 显示思考区域 this.thinkingSection.classList.remove('hidden'); // 显示动态省略号 this.showThinkingAnimation(true); // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } }); // 思考过程完成 - Socket事件处理 this.socket.on('thinking_complete', (data) => { console.log('Socket接收到思考过程完成'); this.thinkingSection.classList.remove('hidden'); // 停止动态省略号动画 this.showThinkingAnimation(false); // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } }); // 分析完成 this.socket.on('analysis_complete', (data) => { console.log('分析完成,接收到结果'); this.updateStatusLight('completed'); this.enableInterface(); // 显示分析结果 if (this.responseContent) { // 使用setElementContent方法处理Markdown this.setElementContent(this.responseContent, data.response); this.responseContent.style.display = 'block'; // 直接滚动到结果区域,不使用setTimeout this.responseContent.scrollIntoView({ behavior: 'smooth' }); } // 确保思考部分完全显示(如果有的话) if (data.thinking && this.thinkingSection && this.thinkingContent) { this.thinkingSection.classList.remove('hidden'); // 使用setElementContent方法处理Markdown this.setElementContent(this.thinkingContent, data.thinking); // 根据用户偏好设置展开/折叠状态 this.thinkingContent.classList.remove('expanded'); this.thinkingContent.classList.remove('collapsed'); if (this.userThinkingExpanded) { this.thinkingContent.classList.add('expanded'); } else { this.thinkingContent.classList.add('collapsed'); } const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = this.userThinkingExpanded ? 'fas fa-chevron-up' : 'fas fa-chevron-down'; } // 弹出提示 window.uiManager.showToast('分析完成', 'success'); } }); } // 新方法:安全设置DOM内容的方法(替代updateElementContent) setElementContent(element, content) { if (!element) return; try { // 检查marked是否已配置 if (typeof marked === 'undefined') { console.warn('Marked库未加载,回退到纯文本显示'); element.textContent = content; return; } // 使用marked库解析Markdown内容 const renderedHtml = marked.parse(content); // 设置解析后的HTML内容 element.innerHTML = renderedHtml; // 为未高亮的代码块应用语法高亮 if (window.hljs) { element.querySelectorAll('pre code:not(.hljs)').forEach((block) => { hljs.highlightElement(block); }); } } catch (error) { console.error('Markdown解析错误:', error); // 发生错误时回退到纯文本显示 element.textContent = content; } // 自动滚动到底部 element.scrollTop = element.scrollHeight; } initializeCropper() { try { // 如果当前没有截图,不要初始化裁剪器 if (!this.screenshotImg || !this.screenshotImg.src || this.screenshotImg.src === '') { console.log('No screenshot to crop'); return; } // Clean up existing cropper instance if (this.cropper) { this.cropper.destroy(); this.cropper = null; } const cropArea = document.querySelector('.crop-area'); if (!cropArea) { console.error('Crop area element not found'); return; } cropArea.innerHTML = ''; const clonedImage = this.screenshotImg.cloneNode(true); clonedImage.style.display = 'block'; cropArea.appendChild(clonedImage); this.cropContainer.classList.remove('hidden'); // Store reference to this for use in ready callback const self = this; this.cropper = new Cropper(clonedImage, { viewMode: 1, dragMode: 'move', aspectRatio: NaN, modal: true, background: true }); } catch (error) { console.error('Failed to initialize cropper', error); window.uiManager.showToast('裁剪器初始化失败', 'error'); // 确保在出错时关闭裁剪界面 if (this.cropContainer) { this.cropContainer.classList.add('hidden'); } } } setupEventListeners() { this.setupCaptureEvents(); this.setupCropEvents(); this.setupAnalysisEvents(); this.setupKeyboardShortcuts(); this.setupThinkingToggle(); // 监听模型选择变化,更新界面 if (window.settingsManager && window.settingsManager.modelSelect) { window.settingsManager.modelSelect.addEventListener('change', () => { this.updateImageActionButtons(); }); } } setupCaptureEvents() { // 截图按钮 this.captureBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; try { this.captureBtn.disabled = true; // 禁用按钮防止重复点击 this.captureBtn.innerHTML = ''; this.socket.emit('capture_screenshot', {}); } catch (error) { console.error('Error starting capture:', error); window.uiManager.showToast('启动截图失败', 'error'); this.captureBtn.disabled = false; this.captureBtn.innerHTML = ''; } }); } setupCropEvents() { // 移除裁剪按钮的点击事件监听 // Crop confirm button document.getElementById('cropConfirm').addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; if (this.cropper) { try { console.log('Starting crop operation...'); // Validate cropper instance if (!this.cropper) { throw new Error('Cropper not initialized'); } // Get and validate crop box data const cropBoxData = this.cropper.getCropBoxData(); console.log('Crop box data:', cropBoxData); if (!cropBoxData || typeof cropBoxData.width !== 'number' || typeof cropBoxData.height !== 'number') { throw new Error('Invalid crop box data'); } if (cropBoxData.width < 10 || cropBoxData.height < 10) { throw new Error('Crop area is too small. Please select a larger area (minimum 10x10 pixels).'); } // Get cropped canvas with more conservative size limits console.log('Getting cropped canvas...'); const canvas = this.cropper.getCroppedCanvas({ maxWidth: 2560, maxHeight: 1440, fillColor: '#fff', imageSmoothingEnabled: true, imageSmoothingQuality: 'high', }); if (!canvas) { throw new Error('Failed to create cropped canvas'); } console.log('Canvas created successfully'); // Convert to data URL with error handling and compression console.log('Converting to data URL...'); try { // Use PNG for better quality this.croppedImage = canvas.toDataURL('image/png'); console.log('Data URL conversion successful'); } catch (dataUrlError) { console.error('Data URL conversion error:', dataUrlError); throw new Error('Failed to process cropped image. The image might be too large or memory insufficient.'); } // Properly destroy the cropper instance this.cropper.destroy(); this.cropper = null; // Clean up cropper and update UI this.cropContainer.classList.add('hidden'); document.querySelector('.crop-area').innerHTML = ''; // Update the screenshot image with the cropped version this.screenshotImg.src = this.croppedImage; this.imagePreview.classList.remove('hidden'); // 根据当前选择的模型类型决定显示哪些按钮 this.updateImageActionButtons(); window.uiManager.showToast('裁剪成功'); } catch (error) { console.error('Cropping error details:', { message: error.message, stack: error.stack, cropperState: this.cropper ? 'initialized' : 'not initialized' }); window.uiManager.showToast(error.message || '裁剪图像时出错', 'error'); } finally { // Always clean up the cropper instance if (this.cropper) { this.cropper.destroy(); this.cropper = null; } } } }); // Crop cancel button document.getElementById('cropCancel').addEventListener('click', () => { if (this.cropper) { this.cropper.destroy(); this.cropper = null; } this.cropContainer.classList.add('hidden'); // 取消裁剪时隐藏图像预览和相关按钮 this.imagePreview.classList.add('hidden'); document.querySelector('.crop-area').innerHTML = ''; }); } setupAnalysisEvents() { // Extract Text button this.extractTextBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; if (!this.croppedImage) { window.uiManager.showToast('请先裁剪图片', 'error'); return; } this.extractTextBtn.disabled = true; this.sendExtractedTextBtn.disabled = true; // Disable the send button while extracting this.extractTextBtn.innerHTML = '提取中...'; const settings = window.settingsManager.getSettings(); const mathpixApiKey = settings.mathpixApiKey; if (!mathpixApiKey || mathpixApiKey === ':') { window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error'); document.getElementById('settingsPanel').classList.remove('hidden'); this.extractTextBtn.disabled = false; this.extractTextBtn.innerHTML = '提取文本'; return; } // 显示文本框和按钮 this.extractedText.classList.remove('hidden'); this.sendExtractedTextBtn.classList.remove('hidden'); if (this.extractedText) { this.extractedText.value = '正在提取文本...'; this.extractedText.disabled = true; } try { this.socket.emit('extract_text', { image: this.croppedImage.split(',')[1], settings: { mathpixApiKey: mathpixApiKey } }); // 监听服务器确认请求的响应 this.socket.once('request_acknowledged', (ackResponse) => { console.log('服务器确认收到文本提取请求', ackResponse); }); } catch (error) { window.uiManager.showToast('提取文本失败: ' + error.message, 'error'); this.extractTextBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; this.extractTextBtn.innerHTML = '提取文本'; if (this.extractedText) { this.extractedText.disabled = false; } } }); // Send Extracted Text button this.sendExtractedTextBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; // 防止重复点击 if (this.sendExtractedTextBtn.disabled) return; const text = this.extractedText.value.trim(); if (!text) { window.uiManager.showToast('请输入一些文本', 'error'); return; } const settings = window.settingsManager.getSettings(); const apiKeys = {}; Object.keys(window.settingsManager.apiKeyInputs).forEach(keyId => { const input = window.settingsManager.apiKeyInputs[keyId]; if (input && input.value) { apiKeys[keyId] = input.value; } }); console.log("Debug - 发送文本分析API密钥:", apiKeys); // 清空之前的结果 this.responseContent.innerHTML = ''; this.thinkingContent.innerHTML = ''; // 显示Claude分析面板 this.claudePanel.classList.remove('hidden'); // 禁用按钮防止重复点击 this.sendExtractedTextBtn.disabled = true; try { this.socket.emit('analyze_text', { text: text, settings: { ...settings, apiKeys: apiKeys, model: settings.model || 'claude-3-7-sonnet-20250219', modelInfo: settings.modelInfo || {}, modelCapabilities: { supportsMultimodal: settings.modelInfo?.supportsMultimodal || false, isReasoning: settings.modelInfo?.isReasoning || false } } }); } catch (error) { this.responseContent.textContent = 'Error: Failed to send text for analysis - ' + error.message; this.sendExtractedTextBtn.disabled = false; window.uiManager.showToast('发送文本进行分析失败', 'error'); } }); // Send to Claude button this.sendToClaudeBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; // 防止重复点击 if (this.sendToClaudeBtn.disabled) return; this.sendToClaudeBtn.disabled = true; // 获取当前模型设置 const settings = window.settingsManager.getSettings(); const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false; const modelName = settings.model || '未知'; if (!isMultimodalModel) { window.uiManager.showToast(`当前选择的模型 ${modelName} 不支持图像分析。请先提取文本或切换到支持多模态的模型。`, 'error'); this.sendToClaudeBtn.disabled = false; return; } if (this.croppedImage) { try { // 清空之前的结果 this.responseContent.innerHTML = ''; this.thinkingContent.innerHTML = ''; // 显示Claude分析面板 this.claudePanel.classList.remove('hidden'); // 发送图片进行分析 this.sendImageToClaude(this.croppedImage); } catch (error) { console.error('Error:', error); window.uiManager.showToast('发送图片失败: ' + error.message, 'error'); this.sendToClaudeBtn.disabled = false; } } else { window.uiManager.showToast('请先裁剪图片', 'error'); this.sendToClaudeBtn.disabled = false; } }); // Handle Claude panel close button const closeClaudePanel = document.getElementById('closeClaudePanel'); if (closeClaudePanel) { closeClaudePanel.addEventListener('click', () => { this.claudePanel.classList.add('hidden'); // 如果图像预览也被隐藏,显示空状态 if (this.imagePreview.classList.contains('hidden')) { this.emptyState.classList.remove('hidden'); } // 重置状态指示灯 this.updateStatusLight(''); // 清空响应内容,准备下一次分析 this.responseContent.innerHTML = ''; // 隐藏思考部分 this.thinkingSection.classList.add('hidden'); this.thinkingContent.innerHTML = ''; this.thinkingContent.className = 'thinking-content collapsed'; }); } } setupThinkingToggle() { // 确保正确获取DOM元素 const thinkingSection = document.getElementById('thinkingSection'); const thinkingToggle = document.getElementById('thinkingToggle'); const thinkingContent = document.getElementById('thinkingContent'); if (!thinkingToggle || !thinkingContent) { console.error('思考切换组件未找到必要的DOM元素'); return; } // 初始化时隐藏动态省略号 this.showThinkingAnimation(false); // 存储DOM引用 this.thinkingSection = thinkingSection; this.thinkingToggle = thinkingToggle; this.thinkingContent = thinkingContent; // 直接使用函数,确保作用域正确 thinkingToggle.onclick = () => { console.log('点击了思考标题'); // 先移除两个类,然后添加正确的类 const isExpanded = thinkingContent.classList.contains('expanded'); const toggleIcon = thinkingToggle.querySelector('.toggle-btn i'); // 从样式上清除当前状态 thinkingContent.classList.remove('expanded'); thinkingContent.classList.remove('collapsed'); if (isExpanded) { console.log('折叠思考内容'); // 添加折叠状态 thinkingContent.classList.add('collapsed'); if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-down'; } // 更新用户偏好 this.userThinkingExpanded = false; } else { console.log('展开思考内容'); // 添加展开状态 thinkingContent.classList.add('expanded'); if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-up'; } // 更新用户偏好 this.userThinkingExpanded = true; // 当展开思考内容时,确保代码高亮生效 if (window.hljs) { setTimeout(() => { thinkingContent.querySelectorAll('pre code').forEach((block) => { hljs.highlightElement(block); }); }, 100); // 添加一点延迟,确保DOM已完全更新 } } }; console.log('思考切换组件初始化完成'); } // 获取用于显示的图像URL,如果原始URL无效则返回占位符 getImageForDisplay(imageUrl) { return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl(); } sendImageToClaude(imageData) { const settings = window.settingsManager.getSettings(); // 获取API密钥 const apiKeys = {}; Object.keys(window.settingsManager.apiKeyInputs).forEach(keyId => { const input = window.settingsManager.apiKeyInputs[keyId]; if (input && input.value) { apiKeys[keyId] = input.value; } }); console.log("Debug - 发送API密钥:", apiKeys); try { // 处理图像数据,去除base64前缀 let processedImageData = imageData; if (imageData.startsWith('data:')) { // 分割数据URL,只保留base64部分 processedImageData = imageData.split(',')[1]; } this.socket.emit('analyze_image', { image: processedImageData, settings: { ...settings, apiKeys: apiKeys, model: settings.model || 'claude-3-7-sonnet-20250219', modelInfo: settings.modelInfo || {}, modelCapabilities: { supportsMultimodal: settings.modelInfo?.supportsMultimodal || false, isReasoning: settings.modelInfo?.isReasoning || false } } }); // 注意:Claude面板的显示已经在点击事件中处理,这里不再重复 } catch (error) { this.responseContent.textContent = 'Error: ' + error.message; window.uiManager.showToast('发送图片分析失败', 'error'); this.sendToClaudeBtn.disabled = false; } } initialize() { console.log('Initializing SnapSolver...'); // 初始化managers window.uiManager = new UIManager(); window.settingsManager = new SettingsManager(); window.app = this; // 便于从其他地方访问 // 初始化Markdown工具 this.initializeMarkdownTools(); // 建立与服务器的连接 this.connectToServer(); // 初始化UI元素和事件处理 this.initializeElements(); this.setupSocketEventHandlers(); this.setupCaptureEvents(); this.setupCropEvents(); this.setupAnalysisEvents(); this.setupKeyboardShortcuts(); // 确保思考切换功能正确初始化 this.setupThinkingToggle(); this.setupEventListeners(); this.setupAutoScroll(); // 监听窗口大小变化,调整界面 window.addEventListener('resize', this.handleResize.bind(this)); // 监听document点击事件,处理面板关闭 document.addEventListener('click', (e) => { // 关闭裁剪器 if (this.cropContainer && !this.cropContainer.contains(e.target) && !e.target.matches('#cropBtn') && !this.cropContainer.classList.contains('hidden')) { this.cropContainer.classList.add('hidden'); } }); // 监听页面卸载事件,清除所有计时器 window.addEventListener('beforeunload', this.cleanup.bind(this)); // 设置默认UI状态 this.enableInterface(); // 更新图像操作按钮 this.updateImageActionButtons(); console.log('SnapSolver initialization complete'); } // 初始化Markdown工具 initializeMarkdownTools() { // 检查marked是否可用 if (typeof marked === 'undefined') { console.warn('Marked.js 未加载,Markdown渲染将不可用'); return; } // 初始化marked设置 marked.setOptions({ gfm: true, // 启用GitHub风格的Markdown breaks: true, // 将换行符转换为
pedantic: false, // 不使用原始markdown规范 sanitize: false, // 不要过滤HTML标签,允许一些HTML smartLists: true, // 使用比原生markdown更智能的列表行为 smartypants: false, // 不要使用更智能的标点符号 xhtml: false, // 不使用自闭合标签 highlight: function(code, lang) { // 如果highlight.js不可用,直接返回代码 if (typeof hljs === 'undefined') { return code; } // 如果指定了语言且hljs支持 if (lang && hljs.getLanguage(lang)) { try { return hljs.highlight(code, { language: lang }).value; } catch (err) { console.error('代码高亮错误:', err); } } // 尝试自动检测语言 try { return hljs.highlightAuto(code).value; } catch (err) { console.error('自动语言检测错误:', err); } return code; // 使用默认编码效果 } }); // 检查highlight.js是否可用 if (typeof hljs === 'undefined') { console.warn('Highlight.js 未加载,代码高亮将不可用'); return; } // 配置hljs以支持自动语言检测 hljs.configure({ languages: ['javascript', 'python', 'java', 'cpp', 'csharp', 'html', 'css', 'json', 'xml', 'markdown', 'bash'] }); console.log('Markdown工具初始化完成'); } handleResize() { // 如果裁剪器存在,需要调整其大小和位置 if (this.cropper) { this.cropper.resize(); } // 可以在这里添加其他响应式UI调整 } enableInterface() { // 启用主要界面元素 if (this.captureBtn) { this.captureBtn.disabled = false; this.captureBtn.innerHTML = ''; } // 显示默认的空白状态 if (this.emptyState && this.imagePreview) { if (!this.originalImage) { this.emptyState.classList.remove('hidden'); this.imagePreview.classList.add('hidden'); } else { this.emptyState.classList.add('hidden'); this.imagePreview.classList.remove('hidden'); } } console.log('Interface enabled'); } connectToServer() { console.log('Connecting to server...'); // 创建Socket.IO连接 this.socket = io({ reconnectionAttempts: 5, reconnectionDelay: 1000, timeout: 20000 }); // 连接事件处理 this.socket.on('connect', () => { console.log('Connected to server'); this.updateConnectionStatus('已连接', true); // 连接后启用界面 this.enableInterface(); }); this.socket.on('disconnect', () => { console.log('Disconnected from server'); this.updateConnectionStatus('已断开', false); // 断开连接时禁用界面 if (this.captureBtn) { this.captureBtn.disabled = true; } }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); this.updateConnectionStatus('连接错误', false); }); this.socket.on('reconnect_attempt', (attemptNumber) => { console.log(`Reconnection attempt ${attemptNumber}`); this.updateConnectionStatus('正在重连...', false); }); this.socket.on('reconnect', () => { console.log('Reconnected to server'); this.updateConnectionStatus('已重连', true); // 重连后启用界面 this.enableInterface(); }); this.socket.on('reconnect_failed', () => { console.error('Failed to reconnect'); this.updateConnectionStatus('重连失败', false); window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error'); }); } isConnected() { return this.connectionStatus && this.connectionStatus.classList.contains('connected'); } checkConnectionBeforeAction(action) { if (!this.isConnected()) { window.uiManager.showToast('未连接到服务器,请等待连接建立后再试', 'error'); return false; } return true; } scrollToBottom() { if (this.responseContent) { // 使用平滑滚动效果 this.responseContent.scrollTo({ top: this.responseContent.scrollHeight, behavior: 'smooth' }); // 确保Claude面板也滚动到可见区域 if (this.claudePanel) { this.claudePanel.scrollIntoView({ behavior: 'smooth', block: 'end' }); } } } // 新增方法:根据所选模型更新图像操作按钮 updateImageActionButtons() { if (!window.settingsManager) { console.error('Settings manager not available'); return; } const settings = window.settingsManager.getSettings(); const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false; const modelName = settings.model || '未知'; console.log(`更新图像操作按钮 - 当前模型: ${modelName}, 是否支持多模态: ${isMultimodalModel}`); // 对于截图后的操作按钮显示逻辑 if (this.sendToClaudeBtn && this.extractTextBtn) { if (!isMultimodalModel) { // 非多模态模型:只显示提取文本按钮,隐藏发送到AI按钮 console.log('非多模态模型:隐藏"发送图片至AI"按钮'); this.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.remove('hidden'); } else { // 多模态模型:显示两个按钮 if (!this.imagePreview.classList.contains('hidden')) { // 只有在有图像时才显示按钮 console.log('多模态模型:显示全部按钮'); this.sendToClaudeBtn.classList.remove('hidden'); this.extractTextBtn.classList.remove('hidden'); } else { // 无图像时隐藏所有按钮 console.log('无图像:隐藏所有按钮'); this.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.add('hidden'); } } } else { console.warn('按钮元素不可用'); } } checkClickOutside() { // 点击其他区域时自动关闭悬浮窗 document.addEventListener('click', (e) => { // 检查是否点击在设置面板、设置按钮或其子元素之外 if ( !e.target.closest('#settingsPanel') && !e.target.matches('#settingsToggle') && !e.target.closest('#settingsToggle') && !document.getElementById('settingsPanel').classList.contains('hidden') ) { document.getElementById('settingsPanel').classList.add('hidden'); } // 检查是否点击在Claude面板、分析按钮或其子元素之外 if ( !e.target.closest('#claudePanel') && !e.target.matches('#sendToClaude') && !e.target.closest('#sendToClaude') && !e.target.matches('#extractText') && !e.target.closest('#extractText') && !e.target.matches('#sendExtractedText') && !e.target.closest('#sendExtractedText') && !this.claudePanel.classList.contains('hidden') ) { // 因为分析可能正在进行,不自动关闭Claude面板 // 但是可以考虑增加一个最小化功能 } }); } // 新增清理方法,移除计时器相关代码 cleanup() { console.log('执行清理操作...'); // 清除所有Socket监听器 if (this.socket) { this.socket.off('text_extracted'); this.socket.off('screenshot_response'); this.socket.off('screenshot_complete'); this.socket.off('request_acknowledged'); this.socket.off('claude_response'); this.socket.off('thinking'); this.socket.off('thinking_complete'); this.socket.off('analysis_complete'); } // 销毁裁剪器实例 if (this.cropper) { this.cropper.destroy(); this.cropper = null; } console.log('清理完成'); } // 空方法替代键盘快捷键实现 setupKeyboardShortcuts() { // 移动端应用不需要键盘快捷键 console.log('键盘快捷键已禁用(移动端应用)'); } // 控制思考动态省略号显示 showThinkingAnimation(show) { const dotsElement = document.querySelector('.thinking-title .dots-animation'); if (dotsElement) { if (show) { dotsElement.style.display = 'inline-block'; } else { dotsElement.style.display = 'none'; } } } } // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { try { console.log('Initializing application...'); window.app = new SnapSolver(); console.log('Application initialized successfully'); } catch (error) { console.error('Failed to initialize application:', error); // 在页面上显示错误信息 const errorDiv = document.createElement('div'); errorDiv.className = 'init-error'; errorDiv.innerHTML = `

Initialization Error

${error.message}

${error.stack}
`; document.body.appendChild(errorDiv); } });