diff --git a/app.py b/app.py index 2c30244..94793f2 100644 --- a/app.py +++ b/app.py @@ -74,23 +74,72 @@ def stream_model_response(response_generator, sid): try: print("Starting response streaming...") - # Send initial status + # 初始化:发送开始状态 socketio.emit('claude_response', { 'status': 'started', 'content': '' }, room=sid) print("Sent initial status to client") - # Stream responses + # 跟踪已发送的内容 + previous_thinking_content = "" + + # 流式处理响应 for response in response_generator: - # For Mathpix responses, use text_extracted event + # 处理Mathpix响应 if isinstance(response.get('content', ''), str) and 'mathpix' in response.get('model', ''): socketio.emit('text_extracted', { 'content': response['content'] }, room=sid) + continue + + # 获取状态和内容 + status = response.get('status', '') + content = response.get('content', '') + + # 根据不同的状态进行处理 + if status == 'thinking': + # 确保只发送新增的思考内容 + if previous_thinking_content and content.startswith(previous_thinking_content): + # 只发送新增部分 + new_content = content[len(previous_thinking_content):] + if new_content: # 只有当有新内容时才发送 + print(f"Streaming thinking content: {len(new_content)} chars") + socketio.emit('claude_response', { + 'status': 'thinking', + 'content': new_content + }, room=sid) + else: + # 直接发送全部内容(首次或内容不连续的情况) + print(f"Streaming thinking content (reset): {len(content)} chars") + socketio.emit('claude_response', { + 'status': 'thinking', + 'content': content + }, room=sid) + + # 更新已发送的思考内容记录 + previous_thinking_content = content + + elif status == 'streaming': + # 流式输出正常内容 + if content: + print(f"Streaming response content: {len(content)} chars") + socketio.emit('claude_response', { + 'status': 'streaming', + 'content': content + }, room=sid) + else: - # For AI model responses, use claude_response event + # 其他状态直接转发 socketio.emit('claude_response', response, room=sid) + + # 调试信息 + if status == 'thinking_complete': + print(f"Thinking complete, total length: {len(content)} chars") + elif status == 'completed': + print("Response completed") + elif status == 'error': + print(f"Error: {response.get('error', 'Unknown error')}") except Exception as e: error_msg = f"Streaming error: {str(e)}" @@ -182,14 +231,20 @@ def handle_text_extraction(data): }, room=request.sid) @socketio.on('analyze_text') -def handle_text_analysis(data): +def handle_analyze_text(data): try: - print("Starting text analysis...") - text = data['text'] - settings = data['settings'] + text = data.get('text') + settings = data.get('settings', {}) + sid = request.sid + + print("Selected model:", settings.get('model', 'claude-3-7-sonnet-20250219')) + + # Get API key and create model + model_name = settings.get('model', 'claude-3-7-sonnet-20250219') + api_key = settings.get('api_keys', {}).get(model_name) # Validate required settings - if not settings.get('apiKey'): + if not api_key: raise ValueError("API key is required for the selected model") # Configure proxy settings if enabled @@ -205,8 +260,8 @@ def handle_text_analysis(data): try: # Create model instance using factory model = ModelFactory.create_model( - model_name=settings.get('model', 'claude-3-5-sonnet-20241022'), - api_key=settings['apiKey'], + model_name=model_name, + api_key=api_key, temperature=float(settings.get('temperature', 0.7)), system_prompt=settings.get('systemPrompt') ) @@ -214,14 +269,14 @@ def handle_text_analysis(data): # Start streaming in a separate thread Thread( target=stream_model_response, - args=(model.analyze_text(text, proxies), request.sid) + args=(model.analyze_text(text, proxies), sid) ).start() except Exception as e: socketio.emit('claude_response', { 'status': 'error', 'error': f'API error: {str(e)}' - }, room=request.sid) + }, room=sid) except Exception as e: print(f"Analysis error: {str(e)}") @@ -231,19 +286,33 @@ def handle_text_analysis(data): }, room=request.sid) @socketio.on('analyze_image') -def handle_image_analysis(data): +def handle_analyze_image(data): try: - print("Starting image analysis...") - settings = data['settings'] - image_data = data['image'] # Base64 encoded image + # 检查数据是否有效 + if not data or not isinstance(data, dict): + raise ValueError("Invalid request data") + + image_data = data.get('image') + if not image_data: + raise ValueError("No image data provided") + + settings = data.get('settings', {}) + + # 不需要分割了,因为前端已经做了分割 + # _, base64_data = image_data_url.split(',', 1) + base64_data = image_data + + # Get API key and create model + model_name = settings.get('model', 'claude-3-7-sonnet-20250219') + api_key = settings.get('api_keys', {}).get(model_name) # Validate required settings - if not settings.get('apiKey'): - raise ValueError("API key is required for the selected model") + if not api_key: + raise ValueError(f"API key is required for the selected model: {model_name}") # Log with model name for better debugging - print(f"Using API key for {settings.get('model', 'unknown')}: {settings['apiKey'][:6]}...") - print("Selected model:", settings.get('model', 'claude-3-5-sonnet-20241022')) + print(f"Using API key for {model_name}: {api_key[:6]}...") + print("Selected model:", model_name) # Configure proxy settings if enabled proxies = None @@ -258,8 +327,8 @@ def handle_image_analysis(data): try: # Create model instance using factory model = ModelFactory.create_model( - model_name=settings.get('model', 'claude-3-5-sonnet-20241022'), - api_key=settings['apiKey'], + model_name=model_name, + api_key=api_key, temperature=float(settings.get('temperature', 0.7)), system_prompt=settings.get('systemPrompt') ) @@ -267,7 +336,7 @@ def handle_image_analysis(data): # Start streaming in a separate thread Thread( target=stream_model_response, - args=(model.analyze_image(image_data, proxies), request.sid) + args=(model.analyze_image(base64_data, proxies), request.sid) ).start() except Exception as e: diff --git a/models/claude.py b/models/claude.py index 7de5ff2..ad1ebaf 100644 --- a/models/claude.py +++ b/models/claude.py @@ -13,7 +13,7 @@ class ClaudeModel(BaseModel): 5. If there are multiple approaches, explain the most efficient one first""" def get_model_identifier(self) -> str: - return "claude-3-5-sonnet-20241022" + return "claude-3-7-sonnet-20250219" def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]: """Stream Claude's response for text analysis""" @@ -35,9 +35,13 @@ class ClaudeModel(BaseModel): payload = { 'model': self.get_model_identifier(), 'stream': True, - 'max_tokens': 4096, - 'temperature': self.temperature, + 'max_tokens': 8192, + 'temperature': 1, 'system': self.system_prompt, + 'thinking': { + 'type': 'enabled', + 'budget_tokens': 4096 + }, 'messages': [{ 'role': 'user', 'content': [ @@ -69,6 +73,9 @@ class ClaudeModel(BaseModel): yield {"status": "error", "error": error_msg} return + thinking_content = "" + response_buffer = "" + for chunk in response.iter_lines(): if not chunk: continue @@ -82,13 +89,29 @@ class ClaudeModel(BaseModel): data = json.loads(chunk_str) if data.get('type') == 'content_block_delta': - if 'delta' in data and 'text' in data['delta']: - yield { - "status": "streaming", - "content": data['delta']['text'] - } + 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: + yield { + "status": "thinking_complete", + "content": thinking_content + } yield { "status": "completed", "content": "" @@ -132,9 +155,13 @@ class ClaudeModel(BaseModel): payload = { 'model': self.get_model_identifier(), 'stream': True, - 'max_tokens': 4096, - 'temperature': self.temperature, + 'max_tokens': 8192, + 'temperature': 1, 'system': self.system_prompt, + 'thinking': { + 'type': 'enabled', + 'budget_tokens': 4096 + }, 'messages': [{ 'role': 'user', 'content': [ @@ -174,6 +201,9 @@ class ClaudeModel(BaseModel): yield {"status": "error", "error": error_msg} return + thinking_content = "" + response_buffer = "" + for chunk in response.iter_lines(): if not chunk: continue @@ -187,13 +217,29 @@ class ClaudeModel(BaseModel): data = json.loads(chunk_str) if data.get('type') == 'content_block_delta': - if 'delta' in data and 'text' in data['delta']: - yield { - "status": "streaming", - "content": data['delta']['text'] - } + 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: + yield { + "status": "thinking_complete", + "content": thinking_content + } yield { "status": "completed", "content": "" diff --git a/models/factory.py b/models/factory.py index ac5e339..37e56fe 100644 --- a/models/factory.py +++ b/models/factory.py @@ -7,7 +7,7 @@ from .mathpix import MathpixModel class ModelFactory: _models: Dict[str, Type[BaseModel]] = { - 'claude-3-5-sonnet-20241022': ClaudeModel, + 'claude-3-7-sonnet-20250219': ClaudeModel, 'gpt-4o-2024-11-20': GPT4oModel, 'deepseek-reasoner': DeepSeekModel, 'mathpix': MathpixModel diff --git a/static/js/main.js b/static/js/main.js index eea2f86..9057ced 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,38 +1,58 @@ class SnapSolver { constructor() { - this.initializeElements(); - this.initializeState(); - this.setupEventListeners(); - this.initializeConnection(); - this.setupAutoScroll(); - - // Initialize managers + // 初始化managers window.uiManager = new UIManager(); window.settingsManager = new SettingsManager(); + + // 初始化应用组件 + this.initializeElements(); + this.initializeState(); + this.initializeConnection(); + this.setupSocketEventHandlers(); + this.setupAutoScroll(); + this.setupEventListeners(); + + // 初始化历史 + window.app = this; // 便于从其他地方访问 + this.updateHistoryPanel(); } initializeElements() { - // Capture elements - this.captureBtn = document.getElementById('captureBtn'); - this.cropBtn = document.getElementById('cropBtn'); - this.connectionStatus = document.getElementById('connectionStatus'); + // Main elements this.screenshotImg = document.getElementById('screenshotImg'); - this.cropContainer = document.getElementById('cropContainer'); this.imagePreview = document.getElementById('imagePreview'); + this.cropBtn = document.getElementById('cropBtn'); + this.captureBtn = document.getElementById('captureBtn'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); this.extractTextBtn = document.getElementById('extractText'); this.textEditor = document.getElementById('textEditor'); this.extractedText = document.getElementById('extractedText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); - this.responseContent = document.getElementById('responseContent'); + this.manualTextInput = document.getElementById('manualTextInput'); this.claudePanel = document.getElementById('claudePanel'); + this.responseContent = document.getElementById('responseContent'); + this.thinkingSection = document.getElementById('thinkingSection'); + this.thinkingContent = document.getElementById('thinkingContent'); + this.thinkingToggle = document.getElementById('thinkingToggle'); + this.connectionStatus = document.getElementById('connectionStatus'); this.statusLight = document.querySelector('.status-light'); + // Crop elements + this.cropContainer = document.getElementById('cropContainer'); + this.cropCancel = document.getElementById('cropCancel'); + this.cropConfirm = document.getElementById('cropConfirm'); + // Format toggle elements this.textFormatBtn = document.getElementById('textFormatBtn'); this.latexFormatBtn = document.getElementById('latexFormatBtn'); 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() { @@ -45,6 +65,21 @@ class SnapSolver { text: '', latex: '' }; + + // 新增:流式输出的内存缓冲区 + this.responseBuffer = ""; + this.thinkingBuffer = ""; + + // 确保裁剪容器和其他面板初始为隐藏状态 + if (this.cropContainer) { + this.cropContainer.classList.add('hidden'); + } + if (this.claudePanel) { + this.claudePanel.classList.add('hidden'); + } + if (this.thinkingSection) { + this.thinkingSection.classList.add('hidden'); + } } setupAutoScroll() { @@ -201,15 +236,61 @@ class SnapSolver { switch (data.status) { case 'started': console.log('Analysis started'); - this.responseContent.textContent = ''; + // 重置内存缓冲区 + this.responseBuffer = ""; + this.thinkingBuffer = ""; + // 清空显示内容 + this.responseContent.innerHTML = ''; + this.thinkingContent.innerHTML = ''; + this.thinkingSection.classList.add('hidden'); this.sendToClaudeBtn.disabled = true; this.sendExtractedTextBtn.disabled = true; break; + case 'thinking': + // 处理思考内容 + if (data.content) { + console.log('Received thinking:', data.content); + this.thinkingSection.classList.remove('hidden'); + + // 添加到内存缓冲区 + this.thinkingBuffer += data.content; + + // 安全地更新DOM + this.updateElementContent(this.thinkingContent, this.thinkingBuffer); + + // 添加打字动画效果 + this.thinkingContent.classList.add('thinking-typing'); + } + break; + + case 'thinking_complete': + // 完整的思考内容 + if (data.content) { + console.log('Thinking complete'); + this.thinkingSection.classList.remove('hidden'); + + // 重置内存缓冲区并更新 + this.thinkingBuffer = data.content; + this.updateElementContent(this.thinkingContent, this.thinkingBuffer); + + // 移除打字动画 + this.thinkingContent.classList.remove('thinking-typing'); + } + break; + case 'streaming': if (data.content) { console.log('Received content:', data.content); - this.responseContent.textContent += data.content; + + // 添加到内存缓冲区 + this.responseBuffer += data.content; + + // 安全地更新DOM + this.updateElementContent(this.responseContent, this.responseBuffer); + + // 移除思考部分的打字动画 + this.thinkingContent.classList.remove('thinking-typing'); } break; @@ -217,14 +298,17 @@ class SnapSolver { console.log('Analysis completed'); this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; - this.addToHistory(this.croppedImage, this.responseContent.textContent); + this.addToHistory(this.croppedImage, this.responseBuffer, this.thinkingBuffer); window.showToast('Analysis completed successfully'); break; case 'error': console.error('Analysis error:', data.error); const errorMessage = data.error || 'Unknown error occurred'; - this.responseContent.textContent += '\nError: ' + errorMessage; + // 错误信息添加到缓冲区 + this.responseBuffer += '\nError: ' + errorMessage; + // 更新DOM + this.updateElementContent(this.responseContent, this.responseBuffer); this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; window.showToast('Analysis failed: ' + errorMessage, 'error'); @@ -233,7 +317,10 @@ class SnapSolver { default: console.warn('Unknown response status:', data.status); if (data.error) { - this.responseContent.textContent += '\nError: ' + data.error; + // 错误信息添加到缓冲区 + this.responseBuffer += '\nError: ' + data.error; + // 更新DOM + this.updateElementContent(this.responseContent, this.responseBuffer); this.sendToClaudeBtn.disabled = false; this.sendExtractedTextBtn.disabled = false; window.showToast('Unknown error occurred', 'error'); @@ -249,8 +336,38 @@ class SnapSolver { }); } + // 新增:安全更新DOM内容的辅助方法 + updateElementContent(element, content) { + if (!element) return; + + // 创建文档片段以提高性能 + const fragment = document.createDocumentFragment(); + const tempDiv = document.createElement('div'); + + // 设置内容 + tempDiv.textContent = content; + + // 将所有子节点移动到文档片段 + while (tempDiv.firstChild) { + fragment.appendChild(tempDiv.firstChild); + } + + // 清空目标元素并添加文档片段 + element.innerHTML = ''; + element.appendChild(fragment); + + // 自动滚动到底部 + element.scrollTop = element.scrollHeight; + } + initializeCropper() { try { + // 如果当前没有截图,不要初始化裁剪器 + if (!this.screenshotImg || !this.screenshotImg.src || this.screenshotImg.src === '') { + console.log('No screenshot to crop'); + return; + } + // Clean up existing cropper instance if (this.cropper) { this.cropper.destroy(); @@ -258,6 +375,11 @@ class SnapSolver { } const cropArea = document.querySelector('.crop-area'); + if (!cropArea) { + console.error('Crop area element not found'); + return; + } + cropArea.innerHTML = ''; const clonedImage = this.screenshotImg.cloneNode(true); clonedImage.style.display = 'block'; @@ -294,28 +416,185 @@ class SnapSolver { } catch (error) { console.error('Error initializing cropper:', error); window.showToast('Failed to initialize cropper', 'error'); + + // 确保在出错时关闭裁剪界面 + if (this.cropContainer) { + this.cropContainer.classList.add('hidden'); + } } } - addToHistory(imageData, response) { - const historyItem = { - id: Date.now(), - timestamp: new Date().toISOString(), - image: imageData, - response: response - }; - this.history.unshift(historyItem); - if (this.history.length > 10) this.history.pop(); - localStorage.setItem('snapHistory', JSON.stringify(this.history)); - window.renderHistory(); + 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() { + 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 = ` +
无历史记录
+No history yet
-${error.message}
+${error.stack}
`;
- return;
+ document.body.appendChild(errorDiv);
}
-
- content.innerHTML = history.map(item => `
-