commit ab4f208e482e34db5fb8a914e396f9781b9f36a8 Author: Zylan Date: Mon Feb 3 01:01:08 2025 +0800 Initial commit diff --git a/app.py b/app.py new file mode 100644 index 0000000..1bc01f0 --- /dev/null +++ b/app.py @@ -0,0 +1,141 @@ +from flask import Flask, jsonify, render_template, request +from flask_socketio import SocketIO +import pyautogui +import base64 +from io import BytesIO +import socket +import requests +import json +import asyncio +from threading import Thread + +app = Flask(__name__) +socketio = SocketIO(app, cors_allowed_origins="*") + +def get_local_ip(): + try: + # Get local IP address + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "127.0.0.1" + +@app.route('/') +def index(): + local_ip = get_local_ip() + return render_template('index.html', local_ip=local_ip) + +@socketio.on('connect') +def handle_connect(): + print('Client connected') + +@socketio.on('disconnect') +def handle_disconnect(): + print('Client disconnected') + +def stream_claude_response(response, sid): + """Stream Claude's response 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: + socketio.emit('claude_response', { + 'error': str(e) + }, room=sid) + +@socketio.on('request_screenshot') +def handle_screenshot_request(): + try: + # Capture the screen + screenshot = pyautogui.screenshot() + + # Convert the image to base64 string + buffered = BytesIO() + screenshot.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode() + + # Emit the screenshot back to the client + socketio.emit('screenshot_response', { + 'success': True, + 'image': img_str + }) + except Exception as e: + socketio.emit('screenshot_response', { + 'success': False, + 'error': str(e) + }) + +@socketio.on('analyze_image') +def handle_image_analysis(data): + try: + 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', + } + + 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." + } + ] + }] + } + + response = requests.post( + 'https://api.anthropic.com/v1/messages', + headers=headers, + json=payload, + stream=True + ) + + if response.status_code != 200: + 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() + + except Exception as e: + socketio.emit('claude_response', { + 'error': str(e) + }) + +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) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..eaeaec6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +flask==3.0.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 diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..f29b125 --- /dev/null +++ b/static/script.js @@ -0,0 +1,316 @@ +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 new file mode 100644 index 0000000..4101409 --- /dev/null +++ b/static/style.css @@ -0,0 +1,413 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 20px; + background-color: #f0f0f0; + touch-action: manipulation; + -webkit-tap-highlight-color: transparent; +} + +.container { + max-width: 1000px; + margin: 0 auto; + text-align: center; +} + +.connection-panel { + background-color: white; + padding: 20px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; + flex-wrap: wrap; +} + +.status { + padding: 8px 16px; + border-radius: 20px; + font-weight: bold; + font-size: 14px; +} + +.status.connected { + background-color: #4CAF50; + color: white; +} + +.status.disconnected { + background-color: #f44336; + 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 { + display: flex; + gap: 10px; + justify-content: center; + margin-bottom: 20px; +} + +#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-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; +} + +#screenshotImg { + max-width: 100%; + height: auto; + display: none; +} + +#screenshotImg[src] { + display: block; +} + +.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); + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; +} + +.ai-settings { + 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; + transition: transform 0.3s ease; +} + +.ai-settings.hidden { + transform: translateX(120%); +} + +.setting-group { + margin-bottom: 15px; +} + +.setting-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #333; +} + +.setting-group input[type="password"], +.setting-group input[type="text"], +.setting-group select, +.setting-group textarea { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; +} + +.setting-group input[type="range"] { + width: 80%; + vertical-align: middle; +} + +#temperatureValue { + display: inline-block; + width: 15%; + text-align: right; +} + +.crop-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: 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 { + position: relative; + width: 100%; + height: calc(100% - 80px); + display: flex; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.crop-area { + width: 100%; + height: 100%; + display: flex; + align-items: center; + 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-actions { + position: fixed; + bottom: 0; + left: 0; + right: 0; + display: flex; + justify-content: space-between; + padding: 15px; + z-index: 1001; + background: rgba(0,0,0,0.8); + gap: 10px; +} + +.crop-actions .action-button { + flex: 1; + max-width: 200px; + margin: 0 5px; + font-size: 16px; + padding: 12px; + border-radius: 8px; +} + +.crop-actions .action-button.confirm { + background-color: #4CAF50; +} + +.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 { + position: fixed; + bottom: 0; + left: 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; + transition: transform 0.3s ease; +} + +.claude-response.hidden { + transform: translateY(100%); +} + +.response-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.response-header h3 { + margin: 0; +} + +.close-button { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #666; +} + +.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 { + 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; + } +} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..9ace523 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,76 @@ + + + + + + Screen Capture + + + + + + +
+ + +
+
Disconnected
+ + +
+
+ + +
+
+ Screenshot will appear here +
+ + + + + + +
+ + +