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