暂无历史记录
-diff --git a/app.py b/app.py index e3a8ac8..e5c3492 100644 --- a/app.py +++ b/app.py @@ -9,6 +9,7 @@ import pystray from PIL import Image, ImageDraw import pyperclip from models import ModelFactory +import time app = Flask(__name__) socketio = SocketIO(app, cors_allowed_origins="*", ping_timeout=30, ping_interval=5, max_http_buffer_size=50 * 1024 * 1024) @@ -85,6 +86,9 @@ def stream_model_response(response_generator, sid): response_buffer = "" thinking_buffer = "" + # 上次发送的时间戳,用于控制发送频率 + last_emit_time = time.time() + # 流式处理响应 for response in response_generator: # 处理Mathpix响应 @@ -100,18 +104,21 @@ def stream_model_response(response_generator, sid): # 根据不同的状态进行处理 if status == 'thinking': - # 累积思考内容到缓冲区 - thinking_buffer += content + # 直接使用模型提供的完整思考内容 + thinking_buffer = content - # 发送完整的思考内容 - socketio.emit('claude_response', { - 'status': 'thinking', - 'content': thinking_buffer - }, room=sid) + # 控制发送频率,至少间隔0.3秒 + current_time = time.time() + if current_time - last_emit_time >= 0.3: + socketio.emit('claude_response', { + 'status': 'thinking', + 'content': thinking_buffer + }, room=sid) + last_emit_time = current_time elif status == 'thinking_complete': # 直接使用完整的思考内容 - thinking_buffer = content # 使用服务器提供的完整内容 + thinking_buffer = content print(f"Thinking complete, total length: {len(thinking_buffer)} chars") socketio.emit('claude_response', { @@ -120,27 +127,34 @@ def stream_model_response(response_generator, sid): }, room=sid) elif status == 'streaming': - # 流式输出正常内容 - if content: - # 累积到服务端缓冲区 - response_buffer += content - - # 发送完整的内容 - # print(f"Streaming response content: {len(response_buffer)} chars") + # 直接使用模型提供的完整内容 + response_buffer = content + + # 控制发送频率,至少间隔0.3秒 + current_time = time.time() + if current_time - last_emit_time >= 0.3: socketio.emit('claude_response', { 'status': 'streaming', 'content': response_buffer }, room=sid) + last_emit_time = current_time - else: - # 其他状态直接转发 - socketio.emit('claude_response', response, room=sid) + elif status == 'completed': + # 确保发送最终完整内容 + socketio.emit('claude_response', { + 'status': 'completed', + 'content': content or response_buffer + }, room=sid) + print("Response completed") - # 调试信息 - if status == 'completed': - print("Response completed") - elif status == 'error': - print(f"Error: {response.get('error', 'Unknown error')}") + elif status == 'error': + # 错误状态直接转发 + socketio.emit('claude_response', response, room=sid) + print(f"Error: {response.get('error', 'Unknown error')}") + + # 其他状态直接转发 + else: + socketio.emit('claude_response', response, room=sid) except Exception as e: error_msg = f"Streaming error: {str(e)}" diff --git a/models/claude.py b/models/claude.py index 6b29bed..813d61a 100644 --- a/models/claude.py +++ b/models/claude.py @@ -91,39 +91,47 @@ class ClaudeModel(BaseModel): if 'delta' in data: if 'text' in data['delta']: text_chunk = data['delta']['text'] - yield { - "status": "streaming", - "content": text_chunk - } response_buffer += text_chunk + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(text_chunk) >= 10 or text_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } elif 'thinking' in data['delta']: thinking_chunk = data['delta']['thinking'] thinking_content += thinking_chunk - yield { - "status": "thinking", - "content": thinking_content - } + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(thinking_chunk) >= 20 or thinking_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + 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": "thinking", - "content": thinking_content - } + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(thinking_chunk) >= 20 or thinking_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "thinking", + "content": thinking_content + } elif data.get('type') == 'message_stop': + # 确保发送完整的思考内容 if thinking_content: yield { "status": "thinking_complete", "content": thinking_content } + # 确保发送完整的响应内容 yield { "status": "completed", - "content": "" + "content": response_buffer } elif data.get('type') == 'error': @@ -144,7 +152,7 @@ class ClaudeModel(BaseModel): "error": f"Streaming error: {str(e)}" } - def analyze_image(self, image_data, prompt, socket=None, proxies=None): + def analyze_image(self, image_data, proxies=None): yield {"status": "started"} api_key = self.api_key @@ -157,6 +165,9 @@ class ClaudeModel(BaseModel): 'content-type': 'application/json' } + # 获取系统提示词,确保包含语言设置 + system_prompt = self.system_prompt + payload = { 'model': 'claude-3-7-sonnet-20250219', 'stream': True, @@ -166,7 +177,7 @@ class ClaudeModel(BaseModel): '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.", + 'system': system_prompt, 'messages': [{ 'role': 'user', 'content': [ @@ -225,36 +236,44 @@ class ClaudeModel(BaseModel): if 'delta' in data: if 'text' in data['delta']: text_chunk = data['delta']['text'] - yield { - "status": "streaming", - "content": text_chunk - } response_buffer += text_chunk + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(text_chunk) >= 10 or text_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } elif 'thinking' in data['delta']: thinking_chunk = data['delta']['thinking'] thinking_content += thinking_chunk - yield { - "status": "thinking", - "content": thinking_content - } + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(thinking_chunk) >= 20 or thinking_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + 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": "thinking", - "content": thinking_content - } + # 只在每累积一定数量的字符后才发送,减少UI跳变 + if len(thinking_chunk) >= 20 or thinking_chunk.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "thinking", + "content": thinking_content + } elif data.get('type') == 'message_stop': + # 确保发送完整的思考内容 if thinking_content: yield { "status": "thinking_complete", "content": thinking_content } + # 确保发送完整的响应内容 yield { "status": "completed", "content": response_buffer diff --git a/models/gpt4o.py b/models/gpt4o.py index 64c7614..b5d86c1 100644 --- a/models/gpt4o.py +++ b/models/gpt4o.py @@ -35,12 +35,10 @@ class GPT4oModel(BaseModel): if 'https' in proxies: os.environ['https_proxy'] = proxies['https'] - # Create OpenAI client - client = OpenAI( - api_key=self.api_key, - base_url="https://api.openai.com/v1" # Replace with actual GPT-4o API endpoint - ) + # Initialize OpenAI client + client = OpenAI(api_key=self.api_key) + # Prepare messages messages = [ { "role": "system", @@ -60,39 +58,49 @@ class GPT4oModel(BaseModel): max_tokens=4000 ) + # 使用累积缓冲区 + response_buffer = "" + for chunk in response: if hasattr(chunk.choices[0].delta, 'content'): content = chunk.choices[0].delta.content if content: - yield { - "status": "streaming", - "content": content - } + # 累积内容 + response_buffer += content + + # 只在累积一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 10 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + + # 确保发送最终完整内容 + if response_buffer: + yield { + "status": "streaming", + "content": response_buffer + } # Send completion status yield { "status": "completed", - "content": "" + "content": response_buffer } finally: # Restore original environment state for key, value in original_env.items(): if value is None: - os.environ.pop(key, None) + if key in os.environ: + del os.environ[key] else: os.environ[key] = value 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}" + "error": str(e) } def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]: @@ -115,12 +123,10 @@ class GPT4oModel(BaseModel): if 'https' in proxies: os.environ['https_proxy'] = proxies['https'] - # Create OpenAI client - client = OpenAI( - api_key=self.api_key, - base_url="https://api.openai.com/v1" # Replace with actual GPT-4o API endpoint - ) + # Initialize OpenAI client + client = OpenAI(api_key=self.api_key) + # Prepare messages with image messages = [ { "role": "system", @@ -132,13 +138,12 @@ class GPT4oModel(BaseModel): { "type": "image_url", "image_url": { - "url": image_data if image_data.startswith('data:') else f"data:image/png;base64,{image_data}", - "detail": "high" + "url": f"data:image/jpeg;base64,{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." + "text": "Please analyze this image and provide a detailed solution." } ] } @@ -152,37 +157,47 @@ class GPT4oModel(BaseModel): max_tokens=4000 ) + # 使用累积缓冲区 + response_buffer = "" + for chunk in response: if hasattr(chunk.choices[0].delta, 'content'): content = chunk.choices[0].delta.content if content: - yield { - "status": "streaming", - "content": content - } + # 累积内容 + response_buffer += content + + # 只在累积一定数量的字符或遇到句子结束标记时才发送 + if len(content) >= 10 or content.endswith(('.', '!', '?', '。', '!', '?', '\n')): + yield { + "status": "streaming", + "content": response_buffer + } + + # 确保发送最终完整内容 + if response_buffer: + yield { + "status": "streaming", + "content": response_buffer + } # Send completion status yield { "status": "completed", - "content": "" + "content": response_buffer } finally: # Restore original environment state for key, value in original_env.items(): if value is None: - os.environ.pop(key, None) + if key in os.environ: + del os.environ[key] else: os.environ[key] = value 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}" + "error": str(e) } diff --git a/static/js/main.js b/static/js/main.js index 97aed27..e718983 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -40,25 +40,14 @@ class SnapSolver { // Crop elements this.cropCancel = document.getElementById('cropCancel'); this.cropConfirm = document.getElementById('cropConfirm'); - - // Format toggle elements - this.confidenceIndicator = document.getElementById('confidenceIndicator'); - this.confidenceValue = document.querySelector('.confidence-value'); - - // History elements - this.historyPanel = document.getElementById('historyPanel'); - this.historyContent = document.querySelector('.history-content'); - this.closeHistory = document.getElementById('closeHistory'); - this.historyToggle = document.getElementById('historyToggle'); } initializeState() { this.socket = null; this.cropper = null; this.croppedImage = null; - this.history = JSON.parse(localStorage.getItem('snapHistory') || '[]'); - this.emitTimeout = null; this.extractedContent = ''; + this.emitTimeout = null; // 确保裁剪容器和其他面板初始为隐藏状态 if (this.cropContainer) { @@ -274,22 +263,6 @@ class SnapSolver { 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); @@ -309,24 +282,33 @@ class SnapSolver { console.log('Received claude_response:', data); this.updateStatusLight(data.status); + // 确保Claude面板可见 + if (this.claudePanel && this.claudePanel.classList.contains('hidden')) { + this.claudePanel.classList.remove('hidden'); + } + switch (data.status) { case 'started': console.log('Analysis started'); // 清空显示内容 - this.responseContent.innerHTML = ''; - this.thinkingContent.innerHTML = ''; - this.thinkingSection.classList.add('hidden'); - this.sendToClaudeBtn.disabled = true; - this.sendExtractedTextBtn.disabled = true; + if (this.responseContent) this.responseContent.innerHTML = ''; + if (this.thinkingContent) this.thinkingContent.innerHTML = ''; + if (this.thinkingSection) this.thinkingSection.classList.add('hidden'); + + // 禁用按钮防止重复点击 + if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = true; + if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = true; // 显示进行中状态 - this.responseContent.innerHTML = '
'; - this.responseContent.style.display = 'block'; + if (this.responseContent) { + this.responseContent.innerHTML = ''; + this.responseContent.style.display = 'block'; + } break; case 'thinking': // 处理思考内容 - if (data.content) { + if (data.content && this.thinkingContent && this.thinkingSection) { console.log('Received thinking content'); this.thinkingSection.classList.remove('hidden'); @@ -365,7 +347,7 @@ class SnapSolver { case 'thinking_complete': // 完整的思考内容 - if (data.content) { + if (data.content && this.thinkingContent && this.thinkingSection) { console.log('Thinking complete'); this.thinkingSection.classList.remove('hidden'); @@ -378,47 +360,59 @@ class SnapSolver { break; case 'streaming': - if (data.content) { - console.log('Received content'); + if (data.content && this.responseContent) { + console.log('Received content chunk'); - // 设置结果内容 - this.responseContent.innerHTML = data.content; + // 使用更安全的方式设置内容,避免HTML解析问题 + this.setElementContent(this.responseContent, data.content); this.responseContent.style.display = 'block'; // 移除思考部分的打字动画 - this.thinkingContent.classList.remove('thinking-typing'); + if (this.thinkingContent) { + this.thinkingContent.classList.remove('thinking-typing'); + } + + // 平滑滚动到最新内容 + this.responseContent.scrollTop = this.responseContent.scrollHeight; } break; case 'completed': console.log('Analysis completed'); - this.sendToClaudeBtn.disabled = false; - this.sendExtractedTextBtn.disabled = false; + + // 重新启用按钮 + if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; + if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; // 恢复界面 this.updateStatusLight('completed'); - // 保存到历史记录 - const responseText = this.responseContent.textContent || ''; - const thinkingText = this.thinkingContent.textContent || ''; - this.addToHistory(this.croppedImage, responseText, thinkingText); + // 确保思考组件可见,只是将内容折叠 + if (this.thinkingSection && this.thinkingContent) { + this.thinkingSection.classList.remove('hidden'); + 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'; + } + } - // 确保思考内容处于折叠状态 - 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'; + // 确保响应内容完整显示 + if (data.content && data.content.trim() !== '' && this.responseContent) { + this.setElementContent(this.responseContent, data.content); } // 添加明确的提示 window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success'); // 确保结果内容可见 - this.responseContent.style.display = 'block'; - - // 滚动到结果内容 - this.responseContent.scrollIntoView({ behavior: 'smooth' }); + if (this.responseContent) { + this.responseContent.style.display = 'block'; + + // 滚动到结果内容 + this.responseContent.scrollIntoView({ behavior: 'smooth' }); + } break; case 'error': @@ -426,25 +420,33 @@ class SnapSolver { const errorMessage = data.error || 'Unknown error occurred'; // 显示错误信息 - if (errorMessage) { + if (errorMessage && this.responseContent) { const currentText = this.responseContent.textContent || ''; this.setElementContent(this.responseContent, currentText + '\nError: ' + errorMessage); } - this.sendToClaudeBtn.disabled = false; - this.sendExtractedTextBtn.disabled = false; + // 重新启用按钮 + if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; + if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; + window.uiManager.showToast('Analysis failed: ' + errorMessage, 'error'); break; default: console.warn('Unknown response status:', data.status); - if (data.error) { - const currentText = this.responseContent.textContent || ''; - this.setElementContent(this.responseContent, currentText + '\nError: ' + data.error); - this.sendToClaudeBtn.disabled = false; - this.sendExtractedTextBtn.disabled = false; - window.uiManager.showToast('Unknown error occurred', 'error'); + + // 对于未知状态,尝试显示内容(如果有) + if (data.content && this.responseContent) { + this.setElementContent(this.responseContent, data.content); + this.responseContent.style.display = 'block'; } + + // 确保按钮可用 + if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false; + if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false; + + window.uiManager.showToast('Unknown error occurred', 'error'); + break; } }); @@ -503,9 +505,6 @@ class SnapSolver { }, 200); } - // 添加到历史记录 - this.addToHistory(this.croppedImage, data.response, data.thinking); - // 确保思考部分完全显示(如果有的话) if (data.thinking && this.thinkingSection && this.thinkingContent) { this.thinkingSection.classList.remove('hidden'); @@ -584,89 +583,6 @@ class SnapSolver { } } - addToHistory(imageData, response, thinking) { - try { - // 读取现有历史记录 - const historyJson = localStorage.getItem('snapHistory') || '[]'; - const history = JSON.parse(historyJson); - - // 限制图像数据大小 - 缩小图像或者移除图像数据 - let optimizedImageData = null; - - if (this.isValidImageDataUrl(imageData)) { - // 检查图像字符串长度,如果过大则不存储完整图像 - if (imageData.length > 50000) { // 约50KB的限制 - // 使用安全的占位符 - optimizedImageData = null; - } else { - optimizedImageData = imageData; - } - } - - // 创建新的历史记录项 - const timestamp = new Date().toISOString(); - const id = Date.now(); - const item = { - id, - timestamp, - image: optimizedImageData, - response: response ? response.substring(0, 5000) : "", // 限制响应长度 - thinking: thinking ? thinking.substring(0, 2000) : "" // 限制思考过程长度 - }; - - // 添加到历史记录并保存 - history.unshift(item); - - // 限制历史记录数量,更激进地清理以防止存储空间不足 - const maxHistoryItems = 10; // 减少最大历史记录数量 - if (history.length > maxHistoryItems) { - history.length = maxHistoryItems; // 直接截断数组 - } - - try { - localStorage.setItem('snapHistory', JSON.stringify(history)); - } catch (storageError) { - console.warn('Storage quota exceeded, clearing older history items'); - - // 如果仍然失败,则更激进地清理 - if (history.length > 3) { - history.length = 3; // 只保留最新的3条记录 - try { - localStorage.setItem('snapHistory', JSON.stringify(history)); - } catch (severeError) { - // 如果还是失败,则清空历史记录 - localStorage.removeItem('snapHistory'); - localStorage.setItem('snapHistory', JSON.stringify([item])); // 只保留当前项 - } - } - } - - // 更新历史面板 - this.updateHistoryPanel(); - - } catch (error) { - console.error('Failed to save to history:', error); - } - } - - // 新增一个工具函数来判断图像URL是否有效 - isValidImageDataUrl(url) { - return url && typeof url === 'string' && url.startsWith('data:image/') && url.includes(','); - } - - // 获取一个安全的占位符图像URL - getPlaceholderImageUrl() { - return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTUwIiB2aWV3Qm94PSIwIDAgMjAwIDE1MCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNTAiIGZpbGw9IiNmMGYwZjAiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE0IiBmaWxsPSIjOTk5Ij7lm77niYflj5HpgIHlt7LkvJjljJY8L3RleHQ+PC9zdmc+'; - } - - updateHistoryPanel() { - // 如果历史面板存在,更新其内容 - if (this.historyContent) { - // 这里可以实现历史记录的加载和显示 - // 暂时留空,后续可以实现 - } - } - setupEventListeners() { this.setupCaptureEvents(); this.setupCropEvents(); @@ -869,6 +785,9 @@ class SnapSolver { this.sendExtractedTextBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; + // 防止重复点击 + if (this.sendExtractedTextBtn.disabled) return; + const text = this.extractedText.value.trim(); if (!text) { window.uiManager.showToast('请输入一些文本', 'error'); @@ -883,8 +802,14 @@ class SnapSolver { } }); + // 清空之前的结果 + this.responseContent.innerHTML = ''; + this.thinkingContent.innerHTML = ''; + + // 显示Claude分析面板 this.claudePanel.classList.remove('hidden'); - this.responseContent.textContent = ''; + + // 禁用按钮防止重复点击 this.sendExtractedTextBtn.disabled = true; try { @@ -907,15 +832,29 @@ class SnapSolver { this.sendToClaudeBtn.addEventListener('click', () => { if (!this.checkConnectionBeforeAction()) return; + // 防止重复点击 + if (this.sendToClaudeBtn.disabled) return; + this.sendToClaudeBtn.disabled = true; + if (this.croppedImage) { try { + // 清空之前的结果 + this.responseContent.innerHTML = ''; + this.thinkingContent.innerHTML = ''; + + // 显示Claude分析面板 + this.claudePanel.classList.remove('hidden'); + + // 发送图片进行分析 this.sendImageToClaude(this.croppedImage); } catch (error) { console.error('Error:', error); window.uiManager.showToast('发送图片失败: ' + error.message, 'error'); + this.sendToClaudeBtn.disabled = false; } } else { window.uiManager.showToast('请先裁剪图片', 'error'); + this.sendToClaudeBtn.disabled = false; } }); @@ -1043,12 +982,11 @@ class SnapSolver { } }); - // 显示Claude分析面板 - this.claudePanel.classList.remove('hidden'); - this.responseContent.textContent = ''; + // 注意:Claude面板的显示已经在点击事件中处理,这里不再重复 } catch (error) { this.responseContent.textContent = 'Error: ' + error.message; window.uiManager.showToast('发送图片分析失败', 'error'); + this.sendToClaudeBtn.disabled = false; } } @@ -1080,21 +1018,20 @@ class SnapSolver { // 监听窗口大小变化,调整界面 window.addEventListener('resize', this.handleResize.bind(this)); - // 点击文档任何地方隐藏历史面板 + // 监听document点击事件,处理面板关闭 document.addEventListener('click', (e) => { - if (this.historyPanel && - !this.historyPanel.contains(e.target) && - !e.target.closest('#historyToggle')) { - this.historyPanel.classList.add('hidden'); + // 关闭裁剪器 + if (this.cropContainer && + !this.cropContainer.contains(e.target) && + !e.target.matches('#cropBtn') && + !this.cropContainer.classList.contains('hidden')) { + this.cropContainer.classList.add('hidden'); } }); // 设置默认UI状态 this.enableInterface(); - // 初始化历史 - this.updateHistoryPanel(); - console.log('SnapSolver initialization complete'); } diff --git a/static/js/ui.js b/static/js/ui.js index afb82c0..301a307 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -1,51 +1,38 @@ class UIManager { constructor() { - this.initializeElements(); - this.setupTheme(); + // UI elements + this.settingsPanel = document.getElementById('settingsPanel'); + this.settingsToggle = document.getElementById('settingsToggle'); + this.closeSettings = document.getElementById('closeSettings'); + this.themeToggle = document.getElementById('themeToggle'); + this.toastContainer = document.getElementById('toastContainer'); + + // Check for preferred color scheme + this.checkPreferredColorScheme(); + + // Initialize event listeners 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() { + + checkPreferredColorScheme() { + const savedTheme = localStorage.getItem('theme'); 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 existingToasts = this.toastContainer.querySelectorAll('.toast'); @@ -73,81 +60,37 @@ class UIManager { setTimeout(() => toast.remove(), 300); }, displayTime); } - + closeAllPanels() { this.settingsPanel.classList.add('hidden'); - this.historyPanel.classList.add('hidden'); } - + setupEventListeners() { + // Settings panel + this.settingsToggle.addEventListener('click', () => { + this.closeAllPanels(); + this.settingsPanel.classList.toggle('hidden'); + }); + + this.closeSettings.addEventListener('click', () => { + this.settingsPanel.classList.add('hidden'); + }); + // Theme toggle this.themeToggle.addEventListener('click', () => { - const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; - this.setTheme(!isDark); + const currentTheme = document.documentElement.getAttribute('data-theme'); + this.setTheme(currentTheme !== 'dark'); }); - - // History panel - this.historyToggle.addEventListener('click', () => { - this.closeAllPanels(); - this.historyPanel.classList.toggle('hidden'); - if (window.app && typeof window.app.updateHistoryPanel === 'function') { - window.app.updateHistoryPanel(); + + // Close panels when clicking outside + document.addEventListener('click', (e) => { + // Only close if click is outside the panel and not on the toggle button + if (this.settingsPanel && + !this.settingsPanel.contains(e.target) && + !e.target.closest('#settingsToggle')) { + this.settingsPanel.classList.add('hidden'); } }); - - 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'); - if (window.app && typeof window.app.updateHistoryPanel === 'function') { - window.app.updateHistoryPanel(); - } - 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'); - } - } } } diff --git a/static/style.css b/static/style.css index a03c0da..8379f09 100644 --- a/static/style.css +++ b/static/style.css @@ -95,88 +95,86 @@ body { /* Header Styles */ .app-header { background-color: var(--surface); - padding: 1rem 1.5rem; + padding: 0.75rem 1rem; 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 { +.header-content { display: flex; + justify-content: space-between; align-items: center; - gap: 2rem; + flex-wrap: nowrap; + max-width: 1200px; + margin: 0 auto; + width: 100%; } -.header-left h1 { - font-size: 1.5rem; +.app-header h1 { + font-size: 1.3rem; color: var(--primary); margin: 0; font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); letter-spacing: -0.5px; + white-space: nowrap; } -.connection-status { - display: flex; - align-items: center; - gap: 1rem; -} - -.status { - padding: 0.4rem 0.8rem; +#connectionStatus { + font-size: 0.8rem; + padding: 0.2rem 0.5rem; border-radius: 1rem; - font-size: 0.875rem; + margin: 0 0.5rem; + white-space: nowrap; font-weight: 600; display: inline-flex; align-items: center; - gap: 0.5rem; + gap: 0.3rem; transition: all 0.3s ease; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.status::before { +#connectionStatus::before { content: ""; display: inline-block; - width: 8px; - height: 8px; + width: 6px; + height: 6px; border-radius: 50%; } -.status.connected { +#connectionStatus.connected { background-color: rgba(76, 175, 80, 0.15); color: var(--success); border: 1px solid rgba(76, 175, 80, 0.3); } -.status.connected::before { +#connectionStatus.connected::before { background-color: var(--success); box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3); } -.status.disconnected { +#connectionStatus.disconnected { background-color: rgba(244, 67, 54, 0.15); color: var(--danger); border: 1px solid rgba(244, 67, 54, 0.3); } -.status.disconnected::before { +#connectionStatus.disconnected::before { background-color: var(--danger); box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.3); } -.header-right { +.header-buttons { display: flex; - gap: 0.8rem; + gap: 0.5rem; } -.header-right .btn-icon { - width: 40px; - height: 40px; +.header-buttons .btn-icon { + width: 36px; + height: 36px; border-radius: 50%; display: flex; align-items: center; @@ -187,14 +185,14 @@ body { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); } -.header-right .btn-icon:hover { +.header-buttons .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 { +.header-buttons .btn-icon:active { transform: translateY(0); } @@ -275,21 +273,20 @@ body { .text-editor { width: 100%; - max-width: 700px; display: flex; flex-direction: column; - gap: 1rem; + gap: 1.5rem; 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; + margin-top: 1.5rem; } .text-editor:hover { box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); - transform: translateY(-2px); } .text-editor textarea { @@ -303,7 +300,7 @@ body { font-size: 1rem; resize: vertical; transition: all 0.2s ease; - line-height: 1.6; + line-height: 1.5; } .text-editor textarea:focus { @@ -312,44 +309,14 @@ body { 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; +/* 发送文本按钮样式 */ +#sendExtractedText { + width: 100%; + justify-content: center; + padding: 0.875rem; 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: 1rem; @@ -388,10 +355,144 @@ body { } @media (max-width: 768px) { + .app-header { + padding: 0.6rem; + } + + .app-header h1 { + font-size: 1.1rem; + } + + #connectionStatus { + font-size: 0.7rem; + padding: 0.15rem 0.4rem; + } + + .header-buttons .btn-icon { + width: 32px; + height: 32px; + } + .toolbar-buttons { flex-direction: row; gap: 0.5rem; } + + .button-group { + flex-wrap: wrap; + } + + .btn-primary, .btn-secondary { + padding: 0.5rem 0.7rem; + font-size: 0.85rem; + } + + .app-main { + padding: 1rem 0.75rem; + } + + .settings-panel { + width: 100%; + } + + .claude-panel { + border-radius: 0.75rem; + min-height: 200px; + } + + .response-content { + padding: 1rem; + font-size: 0.95rem; + } + + .thinking-content { + font-size: 0.85rem; + } + + .thinking-content.expanded { + padding: 0.75rem 1rem; + } + + .fab { + right: 1rem; + bottom: 1rem; + } + + .text-editor { + padding: 1.25rem; + margin-top: 1.25rem; + } + + .text-editor textarea { + min-height: 120px; + font-size: 0.95rem; + } + + #sendExtractedText { + padding: 0.75rem; + } +} + +@media (max-width: 480px) { + .app-header h1 { + font-size: 1rem; + } + + #connectionStatus { + max-width: 70px; + text-overflow: ellipsis; + overflow: hidden; + } + + .header-buttons .btn-icon { + width: 28px; + height: 28px; + font-size: 0.85rem; + } + + .toolbar-buttons { + justify-content: center; + } + + .toast-container { + top: 3.5rem; + right: 0.5rem; + } + + .empty-state i { + font-size: 3rem; + } + + .empty-state h3 { + font-size: 1.2rem; + } + + .empty-state p { + font-size: 0.9rem; + } + + .text-editor { + padding: 1rem; + border-radius: 0.5rem; + margin-top: 1rem; + gap: 1rem; + } + + .text-editor textarea { + min-height: 100px; + padding: 0.75rem; + font-size: 0.9rem; + } + + .confidence-indicator { + padding: 0.375rem 0.625rem; + font-size: 0.8rem; + } + + #sendExtractedText { + padding: 0.75rem; + font-size: 0.9rem; + } } /* Claude Panel */ @@ -403,10 +504,9 @@ body { flex-direction: column; height: auto; min-height: 300px; - max-height: 90vh; border: 1px solid var(--border-color); transition: all 0.3s ease; - overflow: hidden; + overflow: visible; width: 100%; } @@ -498,6 +598,8 @@ body { overflow-wrap: break-word; margin-top: 1rem; font-size: 1rem; + height: auto; + width: 100%; } [data-theme="dark"] .response-content { @@ -847,7 +949,7 @@ button:disabled { /* Toast Notifications */ .toast-container { position: fixed; - top: 1rem; + top: 4rem; right: 1rem; left: auto; transform: none; @@ -937,233 +1039,6 @@ button:disabled { } } -/* 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; - } -} - -/* History Panel */ -/* Hide specific elements when viewing history items */ -body.history-view .text-editor, -body.history-view .text-format-controls, -body.history-view .confidence-indicator, -body.history-view #confidenceIndicator, -body.history-view #confidenceDisplay, -body.history-view #extractText, -body.history-view #sendExtractedText, -body.history-view .format-toggle, -body.history-view .send-text-group, -body.history-view #textEditor, -body.history-view .analysis-button .button-group { - display: none !important; - visibility: hidden !important; - opacity: 0 !important; - pointer-events: none !important; - position: absolute !important; - z-index: -1 !important; -} - -/* Ensure only image and response are visible in history view */ -body.history-view .image-preview, -body.history-view .claude-panel { - display: block !important; -} - -.history-panel { - position: fixed; - 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; -} - -.history-panel:not(.hidden) { - transform: translateX(0); -} - -.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: 0.5rem; - font-size: 0.875rem; - color: var(--text-secondary); -} - -.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; -} - -/* API Key Groups */ -.api-key-group { - margin-bottom: 1.5rem; - padding: 1rem; - background-color: var(--background); - border-radius: 0.5rem; - border: 1px solid var(--border-color); - transition: all 0.3s ease; -} - -.api-key-group:not([style*="display: none"]) { - animation: fade-in 0.3s ease; -} - -.api-key-group label { - font-weight: 500; - color: var(--text-primary); - margin-bottom: 0.75rem; -} - -.api-key-group .input-group { - margin-bottom: 0; -} - -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(-10px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* Utility Classes */ -.hidden { - display: none !important; -} - -/* Additional Responsive Styles */ -@media (max-width: 768px) { - .history-panel { - width: 100%; - } - - .history-item { - padding: 0.75rem; - } -} - /* Thinking Section */ .thinking-section { margin-bottom: 1rem; @@ -1184,7 +1059,6 @@ body.history-view .claude-panel { } .thinking-header::after { - content: "点击展开/折叠"; position: absolute; right: 60px; color: var(--text-secondary); @@ -1256,9 +1130,9 @@ body.history-view .claude-panel { } .thinking-content.expanded { - max-height: 600px; + max-height: none; padding: 1rem 1.5rem; - overflow-y: auto; + overflow-y: visible; } /* Animation for thinking content */ @@ -1313,41 +1187,14 @@ body.history-view .claude-panel { } [data-theme="dark"] .init-error pre { - background-color: rgba(255, 255, 255, 0.05); + background-color: rgba(var(--surface-rgb), 0.5); } -.history-thumbnail { - width: 100%; - height: 100%; - object-fit: cover; - border-radius: 4px; -} +/* 删除历史缩略图相关样式 */ -.history-thumbnail-placeholder { - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - background-color: #f0f0f0; - border-radius: 4px; - color: #666; - text-align: center; - padding: 10px; - box-sizing: border-box; -} - -.history-thumbnail-placeholder i { - font-size: 24px; - margin-bottom: 5px; - color: #999; -} - -.history-thumbnail-placeholder span { - font-size: 12px; - line-height: 1.2; - display: block; +/* Utility Classes */ +.hidden { + display: none !important; } /* Select Styling */ @@ -1427,3 +1274,17 @@ body.history-view .claude-panel { .response-title h3 { display: none; } + +/* 大屏幕设备的布局 */ +@media (min-width: 769px) { + .text-editor { + max-width: 900px; + margin: 1.5rem auto; + } + + #sendExtractedText { + width: auto; + min-width: 200px; + align-self: flex-end; + } +} diff --git a/templates/index.html b/templates/index.html index 8edc6d0..44a3019 100644 --- a/templates/index.html +++ b/templates/index.html @@ -12,23 +12,18 @@