diff --git a/app.ico b/app.ico
new file mode 100644
index 0000000..434cad2
Binary files /dev/null and b/app.ico differ
diff --git a/app.py b/app.py
index 1bc01f0..c10fac8 100644
--- a/app.py
+++ b/app.py
@@ -4,10 +4,11 @@ import pyautogui
import base64
from io import BytesIO
import socket
-import requests
-import json
-import asyncio
from threading import Thread
+import pystray
+from PIL import Image, ImageDraw
+import pyperclip
+from models import ModelFactory
app = Flask(__name__)
socketio = SocketIO(app, cors_allowed_origins="*")
@@ -23,6 +24,33 @@ def get_local_ip():
except Exception:
return "127.0.0.1"
+def create_tray_icon():
+ # Create a simple icon (a colored circle)
+ icon_size = 64
+ icon_image = Image.new('RGB', (icon_size, icon_size), color='white')
+ draw = ImageDraw.Draw(icon_image)
+ draw.ellipse([4, 4, icon_size-4, icon_size-4], fill='#2196F3') # Using the primary color from our CSS
+
+ # Get server URL
+ ip_address = get_local_ip()
+ server_url = f"http://{ip_address}:5000"
+
+ # Create menu
+ menu = pystray.Menu(
+ pystray.MenuItem(server_url, lambda icon, item: None, enabled=False),
+ pystray.MenuItem("Exit", lambda icon, item: icon.stop())
+ )
+
+ # Create icon
+ icon = pystray.Icon(
+ "SnapSolver",
+ icon_image,
+ "Snap Solver",
+ menu
+ )
+
+ return icon
+
@app.route('/')
def index():
local_ip = get_local_ip()
@@ -36,23 +64,28 @@ def handle_connect():
def handle_disconnect():
print('Client disconnected')
-def stream_claude_response(response, sid):
- """Stream Claude's response to the client"""
+def stream_model_response(response_generator, sid):
+ """Stream model responses to the client"""
try:
- for chunk in response.iter_lines():
- if chunk:
- data = json.loads(chunk.decode('utf-8').removeprefix('data: '))
- if data['type'] == 'content_block_delta':
- socketio.emit('claude_response', {
- 'content': data['delta']['text']
- }, room=sid)
- elif data['type'] == 'error':
- socketio.emit('claude_response', {
- 'error': data['error']['message']
- }, room=sid)
- except Exception as e:
+ print("Starting response streaming...")
+
+ # Send initial status
socketio.emit('claude_response', {
- 'error': str(e)
+ 'status': 'started',
+ 'content': ''
+ }, room=sid)
+ print("Sent initial status to client")
+
+ # Stream responses
+ for response in response_generator:
+ socketio.emit('claude_response', response, room=sid)
+
+ except Exception as e:
+ error_msg = f"Streaming error: {str(e)}"
+ print(error_msg)
+ socketio.emit('claude_response', {
+ 'status': 'error',
+ 'error': error_msg
}, room=sid)
@socketio.on('request_screenshot')
@@ -80,62 +113,68 @@ def handle_screenshot_request():
@socketio.on('analyze_image')
def handle_image_analysis(data):
try:
+ print("Starting image analysis...")
settings = data['settings']
image_data = data['image'] # Base64 encoded image
- headers = {
- 'x-api-key': settings['apiKey'],
- 'anthropic-version': '2023-06-01',
- 'content-type': 'application/json',
- }
+ # Validate required settings
+ if not settings.get('apiKey'):
+ raise ValueError("API key is required")
- payload = {
- 'model': settings['model'],
- 'max_tokens': 4096,
- 'temperature': settings['temperature'],
- 'system': settings['systemPrompt'],
- 'messages': [{
- 'role': 'user',
- 'content': [
- {
- 'type': 'image',
- 'source': {
- 'type': 'base64',
- 'media_type': 'image/png',
- 'data': image_data
- }
- },
- {
- 'type': 'text',
- 'text': "Please analyze this image and provide a detailed explanation."
- }
- ]
- }]
- }
+ print("Using API key:", settings['apiKey'][:6] + "..." if settings.get('apiKey') else "None")
+ print("Selected model:", settings.get('model', 'claude-3-5-sonnet-20241022'))
+
+ # Configure proxy settings if enabled
+ proxies = None
+ if settings.get('proxyEnabled', False):
+ proxy_host = settings.get('proxyHost', '127.0.0.1')
+ proxy_port = settings.get('proxyPort', '4780')
+ proxies = {
+ 'http': f'http://{proxy_host}:{proxy_port}',
+ 'https': f'http://{proxy_host}:{proxy_port}'
+ }
- response = requests.post(
- 'https://api.anthropic.com/v1/messages',
- headers=headers,
- json=payload,
- stream=True
- )
+ try:
+ # Create model instance using factory
+ model = ModelFactory.create_model(
+ model_name=settings.get('model', 'claude-3-5-sonnet-20241022'),
+ api_key=settings['apiKey'],
+ temperature=float(settings.get('temperature', 0.7)),
+ system_prompt=settings.get('systemPrompt')
+ )
+
+ # Start streaming in a separate thread
+ Thread(
+ target=stream_model_response,
+ args=(model.analyze_image(image_data, proxies), request.sid)
+ ).start()
- if response.status_code != 200:
+ except Exception as e:
socketio.emit('claude_response', {
- 'error': f'Claude API error: {response.status_code} - {response.text}'
- })
- return
-
- # Start streaming in a separate thread to not block
- Thread(target=stream_claude_response, args=(response, request.sid)).start()
+ 'status': 'error',
+ 'error': f'API error: {str(e)}'
+ }, room=request.sid)
except Exception as e:
+ print(f"Analysis error: {str(e)}")
socketio.emit('claude_response', {
- 'error': str(e)
- })
+ 'status': 'error',
+ 'error': f'Analysis error: {str(e)}'
+ }, room=request.sid)
+
+def run_tray():
+ icon = create_tray_icon()
+ icon.run()
if __name__ == '__main__':
local_ip = get_local_ip()
print(f"Local IP Address: {local_ip}")
print(f"Connect from your mobile device using: {local_ip}:5000")
- socketio.run(app, host='0.0.0.0', port=5000, debug=True)
+
+ # Run system tray icon in a separate thread
+ tray_thread = Thread(target=run_tray)
+ tray_thread.daemon = True
+ tray_thread.start()
+
+ # Run Flask in the main thread without debug mode
+ socketio.run(app, host='0.0.0.0', port=5000, allow_unsafe_werkzeug=True)
diff --git a/icon.py b/icon.py
new file mode 100644
index 0000000..74ef89e
--- /dev/null
+++ b/icon.py
@@ -0,0 +1,14 @@
+from PIL import Image, ImageDraw
+
+def create_icon():
+ # Create a simple icon (a colored circle)
+ icon_size = 64
+ icon_image = Image.new('RGB', (icon_size, icon_size), color='white')
+ draw = ImageDraw.Draw(icon_image)
+ draw.ellipse([4, 4, icon_size-4, icon_size-4], fill='#2196F3')
+
+ # Save as ICO file
+ icon_image.save('app.ico', format='ICO')
+
+if __name__ == '__main__':
+ create_icon()
diff --git a/models/__init__.py b/models/__init__.py
new file mode 100644
index 0000000..e7e9945
--- /dev/null
+++ b/models/__init__.py
@@ -0,0 +1,13 @@
+from .base import BaseModel
+from .claude import ClaudeModel
+from .gpt4o import GPT4oModel
+from .deepseek import DeepSeekModel
+from .factory import ModelFactory
+
+__all__ = [
+ 'BaseModel',
+ 'ClaudeModel',
+ 'GPT4oModel',
+ 'DeepSeekModel',
+ 'ModelFactory'
+]
diff --git a/models/__pycache__/__init__.cpython-312.pyc b/models/__pycache__/__init__.cpython-312.pyc
new file mode 100644
index 0000000..2da4776
Binary files /dev/null and b/models/__pycache__/__init__.cpython-312.pyc differ
diff --git a/models/__pycache__/base.cpython-312.pyc b/models/__pycache__/base.cpython-312.pyc
new file mode 100644
index 0000000..60dff01
Binary files /dev/null and b/models/__pycache__/base.cpython-312.pyc differ
diff --git a/models/__pycache__/claude.cpython-312.pyc b/models/__pycache__/claude.cpython-312.pyc
new file mode 100644
index 0000000..bc97c58
Binary files /dev/null and b/models/__pycache__/claude.cpython-312.pyc differ
diff --git a/models/__pycache__/deepseek.cpython-312.pyc b/models/__pycache__/deepseek.cpython-312.pyc
new file mode 100644
index 0000000..d08926d
Binary files /dev/null and b/models/__pycache__/deepseek.cpython-312.pyc differ
diff --git a/models/__pycache__/factory.cpython-312.pyc b/models/__pycache__/factory.cpython-312.pyc
new file mode 100644
index 0000000..8781072
Binary files /dev/null and b/models/__pycache__/factory.cpython-312.pyc differ
diff --git a/models/__pycache__/gpt4o.cpython-312.pyc b/models/__pycache__/gpt4o.cpython-312.pyc
new file mode 100644
index 0000000..243eaa1
Binary files /dev/null and b/models/__pycache__/gpt4o.cpython-312.pyc differ
diff --git a/models/base.py b/models/base.py
new file mode 100644
index 0000000..c225373
--- /dev/null
+++ b/models/base.py
@@ -0,0 +1,36 @@
+from abc import ABC, abstractmethod
+from typing import Generator, Any
+
+class BaseModel(ABC):
+ def __init__(self, api_key: str, temperature: float = 0.7, system_prompt: str = None):
+ self.api_key = api_key
+ self.temperature = temperature
+ self.system_prompt = system_prompt or self.get_default_system_prompt()
+
+ @abstractmethod
+ def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
+ """
+ Analyze the given image and yield response chunks.
+
+ Args:
+ image_data: Base64 encoded image data
+ proxies: Optional proxy configuration
+
+ Yields:
+ dict: Response chunks with status and content
+ """
+ pass
+
+ @abstractmethod
+ def get_default_system_prompt(self) -> str:
+ """Return the default system prompt for this model"""
+ pass
+
+ @abstractmethod
+ def get_model_identifier(self) -> str:
+ """Return the model identifier used in API calls"""
+ pass
+
+ def validate_api_key(self) -> bool:
+ """Validate if the API key is in the correct format"""
+ return bool(self.api_key and self.api_key.strip())
diff --git a/models/claude.py b/models/claude.py
new file mode 100644
index 0000000..dbcc18e
--- /dev/null
+++ b/models/claude.py
@@ -0,0 +1,121 @@
+import json
+import requests
+from typing import Generator
+from .base import BaseModel
+
+class ClaudeModel(BaseModel):
+ def get_default_system_prompt(self) -> str:
+ return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question:
+1. First read and understand the question carefully
+2. Break down the key components of the question
+3. Provide a clear, step-by-step solution
+4. If relevant, explain any concepts or theories involved
+5. If there are multiple approaches, explain the most efficient one first"""
+
+ def get_model_identifier(self) -> str:
+ return "claude-3-5-sonnet-20241022"
+
+ def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
+ """Stream Claude's response for image analysis"""
+ try:
+ # Initial status
+ yield {"status": "started", "content": ""}
+
+ api_key = self.api_key.strip()
+ if api_key.startswith('Bearer '):
+ api_key = api_key[7:]
+
+ headers = {
+ 'x-api-key': api_key,
+ 'anthropic-version': '2023-06-01',
+ 'content-type': 'application/json',
+ 'accept': 'application/json',
+ }
+
+ payload = {
+ 'model': self.get_model_identifier(),
+ 'stream': True,
+ 'max_tokens': 4096,
+ 'temperature': self.temperature,
+ 'system': self.system_prompt,
+ 'messages': [{
+ 'role': 'user',
+ 'content': [
+ {
+ 'type': 'image',
+ 'source': {
+ 'type': 'base64',
+ 'media_type': 'image/png',
+ 'data': image_data
+ }
+ },
+ {
+ 'type': 'text',
+ 'text': "Please analyze this question and provide a detailed solution. If you see multiple questions, focus on solving them one at a time."
+ }
+ ]
+ }]
+ }
+
+ response = requests.post(
+ 'https://api.anthropic.com/v1/messages',
+ headers=headers,
+ json=payload,
+ stream=True,
+ proxies=proxies,
+ timeout=60
+ )
+
+ if response.status_code != 200:
+ error_msg = f'API error: {response.status_code}'
+ try:
+ error_data = response.json()
+ if 'error' in error_data:
+ error_msg += f" - {error_data['error']['message']}"
+ except:
+ error_msg += f" - {response.text}"
+ yield {"status": "error", "error": error_msg}
+ return
+
+ for chunk in response.iter_lines():
+ if not chunk:
+ continue
+
+ try:
+ chunk_str = chunk.decode('utf-8')
+ if not chunk_str.startswith('data: '):
+ continue
+
+ chunk_str = chunk_str[6:]
+ data = json.loads(chunk_str)
+
+ if data.get('type') == 'content_block_delta':
+ if 'delta' in data and 'text' in data['delta']:
+ yield {
+ "status": "streaming",
+ "content": data['delta']['text']
+ }
+
+ elif data.get('type') == 'message_stop':
+ yield {
+ "status": "completed",
+ "content": ""
+ }
+
+ elif data.get('type') == 'error':
+ error_msg = data.get('error', {}).get('message', 'Unknown error')
+ yield {
+ "status": "error",
+ "error": error_msg
+ }
+ break
+
+ except json.JSONDecodeError as e:
+ print(f"JSON decode error: {str(e)}")
+ continue
+
+ except Exception as e:
+ yield {
+ "status": "error",
+ "error": f"Streaming error: {str(e)}"
+ }
diff --git a/models/deepseek.py b/models/deepseek.py
new file mode 100644
index 0000000..d45f6e1
--- /dev/null
+++ b/models/deepseek.py
@@ -0,0 +1,84 @@
+import json
+import requests
+from typing import Generator
+from openai import OpenAI
+from .base import BaseModel
+
+class DeepSeekModel(BaseModel):
+ def get_default_system_prompt(self) -> str:
+ return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question:
+1. First read and understand the question carefully
+2. Break down the key components of the question
+3. Provide a clear, step-by-step solution
+4. If relevant, explain any concepts or theories involved
+5. If there are multiple approaches, explain the most efficient one first"""
+
+ def get_model_identifier(self) -> str:
+ return "deepseek-reasoner"
+
+ def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
+ """Stream DeepSeek's response for image analysis"""
+ try:
+ # Initial status
+ yield {"status": "started", "content": ""}
+
+ # Configure client with proxy if needed
+ client_args = {
+ "api_key": self.api_key,
+ "base_url": "https://api.deepseek.com"
+ }
+
+ if proxies:
+ session = requests.Session()
+ session.proxies = proxies
+ client_args["http_client"] = session
+
+ client = OpenAI(**client_args)
+
+ response = client.chat.completions.create(
+ model=self.get_model_identifier(),
+ messages=[{
+ 'role': 'user',
+ 'content': f"Here's an image of a question to analyze: data:image/png;base64,{image_data}"
+ }],
+ stream=True
+ )
+
+ for chunk in response:
+ try:
+ if hasattr(chunk.choices[0].delta, 'reasoning_content'):
+ content = chunk.choices[0].delta.reasoning_content
+ if content:
+ yield {
+ "status": "streaming",
+ "content": content
+ }
+ elif hasattr(chunk.choices[0].delta, 'content'):
+ content = chunk.choices[0].delta.content
+ if content:
+ yield {
+ "status": "streaming",
+ "content": content
+ }
+
+ except Exception as e:
+ print(f"Chunk processing error: {str(e)}")
+ continue
+
+ # Send completion status
+ yield {
+ "status": "completed",
+ "content": ""
+ }
+
+ except Exception as e:
+ error_msg = str(e)
+ if "invalid_api_key" in error_msg.lower():
+ error_msg = "Invalid API key provided"
+ elif "rate_limit" in error_msg.lower():
+ error_msg = "Rate limit exceeded. Please try again later."
+
+ yield {
+ "status": "error",
+ "error": f"DeepSeek API error: {error_msg}"
+ }
diff --git a/models/factory.py b/models/factory.py
new file mode 100644
index 0000000..6490e2c
--- /dev/null
+++ b/models/factory.py
@@ -0,0 +1,55 @@
+from typing import Dict, Type
+from .base import BaseModel
+from .claude import ClaudeModel
+from .gpt4o import GPT4oModel
+from .deepseek import DeepSeekModel
+
+class ModelFactory:
+ _models: Dict[str, Type[BaseModel]] = {
+ 'claude-3-5-sonnet-20241022': ClaudeModel,
+ 'gpt-4o-2024-11-20': GPT4oModel,
+ 'deepseek-reasoner': DeepSeekModel
+ }
+
+ @classmethod
+ def create_model(cls, model_name: str, api_key: str, temperature: float = 0.7, system_prompt: str = None) -> BaseModel:
+ """
+ Create and return an instance of the specified model.
+
+ Args:
+ model_name: The identifier of the model to create
+ api_key: The API key for the model
+ temperature: Optional temperature parameter for response generation
+ system_prompt: Optional custom system prompt
+
+ Returns:
+ An instance of the specified model
+
+ Raises:
+ ValueError: If the model_name is not recognized
+ """
+ model_class = cls._models.get(model_name)
+ if not model_class:
+ raise ValueError(f"Unknown model: {model_name}")
+
+ return model_class(
+ api_key=api_key,
+ temperature=temperature,
+ system_prompt=system_prompt
+ )
+
+ @classmethod
+ def get_available_models(cls) -> list[str]:
+ """Return a list of available model identifiers"""
+ return list(cls._models.keys())
+
+ @classmethod
+ def register_model(cls, model_name: str, model_class: Type[BaseModel]) -> None:
+ """
+ Register a new model type with the factory.
+
+ Args:
+ model_name: The identifier for the model
+ model_class: The model class to register
+ """
+ cls._models[model_name] = model_class
diff --git a/models/gpt4o.py b/models/gpt4o.py
new file mode 100644
index 0000000..0d2bc00
--- /dev/null
+++ b/models/gpt4o.py
@@ -0,0 +1,94 @@
+import json
+import requests
+from typing import Generator
+from openai import OpenAI
+from .base import BaseModel
+
+class GPT4oModel(BaseModel):
+ def get_default_system_prompt(self) -> str:
+ return """You are an expert at analyzing questions and providing detailed solutions. When presented with an image of a question:
+1. First read and understand the question carefully
+2. Break down the key components of the question
+3. Provide a clear, step-by-step solution
+4. If relevant, explain any concepts or theories involved
+5. If there are multiple approaches, explain the most efficient one first"""
+
+ def get_model_identifier(self) -> str:
+ return "gpt-4o-2024-11-20"
+
+ def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
+ """Stream GPT-4o's response for image analysis"""
+ try:
+ # Initial status
+ yield {"status": "started", "content": ""}
+
+ # Configure client with proxy if needed
+ client_args = {
+ "api_key": self.api_key,
+ "base_url": "https://api.openai.com/v1" # Replace with actual GPT-4o API endpoint
+ }
+
+ if proxies:
+ session = requests.Session()
+ session.proxies = proxies
+ client_args["http_client"] = session
+
+ client = OpenAI(**client_args)
+
+ messages = [
+ {
+ "role": "system",
+ "content": self.system_prompt
+ },
+ {
+ "role": "user",
+ "content": [
+ {
+ "type": "image_url",
+ "image_url": {
+ "url": f"data:image/png;base64,{image_data}",
+ "detail": "high"
+ }
+ },
+ {
+ "type": "text",
+ "text": "Please analyze this question and provide a detailed solution. If you see multiple questions, focus on solving them one at a time."
+ }
+ ]
+ }
+ ]
+
+ response = client.chat.completions.create(
+ model=self.get_model_identifier(),
+ messages=messages,
+ temperature=self.temperature,
+ stream=True,
+ max_tokens=4000
+ )
+
+ for chunk in response:
+ if hasattr(chunk.choices[0].delta, 'content'):
+ content = chunk.choices[0].delta.content
+ if content:
+ yield {
+ "status": "streaming",
+ "content": content
+ }
+
+ # Send completion status
+ yield {
+ "status": "completed",
+ "content": ""
+ }
+
+ except Exception as e:
+ error_msg = str(e)
+ if "invalid_api_key" in error_msg.lower():
+ error_msg = "Invalid API key provided"
+ elif "rate_limit" in error_msg.lower():
+ error_msg = "Rate limit exceeded. Please try again later."
+
+ yield {
+ "status": "error",
+ "error": f"GPT-4o API error: {error_msg}"
+ }
diff --git a/requirements.txt b/requirements.txt
index eaeaec6..31de3d8 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,8 @@
-flask==3.0.0
+flask==3.1.0
pyautogui==0.9.54
-Pillow==10.1.0
-flask-socketio==5.3.6
-python-engineio==4.8.0
-python-socketio==5.10.0
-requests==2.31.0
+Pillow==11.1.0
+flask-socketio==5.5.1
+python-engineio==4.11.2
+python-socketio==5.12.1
+requests==2.32.3
+openai==1.61.0
diff --git a/static/js/main.js b/static/js/main.js
new file mode 100644
index 0000000..3d835eb
--- /dev/null
+++ b/static/js/main.js
@@ -0,0 +1,391 @@
+class SnapSolver {
+ constructor() {
+ this.initializeElements();
+ this.initializeState();
+ this.setupEventListeners();
+ this.initializeConnection();
+
+ // 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.responseContent = document.getElementById('responseContent');
+ this.claudePanel = document.getElementById('claudePanel');
+ }
+
+ initializeState() {
+ this.socket = null;
+ this.cropper = null;
+ this.croppedImage = null;
+ this.history = JSON.parse(localStorage.getItem('snapHistory') || '[]');
+ }
+
+ 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');
+ }
+ }
+
+ 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() {
+ 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');
+ window.showToast('Screenshot captured successfully');
+ } else {
+ window.showToast('Failed to capture screenshot: ' + data.error, 'error');
+ this.captureBtn.disabled = false;
+ this.captureBtn.innerHTML = 'Capture';
+ }
+ });
+
+ this.socket.on('claude_response', (data) => {
+ console.log('Received claude_response:', data);
+
+ switch (data.status) {
+ case 'started':
+ console.log('Analysis started');
+ this.responseContent.textContent = 'Starting analysis...\n';
+ this.sendToClaudeBtn.disabled = true;
+ break;
+
+ case 'streaming':
+ if (data.content) {
+ console.log('Received content:', data.content);
+ if (this.responseContent.textContent === 'Starting analysis...\n') {
+ this.responseContent.textContent = data.content;
+ } else {
+ this.responseContent.textContent += data.content;
+ }
+ this.responseContent.scrollTo({
+ top: this.responseContent.scrollHeight,
+ behavior: 'smooth'
+ });
+ }
+ break;
+
+ case 'completed':
+ console.log('Analysis completed');
+ this.responseContent.textContent += '\n\nAnalysis complete.';
+ this.sendToClaudeBtn.disabled = false;
+ this.addToHistory(this.croppedImage, this.responseContent.textContent);
+ window.showToast('Analysis completed successfully');
+ this.responseContent.scrollTo({
+ top: this.responseContent.scrollHeight,
+ behavior: 'smooth'
+ });
+ break;
+
+ case 'error':
+ console.error('Claude analysis error:', data.error);
+ const errorMessage = data.error || 'Unknown error occurred';
+ this.responseContent.textContent += '\n\nError: ' + errorMessage;
+ this.sendToClaudeBtn.disabled = false;
+ this.responseContent.scrollTop = this.responseContent.scrollHeight;
+ window.showToast('Analysis failed: ' + errorMessage, 'error');
+ break;
+
+ default:
+ console.warn('Unknown response status:', data.status);
+ if (data.error) {
+ this.responseContent.textContent += '\n\nError: ' + data.error;
+ this.sendToClaudeBtn.disabled = false;
+ this.responseContent.scrollTop = this.responseContent.scrollHeight;
+ 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() {
+ 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();
+ }
+ });
+ }
+
+ 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() {
+ // 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';
+ }
+ });
+
+ // Crop button
+ this.cropBtn.addEventListener('click', () => {
+ if (this.screenshotImg.src) {
+ this.initializeCropper();
+ }
+ });
+
+ // Crop confirm button
+ document.getElementById('cropConfirm').addEventListener('click', () => {
+ if (this.cropper) {
+ try {
+ const canvas = this.cropper.getCroppedCanvas({
+ maxWidth: 4096,
+ maxHeight: 4096,
+ fillColor: '#fff',
+ imageSmoothingEnabled: true,
+ imageSmoothingQuality: 'high',
+ });
+
+ if (!canvas) {
+ throw new Error('Failed to create cropped canvas');
+ }
+
+ this.croppedImage = canvas.toDataURL('image/png');
+
+ this.cropper.destroy();
+ this.cropper = null;
+ this.cropContainer.classList.add('hidden');
+ document.querySelector('.crop-area').innerHTML = '';
+ this.settingsPanel.classList.add('hidden');
+
+ this.screenshotImg.src = this.croppedImage;
+ this.imagePreview.classList.remove('hidden');
+ this.cropBtn.classList.remove('hidden');
+ this.sendToClaudeBtn.classList.remove('hidden');
+ window.showToast('Image cropped successfully');
+ } catch (error) {
+ console.error('Cropping error:', error);
+ window.showToast('Error while cropping image', 'error');
+ }
+ }
+ });
+
+ // 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');
+ document.querySelector('.crop-area').innerHTML = '';
+ });
+
+ // 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();
+ if (!settings.apiKey) {
+ window.showToast('Please enter your API key in settings', 'error');
+ this.settingsPanel.classList.remove('hidden');
+ return;
+ }
+
+ this.claudePanel.classList.remove('hidden');
+ this.responseContent.textContent = 'Preparing to analyze image...\n';
+ this.sendToClaudeBtn.disabled = true;
+
+ try {
+ this.socket.emit('analyze_image', {
+ image: this.croppedImage.split(',')[1],
+ settings: {
+ apiKey: settings.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 += '\nError: Failed to send image for analysis - ' + error.message;
+ this.sendToClaudeBtn.disabled = false;
+ window.showToast('Failed to send image for analysis', 'error');
+ }
+ });
+
+ // 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 = `
+
+ `;
+ return;
+ }
+
+ content.innerHTML = history.map(item => `
+
+
+

+
+ `).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');
+ if (historyItem.response) {
+ window.app.claudePanel.classList.remove('hidden');
+ window.app.responseContent.textContent = historyItem.response;
+ }
+ }
+ });
+ });
+};
diff --git a/static/js/settings.js b/static/js/settings.js
new file mode 100644
index 0000000..8a2dfdc
--- /dev/null
+++ b/static/js/settings.js
@@ -0,0 +1,100 @@
+class SettingsManager {
+ constructor() {
+ this.initializeElements();
+ this.loadSettings();
+ this.setupEventListeners();
+ }
+
+ 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');
+ this.systemPromptInput = document.getElementById('systemPrompt');
+ this.proxyEnabledInput = document.getElementById('proxyEnabled');
+ this.proxyHostInput = document.getElementById('proxyHost');
+ this.proxyPortInput = document.getElementById('proxyPort');
+ this.proxySettings = document.getElementById('proxySettings');
+
+ // Settings toggle elements
+ this.settingsToggle = document.getElementById('settingsToggle');
+ this.closeSettings = document.getElementById('closeSettings');
+ this.toggleApiKey = document.getElementById('toggleApiKey');
+ }
+
+ loadSettings() {
+ const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
+
+ if (settings.apiKey) this.apiKeyInput.value = settings.apiKey;
+ if (settings.model) this.modelSelect.value = settings.model;
+ if (settings.temperature) {
+ this.temperatureInput.value = settings.temperature;
+ this.temperatureValue.textContent = settings.temperature;
+ }
+ if (settings.systemPrompt) this.systemPromptInput.value = settings.systemPrompt;
+ if (settings.proxyEnabled !== undefined) {
+ this.proxyEnabledInput.checked = settings.proxyEnabled;
+ }
+ if (settings.proxyHost) this.proxyHostInput.value = settings.proxyHost;
+ if (settings.proxyPort) this.proxyPortInput.value = settings.proxyPort;
+
+ this.proxySettings.style.display = this.proxyEnabledInput.checked ? 'block' : 'none';
+ }
+
+ saveSettings() {
+ const settings = {
+ apiKey: this.apiKeyInput.value,
+ model: this.modelSelect.value,
+ temperature: this.temperatureInput.value,
+ systemPrompt: this.systemPromptInput.value,
+ proxyEnabled: this.proxyEnabledInput.checked,
+ proxyHost: this.proxyHostInput.value,
+ proxyPort: this.proxyPortInput.value
+ };
+ localStorage.setItem('aiSettings', JSON.stringify(settings));
+ window.showToast('Settings saved successfully');
+ }
+
+ getSettings() {
+ return JSON.parse(localStorage.getItem('aiSettings') || '{}');
+ }
+
+ setupEventListeners() {
+ // Save settings on change
+ this.apiKeyInput.addEventListener('change', () => this.saveSettings());
+ this.modelSelect.addEventListener('change', () => 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';
+ this.saveSettings();
+ });
+ 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();
+ this.settingsPanel.classList.toggle('hidden');
+ });
+
+ this.closeSettings.addEventListener('click', () => {
+ this.settingsPanel.classList.add('hidden');
+ });
+ }
+}
+
+// Export for use in other modules
+window.SettingsManager = SettingsManager;
diff --git a/static/js/ui.js b/static/js/ui.js
new file mode 100644
index 0000000..bd75366
--- /dev/null
+++ b/static/js/ui.js
@@ -0,0 +1,140 @@
+class UIManager {
+ constructor() {
+ this.initializeElements();
+ this.setupTheme();
+ this.setupEventListeners();
+ }
+
+ initializeElements() {
+ // Theme elements
+ this.themeToggle = document.getElementById('themeToggle');
+
+ // Panel elements
+ this.settingsPanel = document.getElementById('settingsPanel');
+ this.historyPanel = document.getElementById('historyPanel');
+ this.claudePanel = document.getElementById('claudePanel');
+
+ // History elements
+ this.historyToggle = document.getElementById('historyToggle');
+ this.closeHistory = document.getElementById('closeHistory');
+
+ // Claude panel elements
+ this.closeClaudePanel = document.getElementById('closeClaudePanel');
+
+ // Toast container
+ this.toastContainer = document.getElementById('toastContainer');
+ }
+
+ setupTheme() {
+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)');
+
+ // Initialize theme
+ const savedTheme = localStorage.getItem('theme');
+ if (savedTheme) {
+ this.setTheme(savedTheme === 'dark');
+ } else {
+ this.setTheme(prefersDark.matches);
+ }
+
+ // Listen for system theme changes
+ prefersDark.addEventListener('change', (e) => this.setTheme(e.matches));
+ }
+
+ setTheme(isDark) {
+ document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
+ this.themeToggle.innerHTML = ``;
+ localStorage.setItem('theme', isDark ? 'dark' : 'light');
+ }
+
+ showToast(message, type = 'success') {
+ const toast = document.createElement('div');
+ toast.className = `toast ${type}`;
+ toast.innerHTML = `
+
+ ${message}
+ `;
+ this.toastContainer.appendChild(toast);
+
+ setTimeout(() => {
+ toast.style.opacity = '0';
+ setTimeout(() => toast.remove(), 300);
+ }, 3000);
+ }
+
+ closeAllPanels() {
+ this.settingsPanel.classList.add('hidden');
+ this.historyPanel.classList.add('hidden');
+ }
+
+ setupEventListeners() {
+ // Theme toggle
+ this.themeToggle.addEventListener('click', () => {
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
+ this.setTheme(!isDark);
+ });
+
+ // History panel
+ this.historyToggle.addEventListener('click', () => {
+ this.closeAllPanels();
+ this.historyPanel.classList.toggle('hidden');
+ window.renderHistory(); // Call global renderHistory function
+ });
+
+ this.closeHistory.addEventListener('click', () => {
+ this.historyPanel.classList.add('hidden');
+ });
+
+ // Claude panel
+ this.closeClaudePanel.addEventListener('click', () => {
+ this.claudePanel.classList.add('hidden');
+ });
+
+ // Mobile touch events
+ let touchStartX = 0;
+ let touchEndX = 0;
+
+ document.addEventListener('touchstart', (e) => {
+ touchStartX = e.changedTouches[0].screenX;
+ });
+
+ document.addEventListener('touchend', (e) => {
+ touchEndX = e.changedTouches[0].screenX;
+ this.handleSwipe(touchStartX, touchEndX);
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', (e) => {
+ if (e.ctrlKey || e.metaKey) {
+ switch(e.key) {
+ case ',':
+ this.settingsPanel.classList.toggle('hidden');
+ break;
+ case 'h':
+ this.historyPanel.classList.toggle('hidden');
+ window.renderHistory();
+ break;
+ }
+ } else if (e.key === 'Escape') {
+ this.closeAllPanels();
+ }
+ });
+ }
+
+ handleSwipe(startX, endX) {
+ const swipeThreshold = 50;
+ const diff = endX - startX;
+
+ if (Math.abs(diff) > swipeThreshold) {
+ if (diff > 0) {
+ this.closeAllPanels();
+ } else {
+ this.settingsPanel.classList.remove('hidden');
+ }
+ }
+ }
+}
+
+// Export for use in other modules
+window.UIManager = UIManager;
+window.showToast = (message, type) => window.uiManager.showToast(message, type);
+window.closeAllPanels = () => window.uiManager.closeAllPanels();
diff --git a/static/script.js b/static/script.js
deleted file mode 100644
index f29b125..0000000
--- a/static/script.js
+++ /dev/null
@@ -1,316 +0,0 @@
-document.addEventListener('DOMContentLoaded', () => {
- const captureBtn = document.getElementById('captureBtn');
- const cropBtn = document.getElementById('cropBtn');
- const connectBtn = document.getElementById('connectBtn');
- const ipInput = document.getElementById('ipInput');
- const connectionStatus = document.getElementById('connectionStatus');
- const screenshotImg = document.getElementById('screenshotImg');
- const cropContainer = document.getElementById('cropContainer');
- const claudeActions = document.getElementById('claudeActions');
- const claudeResponse = document.getElementById('claudeResponse');
- const responseContent = document.getElementById('responseContent');
- const aiSettingsToggle = document.getElementById('aiSettingsToggle');
- const aiSettings = document.getElementById('aiSettings');
- const temperatureInput = document.getElementById('temperature');
- const temperatureValue = document.getElementById('temperatureValue');
-
- let socket = null;
- let cropper = null;
- let croppedImage = null;
-
- // Load saved AI settings
- function loadAISettings() {
- const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
- if (settings.apiKey) document.getElementById('apiKey').value = settings.apiKey;
- if (settings.model) document.getElementById('modelSelect').value = settings.model;
- if (settings.temperature) {
- temperatureInput.value = settings.temperature;
- temperatureValue.textContent = settings.temperature;
- }
- if (settings.systemPrompt) document.getElementById('systemPrompt').value = settings.systemPrompt;
- }
-
- // Save AI settings
- function saveAISettings() {
- const settings = {
- apiKey: document.getElementById('apiKey').value,
- model: document.getElementById('modelSelect').value,
- temperature: temperatureInput.value,
- systemPrompt: document.getElementById('systemPrompt').value
- };
- localStorage.setItem('aiSettings', JSON.stringify(settings));
- }
-
- // Initialize settings
- loadAISettings();
-
- // AI Settings panel toggle
- aiSettingsToggle.addEventListener('click', () => {
- aiSettings.classList.toggle('hidden');
- });
-
- // Save settings when changed
- document.getElementById('apiKey').addEventListener('change', saveAISettings);
- document.getElementById('modelSelect').addEventListener('change', saveAISettings);
- temperatureInput.addEventListener('input', (e) => {
- temperatureValue.textContent = e.target.value;
- saveAISettings();
- });
- document.getElementById('systemPrompt').addEventListener('change', saveAISettings);
-
- function updateConnectionStatus(connected) {
- connectionStatus.textContent = connected ? 'Connected' : 'Disconnected';
- connectionStatus.className = `status ${connected ? 'connected' : 'disconnected'}`;
- captureBtn.disabled = !connected;
- cropBtn.disabled = !screenshotImg.src;
- connectBtn.textContent = connected ? 'Disconnect' : 'Connect';
- }
-
- function connectToServer(serverUrl) {
- if (socket) {
- socket.disconnect();
- socket = null;
- updateConnectionStatus(false);
- return;
- }
-
- try {
- socket = io(serverUrl);
-
- socket.on('connect', () => {
- console.log('Connected to server');
- updateConnectionStatus(true);
- });
-
- socket.on('disconnect', () => {
- console.log('Disconnected from server');
- updateConnectionStatus(false);
- socket = null;
- });
-
- socket.on('screenshot_response', (data) => {
- if (data.success) {
- screenshotImg.src = `data:image/png;base64,${data.image}`;
- cropBtn.disabled = false;
- captureBtn.disabled = false;
- captureBtn.textContent = 'Capture Screenshot';
- claudeActions.classList.add('hidden');
- } else {
- alert('Failed to capture screenshot: ' + data.error);
- captureBtn.disabled = false;
- captureBtn.textContent = 'Capture Screenshot';
- }
- });
-
- socket.on('claude_response', (data) => {
- if (data.error) {
- responseContent.textContent += '\nError: ' + data.error;
- } else {
- responseContent.textContent += data.content;
- }
- responseContent.scrollTop = responseContent.scrollHeight;
- });
-
- socket.on('connect_error', (error) => {
- console.error('Connection error:', error);
- alert('Failed to connect to server. Please check the IP address and ensure the server is running.');
- updateConnectionStatus(false);
- socket = null;
- });
-
- } catch (error) {
- console.error('Connection error:', error);
- alert('Failed to connect to server: ' + error.message);
- updateConnectionStatus(false);
- }
- }
-
- function initializeCropper() {
- if (cropper) {
- cropper.destroy();
- cropper = null;
- }
-
- // Reset the image container and move it to crop area
- const cropArea = document.querySelector('.crop-area');
- cropArea.innerHTML = '';
- const clonedImage = screenshotImg.cloneNode(true);
- clonedImage.style.maxWidth = '100%';
- clonedImage.style.maxHeight = '100%';
- cropArea.appendChild(clonedImage);
-
- // Show crop container
- cropContainer.classList.remove('hidden');
-
- // Initialize cropper with mobile-friendly settings
- 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: 100,
- minContainerHeight: 100,
- minCropBoxWidth: 50,
- minCropBoxHeight: 50,
- background: true,
- responsive: true,
- checkOrientation: true,
- ready: function() {
- // Ensure the cropper is properly sized
- this.cropper.crop();
- }
- });
- }
-
- // Capture and Crop Event Listeners
- connectBtn.addEventListener('click', () => {
- const serverUrl = ipInput.value.trim();
- if (!serverUrl) {
- alert('Please enter the server IP address');
- return;
- }
- if (!serverUrl.startsWith('http://')) {
- connectToServer('http://' + serverUrl);
- } else {
- connectToServer(serverUrl);
- }
- });
-
- cropBtn.addEventListener('click', () => {
- if (screenshotImg.src) {
- initializeCropper();
- }
- });
-
- captureBtn.addEventListener('click', async () => {
- if (!socket || !socket.connected) {
- alert('Not connected to server');
- return;
- }
-
- try {
- captureBtn.disabled = true;
- captureBtn.textContent = 'Capturing...';
- socket.emit('request_screenshot');
- } catch (error) {
- alert('Error requesting screenshot: ' + error.message);
- captureBtn.disabled = false;
- captureBtn.textContent = 'Capture Screenshot';
- }
- });
-
- // Crop confirmation
- document.getElementById('cropConfirm').addEventListener('click', () => {
- if (cropper) {
- try {
- const canvas = cropper.getCroppedCanvas({
- maxWidth: 4096,
- maxHeight: 4096,
- fillColor: '#fff',
- imageSmoothingEnabled: true,
- imageSmoothingQuality: 'high',
- });
-
- if (!canvas) {
- throw new Error('Failed to create cropped canvas');
- }
-
- croppedImage = canvas.toDataURL('image/png');
-
- // Clean up
- cropper.destroy();
- cropper = null;
- cropContainer.classList.add('hidden');
- document.querySelector('.crop-area').innerHTML = '';
-
- // Show the cropped image and Claude actions
- screenshotImg.src = croppedImage;
- cropBtn.disabled = false;
- claudeActions.classList.remove('hidden');
- } catch (error) {
- console.error('Cropping error:', error);
- alert('Error while cropping image. Please try again.');
- }
- }
- });
-
- // Crop cancellation
- document.getElementById('cropCancel').addEventListener('click', () => {
- if (cropper) {
- cropper.destroy();
- cropper = null;
- }
- cropContainer.classList.add('hidden');
- claudeActions.classList.add('hidden');
- document.querySelector('.crop-area').innerHTML = '';
- });
-
- // Send to Claude
- document.getElementById('sendToClaude').addEventListener('click', () => {
- if (!croppedImage) {
- alert('Please crop the image first');
- return;
- }
-
- const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
- if (!settings.apiKey) {
- alert('Please enter your Claude API key in the settings');
- aiSettings.classList.remove('hidden');
- return;
- }
-
- claudeResponse.classList.remove('hidden');
- responseContent.textContent = 'Analyzing image...';
-
- socket.emit('analyze_image', {
- image: croppedImage.split(',')[1],
- settings: {
- apiKey: settings.apiKey,
- model: settings.model || 'claude-3-opus',
- temperature: parseFloat(settings.temperature) || 0.7,
- systemPrompt: settings.systemPrompt || 'You are a helpful AI assistant. Analyze the image and provide detailed explanations.'
- }
- });
- });
-
- // Close Claude response
- document.getElementById('closeResponse').addEventListener('click', () => {
- claudeResponse.classList.add('hidden');
- responseContent.textContent = '';
- });
-
- // Handle touch events for mobile
- let touchStartX = 0;
- let touchEndX = 0;
-
- document.addEventListener('touchstart', (e) => {
- touchStartX = e.changedTouches[0].screenX;
- });
-
- document.addEventListener('touchend', (e) => {
- touchEndX = e.changedTouches[0].screenX;
- handleSwipe();
- });
-
- function handleSwipe() {
- const swipeThreshold = 50;
- const diff = touchEndX - touchStartX;
-
- if (Math.abs(diff) > swipeThreshold) {
- if (diff > 0) {
- // Swipe right - hide panels
- aiSettings.classList.add('hidden');
- claudeResponse.classList.add('hidden');
- } else {
- // Swipe left - show AI settings
- aiSettings.classList.remove('hidden');
- }
- }
- }
-});
diff --git a/static/style.css b/static/style.css
index 4101409..3fbcbb5 100644
--- a/static/style.css
+++ b/static/style.css
@@ -1,226 +1,480 @@
-body {
- font-family: Arial, sans-serif;
+:root {
+ /* Light theme colors */
+ --primary-color: #2196F3;
+ --primary-dark: #1976D2;
+ --secondary-color: #4CAF50;
+ --secondary-dark: #45a049;
+ --background: #f8f9fa;
+ --surface: #ffffff;
+ --text-primary: #212121;
+ --text-secondary: #666666;
+ --border-color: #e0e0e0;
+ --shadow-color: rgba(0, 0, 0, 0.1);
+ --error-color: #f44336;
+ --success-color: #4CAF50;
+}
+
+[data-theme="dark"] {
+ --primary-color: #64B5F6;
+ --primary-dark: #42A5F5;
+ --secondary-color: #81C784;
+ --secondary-dark: #66BB6A;
+ --background: #121212;
+ --surface: #1E1E1E;
+ --text-primary: #FFFFFF;
+ --text-secondary: #B0B0B0;
+ --border-color: #333333;
+ --shadow-color: rgba(0, 0, 0, 0.3);
+}
+
+/* Base Styles */
+* {
margin: 0;
- padding: 20px;
- background-color: #f0f0f0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background-color: var(--background);
+ color: var(--text-primary);
+ line-height: 1.6;
+ transition: background-color 0.3s, color 0.3s;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
}
-.container {
- max-width: 1000px;
- margin: 0 auto;
- text-align: center;
+.app-container {
+ display: flex;
+ flex-direction: column;
+ min-height: 100vh;
}
-.connection-panel {
- background-color: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- margin-bottom: 20px;
+/* Header Styles */
+.app-header {
+ background-color: var(--surface);
+ padding: 1rem;
+ box-shadow: 0 2px 4px var(--shadow-color);
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 100;
+}
+
+.header-left {
display: flex;
align-items: center;
- justify-content: center;
- gap: 10px;
- flex-wrap: wrap;
+ gap: 2rem;
+}
+
+.header-left h1 {
+ font-size: 1.5rem;
+ color: var(--primary-color);
+ margin: 0;
+}
+
+.connection-status {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.connection-form {
+ display: flex;
+ gap: 0.5rem;
}
.status {
- padding: 8px 16px;
- border-radius: 20px;
- font-weight: bold;
- font-size: 14px;
+ padding: 0.4rem 0.8rem;
+ border-radius: 1rem;
+ font-size: 0.875rem;
+ font-weight: 500;
}
.status.connected {
- background-color: #4CAF50;
+ background-color: var(--success-color);
color: white;
}
.status.disconnected {
- background-color: #f44336;
+ background-color: var(--error-color);
color: white;
}
-#ipInput {
- padding: 8px 12px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
- width: 200px;
-}
-
-#connectBtn {
- padding: 8px 16px;
- font-size: 14px;
- background-color: #2196F3;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.3s;
-}
-
-#connectBtn:hover {
- background-color: #1976D2;
-}
-
-.action-buttons {
+.header-right {
+ display: flex;
+ gap: 0.5rem;
+}
+
+/* Main Content */
+.app-main {
+ flex: 1;
+ display: flex;
+ padding: 1rem;
+ gap: 1rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.content-panel {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+ max-width: 1200px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+/* Capture Section */
+.capture-section {
+ background-color: var(--surface);
+ border-radius: 0.5rem;
+ box-shadow: 0 2px 4px var(--shadow-color);
+ padding: 1rem;
+}
+
+.toolbar {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 1rem;
+ padding: 0.5rem;
+ background-color: var(--surface);
+ border-radius: 0.5rem;
+}
+
+.toolbar-buttons {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min-content, max-content));
+ gap: 1rem;
+ justify-content: start;
+ align-items: center;
+}
+
+.analysis-button {
display: flex;
- gap: 10px;
justify-content: center;
- margin-bottom: 20px;
+ margin-top: 1rem;
+ padding: 1rem;
}
-#captureBtn, #cropBtn {
- padding: 12px 24px;
- font-size: 16px;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- transition: background-color 0.3s;
- min-width: 160px;
-}
-
-#captureBtn {
- background-color: #4CAF50;
-}
-
-#captureBtn:hover {
- background-color: #45a049;
-}
-
-#cropBtn {
- background-color: #2196F3;
-}
-
-#cropBtn:hover {
- background-color: #1976D2;
-}
-
-#captureBtn:disabled, #cropBtn:disabled {
- background-color: #cccccc;
- cursor: not-allowed;
+.image-preview {
+ position: relative;
+ border-radius: 0.5rem;
+ overflow: hidden;
+ background-color: var(--background);
+ margin: 0;
+ padding: 1rem;
}
.image-container {
- background-color: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- min-height: 300px;
- display: flex;
- align-items: center;
- justify-content: center;
+ display: inline-block;
+ position: relative;
+ margin: 0 auto;
}
#screenshotImg {
- max-width: 100%;
- height: auto;
- display: none;
-}
-
-#screenshotImg[src] {
display: block;
+ width: auto;
+ height: auto;
+ max-width: 100%;
+ border-radius: 0.5rem;
}
-.toggle-button {
- position: fixed;
- top: 20px;
- right: 20px;
- z-index: 1000;
- padding: 10px;
- border-radius: 50%;
- width: 40px;
- height: 40px;
- background: #2196F3;
- color: white;
- border: none;
- cursor: pointer;
- box-shadow: 0 2px 5px rgba(0,0,0,0.2);
+@media (max-width: 768px) {
+ .toolbar-buttons {
+ flex-direction: row;
+ gap: 0.5rem;
+ }
+}
+
+/* Claude Panel */
+.claude-panel {
+ background-color: var(--surface);
+ border-radius: 0.5rem;
+ box-shadow: 0 2px 4px var(--shadow-color);
+ padding: 1rem;
+ flex: 1;
display: flex;
+ flex-direction: column;
+}
+
+.panel-header {
+ display: flex;
+ justify-content: space-between;
align-items: center;
- justify-content: center;
- font-size: 20px;
+ margin-bottom: 1rem;
}
-.ai-settings {
+.panel-header h2 {
+ font-size: 1.25rem;
+ color: var(--text-primary);
+}
+
+.response-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem;
+ background-color: var(--background);
+ border-radius: 0.5rem;
+ white-space: pre-wrap;
+ font-size: 0.9375rem;
+ line-height: 1.6;
+}
+
+/* Settings Panel */
+.settings-panel {
position: fixed;
- top: 70px;
- right: 20px;
- background: white;
- padding: 20px;
- border-radius: 8px;
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
- z-index: 999;
- width: 300px;
- max-width: 90vw;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 400px;
+ max-width: 100vw;
+ background-color: var(--surface);
+ box-shadow: -2px 0 4px var(--shadow-color);
+ z-index: 1000;
+ transform: translateX(100%);
transition: transform 0.3s ease;
+ display: flex;
+ flex-direction: column;
}
-.ai-settings.hidden {
- transform: translateX(120%);
+.settings-panel:not(.hidden) {
+ transform: translateX(0);
}
+.settings-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem;
+}
+
+.settings-section {
+ margin-bottom: 2rem;
+}
+
+.settings-section h3 {
+ color: var(--text-primary);
+ margin-bottom: 1rem;
+ font-size: 1.1rem;
+}
+
+/* Form Elements */
.setting-group {
- margin-bottom: 15px;
+ margin-bottom: 1rem;
}
.setting-group label {
display: block;
- margin-bottom: 5px;
- font-weight: bold;
- color: #333;
+ margin-bottom: 0.5rem;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
}
-.setting-group input[type="password"],
-.setting-group input[type="text"],
-.setting-group select,
-.setting-group textarea {
+input[type="text"],
+input[type="password"],
+input[type="number"],
+select,
+textarea {
width: 100%;
- padding: 8px;
- border: 1px solid #ddd;
- border-radius: 4px;
- font-size: 14px;
+ padding: 0.75rem;
+ border: 1px solid var(--border-color);
+ border-radius: 0.375rem;
+ background-color: var(--background);
+ color: var(--text-primary);
+ font-size: 0.9375rem;
+ transition: border-color 0.3s, box-shadow 0.3s;
}
-.setting-group input[type="range"] {
- width: 80%;
- vertical-align: middle;
+input:focus,
+select:focus,
+textarea:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
}
-#temperatureValue {
- display: inline-block;
- width: 15%;
- text-align: right;
+.input-group {
+ position: relative;
+ display: flex;
+ align-items: center;
}
+.input-group input {
+ padding-right: 2.5rem;
+}
+
+.input-group .btn-icon {
+ position: absolute;
+ right: 0.5rem;
+}
+
+.range-group {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+input[type="range"] {
+ flex: 1;
+ height: 4px;
+ -webkit-appearance: none;
+ background: var(--primary-color);
+ border-radius: 2px;
+}
+
+input[type="range"]::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ width: 16px;
+ height: 16px;
+ border-radius: 50%;
+ background: var(--primary-color);
+ cursor: pointer;
+ border: 2px solid var(--surface);
+ box-shadow: 0 1px 3px var(--shadow-color);
+}
+
+.checkbox-label {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ cursor: pointer;
+}
+
+/* Buttons */
+.btn-primary,
+.btn-secondary,
+.btn-icon {
+ padding: 0.75rem 1.5rem;
+ border: none;
+ border-radius: 0.375rem;
+ font-size: 0.9375rem;
+ font-weight: 500;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ transition: all 0.2s;
+}
+
+.btn-primary {
+ background-color: var(--primary-color);
+ color: white;
+}
+
+.btn-primary:hover {
+ background-color: var(--primary-dark);
+}
+
+.btn-secondary {
+ background-color: var(--background);
+ color: var(--text-primary);
+ border: 1px solid var(--border-color);
+}
+
+.btn-secondary:hover {
+ background-color: var(--border-color);
+}
+
+.btn-icon {
+ padding: 0.5rem;
+ border-radius: 0.375rem;
+ background: transparent;
+ color: var(--text-secondary);
+}
+
+.btn-icon:hover {
+ background-color: var(--background);
+ color: var(--text-primary);
+}
+
+button:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Floating Action Button */
+.fab {
+ position: fixed;
+ right: 2rem;
+ bottom: 2rem;
+ width: 3.5rem;
+ height: 3.5rem;
+ border-radius: 50%;
+ background-color: var(--primary-color);
+ color: white;
+ border: none;
+ cursor: pointer;
+ box-shadow: 0 2px 8px var(--shadow-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 1.5rem;
+ transition: transform 0.2s, background-color 0.2s;
+ z-index: 900;
+}
+
+.fab:hover {
+ transform: scale(1.05);
+ background-color: var(--primary-dark);
+}
+
+/* Toast Notifications */
+.toast-container {
+ position: fixed;
+ bottom: 2rem;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+ pointer-events: none;
+}
+
+.toast {
+ background-color: var(--surface);
+ color: var(--text-primary);
+ padding: 1rem 1.5rem;
+ border-radius: 0.375rem;
+ box-shadow: 0 4px 6px var(--shadow-color);
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ pointer-events: auto;
+ animation: toast-in 0.3s ease;
+}
+
+.toast.success {
+ border-left: 4px solid var(--success-color);
+}
+
+.toast.error {
+ border-left: 4px solid var(--error-color);
+}
+
+/* Crop Container */
.crop-container {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
- background: rgba(0,0,0,0.9);
+ background-color: rgba(0, 0, 0, 0.9);
z-index: 1000;
display: flex;
flex-direction: column;
- align-items: center;
- justify-content: center;
- touch-action: none;
- overflow: hidden;
}
.crop-wrapper {
+ flex: 1;
position: relative;
- width: 100%;
- height: calc(100% - 80px);
+ overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
- overflow: hidden;
}
.crop-area {
@@ -231,183 +485,193 @@ body {
justify-content: center;
}
-/* Cropper.js custom styles */
-.cropper-container {
- width: 100% !important;
- height: 100% !important;
- max-width: none !important;
- max-height: none !important;
-}
-
-.cropper-wrap-box {
- background-color: rgba(0,0,0,0.8);
-}
-
-.cropper-view-box,
-.cropper-face {
- border-radius: 0;
-}
-
-.cropper-point {
- width: 20px;
- height: 20px;
- opacity: 0.9;
-}
-
-.cropper-point.point-se,
-.cropper-point.point-sw,
-.cropper-point.point-ne,
-.cropper-point.point-nw {
- width: 20px;
- height: 20px;
-}
-
-.cropper-line,
-.cropper-point {
- background-color: #2196F3;
-}
-
-.crop-container.hidden {
- display: none;
+.crop-area img {
+ max-width: 100%;
+ max-height: 100%;
}
.crop-actions {
- position: fixed;
- bottom: 0;
- left: 0;
- right: 0;
+ padding: 1rem;
display: flex;
- justify-content: space-between;
- padding: 15px;
- z-index: 1001;
- background: rgba(0,0,0,0.8);
- gap: 10px;
+ justify-content: center;
+ gap: 1rem;
+ background-color: var(--surface);
}
-.crop-actions .action-button {
- flex: 1;
- max-width: 200px;
- margin: 0 5px;
- font-size: 16px;
- padding: 12px;
- border-radius: 8px;
+/* Animations */
+@keyframes toast-in {
+ from {
+ transform: translateY(100%);
+ opacity: 0;
+ }
+ to {
+ transform: translateY(0);
+ opacity: 1;
+ }
}
-.crop-actions .action-button.confirm {
- background-color: #4CAF50;
+/* Responsive Design */
+@media (max-width: 768px) {
+ .app-header {
+ flex-direction: column;
+ gap: 1rem;
+ padding: 0.75rem;
+ }
+
+ .header-left {
+ flex-direction: column;
+ gap: 1rem;
+ width: 100%;
+ }
+
+ .connection-status {
+ flex-direction: column;
+ width: 100%;
+ }
+
+ .connection-form {
+ width: 100%;
+ }
+
+ .connection-form input {
+ flex: 1;
+ }
+
+ .header-right {
+ width: 100%;
+ justify-content: center;
+ }
+
+ .settings-panel {
+ width: 100%;
+ }
+
+ .fab {
+ right: 1rem;
+ bottom: 1rem;
+ }
}
-.crop-actions .action-button.confirm:hover {
- background-color: #45a049;
-}
-
-.action-button {
- padding: 12px 24px;
- font-size: 16px;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- background: #2196F3;
- color: white;
- transition: background-color 0.3s;
-}
-
-.action-button:hover {
- background: #1976D2;
-}
-
-.claude-actions {
- margin-top: 20px;
- text-align: center;
-}
-
-.claude-actions.hidden {
- display: none;
-}
-
-.claude-response {
+/* History Panel */
+.history-panel {
position: fixed;
- bottom: 0;
- left: 0;
+ top: 0;
right: 0;
- background: white;
- padding: 20px;
- border-radius: 8px 8px 0 0;
- box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
- z-index: 998;
- max-height: 50vh;
- overflow-y: auto;
+ bottom: 0;
+ width: 400px;
+ max-width: 100vw;
+ background-color: var(--surface);
+ box-shadow: -2px 0 4px var(--shadow-color);
+ z-index: 1000;
+ transform: translateX(100%);
transition: transform 0.3s ease;
+ display: flex;
+ flex-direction: column;
}
-.claude-response.hidden {
- transform: translateY(100%);
+.history-panel:not(.hidden) {
+ transform: translateX(0);
}
-.response-header {
+.history-content {
+ flex: 1;
+ overflow-y: auto;
+ padding: 1rem;
+}
+
+.history-empty {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ color: var(--text-secondary);
+ gap: 1rem;
+}
+
+.history-empty i {
+ font-size: 3rem;
+}
+
+.history-item {
+ background-color: var(--background);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ margin-bottom: 1rem;
+ cursor: pointer;
+ transition: all 0.2s;
+ position: relative;
+ border: 1px solid var(--border-color);
+}
+
+.history-item:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 2px 8px var(--shadow-color);
+}
+
+.history-item[data-has-response="true"]::after {
+ content: "Has Analysis";
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ background-color: var(--primary-color);
+ color: white;
+ padding: 0.25rem 0.5rem;
+ border-radius: 1rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.history-item-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 15px;
+ margin-bottom: 0.5rem;
+ font-size: 0.875rem;
+ color: var(--text-secondary);
}
-.response-header h3 {
+.history-image {
+ width: 100%;
+ height: auto;
+ border-radius: 0.25rem;
+ margin-bottom: 1rem;
+}
+
+.history-response {
+ background-color: var(--background);
+ border-radius: 0.5rem;
+ padding: 1rem;
+ margin-top: 1rem;
+}
+
+.history-response h4 {
+ color: var(--text-primary);
+ margin-bottom: 0.5rem;
+ font-size: 1rem;
+}
+
+.history-response pre {
+ white-space: pre-wrap;
+ font-family: inherit;
+ font-size: 0.9375rem;
+ line-height: 1.6;
+ color: var(--text-secondary);
margin: 0;
}
-.close-button {
- background: none;
- border: none;
- font-size: 24px;
- cursor: pointer;
- color: #666;
+/* Utility Classes */
+.hidden {
+ display: none !important;
}
-.response-content {
- white-space: pre-wrap;
- font-size: 14px;
- line-height: 1.5;
-}
-
-@media (max-width: 600px) {
- body {
- padding: 10px;
- }
-
- .connection-panel {
- flex-direction: column;
- gap: 15px;
- padding: 15px;
- }
-
- #ipInput {
+/* Additional Responsive Styles */
+@media (max-width: 768px) {
+ .history-panel {
width: 100%;
- max-width: 300px;
}
- .toggle-button {
- top: 10px;
- right: 10px;
- }
-
- .ai-settings {
- top: 60px;
- right: 10px;
- padding: 15px;
- }
-
- .crop-actions {
- bottom: 10px;
- gap: 10px;
- }
-
- .action-button {
- padding: 10px 20px;
- font-size: 14px;
- }
-
- .claude-response {
- padding: 15px;
- max-height: 60vh;
+ .history-item {
+ padding: 0.75rem;
}
}
diff --git a/templates/index.html b/templates/index.html
index 9ace523..8641199 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1,76 +1,172 @@
-
+
- Screen Capture
+ Snap Solver
+
-
-
-
-
-
AI Configuration
-
-
-
-
-
-
-
-
-
-
-
- 0.7
-
-
-
-
+
+