From b514145c13b433fe0f5d050c1a2b9ed6a05b592f Mon Sep 17 00:00:00 2001 From: Zylan Date: Thu, 6 Mar 2025 14:45:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=88=AA=E5=9B=BE=E5=92=8C?= =?UTF-8?q?=E5=88=86=E6=9E=90=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.py | 27 +- models/claude.py | 251 ++++++------ models/gpt4o.py | 2 +- static/js/main.js | 865 ++++++++++++++++++++++++++++-------------- static/js/settings.js | 13 +- static/js/ui.js | 15 +- static/style.css | 764 ++++++++++++++++++++++++++----------- templates/index.html | 100 ++--- 8 files changed, 1352 insertions(+), 685 deletions(-) diff --git a/app.py b/app.py index 4e96ac0..e3a8ac8 100644 --- a/app.py +++ b/app.py @@ -104,7 +104,6 @@ def stream_model_response(response_generator, sid): thinking_buffer += content # 发送完整的思考内容 - print(f"Streaming thinking content: {len(thinking_buffer)} chars") socketio.emit('claude_response', { 'status': 'thinking', 'content': thinking_buffer @@ -127,7 +126,7 @@ def stream_model_response(response_generator, sid): response_buffer += content # 发送完整的内容 - print(f"Streaming response content: {len(response_buffer)} chars") + # print(f"Streaming response content: {len(response_buffer)} chars") socketio.emit('claude_response', { 'status': 'streaming', 'content': response_buffer @@ -379,6 +378,30 @@ def handle_analyze_image(data): 'error': f'Analysis error: {str(e)}' }, room=request.sid) +@socketio.on('capture_screenshot') +def handle_capture_screenshot(data): + 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_complete', { + 'success': True, + 'image': img_str + }, room=request.sid) + except Exception as e: + error_msg = f"Screenshot error: {str(e)}" + print(f"Error capturing screenshot: {error_msg}") + socketio.emit('screenshot_complete', { + 'success': False, + 'error': error_msg + }, room=request.sid) + def run_tray(): icon = create_tray_icon() icon.run() diff --git a/models/claude.py b/models/claude.py index ad1ebaf..6b29bed 100644 --- a/models/claude.py +++ b/models/claude.py @@ -18,13 +18,12 @@ class ClaudeModel(BaseModel): def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: """Stream Claude's response for text analysis""" try: - # Initial status - yield {"status": "started", "content": ""} - - api_key = self.api_key.strip() + yield {"status": "started"} + + api_key = self.api_key if api_key.startswith('Bearer '): api_key = api_key[7:] - + headers = { 'x-api-key': api_key, 'anthropic-version': '2023-06-01', @@ -105,6 +104,16 @@ class ClaudeModel(BaseModel): "status": "thinking", "content": thinking_content } + + # 处理新的extended_thinking格式 + elif data.get('type') == 'extended_thinking_delta': + if 'delta' in data and 'text' in data['delta']: + thinking_chunk = data['delta']['text'] + thinking_content += thinking_chunk + yield { + "status": "thinking", + "content": thinking_content + } elif data.get('type') == 'message_stop': if thinking_content: @@ -135,130 +144,132 @@ class ClaudeModel(BaseModel): "error": f"Streaming error: {str(e)}" } - 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': 8192, - 'temperature': 1, - 'system': self.system_prompt, - 'thinking': { - 'type': 'enabled', - 'budget_tokens': 4096 - }, - '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 - - thinking_content = "" - response_buffer = "" + def analyze_image(self, image_data, prompt, socket=None, proxies=None): + yield {"status": "started"} + + api_key = self.api_key + if api_key.startswith('Bearer '): + api_key = api_key[7:] - for chunk in response.iter_lines(): - if not chunk: + headers = { + 'x-api-key': api_key, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + } + + payload = { + 'model': 'claude-3-7-sonnet-20250219', + 'stream': True, + 'max_tokens': 8192, + 'temperature': 1, + 'thinking': { + 'type': 'enabled', + 'budget_tokens': 4096 + }, + 'system': "You are a helpful AI assistant that specializes in solving math problems. You should provide step-by-step solutions and explanations for any math problem presented to you. If you're given an image, analyze any mathematical content in it and provide a detailed solution.", + '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 + + thinking_content = "" + response_buffer = "" + + for chunk in response.iter_lines(): + if not chunk: + continue + + try: + chunk_str = chunk.decode('utf-8') + if not chunk_str.startswith('data: '): 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) - chunk_str = chunk_str[6:] - data = json.loads(chunk_str) - - if data.get('type') == 'content_block_delta': - if 'delta' in data: - if 'text' in data['delta']: - text_chunk = data['delta']['text'] - yield { - "status": "streaming", - "content": text_chunk - } - response_buffer += text_chunk - - elif 'thinking' in data['delta']: - thinking_chunk = data['delta']['thinking'] - thinking_content += thinking_chunk - yield { - "status": "thinking", - "content": thinking_content - } - - elif data.get('type') == 'message_stop': - if thinking_content: + if data.get('type') == 'content_block_delta': + if 'delta' in data: + if 'text' in data['delta']: + text_chunk = data['delta']['text'] yield { - "status": "thinking_complete", + "status": "streaming", + "content": text_chunk + } + response_buffer += text_chunk + + elif 'thinking' in data['delta']: + thinking_chunk = data['delta']['thinking'] + thinking_content += thinking_chunk + yield { + "status": "thinking", "content": thinking_content } + + # 处理新的extended_thinking格式 + elif data.get('type') == 'extended_thinking_delta': + if 'delta' in data and 'text' in data['delta']: + thinking_chunk = data['delta']['text'] + thinking_content += thinking_chunk yield { - "status": "completed", - "content": "" + "status": "thinking", + "content": thinking_content } - elif data.get('type') == 'error': - error_msg = data.get('error', {}).get('message', 'Unknown error') + elif data.get('type') == 'message_stop': + if thinking_content: yield { - "status": "error", - "error": error_msg + "status": "thinking_complete", + "content": thinking_content } - 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)}" - } + yield { + "status": "completed", + "content": response_buffer + } + + elif data.get('type') == 'error': + error_message = data.get('error', {}).get('message', 'Unknown error') + yield { + "status": "error", + "error": error_message + } + + except Exception as e: + yield { + "status": "error", + "error": f"Error processing response: {str(e)}" + } + break diff --git a/models/gpt4o.py b/models/gpt4o.py index 1124256..64c7614 100644 --- a/models/gpt4o.py +++ b/models/gpt4o.py @@ -132,7 +132,7 @@ class GPT4oModel(BaseModel): { "type": "image_url", "image_url": { - "url": f"data:image/png;base64,{image_data}", + "url": image_data if image_data.startswith('data:') else f"data:image/png;base64,{image_data}", "detail": "high" } }, diff --git a/static/js/main.js b/static/js/main.js index 1855a86..97aed27 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,26 +1,25 @@ class SnapSolver { constructor() { - // 初始化managers - window.uiManager = new UIManager(); - window.settingsManager = new SettingsManager(); + console.log('Creating SnapSolver instance...'); - // 初始化应用组件 - this.initializeElements(); - this.initializeState(); - this.initializeConnection(); - this.setupSocketEventHandlers(); - this.setupAutoScroll(); - this.setupEventListeners(); + // 初始化属性 + this.socket = null; + this.cropper = null; + this.originalImage = null; + this.croppedImage = null; + this.extractedContent = ''; + this.emitTimeout = null; + this.eventsSetup = false; - // 初始化历史 - window.app = this; // 便于从其他地方访问 - this.updateHistoryPanel(); + // 初始化应用 + this.initialize(); } initializeElements() { // Main elements this.screenshotImg = document.getElementById('screenshotImg'); this.imagePreview = document.getElementById('imagePreview'); + this.emptyState = document.getElementById('emptyState'); this.cropBtn = document.getElementById('cropBtn'); this.captureBtn = document.getElementById('captureBtn'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); @@ -28,6 +27,7 @@ class SnapSolver { this.textEditor = document.getElementById('textEditor'); this.extractedText = document.getElementById('extractedText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); + this.cropContainer = document.getElementById('cropContainer'); this.manualTextInput = document.getElementById('manualTextInput'); this.claudePanel = document.getElementById('claudePanel'); this.responseContent = document.getElementById('responseContent'); @@ -38,7 +38,6 @@ class SnapSolver { this.statusLight = document.querySelector('.status-light'); // Crop elements - this.cropContainer = document.getElementById('cropContainer'); this.cropCancel = document.getElementById('cropCancel'); this.cropConfirm = document.getElementById('cropConfirm'); @@ -94,17 +93,17 @@ class SnapSolver { }); } - 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'); - this.extractTextBtn.classList.add('hidden'); - this.textEditor.classList.add('hidden'); + updateConnectionStatus(status, isConnected) { + if (this.connectionStatus) { + this.connectionStatus.textContent = status; + + if (isConnected) { + this.connectionStatus.classList.remove('disconnected'); + this.connectionStatus.classList.add('connected'); + } else { + this.connectionStatus.classList.remove('connected'); + this.connectionStatus.classList.add('disconnected'); + } } } @@ -140,22 +139,22 @@ class SnapSolver { this.socket.on('connect', () => { console.log('Connected to server'); - this.updateConnectionStatus(true); + this.updateConnectionStatus('已连接', true); }); this.socket.on('disconnect', () => { console.log('Disconnected from server'); - this.updateConnectionStatus(false); + this.updateConnectionStatus('已断开', false); }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); - this.updateConnectionStatus(false); + this.updateConnectionStatus('连接错误', false); }); this.socket.on('reconnect', (attemptNumber) => { console.log(`Reconnected after ${attemptNumber} attempts`); - this.updateConnectionStatus(true); + this.updateConnectionStatus('已重连', true); }); this.socket.on('reconnect_attempt', (attemptNumber) => { @@ -168,76 +167,142 @@ class SnapSolver { this.socket.on('reconnect_failed', () => { console.error('Failed to reconnect'); - window.showToast('连接服务器失败,请刷新页面重试', 'error'); + window.uiManager.showToast('Connection to server failed, please refresh the page and try again', 'error'); }); this.setupSocketEventHandlers(); } catch (error) { console.error('Connection error:', error); - this.updateConnectionStatus(false); + this.updateConnectionStatus('重连失败', false); setTimeout(() => this.initializeConnection(), 5000); } } setupSocketEventHandlers() { - // Screenshot response handler + // 如果已经设置过事件处理器,先移除它们 + if (this.hasOwnProperty('eventsSetup') && this.eventsSetup) { + this.socket.off('screenshot_response'); + this.socket.off('screenshot_complete'); + this.socket.off('request_acknowledged'); + this.socket.off('text_extracted'); + this.socket.off('claude_response'); + } + + // 标记事件处理器已设置 + this.eventsSetup = true; + + // 旧版截图响应处理器 (保留兼容性) this.socket.on('screenshot_response', (data) => { if (data.success) { this.screenshotImg.src = `data:image/png;base64,${data.image}`; + this.originalImage = `data:image/png;base64,${data.image}`; this.imagePreview.classList.remove('hidden'); - this.cropBtn.classList.remove('hidden'); + this.emptyState.classList.add('hidden'); + + // 显示Claude和提取文本按钮 + this.sendToClaudeBtn.classList.remove('hidden'); + this.extractTextBtn.classList.remove('hidden'); + + // 恢复按钮状态 this.captureBtn.disabled = false; - this.captureBtn.innerHTML = 'Capture'; - this.sendToClaudeBtn.classList.add('hidden'); - this.extractTextBtn.classList.add('hidden'); - this.textEditor.classList.add('hidden'); - window.showToast('Screenshot captured successfully'); + this.captureBtn.innerHTML = '截图'; + + // 初始化裁剪器 + this.initializeCropper(); + + window.uiManager.showToast('截图成功', 'success'); } else { - window.showToast('Failed to capture screenshot: ' + data.error, 'error'); this.captureBtn.disabled = false; - this.captureBtn.innerHTML = 'Capture'; + this.captureBtn.innerHTML = '截图'; + console.error('截图失败:', data.error); + window.uiManager.showToast('截图失败: ' + data.error, 'error'); + } + }); + + // 新版截图响应处理器 + this.socket.on('screenshot_complete', (data) => { + this.captureBtn.disabled = false; + this.captureBtn.innerHTML = '截图'; + + if (data.success) { + // 显示截图预览 + this.screenshotImg.src = 'data:image/png;base64,' + data.image; + this.originalImage = 'data:image/png;base64,' + data.image; + this.imagePreview.classList.remove('hidden'); + this.emptyState.classList.add('hidden'); + + // 显示Claude和提取文本按钮 + this.sendToClaudeBtn.classList.remove('hidden'); + this.extractTextBtn.classList.remove('hidden'); + + // 初始化裁剪工具 + this.initializeCropper(); + + // 显示成功消息 + window.uiManager.showToast('截图成功', 'success'); + } else { + // 显示错误消息 + window.uiManager.showToast('截图失败: ' + (data.error || '未知错误'), 'error'); } }); - // 请求确认响应处理器 + // 确认请求处理 this.socket.on('request_acknowledged', (data) => { - console.log('服务器确认收到请求:', data); - window.showToast(data.message || '请求已接收,正在处理...', 'info'); - // 清除可能存在的超时计时器 + console.log('服务器确认收到请求:', data); + }); + + // Text extraction response + this.socket.on('text_extracted', (data) => { + // 重新启用按钮 + this.extractTextBtn.disabled = false; + this.extractTextBtn.innerHTML = '提取文本'; + + if (this.extractedText) { + this.extractedText.disabled = false; + } + if (this.emitTimeout) { clearTimeout(this.emitTimeout); this.emitTimeout = null; } - }); - - // Text extraction response handler - this.socket.on('text_extracted', (data) => { - if (data.error) { - console.error('Text extraction error:', data.error); - window.showToast('Failed to extract text: ' + data.error, 'error'); - if (this.extractedText) { - this.extractedText.value = ''; - this.extractedText.disabled = false; - } - this.sendExtractedTextBtn.disabled = false; // Re-enable send button on server error - } else if (data.content) { - // 直接使用提取的文本内容 - this.extractedContent = data.content; - - // 更新文本编辑器 - if (this.extractedText) { - this.extractedText.value = data.content; - this.extractedText.disabled = false; - this.extractedText.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); - this.sendExtractedTextBtn.disabled = false; - } - - window.showToast('文本提取成功'); - } - this.extractTextBtn.disabled = false; - this.extractTextBtn.innerHTML = 'Extract Text'; + // 检查是否有内容数据 + if (data.content) { + this.extractedText.value = data.content; + this.extractedContent = data.content; + this.textEditor.classList.remove('hidden'); + this.sendExtractedTextBtn.disabled = false; + + // 更新置信度指示器 (如果有的话) + if (data.confidence !== undefined) { + const confidence = data.confidence || 0; + this.confidenceValue.textContent = `${Math.round(confidence * 100)}%`; + + // 置信度颜色 + const confidenceEl = this.confidenceIndicator; + if (confidence > 0.8) { + confidenceEl.style.color = 'var(--success)'; + } else if (confidence > 0.5) { + confidenceEl.style.color = 'var(--primary)'; + } else { + confidenceEl.style.color = 'var(--danger)'; + } + } + + window.uiManager.showToast('文本提取成功', 'success'); + } else if (data.error) { + console.error('文本提取失败:', data.error); + window.uiManager.showToast('文本提取失败: ' + data.error, 'error'); + + // 启用发送按钮以便用户可以手动输入文本 + this.sendExtractedTextBtn.disabled = false; + } else { + // 未知响应格式 + console.error('未知的文本提取响应格式:', data); + window.uiManager.showToast('文本提取返回未知格式', 'error'); + this.sendExtractedTextBtn.disabled = false; + } }); this.socket.on('claude_response', (data) => { @@ -253,6 +318,10 @@ class SnapSolver { this.thinkingSection.classList.add('hidden'); this.sendToClaudeBtn.disabled = true; this.sendExtractedTextBtn.disabled = true; + + // 显示进行中状态 + this.responseContent.innerHTML = '
分析进行中,请稍候...
'; + this.responseContent.style.display = 'block'; break; case 'thinking': @@ -261,11 +330,36 @@ class SnapSolver { console.log('Received thinking content'); this.thinkingSection.classList.remove('hidden'); + // 记住用户的展开/折叠状态 + const wasExpanded = this.thinkingContent.classList.contains('expanded'); + // 直接设置完整内容而不是追加 this.setElementContent(this.thinkingContent, data.content); // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); + + // 根据之前的状态决定是否展开 + if (wasExpanded) { + this.thinkingContent.classList.add('expanded'); + this.thinkingContent.classList.remove('collapsed'); + + // 更新切换按钮图标 + const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-up'; + } + } else { + // 初始状态为折叠 + this.thinkingContent.classList.add('collapsed'); + this.thinkingContent.classList.remove('expanded'); + + // 更新切换按钮图标 + const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } + } } break; @@ -287,8 +381,9 @@ class SnapSolver { if (data.content) { console.log('Received content'); - // 直接设置完整内容 - this.setElementContent(this.responseContent, data.content); + // 设置结果内容 + this.responseContent.innerHTML = data.content; + this.responseContent.style.display = 'block'; // 移除思考部分的打字动画 this.thinkingContent.classList.remove('thinking-typing'); @@ -300,12 +395,30 @@ class SnapSolver { this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; + // 恢复界面 + this.updateStatusLight('completed'); + // 保存到历史记录 const responseText = this.responseContent.textContent || ''; const thinkingText = this.thinkingContent.textContent || ''; this.addToHistory(this.croppedImage, responseText, thinkingText); - window.showToast('Analysis completed successfully'); + // 确保思考内容处于折叠状态 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.add('collapsed'); + const toggleBtn = document.querySelector('#thinkingToggle .toggle-btn i'); + if (toggleBtn) { + toggleBtn.className = 'fas fa-chevron-down'; + } + + // 添加明确的提示 + window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success'); + + // 确保结果内容可见 + this.responseContent.style.display = 'block'; + + // 滚动到结果内容 + this.responseContent.scrollIntoView({ behavior: 'smooth' }); break; case 'error': @@ -320,7 +433,7 @@ class SnapSolver { this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; - window.showToast('Analysis failed: ' + errorMessage, 'error'); + window.uiManager.showToast('Analysis failed: ' + errorMessage, 'error'); break; default: @@ -330,10 +443,86 @@ class SnapSolver { this.setElementContent(this.responseContent, currentText + '\nError: ' + data.error); this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; - window.showToast('Unknown error occurred', 'error'); + window.uiManager.showToast('Unknown error occurred', 'error'); } } }); + + // 接收到thinking数据时 + this.socket.on('thinking', (data) => { + console.log('收到思考过程数据'); + + // 显示思考区域 + this.thinkingSection.classList.remove('hidden'); + this.thinkingContent.textContent = data.thinking; + + // 记住用户的展开/折叠状态 + const wasExpanded = this.thinkingContent.classList.contains('expanded'); + + // 如果之前没有设置状态,默认为折叠 + if (!wasExpanded && !this.thinkingContent.classList.contains('collapsed')) { + this.thinkingContent.classList.add('collapsed'); + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } + } + }); + + // 思考过程完成 + this.socket.on('thinking_complete', (data) => { + console.log('思考过程完成'); + this.thinkingSection.classList.remove('hidden'); + this.thinkingContent.textContent = data.thinking; + + // 确保图标正确显示 + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } + + // 添加提示信息 + const toast = window.uiManager.showToast('思考过程已完成,点击标题可查看详细思考过程', 'success'); + toast.style.zIndex = '1000'; + }); + + // 分析完成 + this.socket.on('analysis_complete', (data) => { + console.log('分析完成,接收到结果'); + this.updateStatusLight('completed'); + this.enableInterface(); + + // 显示分析结果 + if (this.responseContent) { + this.responseContent.innerHTML = data.response; + this.responseContent.style.display = 'block'; + + // 滚动到结果区域 + setTimeout(() => { + this.responseContent.scrollIntoView({ behavior: 'smooth' }); + }, 200); + } + + // 添加到历史记录 + this.addToHistory(this.croppedImage, data.response, data.thinking); + + // 确保思考部分完全显示(如果有的话) + if (data.thinking && this.thinkingSection && this.thinkingContent) { + this.thinkingSection.classList.remove('hidden'); + this.thinkingContent.textContent = data.thinking; + + // 确保初始状态为折叠 + this.thinkingContent.classList.remove('expanded'); + this.thinkingContent.classList.add('collapsed'); + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } + + // 弹出提示 + window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success'); + } + }); } // 新方法:安全设置DOM内容的方法(替代updateElementContent) @@ -379,30 +568,14 @@ class SnapSolver { this.cropper = new Cropper(clonedImage, { viewMode: 1, - dragMode: 'crop', - autoCropArea: 0, - restore: false, + dragMode: 'move', + aspectRatio: NaN, modal: true, - guides: true, - highlight: true, - cropBoxMovable: true, - cropBoxResizable: true, - toggleDragModeOnDblclick: false, - minCropBoxWidth: 50, - minCropBoxHeight: 50, - background: true, - responsive: true, - checkOrientation: true, - ready: function() { - // Use the stored reference to this - if (self.cropper) { - self.cropper.crop(); - } - } + background: true }); } catch (error) { - console.error('Error initializing cropper:', error); - window.showToast('Failed to initialize cropper', 'error'); + console.error('Failed to initialize cropper', error); + window.uiManager.showToast('裁剪器初始化失败', 'error'); // 确保在出错时关闭裁剪界面 if (this.cropContainer) { @@ -487,92 +660,11 @@ class SnapSolver { } updateHistoryPanel() { - const historyContent = document.querySelector('.history-content'); - if (!historyContent) return; - - const historyJson = localStorage.getItem('snapHistory') || '[]'; - const history = JSON.parse(historyJson); - - if (history.length === 0) { - historyContent.innerHTML = ` -
- -

无历史记录

-
- `; - return; + // 如果历史面板存在,更新其内容 + if (this.historyContent) { + // 这里可以实现历史记录的加载和显示 + // 暂时留空,后续可以实现 } - - const historyItems = history.map(item => { - const date = new Date(item.timestamp); - const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; - const hasResponse = item.response ? 'true' : 'false'; - - // 检查图像是否为有效的数据URL - let imageHtml = ''; - if (this.isValidImageDataUrl(item.image)) { - // 有效的图像数据URL - imageHtml = `历史记录图片`; - } else { - // 图像已被优化或不存在,显示占位符 - imageHtml = `
- - 图片已优化 -
`; - } - - return ` -
-
- ${formattedDate} -
-
- ${imageHtml} -
-
- `; - }).join(''); - - historyContent.innerHTML = historyItems; - - // Add click event listeners for history items - document.querySelectorAll('.history-item').forEach(item => { - item.addEventListener('click', () => { - const historyItem = history.find(h => h.id === parseInt(item.dataset.id)); - if (historyItem) { - // 检查图像是否为有效的数据URL - if (this.isValidImageDataUrl(historyItem.image)) { - // 有效的图像数据 - window.app.screenshotImg.src = historyItem.image; - window.app.imagePreview.classList.remove('hidden'); - } else { - // 图像已优化或不存在,显示占位符图像 - window.app.screenshotImg.src = this.getPlaceholderImageUrl(); - 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'); - window.app.extractTextBtn.classList.add('hidden'); - - // Set response content - if (historyItem.response) { - window.app.claudePanel.classList.remove('hidden'); - window.app.responseContent.textContent = historyItem.response; - } - - // Set thinking content if available - if (historyItem.thinking) { - window.app.thinkingSection.classList.remove('hidden'); - window.app.thinkingContent.textContent = historyItem.thinking; - } else { - window.app.thinkingSection.classList.add('hidden'); - } - } - }); - }); } setupEventListeners() { @@ -584,21 +676,19 @@ class SnapSolver { } setupCaptureEvents() { - // Capture button - this.captureBtn.addEventListener('click', async () => { - if (!this.socket || !this.socket.connected) { - window.showToast('Not connected to server', 'error'); - return; - } - + // 截图按钮 + this.captureBtn.addEventListener('click', () => { + if (!this.checkConnectionBeforeAction()) return; + try { - this.captureBtn.disabled = true; - this.captureBtn.innerHTML = 'Capturing...'; - this.socket.emit('request_screenshot'); + this.captureBtn.disabled = true; // 禁用按钮防止重复点击 + this.captureBtn.innerHTML = '正在截图...'; + this.socket.emit('capture_screenshot', {}); } catch (error) { - window.showToast('Error requesting screenshot: ' + error.message, 'error'); + console.error('Error starting capture:', error); + window.uiManager.showToast('启动截图失败', 'error'); this.captureBtn.disabled = false; - this.captureBtn.innerHTML = 'Capture'; + this.captureBtn.innerHTML = '截取屏幕'; } }); } @@ -606,6 +696,8 @@ class SnapSolver { setupCropEvents() { // Crop button this.cropBtn.addEventListener('click', () => { + if (!this.checkConnectionBeforeAction()) return; + if (this.screenshotImg.src) { this.initializeCropper(); } @@ -613,6 +705,8 @@ class SnapSolver { // Crop confirm button document.getElementById('cropConfirm').addEventListener('click', () => { + if (!this.checkConnectionBeforeAction()) return; + if (this.cropper) { try { console.log('Starting crop operation...'); @@ -675,14 +769,14 @@ class SnapSolver { this.cropBtn.classList.remove('hidden'); this.sendToClaudeBtn.classList.remove('hidden'); this.extractTextBtn.classList.remove('hidden'); - window.showToast('Image cropped successfully'); + window.uiManager.showToast('Cropping successful'); } catch (error) { console.error('Cropping error details:', { message: error.message, stack: error.stack, cropperState: this.cropper ? 'initialized' : 'not initialized' }); - window.showToast(error.message || 'Error while cropping image', 'error'); + window.uiManager.showToast(error.message || '裁剪图像时出错', 'error'); } finally { // Always clean up the cropper instance if (this.cropper) { @@ -709,66 +803,75 @@ class SnapSolver { setupAnalysisEvents() { // Extract Text button this.extractTextBtn.addEventListener('click', () => { + if (!this.checkConnectionBeforeAction()) return; + if (!this.croppedImage) { - window.showToast('Please crop the image first', 'error'); + window.uiManager.showToast('请先裁剪图片', 'error'); return; } this.extractTextBtn.disabled = true; this.sendExtractedTextBtn.disabled = true; // Disable the send button while extracting - this.extractTextBtn.innerHTML = 'Extracting...'; + this.extractTextBtn.innerHTML = '提取中...'; const settings = window.settingsManager.getSettings(); const mathpixAppId = settings.mathpixAppId; const mathpixAppKey = settings.mathpixAppKey; if (!mathpixAppId || !mathpixAppKey) { - window.showToast('Please enter Mathpix credentials in settings', 'error'); + window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error'); document.getElementById('settingsPanel').classList.remove('hidden'); this.extractTextBtn.disabled = false; - this.extractTextBtn.innerHTML = 'Extract Text'; + this.extractTextBtn.innerHTML = '提取文本'; return; } // Show text editor and prepare UI this.textEditor.classList.remove('hidden'); if (this.extractedText) { - this.extractedText.value = 'Extracting text...'; + this.extractedText.value = '正在提取文本...'; this.extractedText.disabled = true; } try { - // 设置超时时间(10秒)以避免长时间无响应 + // 设置超时时间(15秒)以避免长时间无响应 this.emitTimeout = setTimeout(() => { - window.showToast('文本提取超时,请重试或手动输入文本', 'error'); + window.uiManager.showToast('文本提取超时,请重试或手动输入文本', 'error'); this.extractTextBtn.disabled = false; - this.extractTextBtn.innerHTML = 'Extract Text'; + this.extractTextBtn.innerHTML = '提取文本'; this.extractedText.disabled = false; - }, 10000); + this.sendExtractedTextBtn.disabled = false; + }, 15000); this.socket.emit('extract_text', { image: this.croppedImage.split(',')[1], settings: { mathpixApiKey: `${mathpixAppId}:${mathpixAppKey}` } - }, (ackResponse) => { - // 如果服务器确认收到请求,清除超时 - clearTimeout(this.emitTimeout); + }); + + // 监听服务器确认请求的响应 + this.socket.once('request_acknowledged', (ackResponse) => { console.log('服务器确认收到文本提取请求', ackResponse); }); } catch (error) { - window.showToast('Failed to extract text: ' + error.message, 'error'); + window.uiManager.showToast('提取文本失败: ' + error.message, 'error'); this.extractTextBtn.disabled = false; - this.sendExtractedTextBtn.disabled = false; // Re-enable send button on error - this.extractTextBtn.innerHTML = 'Extract Text'; + this.sendExtractedTextBtn.disabled = false; + this.extractTextBtn.innerHTML = '提取文本'; + if (this.extractedText) { + this.extractedText.disabled = false; + } } }); // Send Extracted Text button this.sendExtractedTextBtn.addEventListener('click', () => { + if (!this.checkConnectionBeforeAction()) return; + const text = this.extractedText.value.trim(); if (!text) { - window.showToast('Please enter some text', 'error'); + window.uiManager.showToast('请输入一些文本', 'error'); return; } @@ -796,67 +899,49 @@ class SnapSolver { } catch (error) { this.responseContent.textContent = 'Error: Failed to send text for analysis - ' + error.message; this.sendExtractedTextBtn.disabled = false; - window.showToast('Failed to send text for analysis', 'error'); + window.uiManager.showToast('发送文本进行分析失败', 'error'); } }); // 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(); - const apiKeys = {}; - Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => { - if (input.value) { - apiKeys[model] = input.value; - } - }); + if (!this.checkConnectionBeforeAction()) return; - this.claudePanel.classList.remove('hidden'); - this.responseContent.textContent = ''; - this.sendToClaudeBtn.disabled = true; - - try { - // 添加图片大小检查和压缩 - const base64Data = this.croppedImage.split(',')[1]; - // 计算图片大小(以字节为单位) - const imageSize = Math.ceil((base64Data.length * 3) / 4); - console.log(`图片大小: ${imageSize / 1024} KB`); - - // 如果图片大小超过8MB(WebSocket默认限制),则显示错误 - if (imageSize > 8 * 1024 * 1024) { - window.showToast('图片太大,请裁剪更小的区域或使用文本提取功能', 'error'); - this.sendToClaudeBtn.disabled = false; - return; + if (this.croppedImage) { + try { + this.sendImageToClaude(this.croppedImage); + } catch (error) { + console.error('Error:', error); + window.uiManager.showToast('发送图片失败: ' + error.message, 'error'); } - - // 设置超时时间(10秒)以避免长时间无响应 - this.emitTimeout = setTimeout(() => { - window.showToast('发送图片超时,请重试或使用文本提取功能', 'error'); - this.sendToClaudeBtn.disabled = false; - }, 10000); - - this.socket.emit('analyze_image', { - image: base64Data, - settings: { - ...settings, - api_keys: apiKeys, - model: settings.model || 'claude-3-7-sonnet-20250219', - } - }, (ackResponse) => { - // 如果服务器确认收到请求,清除超时 - clearTimeout(this.emitTimeout); - console.log('服务器确认收到图片分析请求', ackResponse); - }); - } catch (error) { - this.responseContent.textContent = 'Error: Failed to send image for analysis - ' + error.message; - this.sendToClaudeBtn.disabled = false; - window.showToast('Failed to send image for analysis', 'error'); + } else { + window.uiManager.showToast('请先裁剪图片', 'error'); } }); + + // Handle Claude panel close button + const closeClaudePanel = document.getElementById('closeClaudePanel'); + if (closeClaudePanel) { + closeClaudePanel.addEventListener('click', () => { + this.claudePanel.classList.add('hidden'); + + // 如果图像预览也被隐藏,显示空状态 + if (this.imagePreview.classList.contains('hidden')) { + this.emptyState.classList.remove('hidden'); + } + + // 重置状态指示灯 + this.updateStatusLight(''); + + // 清空响应内容,准备下一次分析 + this.responseContent.innerHTML = ''; + + // 隐藏思考部分 + this.thinkingSection.classList.add('hidden'); + this.thinkingContent.innerHTML = ''; + this.thinkingContent.className = 'thinking-content collapsed'; + }); + } } setupKeyboardShortcuts() { @@ -876,38 +961,238 @@ class SnapSolver { } setupThinkingToggle() { - // Toggle thinking content visibility - if (this.thinkingToggle) { - this.thinkingToggle.addEventListener('click', () => { - const isCollapsed = this.thinkingContent.classList.contains('collapsed'); - - if (isCollapsed) { - this.thinkingContent.classList.remove('collapsed'); - this.thinkingContent.classList.add('expanded'); - this.thinkingToggle.classList.add('thinking-toggle-active'); - const icon = this.thinkingToggle.querySelector('.toggle-btn i'); - if (icon) { - icon.classList.remove('fa-chevron-down'); - icon.classList.add('fa-chevron-up'); - } - } else { - this.thinkingContent.classList.add('collapsed'); - this.thinkingContent.classList.remove('expanded'); - this.thinkingToggle.classList.remove('thinking-toggle-active'); - const icon = this.thinkingToggle.querySelector('.toggle-btn i'); - if (icon) { - icon.classList.remove('fa-chevron-up'); - icon.classList.add('fa-chevron-down'); - } - } - }); + // 确保正确获取DOM元素 + const thinkingSection = document.getElementById('thinkingSection'); + const thinkingToggle = document.getElementById('thinkingToggle'); + const thinkingContent = document.getElementById('thinkingContent'); + + if (!thinkingToggle || !thinkingContent) { + console.error('思考切换组件未找到必要的DOM元素'); + return; } + + // 存储DOM引用 + this.thinkingSection = thinkingSection; + this.thinkingToggle = thinkingToggle; + this.thinkingContent = thinkingContent; + + // 直接使用函数,确保作用域正确 + thinkingToggle.onclick = () => { + console.log('点击了思考标题'); + + // 先移除两个类,然后添加正确的类 + const isExpanded = thinkingContent.classList.contains('expanded'); + const toggleIcon = thinkingToggle.querySelector('.toggle-btn i'); + + // 从样式上清除当前状态 + thinkingContent.classList.remove('expanded'); + thinkingContent.classList.remove('collapsed'); + + if (isExpanded) { + console.log('折叠思考内容'); + // 添加折叠状态 + thinkingContent.classList.add('collapsed'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } + window.uiManager.showToast('思考内容已折叠', 'info'); + } else { + console.log('展开思考内容'); + // 添加展开状态 + thinkingContent.classList.add('expanded'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-up'; + } + window.uiManager.showToast('思考内容已展开', 'info'); + } + }; + + console.log('思考切换组件初始化完成'); } // 获取用于显示的图像URL,如果原始URL无效则返回占位符 getImageForDisplay(imageUrl) { return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl(); } + + sendImageToClaude(imageData) { + const settings = window.settingsManager.getSettings(); + + // 获取API密钥 + const apiKeys = {}; + Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => { + if (input.value) { + apiKeys[model] = input.value; + } + }); + + try { + // 处理图像数据,去除base64前缀 + let processedImageData = imageData; + if (imageData.startsWith('data:')) { + // 分割数据URL,只保留base64部分 + processedImageData = imageData.split(',')[1]; + } + + this.socket.emit('analyze_image', { + image: processedImageData, + settings: { + ...settings, + api_keys: apiKeys, + model: settings.model || 'claude-3-7-sonnet-20250219', + } + }); + + // 显示Claude分析面板 + this.claudePanel.classList.remove('hidden'); + this.responseContent.textContent = ''; + } catch (error) { + this.responseContent.textContent = 'Error: ' + error.message; + window.uiManager.showToast('发送图片分析失败', 'error'); + } + } + + initialize() { + console.log('Initializing SnapSolver...'); + + // 初始化managers + window.uiManager = new UIManager(); + window.settingsManager = new SettingsManager(); + window.app = this; // 便于从其他地方访问 + + // 建立与服务器的连接 + this.connectToServer(); + + // 初始化UI元素和事件处理 + this.initializeElements(); + this.setupSocketEventHandlers(); + this.setupCaptureEvents(); + this.setupCropEvents(); + this.setupAnalysisEvents(); + this.setupKeyboardShortcuts(); + + // 确保思考切换功能正确初始化 + this.setupThinkingToggle(); + + this.setupEventListeners(); + this.setupAutoScroll(); + + // 监听窗口大小变化,调整界面 + window.addEventListener('resize', this.handleResize.bind(this)); + + // 点击文档任何地方隐藏历史面板 + document.addEventListener('click', (e) => { + if (this.historyPanel && + !this.historyPanel.contains(e.target) && + !e.target.closest('#historyToggle')) { + this.historyPanel.classList.add('hidden'); + } + }); + + // 设置默认UI状态 + this.enableInterface(); + + // 初始化历史 + this.updateHistoryPanel(); + + console.log('SnapSolver initialization complete'); + } + + handleResize() { + // 如果裁剪器存在,需要调整其大小和位置 + if (this.cropper) { + this.cropper.resize(); + } + + // 可以在这里添加其他响应式UI调整 + } + + enableInterface() { + // 启用主要界面元素 + if (this.captureBtn) { + this.captureBtn.disabled = false; + this.captureBtn.innerHTML = '截图'; + } + + // 显示默认的空白状态 + if (this.emptyState && this.imagePreview) { + if (!this.originalImage) { + this.emptyState.classList.remove('hidden'); + this.imagePreview.classList.add('hidden'); + } else { + this.emptyState.classList.add('hidden'); + this.imagePreview.classList.remove('hidden'); + } + } + + console.log('Interface enabled'); + } + + connectToServer() { + console.log('Connecting to server...'); + + // 创建Socket.IO连接 + this.socket = io({ + reconnectionAttempts: 5, + reconnectionDelay: 1000, + timeout: 20000 + }); + + // 连接事件处理 + this.socket.on('connect', () => { + console.log('Connected to server'); + this.updateConnectionStatus('已连接', true); + + // 连接后启用界面 + this.enableInterface(); + }); + + this.socket.on('disconnect', () => { + console.log('Disconnected from server'); + this.updateConnectionStatus('已断开', false); + + // 断开连接时禁用界面 + if (this.captureBtn) { + this.captureBtn.disabled = true; + } + }); + + this.socket.on('connect_error', (error) => { + console.error('Connection error:', error); + this.updateConnectionStatus('连接错误', false); + }); + + this.socket.on('reconnect_attempt', (attemptNumber) => { + console.log(`Reconnection attempt ${attemptNumber}`); + this.updateConnectionStatus('正在重连...', false); + }); + + this.socket.on('reconnect', () => { + console.log('Reconnected to server'); + this.updateConnectionStatus('已重连', true); + + // 重连后启用界面 + this.enableInterface(); + }); + + this.socket.on('reconnect_failed', () => { + console.error('Failed to reconnect'); + this.updateConnectionStatus('重连失败', false); + window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error'); + }); + } + + isConnected() { + return this.connectionStatus && this.connectionStatus.classList.contains('connected'); + } + + checkConnectionBeforeAction(action) { + if (!this.isConnected()) { + window.uiManager.showToast('未连接到服务器,请等待连接建立后再试', 'error'); + return false; + } + return true; + } } // Initialize the application when the DOM is loaded diff --git a/static/js/settings.js b/static/js/settings.js index 35db9bc..d393e0e 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -136,11 +136,20 @@ class SettingsManager { } getSettings() { + const language = this.languageInput.value || '中文'; + const basePrompt = this.systemPromptInput.value || ''; + + // 检查系统提示词是否已包含语言设置 + let systemPrompt = basePrompt; + if (!basePrompt.includes('Please respond in') && !basePrompt.includes('请用') && !basePrompt.includes('使用')) { + systemPrompt = `${basePrompt}\n\n请务必使用${language}回答。`; + } + return { model: this.modelSelect.value, temperature: this.temperatureInput.value, - language: this.languageInput.value, - systemPrompt: this.systemPromptInput.value + ` Please respond in ${this.languageInput.value}.`, + language: language, + systemPrompt: systemPrompt, proxyEnabled: this.proxyEnabledInput.checked, proxyHost: this.proxyHostInput.value, proxyPort: this.proxyPortInput.value, diff --git a/static/js/ui.js b/static/js/ui.js index 8959428..afb82c0 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -47,6 +47,16 @@ class UIManager { } showToast(message, type = 'success') { + // 检查是否已经存在相同内容的提示 + const existingToasts = this.toastContainer.querySelectorAll('.toast'); + for (const existingToast of existingToasts) { + const existingMessage = existingToast.querySelector('span').textContent; + if (existingMessage === message) { + // 已经存在相同的提示,不再创建新的 + return; + } + } + const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` @@ -55,10 +65,13 @@ class UIManager { `; this.toastContainer.appendChild(toast); + // 为不同类型的提示设置不同的显示时间 + const displayTime = message === '截图成功' ? 1500 : 3000; + setTimeout(() => { toast.style.opacity = '0'; setTimeout(() => toast.remove(), 300); - }, 3000); + }, displayTime); } closeAllPanels() { diff --git a/static/style.css b/static/style.css index 7fa580a..a03c0da 100644 --- a/static/style.css +++ b/static/style.css @@ -12,17 +12,22 @@ --shadow-color: rgba(0, 0, 0, 0.1); --error-color: #f44336; --success-color: #4CAF50; - --primary: #4a6cf7; - --secondary: #6c757d; - --danger: #f44336; + --primary: #2196f3; + --primary-rgb: 33, 150, 243; + --primary-light: #bbdefb; + --secondary: #ff9800; --success: #4caf50; + --danger: #f44336; + --warning: #ff9800; + --info: #2196f3; --background: #f5f7fb; --surface: #ffffff; + --surface-rgb: 255, 255, 255; --surface-alt: #f0f4f8; + --surface-alt-rgb: 248, 249, 250; --text: #2c3e50; - --text-secondary: #505a66; + --text-tertiary: #9e9e9e; --shadow-color: rgba(0, 0, 0, 0.1); - --border-color: #e0e0e0; --hover-color: #e9ecef; --accent: #4a6cf7; --placeholder: #a0a0a0; @@ -36,17 +41,22 @@ --secondary-dark: #66BB6A; --background: #121212; --surface: #1E1E1E; + --surface-rgb: 30, 30, 30; --text-primary: #FFFFFF; --text-secondary: #B0B0B0; + --text-tertiary: #909090; --border-color: #333333; --shadow-color: rgba(0, 0, 0, 0.3); - --primary: #4a6cf7; - --secondary: #6c757d; + --primary: #64b5f6; + --primary-rgb: 100, 181, 246; + --primary-light: #bbdefb; + --secondary: #ff9800; --danger: #f44336; --success: #4caf50; --background: #1a1a2e; --surface: #272741; --surface-alt: #202035; + --surface-alt-rgb: 37, 37, 37; --text: #f0f0f0; --text-secondary: #a0a0a0; --shadow-color: rgba(0, 0, 0, 0.4); @@ -78,17 +88,22 @@ body { display: flex; flex-direction: column; min-height: 100vh; + background-color: var(--background); + position: relative; } /* Header Styles */ .app-header { background-color: var(--surface); - padding: 1rem; - box-shadow: 0 2px 4px var(--shadow-color); + padding: 1rem 1.5rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); display: flex; justify-content: space-between; align-items: center; z-index: 100; + position: sticky; + top: 0; + border-bottom: 1px solid var(--border-color); } .header-left { @@ -99,8 +114,11 @@ body { .header-left h1 { font-size: 1.5rem; - color: var(--primary-color); + color: var(--primary); margin: 0; + font-weight: 600; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + letter-spacing: -0.5px; } .connection-status { @@ -109,48 +127,93 @@ body { gap: 1rem; } -.connection-form { - display: flex; - gap: 0.5rem; -} - .status { padding: 0.4rem 0.8rem; border-radius: 1rem; font-size: 0.875rem; - font-weight: 500; + font-weight: 600; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.3s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.status::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; } .status.connected { - background-color: var(--success-color); - color: white; + background-color: rgba(76, 175, 80, 0.15); + color: var(--success); + border: 1px solid rgba(76, 175, 80, 0.3); +} + +.status.connected::before { + background-color: var(--success); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); } .status.disconnected { - background-color: var(--error-color); - color: white; + background-color: rgba(244, 67, 54, 0.15); + color: var(--danger); + border: 1px solid rgba(244, 67, 54, 0.3); +} + +.status.disconnected::before { + background-color: var(--danger); + box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.3); } .header-right { display: flex; - gap: 0.5rem; + gap: 0.8rem; +} + +.header-right .btn-icon { + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background-color: var(--surface-alt); + color: var(--text-secondary); + transition: all 0.2s ease; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.header-right .btn-icon:hover { + transform: translateY(-2px); + background-color: var(--hover-color); + color: var(--primary); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.header-right .btn-icon:active { + transform: translateY(0); } /* Main Content */ .app-main { flex: 1; display: flex; - padding: 1rem; - gap: 1rem; + padding: 1.5rem; + gap: 1.5rem; position: relative; overflow: hidden; + background-color: var(--background); } .content-panel { flex: 1; display: flex; flex-direction: column; - gap: 1rem; + gap: 1.5rem; max-width: 1200px; margin: 0 auto; width: 100%; @@ -159,30 +222,34 @@ body { /* Capture Section */ .capture-section { background-color: var(--surface); - border-radius: 0.5rem; - box-shadow: 0 2px 4px var(--shadow-color); - padding: 1rem; + border-radius: 1rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + padding: 1.5rem; + transition: all 0.3s ease; + border: 1px solid var(--border-color); } .toolbar { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; - padding: 0.5rem; - background-color: var(--surface); - border-radius: 0.5rem; + margin-bottom: 1.5rem; + padding: 0.75rem 1rem; + background-color: var(--surface-alt); + border-radius: 0.75rem; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03); } .toolbar-buttons { display: flex; justify-content: flex-start; align-items: center; + width: 100%; } .button-group { display: flex; - gap: 0.5rem; + gap: 1rem; align-items: center; } @@ -190,62 +257,125 @@ body { display: flex; flex-direction: column; align-items: center; - gap: 1rem; - margin-top: 1rem; - padding: 1rem; + gap: 1.5rem; + margin-top: 1.5rem; + padding: 1.5rem; + background-color: var(--surface-alt); + border-radius: 0.75rem; + transition: all 0.3s ease; } .analysis-button .button-group { display: flex; - gap: 0.5rem; + gap: 1rem; width: 100%; - max-width: 400px; + max-width: 500px; justify-content: center; } .text-editor { width: 100%; - max-width: 600px; + max-width: 700px; display: flex; flex-direction: column; gap: 1rem; + background-color: var(--surface); + padding: 1.5rem; + border-radius: 0.75rem; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + border: 1px solid var(--border-color); + transition: all 0.3s ease; +} + +.text-editor:hover { + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); } .text-editor textarea { width: 100%; - min-height: 120px; - padding: 0.75rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; + min-height: 150px; + padding: 1rem; + border: 1.5px solid var(--border-color); + border-radius: 0.5rem; background-color: var(--background); color: var(--text-primary); - font-size: 0.9375rem; + font-size: 1rem; resize: vertical; + transition: all 0.2s ease; + line-height: 1.6; } .text-editor textarea:focus { outline: none; - border-color: var(--primary-color); - box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1); + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15); } .text-editor button { align-self: flex-end; } +.text-format-controls { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 0.5rem; +} + +.send-text-group { + display: flex; + align-items: center; + gap: 1rem; + margin-left: auto; +} + +.confidence-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + background-color: var(--surface-alt); + color: var(--success); + font-size: 0.875rem; + font-weight: 500; +} + +.confidence-indicator i { + font-size: 1rem; +} + +.confidence-value { + font-weight: 600; +} + .image-preview { position: relative; - border-radius: 0.5rem; + border-radius: 1rem; overflow: hidden; - background-color: var(--background); + background-color: var(--surface-alt); margin: 0; - padding: 1rem; + padding: 1.5rem; + text-align: center; + transition: all 0.3s ease; + border: 1px dashed var(--border-color); } .image-container { display: inline-block; position: relative; margin: 0 auto; + border-radius: 0.75rem; + overflow: hidden; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + max-width: 90%; +} + +.image-container:hover { + transform: scale(1.01); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15); } #screenshotImg { @@ -253,7 +383,8 @@ body { width: auto; height: auto; max-width: 100%; - border-radius: 0.5rem; + border-radius: 0.75rem; + transition: all 0.3s ease; } @media (max-width: 768px) { @@ -266,30 +397,50 @@ body { /* Claude Panel */ .claude-panel { background-color: var(--surface); - border-radius: 0.5rem; - box-shadow: 0 2px 4px var(--shadow-color); - padding: 1rem; - flex: 1; + border-radius: 1rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); display: flex; flex-direction: column; + height: auto; + min-height: 300px; + max-height: 90vh; + border: 1px solid var(--border-color); + transition: all 0.3s ease; + overflow: hidden; + width: 100%; +} + +.claude-panel:not(.hidden) { + animation: panel-slide-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards; +} + +@keyframes panel-slide-in { + 0% { + opacity: 0; + transform: translateY(20px); + } + 100% { + opacity: 1; + transform: translateY(0); + } } .panel-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 1rem; + padding: 1.25rem; + border-bottom: 1px solid var(--border-color); + background-color: var(--surface); + position: sticky; + top: 0; + z-index: 5; } .header-title { display: flex; align-items: center; - gap: 0.75rem; -} - -.panel-header h2 { - font-size: 1.25rem; - color: var(--text-primary); + gap: 1rem; } .analysis-status { @@ -303,42 +454,55 @@ body { border-radius: 50%; background-color: var(--text-secondary); transition: background-color 0.3s ease; + box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); } .status-light.processing { background-color: #ffd700; animation: pulse 1.5s infinite; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); } .status-light.completed { - background-color: var(--success-color); + background-color: var(--success); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); } .status-light.error { - background-color: var(--error-color); + background-color: var(--danger); + box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2); } @keyframes pulse { 0% { opacity: 1; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); } 50% { - opacity: 0.5; + opacity: 0.6; + box-shadow: 0 0 0 4px rgba(255, 215, 0, 0.4); } 100% { opacity: 1; + box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); } } .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; + padding: 1.5rem; + background-color: #fff; + border-radius: 6px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); line-height: 1.6; + white-space: pre-wrap; + overflow-wrap: break-word; + margin-top: 1rem; + font-size: 1rem; +} + +[data-theme="dark"] .response-content { + background-color: rgba(var(--surface-rgb), 0.7); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } /* Settings Panel */ @@ -350,12 +514,13 @@ body { width: 400px; max-width: 100vw; background-color: var(--surface); - box-shadow: -2px 0 4px var(--shadow-color); + box-shadow: -8px 0 20px var(--shadow-color); z-index: 1000; transform: translateX(100%); - transition: transform 0.3s ease; + transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1); display: flex; flex-direction: column; + border-left: 1px solid var(--border-color); } .settings-panel:not(.hidden) { @@ -365,29 +530,63 @@ body { .settings-content { flex: 1; overflow-y: auto; - padding: 1rem; + padding: 1.5rem; } .settings-section { - margin-bottom: 2rem; + margin-bottom: 2.5rem; + background-color: var(--surface-alt); + border-radius: 0.75rem; + padding: 1.5rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.settings-section:hover { + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + transform: translateY(-2px); } .settings-section h3 { color: var(--text-primary); - margin-bottom: 1rem; - font-size: 1.1rem; + margin-bottom: 1.5rem; + font-size: 1.15rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.settings-section h3::before { + content: ""; + display: inline-block; + width: 4px; + height: 18px; + background-color: var(--primary); + border-radius: 2px; } /* Form Elements */ .setting-group { - margin-bottom: 1rem; + margin-bottom: 1.5rem; + position: relative; +} + +.setting-group:last-child { + margin-bottom: 0; } .setting-group label { display: block; - margin-bottom: 0.5rem; + margin-bottom: 0.75rem; color: var(--text-secondary); font-size: 0.875rem; + font-weight: 500; + transition: color 0.2s ease; +} + +.setting-group:hover label { + color: var(--primary); } input[type="text"], @@ -396,21 +595,23 @@ input[type="number"], select, textarea { width: 100%; - padding: 0.75rem; - border: 1px solid var(--border-color); - border-radius: 0.375rem; + padding: 0.85rem 1rem; + border: 1.5px solid var(--border-color); + border-radius: 0.5rem; background-color: var(--background); color: var(--text-primary); font-size: 0.9375rem; - transition: border-color 0.3s, box-shadow 0.3s; + transition: all 0.2s ease-in-out; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); } 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); + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15); + transform: translateY(-1px); } .input-group { @@ -420,12 +621,19 @@ textarea:focus { } .input-group input { - padding-right: 2.5rem; + padding-right: 2.75rem; } .input-group .btn-icon { position: absolute; - right: 0.5rem; + right: 0.75rem; + background-color: transparent; + transition: all 0.2s ease; +} + +.input-group .btn-icon:hover { + color: var(--primary); + transform: scale(1.1); } .range-group { @@ -436,28 +644,83 @@ textarea:focus { input[type="range"] { flex: 1; - height: 4px; + height: 6px; -webkit-appearance: none; - background: var(--primary-color); - border-radius: 2px; + background: linear-gradient(to right, var(--primary) 0%, var(--primary) 70%, var(--border-color) 70%, var(--border-color) 100%); + border-radius: 3px; + cursor: pointer; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; - width: 16px; - height: 16px; + width: 18px; + height: 18px; border-radius: 50%; - background: var(--primary-color); + background: var(--primary); + box-shadow: 0 0 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s ease, box-shadow 0.2s ease; cursor: pointer; - border: 2px solid var(--surface); - box-shadow: 0 1px 3px var(--shadow-color); +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); + box-shadow: 0 0 0 4px rgba(74, 108, 247, 0.2); +} + +#temperatureValue { + min-width: 30px; + text-align: center; + font-weight: 600; + color: var(--primary); } .checkbox-label { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.75rem; cursor: pointer; + user-select: none; +} + +.checkbox-label input[type="checkbox"] { + appearance: none; + width: 18px; + height: 18px; + border: 1.5px solid var(--border-color); + border-radius: 4px; + position: relative; + transition: all 0.2s ease; + background-color: var(--background); +} + +.checkbox-label input[type="checkbox"]:checked { + background-color: var(--primary); + border-color: var(--primary); +} + +.checkbox-label input[type="checkbox"]:checked::after { + content: "✓"; + position: absolute; + color: white; + font-size: 14px; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +.checkbox-label input[type="checkbox"]:focus { + box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15); +} + +.proxy-settings { + padding-top: 1rem; + margin-top: 0.5rem; + border-top: 1px dashed var(--border-color); + transition: opacity 0.3s ease, transform 0.3s ease; +} + +#proxyEnabled:not(:checked) ~ #proxySettings { + opacity: 0.5; } /* Buttons */ @@ -466,51 +729,93 @@ input[type="range"]::-webkit-slider-thumb { .btn-icon { padding: 0.75rem 1.5rem; border: none; - border-radius: 0.375rem; + border-radius: 0.5rem; font-size: 0.9375rem; - font-weight: 500; + font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; - transition: all 0.2s; + transition: all 0.2s ease; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.btn-primary::after, +.btn-secondary::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.1); + transform: translateY(100%); + transition: transform 0.2s ease; +} + +.btn-primary:hover::after, +.btn-secondary:hover::after { + transform: translateY(0); } .btn-primary { - background-color: var(--primary-color); + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); color: white; } .btn-primary:hover { - background-color: var(--primary-dark); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.btn-primary:active { + transform: translateY(0); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .btn-secondary { - background-color: var(--background); + background-color: var(--surface-alt); color: var(--text-primary); border: 1px solid var(--border-color); } .btn-secondary:hover { - background-color: var(--border-color); + background-color: var(--hover-color); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.btn-secondary:active { + transform: translateY(0); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); } .btn-icon { padding: 0.5rem; - border-radius: 0.375rem; + border-radius: 0.5rem; background: transparent; color: var(--text-secondary); + box-shadow: none; } .btn-icon:hover { - background-color: var(--background); - color: var(--text-primary); + background-color: var(--hover-color); + color: var(--primary); + transform: translateY(-2px); +} + +.btn-icon:active { + transform: translateY(0); } button:disabled { opacity: 0.6; cursor: not-allowed; + transform: none !important; + box-shadow: none !important; } /* Floating Action Button */ @@ -542,9 +847,10 @@ button:disabled { /* Toast Notifications */ .toast-container { position: fixed; - bottom: 2rem; - left: 50%; - transform: translateX(-50%); + top: 1rem; + right: 1rem; + left: auto; + transform: none; z-index: 1000; display: flex; flex-direction: column; @@ -555,7 +861,7 @@ button:disabled { .toast { background-color: var(--surface); color: var(--text-primary); - padding: 1rem 1.5rem; + padding: 0.75rem 1rem; border-radius: 0.375rem; box-shadow: 0 4px 6px var(--shadow-color); display: flex; @@ -563,6 +869,9 @@ button:disabled { gap: 0.75rem; pointer-events: auto; animation: toast-in 0.3s ease; + max-width: 250px; + opacity: 0.9; + font-size: 0.9rem; } .toast.success { @@ -619,12 +928,12 @@ button:disabled { /* Animations */ @keyframes toast-in { from { - transform: translateY(100%); + transform: translateX(100%); opacity: 0; } to { - transform: translateY(0); - opacity: 1; + transform: translateX(0); + opacity: 0.9; } } @@ -855,109 +1164,60 @@ body.history-view .claude-panel { } } -/* Text Format Controls */ -.text-format-controls { - display: flex; - flex-direction: column; - align-items: center; - gap: 1rem; - padding: 0.5rem 0; -} - -.format-toggle { - display: flex; - gap: 0.25rem; - background-color: var(--background); - padding: 0.25rem; - border-radius: 0.375rem; - border: 1px solid var(--border-color); - margin: 0 auto; -} - -.format-btn { - padding: 0.5rem 1rem; - border: none; - border-radius: 0.25rem; - background: transparent; - color: var(--text-secondary); - font-size: 0.875rem; - cursor: pointer; - transition: all 0.2s; -} - -.format-btn.active { - background-color: var(--primary-color); - color: white; -} - -.send-text-group { - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; - justify-content: flex-end; -} - -.confidence-indicator { - display: flex; - align-items: center; - gap: 0.25rem; - font-size: 0.875rem; - color: var(--success-color); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - background-color: var(--background); - border: 1px solid var(--border-color); -} - -.confidence-indicator i { - font-size: 1rem; -} - -.confidence-value { - font-weight: 500; -} - /* Thinking Section */ .thinking-section { - background-color: var(--surface-alt); - border-radius: 0.5rem; margin-bottom: 1rem; - overflow: hidden; - border: 1px solid var(--border-color); + border-bottom: 1px solid var(--border-color); } .thinking-header { + padding: 1rem 1.5rem; + cursor: pointer; display: flex; justify-content: space-between; align-items: center; - padding: 0.8rem 1rem; - cursor: pointer; - background-color: var(--surface-alt); - border-bottom: 1px solid var(--border-color); - transition: background-color 0.2s ease; + transition: all 0.2s ease; + background-color: rgba(var(--primary-rgb), 0.1); + border: 2px solid transparent; + border-radius: 6px; + position: relative; +} + +.thinking-header::after { + content: "点击展开/折叠"; + position: absolute; + right: 60px; + color: var(--text-secondary); + font-size: 0.8rem; + opacity: 0; + transition: opacity 0.2s ease; } .thinking-header:hover { - background-color: var(--hover-color); + background-color: rgba(var(--primary-rgb), 0.15); + border: 2px dashed rgba(var(--primary-rgb), 0.3); +} + +.thinking-header:hover::after { + opacity: 0.7; } .thinking-title { display: flex; align-items: center; - gap: 0.5rem; + gap: 0.75rem; } .thinking-title i { - color: var(--accent); - font-size: 1.1rem; + color: var(--primary); + font-size: 1.125rem; } .thinking-title h3 { margin: 0; font-size: 1rem; font-weight: 600; - color: var(--text); + color: var(--text-primary); } .toggle-btn { @@ -965,58 +1225,40 @@ body.history-view .claude-panel { border: none; color: var(--text-secondary); cursor: pointer; - transition: transform 0.3s ease; - padding: 0.3rem; - border-radius: 50%; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; } .toggle-btn:hover { - background-color: var(--hover-color); + color: var(--primary); } .thinking-content { padding: 0; - max-height: 500px; - overflow-y: auto; + background-color: rgba(var(--primary-rgb), 0.05); + border-left: 3px solid rgba(var(--primary-rgb), 0.3); + overflow: hidden; transition: max-height 0.3s ease, padding 0.3s ease; - overflow-x: hidden; - white-space: pre-wrap; font-family: monospace; + white-space: pre-wrap; font-size: 0.9rem; line-height: 1.5; - color: var(--text-secondary); - background-color: rgba(0, 0, 0, 0.02); - border-radius: 0.3rem; -} - -[data-theme="dark"] .thinking-content { - background-color: rgba(255, 255, 255, 0.03); -} - -.thinking-content code { - background-color: rgba(0, 0, 0, 0.05); - padding: 0.2rem 0.4rem; - border-radius: 0.2rem; - font-size: 0.85rem; -} - -[data-theme="dark"] .thinking-content code { - background-color: rgba(255, 255, 255, 0.05); } .thinking-content.collapsed { max-height: 0; - padding: 0 1rem; + padding: 0 1.5rem; + overflow: hidden; } .thinking-content.expanded { - padding: 1rem; + max-height: 600px; + padding: 1rem 1.5rem; overflow-y: auto; - border-top: 1px solid var(--border-color); -} - -.thinking-toggle-active .toggle-btn i { - transform: rotate(180deg); } /* Animation for thinking content */ @@ -1107,3 +1349,81 @@ body.history-view .claude-panel { line-height: 1.2; display: block; } + +/* Select Styling */ +.select-styled { + appearance: none; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat; + background-position: right 0.7rem top 50%; + background-size: 0.65rem auto; + padding-right: 2rem !important; + cursor: pointer; +} + +.select-styled:focus { + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); +} + +[data-theme="dark"] .select-styled { + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23A0A0A0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); +} + +[data-theme="dark"] .select-styled:focus { + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E"); +} + +/* 空状态提示样式 */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 4rem 2rem; + text-align: center; + color: var(--text-secondary); + height: 100%; +} + +.empty-state i { + font-size: 4rem; + margin-bottom: 1.5rem; + color: var(--border-color); + opacity: 0.7; +} + +.empty-state h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1rem; + color: var(--text-primary); +} + +.empty-state p { + font-size: 1rem; + max-width: 400px; + margin: 0 auto 1.5rem; + line-height: 1.6; +} + +.loading-message { + text-align: center; + padding: 2rem; + color: var(--text-secondary); + font-style: italic; +} + +.thinking-hint { + font-size: 0.8rem; + font-weight: normal; + color: var(--text-secondary); + margin-left: 0.5rem; +} + +/* 响应标题样式 */ +.response-header, +.response-title, +.response-title i, +.response-title h3 { + display: none; +} diff --git a/templates/index.html b/templates/index.html index bfbe0a9..8edc6d0 100644 --- a/templates/index.html +++ b/templates/index.html @@ -15,17 +15,17 @@

Snap Solver

-
Disconnected
+
未连接
- - -
@@ -35,45 +35,50 @@
-
-
- - +
+
+ + +
+
+ +

准备好开始了吗?

+

点击"截图"按钮捕获屏幕,然后使用AI分析图像或提取文本。您可以截取数学题、代码或任何需要帮助的内容。