ai factory

This commit is contained in:
Zylan
2025-02-03 14:49:18 +08:00
parent ab4f208e48
commit 597d6353b4
22 changed files with 1868 additions and 736 deletions

BIN
app.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

163
app.py
View File

@@ -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'))
response = requests.post(
'https://api.anthropic.com/v1/messages',
headers=headers,
json=payload,
stream=True
)
# 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}'
}
if response.status_code != 200:
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()
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)

14
icon.py Normal file
View File

@@ -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()

13
models/__init__.py Normal file
View File

@@ -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'
]

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

36
models/base.py Normal file
View File

@@ -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())

121
models/claude.py Normal file
View File

@@ -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)}"
}

84
models/deepseek.py Normal file
View File

@@ -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}"
}

55
models/factory.py Normal file
View File

@@ -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

94
models/gpt4o.py Normal file
View File

@@ -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}"
}

View File

@@ -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

391
static/js/main.js Normal file
View File

@@ -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 = '<i class="fas fa-camera"></i><span>Capture</span>';
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 = '<i class="fas fa-camera"></i><span>Capture</span>';
}
});
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 = '<i class="fas fa-spinner fa-spin"></i><span>Capturing...</span>';
this.socket.emit('request_screenshot');
} catch (error) {
window.showToast('Error requesting screenshot: ' + error.message, 'error');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
}
});
// 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 = `
<div class="history-empty">
<i class="fas fa-history"></i>
<p>No history yet</p>
</div>
`;
return;
}
content.innerHTML = history.map(item => `
<div class="history-item" data-id="${item.id}">
<div class="history-item-header">
<span>${new Date(item.timestamp).toLocaleString()}</span>
<button class="btn-icon delete-history" data-id="${item.id}">
<i class="fas fa-trash"></i>
</button>
</div>
<img src="${item.image}" alt="Historical screenshot" class="history-image">
</div>
`).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;
}
}
});
});
};

100
static/js/settings.js Normal file
View File

@@ -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 = `<i class="fas fa-${type === 'password' ? 'eye' : 'eye-slash'}"></i>`;
});
// 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;

140
static/js/ui.js Normal file
View File

@@ -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 = `<i class="fas fa-${isDark ? 'sun' : 'moon'}"></i>`;
localStorage.setItem('theme', isDark ? 'dark' : 'light');
}
showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
<i class="fas fa-${type === 'success' ? 'check-circle' : 'exclamation-circle'}"></i>
<span>${message}</span>
`;
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();

View File

@@ -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');
}
}
}
});

View File

@@ -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;
}
}

View File

@@ -1,76 +1,172 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<title>Screen Capture</title>
<title>Snap Solver</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js"></script>
</head>
<body>
<div class="container">
<button id="aiSettingsToggle" class="toggle-button">⚙️</button>
<div id="aiSettings" class="ai-settings hidden">
<h3>AI Configuration</h3>
<div class="setting-group">
<label for="apiKey">Claude API Key:</label>
<input type="password" id="apiKey" placeholder="Enter API key">
</div>
<div class="setting-group">
<label for="modelSelect">Model:</label>
<select id="modelSelect">
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
<option value="claude-3-opus-20240229">Claude 3 Opus</option>
<option value="claude-3-5-haiku-20241022">Claude 3.5 Haiku</option>
</select>
</div>
<div class="setting-group">
<label for="temperature">Temperature:</label>
<input type="range" id="temperature" min="0" max="1" step="0.1" value="0.7">
<span id="temperatureValue">0.7</span>
</div>
<div class="setting-group">
<label for="systemPrompt">System Prompt:</label>
<textarea id="systemPrompt" rows="3">You are a helpful AI assistant. Analyze the image and provide detailed explanations.</textarea>
<body class="app-container">
<header class="app-header">
<div class="header-left">
<h1>Snap Solver</h1>
<div class="connection-status">
<div id="connectionStatus" class="status disconnected">Disconnected</div>
</div>
</div>
<div class="connection-panel">
<div id="connectionStatus" class="status disconnected">Disconnected</div>
<input type="text" id="ipInput" placeholder="Enter PC's IP address" value="{{ local_ip }}:5000">
<button id="connectBtn">Connect</button>
</div>
<div class="action-buttons">
<button id="captureBtn" disabled>Capture Screenshot</button>
<button id="cropBtn" class="secondary" disabled>Crop Image</button>
</div>
<div class="image-container">
<img id="screenshotImg" src="" alt="Screenshot will appear here">
<div class="header-right">
<button id="themeToggle" class="btn-icon" title="Toggle theme">
<i class="fas fa-moon"></i>
</button>
<button id="historyToggle" class="btn-icon" title="View history">
<i class="fas fa-history"></i>
</button>
<button id="settingsToggle" class="btn-icon" title="Settings">
<i class="fas fa-cog"></i>
</button>
</div>
</header>
<div id="cropContainer" class="crop-container hidden">
<div class="crop-wrapper">
<div class="crop-area"></div>
<main class="app-main">
<div class="content-panel">
<div class="capture-section">
<div class="toolbar">
<div class="toolbar-buttons">
<button id="captureBtn" class="btn-primary" disabled>
<i class="fas fa-camera"></i>
<span>Capture</span>
</button>
<button id="cropBtn" class="btn-secondary hidden">
<i class="fas fa-crop"></i>
<span>Crop</span>
</button>
</div>
</div>
<div id="imagePreview" class="image-preview hidden">
<div class="image-container">
<img id="screenshotImg" src="" alt="Screenshot preview">
</div>
<div class="analysis-button">
<button id="sendToClaude" class="btn-primary hidden">
<i class="fas fa-robot"></i>
<span>Analyze Image</span>
</button>
</div>
</div>
</div>
<div class="crop-actions">
<button id="cropCancel" class="action-button">Cancel</button>
<button id="cropConfirm" class="action-button confirm">Confirm</button>
<div id="claudePanel" class="claude-panel hidden">
<div class="panel-header">
<h2>Analysis Result</h2>
<button class="btn-icon" id="closeClaudePanel">
<i class="fas fa-times"></i>
</button>
</div>
<div id="responseContent" class="response-content"></div>
</div>
</div>
<div id="claudeActions" class="claude-actions hidden">
<button id="sendToClaude" class="action-button">Send to Claude</button>
</div>
<div id="claudeResponse" class="claude-response hidden">
<div class="response-header">
<h3>Claude's Response</h3>
<button id="closeResponse" class="close-button">×</button>
<aside id="settingsPanel" class="settings-panel hidden">
<div class="panel-header">
<h2>Settings</h2>
<button class="btn-icon" id="closeSettings">
<i class="fas fa-times"></i>
</button>
</div>
<div class="settings-content">
<div class="settings-section">
<h3>AI Configuration</h3>
<div class="setting-group">
<label for="apiKey">API Key</label>
<div class="input-group">
<input type="password" id="apiKey" placeholder="Enter API key (Claude/GPT-4o/DeepSeek)">
<button class="btn-icon" id="toggleApiKey">
<i class="fas fa-eye"></i>
</button>
</div>
</div>
<div class="setting-group">
<label for="modelSelect">Model</label>
<select id="modelSelect" class="select-styled">
<option value="claude-3-5-sonnet-20241022">Claude 3.5 Sonnet</option>
<option value="gpt-4o-2024-11-20">GPT-4o</option>
<option value="deepseek-reasoner">DeepSeek Reasoner</option>
</select>
</div>
<div class="setting-group">
<label for="temperature">Temperature</label>
<div class="range-group">
<input type="range" id="temperature" min="0" max="1" step="0.1" value="0.7">
<span id="temperatureValue">0.7</span>
</div>
</div>
<div class="setting-group">
<label for="systemPrompt">System Prompt</label>
<textarea id="systemPrompt" rows="3">You are a helpful AI assistant. Analyze the image and provide detailed explanations.</textarea>
</div>
</div>
<div class="settings-section">
<h3>Proxy Settings</h3>
<div class="setting-group">
<label class="checkbox-label">
<input type="checkbox" id="proxyEnabled">
<span>Enable VPN Proxy</span>
</label>
</div>
<div id="proxySettings" class="proxy-settings">
<div class="setting-group">
<label for="proxyHost">Proxy Host</label>
<input type="text" id="proxyHost" value="127.0.0.1" placeholder="Enter proxy host">
</div>
<div class="setting-group">
<label for="proxyPort">Proxy Port</label>
<input type="number" id="proxyPort" value="4780" placeholder="Enter proxy port">
</div>
</div>
</div>
</div>
</aside>
<div id="historyPanel" class="history-panel hidden">
<div class="panel-header">
<h2>History</h2>
<button class="btn-icon" id="closeHistory">
<i class="fas fa-times"></i>
</button>
</div>
<div class="history-content">
<div class="history-empty">
<i class="fas fa-history"></i>
<p>No history yet</p>
</div>
</div>
<div id="responseContent" class="response-content"></div>
</div>
</div>
<script src="{{ url_for('static', filename='script.js') }}"></script>
</main>
<div id="cropContainer" class="crop-container hidden">
<div class="crop-wrapper">
<div class="crop-area"></div>
</div>
<div class="crop-actions">
<button id="cropCancel" class="btn-secondary">
<i class="fas fa-times"></i>
<span>Cancel</span>
</button>
<button id="cropConfirm" class="btn-primary">
<i class="fas fa-check"></i>
<span>Confirm</span>
</button>
</div>
<div id="toastContainer" class="toast-container"></div>
<script src="{{ url_for('static', filename='js/ui.js') }}"></script>
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
</body>
</html>