diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0e5575c --- /dev/null +++ b/.gitignore @@ -0,0 +1,134 @@ +# Dependencies +node_modules/ +/.pnp +.pnp.js +yarn.lock +package-lock.json + +# Testing +/coverage +.nyc_output + +# Production +/build +/dist +/out + +# Development +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# TypeScript +*.tsbuildinfo + +# Next.js +.next/ +out/ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Java +*.class +*.war +*.ear +*.jar +target/ + +# Gradle +.gradle +/build/ + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml + +# Backup files +*.bak +*.swp +*.swo +*~ + +# Local development configuration files +config.local.js +config.dev.js + +# Large media files +*.mp4 +*.tiff +*.avi +*.flv +*.mov +*.wmv diff --git a/app.py b/app.py index c10fac8..e4d88f5 100644 --- a/app.py +++ b/app.py @@ -119,9 +119,10 @@ def handle_image_analysis(data): # Validate required settings if not settings.get('apiKey'): - raise ValueError("API key is required") + raise ValueError("API key is required for the selected model") - print("Using API key:", settings['apiKey'][:6] + "..." if settings.get('apiKey') else "None") + # Log with model name for better debugging + print(f"Using API key for {settings.get('model', 'unknown')}: {settings['apiKey'][:6]}...") print("Selected model:", settings.get('model', 'claude-3-5-sonnet-20241022')) # Configure proxy settings if enabled diff --git a/static/js/main.js b/static/js/main.js index 3d835eb..ada78fd 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -150,41 +150,53 @@ class SnapSolver { } initializeCropper() { - 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'); - - this.cropper = new Cropper(clonedImage, { - viewMode: 1, - dragMode: 'move', - autoCropArea: 1, - restore: false, - modal: true, - guides: true, - highlight: true, - cropBoxMovable: true, - cropBoxResizable: true, - toggleDragModeOnDblclick: false, - minContainerWidth: window.innerWidth, - minContainerHeight: window.innerHeight - 100, - minCropBoxWidth: 100, - minCropBoxHeight: 100, - background: true, - responsive: true, - checkOrientation: true, - ready: function() { - this.cropper.crop(); + 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: 'move', + autoCropArea: 0.8, + restore: false, + modal: true, + guides: true, + highlight: true, + cropBoxMovable: true, + cropBoxResizable: true, + toggleDragModeOnDblclick: false, + minContainerWidth: 800, + minContainerHeight: 600, + minCropBoxWidth: 100, + minCropBoxHeight: 100, + 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) { @@ -230,22 +242,53 @@ class SnapSolver { 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: 4096, - maxHeight: 4096, + maxWidth: 1280, + maxHeight: 720, fillColor: '#fff', imageSmoothingEnabled: true, - imageSmoothingQuality: 'high', + imageSmoothingQuality: 'low', }); if (!canvas) { throw new Error('Failed to create cropped canvas'); } - this.croppedImage = canvas.toDataURL('image/png'); - - this.cropper.destroy(); - this.cropper = null; + console.log('Canvas created successfully'); + + // Convert to data URL with error handling and compression + console.log('Converting to data URL...'); + try { + // Use lower quality for JPEG to reduce size + this.croppedImage = canvas.toDataURL('image/jpeg', 0.6); + 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.'); + } + + // Clean up cropper and update UI this.cropContainer.classList.add('hidden'); document.querySelector('.crop-area').innerHTML = ''; this.settingsPanel.classList.add('hidden'); @@ -256,8 +299,13 @@ class SnapSolver { this.sendToClaudeBtn.classList.remove('hidden'); window.showToast('Image cropped successfully'); } catch (error) { - console.error('Cropping error:', error); - window.showToast('Error while cropping image', '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'); + return; // Exit the function to prevent cleanup if error occurs } } }); @@ -281,8 +329,9 @@ class SnapSolver { } const settings = window.settingsManager.getSettings(); - if (!settings.apiKey) { - window.showToast('Please enter your API key in settings', 'error'); + const apiKey = window.settingsManager.getApiKey(); + + if (!apiKey) { this.settingsPanel.classList.remove('hidden'); return; } @@ -295,7 +344,7 @@ class SnapSolver { this.socket.emit('analyze_image', { image: this.croppedImage.split(',')[1], settings: { - apiKey: settings.apiKey, + 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.', diff --git a/static/js/settings.js b/static/js/settings.js index 8a2dfdc..f8c00e0 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -8,7 +8,6 @@ class SettingsManager { initializeElements() { // Settings panel elements this.settingsPanel = document.getElementById('settingsPanel'); - this.apiKeyInput = document.getElementById('apiKey'); this.modelSelect = document.getElementById('modelSelect'); this.temperatureInput = document.getElementById('temperature'); this.temperatureValue = document.getElementById('temperatureValue'); @@ -18,17 +17,52 @@ class SettingsManager { this.proxyPortInput = document.getElementById('proxyPort'); this.proxySettings = document.getElementById('proxySettings'); + // API Key elements + this.apiKeyInputs = { + 'claude-3-5-sonnet-20241022': document.getElementById('claudeApiKey'), + 'gpt-4o-2024-11-20': document.getElementById('gpt4oApiKey'), + 'deepseek-reasoner': document.getElementById('deepseekApiKey') + }; + // Settings toggle elements this.settingsToggle = document.getElementById('settingsToggle'); this.closeSettings = document.getElementById('closeSettings'); - this.toggleApiKey = document.getElementById('toggleApiKey'); + this.apiKeyGroups = document.querySelectorAll('.api-key-group'); + + // Initialize API key toggle buttons + document.querySelectorAll('.toggle-api-key').forEach(button => { + button.addEventListener('click', (e) => { + const input = e.target.closest('.input-group').querySelector('input'); + const type = input.type === 'password' ? 'text' : 'password'; + input.type = type; + const icon = e.target.querySelector('i'); + if (icon) { + icon.className = `fas fa-${type === 'password' ? 'eye' : 'eye-slash'}`; + } + }); + }); } loadSettings() { const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}'); - if (settings.apiKey) this.apiKeyInput.value = settings.apiKey; - if (settings.model) this.modelSelect.value = settings.model; + // Load API keys + if (settings.apiKeys) { + Object.entries(this.apiKeyInputs).forEach(([model, input]) => { + if (settings.apiKeys[model]) { + input.value = settings.apiKeys[model]; + } + }); + } + + if (settings.model) { + this.modelSelect.value = settings.model; + this.updateVisibleApiKey(settings.model); + } else { + // Default to first model if none selected + this.updateVisibleApiKey(this.modelSelect.value); + } + if (settings.temperature) { this.temperatureInput.value = settings.temperature; this.temperatureValue.textContent = settings.temperature; @@ -43,9 +77,16 @@ class SettingsManager { this.proxySettings.style.display = this.proxyEnabledInput.checked ? 'block' : 'none'; } + updateVisibleApiKey(selectedModel) { + this.apiKeyGroups.forEach(group => { + const modelValue = group.dataset.model; + group.style.display = modelValue === selectedModel ? 'block' : 'none'; + }); + } + saveSettings() { const settings = { - apiKey: this.apiKeyInput.value, + apiKeys: {}, model: this.modelSelect.value, temperature: this.temperatureInput.value, systemPrompt: this.systemPromptInput.value, @@ -53,22 +94,46 @@ class SettingsManager { proxyHost: this.proxyHostInput.value, proxyPort: this.proxyPortInput.value }; + + // Save all API keys + Object.entries(this.apiKeyInputs).forEach(([model, input]) => { + if (input.value) { + settings.apiKeys[model] = input.value; + } + }); + localStorage.setItem('aiSettings', JSON.stringify(settings)); window.showToast('Settings saved successfully'); } - getSettings() { - return JSON.parse(localStorage.getItem('aiSettings') || '{}'); + getApiKey() { + const selectedModel = this.modelSelect.value; + const apiKey = this.apiKeyInputs[selectedModel]?.value; + + if (!apiKey) { + window.showToast('Please enter API key for the selected model', 'error'); + return ''; + } + + return apiKey; } setupEventListeners() { // Save settings on change - this.apiKeyInput.addEventListener('change', () => this.saveSettings()); - this.modelSelect.addEventListener('change', () => this.saveSettings()); + Object.values(this.apiKeyInputs).forEach(input => { + input.addEventListener('change', () => this.saveSettings()); + }); + + this.modelSelect.addEventListener('change', (e) => { + this.updateVisibleApiKey(e.target.value); + this.saveSettings(); + }); + this.temperatureInput.addEventListener('input', (e) => { this.temperatureValue.textContent = e.target.value; this.saveSettings(); }); + this.systemPromptInput.addEventListener('change', () => this.saveSettings()); this.proxyEnabledInput.addEventListener('change', (e) => { this.proxySettings.style.display = e.target.checked ? 'block' : 'none'; @@ -77,13 +142,6 @@ class SettingsManager { this.proxyHostInput.addEventListener('change', () => this.saveSettings()); this.proxyPortInput.addEventListener('change', () => this.saveSettings()); - // Toggle API key visibility - this.toggleApiKey.addEventListener('click', () => { - const type = this.apiKeyInput.type === 'password' ? 'text' : 'password'; - this.apiKeyInput.type = type; - this.toggleApiKey.innerHTML = ``; - }); - // Panel visibility this.settingsToggle.addEventListener('click', () => { window.closeAllPanels(); diff --git a/static/style.css b/static/style.css index 3fbcbb5..289429b 100644 --- a/static/style.css +++ b/static/style.css @@ -660,6 +660,41 @@ button:disabled { margin: 0; } +/* API Key Groups */ +.api-key-group { + margin-bottom: 1.5rem; + padding: 1rem; + background-color: var(--background); + border-radius: 0.5rem; + border: 1px solid var(--border-color); + transition: all 0.3s ease; +} + +.api-key-group:not([style*="display: none"]) { + animation: fade-in 0.3s ease; +} + +.api-key-group label { + font-weight: 500; + color: var(--text-primary); + margin-bottom: 0.75rem; +} + +.api-key-group .input-group { + margin-bottom: 0; +} + +@keyframes fade-in { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + /* Utility Classes */ .hidden { display: none !important; diff --git a/templates/index.html b/templates/index.html index 8641199..fa6c1ca 100644 --- a/templates/index.html +++ b/templates/index.html @@ -80,11 +80,29 @@

AI Configuration

-
- +
+
- - +
+
+
+ +
+ + +
+
+
+ +
+ +