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 = ` +
+ +

无历史记录

+
+ `; + return; + } + + 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() { + this.setupFormatToggle(); this.setupCaptureEvents(); this.setupCropEvents(); this.setupAnalysisEvents(); this.setupKeyboardShortcuts(); - this.setupFormatToggle(); + this.setupThinkingToggle(); } setupFormatToggle() { @@ -516,13 +795,13 @@ class SnapSolver { } const settings = window.settingsManager.getSettings(); - const apiKey = window.settingsManager.getApiKey(); + const apiKeys = {}; + Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => { + if (input.value) { + apiKeys[model] = input.value; + } + }); - if (!apiKey) { - this.settingsPanel.classList.remove('hidden'); - return; - } - this.claudePanel.classList.remove('hidden'); this.responseContent.textContent = ''; this.sendExtractedTextBtn.disabled = true; @@ -531,13 +810,9 @@ class SnapSolver { this.socket.emit('analyze_text', { text: text, settings: { - apiKey: apiKey, - model: settings.model || 'claude-3-5-sonnet-20241022', - temperature: parseFloat(settings.temperature) || 0.7, - systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.', - proxyEnabled: settings.proxyEnabled || false, - proxyHost: settings.proxyHost || '127.0.0.1', - proxyPort: settings.proxyPort || '4780' + ...settings, + api_keys: apiKeys, + model: settings.model || 'claude-3-7-sonnet-20250219', } }); } catch (error) { @@ -555,13 +830,13 @@ class SnapSolver { } const settings = window.settingsManager.getSettings(); - const apiKey = window.settingsManager.getApiKey(); + const apiKeys = {}; + Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => { + if (input.value) { + apiKeys[model] = input.value; + } + }); - if (!apiKey) { - this.settingsPanel.classList.remove('hidden'); - return; - } - this.claudePanel.classList.remove('hidden'); this.responseContent.textContent = ''; this.sendToClaudeBtn.disabled = true; @@ -570,13 +845,9 @@ class SnapSolver { this.socket.emit('analyze_image', { image: this.croppedImage.split(',')[1], settings: { - apiKey: apiKey, - model: settings.model || 'claude-3-5-sonnet-20241022', - temperature: parseFloat(settings.temperature) || 0.7, - systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.', - proxyEnabled: settings.proxyEnabled || false, - proxyHost: settings.proxyHost || '127.0.0.1', - proxyPort: settings.proxyPort || '4780' + ...settings, + api_keys: apiKeys, + model: settings.model || 'claude-3-7-sonnet-20250219', } }); } catch (error) { @@ -602,68 +873,58 @@ 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'); + } + } + }); + } + } + + // 获取用于显示的图像URL,如果原始URL无效则返回占位符 + getImageForDisplay(imageUrl) { + return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl(); + } } // Initialize the application when the DOM is loaded document.addEventListener('DOMContentLoaded', () => { - window.app = new SnapSolver(); -}); - -// Global function for history rendering -window.renderHistory = function() { - const content = document.querySelector('.history-content'); - const history = JSON.parse(localStorage.getItem('snapHistory') || '[]'); - - if (history.length === 0) { - content.innerHTML = ` -
- -

No history yet

-
+ try { + console.log('Initializing application...'); + window.app = new SnapSolver(); + console.log('Application initialized successfully'); + } catch (error) { + console.error('Failed to initialize application:', error); + // 在页面上显示错误信息 + const errorDiv = document.createElement('div'); + errorDiv.className = 'init-error'; + errorDiv.innerHTML = ` +

Initialization Error

+

${error.message}

+
${error.stack}
`; - return; + document.body.appendChild(errorDiv); } - - content.innerHTML = history.map(item => ` -
-
- ${new Date(item.timestamp).toLocaleString()} - -
- Historical screenshot -
- `).join(''); - - // Add click handlers for history items - content.querySelectorAll('.delete-history').forEach(btn => { - btn.addEventListener('click', (e) => { - e.stopPropagation(); - const id = parseInt(btn.dataset.id); - const updatedHistory = history.filter(item => item.id !== id); - localStorage.setItem('snapHistory', JSON.stringify(updatedHistory)); - window.renderHistory(); - window.showToast('History item deleted'); - }); - }); - - content.querySelectorAll('.history-item').forEach(item => { - item.addEventListener('click', () => { - const historyItem = history.find(h => h.id === parseInt(item.dataset.id)); - if (historyItem) { - window.app.screenshotImg.src = historyItem.image; - window.app.imagePreview.classList.remove('hidden'); - document.getElementById('historyPanel').classList.add('hidden'); - window.app.cropBtn.classList.add('hidden'); - window.app.captureBtn.classList.add('hidden'); - window.app.sendToClaudeBtn.classList.add('hidden'); - window.app.extractTextBtn.classList.add('hidden'); - if (historyItem.response) { - window.app.claudePanel.classList.remove('hidden'); - window.app.responseContent.textContent = historyItem.response; - } - } - }); - }); -}; +}); diff --git a/static/js/settings.js b/static/js/settings.js index bec6f46..35db9bc 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -24,7 +24,7 @@ class SettingsManager { // API Key elements this.apiKeyInputs = { - 'claude-3-5-sonnet-20241022': document.getElementById('claudeApiKey'), + 'claude-3-7-sonnet-20250219': document.getElementById('claudeApiKey'), 'gpt-4o-2024-11-20': document.getElementById('gpt4oApiKey'), 'deepseek-reasoner': document.getElementById('deepseekApiKey') }; diff --git a/static/js/ui.js b/static/js/ui.js index bd75366..8959428 100644 --- a/static/js/ui.js +++ b/static/js/ui.js @@ -77,7 +77,9 @@ class UIManager { this.historyToggle.addEventListener('click', () => { this.closeAllPanels(); this.historyPanel.classList.toggle('hidden'); - window.renderHistory(); // Call global renderHistory function + if (window.app && typeof window.app.updateHistoryPanel === 'function') { + window.app.updateHistoryPanel(); + } }); this.closeHistory.addEventListener('click', () => { @@ -111,7 +113,9 @@ class UIManager { break; case 'h': this.historyPanel.classList.toggle('hidden'); - window.renderHistory(); + if (window.app && typeof window.app.updateHistoryPanel === 'function') { + window.app.updateHistoryPanel(); + } break; } } else if (e.key === 'Escape') { diff --git a/static/style.css b/static/style.css index a675f9e..7fa580a 100644 --- a/static/style.css +++ b/static/style.css @@ -12,6 +12,21 @@ --shadow-color: rgba(0, 0, 0, 0.1); --error-color: #f44336; --success-color: #4CAF50; + --primary: #4a6cf7; + --secondary: #6c757d; + --danger: #f44336; + --success: #4caf50; + --background: #f5f7fb; + --surface: #ffffff; + --surface-alt: #f0f4f8; + --text: #2c3e50; + --text-secondary: #505a66; + --shadow-color: rgba(0, 0, 0, 0.1); + --border-color: #e0e0e0; + --hover-color: #e9ecef; + --accent: #4a6cf7; + --placeholder: #a0a0a0; + --disabled: #e6e6e6; } [data-theme="dark"] { @@ -25,6 +40,21 @@ --text-secondary: #B0B0B0; --border-color: #333333; --shadow-color: rgba(0, 0, 0, 0.3); + --primary: #4a6cf7; + --secondary: #6c757d; + --danger: #f44336; + --success: #4caf50; + --background: #1a1a2e; + --surface: #272741; + --surface-alt: #202035; + --text: #f0f0f0; + --text-secondary: #a0a0a0; + --shadow-color: rgba(0, 0, 0, 0.4); + --border-color: #353545; + --hover-color: #32324b; + --accent: #4a6cf7; + --placeholder: #515151; + --disabled: #3a3a3a; } /* Base Styles */ @@ -887,3 +917,193 @@ body.history-view .claude-panel { .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); +} + +.thinking-header { + 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; +} + +.thinking-header:hover { + background-color: var(--hover-color); +} + +.thinking-title { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.thinking-title i { + color: var(--accent); + font-size: 1.1rem; +} + +.thinking-title h3 { + margin: 0; + font-size: 1rem; + font-weight: 600; + color: var(--text); +} + +.toggle-btn { + background: transparent; + border: none; + color: var(--text-secondary); + cursor: pointer; + transition: transform 0.3s ease; + padding: 0.3rem; + border-radius: 50%; +} + +.toggle-btn:hover { + background-color: var(--hover-color); +} + +.thinking-content { + padding: 0; + max-height: 500px; + overflow-y: auto; + transition: max-height 0.3s ease, padding 0.3s ease; + overflow-x: hidden; + white-space: pre-wrap; + font-family: monospace; + 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; +} + +.thinking-content.expanded { + padding: 1rem; + overflow-y: auto; + border-top: 1px solid var(--border-color); +} + +.thinking-toggle-active .toggle-btn i { + transform: rotate(180deg); +} + +/* Animation for thinking content */ +@keyframes thinking-typing { + from { opacity: 0.4; } + to { opacity: 1; } +} + +.thinking-typing { + animation: thinking-typing 0.5s infinite alternate; +} + +/* 初始化错误样式 */ +.init-error { + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: var(--surface); + border: 2px solid var(--danger); + border-radius: 8px; + padding: 1.5rem; + max-width: 80%; + max-height: 80%; + overflow: auto; + z-index: 9999; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); +} + +.init-error h2 { + color: var(--danger); + margin-top: 0; + margin-bottom: 1rem; + font-size: 1.5rem; +} + +.init-error p { + margin-bottom: 1rem; + font-size: 1rem; + line-height: 1.5; +} + +.init-error pre { + background-color: rgba(0, 0, 0, 0.05); + padding: 1rem; + border-radius: 4px; + font-family: monospace; + font-size: 0.9rem; + overflow: auto; + white-space: pre-wrap; + max-height: 300px; +} + +[data-theme="dark"] .init-error pre { + background-color: rgba(255, 255, 255, 0.05); +} + +.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; +} diff --git a/templates/index.html b/templates/index.html index c66775a..121afe4 100644 --- a/templates/index.html +++ b/templates/index.html @@ -98,6 +98,18 @@ +
@@ -134,7 +146,7 @@

AI Configuration

-
+
@@ -168,7 +180,7 @@