From 597d6353b42e1db5a2ec9b219b533336f2dc0896 Mon Sep 17 00:00:00 2001 From: Zylan Date: Mon, 3 Feb 2025 14:49:18 +0800 Subject: [PATCH] ai factory --- app.ico | Bin 0 -> 4142 bytes app.py | 163 ++-- icon.py | 14 + models/__init__.py | 13 + models/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 410 bytes models/__pycache__/base.cpython-312.pyc | Bin 0 -> 2175 bytes models/__pycache__/claude.cpython-312.pyc | Bin 0 -> 4444 bytes models/__pycache__/deepseek.cpython-312.pyc | Bin 0 -> 3610 bytes models/__pycache__/factory.cpython-312.pyc | Bin 0 -> 2639 bytes models/__pycache__/gpt4o.cpython-312.pyc | Bin 0 -> 3507 bytes models/base.py | 36 + models/claude.py | 121 +++ models/deepseek.py | 84 ++ models/factory.py | 55 ++ models/gpt4o.py | 94 +++ requirements.txt | 13 +- static/js/main.js | 391 +++++++++ static/js/settings.js | 100 +++ static/js/ui.js | 140 ++++ static/script.js | 316 -------- static/style.css | 856 +++++++++++++------- templates/index.html | 208 +++-- 22 files changed, 1868 insertions(+), 736 deletions(-) create mode 100644 app.ico create mode 100644 icon.py create mode 100644 models/__init__.py create mode 100644 models/__pycache__/__init__.cpython-312.pyc create mode 100644 models/__pycache__/base.cpython-312.pyc create mode 100644 models/__pycache__/claude.cpython-312.pyc create mode 100644 models/__pycache__/deepseek.cpython-312.pyc create mode 100644 models/__pycache__/factory.cpython-312.pyc create mode 100644 models/__pycache__/gpt4o.cpython-312.pyc create mode 100644 models/base.py create mode 100644 models/claude.py create mode 100644 models/deepseek.py create mode 100644 models/factory.py create mode 100644 models/gpt4o.py create mode 100644 static/js/main.js create mode 100644 static/js/settings.js create mode 100644 static/js/ui.js delete mode 100644 static/script.js diff --git a/app.ico b/app.ico new file mode 100644 index 0000000000000000000000000000000000000000..434cad28199da39b586b572723ab35f9dd7d8480 GIT binary patch literal 4142 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2da4776dad89068fe9da97b589f64dad5f08146f GIT binary patch literal 410 zcmX|6Jxc>Y5S_iRy(AuDYY-8uD`J&O#KMGViii-a<6t>+YY6Vf?EEeM zgN3WC?8J1boL!AmJl=f0H}f6@ZNQk!UgrA*;6o?du>Q#U3du91kP-osM1*K=2}|0M zZMZEQ=|-;Mj_@RnsNt^gWh-hCm_zEllBiwN(Emgq2#IQ4Bqg6@DHoV@1|nId|7rX3 z=C+@0o57Ite9HO5wyGa44rpoWhA#x%5xqh{PpnL5cR#LlF`;w=S2ScyGMk)F~k5@Mhk;kNv&h zdpp16a#;eavb0^@Hwk%)N^hm&L#F_TEy4)XJW|t2T1_wM8tS@d)Xb7uOO;X@(aAN! zj19s}uI(7z*iyQg%BTNCC9g-u+@;IWh`l63VLM@shsyzj>%nW>=fVyH0UmzS>4tXY zP^@&mfx{Nzq@*!Y(piq_++YSb;cG@E%~BhHfv4FBOT+yDA7B}t0jV*qVw{9n@EP1l z?ZoLygW#KiNS$Rj%;Ywk+z!2RI|QD?;5ocumU2xkKN<~QvL*jKVB9N419siD?($|d z6!Kaf>u3lb{`0$UrMtn$!eyka-J(CR}aklef(nUZ^z!w7XP5iy1J zGJmB|z6_;abv<5Q^zHi0V&JWCF*843t|0}oyaa5_)|*krvRvN{Evt1rA!xRDhXu&@ zK&+F4@iUv|x5Edg-rY2R%}gAeKKJN!@jlz1UD%slcsyI&G=IxXcEFWHS+{0axy9_z zjs}3tRhP>sEjWO>Lji zrprA>1()@}mz+Aw4gap3O+u5yoT$n|qG8rUcm;|zfBX^Uz5}$dI6Wfoi-?OT(ChWk z4FD9yn;0RmfvOkrXwvRzA{p?*&`Kb{>Po6ctSH7I%L7dzmYBVZQjEZ{HFb#XUuS&+96K@2 z^#8^=^9oJ{gt`o9bSo|wv>~y{KApQUPaWIyWEZ8FgipPKF&aZqD%v`XZk_1|ebjy$ zl893%ULzC-ToKCPutmazYG<=UpUut@Xie+jC)*IdvtvK%(0a?s};YYt;s`Hj@sUa zy5tmyb@Kb@iF;SJ7QS0}G7WRm1jTiveJxRK*MIP`m+Q0tt^y{LutHVghGLO`cF) zdJEOFC~!#Cr`0bcGN_`!1oC?j8|0a8q^@h*Mw{UNaJswy>~xgg&XV-lGkqwPyAK|) zJ!ES8$=$tiYxj0(w|r;!#+|*Z_H#6L4DfEFcY9Kubcp-#Ie{%U!Kt0;nYNwROnU0# z_B$^i;9?gxf-U9;BEw#C1kPBu_H@{+yMDD>eHyP-Y5nEC%zV-%^iy#TTyT0MdbABq X)1Hv2C*BWEvv;H< zD+zKScV~BJXJ%(+XJ$WhyIlyrUtBLu^|v7OPdcc5Tou?l1HdA}2s09zFneZ#fw4uh zB-sg;L72r>$(rOQI0jkJD8k%Lgl&Yo$C@-IY&j-qe?$itgN*JRB`G2cYFd88z!aT1 zBPirT8WSmD?l4VmqHHHDm?7+>1zT=fC#+SN1(?GQ%n=qC+hEoXtrc4* zSs(13v|;;AZo-kX2Az7{%aV}ACOh3G6E!QR5>$}gAC-K|3e6${*)11^UjxR~be0!n z!V4*$%x8$K@&f!)f|SdPsVV-=EKyW3ol;;9^BFlkCt^B}i7JQ^!Mu`|vIfF1e`T7a z0EH+drNYV$QJtn_#iTGr`1B+%Y*QO?9USJ*iL#>dG7+#rJDb7;CMX+qnrx%;36O3w zD@i$5Y?wa-=(9Xd-$)rqvn0nS(#cFZ1uPZHWA|3qp<(`#Au$|}Pe??N!@Q!BOyqhl zLi=suU5AJH@kx+HB6C7Y4O3M}f|vrqD1%gjWGH!=l26MbQFt*mmzL%Tb{#Qx!aUU> z{F7Np6*CeHGN3IXF-?>ma#Xrx8ct0nC&h#aLh~RxKS?$5A^MQftqPGQ6>2#Co6y;j zYcGTUoC7^yyPOg-k;{~&92py={5dvdA!~fM#mgyZV{kRI2m$s(q0yursw# zvTF~v-m6|YnJciE!!|#nxY!Q2*nyq5twk$vqnh6H)hJ^Zt^*8ehCLt5)*{EQ6)n_dq48s zvzaSJTfsKdw1ZWHuz&U#m~{kqeE0fq(eErpyA#1`Yi-X?Upr~hfjbM1IV3j}?6|7{ zBO23ifVPxE9waW>0mqKJYnIAUi99>BYEX^lS@hG-V3)JtRG3>fkia=Z$?f?tU$aut zRd9W$9eZjRKxe4n!o7wiUF!CEX8e1I!m!|~?JCw4>SnsWje+}W7`T6S5OnCn1BE)! zK?b)J>hJ&+7Dgi8R~z9LLo3u*S?a5)pZp-@{RU+R9<2Jfx$o2u?&~!6g}w)`Q$+VH z=83-ZWC0wkt?#m$!J9!j+pWJ_q3whFXIR@d)Anuc>{$IOy7j+#_A0u;^dS{ec$IIU z2JQ6yNbW7xt39#^Xsqe2ijf5 zdfbUa-$&E%Oyi6-nmOBEihp2lFu`y>bXkSWm8?d&{R*TS1+ta`u^+OEnIVm&tB8tj zhb$;V2Gvo}qT67gW?>T2kP6{4Z$AThjL7^W8b>bXT@Y;#MGhQ|95|S7f}A6X34!M0 z=nO>Tpf&HDj|dqt0+}-345@8ePG`hK1k#8CNcs9I)rgwQ5ZxvuXr_BjL~!qQYgQq$ z&Kbh#w(C&M96g+O8pCKNH5FuatFg($;C*744=HqI5mZTKD4st0JMqmXQRAlB> z-H{}U0(Fg&ADC9vj4~39f|`f7H9QPuNp$XDw1)QRMaqZM?bA^2fbw-KwU0aivoZ}U zx}A!c&Z>`AV3eC!k)WH~L5@H%Gzd6tjxy1hye&O#%h9HrP?R-KGIpSMxvkMNIZ z05>hrS~YL{x|B}LLVX}XqU6lZX&x1HrPo)9#uYW4(VeuSk_b3p-u>g$Y>F0JrUb3$ zFaPKw7$*VMM@_nXv+3GY=T(7&t-2kQ5eJ{sIp88@beG}M3M~ocW-8u(Xms~8g^KQw ziD6MtP6mo3z0P#~IAte?=`LzHNct*~>(%YyN;1=+(OW*1MzS?kEh` z{o%4dywTYUy9dG>g9o(XPb@c5lPmV?d|LyWZGWZtN>54;zQH zKyYbreduI)=;Uhf)N1GH1t%1w&BwH!;N7k}UBB;MIInpErJ=GXta*D%%F@X4R5^6= zey$uEUmZHXI`C@Q`|5j65W6?He7xL#;$hPXt*L#(*R$>mmwn+i-|%D9=k8kI{^9A= z+Bz%N?&dCyAGlk8r+(?m8h>PAT*y`v_fIX&-EX@;_2;f~^xVeK5iJy5 zwtslyetS7|8kE%+*g|YqpyC1(eBgcGTiUng3oiZaq3?x_eFwGuK|tyD0}3ehS(Gae z!BqW&cgOCGEnQi@zSe(yz5hhH|HOT9rF*Ua>U++OeGzS7*x3J=?%#AEU*PM_Q*hp| zD|HNLv8}6gs5F1Kc&AuOul5`*w;lb>_|qrn8RYF;_k_xx&|_q2?)tLLx7r!~6TAG< zA8kwXtN!OdZ#(`~Tj$dX2a8XiI8c-CF|vX~`g^pF@JE-IoA158G`8wH0<2pb{)1R| z!_x{22>(^y1wZ~3Gd9B9JQEvhMt?ijJBrZ9K0Cl4^K|~>o^}d9-#hBFeBx&S{-om! z4}(u%po33eq)4Bhuu%9Eg&&}{(RTJh8wWEFe63>*+=H-Z%*m~=Hkes)axk;fz*4x` zId+U)@wbk}*p&zedsku{ojJw=9AqxS)7cS^r-USl$8}enzDu$ah3n(-H?x9duE=NM z3^J{pjo_tQv6xV0`a+N|(8hS@&=-ku#buh9jfMtdWgMED@TRhH7nr3(TZs0RCWZ@_ z5?ctmP1_X1H7v0eMEhlI_J0*uFTJ+<`o!wBH&!pbQ9dU;F_w*Yk3lArEiXdd#U{2HLp2aD1TO@(C`<_lE!1#0{Xb$*39o>&!z PX;=vU8(wcrgTeU!h#;N# literal 0 HcmV?d00001 diff --git a/models/__pycache__/deepseek.cpython-312.pyc b/models/__pycache__/deepseek.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d08926d95466bb3a07958f1be8812c92e4f0be21 GIT binary patch literal 3610 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 Y>3vAT4@vZ?XF?EK7RUcZuy>IB7tIK(`2YX_ literal 0 HcmV?d00001 diff --git a/models/__pycache__/factory.cpython-312.pyc b/models/__pycache__/factory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8781072cbcd72aadf990e48cb05534c99a0d21f3 GIT binary patch literal 2639 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 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 = ` +
+ +

No history yet

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