class SnapSolver { constructor() { this.initializeElements(); this.initializeState(); this.setupEventListeners(); this.initializeConnection(); this.setupAutoScroll(); // Initialize managers window.uiManager = new UIManager(); window.settingsManager = new SettingsManager(); } initializeElements() { // Capture elements this.captureBtn = document.getElementById('captureBtn'); this.cropBtn = document.getElementById('cropBtn'); this.connectionStatus = document.getElementById('connectionStatus'); this.screenshotImg = document.getElementById('screenshotImg'); this.cropContainer = document.getElementById('cropContainer'); this.imagePreview = document.getElementById('imagePreview'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); this.extractTextBtn = document.getElementById('extractText'); this.textEditor = document.getElementById('textEditor'); this.extractedText = document.getElementById('extractedText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); this.responseContent = document.getElementById('responseContent'); this.claudePanel = document.getElementById('claudePanel'); this.statusLight = document.querySelector('.status-light'); } initializeState() { this.socket = null; this.cropper = null; this.croppedImage = null; this.history = JSON.parse(localStorage.getItem('snapHistory') || '[]'); } 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(connected) { this.connectionStatus.textContent = connected ? 'Connected' : 'Disconnected'; this.connectionStatus.className = `status ${connected ? 'connected' : 'disconnected'}`; this.captureBtn.disabled = !connected; if (!connected) { this.imagePreview.classList.add('hidden'); this.cropBtn.classList.add('hidden'); this.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.add('hidden'); this.textEditor.classList.add('hidden'); } } updateStatusLight(status) { 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 { this.socket = io(window.location.origin); this.socket.on('connect', () => { console.log('Connected to server'); this.updateConnectionStatus(true); }); this.socket.on('disconnect', () => { console.log('Disconnected from server'); this.updateConnectionStatus(false); this.socket = null; setTimeout(() => this.initializeConnection(), 5000); }); this.setupSocketEventHandlers(); } catch (error) { console.error('Connection error:', error); this.updateConnectionStatus(false); setTimeout(() => this.initializeConnection(), 5000); } } setupSocketEventHandlers() { // Screenshot response handler this.socket.on('screenshot_response', (data) => { if (data.success) { this.screenshotImg.src = `data:image/png;base64,${data.image}`; this.imagePreview.classList.remove('hidden'); this.cropBtn.classList.remove('hidden'); this.captureBtn.disabled = false; this.captureBtn.innerHTML = 'Capture'; this.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.add('hidden'); this.textEditor.classList.add('hidden'); window.showToast('Screenshot captured successfully'); } else { window.showToast('Failed to capture screenshot: ' + data.error, 'error'); this.captureBtn.disabled = false; this.captureBtn.innerHTML = 'Capture'; } }); // Text extraction response handler this.socket.on('text_extraction_response', (data) => { if (data.success) { this.extractedText.value = data.text; this.textEditor.classList.remove('hidden'); window.showToast('Text extracted successfully'); } else { window.showToast('Failed to extract text: ' + data.error, 'error'); } this.extractTextBtn.disabled = false; this.extractTextBtn.innerHTML = 'Extract Text'; }); this.socket.on('claude_response', (data) => { console.log('Received claude_response:', data); this.updateStatusLight(data.status); switch (data.status) { case 'started': console.log('Analysis started'); this.responseContent.textContent = ''; this.sendToClaudeBtn.disabled = true; this.sendExtractedTextBtn.disabled = true; break; case 'streaming': if (data.content) { console.log('Received content:', data.content); this.responseContent.textContent += data.content; } break; case 'completed': console.log('Analysis completed'); this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; this.addToHistory(this.croppedImage, this.responseContent.textContent); window.showToast('Analysis completed successfully'); break; case 'error': console.error('Claude analysis error:', data.error); const errorMessage = data.error || 'Unknown error occurred'; this.responseContent.textContent += '\nError: ' + errorMessage; this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; window.showToast('Analysis failed: ' + errorMessage, 'error'); break; default: console.warn('Unknown response status:', data.status); if (data.error) { this.responseContent.textContent += '\nError: ' + data.error; this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; window.showToast('Unknown error occurred', 'error'); } } }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); this.updateConnectionStatus(false); this.socket = null; setTimeout(() => this.initializeConnection(), 5000); }); } initializeCropper() { try { // Clean up existing cropper instance if (this.cropper) { this.cropper.destroy(); this.cropper = null; } const cropArea = document.querySelector('.crop-area'); 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: 'crop', autoCropArea: 0, restore: false, modal: true, guides: true, highlight: true, cropBoxMovable: true, cropBoxResizable: true, toggleDragModeOnDblclick: false, minCropBoxWidth: 50, minCropBoxHeight: 50, background: true, responsive: true, checkOrientation: true, ready: function() { // Use the stored reference to this if (self.cropper) { self.cropper.crop(); } } }); } catch (error) { console.error('Error initializing cropper:', error); window.showToast('Failed to initialize cropper', 'error'); } } addToHistory(imageData, response) { const historyItem = { id: Date.now(), timestamp: new Date().toISOString(), image: imageData, response: response }; this.history.unshift(historyItem); if (this.history.length > 10) this.history.pop(); localStorage.setItem('snapHistory', JSON.stringify(this.history)); window.renderHistory(); } setupEventListeners() { this.setupCaptureEvents(); this.setupCropEvents(); this.setupAnalysisEvents(); this.setupKeyboardShortcuts(); } setupCaptureEvents() { // Capture button this.captureBtn.addEventListener('click', async () => { if (!this.socket || !this.socket.connected) { window.showToast('Not connected to server', 'error'); return; } try { this.captureBtn.disabled = true; this.captureBtn.innerHTML = 'Capturing...'; this.socket.emit('request_screenshot'); } catch (error) { window.showToast('Error requesting screenshot: ' + error.message, 'error'); this.captureBtn.disabled = false; this.captureBtn.innerHTML = 'Capture'; } }); } setupCropEvents() { // Crop button this.cropBtn.addEventListener('click', () => { if (this.screenshotImg.src) { this.initializeCropper(); } }); // Crop confirm button document.getElementById('cropConfirm').addEventListener('click', () => { 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.cropBtn.classList.remove('hidden'); this.sendToClaudeBtn.classList.remove('hidden'); this.extractTextBtn.classList.remove('hidden'); window.showToast('Image cropped successfully'); } catch (error) { console.error('Cropping error details:', { message: error.message, stack: error.stack, cropperState: this.cropper ? 'initialized' : 'not initialized' }); window.showToast(error.message || 'Error while cropping image', '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.sendToClaudeBtn.classList.add('hidden'); this.extractTextBtn.classList.add('hidden'); document.querySelector('.crop-area').innerHTML = ''; }); } setupAnalysisEvents() { // Extract Text button this.extractTextBtn.addEventListener('click', () => { if (!this.croppedImage) { window.showToast('Please crop the image first', 'error'); return; } this.extractTextBtn.disabled = true; this.extractTextBtn.innerHTML = 'Extracting...'; try { this.socket.emit('extract_text', { image: this.croppedImage.split(',')[1] }); } catch (error) { window.showToast('Failed to extract text: ' + error.message, 'error'); this.extractTextBtn.disabled = false; this.extractTextBtn.innerHTML = 'Extract Text'; } }); // Send Extracted Text button this.sendExtractedTextBtn.addEventListener('click', () => { const text = this.extractedText.value.trim(); if (!text) { window.showToast('Please enter some text', 'error'); return; } const settings = window.settingsManager.getSettings(); const apiKey = window.settingsManager.getApiKey(); if (!apiKey) { this.settingsPanel.classList.remove('hidden'); return; } this.claudePanel.classList.remove('hidden'); this.responseContent.textContent = ''; this.sendExtractedTextBtn.disabled = true; try { this.socket.emit('analyze_text', { text: text, settings: { apiKey: apiKey, model: settings.model || 'claude-3-5-sonnet-20241022', temperature: parseFloat(settings.temperature) || 0.7, systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.', proxyEnabled: settings.proxyEnabled || false, proxyHost: settings.proxyHost || '127.0.0.1', proxyPort: settings.proxyPort || '4780' } }); } catch (error) { this.responseContent.textContent = 'Error: Failed to send text for analysis - ' + error.message; this.sendExtractedTextBtn.disabled = false; window.showToast('Failed to send text for analysis', 'error'); } }); // Send to Claude button this.sendToClaudeBtn.addEventListener('click', () => { if (!this.croppedImage) { window.showToast('Please crop the image first', 'error'); return; } const settings = window.settingsManager.getSettings(); const apiKey = window.settingsManager.getApiKey(); if (!apiKey) { this.settingsPanel.classList.remove('hidden'); return; } this.claudePanel.classList.remove('hidden'); this.responseContent.textContent = ''; this.sendToClaudeBtn.disabled = true; try { this.socket.emit('analyze_image', { image: this.croppedImage.split(',')[1], settings: { apiKey: apiKey, model: settings.model || 'claude-3-5-sonnet-20241022', temperature: parseFloat(settings.temperature) || 0.7, systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.', proxyEnabled: settings.proxyEnabled || false, proxyHost: settings.proxyHost || '127.0.0.1', proxyPort: settings.proxyPort || '4780' } }); } catch (error) { this.responseContent.textContent = 'Error: Failed to send image for analysis - ' + error.message; this.sendToClaudeBtn.disabled = false; window.showToast('Failed to send image for analysis', 'error'); } }); } setupKeyboardShortcuts() { // Keyboard shortcuts for capture and crop document.addEventListener('keydown', (e) => { if (e.ctrlKey || e.metaKey) { switch(e.key) { case 'c': if (!this.captureBtn.disabled) this.captureBtn.click(); break; case 'x': if (!this.cropBtn.disabled) this.cropBtn.click(); break; } } }); } } // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { window.app = new SnapSolver(); }); // Global function for history rendering window.renderHistory = function() { const content = document.querySelector('.history-content'); const history = JSON.parse(localStorage.getItem('snapHistory') || '[]'); if (history.length === 0) { content.innerHTML = `

No history yet

`; return; } content.innerHTML = history.map(item => `
${new Date(item.timestamp).toLocaleString()}
Historical screenshot
`).join(''); // Add click handlers for history items content.querySelectorAll('.delete-history').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const id = parseInt(btn.dataset.id); const updatedHistory = history.filter(item => item.id !== id); localStorage.setItem('snapHistory', JSON.stringify(updatedHistory)); window.renderHistory(); window.showToast('History item deleted'); }); }); content.querySelectorAll('.history-item').forEach(item => { item.addEventListener('click', () => { const historyItem = history.find(h => h.id === parseInt(item.dataset.id)); if (historyItem) { window.app.screenshotImg.src = historyItem.image; window.app.imagePreview.classList.remove('hidden'); document.getElementById('historyPanel').classList.add('hidden'); window.app.cropBtn.classList.add('hidden'); window.app.captureBtn.classList.add('hidden'); window.app.sendToClaudeBtn.classList.add('hidden'); window.app.extractTextBtn.classList.add('hidden'); if (historyItem.response) { window.app.claudePanel.classList.remove('hidden'); window.app.responseContent.textContent = historyItem.response; } } }); }); };