diff --git a/static/js/main.js b/static/js/main.js index fbb6502..440bdef 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -8,9 +8,34 @@ class SnapSolver { this.originalImage = null; this.croppedImage = null; this.extractedContent = ''; - this.emitTimeout = null; this.eventsSetup = false; + // Cache DOM elements + this.captureBtn = document.getElementById('captureBtn'); + // 移除裁剪按钮引用 + this.screenshotImg = document.getElementById('screenshotImg'); + this.imagePreview = document.getElementById('imagePreview'); + this.emptyState = document.getElementById('emptyState'); + this.extractedText = document.getElementById('extractedText'); + this.cropContainer = document.getElementById('cropContainer'); + this.sendToClaudeBtn = document.getElementById('sendToClaude'); + this.extractTextBtn = document.getElementById('extractText'); + this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); + this.claudePanel = document.getElementById('claudePanel'); + this.responseContent = document.getElementById('responseContent'); + this.thinkingContent = document.getElementById('thinkingContent'); + this.thinkingSection = document.getElementById('thinkingSection'); + this.thinkingToggle = document.getElementById('thinkingToggle'); + this.connectionStatus = document.getElementById('connectionStatus'); + this.statusLight = document.querySelector('.status-light'); + this.progressLine = document.querySelector('.progress-line'); + this.statusText = document.querySelector('.status-text'); + this.analysisIndicator = document.querySelector('.analysis-indicator'); + + // Crop elements + this.cropCancel = document.getElementById('cropCancel'); + this.cropConfirm = document.getElementById('cropConfirm'); + // 初始化应用 this.initialize(); } @@ -20,11 +45,10 @@ class SnapSolver { this.screenshotImg = document.getElementById('screenshotImg'); this.imagePreview = document.getElementById('imagePreview'); this.emptyState = document.getElementById('emptyState'); - this.cropBtn = document.getElementById('cropBtn'); + // 移除对裁剪按钮的引用 this.captureBtn = document.getElementById('captureBtn'); this.sendToClaudeBtn = document.getElementById('sendToClaude'); this.extractTextBtn = document.getElementById('extractText'); - this.textEditor = document.getElementById('textEditor'); this.extractedText = document.getElementById('extractedText'); this.sendExtractedTextBtn = document.getElementById('sendExtractedText'); this.cropContainer = document.getElementById('cropContainer'); @@ -36,6 +60,9 @@ class SnapSolver { this.thinkingToggle = document.getElementById('thinkingToggle'); this.connectionStatus = document.getElementById('connectionStatus'); this.statusLight = document.querySelector('.status-light'); + this.progressLine = document.querySelector('.progress-line'); + this.statusText = document.querySelector('.status-text'); + this.analysisIndicator = document.querySelector('.analysis-indicator'); // Crop elements this.cropCancel = document.getElementById('cropCancel'); @@ -47,7 +74,6 @@ class SnapSolver { this.cropper = null; this.croppedImage = null; this.extractedContent = ''; - this.emitTimeout = null; // 确保裁剪容器和其他面板初始为隐藏状态 if (this.cropContainer) { @@ -97,21 +123,51 @@ class SnapSolver { } updateStatusLight(status) { - this.statusLight.className = 'status-light'; - switch (status) { - case 'started': - case 'streaming': - this.statusLight.classList.add('processing'); - break; - case 'completed': - this.statusLight.classList.add('completed'); - break; - case 'error': - this.statusLight.classList.add('error'); - break; - default: - // Reset to default state - break; + // 更新状态指示器 + if (this.analysisIndicator) { + this.analysisIndicator.className = 'analysis-indicator'; + this.progressLine.className = 'progress-line'; + + switch (status) { + case 'started': + case 'streaming': + this.analysisIndicator.classList.add('processing'); + this.progressLine.classList.add('processing'); + this.statusText.textContent = '处理中...'; + break; + case 'completed': + this.analysisIndicator.classList.add('completed'); + this.progressLine.classList.add('completed'); + this.statusText.textContent = '已完成'; + break; + case 'error': + this.analysisIndicator.classList.add('error'); + this.progressLine.classList.add('error'); + this.statusText.textContent = '错误'; + break; + default: + // 重置为默认状态 + this.statusText.textContent = '准备中'; + break; + } + } else if (this.statusLight) { + // 兼容旧版状态灯 + this.statusLight.className = 'status-light'; + switch (status) { + case 'started': + case 'streaming': + this.statusLight.classList.add('processing'); + break; + case 'completed': + this.statusLight.classList.add('completed'); + break; + case 'error': + this.statusLight.classList.add('error'); + break; + default: + // Reset to default state + break; + } } } @@ -176,7 +232,8 @@ class SnapSolver { } catch (error) { console.error('Connection error:', error); this.updateConnectionStatus('重连失败', false); - setTimeout(() => this.initializeConnection(), 5000); + // 移除setTimeout,让用户手动刷新页面重连 + window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error'); } } @@ -206,7 +263,7 @@ class SnapSolver { // 恢复按钮状态 this.captureBtn.disabled = false; - this.captureBtn.innerHTML = '截图'; + this.captureBtn.innerHTML = ''; // 初始化裁剪器 this.initializeCropper(); @@ -214,7 +271,7 @@ class SnapSolver { window.uiManager.showToast('截图成功', 'success'); } else { this.captureBtn.disabled = false; - this.captureBtn.innerHTML = '截图'; + this.captureBtn.innerHTML = ''; console.error('截图失败:', data.error); window.uiManager.showToast('截图失败: ' + data.error, 'error'); } @@ -223,7 +280,7 @@ class SnapSolver { // 新版截图响应处理器 this.socket.on('screenshot_complete', (data) => { this.captureBtn.disabled = false; - this.captureBtn.innerHTML = '截图'; + this.captureBtn.innerHTML = ''; if (data.success) { // 显示截图预览 @@ -257,21 +314,17 @@ class SnapSolver { this.extractTextBtn.disabled = false; this.extractTextBtn.innerHTML = '提取文本'; - if (this.extractedText) { - this.extractedText.disabled = false; - } - - if (this.emitTimeout) { - clearTimeout(this.emitTimeout); - this.emitTimeout = null; + if (this.extractedText) { + this.extractedText.disabled = false; } // 检查是否有内容数据 if (data.content) { - this.extractedText.value = data.content; + this.extractedText.value = data.content; this.extractedContent = data.content; - this.textEditor.classList.remove('hidden'); - this.sendExtractedTextBtn.disabled = false; + this.extractedText.classList.remove('hidden'); + this.sendExtractedTextBtn.classList.remove('hidden'); + this.sendExtractedTextBtn.disabled = false; window.uiManager.showToast('文本提取成功', 'success'); } else if (data.error) { @@ -322,35 +375,23 @@ class SnapSolver { console.log('Received thinking content'); this.thinkingSection.classList.remove('hidden'); - // 记住用户的展开/折叠状态 - const wasExpanded = this.thinkingContent.classList.contains('expanded'); + // 显示动态省略号 + this.showThinkingAnimation(true); - // 直接设置完整内容而不是追加 + // 直接设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); - // 根据之前的状态决定是否展开 - if (wasExpanded) { - this.thinkingContent.classList.add('expanded'); - this.thinkingContent.classList.remove('collapsed'); - - // 更新切换按钮图标 - const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); - if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-up'; - } - } else { - // 初始状态为折叠 - this.thinkingContent.classList.add('collapsed'); - this.thinkingContent.classList.remove('expanded'); - - // 更新切换按钮图标 - const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); - if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; - } + // 默认为折叠状态 + this.thinkingContent.classList.add('collapsed'); + this.thinkingContent.classList.remove('expanded'); + + // 更新切换按钮图标 + const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; } } break; @@ -361,35 +402,23 @@ class SnapSolver { console.log('Received reasoning content'); this.thinkingSection.classList.remove('hidden'); - // 记住用户的展开/折叠状态 - const wasExpanded = this.thinkingContent.classList.contains('expanded'); + // 显示动态省略号 + this.showThinkingAnimation(true); - // 直接设置完整内容而不是追加 + // 直接设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 添加打字动画效果 this.thinkingContent.classList.add('thinking-typing'); - // 根据之前的状态决定是否展开 - if (wasExpanded) { - this.thinkingContent.classList.add('expanded'); - this.thinkingContent.classList.remove('collapsed'); - - // 更新切换按钮图标 - const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); - if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-up'; - } - } else { - // 初始状态为折叠 - this.thinkingContent.classList.add('collapsed'); - this.thinkingContent.classList.remove('expanded'); - - // 更新切换按钮图标 - const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); - if (toggleIcon) { - toggleIcon.className = 'fas fa-chevron-down'; - } + // 默认为折叠状态 + this.thinkingContent.classList.add('collapsed'); + this.thinkingContent.classList.remove('expanded'); + + // 更新切换按钮图标 + const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; } } break; @@ -397,14 +426,23 @@ class SnapSolver { case 'thinking_complete': // 完整的思考内容 if (data.content && this.thinkingContent && this.thinkingSection) { - console.log('Thinking complete'); + console.log('思考过程完成'); this.thinkingSection.classList.remove('hidden'); + // 停止动态省略号 + this.showThinkingAnimation(false); + // 设置完整内容 this.setElementContent(this.thinkingContent, data.content); // 移除打字动画 this.thinkingContent.classList.remove('thinking-typing'); + + // 确保图标正确显示 + const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); + if (toggleIcon) { + toggleIcon.className = 'fas fa-chevron-down'; + } } break; @@ -414,6 +452,9 @@ class SnapSolver { console.log('Reasoning complete'); this.thinkingSection.classList.remove('hidden'); + // 停止动态省略号 + this.showThinkingAnimation(false); + // 设置完整内容 this.setElementContent(this.thinkingContent, data.content); @@ -430,6 +471,9 @@ class SnapSolver { this.setElementContent(this.responseContent, data.content); this.responseContent.style.display = 'block'; + // 停止省略号动画 + this.showThinkingAnimation(false); + // 移除思考部分的打字动画 if (this.thinkingContent) { this.thinkingContent.classList.remove('thinking-typing'); @@ -465,8 +509,8 @@ class SnapSolver { toggleBtn.className = 'fas fa-chevron-down'; } - // 添加明确的提示 - window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success'); + // 简化提示信息 + window.uiManager.showToast('分析完成', 'success'); } else { // 没有思考内容,隐藏思考组件 this.thinkingSection.classList.add('hidden'); @@ -529,7 +573,12 @@ class SnapSolver { // 显示思考区域 this.thinkingSection.classList.remove('hidden'); - this.thinkingContent.textContent = data.thinking; + + // 显示动态省略号 + this.showThinkingAnimation(true); + + // 使用setElementContent方法处理Markdown + this.setElementContent(this.thinkingContent, data.thinking); // 记住用户的展开/折叠状态 const wasExpanded = this.thinkingContent.classList.contains('expanded'); @@ -544,21 +593,22 @@ class SnapSolver { } }); - // 思考过程完成 + // 思考过程完成 - Socket事件处理 this.socket.on('thinking_complete', (data) => { - console.log('思考过程完成'); + console.log('Socket接收到思考过程完成'); this.thinkingSection.classList.remove('hidden'); - this.thinkingContent.textContent = data.thinking; + + // 停止动态省略号动画 + this.showThinkingAnimation(false); + + // 使用setElementContent方法处理Markdown + this.setElementContent(this.thinkingContent, data.thinking); // 确保图标正确显示 const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i'); if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-down'; } - - // 添加提示信息 - const toast = window.uiManager.showToast('思考过程已完成,点击标题可查看详细思考过程', 'success'); - toast.style.zIndex = '1000'; }); // 分析完成 @@ -569,19 +619,19 @@ class SnapSolver { // 显示分析结果 if (this.responseContent) { - this.responseContent.innerHTML = data.response; + // 使用setElementContent方法处理Markdown + this.setElementContent(this.responseContent, data.response); this.responseContent.style.display = 'block'; - // 滚动到结果区域 - setTimeout(() => { - this.responseContent.scrollIntoView({ behavior: 'smooth' }); - }, 200); + // 直接滚动到结果区域,不使用setTimeout + this.responseContent.scrollIntoView({ behavior: 'smooth' }); } // 确保思考部分完全显示(如果有的话) if (data.thinking && this.thinkingSection && this.thinkingContent) { this.thinkingSection.classList.remove('hidden'); - this.thinkingContent.textContent = data.thinking; + // 使用setElementContent方法处理Markdown + this.setElementContent(this.thinkingContent, data.thinking); // 确保初始状态为折叠 this.thinkingContent.classList.remove('expanded'); @@ -592,7 +642,7 @@ class SnapSolver { } // 弹出提示 - window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success'); + window.uiManager.showToast('分析完成', 'success'); } }); } @@ -601,8 +651,31 @@ class SnapSolver { setElementContent(element, content) { if (!element) return; - // 直接设置内容 - element.textContent = content; + try { + // 检查marked是否已配置 + if (typeof marked === 'undefined') { + console.warn('Marked库未加载,回退到纯文本显示'); + element.textContent = content; + return; + } + + // 使用marked库解析Markdown内容 + const renderedHtml = marked.parse(content); + + // 设置解析后的HTML内容 + element.innerHTML = renderedHtml; + + // 为未高亮的代码块应用语法高亮 + if (window.hljs) { + element.querySelectorAll('pre code:not(.hljs)').forEach((block) => { + hljs.highlightElement(block); + }); + } + } catch (error) { + console.error('Markdown解析错误:', error); + // 发生错误时回退到纯文本显示 + element.textContent = content; + } // 自动滚动到底部 element.scrollTop = element.scrollHeight; @@ -678,26 +751,19 @@ class SnapSolver { try { this.captureBtn.disabled = true; // 禁用按钮防止重复点击 - this.captureBtn.innerHTML = '正在截图...'; + this.captureBtn.innerHTML = ''; this.socket.emit('capture_screenshot', {}); } catch (error) { console.error('Error starting capture:', error); window.uiManager.showToast('启动截图失败', 'error'); this.captureBtn.disabled = false; - this.captureBtn.innerHTML = '截取屏幕'; + this.captureBtn.innerHTML = ''; } }); } setupCropEvents() { - // Crop button - this.cropBtn.addEventListener('click', () => { - if (!this.checkConnectionBeforeAction()) return; - - if (this.screenshotImg.src) { - this.initializeCropper(); - } - }); + // 移除裁剪按钮的点击事件监听 // Crop confirm button document.getElementById('cropConfirm').addEventListener('click', () => { @@ -762,12 +828,11 @@ class SnapSolver { // Update the screenshot image with the cropped version this.screenshotImg.src = this.croppedImage; this.imagePreview.classList.remove('hidden'); - this.cropBtn.classList.remove('hidden'); // 根据当前选择的模型类型决定显示哪些按钮 this.updateImageActionButtons(); - window.uiManager.showToast('Cropping successful'); + window.uiManager.showToast('裁剪成功'); } catch (error) { console.error('Cropping error details:', { message: error.message, @@ -823,23 +888,16 @@ class SnapSolver { return; } - // Show text editor and prepare UI - this.textEditor.classList.remove('hidden'); + // 显示文本框和按钮 + this.extractedText.classList.remove('hidden'); + this.sendExtractedTextBtn.classList.remove('hidden'); + if (this.extractedText) { this.extractedText.value = '正在提取文本...'; this.extractedText.disabled = true; } try { - // 设置超时时间(15秒)以避免长时间无响应 - this.emitTimeout = setTimeout(() => { - window.uiManager.showToast('文本提取超时,请重试或手动输入文本', 'error'); - this.extractTextBtn.disabled = false; - this.extractTextBtn.innerHTML = '提取文本'; - this.extractedText.disabled = false; - this.sendExtractedTextBtn.disabled = false; - }, 15000); - this.socket.emit('extract_text', { image: this.croppedImage.split(',')[1], settings: { @@ -983,22 +1041,6 @@ class SnapSolver { } } - setupKeyboardShortcuts() { - // Keyboard shortcuts for capture and crop - document.addEventListener('keydown', (e) => { - if (e.ctrlKey || e.metaKey) { - switch(e.key) { - case 'c': - if (!this.captureBtn.disabled) this.captureBtn.click(); - break; - case 'x': - if (!this.cropBtn.disabled) this.cropBtn.click(); - break; - } - } - }); - } - setupThinkingToggle() { // 确保正确获取DOM元素 const thinkingSection = document.getElementById('thinkingSection'); @@ -1010,6 +1052,9 @@ class SnapSolver { return; } + // 初始化时隐藏动态省略号 + this.showThinkingAnimation(false); + // 存储DOM引用 this.thinkingSection = thinkingSection; this.thinkingToggle = thinkingToggle; @@ -1034,7 +1079,6 @@ class SnapSolver { if (toggleIcon) { toggleIcon.className = 'fas fa-chevron-down'; } - window.uiManager.showToast('思考内容已折叠', 'info'); } else { console.log('展开思考内容'); // 添加展开状态 @@ -1043,9 +1087,14 @@ class SnapSolver { toggleIcon.className = 'fas fa-chevron-up'; } - // 移除自动滚动到底部的代码,让用户自行控制滚动位置 - - window.uiManager.showToast('思考内容已展开', 'info'); + // 当展开思考内容时,确保代码高亮生效 + if (window.hljs) { + setTimeout(() => { + thinkingContent.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + }); + }, 100); // 添加一点延迟,确保DOM已完全更新 + } } }; @@ -1109,6 +1158,9 @@ class SnapSolver { window.settingsManager = new SettingsManager(); window.app = this; // 便于从其他地方访问 + // 初始化Markdown工具 + this.initializeMarkdownTools(); + // 建立与服务器的连接 this.connectToServer(); @@ -1140,20 +1192,74 @@ class SnapSolver { } }); + // 监听页面卸载事件,清除所有计时器 + window.addEventListener('beforeunload', this.cleanup.bind(this)); + // 设置默认UI状态 this.enableInterface(); // 更新图像操作按钮 this.updateImageActionButtons(); - // 确保DOM完全加载后再次更新按钮状态(以防设置未完全加载) - setTimeout(() => { - this.updateImageActionButtons(); - console.log('延时更新图像操作按钮完成'); - }, 1000); - console.log('SnapSolver initialization complete'); } + + // 初始化Markdown工具 + initializeMarkdownTools() { + // 检查marked是否可用 + if (typeof marked === 'undefined') { + console.warn('Marked.js 未加载,Markdown渲染将不可用'); + return; + } + + // 初始化marked设置 + marked.setOptions({ + gfm: true, // 启用GitHub风格的Markdown + breaks: true, // 将换行符转换为
+ pedantic: false, // 不使用原始markdown规范 + sanitize: false, // 不要过滤HTML标签,允许一些HTML + smartLists: true, // 使用比原生markdown更智能的列表行为 + smartypants: false, // 不要使用更智能的标点符号 + xhtml: false, // 不使用自闭合标签 + highlight: function(code, lang) { + // 如果highlight.js不可用,直接返回代码 + if (typeof hljs === 'undefined') { + return code; + } + + // 如果指定了语言且hljs支持 + if (lang && hljs.getLanguage(lang)) { + try { + return hljs.highlight(code, { language: lang }).value; + } catch (err) { + console.error('代码高亮错误:', err); + } + } + + // 尝试自动检测语言 + try { + return hljs.highlightAuto(code).value; + } catch (err) { + console.error('自动语言检测错误:', err); + } + + return code; // 使用默认编码效果 + } + }); + + // 检查highlight.js是否可用 + if (typeof hljs === 'undefined') { + console.warn('Highlight.js 未加载,代码高亮将不可用'); + return; + } + + // 配置hljs以支持自动语言检测 + hljs.configure({ + languages: ['javascript', 'python', 'java', 'cpp', 'csharp', 'html', 'css', 'json', 'xml', 'markdown', 'bash'] + }); + + console.log('Markdown工具初始化完成'); + } handleResize() { // 如果裁剪器存在,需要调整其大小和位置 @@ -1168,7 +1274,7 @@ class SnapSolver { // 启用主要界面元素 if (this.captureBtn) { this.captureBtn.disabled = false; - this.captureBtn.innerHTML = '截图'; + this.captureBtn.innerHTML = ''; } // 显示默认的空白状态 @@ -1307,6 +1413,79 @@ class SnapSolver { console.warn('按钮元素不可用'); } } + + checkClickOutside() { + // 点击其他区域时自动关闭悬浮窗 + document.addEventListener('click', (e) => { + // 检查是否点击在设置面板、设置按钮或其子元素之外 + if ( + !e.target.closest('#settingsPanel') && + !e.target.matches('#settingsToggle') && + !e.target.closest('#settingsToggle') && + !document.getElementById('settingsPanel').classList.contains('hidden') + ) { + document.getElementById('settingsPanel').classList.add('hidden'); + } + + // 检查是否点击在Claude面板、分析按钮或其子元素之外 + if ( + !e.target.closest('#claudePanel') && + !e.target.matches('#sendToClaude') && + !e.target.closest('#sendToClaude') && + !e.target.matches('#extractText') && + !e.target.closest('#extractText') && + !e.target.matches('#sendExtractedText') && + !e.target.closest('#sendExtractedText') && + !this.claudePanel.classList.contains('hidden') + ) { + // 因为分析可能正在进行,不自动关闭Claude面板 + // 但是可以考虑增加一个最小化功能 + } + }); + } + + // 新增清理方法,移除计时器相关代码 + cleanup() { + console.log('执行清理操作...'); + + // 清除所有Socket监听器 + if (this.socket) { + this.socket.off('text_extracted'); + this.socket.off('screenshot_response'); + this.socket.off('screenshot_complete'); + this.socket.off('request_acknowledged'); + this.socket.off('claude_response'); + this.socket.off('thinking'); + this.socket.off('thinking_complete'); + this.socket.off('analysis_complete'); + } + + // 销毁裁剪器实例 + if (this.cropper) { + this.cropper.destroy(); + this.cropper = null; + } + + console.log('清理完成'); + } + + // 空方法替代键盘快捷键实现 + setupKeyboardShortcuts() { + // 移动端应用不需要键盘快捷键 + console.log('键盘快捷键已禁用(移动端应用)'); + } + + // 控制思考动态省略号显示 + showThinkingAnimation(show) { + const dotsElement = document.querySelector('.thinking-title .dots-animation'); + if (dotsElement) { + if (show) { + dotsElement.style.display = 'inline-block'; + } else { + dotsElement.style.display = 'none'; + } + } + } } // Initialize the application when the DOM is loaded diff --git a/static/style.css b/static/style.css index 39bf139..d7fb90c 100644 --- a/static/style.css +++ b/static/style.css @@ -255,9 +255,9 @@ body { display: flex; flex-direction: column; align-items: center; - gap: 1.5rem; + gap: 1rem; margin-top: 1.5rem; - padding: 1.5rem; + padding: 1rem; background-color: var(--surface-alt); border-radius: 0.75rem; transition: all 0.3s ease; @@ -265,24 +265,139 @@ body { .analysis-button .button-group { display: flex; - gap: 1rem; + gap: 0.6rem; width: 100%; max-width: 500px; justify-content: center; } +/* 提取的文本框样式 */ +.extracted-text-area { + width: 100%; + min-height: 150px; + padding: 1rem; + margin-top: 1.5rem; + border: 1.5px solid var(--border-color); + border-radius: 0.5rem; + background-color: var(--background); + color: var(--text-primary); + font-size: 1rem; + resize: vertical; + transition: all 0.2s ease; + line-height: 1.5; + max-width: 900px; + margin-left: auto; + margin-right: auto; + display: block; +} + +.extracted-text-area:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15); +} + +/* 发送文本按钮样式 */ +.send-text-btn { + margin-top: 1rem; + margin-left: auto; + margin-right: auto; + display: block; + min-width: 150px; + justify-content: center; + padding: 0.6rem 1rem; +} + +@media (min-width: 769px) { + .send-text-btn { + margin-left: auto; + margin-right: 0; + max-width: 900px; + } + + .extracted-text-area { + margin-bottom: 0; + } +} + +@media (max-width: 480px) { + .extracted-text-area { + min-height: 120px; + padding: 0.75rem; + font-size: 0.9rem; + margin-top: 1rem; + } + + .send-text-btn { + width: 100%; + padding: 0.6rem 0.8rem; + font-size: 0.8rem; + } +} + +/* 图片操作按钮新样式 */ +.btn-action { + background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%); + color: white; + padding: 0.6rem 1rem; + border: none; + border-radius: 0.4rem; + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.4rem; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +.btn-action i { + font-size: 1rem; +} + +.btn-action:hover { + transform: translateY(-2px); + box-shadow: 0 3px 8px rgba(0, 0, 0, 0.15); + filter: brightness(1.05); +} + +.btn-action:active { + transform: translateY(0); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.btn-action::after { + content: ""; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0)); + opacity: 0; + transition: opacity 0.25s ease; +} + +.btn-action:hover::after { + opacity: 1; +} + .text-editor { width: 100%; display: flex; flex-direction: column; - gap: 1.5rem; + gap: 1rem; background-color: var(--surface); - padding: 1.5rem; - border-radius: 0.75rem; + padding: 1rem; + border-radius: 0.5rem; 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; + margin-top: 1rem; } .text-editor:hover { @@ -311,9 +426,10 @@ body { /* 发送文本按钮样式 */ #sendExtractedText { - width: 100%; + width: auto; + min-width: 120px; justify-content: center; - padding: 0.875rem; + padding: 0.6rem 1rem; margin-top: 0.5rem; } @@ -402,7 +518,7 @@ body { .response-content { padding: 1rem; - font-size: 0.95rem; + font-size: 0.85rem; } .thinking-content { @@ -472,10 +588,10 @@ body { } .text-editor { - padding: 1rem; + padding: 0.8rem; border-radius: 0.5rem; margin-top: 1rem; - gap: 1rem; + gap: 0.8rem; } .text-editor textarea { @@ -490,16 +606,40 @@ body { } #sendExtractedText { - padding: 0.75rem; + padding: 0.6rem 0.8rem; + font-size: 0.8rem; + width: 100%; + align-self: stretch; + } + + .btn-action { + padding: 0.5rem 0.8rem; + font-size: 0.75rem; + } + + .btn-action i { font-size: 0.9rem; } + + .analysis-button { + padding: 0.8rem; + } + + .analysis-button .button-group { + gap: 0.5rem; + } + + .response-content { + padding: 0.9rem; + font-size: 0.82rem; + } } /* Claude Panel */ .claude-panel { background-color: var(--surface); - border-radius: 1rem; - box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); + border-radius: 0.75rem; + box-shadow: 0 3px 15px rgba(0, 0, 0, 0.05); display: flex; flex-direction: column; height: auto; @@ -511,13 +651,13 @@ body { } .claude-panel:not(.hidden) { - animation: panel-slide-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards; + animation: panel-slide-in 0.3s cubic-bezier(0.19, 1, 0.22, 1) forwards; } @keyframes panel-slide-in { 0% { opacity: 0; - transform: translateY(20px); + transform: translateY(15px); } 100% { opacity: 1; @@ -529,84 +669,405 @@ body { display: flex; justify-content: space-between; align-items: center; - padding: 1.25rem; + padding: 1rem 1.25rem; border-bottom: 1px solid var(--border-color); background-color: var(--surface); position: sticky; top: 0; z-index: 5; + backdrop-filter: blur(8px); } .header-title { display: flex; - align-items: center; - gap: 1rem; + flex-direction: column; + gap: 0.5rem; } -.analysis-status { +.header-title h2 { + font-size: 1.1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; display: flex; align-items: center; + gap: 0.5rem; } -.status-light { - width: 12px; - height: 12px; - border-radius: 50%; - background-color: var(--text-secondary); - transition: background-color 0.3s ease; - box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05); +.header-title h2 i { + color: var(--primary); + font-size: 1rem; } -.status-light.processing { - background-color: #ffd700; - animation: pulse 1.5s infinite; - box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); +/* 新的状态指示器 */ +.analysis-indicator { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.75rem; + color: var(--text-secondary); } -.status-light.completed { +.progress-line { + height: 2px; + width: 60px; + background-color: var(--border-color); + border-radius: 1px; + overflow: hidden; + position: relative; +} + +.progress-line::before { + content: ''; + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 0%; + background-color: var(--primary); + border-radius: 1px; + transition: width 0.3s ease; +} + +.progress-line.processing::before { + animation: progress-animation 2s ease-in-out infinite; +} + +.progress-line.completed::before { + width: 100%; background-color: var(--success); - box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); } -.status-light.error { +.progress-line.error::before { + width: 100%; background-color: var(--danger); - box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2); } -@keyframes pulse { +@keyframes progress-animation { 0% { - opacity: 1; - box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); + width: 0%; + left: 0; } 50% { - opacity: 0.6; - box-shadow: 0 0 0 4px rgba(255, 215, 0, 0.4); + width: 50%; + left: 25%; } 100% { - opacity: 1; - box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2); + width: 0%; + left: 100%; } } +.status-text { + font-weight: 500; + white-space: nowrap; + transition: color 0.3s ease; +} + +.analysis-indicator.processing .status-text { + color: var(--primary); +} + +.analysis-indicator.completed .status-text { + color: var(--success); +} + +.analysis-indicator.error .status-text { + color: var(--danger); +} + +/* Thinking Section */ +.thinking-section { + margin: 0.25rem 0 0.1rem 0; + border-top: none; + border-bottom: none; +} + +.thinking-header { + padding: 0.5rem 1rem; + cursor: pointer; + display: flex; + justify-content: space-between; + align-items: center; + transition: all 0.2s ease; + background-color: rgba(var(--primary-rgb), 0.04); + border: none; + border-radius: 0.35rem; + margin: 0.25rem 1rem 0.15rem 1rem; + opacity: 0.9; +} + +.thinking-header:hover { + background-color: rgba(var(--primary-rgb), 0.08); + opacity: 1; +} + +.thinking-title { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.thinking-title i { + color: var(--text-secondary); + font-size: 0.9rem; + opacity: 0.85; +} + +.thinking-title h3 { + margin: 0; + font-size: 0.9rem; + font-weight: 500; + color: var(--text-secondary); +} + +.toggle-btn { + background: none; + border: none; + cursor: pointer; + color: var(--text-secondary); + padding: 0.2rem; + width: 24px; + height: 24px; + border-radius: 3px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + opacity: 0.8; +} + +.toggle-btn:hover { + background-color: rgba(var(--primary-rgb), 0.1); + color: var(--primary); + opacity: 1; +} + +.toggle-btn i { + font-size: 0.8rem; +} + +.thinking-content { + padding: 0; + background-color: rgba(var(--primary-rgb), 0.02); + border-left: 1px solid rgba(var(--primary-rgb), 0.1); + margin: 0 1.5rem 0.3rem 1.75rem; + overflow: hidden; + transition: all 0.3s ease; + font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; + font-size: 0.85rem; + line-height: 1.5; +} + +/* 思考进行中的动态省略号 */ +@keyframes thinking-dots { + 0% { content: ''; } + 25% { content: '.'; } + 50% { content: '..'; } + 75% { content: '...'; } + 100% { content: ''; } +} + +.thinking-title .dots-animation { + display: inline-block; + min-width: 24px; + margin-left: 4px; + height: 1em; + position: relative; +} + +.thinking-title .dots-animation::after { + content: ''; + animation: thinking-dots 1.5s infinite; + position: absolute; + left: 0; + color: var(--primary); + font-weight: bold; +} + .response-content { - padding: 1.5rem; - background-color: #fff; - border-radius: 6px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); - line-height: 1.6; - white-space: pre-wrap; + padding: 1rem 1.5rem; + background-color: transparent; + border-radius: 0; + box-shadow: none; + line-height: 1.5; + white-space: normal; overflow-wrap: break-word; - margin-top: 1rem; - font-size: 1rem; + margin-top: 0.1rem; + font-size: 0.9rem; height: auto; width: 100%; } +[data-theme="dark"] .response-content { + background-color: transparent; + box-shadow: none; +} + +/* 添加Markdown样式 */ +.response-content h1, +.response-content h2, +.response-content h3, +.response-content h4, +.response-content h5, +.response-content h6 { + margin-top: 1.3rem; + margin-bottom: 0.8rem; + font-weight: 600; + line-height: 1.25; + color: var(--text-primary); +} + +.response-content h1 { + font-size: 1.8em; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.3em; +} + +.response-content h2 { + font-size: 1.4em; + border-bottom: 1px solid var(--border-color); + padding-bottom: 0.3em; +} + +.response-content h3 { + font-size: 1.2em; +} + +.response-content h4 { + font-size: 1em; +} + +.response-content h5 { + font-size: 0.875em; +} + +.response-content h6 { + font-size: 0.85em; + color: var(--text-secondary); +} + +.response-content p { + margin-top: 0; + margin-bottom: 0.8rem; +} + +.response-content a { + color: var(--primary); + text-decoration: none; +} + +.response-content a:hover { + text-decoration: underline; +} + +.response-content blockquote { + margin: 0 0 0.8rem; + padding: 0 0.8rem; + color: var(--text-secondary); + border-left: 0.25rem solid var(--border-color); +} + +.response-content code { + font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; + font-size: 0.9em; + padding: 0.2em 0.4em; + margin: 0; + background-color: rgba(var(--surface-alt-rgb), 0.5); + border-radius: 3px; +} + +.response-content pre { + background-color: rgba(var(--surface-alt-rgb), 0.5); + border-radius: 6px; + padding: 0.8rem; + overflow: auto; + margin-bottom: 0.8rem; +} + +.response-content pre code { + background-color: transparent; + padding: 0; + margin: 0; + font-size: 0.85em; + white-space: pre; + overflow-wrap: normal; +} + +.response-content ul, +.response-content ol { + margin-top: 0; + margin-bottom: 0.8rem; + padding-left: 1.8rem; +} + +.response-content li + li { + margin-top: 0.2rem; +} + +.response-content table { + display: block; + width: 100%; + overflow: auto; + border-spacing: 0; + border-collapse: collapse; + margin-bottom: 0.8rem; +} + +.response-content table th { + font-weight: 600; + background-color: var(--surface-alt); +} + +.response-content table th, +.response-content table td { + padding: 5px 10px; + border: 1px solid var(--border-color); +} + +.response-content table tr { + background-color: var(--surface); + border-top: 1px solid var(--border-color); +} + +.response-content table tr:nth-child(2n) { + background-color: var(--surface-alt); +} + +.response-content img { + max-width: 100%; + box-sizing: content-box; + margin: 0.8rem 0; + border-radius: 4px; +} + +/* 特殊样式:任务列表 */ +.response-content input[type="checkbox"] { + margin-right: 0.5rem; +} + [data-theme="dark"] .response-content { background-color: rgba(var(--surface-rgb), 0.7); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); } +/* 暗色模式下的代码块样式 */ +[data-theme="dark"] .response-content pre { + background-color: rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .response-content code { + background-color: rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] .response-content table th { + background-color: rgba(var(--surface-alt-rgb), 0.3); +} + +[data-theme="dark"] .response-content table tr:nth-child(2n) { + background-color: rgba(var(--surface-alt-rgb), 0.2); +} + /* Settings Panel */ .settings-panel { position: fixed; @@ -1061,100 +1522,44 @@ button:disabled { } } -/* Thinking Section */ -.thinking-section { - margin-bottom: 1rem; - border-bottom: 1px solid var(--border-color); +/* 应用同样的Markdown样式到思考内容 */ +.thinking-content h1, +.thinking-content h2, +.thinking-content h3, +.thinking-content h4, +.thinking-content h5, +.thinking-content h6, +.thinking-content p, +.thinking-content a, +.thinking-content blockquote, +.thinking-content code, +.thinking-content pre, +.thinking-content ul, +.thinking-content ol, +.thinking-content table, +.thinking-content img { + /* 继承响应内容的样式,但字体更小 */ + font-size: 90%; } -.thinking-header { - padding: 1rem 1.5rem; - cursor: pointer; - display: flex; - justify-content: space-between; - align-items: center; - transition: all 0.2s ease; - background-color: rgba(var(--primary-rgb), 0.1); - border: 2px solid transparent; - border-radius: 6px; - position: relative; -} - -.thinking-header::after { - position: absolute; - right: 60px; - color: var(--text-secondary); - font-size: 0.8rem; - opacity: 0; - transition: opacity 0.2s ease; -} - -.thinking-header:hover { - background-color: rgba(var(--primary-rgb), 0.15); - border: 2px dashed rgba(var(--primary-rgb), 0.3); -} - -.thinking-header:hover::after { - opacity: 0.7; -} - -.thinking-title { - display: flex; - align-items: center; - gap: 0.75rem; -} - -.thinking-title i { - color: var(--primary); - font-size: 1.125rem; -} - -.thinking-title h3 { - margin: 0; - font-size: 1rem; - font-weight: 600; - color: var(--text-primary); -} - -.toggle-btn { - background: none; - border: none; - cursor: pointer; - color: var(--text-color); - padding: 5px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - transition: background-color 0.3s ease; -} - -.toggle-btn:hover { - background-color: var(--hover-color); -} - -.thinking-content { - padding: 0; - background-color: rgba(var(--primary-rgb), 0.05); - border-left: 3px solid rgba(var(--primary-rgb), 0.3); - overflow: hidden; - transition: max-height 0.3s ease, padding 0.3s ease; - font-family: monospace; - white-space: pre-wrap; - font-size: 0.9rem; - line-height: 1.5; +.thinking-content pre code { + white-space: pre; + font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace; } +/* 思考内容的展开折叠样式 */ .thinking-content.collapsed { max-height: 0; - padding: 0 1.5rem; - overflow: hidden; + padding: 0 1.25rem; + margin-bottom: 0; + opacity: 0; } .thinking-content.expanded { max-height: none; - padding: 1rem 1.5rem; - overflow-y: visible; + padding: 0.75rem 1.25rem; + margin-bottom: 0.3rem; + opacity: 1; } /* Animation for thinking content */ @@ -1312,14 +1717,25 @@ button:disabled { /* 大屏幕设备的布局 */ @media (min-width: 769px) { .text-editor { + flex-direction: row; + align-items: flex-end; max-width: 900px; margin: 1.5rem auto; + gap: 1rem; + } + + .text-editor textarea { + flex: 1; + min-height: 120px; + margin-bottom: 0; } #sendExtractedText { width: auto; - min-width: 200px; + min-width: 150px; align-self: flex-end; + margin-top: 0; + flex: 0 0 auto; } } @@ -1590,3 +2006,170 @@ button:disabled { gap: 0.5rem; } } + +/* 截图按钮高亮样式 */ +.capture-btn-highlight { + background-color: #ff5a5f !important; + color: white !important; + transform: scale(1.15); + box-shadow: 0 0 8px rgba(255, 90, 95, 0.5); + border-radius: 50%; + position: relative; +} + +.capture-btn-highlight i { + font-size: 1.2em; +} + +.capture-btn-highlight:hover { + background-color: #ff7e82 !important; + transform: scale(1.2); + box-shadow: 0 0 12px rgba(255, 90, 95, 0.7); +} + +.capture-btn-highlight:active { + transform: scale(1.1); + box-shadow: 0 0 5px rgba(255, 90, 95, 0.4); +} + +/* 添加呼吸动画效果 */ +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(255, 90, 95, 0.7); + } + 70% { + box-shadow: 0 0 0 10px rgba(255, 90, 95, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(255, 90, 95, 0); + } +} + +.capture-btn-highlight:not(:disabled) { + animation: pulse 2s infinite; +} + +/* 暗色模式适配 */ +[data-theme="dark"] .capture-btn-highlight { + background-color: #ff5a5f !important; + color: white !important; +} + +@media (min-width: 769px) { + .text-editor { + display: flex; + flex-direction: row; + align-items: flex-end; + flex-wrap: wrap; + } + + .text-editor textarea { + flex: 1; + margin-right: 1rem; + min-height: 120px; + } + + #sendExtractedText { + margin-top: 0; + } +} + +/* 移动设备上的思考过程样式优化 */ +@media (max-width: 768px) { + .thinking-section { + margin: 0.2rem 0 0.1rem 0; + } + + .thinking-header { + padding: 0.45rem 0.8rem; + margin: 0.2rem 0.8rem 0.1rem 0.8rem; + border-radius: 0.35rem; + } + + .thinking-title i { + font-size: 0.85rem; + } + + .thinking-title h3 { + font-size: 0.85rem; + } + + .thinking-title .dots-animation { + min-width: 16px; + } + + .toggle-btn { + width: 22px; + height: 22px; + } + + .thinking-content { + margin: 0 1.25rem 0.2rem 1.5rem; + font-size: 0.8rem; + } + + .thinking-content.expanded { + padding: 0.6rem 1rem; + margin-bottom: 0.2rem; + } + + .response-content { + padding: 0.8rem 1rem; + margin-top: 0.1rem; + } +} + +@media (max-width: 480px) { + .thinking-section { + margin: 0.15rem 0 0.1rem 0; + } + + .thinking-header { + padding: 0.4rem 0.7rem; + margin: 0.15rem 0.65rem 0.1rem 0.65rem; + } + + .thinking-title { + gap: 0.4rem; + } + + .thinking-title i { + font-size: 0.8rem; + } + + .thinking-title h3 { + font-size: 0.8rem; + } + + .thinking-content { + margin: 0 1.1rem 0.15rem 1.3rem; + } + + .thinking-content.expanded { + padding: 0.5rem 0.8rem; + margin-bottom: 0.15rem; + } + + .response-content { + padding: 0.7rem 0.9rem; + margin-top: 0.1rem; + } +} + +/* 思考过程和回复内容的分隔 */ +.thinking-section:not(.hidden) + .response-content { + border-top: 1px dashed rgba(var(--primary-rgb), 0.15); + padding-top: 0.8rem; +} + +@media (max-width: 768px) { + .thinking-section:not(.hidden) + .response-content { + padding-top: 0.7rem; + } +} + +@media (max-width: 480px) { + .thinking-section:not(.hidden) + .response-content { + padding-top: 0.6rem; + } +} diff --git a/templates/index.html b/templates/index.html index 668d0b3..a914d6b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -21,6 +21,11 @@ }; + + + + +
@@ -28,6 +33,9 @@

Snap Solver

未连接
+ @@ -53,24 +61,10 @@
-
-
-
- - -
-
-

准备好开始了吗?

-

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

+

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

如果觉得好用,别忘了给项目点个 Star ⭐

- -
- + +