diff --git a/app.py b/app.py
index 4e96ac0..e3a8ac8 100644
--- a/app.py
+++ b/app.py
@@ -104,7 +104,6 @@ def stream_model_response(response_generator, sid):
thinking_buffer += content
# 发送完整的思考内容
- print(f"Streaming thinking content: {len(thinking_buffer)} chars")
socketio.emit('claude_response', {
'status': 'thinking',
'content': thinking_buffer
@@ -127,7 +126,7 @@ def stream_model_response(response_generator, sid):
response_buffer += content
# 发送完整的内容
- print(f"Streaming response content: {len(response_buffer)} chars")
+ # print(f"Streaming response content: {len(response_buffer)} chars")
socketio.emit('claude_response', {
'status': 'streaming',
'content': response_buffer
@@ -379,6 +378,30 @@ def handle_analyze_image(data):
'error': f'Analysis error: {str(e)}'
}, room=request.sid)
+@socketio.on('capture_screenshot')
+def handle_capture_screenshot(data):
+ try:
+ # Capture the screen
+ screenshot = pyautogui.screenshot()
+
+ # Convert the image to base64 string
+ buffered = BytesIO()
+ screenshot.save(buffered, format="PNG")
+ img_str = base64.b64encode(buffered.getvalue()).decode()
+
+ # Emit the screenshot back to the client
+ socketio.emit('screenshot_complete', {
+ 'success': True,
+ 'image': img_str
+ }, room=request.sid)
+ except Exception as e:
+ error_msg = f"Screenshot error: {str(e)}"
+ print(f"Error capturing screenshot: {error_msg}")
+ socketio.emit('screenshot_complete', {
+ 'success': False,
+ 'error': error_msg
+ }, room=request.sid)
+
def run_tray():
icon = create_tray_icon()
icon.run()
diff --git a/models/claude.py b/models/claude.py
index ad1ebaf..6b29bed 100644
--- a/models/claude.py
+++ b/models/claude.py
@@ -18,13 +18,12 @@ class ClaudeModel(BaseModel):
def analyze_text(self, text: str, proxies: dict = None) -> Generator[dict, None, None]:
"""Stream Claude's response for text analysis"""
try:
- # Initial status
- yield {"status": "started", "content": ""}
-
- api_key = self.api_key.strip()
+ yield {"status": "started"}
+
+ api_key = self.api_key
if api_key.startswith('Bearer '):
api_key = api_key[7:]
-
+
headers = {
'x-api-key': api_key,
'anthropic-version': '2023-06-01',
@@ -105,6 +104,16 @@ class ClaudeModel(BaseModel):
"status": "thinking",
"content": thinking_content
}
+
+ # 处理新的extended_thinking格式
+ elif data.get('type') == 'extended_thinking_delta':
+ if 'delta' in data and 'text' in data['delta']:
+ thinking_chunk = data['delta']['text']
+ thinking_content += thinking_chunk
+ yield {
+ "status": "thinking",
+ "content": thinking_content
+ }
elif data.get('type') == 'message_stop':
if thinking_content:
@@ -135,130 +144,132 @@ class ClaudeModel(BaseModel):
"error": f"Streaming error: {str(e)}"
}
- def analyze_image(self, image_data: str, proxies: dict = None) -> Generator[dict, None, None]:
- """Stream Claude's response for image analysis"""
- try:
- # Initial status
- yield {"status": "started", "content": ""}
-
- api_key = self.api_key.strip()
- if api_key.startswith('Bearer '):
- api_key = api_key[7:]
-
- headers = {
- 'x-api-key': api_key,
- 'anthropic-version': '2023-06-01',
- 'content-type': 'application/json',
- 'accept': 'application/json',
- }
-
- payload = {
- 'model': self.get_model_identifier(),
- 'stream': True,
- 'max_tokens': 8192,
- 'temperature': 1,
- 'system': self.system_prompt,
- 'thinking': {
- 'type': 'enabled',
- 'budget_tokens': 4096
- },
- 'messages': [{
- 'role': 'user',
- 'content': [
- {
- 'type': 'image',
- 'source': {
- 'type': 'base64',
- 'media_type': 'image/png',
- 'data': image_data
- }
- },
- {
- 'type': 'text',
- 'text': "Please analyze this question and provide a detailed solution. If you see multiple questions, focus on solving them one at a time."
- }
- ]
- }]
- }
-
- response = requests.post(
- 'https://api.anthropic.com/v1/messages',
- headers=headers,
- json=payload,
- stream=True,
- proxies=proxies,
- timeout=60
- )
-
- if response.status_code != 200:
- error_msg = f'API error: {response.status_code}'
- try:
- error_data = response.json()
- if 'error' in error_data:
- error_msg += f" - {error_data['error']['message']}"
- except:
- error_msg += f" - {response.text}"
- yield {"status": "error", "error": error_msg}
- return
-
- thinking_content = ""
- response_buffer = ""
+ def analyze_image(self, image_data, prompt, socket=None, proxies=None):
+ yield {"status": "started"}
+
+ api_key = self.api_key
+ if api_key.startswith('Bearer '):
+ api_key = api_key[7:]
- for chunk in response.iter_lines():
- if not chunk:
+ headers = {
+ 'x-api-key': api_key,
+ 'anthropic-version': '2023-06-01',
+ 'content-type': 'application/json'
+ }
+
+ payload = {
+ 'model': 'claude-3-7-sonnet-20250219',
+ 'stream': True,
+ 'max_tokens': 8192,
+ 'temperature': 1,
+ 'thinking': {
+ 'type': 'enabled',
+ 'budget_tokens': 4096
+ },
+ 'system': "You are a helpful AI assistant that specializes in solving math problems. You should provide step-by-step solutions and explanations for any math problem presented to you. If you're given an image, analyze any mathematical content in it and provide a detailed solution.",
+ 'messages': [{
+ 'role': 'user',
+ 'content': [
+ {
+ 'type': 'image',
+ 'source': {
+ 'type': 'base64',
+ 'media_type': 'image/png',
+ 'data': image_data
+ }
+ },
+ {
+ 'type': 'text',
+ 'text': "Please analyze this question and provide a detailed solution. If you see multiple questions, focus on solving them one at a time."
+ }
+ ]
+ }]
+ }
+
+ response = requests.post(
+ 'https://api.anthropic.com/v1/messages',
+ headers=headers,
+ json=payload,
+ stream=True,
+ proxies=proxies,
+ timeout=60
+ )
+
+ if response.status_code != 200:
+ error_msg = f'API error: {response.status_code}'
+ try:
+ error_data = response.json()
+ if 'error' in error_data:
+ error_msg += f" - {error_data['error']['message']}"
+ except:
+ error_msg += f" - {response.text}"
+ yield {"status": "error", "error": error_msg}
+ return
+
+ thinking_content = ""
+ response_buffer = ""
+
+ for chunk in response.iter_lines():
+ if not chunk:
+ continue
+
+ try:
+ chunk_str = chunk.decode('utf-8')
+ if not chunk_str.startswith('data: '):
continue
- try:
- chunk_str = chunk.decode('utf-8')
- if not chunk_str.startswith('data: '):
- continue
+ chunk_str = chunk_str[6:]
+ data = json.loads(chunk_str)
- chunk_str = chunk_str[6:]
- data = json.loads(chunk_str)
-
- if data.get('type') == 'content_block_delta':
- if 'delta' in data:
- if 'text' in data['delta']:
- text_chunk = data['delta']['text']
- yield {
- "status": "streaming",
- "content": text_chunk
- }
- response_buffer += text_chunk
-
- elif 'thinking' in data['delta']:
- thinking_chunk = data['delta']['thinking']
- thinking_content += thinking_chunk
- yield {
- "status": "thinking",
- "content": thinking_content
- }
-
- elif data.get('type') == 'message_stop':
- if thinking_content:
+ if data.get('type') == 'content_block_delta':
+ if 'delta' in data:
+ if 'text' in data['delta']:
+ text_chunk = data['delta']['text']
yield {
- "status": "thinking_complete",
+ "status": "streaming",
+ "content": text_chunk
+ }
+ response_buffer += text_chunk
+
+ elif 'thinking' in data['delta']:
+ thinking_chunk = data['delta']['thinking']
+ thinking_content += thinking_chunk
+ yield {
+ "status": "thinking",
"content": thinking_content
}
+
+ # 处理新的extended_thinking格式
+ elif data.get('type') == 'extended_thinking_delta':
+ if 'delta' in data and 'text' in data['delta']:
+ thinking_chunk = data['delta']['text']
+ thinking_content += thinking_chunk
yield {
- "status": "completed",
- "content": ""
+ "status": "thinking",
+ "content": thinking_content
}
- elif data.get('type') == 'error':
- error_msg = data.get('error', {}).get('message', 'Unknown error')
+ elif data.get('type') == 'message_stop':
+ if thinking_content:
yield {
- "status": "error",
- "error": error_msg
+ "status": "thinking_complete",
+ "content": thinking_content
}
- break
-
- except json.JSONDecodeError as e:
- print(f"JSON decode error: {str(e)}")
- continue
-
- except Exception as e:
- yield {
- "status": "error",
- "error": f"Streaming error: {str(e)}"
- }
+ yield {
+ "status": "completed",
+ "content": response_buffer
+ }
+
+ elif data.get('type') == 'error':
+ error_message = data.get('error', {}).get('message', 'Unknown error')
+ yield {
+ "status": "error",
+ "error": error_message
+ }
+
+ except Exception as e:
+ yield {
+ "status": "error",
+ "error": f"Error processing response: {str(e)}"
+ }
+ break
diff --git a/models/gpt4o.py b/models/gpt4o.py
index 1124256..64c7614 100644
--- a/models/gpt4o.py
+++ b/models/gpt4o.py
@@ -132,7 +132,7 @@ class GPT4oModel(BaseModel):
{
"type": "image_url",
"image_url": {
- "url": f"data:image/png;base64,{image_data}",
+ "url": image_data if image_data.startswith('data:') else f"data:image/png;base64,{image_data}",
"detail": "high"
}
},
diff --git a/static/js/main.js b/static/js/main.js
index 1855a86..97aed27 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -1,26 +1,25 @@
class SnapSolver {
constructor() {
- // 初始化managers
- window.uiManager = new UIManager();
- window.settingsManager = new SettingsManager();
+ console.log('Creating SnapSolver instance...');
- // 初始化应用组件
- this.initializeElements();
- this.initializeState();
- this.initializeConnection();
- this.setupSocketEventHandlers();
- this.setupAutoScroll();
- this.setupEventListeners();
+ // 初始化属性
+ this.socket = null;
+ this.cropper = null;
+ this.originalImage = null;
+ this.croppedImage = null;
+ this.extractedContent = '';
+ this.emitTimeout = null;
+ this.eventsSetup = false;
- // 初始化历史
- window.app = this; // 便于从其他地方访问
- this.updateHistoryPanel();
+ // 初始化应用
+ this.initialize();
}
initializeElements() {
// Main elements
this.screenshotImg = document.getElementById('screenshotImg');
this.imagePreview = document.getElementById('imagePreview');
+ this.emptyState = document.getElementById('emptyState');
this.cropBtn = document.getElementById('cropBtn');
this.captureBtn = document.getElementById('captureBtn');
this.sendToClaudeBtn = document.getElementById('sendToClaude');
@@ -28,6 +27,7 @@ class SnapSolver {
this.textEditor = document.getElementById('textEditor');
this.extractedText = document.getElementById('extractedText');
this.sendExtractedTextBtn = document.getElementById('sendExtractedText');
+ this.cropContainer = document.getElementById('cropContainer');
this.manualTextInput = document.getElementById('manualTextInput');
this.claudePanel = document.getElementById('claudePanel');
this.responseContent = document.getElementById('responseContent');
@@ -38,7 +38,6 @@ class SnapSolver {
this.statusLight = document.querySelector('.status-light');
// Crop elements
- this.cropContainer = document.getElementById('cropContainer');
this.cropCancel = document.getElementById('cropCancel');
this.cropConfirm = document.getElementById('cropConfirm');
@@ -94,17 +93,17 @@ class SnapSolver {
});
}
- updateConnectionStatus(connected) {
- this.connectionStatus.textContent = connected ? 'Connected' : 'Disconnected';
- this.connectionStatus.className = `status ${connected ? 'connected' : 'disconnected'}`;
- this.captureBtn.disabled = !connected;
-
- if (!connected) {
- this.imagePreview.classList.add('hidden');
- this.cropBtn.classList.add('hidden');
- this.sendToClaudeBtn.classList.add('hidden');
- this.extractTextBtn.classList.add('hidden');
- this.textEditor.classList.add('hidden');
+ updateConnectionStatus(status, isConnected) {
+ if (this.connectionStatus) {
+ this.connectionStatus.textContent = status;
+
+ if (isConnected) {
+ this.connectionStatus.classList.remove('disconnected');
+ this.connectionStatus.classList.add('connected');
+ } else {
+ this.connectionStatus.classList.remove('connected');
+ this.connectionStatus.classList.add('disconnected');
+ }
}
}
@@ -140,22 +139,22 @@ class SnapSolver {
this.socket.on('connect', () => {
console.log('Connected to server');
- this.updateConnectionStatus(true);
+ this.updateConnectionStatus('已连接', true);
});
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
- this.updateConnectionStatus(false);
+ this.updateConnectionStatus('已断开', false);
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
- this.updateConnectionStatus(false);
+ this.updateConnectionStatus('连接错误', false);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
- this.updateConnectionStatus(true);
+ this.updateConnectionStatus('已重连', true);
});
this.socket.on('reconnect_attempt', (attemptNumber) => {
@@ -168,76 +167,142 @@ class SnapSolver {
this.socket.on('reconnect_failed', () => {
console.error('Failed to reconnect');
- window.showToast('连接服务器失败,请刷新页面重试', 'error');
+ window.uiManager.showToast('Connection to server failed, please refresh the page and try again', 'error');
});
this.setupSocketEventHandlers();
} catch (error) {
console.error('Connection error:', error);
- this.updateConnectionStatus(false);
+ this.updateConnectionStatus('重连失败', false);
setTimeout(() => this.initializeConnection(), 5000);
}
}
setupSocketEventHandlers() {
- // Screenshot response handler
+ // 如果已经设置过事件处理器,先移除它们
+ if (this.hasOwnProperty('eventsSetup') && this.eventsSetup) {
+ this.socket.off('screenshot_response');
+ this.socket.off('screenshot_complete');
+ this.socket.off('request_acknowledged');
+ this.socket.off('text_extracted');
+ this.socket.off('claude_response');
+ }
+
+ // 标记事件处理器已设置
+ this.eventsSetup = true;
+
+ // 旧版截图响应处理器 (保留兼容性)
this.socket.on('screenshot_response', (data) => {
if (data.success) {
this.screenshotImg.src = `data:image/png;base64,${data.image}`;
+ this.originalImage = `data:image/png;base64,${data.image}`;
this.imagePreview.classList.remove('hidden');
- this.cropBtn.classList.remove('hidden');
+ this.emptyState.classList.add('hidden');
+
+ // 显示Claude和提取文本按钮
+ this.sendToClaudeBtn.classList.remove('hidden');
+ this.extractTextBtn.classList.remove('hidden');
+
+ // 恢复按钮状态
this.captureBtn.disabled = false;
- this.captureBtn.innerHTML = 'Capture';
- this.sendToClaudeBtn.classList.add('hidden');
- this.extractTextBtn.classList.add('hidden');
- this.textEditor.classList.add('hidden');
- window.showToast('Screenshot captured successfully');
+ this.captureBtn.innerHTML = '截图';
+
+ // 初始化裁剪器
+ this.initializeCropper();
+
+ window.uiManager.showToast('截图成功', 'success');
} else {
- window.showToast('Failed to capture screenshot: ' + data.error, 'error');
this.captureBtn.disabled = false;
- this.captureBtn.innerHTML = 'Capture';
+ this.captureBtn.innerHTML = '截图';
+ console.error('截图失败:', data.error);
+ window.uiManager.showToast('截图失败: ' + data.error, 'error');
+ }
+ });
+
+ // 新版截图响应处理器
+ this.socket.on('screenshot_complete', (data) => {
+ this.captureBtn.disabled = false;
+ this.captureBtn.innerHTML = '截图';
+
+ if (data.success) {
+ // 显示截图预览
+ this.screenshotImg.src = 'data:image/png;base64,' + data.image;
+ this.originalImage = 'data:image/png;base64,' + data.image;
+ this.imagePreview.classList.remove('hidden');
+ this.emptyState.classList.add('hidden');
+
+ // 显示Claude和提取文本按钮
+ this.sendToClaudeBtn.classList.remove('hidden');
+ this.extractTextBtn.classList.remove('hidden');
+
+ // 初始化裁剪工具
+ this.initializeCropper();
+
+ // 显示成功消息
+ window.uiManager.showToast('截图成功', 'success');
+ } else {
+ // 显示错误消息
+ window.uiManager.showToast('截图失败: ' + (data.error || '未知错误'), 'error');
}
});
- // 请求确认响应处理器
+ // 确认请求处理
this.socket.on('request_acknowledged', (data) => {
- console.log('服务器确认收到请求:', data);
- window.showToast(data.message || '请求已接收,正在处理...', 'info');
- // 清除可能存在的超时计时器
+ console.log('服务器确认收到请求:', data);
+ });
+
+ // Text extraction response
+ this.socket.on('text_extracted', (data) => {
+ // 重新启用按钮
+ this.extractTextBtn.disabled = false;
+ this.extractTextBtn.innerHTML = '提取文本';
+
+ if (this.extractedText) {
+ this.extractedText.disabled = false;
+ }
+
if (this.emitTimeout) {
clearTimeout(this.emitTimeout);
this.emitTimeout = null;
}
- });
-
- // Text extraction response handler
- this.socket.on('text_extracted', (data) => {
- if (data.error) {
- console.error('Text extraction error:', data.error);
- window.showToast('Failed to extract text: ' + data.error, 'error');
- if (this.extractedText) {
- this.extractedText.value = '';
- this.extractedText.disabled = false;
- }
- this.sendExtractedTextBtn.disabled = false; // Re-enable send button on server error
- } else if (data.content) {
- // 直接使用提取的文本内容
- this.extractedContent = data.content;
-
- // 更新文本编辑器
- if (this.extractedText) {
- this.extractedText.value = data.content;
- this.extractedText.disabled = false;
- this.extractedText.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
- this.sendExtractedTextBtn.disabled = false;
- }
-
- window.showToast('文本提取成功');
- }
- this.extractTextBtn.disabled = false;
- this.extractTextBtn.innerHTML = 'Extract Text';
+ // 检查是否有内容数据
+ if (data.content) {
+ this.extractedText.value = data.content;
+ this.extractedContent = data.content;
+ this.textEditor.classList.remove('hidden');
+ this.sendExtractedTextBtn.disabled = false;
+
+ // 更新置信度指示器 (如果有的话)
+ if (data.confidence !== undefined) {
+ const confidence = data.confidence || 0;
+ this.confidenceValue.textContent = `${Math.round(confidence * 100)}%`;
+
+ // 置信度颜色
+ const confidenceEl = this.confidenceIndicator;
+ if (confidence > 0.8) {
+ confidenceEl.style.color = 'var(--success)';
+ } else if (confidence > 0.5) {
+ confidenceEl.style.color = 'var(--primary)';
+ } else {
+ confidenceEl.style.color = 'var(--danger)';
+ }
+ }
+
+ window.uiManager.showToast('文本提取成功', 'success');
+ } else if (data.error) {
+ console.error('文本提取失败:', data.error);
+ window.uiManager.showToast('文本提取失败: ' + data.error, 'error');
+
+ // 启用发送按钮以便用户可以手动输入文本
+ this.sendExtractedTextBtn.disabled = false;
+ } else {
+ // 未知响应格式
+ console.error('未知的文本提取响应格式:', data);
+ window.uiManager.showToast('文本提取返回未知格式', 'error');
+ this.sendExtractedTextBtn.disabled = false;
+ }
});
this.socket.on('claude_response', (data) => {
@@ -253,6 +318,10 @@ class SnapSolver {
this.thinkingSection.classList.add('hidden');
this.sendToClaudeBtn.disabled = true;
this.sendExtractedTextBtn.disabled = true;
+
+ // 显示进行中状态
+ this.responseContent.innerHTML = '
分析进行中,请稍候...
';
+ this.responseContent.style.display = 'block';
break;
case 'thinking':
@@ -261,11 +330,36 @@ class SnapSolver {
console.log('Received thinking content');
this.thinkingSection.classList.remove('hidden');
+ // 记住用户的展开/折叠状态
+ const wasExpanded = this.thinkingContent.classList.contains('expanded');
+
// 直接设置完整内容而不是追加
this.setElementContent(this.thinkingContent, data.content);
// 添加打字动画效果
this.thinkingContent.classList.add('thinking-typing');
+
+ // 根据之前的状态决定是否展开
+ if (wasExpanded) {
+ this.thinkingContent.classList.add('expanded');
+ this.thinkingContent.classList.remove('collapsed');
+
+ // 更新切换按钮图标
+ const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-up';
+ }
+ } else {
+ // 初始状态为折叠
+ this.thinkingContent.classList.add('collapsed');
+ this.thinkingContent.classList.remove('expanded');
+
+ // 更新切换按钮图标
+ const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+ }
}
break;
@@ -287,8 +381,9 @@ class SnapSolver {
if (data.content) {
console.log('Received content');
- // 直接设置完整内容
- this.setElementContent(this.responseContent, data.content);
+ // 设置结果内容
+ this.responseContent.innerHTML = data.content;
+ this.responseContent.style.display = 'block';
// 移除思考部分的打字动画
this.thinkingContent.classList.remove('thinking-typing');
@@ -300,12 +395,30 @@ class SnapSolver {
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
+ // 恢复界面
+ this.updateStatusLight('completed');
+
// 保存到历史记录
const responseText = this.responseContent.textContent || '';
const thinkingText = this.thinkingContent.textContent || '';
this.addToHistory(this.croppedImage, responseText, thinkingText);
- window.showToast('Analysis completed successfully');
+ // 确保思考内容处于折叠状态
+ this.thinkingContent.classList.remove('expanded');
+ this.thinkingContent.classList.add('collapsed');
+ const toggleBtn = document.querySelector('#thinkingToggle .toggle-btn i');
+ if (toggleBtn) {
+ toggleBtn.className = 'fas fa-chevron-down';
+ }
+
+ // 添加明确的提示
+ window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success');
+
+ // 确保结果内容可见
+ this.responseContent.style.display = 'block';
+
+ // 滚动到结果内容
+ this.responseContent.scrollIntoView({ behavior: 'smooth' });
break;
case 'error':
@@ -320,7 +433,7 @@ class SnapSolver {
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
- window.showToast('Analysis failed: ' + errorMessage, 'error');
+ window.uiManager.showToast('Analysis failed: ' + errorMessage, 'error');
break;
default:
@@ -330,10 +443,86 @@ class SnapSolver {
this.setElementContent(this.responseContent, currentText + '\nError: ' + data.error);
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
- window.showToast('Unknown error occurred', 'error');
+ window.uiManager.showToast('Unknown error occurred', 'error');
}
}
});
+
+ // 接收到thinking数据时
+ this.socket.on('thinking', (data) => {
+ console.log('收到思考过程数据');
+
+ // 显示思考区域
+ this.thinkingSection.classList.remove('hidden');
+ this.thinkingContent.textContent = data.thinking;
+
+ // 记住用户的展开/折叠状态
+ const wasExpanded = this.thinkingContent.classList.contains('expanded');
+
+ // 如果之前没有设置状态,默认为折叠
+ if (!wasExpanded && !this.thinkingContent.classList.contains('collapsed')) {
+ this.thinkingContent.classList.add('collapsed');
+ const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+ }
+ });
+
+ // 思考过程完成
+ this.socket.on('thinking_complete', (data) => {
+ console.log('思考过程完成');
+ this.thinkingSection.classList.remove('hidden');
+ this.thinkingContent.textContent = data.thinking;
+
+ // 确保图标正确显示
+ const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+
+ // 添加提示信息
+ const toast = window.uiManager.showToast('思考过程已完成,点击标题可查看详细思考过程', 'success');
+ toast.style.zIndex = '1000';
+ });
+
+ // 分析完成
+ this.socket.on('analysis_complete', (data) => {
+ console.log('分析完成,接收到结果');
+ this.updateStatusLight('completed');
+ this.enableInterface();
+
+ // 显示分析结果
+ if (this.responseContent) {
+ this.responseContent.innerHTML = data.response;
+ this.responseContent.style.display = 'block';
+
+ // 滚动到结果区域
+ setTimeout(() => {
+ this.responseContent.scrollIntoView({ behavior: 'smooth' });
+ }, 200);
+ }
+
+ // 添加到历史记录
+ this.addToHistory(this.croppedImage, data.response, data.thinking);
+
+ // 确保思考部分完全显示(如果有的话)
+ if (data.thinking && this.thinkingSection && this.thinkingContent) {
+ this.thinkingSection.classList.remove('hidden');
+ this.thinkingContent.textContent = data.thinking;
+
+ // 确保初始状态为折叠
+ this.thinkingContent.classList.remove('expanded');
+ this.thinkingContent.classList.add('collapsed');
+ const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+
+ // 弹出提示
+ window.uiManager.showToast('分析完成,可点击"AI思考过程"查看详细思考内容', 'success');
+ }
+ });
}
// 新方法:安全设置DOM内容的方法(替代updateElementContent)
@@ -379,30 +568,14 @@ class SnapSolver {
this.cropper = new Cropper(clonedImage, {
viewMode: 1,
- dragMode: 'crop',
- autoCropArea: 0,
- restore: false,
+ dragMode: 'move',
+ aspectRatio: NaN,
modal: true,
- guides: true,
- highlight: true,
- cropBoxMovable: true,
- cropBoxResizable: true,
- toggleDragModeOnDblclick: false,
- minCropBoxWidth: 50,
- minCropBoxHeight: 50,
- background: true,
- responsive: true,
- checkOrientation: true,
- ready: function() {
- // Use the stored reference to this
- if (self.cropper) {
- self.cropper.crop();
- }
- }
+ background: true
});
} catch (error) {
- console.error('Error initializing cropper:', error);
- window.showToast('Failed to initialize cropper', 'error');
+ console.error('Failed to initialize cropper', error);
+ window.uiManager.showToast('裁剪器初始化失败', 'error');
// 确保在出错时关闭裁剪界面
if (this.cropContainer) {
@@ -487,92 +660,11 @@ class SnapSolver {
}
updateHistoryPanel() {
- const historyContent = document.querySelector('.history-content');
- if (!historyContent) return;
-
- const historyJson = localStorage.getItem('snapHistory') || '[]';
- const history = JSON.parse(historyJson);
-
- if (history.length === 0) {
- historyContent.innerHTML = `
-
- `;
- return;
+ // 如果历史面板存在,更新其内容
+ if (this.historyContent) {
+ // 这里可以实现历史记录的加载和显示
+ // 暂时留空,后续可以实现
}
-
- const historyItems = history.map(item => {
- const date = new Date(item.timestamp);
- const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
- const hasResponse = item.response ? 'true' : 'false';
-
- // 检查图像是否为有效的数据URL
- let imageHtml = '';
- if (this.isValidImageDataUrl(item.image)) {
- // 有效的图像数据URL
- imageHtml = `
`;
- } else {
- // 图像已被优化或不存在,显示占位符
- imageHtml = `
-
- 图片已优化
-
`;
- }
-
- return `
-
- `;
- }).join('');
-
- historyContent.innerHTML = historyItems;
-
- // Add click event listeners for history items
- document.querySelectorAll('.history-item').forEach(item => {
- item.addEventListener('click', () => {
- const historyItem = history.find(h => h.id === parseInt(item.dataset.id));
- if (historyItem) {
- // 检查图像是否为有效的数据URL
- if (this.isValidImageDataUrl(historyItem.image)) {
- // 有效的图像数据
- window.app.screenshotImg.src = historyItem.image;
- window.app.imagePreview.classList.remove('hidden');
- } else {
- // 图像已优化或不存在,显示占位符图像
- window.app.screenshotImg.src = this.getPlaceholderImageUrl();
- window.app.imagePreview.classList.remove('hidden');
- }
-
- document.getElementById('historyPanel').classList.add('hidden');
- window.app.cropBtn.classList.add('hidden');
- window.app.captureBtn.classList.add('hidden');
- window.app.sendToClaudeBtn.classList.add('hidden');
- window.app.extractTextBtn.classList.add('hidden');
-
- // Set response content
- if (historyItem.response) {
- window.app.claudePanel.classList.remove('hidden');
- window.app.responseContent.textContent = historyItem.response;
- }
-
- // Set thinking content if available
- if (historyItem.thinking) {
- window.app.thinkingSection.classList.remove('hidden');
- window.app.thinkingContent.textContent = historyItem.thinking;
- } else {
- window.app.thinkingSection.classList.add('hidden');
- }
- }
- });
- });
}
setupEventListeners() {
@@ -584,21 +676,19 @@ class SnapSolver {
}
setupCaptureEvents() {
- // Capture button
- this.captureBtn.addEventListener('click', async () => {
- if (!this.socket || !this.socket.connected) {
- window.showToast('Not connected to server', 'error');
- return;
- }
-
+ // 截图按钮
+ this.captureBtn.addEventListener('click', () => {
+ if (!this.checkConnectionBeforeAction()) return;
+
try {
- this.captureBtn.disabled = true;
- this.captureBtn.innerHTML = 'Capturing...';
- this.socket.emit('request_screenshot');
+ this.captureBtn.disabled = true; // 禁用按钮防止重复点击
+ this.captureBtn.innerHTML = '正在截图...';
+ this.socket.emit('capture_screenshot', {});
} catch (error) {
- window.showToast('Error requesting screenshot: ' + error.message, 'error');
+ console.error('Error starting capture:', error);
+ window.uiManager.showToast('启动截图失败', 'error');
this.captureBtn.disabled = false;
- this.captureBtn.innerHTML = 'Capture';
+ this.captureBtn.innerHTML = '截取屏幕';
}
});
}
@@ -606,6 +696,8 @@ class SnapSolver {
setupCropEvents() {
// Crop button
this.cropBtn.addEventListener('click', () => {
+ if (!this.checkConnectionBeforeAction()) return;
+
if (this.screenshotImg.src) {
this.initializeCropper();
}
@@ -613,6 +705,8 @@ class SnapSolver {
// Crop confirm button
document.getElementById('cropConfirm').addEventListener('click', () => {
+ if (!this.checkConnectionBeforeAction()) return;
+
if (this.cropper) {
try {
console.log('Starting crop operation...');
@@ -675,14 +769,14 @@ class SnapSolver {
this.cropBtn.classList.remove('hidden');
this.sendToClaudeBtn.classList.remove('hidden');
this.extractTextBtn.classList.remove('hidden');
- window.showToast('Image cropped successfully');
+ window.uiManager.showToast('Cropping successful');
} catch (error) {
console.error('Cropping error details:', {
message: error.message,
stack: error.stack,
cropperState: this.cropper ? 'initialized' : 'not initialized'
});
- window.showToast(error.message || 'Error while cropping image', 'error');
+ window.uiManager.showToast(error.message || '裁剪图像时出错', 'error');
} finally {
// Always clean up the cropper instance
if (this.cropper) {
@@ -709,66 +803,75 @@ class SnapSolver {
setupAnalysisEvents() {
// Extract Text button
this.extractTextBtn.addEventListener('click', () => {
+ if (!this.checkConnectionBeforeAction()) return;
+
if (!this.croppedImage) {
- window.showToast('Please crop the image first', 'error');
+ window.uiManager.showToast('请先裁剪图片', 'error');
return;
}
this.extractTextBtn.disabled = true;
this.sendExtractedTextBtn.disabled = true; // Disable the send button while extracting
- this.extractTextBtn.innerHTML = 'Extracting...';
+ this.extractTextBtn.innerHTML = '提取中...';
const settings = window.settingsManager.getSettings();
const mathpixAppId = settings.mathpixAppId;
const mathpixAppKey = settings.mathpixAppKey;
if (!mathpixAppId || !mathpixAppKey) {
- window.showToast('Please enter Mathpix credentials in settings', 'error');
+ window.uiManager.showToast('请在设置中输入Mathpix API凭据', 'error');
document.getElementById('settingsPanel').classList.remove('hidden');
this.extractTextBtn.disabled = false;
- this.extractTextBtn.innerHTML = 'Extract Text';
+ this.extractTextBtn.innerHTML = '提取文本';
return;
}
// Show text editor and prepare UI
this.textEditor.classList.remove('hidden');
if (this.extractedText) {
- this.extractedText.value = 'Extracting text...';
+ this.extractedText.value = '正在提取文本...';
this.extractedText.disabled = true;
}
try {
- // 设置超时时间(10秒)以避免长时间无响应
+ // 设置超时时间(15秒)以避免长时间无响应
this.emitTimeout = setTimeout(() => {
- window.showToast('文本提取超时,请重试或手动输入文本', 'error');
+ window.uiManager.showToast('文本提取超时,请重试或手动输入文本', 'error');
this.extractTextBtn.disabled = false;
- this.extractTextBtn.innerHTML = 'Extract Text';
+ this.extractTextBtn.innerHTML = '提取文本';
this.extractedText.disabled = false;
- }, 10000);
+ this.sendExtractedTextBtn.disabled = false;
+ }, 15000);
this.socket.emit('extract_text', {
image: this.croppedImage.split(',')[1],
settings: {
mathpixApiKey: `${mathpixAppId}:${mathpixAppKey}`
}
- }, (ackResponse) => {
- // 如果服务器确认收到请求,清除超时
- clearTimeout(this.emitTimeout);
+ });
+
+ // 监听服务器确认请求的响应
+ this.socket.once('request_acknowledged', (ackResponse) => {
console.log('服务器确认收到文本提取请求', ackResponse);
});
} catch (error) {
- window.showToast('Failed to extract text: ' + error.message, 'error');
+ window.uiManager.showToast('提取文本失败: ' + error.message, 'error');
this.extractTextBtn.disabled = false;
- this.sendExtractedTextBtn.disabled = false; // Re-enable send button on error
- this.extractTextBtn.innerHTML = 'Extract Text';
+ this.sendExtractedTextBtn.disabled = false;
+ this.extractTextBtn.innerHTML = '提取文本';
+ if (this.extractedText) {
+ this.extractedText.disabled = false;
+ }
}
});
// Send Extracted Text button
this.sendExtractedTextBtn.addEventListener('click', () => {
+ if (!this.checkConnectionBeforeAction()) return;
+
const text = this.extractedText.value.trim();
if (!text) {
- window.showToast('Please enter some text', 'error');
+ window.uiManager.showToast('请输入一些文本', 'error');
return;
}
@@ -796,67 +899,49 @@ class SnapSolver {
} catch (error) {
this.responseContent.textContent = 'Error: Failed to send text for analysis - ' + error.message;
this.sendExtractedTextBtn.disabled = false;
- window.showToast('Failed to send text for analysis', 'error');
+ window.uiManager.showToast('发送文本进行分析失败', 'error');
}
});
// Send to Claude button
this.sendToClaudeBtn.addEventListener('click', () => {
- if (!this.croppedImage) {
- window.showToast('Please crop the image first', 'error');
- return;
- }
-
- const settings = window.settingsManager.getSettings();
- const apiKeys = {};
- Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => {
- if (input.value) {
- apiKeys[model] = input.value;
- }
- });
+ if (!this.checkConnectionBeforeAction()) return;
- this.claudePanel.classList.remove('hidden');
- this.responseContent.textContent = '';
- this.sendToClaudeBtn.disabled = true;
-
- try {
- // 添加图片大小检查和压缩
- const base64Data = this.croppedImage.split(',')[1];
- // 计算图片大小(以字节为单位)
- const imageSize = Math.ceil((base64Data.length * 3) / 4);
- console.log(`图片大小: ${imageSize / 1024} KB`);
-
- // 如果图片大小超过8MB(WebSocket默认限制),则显示错误
- if (imageSize > 8 * 1024 * 1024) {
- window.showToast('图片太大,请裁剪更小的区域或使用文本提取功能', 'error');
- this.sendToClaudeBtn.disabled = false;
- return;
+ if (this.croppedImage) {
+ try {
+ this.sendImageToClaude(this.croppedImage);
+ } catch (error) {
+ console.error('Error:', error);
+ window.uiManager.showToast('发送图片失败: ' + error.message, 'error');
}
-
- // 设置超时时间(10秒)以避免长时间无响应
- this.emitTimeout = setTimeout(() => {
- window.showToast('发送图片超时,请重试或使用文本提取功能', 'error');
- this.sendToClaudeBtn.disabled = false;
- }, 10000);
-
- this.socket.emit('analyze_image', {
- image: base64Data,
- settings: {
- ...settings,
- api_keys: apiKeys,
- model: settings.model || 'claude-3-7-sonnet-20250219',
- }
- }, (ackResponse) => {
- // 如果服务器确认收到请求,清除超时
- clearTimeout(this.emitTimeout);
- console.log('服务器确认收到图片分析请求', ackResponse);
- });
- } catch (error) {
- this.responseContent.textContent = 'Error: Failed to send image for analysis - ' + error.message;
- this.sendToClaudeBtn.disabled = false;
- window.showToast('Failed to send image for analysis', 'error');
+ } else {
+ window.uiManager.showToast('请先裁剪图片', 'error');
}
});
+
+ // Handle Claude panel close button
+ const closeClaudePanel = document.getElementById('closeClaudePanel');
+ if (closeClaudePanel) {
+ closeClaudePanel.addEventListener('click', () => {
+ this.claudePanel.classList.add('hidden');
+
+ // 如果图像预览也被隐藏,显示空状态
+ if (this.imagePreview.classList.contains('hidden')) {
+ this.emptyState.classList.remove('hidden');
+ }
+
+ // 重置状态指示灯
+ this.updateStatusLight('');
+
+ // 清空响应内容,准备下一次分析
+ this.responseContent.innerHTML = '';
+
+ // 隐藏思考部分
+ this.thinkingSection.classList.add('hidden');
+ this.thinkingContent.innerHTML = '';
+ this.thinkingContent.className = 'thinking-content collapsed';
+ });
+ }
}
setupKeyboardShortcuts() {
@@ -876,38 +961,238 @@ class SnapSolver {
}
setupThinkingToggle() {
- // Toggle thinking content visibility
- if (this.thinkingToggle) {
- this.thinkingToggle.addEventListener('click', () => {
- const isCollapsed = this.thinkingContent.classList.contains('collapsed');
-
- if (isCollapsed) {
- this.thinkingContent.classList.remove('collapsed');
- this.thinkingContent.classList.add('expanded');
- this.thinkingToggle.classList.add('thinking-toggle-active');
- const icon = this.thinkingToggle.querySelector('.toggle-btn i');
- if (icon) {
- icon.classList.remove('fa-chevron-down');
- icon.classList.add('fa-chevron-up');
- }
- } else {
- this.thinkingContent.classList.add('collapsed');
- this.thinkingContent.classList.remove('expanded');
- this.thinkingToggle.classList.remove('thinking-toggle-active');
- const icon = this.thinkingToggle.querySelector('.toggle-btn i');
- if (icon) {
- icon.classList.remove('fa-chevron-up');
- icon.classList.add('fa-chevron-down');
- }
- }
- });
+ // 确保正确获取DOM元素
+ const thinkingSection = document.getElementById('thinkingSection');
+ const thinkingToggle = document.getElementById('thinkingToggle');
+ const thinkingContent = document.getElementById('thinkingContent');
+
+ if (!thinkingToggle || !thinkingContent) {
+ console.error('思考切换组件未找到必要的DOM元素');
+ return;
}
+
+ // 存储DOM引用
+ this.thinkingSection = thinkingSection;
+ this.thinkingToggle = thinkingToggle;
+ this.thinkingContent = thinkingContent;
+
+ // 直接使用函数,确保作用域正确
+ thinkingToggle.onclick = () => {
+ console.log('点击了思考标题');
+
+ // 先移除两个类,然后添加正确的类
+ const isExpanded = thinkingContent.classList.contains('expanded');
+ const toggleIcon = thinkingToggle.querySelector('.toggle-btn i');
+
+ // 从样式上清除当前状态
+ thinkingContent.classList.remove('expanded');
+ thinkingContent.classList.remove('collapsed');
+
+ if (isExpanded) {
+ console.log('折叠思考内容');
+ // 添加折叠状态
+ thinkingContent.classList.add('collapsed');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-down';
+ }
+ window.uiManager.showToast('思考内容已折叠', 'info');
+ } else {
+ console.log('展开思考内容');
+ // 添加展开状态
+ thinkingContent.classList.add('expanded');
+ if (toggleIcon) {
+ toggleIcon.className = 'fas fa-chevron-up';
+ }
+ window.uiManager.showToast('思考内容已展开', 'info');
+ }
+ };
+
+ console.log('思考切换组件初始化完成');
}
// 获取用于显示的图像URL,如果原始URL无效则返回占位符
getImageForDisplay(imageUrl) {
return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl();
}
+
+ sendImageToClaude(imageData) {
+ const settings = window.settingsManager.getSettings();
+
+ // 获取API密钥
+ const apiKeys = {};
+ Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => {
+ if (input.value) {
+ apiKeys[model] = input.value;
+ }
+ });
+
+ try {
+ // 处理图像数据,去除base64前缀
+ let processedImageData = imageData;
+ if (imageData.startsWith('data:')) {
+ // 分割数据URL,只保留base64部分
+ processedImageData = imageData.split(',')[1];
+ }
+
+ this.socket.emit('analyze_image', {
+ image: processedImageData,
+ settings: {
+ ...settings,
+ api_keys: apiKeys,
+ model: settings.model || 'claude-3-7-sonnet-20250219',
+ }
+ });
+
+ // 显示Claude分析面板
+ this.claudePanel.classList.remove('hidden');
+ this.responseContent.textContent = '';
+ } catch (error) {
+ this.responseContent.textContent = 'Error: ' + error.message;
+ window.uiManager.showToast('发送图片分析失败', 'error');
+ }
+ }
+
+ initialize() {
+ console.log('Initializing SnapSolver...');
+
+ // 初始化managers
+ window.uiManager = new UIManager();
+ window.settingsManager = new SettingsManager();
+ window.app = this; // 便于从其他地方访问
+
+ // 建立与服务器的连接
+ this.connectToServer();
+
+ // 初始化UI元素和事件处理
+ this.initializeElements();
+ this.setupSocketEventHandlers();
+ this.setupCaptureEvents();
+ this.setupCropEvents();
+ this.setupAnalysisEvents();
+ this.setupKeyboardShortcuts();
+
+ // 确保思考切换功能正确初始化
+ this.setupThinkingToggle();
+
+ this.setupEventListeners();
+ this.setupAutoScroll();
+
+ // 监听窗口大小变化,调整界面
+ window.addEventListener('resize', this.handleResize.bind(this));
+
+ // 点击文档任何地方隐藏历史面板
+ document.addEventListener('click', (e) => {
+ if (this.historyPanel &&
+ !this.historyPanel.contains(e.target) &&
+ !e.target.closest('#historyToggle')) {
+ this.historyPanel.classList.add('hidden');
+ }
+ });
+
+ // 设置默认UI状态
+ this.enableInterface();
+
+ // 初始化历史
+ this.updateHistoryPanel();
+
+ console.log('SnapSolver initialization complete');
+ }
+
+ handleResize() {
+ // 如果裁剪器存在,需要调整其大小和位置
+ if (this.cropper) {
+ this.cropper.resize();
+ }
+
+ // 可以在这里添加其他响应式UI调整
+ }
+
+ enableInterface() {
+ // 启用主要界面元素
+ if (this.captureBtn) {
+ this.captureBtn.disabled = false;
+ this.captureBtn.innerHTML = '截图';
+ }
+
+ // 显示默认的空白状态
+ if (this.emptyState && this.imagePreview) {
+ if (!this.originalImage) {
+ this.emptyState.classList.remove('hidden');
+ this.imagePreview.classList.add('hidden');
+ } else {
+ this.emptyState.classList.add('hidden');
+ this.imagePreview.classList.remove('hidden');
+ }
+ }
+
+ console.log('Interface enabled');
+ }
+
+ connectToServer() {
+ console.log('Connecting to server...');
+
+ // 创建Socket.IO连接
+ this.socket = io({
+ reconnectionAttempts: 5,
+ reconnectionDelay: 1000,
+ timeout: 20000
+ });
+
+ // 连接事件处理
+ this.socket.on('connect', () => {
+ console.log('Connected to server');
+ this.updateConnectionStatus('已连接', true);
+
+ // 连接后启用界面
+ this.enableInterface();
+ });
+
+ this.socket.on('disconnect', () => {
+ console.log('Disconnected from server');
+ this.updateConnectionStatus('已断开', false);
+
+ // 断开连接时禁用界面
+ if (this.captureBtn) {
+ this.captureBtn.disabled = true;
+ }
+ });
+
+ this.socket.on('connect_error', (error) => {
+ console.error('Connection error:', error);
+ this.updateConnectionStatus('连接错误', false);
+ });
+
+ this.socket.on('reconnect_attempt', (attemptNumber) => {
+ console.log(`Reconnection attempt ${attemptNumber}`);
+ this.updateConnectionStatus('正在重连...', false);
+ });
+
+ this.socket.on('reconnect', () => {
+ console.log('Reconnected to server');
+ this.updateConnectionStatus('已重连', true);
+
+ // 重连后启用界面
+ this.enableInterface();
+ });
+
+ this.socket.on('reconnect_failed', () => {
+ console.error('Failed to reconnect');
+ this.updateConnectionStatus('重连失败', false);
+ window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error');
+ });
+ }
+
+ isConnected() {
+ return this.connectionStatus && this.connectionStatus.classList.contains('connected');
+ }
+
+ checkConnectionBeforeAction(action) {
+ if (!this.isConnected()) {
+ window.uiManager.showToast('未连接到服务器,请等待连接建立后再试', 'error');
+ return false;
+ }
+ return true;
+ }
}
// Initialize the application when the DOM is loaded
diff --git a/static/js/settings.js b/static/js/settings.js
index 35db9bc..d393e0e 100644
--- a/static/js/settings.js
+++ b/static/js/settings.js
@@ -136,11 +136,20 @@ class SettingsManager {
}
getSettings() {
+ const language = this.languageInput.value || '中文';
+ const basePrompt = this.systemPromptInput.value || '';
+
+ // 检查系统提示词是否已包含语言设置
+ let systemPrompt = basePrompt;
+ if (!basePrompt.includes('Please respond in') && !basePrompt.includes('请用') && !basePrompt.includes('使用')) {
+ systemPrompt = `${basePrompt}\n\n请务必使用${language}回答。`;
+ }
+
return {
model: this.modelSelect.value,
temperature: this.temperatureInput.value,
- language: this.languageInput.value,
- systemPrompt: this.systemPromptInput.value + ` Please respond in ${this.languageInput.value}.`,
+ language: language,
+ systemPrompt: systemPrompt,
proxyEnabled: this.proxyEnabledInput.checked,
proxyHost: this.proxyHostInput.value,
proxyPort: this.proxyPortInput.value,
diff --git a/static/js/ui.js b/static/js/ui.js
index 8959428..afb82c0 100644
--- a/static/js/ui.js
+++ b/static/js/ui.js
@@ -47,6 +47,16 @@ class UIManager {
}
showToast(message, type = 'success') {
+ // 检查是否已经存在相同内容的提示
+ const existingToasts = this.toastContainer.querySelectorAll('.toast');
+ for (const existingToast of existingToasts) {
+ const existingMessage = existingToast.querySelector('span').textContent;
+ if (existingMessage === message) {
+ // 已经存在相同的提示,不再创建新的
+ return;
+ }
+ }
+
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
@@ -55,10 +65,13 @@ class UIManager {
`;
this.toastContainer.appendChild(toast);
+ // 为不同类型的提示设置不同的显示时间
+ const displayTime = message === '截图成功' ? 1500 : 3000;
+
setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
- }, 3000);
+ }, displayTime);
}
closeAllPanels() {
diff --git a/static/style.css b/static/style.css
index 7fa580a..a03c0da 100644
--- a/static/style.css
+++ b/static/style.css
@@ -12,17 +12,22 @@
--shadow-color: rgba(0, 0, 0, 0.1);
--error-color: #f44336;
--success-color: #4CAF50;
- --primary: #4a6cf7;
- --secondary: #6c757d;
- --danger: #f44336;
+ --primary: #2196f3;
+ --primary-rgb: 33, 150, 243;
+ --primary-light: #bbdefb;
+ --secondary: #ff9800;
--success: #4caf50;
+ --danger: #f44336;
+ --warning: #ff9800;
+ --info: #2196f3;
--background: #f5f7fb;
--surface: #ffffff;
+ --surface-rgb: 255, 255, 255;
--surface-alt: #f0f4f8;
+ --surface-alt-rgb: 248, 249, 250;
--text: #2c3e50;
- --text-secondary: #505a66;
+ --text-tertiary: #9e9e9e;
--shadow-color: rgba(0, 0, 0, 0.1);
- --border-color: #e0e0e0;
--hover-color: #e9ecef;
--accent: #4a6cf7;
--placeholder: #a0a0a0;
@@ -36,17 +41,22 @@
--secondary-dark: #66BB6A;
--background: #121212;
--surface: #1E1E1E;
+ --surface-rgb: 30, 30, 30;
--text-primary: #FFFFFF;
--text-secondary: #B0B0B0;
+ --text-tertiary: #909090;
--border-color: #333333;
--shadow-color: rgba(0, 0, 0, 0.3);
- --primary: #4a6cf7;
- --secondary: #6c757d;
+ --primary: #64b5f6;
+ --primary-rgb: 100, 181, 246;
+ --primary-light: #bbdefb;
+ --secondary: #ff9800;
--danger: #f44336;
--success: #4caf50;
--background: #1a1a2e;
--surface: #272741;
--surface-alt: #202035;
+ --surface-alt-rgb: 37, 37, 37;
--text: #f0f0f0;
--text-secondary: #a0a0a0;
--shadow-color: rgba(0, 0, 0, 0.4);
@@ -78,17 +88,22 @@ body {
display: flex;
flex-direction: column;
min-height: 100vh;
+ background-color: var(--background);
+ position: relative;
}
/* Header Styles */
.app-header {
background-color: var(--surface);
- padding: 1rem;
- box-shadow: 0 2px 4px var(--shadow-color);
+ padding: 1rem 1.5rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
display: flex;
justify-content: space-between;
align-items: center;
z-index: 100;
+ position: sticky;
+ top: 0;
+ border-bottom: 1px solid var(--border-color);
}
.header-left {
@@ -99,8 +114,11 @@ body {
.header-left h1 {
font-size: 1.5rem;
- color: var(--primary-color);
+ color: var(--primary);
margin: 0;
+ font-weight: 600;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+ letter-spacing: -0.5px;
}
.connection-status {
@@ -109,48 +127,93 @@ body {
gap: 1rem;
}
-.connection-form {
- display: flex;
- gap: 0.5rem;
-}
-
.status {
padding: 0.4rem 0.8rem;
border-radius: 1rem;
font-size: 0.875rem;
- font-weight: 500;
+ font-weight: 600;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ transition: all 0.3s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+}
+
+.status::before {
+ content: "";
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
}
.status.connected {
- background-color: var(--success-color);
- color: white;
+ background-color: rgba(76, 175, 80, 0.15);
+ color: var(--success);
+ border: 1px solid rgba(76, 175, 80, 0.3);
+}
+
+.status.connected::before {
+ background-color: var(--success);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.3);
}
.status.disconnected {
- background-color: var(--error-color);
- color: white;
+ background-color: rgba(244, 67, 54, 0.15);
+ color: var(--danger);
+ border: 1px solid rgba(244, 67, 54, 0.3);
+}
+
+.status.disconnected::before {
+ background-color: var(--danger);
+ box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.3);
}
.header-right {
display: flex;
- gap: 0.5rem;
+ gap: 0.8rem;
+}
+
+.header-right .btn-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--surface-alt);
+ color: var(--text-secondary);
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
+}
+
+.header-right .btn-icon:hover {
+ transform: translateY(-2px);
+ background-color: var(--hover-color);
+ color: var(--primary);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+}
+
+.header-right .btn-icon:active {
+ transform: translateY(0);
}
/* Main Content */
.app-main {
flex: 1;
display: flex;
- padding: 1rem;
- gap: 1rem;
+ padding: 1.5rem;
+ gap: 1.5rem;
position: relative;
overflow: hidden;
+ background-color: var(--background);
}
.content-panel {
flex: 1;
display: flex;
flex-direction: column;
- gap: 1rem;
+ gap: 1.5rem;
max-width: 1200px;
margin: 0 auto;
width: 100%;
@@ -159,30 +222,34 @@ body {
/* Capture Section */
.capture-section {
background-color: var(--surface);
- border-radius: 0.5rem;
- box-shadow: 0 2px 4px var(--shadow-color);
- padding: 1rem;
+ border-radius: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
+ padding: 1.5rem;
+ transition: all 0.3s ease;
+ border: 1px solid var(--border-color);
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 1rem;
- padding: 0.5rem;
- background-color: var(--surface);
- border-radius: 0.5rem;
+ margin-bottom: 1.5rem;
+ padding: 0.75rem 1rem;
+ background-color: var(--surface-alt);
+ border-radius: 0.75rem;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.03);
}
.toolbar-buttons {
display: flex;
justify-content: flex-start;
align-items: center;
+ width: 100%;
}
.button-group {
display: flex;
- gap: 0.5rem;
+ gap: 1rem;
align-items: center;
}
@@ -190,62 +257,125 @@ body {
display: flex;
flex-direction: column;
align-items: center;
- gap: 1rem;
- margin-top: 1rem;
- padding: 1rem;
+ gap: 1.5rem;
+ margin-top: 1.5rem;
+ padding: 1.5rem;
+ background-color: var(--surface-alt);
+ border-radius: 0.75rem;
+ transition: all 0.3s ease;
}
.analysis-button .button-group {
display: flex;
- gap: 0.5rem;
+ gap: 1rem;
width: 100%;
- max-width: 400px;
+ max-width: 500px;
justify-content: center;
}
.text-editor {
width: 100%;
- max-width: 600px;
+ max-width: 700px;
display: flex;
flex-direction: column;
gap: 1rem;
+ background-color: var(--surface);
+ padding: 1.5rem;
+ border-radius: 0.75rem;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
+ border: 1px solid var(--border-color);
+ transition: all 0.3s ease;
+}
+
+.text-editor:hover {
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
}
.text-editor textarea {
width: 100%;
- min-height: 120px;
- padding: 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
+ min-height: 150px;
+ padding: 1rem;
+ border: 1.5px solid var(--border-color);
+ border-radius: 0.5rem;
background-color: var(--background);
color: var(--text-primary);
- font-size: 0.9375rem;
+ font-size: 1rem;
resize: vertical;
+ transition: all 0.2s ease;
+ line-height: 1.6;
}
.text-editor textarea:focus {
outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15);
}
.text-editor button {
align-self: flex-end;
}
+.text-format-controls {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-top: 0.5rem;
+}
+
+.send-text-group {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-left: auto;
+}
+
+.confidence-indicator {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 0.75rem;
+ border-radius: 0.5rem;
+ background-color: var(--surface-alt);
+ color: var(--success);
+ font-size: 0.875rem;
+ font-weight: 500;
+}
+
+.confidence-indicator i {
+ font-size: 1rem;
+}
+
+.confidence-value {
+ font-weight: 600;
+}
+
.image-preview {
position: relative;
- border-radius: 0.5rem;
+ border-radius: 1rem;
overflow: hidden;
- background-color: var(--background);
+ background-color: var(--surface-alt);
margin: 0;
- padding: 1rem;
+ padding: 1.5rem;
+ text-align: center;
+ transition: all 0.3s ease;
+ border: 1px dashed var(--border-color);
}
.image-container {
display: inline-block;
position: relative;
margin: 0 auto;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1);
+ transition: all 0.3s ease;
+ max-width: 90%;
+}
+
+.image-container:hover {
+ transform: scale(1.01);
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.15);
}
#screenshotImg {
@@ -253,7 +383,8 @@ body {
width: auto;
height: auto;
max-width: 100%;
- border-radius: 0.5rem;
+ border-radius: 0.75rem;
+ transition: all 0.3s ease;
}
@media (max-width: 768px) {
@@ -266,30 +397,50 @@ body {
/* Claude Panel */
.claude-panel {
background-color: var(--surface);
- border-radius: 0.5rem;
- box-shadow: 0 2px 4px var(--shadow-color);
- padding: 1rem;
- flex: 1;
+ border-radius: 1rem;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
display: flex;
flex-direction: column;
+ height: auto;
+ min-height: 300px;
+ max-height: 90vh;
+ border: 1px solid var(--border-color);
+ transition: all 0.3s ease;
+ overflow: hidden;
+ width: 100%;
+}
+
+.claude-panel:not(.hidden) {
+ animation: panel-slide-in 0.4s cubic-bezier(0.19, 1, 0.22, 1) forwards;
+}
+
+@keyframes panel-slide-in {
+ 0% {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+ 100% {
+ opacity: 1;
+ transform: translateY(0);
+ }
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 1rem;
+ padding: 1.25rem;
+ border-bottom: 1px solid var(--border-color);
+ background-color: var(--surface);
+ position: sticky;
+ top: 0;
+ z-index: 5;
}
.header-title {
display: flex;
align-items: center;
- gap: 0.75rem;
-}
-
-.panel-header h2 {
- font-size: 1.25rem;
- color: var(--text-primary);
+ gap: 1rem;
}
.analysis-status {
@@ -303,42 +454,55 @@ body {
border-radius: 50%;
background-color: var(--text-secondary);
transition: background-color 0.3s ease;
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.05);
}
.status-light.processing {
background-color: #ffd700;
animation: pulse 1.5s infinite;
+ box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2);
}
.status-light.completed {
- background-color: var(--success-color);
+ background-color: var(--success);
+ box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
.status-light.error {
- background-color: var(--error-color);
+ background-color: var(--danger);
+ box-shadow: 0 0 0 2px rgba(244, 67, 54, 0.2);
}
@keyframes pulse {
0% {
opacity: 1;
+ box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2);
}
50% {
- opacity: 0.5;
+ opacity: 0.6;
+ box-shadow: 0 0 0 4px rgba(255, 215, 0, 0.4);
}
100% {
opacity: 1;
+ box-shadow: 0 0 0 2px rgba(255, 215, 0, 0.2);
}
}
.response-content {
- flex: 1;
- overflow-y: auto;
- padding: 1rem;
- background-color: var(--background);
- border-radius: 0.5rem;
- white-space: pre-wrap;
- font-size: 0.9375rem;
+ padding: 1.5rem;
+ background-color: #fff;
+ border-radius: 6px;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
line-height: 1.6;
+ white-space: pre-wrap;
+ overflow-wrap: break-word;
+ margin-top: 1rem;
+ font-size: 1rem;
+}
+
+[data-theme="dark"] .response-content {
+ background-color: rgba(var(--surface-rgb), 0.7);
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
/* Settings Panel */
@@ -350,12 +514,13 @@ body {
width: 400px;
max-width: 100vw;
background-color: var(--surface);
- box-shadow: -2px 0 4px var(--shadow-color);
+ box-shadow: -8px 0 20px var(--shadow-color);
z-index: 1000;
transform: translateX(100%);
- transition: transform 0.3s ease;
+ transition: transform 0.3s cubic-bezier(0.19, 1, 0.22, 1);
display: flex;
flex-direction: column;
+ border-left: 1px solid var(--border-color);
}
.settings-panel:not(.hidden) {
@@ -365,29 +530,63 @@ body {
.settings-content {
flex: 1;
overflow-y: auto;
- padding: 1rem;
+ padding: 1.5rem;
}
.settings-section {
- margin-bottom: 2rem;
+ margin-bottom: 2.5rem;
+ background-color: var(--surface-alt);
+ border-radius: 0.75rem;
+ padding: 1.5rem;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
+}
+
+.settings-section:hover {
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
+ transform: translateY(-2px);
}
.settings-section h3 {
color: var(--text-primary);
- margin-bottom: 1rem;
- font-size: 1.1rem;
+ margin-bottom: 1.5rem;
+ font-size: 1.15rem;
+ font-weight: 600;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.settings-section h3::before {
+ content: "";
+ display: inline-block;
+ width: 4px;
+ height: 18px;
+ background-color: var(--primary);
+ border-radius: 2px;
}
/* Form Elements */
.setting-group {
- margin-bottom: 1rem;
+ margin-bottom: 1.5rem;
+ position: relative;
+}
+
+.setting-group:last-child {
+ margin-bottom: 0;
}
.setting-group label {
display: block;
- margin-bottom: 0.5rem;
+ margin-bottom: 0.75rem;
color: var(--text-secondary);
font-size: 0.875rem;
+ font-weight: 500;
+ transition: color 0.2s ease;
+}
+
+.setting-group:hover label {
+ color: var(--primary);
}
input[type="text"],
@@ -396,21 +595,23 @@ input[type="number"],
select,
textarea {
width: 100%;
- padding: 0.75rem;
- border: 1px solid var(--border-color);
- border-radius: 0.375rem;
+ padding: 0.85rem 1rem;
+ border: 1.5px solid var(--border-color);
+ border-radius: 0.5rem;
background-color: var(--background);
color: var(--text-primary);
font-size: 0.9375rem;
- transition: border-color 0.3s, box-shadow 0.3s;
+ transition: all 0.2s ease-in-out;
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
input:focus,
select:focus,
textarea:focus {
outline: none;
- border-color: var(--primary-color);
- box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.1);
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15);
+ transform: translateY(-1px);
}
.input-group {
@@ -420,12 +621,19 @@ textarea:focus {
}
.input-group input {
- padding-right: 2.5rem;
+ padding-right: 2.75rem;
}
.input-group .btn-icon {
position: absolute;
- right: 0.5rem;
+ right: 0.75rem;
+ background-color: transparent;
+ transition: all 0.2s ease;
+}
+
+.input-group .btn-icon:hover {
+ color: var(--primary);
+ transform: scale(1.1);
}
.range-group {
@@ -436,28 +644,83 @@ textarea:focus {
input[type="range"] {
flex: 1;
- height: 4px;
+ height: 6px;
-webkit-appearance: none;
- background: var(--primary-color);
- border-radius: 2px;
+ background: linear-gradient(to right, var(--primary) 0%, var(--primary) 70%, var(--border-color) 70%, var(--border-color) 100%);
+ border-radius: 3px;
+ cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
- width: 16px;
- height: 16px;
+ width: 18px;
+ height: 18px;
border-radius: 50%;
- background: var(--primary-color);
+ background: var(--primary);
+ box-shadow: 0 0 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer;
- border: 2px solid var(--surface);
- box-shadow: 0 1px 3px var(--shadow-color);
+}
+
+input[type="range"]::-webkit-slider-thumb:hover {
+ transform: scale(1.2);
+ box-shadow: 0 0 0 4px rgba(74, 108, 247, 0.2);
+}
+
+#temperatureValue {
+ min-width: 30px;
+ text-align: center;
+ font-weight: 600;
+ color: var(--primary);
}
.checkbox-label {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: 0.75rem;
cursor: pointer;
+ user-select: none;
+}
+
+.checkbox-label input[type="checkbox"] {
+ appearance: none;
+ width: 18px;
+ height: 18px;
+ border: 1.5px solid var(--border-color);
+ border-radius: 4px;
+ position: relative;
+ transition: all 0.2s ease;
+ background-color: var(--background);
+}
+
+.checkbox-label input[type="checkbox"]:checked {
+ background-color: var(--primary);
+ border-color: var(--primary);
+}
+
+.checkbox-label input[type="checkbox"]:checked::after {
+ content: "✓";
+ position: absolute;
+ color: white;
+ font-size: 14px;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+
+.checkbox-label input[type="checkbox"]:focus {
+ box-shadow: 0 0 0 3px rgba(74, 108, 247, 0.15);
+}
+
+.proxy-settings {
+ padding-top: 1rem;
+ margin-top: 0.5rem;
+ border-top: 1px dashed var(--border-color);
+ transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+#proxyEnabled:not(:checked) ~ #proxySettings {
+ opacity: 0.5;
}
/* Buttons */
@@ -466,51 +729,93 @@ input[type="range"]::-webkit-slider-thumb {
.btn-icon {
padding: 0.75rem 1.5rem;
border: none;
- border-radius: 0.375rem;
+ border-radius: 0.5rem;
font-size: 0.9375rem;
- font-weight: 500;
+ font-weight: 600;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
- transition: all 0.2s;
+ transition: all 0.2s ease;
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
+ position: relative;
+ overflow: hidden;
+}
+
+.btn-primary::after,
+.btn-secondary::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(255, 255, 255, 0.1);
+ transform: translateY(100%);
+ transition: transform 0.2s ease;
+}
+
+.btn-primary:hover::after,
+.btn-secondary:hover::after {
+ transform: translateY(0);
}
.btn-primary {
- background-color: var(--primary-color);
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
}
.btn-primary:hover {
- background-color: var(--primary-dark);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+}
+
+.btn-primary:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn-secondary {
- background-color: var(--background);
+ background-color: var(--surface-alt);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
- background-color: var(--border-color);
+ background-color: var(--hover-color);
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
+}
+
+.btn-secondary:active {
+ transform: translateY(0);
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.btn-icon {
padding: 0.5rem;
- border-radius: 0.375rem;
+ border-radius: 0.5rem;
background: transparent;
color: var(--text-secondary);
+ box-shadow: none;
}
.btn-icon:hover {
- background-color: var(--background);
- color: var(--text-primary);
+ background-color: var(--hover-color);
+ color: var(--primary);
+ transform: translateY(-2px);
+}
+
+.btn-icon:active {
+ transform: translateY(0);
}
button:disabled {
opacity: 0.6;
cursor: not-allowed;
+ transform: none !important;
+ box-shadow: none !important;
}
/* Floating Action Button */
@@ -542,9 +847,10 @@ button:disabled {
/* Toast Notifications */
.toast-container {
position: fixed;
- bottom: 2rem;
- left: 50%;
- transform: translateX(-50%);
+ top: 1rem;
+ right: 1rem;
+ left: auto;
+ transform: none;
z-index: 1000;
display: flex;
flex-direction: column;
@@ -555,7 +861,7 @@ button:disabled {
.toast {
background-color: var(--surface);
color: var(--text-primary);
- padding: 1rem 1.5rem;
+ padding: 0.75rem 1rem;
border-radius: 0.375rem;
box-shadow: 0 4px 6px var(--shadow-color);
display: flex;
@@ -563,6 +869,9 @@ button:disabled {
gap: 0.75rem;
pointer-events: auto;
animation: toast-in 0.3s ease;
+ max-width: 250px;
+ opacity: 0.9;
+ font-size: 0.9rem;
}
.toast.success {
@@ -619,12 +928,12 @@ button:disabled {
/* Animations */
@keyframes toast-in {
from {
- transform: translateY(100%);
+ transform: translateX(100%);
opacity: 0;
}
to {
- transform: translateY(0);
- opacity: 1;
+ transform: translateX(0);
+ opacity: 0.9;
}
}
@@ -855,109 +1164,60 @@ body.history-view .claude-panel {
}
}
-/* Text Format Controls */
-.text-format-controls {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
- padding: 0.5rem 0;
-}
-
-.format-toggle {
- display: flex;
- gap: 0.25rem;
- background-color: var(--background);
- padding: 0.25rem;
- border-radius: 0.375rem;
- border: 1px solid var(--border-color);
- margin: 0 auto;
-}
-
-.format-btn {
- padding: 0.5rem 1rem;
- border: none;
- border-radius: 0.25rem;
- background: transparent;
- color: var(--text-secondary);
- font-size: 0.875rem;
- cursor: pointer;
- transition: all 0.2s;
-}
-
-.format-btn.active {
- background-color: var(--primary-color);
- color: white;
-}
-
-.send-text-group {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- width: 100%;
- justify-content: flex-end;
-}
-
-.confidence-indicator {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- font-size: 0.875rem;
- color: var(--success-color);
- padding: 0.25rem 0.5rem;
- border-radius: 0.25rem;
- background-color: var(--background);
- border: 1px solid var(--border-color);
-}
-
-.confidence-indicator i {
- font-size: 1rem;
-}
-
-.confidence-value {
- font-weight: 500;
-}
-
/* Thinking Section */
.thinking-section {
- background-color: var(--surface-alt);
- border-radius: 0.5rem;
margin-bottom: 1rem;
- overflow: hidden;
- border: 1px solid var(--border-color);
+ border-bottom: 1px solid var(--border-color);
}
.thinking-header {
+ padding: 1rem 1.5rem;
+ cursor: pointer;
display: flex;
justify-content: space-between;
align-items: center;
- padding: 0.8rem 1rem;
- cursor: pointer;
- background-color: var(--surface-alt);
- border-bottom: 1px solid var(--border-color);
- transition: background-color 0.2s ease;
+ transition: all 0.2s ease;
+ background-color: rgba(var(--primary-rgb), 0.1);
+ border: 2px solid transparent;
+ border-radius: 6px;
+ position: relative;
+}
+
+.thinking-header::after {
+ content: "点击展开/折叠";
+ position: absolute;
+ right: 60px;
+ color: var(--text-secondary);
+ font-size: 0.8rem;
+ opacity: 0;
+ transition: opacity 0.2s ease;
}
.thinking-header:hover {
- background-color: var(--hover-color);
+ background-color: rgba(var(--primary-rgb), 0.15);
+ border: 2px dashed rgba(var(--primary-rgb), 0.3);
+}
+
+.thinking-header:hover::after {
+ opacity: 0.7;
}
.thinking-title {
display: flex;
align-items: center;
- gap: 0.5rem;
+ gap: 0.75rem;
}
.thinking-title i {
- color: var(--accent);
- font-size: 1.1rem;
+ color: var(--primary);
+ font-size: 1.125rem;
}
.thinking-title h3 {
margin: 0;
font-size: 1rem;
font-weight: 600;
- color: var(--text);
+ color: var(--text-primary);
}
.toggle-btn {
@@ -965,58 +1225,40 @@ body.history-view .claude-panel {
border: none;
color: var(--text-secondary);
cursor: pointer;
- transition: transform 0.3s ease;
- padding: 0.3rem;
- border-radius: 50%;
+ width: 32px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
}
.toggle-btn:hover {
- background-color: var(--hover-color);
+ color: var(--primary);
}
.thinking-content {
padding: 0;
- max-height: 500px;
- overflow-y: auto;
+ background-color: rgba(var(--primary-rgb), 0.05);
+ border-left: 3px solid rgba(var(--primary-rgb), 0.3);
+ overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
- overflow-x: hidden;
- white-space: pre-wrap;
font-family: monospace;
+ white-space: pre-wrap;
font-size: 0.9rem;
line-height: 1.5;
- color: var(--text-secondary);
- background-color: rgba(0, 0, 0, 0.02);
- border-radius: 0.3rem;
-}
-
-[data-theme="dark"] .thinking-content {
- background-color: rgba(255, 255, 255, 0.03);
-}
-
-.thinking-content code {
- background-color: rgba(0, 0, 0, 0.05);
- padding: 0.2rem 0.4rem;
- border-radius: 0.2rem;
- font-size: 0.85rem;
-}
-
-[data-theme="dark"] .thinking-content code {
- background-color: rgba(255, 255, 255, 0.05);
}
.thinking-content.collapsed {
max-height: 0;
- padding: 0 1rem;
+ padding: 0 1.5rem;
+ overflow: hidden;
}
.thinking-content.expanded {
- padding: 1rem;
+ max-height: 600px;
+ padding: 1rem 1.5rem;
overflow-y: auto;
- border-top: 1px solid var(--border-color);
-}
-
-.thinking-toggle-active .toggle-btn i {
- transform: rotate(180deg);
}
/* Animation for thinking content */
@@ -1107,3 +1349,81 @@ body.history-view .claude-panel {
line-height: 1.2;
display: block;
}
+
+/* Select Styling */
+.select-styled {
+ appearance: none;
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23007CB2%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+ background-repeat: no-repeat;
+ background-position: right 0.7rem top 50%;
+ background-size: 0.65rem auto;
+ padding-right: 2rem !important;
+ cursor: pointer;
+}
+
+.select-styled:focus {
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+}
+
+[data-theme="dark"] .select-styled {
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23A0A0A0%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+}
+
+[data-theme="dark"] .select-styled:focus {
+ background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%234A6CF7%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C%2Fsvg%3E");
+}
+
+/* 空状态提示样式 */
+.empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ padding: 4rem 2rem;
+ text-align: center;
+ color: var(--text-secondary);
+ height: 100%;
+}
+
+.empty-state i {
+ font-size: 4rem;
+ margin-bottom: 1.5rem;
+ color: var(--border-color);
+ opacity: 0.7;
+}
+
+.empty-state h3 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ margin-bottom: 1rem;
+ color: var(--text-primary);
+}
+
+.empty-state p {
+ font-size: 1rem;
+ max-width: 400px;
+ margin: 0 auto 1.5rem;
+ line-height: 1.6;
+}
+
+.loading-message {
+ text-align: center;
+ padding: 2rem;
+ color: var(--text-secondary);
+ font-style: italic;
+}
+
+.thinking-hint {
+ font-size: 0.8rem;
+ font-weight: normal;
+ color: var(--text-secondary);
+ margin-left: 0.5rem;
+}
+
+/* 响应标题样式 */
+.response-header,
+.response-title,
+.response-title i,
+.response-title h3 {
+ display: none;
+}
diff --git a/templates/index.html b/templates/index.html
index bfbe0a9..8edc6d0 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -15,17 +15,17 @@
@@ -35,45 +35,50 @@