Files
Snap-Solver/static/js/main.js

932 lines
39 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
class SnapSolver {
constructor() {
// 初始化managers
window.uiManager = new UIManager();
window.settingsManager = new SettingsManager();
// 初始化应用组件
this.initializeElements();
this.initializeState();
this.initializeConnection();
this.setupSocketEventHandlers();
this.setupAutoScroll();
this.setupEventListeners();
// 初始化历史
window.app = this; // 便于从其他地方访问
this.updateHistoryPanel();
}
initializeElements() {
// Main elements
this.screenshotImg = document.getElementById('screenshotImg');
this.imagePreview = document.getElementById('imagePreview');
this.cropBtn = document.getElementById('cropBtn');
this.captureBtn = document.getElementById('captureBtn');
this.sendToClaudeBtn = document.getElementById('sendToClaude');
this.extractTextBtn = document.getElementById('extractText');
this.textEditor = document.getElementById('textEditor');
this.extractedText = document.getElementById('extractedText');
this.sendExtractedTextBtn = document.getElementById('sendExtractedText');
this.manualTextInput = document.getElementById('manualTextInput');
this.claudePanel = document.getElementById('claudePanel');
this.responseContent = document.getElementById('responseContent');
this.thinkingSection = document.getElementById('thinkingSection');
this.thinkingContent = document.getElementById('thinkingContent');
this.thinkingToggle = document.getElementById('thinkingToggle');
this.connectionStatus = document.getElementById('connectionStatus');
this.statusLight = document.querySelector('.status-light');
// Crop elements
this.cropContainer = document.getElementById('cropContainer');
this.cropCancel = document.getElementById('cropCancel');
this.cropConfirm = document.getElementById('cropConfirm');
// Format toggle elements
this.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 = '';
// 确保裁剪容器和其他面板初始为隐藏状态
if (this.cropContainer) {
this.cropContainer.classList.add('hidden');
}
if (this.claudePanel) {
this.claudePanel.classList.add('hidden');
}
if (this.thinkingSection) {
this.thinkingSection.classList.add('hidden');
}
}
setupAutoScroll() {
// Create MutationObserver to watch for content changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'characterData' || mutation.type === 'childList') {
this.responseContent.scrollTo({
top: this.responseContent.scrollHeight,
behavior: 'smooth'
});
}
});
});
// Start observing the response content
observer.observe(this.responseContent, {
childList: true,
characterData: true,
subtree: true
});
}
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');
}
}
updateStatusLight(status) {
this.statusLight.className = 'status-light';
switch (status) {
case 'started':
case 'streaming':
this.statusLight.classList.add('processing');
break;
case 'completed':
this.statusLight.classList.add('completed');
break;
case 'error':
this.statusLight.classList.add('error');
break;
default:
// Reset to default state
break;
}
}
initializeConnection() {
try {
this.socket = io(window.location.origin, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
autoConnect: true
});
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus(true);
});
this.socket.on('disconnect', () => {
console.log('Disconnected from server');
this.updateConnectionStatus(false);
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.updateConnectionStatus(false);
});
this.socket.on('reconnect', (attemptNumber) => {
console.log(`Reconnected after ${attemptNumber} attempts`);
this.updateConnectionStatus(true);
});
this.socket.on('reconnect_attempt', (attemptNumber) => {
console.log(`Reconnection attempt: ${attemptNumber}`);
});
this.socket.on('reconnect_error', (error) => {
console.error('Reconnection error:', error);
});
this.socket.on('reconnect_failed', () => {
console.error('Failed to reconnect');
window.showToast('连接服务器失败,请刷新页面重试', 'error');
});
this.setupSocketEventHandlers();
} catch (error) {
console.error('Connection error:', error);
this.updateConnectionStatus(false);
setTimeout(() => this.initializeConnection(), 5000);
}
}
setupSocketEventHandlers() {
// Screenshot response handler
this.socket.on('screenshot_response', (data) => {
if (data.success) {
this.screenshotImg.src = `data:image/png;base64,${data.image}`;
this.imagePreview.classList.remove('hidden');
this.cropBtn.classList.remove('hidden');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
this.sendToClaudeBtn.classList.add('hidden');
this.extractTextBtn.classList.add('hidden');
this.textEditor.classList.add('hidden');
window.showToast('Screenshot captured successfully');
} else {
window.showToast('Failed to capture screenshot: ' + data.error, 'error');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
}
});
// 请求确认响应处理器
this.socket.on('request_acknowledged', (data) => {
console.log('服务器确认收到请求:', data);
window.showToast(data.message || '请求已接收,正在处理...', 'info');
// 清除可能存在的超时计时器
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 = '<i class="fas fa-font"></i><span>Extract Text</span>';
});
this.socket.on('claude_response', (data) => {
console.log('Received claude_response:', data);
this.updateStatusLight(data.status);
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;
break;
case 'thinking':
// 处理思考内容
if (data.content) {
console.log('Received thinking content');
this.thinkingSection.classList.remove('hidden');
// 直接设置完整内容而不是追加
this.setElementContent(this.thinkingContent, data.content);
// 添加打字动画效果
this.thinkingContent.classList.add('thinking-typing');
}
break;
case 'thinking_complete':
// 完整的思考内容
if (data.content) {
console.log('Thinking complete');
this.thinkingSection.classList.remove('hidden');
// 设置完整内容
this.setElementContent(this.thinkingContent, data.content);
// 移除打字动画
this.thinkingContent.classList.remove('thinking-typing');
}
break;
case 'streaming':
if (data.content) {
console.log('Received content');
// 直接设置完整内容
this.setElementContent(this.responseContent, data.content);
// 移除思考部分的打字动画
this.thinkingContent.classList.remove('thinking-typing');
}
break;
case 'completed':
console.log('Analysis completed');
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
// 保存到历史记录
const responseText = this.responseContent.textContent || '';
const thinkingText = this.thinkingContent.textContent || '';
this.addToHistory(this.croppedImage, responseText, thinkingText);
window.showToast('Analysis completed successfully');
break;
case 'error':
console.error('Analysis error:', data.error);
const errorMessage = data.error || 'Unknown error occurred';
// 显示错误信息
if (errorMessage) {
const currentText = this.responseContent.textContent || '';
this.setElementContent(this.responseContent, currentText + '\nError: ' + errorMessage);
}
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
window.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.showToast('Unknown error occurred', 'error');
}
}
});
}
// 新方法安全设置DOM内容的方法替代updateElementContent
setElementContent(element, content) {
if (!element) return;
// 直接设置内容
element.textContent = content;
// 自动滚动到底部
element.scrollTop = element.scrollHeight;
}
initializeCropper() {
try {
// 如果当前没有截图,不要初始化裁剪器
if (!this.screenshotImg || !this.screenshotImg.src || this.screenshotImg.src === '') {
console.log('No screenshot to crop');
return;
}
// Clean up existing cropper instance
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
const cropArea = document.querySelector('.crop-area');
if (!cropArea) {
console.error('Crop area element not found');
return;
}
cropArea.innerHTML = '';
const clonedImage = this.screenshotImg.cloneNode(true);
clonedImage.style.display = 'block';
cropArea.appendChild(clonedImage);
this.cropContainer.classList.remove('hidden');
// Store reference to this for use in ready callback
const self = this;
this.cropper = new Cropper(clonedImage, {
viewMode: 1,
dragMode: 'crop',
autoCropArea: 0,
restore: false,
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();
}
}
});
} catch (error) {
console.error('Error initializing cropper:', error);
window.showToast('Failed to initialize cropper', 'error');
// 确保在出错时关闭裁剪界面
if (this.cropContainer) {
this.cropContainer.classList.add('hidden');
}
}
}
addToHistory(imageData, response, thinking) {
try {
// 读取现有历史记录
const historyJson = localStorage.getItem('snapHistory') || '[]';
const history = JSON.parse(historyJson);
// 限制图像数据大小 - 缩小图像或者移除图像数据
let optimizedImageData = null;
if (this.isValidImageDataUrl(imageData)) {
// 检查图像字符串长度,如果过大则不存储完整图像
if (imageData.length > 50000) { // 约50KB的限制
// 使用安全的占位符
optimizedImageData = null;
} else {
optimizedImageData = imageData;
}
}
// 创建新的历史记录项
const timestamp = new Date().toISOString();
const id = Date.now();
const item = {
id,
timestamp,
image: optimizedImageData,
response: response ? response.substring(0, 5000) : "", // 限制响应长度
thinking: thinking ? thinking.substring(0, 2000) : "" // 限制思考过程长度
};
// 添加到历史记录并保存
history.unshift(item);
// 限制历史记录数量,更激进地清理以防止存储空间不足
const maxHistoryItems = 10; // 减少最大历史记录数量
if (history.length > maxHistoryItems) {
history.length = maxHistoryItems; // 直接截断数组
}
try {
localStorage.setItem('snapHistory', JSON.stringify(history));
} catch (storageError) {
console.warn('Storage quota exceeded, clearing older history items');
// 如果仍然失败,则更激进地清理
if (history.length > 3) {
history.length = 3; // 只保留最新的3条记录
try {
localStorage.setItem('snapHistory', JSON.stringify(history));
} catch (severeError) {
// 如果还是失败,则清空历史记录
localStorage.removeItem('snapHistory');
localStorage.setItem('snapHistory', JSON.stringify([item])); // 只保留当前项
}
}
}
// 更新历史面板
this.updateHistoryPanel();
} catch (error) {
console.error('Failed to save to history:', error);
}
}
// 新增一个工具函数来判断图像URL是否有效
isValidImageDataUrl(url) {
return url && typeof url === 'string' && url.startsWith('data:image/') && url.includes(',');
}
// 获取一个安全的占位符图像URL
getPlaceholderImageUrl() {
return 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMDAiIGhlaWdodD0iMTUwIiB2aWV3Qm94PSIwIDAgMjAwIDE1MCI+PHJlY3Qgd2lkdGg9IjIwMCIgaGVpZ2h0PSIxNTAiIGZpbGw9IiNmMGYwZjAiLz48dGV4dCB4PSI1MCUiIHk9IjUwJSIgZG9taW5hbnQtYmFzZWxpbmU9Im1pZGRsZSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsIiBmb250LXNpemU9IjE0IiBmaWxsPSIjOTk5Ij7lm77niYflj5HpgIHlt7LkvJjljJY8L3RleHQ+PC9zdmc+';
}
updateHistoryPanel() {
const historyContent = document.querySelector('.history-content');
if (!historyContent) return;
const historyJson = localStorage.getItem('snapHistory') || '[]';
const history = JSON.parse(historyJson);
if (history.length === 0) {
historyContent.innerHTML = `
<div class="history-empty">
<i class="fas fa-history"></i>
<p>无历史记录</p>
</div>
`;
return;
}
const historyItems = history.map(item => {
const date = new Date(item.timestamp);
const formattedDate = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
const hasResponse = item.response ? 'true' : 'false';
// 检查图像是否为有效的数据URL
let imageHtml = '';
if (this.isValidImageDataUrl(item.image)) {
// 有效的图像数据URL
imageHtml = `<img src="${item.image}" alt="历史记录图片" class="history-thumbnail">`;
} else {
// 图像已被优化或不存在,显示占位符
imageHtml = `<div class="history-thumbnail-placeholder">
<i class="fas fa-image"></i>
<span>图片已优化</span>
</div>`;
}
return `
<div class="history-item" data-id="${item.id}" data-has-response="${hasResponse}">
<div class="history-item-header">
<span class="history-date">${formattedDate}</span>
</div>
<div class="history-preview">
${imageHtml}
</div>
</div>
`;
}).join('');
historyContent.innerHTML = historyItems;
// Add click event listeners for history items
document.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const historyItem = history.find(h => h.id === parseInt(item.dataset.id));
if (historyItem) {
// 检查图像是否为有效的数据URL
if (this.isValidImageDataUrl(historyItem.image)) {
// 有效的图像数据
window.app.screenshotImg.src = historyItem.image;
window.app.imagePreview.classList.remove('hidden');
} else {
// 图像已优化或不存在,显示占位符图像
window.app.screenshotImg.src = this.getPlaceholderImageUrl();
window.app.imagePreview.classList.remove('hidden');
}
document.getElementById('historyPanel').classList.add('hidden');
window.app.cropBtn.classList.add('hidden');
window.app.captureBtn.classList.add('hidden');
window.app.sendToClaudeBtn.classList.add('hidden');
window.app.extractTextBtn.classList.add('hidden');
// Set response content
if (historyItem.response) {
window.app.claudePanel.classList.remove('hidden');
window.app.responseContent.textContent = historyItem.response;
}
// Set thinking content if available
if (historyItem.thinking) {
window.app.thinkingSection.classList.remove('hidden');
window.app.thinkingContent.textContent = historyItem.thinking;
} else {
window.app.thinkingSection.classList.add('hidden');
}
}
});
});
}
setupEventListeners() {
this.setupCaptureEvents();
this.setupCropEvents();
this.setupAnalysisEvents();
this.setupKeyboardShortcuts();
this.setupThinkingToggle();
}
setupCaptureEvents() {
// Capture button
this.captureBtn.addEventListener('click', async () => {
if (!this.socket || !this.socket.connected) {
window.showToast('Not connected to server', 'error');
return;
}
try {
this.captureBtn.disabled = true;
this.captureBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Capturing...</span>';
this.socket.emit('request_screenshot');
} catch (error) {
window.showToast('Error requesting screenshot: ' + error.message, 'error');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
}
});
}
setupCropEvents() {
// Crop button
this.cropBtn.addEventListener('click', () => {
if (this.screenshotImg.src) {
this.initializeCropper();
}
});
// Crop confirm button
document.getElementById('cropConfirm').addEventListener('click', () => {
if (this.cropper) {
try {
console.log('Starting crop operation...');
// Validate cropper instance
if (!this.cropper) {
throw new Error('Cropper not initialized');
}
// Get and validate crop box data
const cropBoxData = this.cropper.getCropBoxData();
console.log('Crop box data:', cropBoxData);
if (!cropBoxData || typeof cropBoxData.width !== 'number' || typeof cropBoxData.height !== 'number') {
throw new Error('Invalid crop box data');
}
if (cropBoxData.width < 10 || cropBoxData.height < 10) {
throw new Error('Crop area is too small. Please select a larger area (minimum 10x10 pixels).');
}
// Get cropped canvas with more conservative size limits
console.log('Getting cropped canvas...');
const canvas = this.cropper.getCroppedCanvas({
maxWidth: 2560,
maxHeight: 1440,
fillColor: '#fff',
imageSmoothingEnabled: true,
imageSmoothingQuality: 'high',
});
if (!canvas) {
throw new Error('Failed to create cropped canvas');
}
console.log('Canvas created successfully');
// Convert to data URL with error handling and compression
console.log('Converting to data URL...');
try {
// Use PNG for better quality
this.croppedImage = canvas.toDataURL('image/png');
console.log('Data URL conversion successful');
} catch (dataUrlError) {
console.error('Data URL conversion error:', dataUrlError);
throw new Error('Failed to process cropped image. The image might be too large or memory insufficient.');
}
// Properly destroy the cropper instance
this.cropper.destroy();
this.cropper = null;
// Clean up cropper and update UI
this.cropContainer.classList.add('hidden');
document.querySelector('.crop-area').innerHTML = '';
// Update the screenshot image with the cropped version
this.screenshotImg.src = this.croppedImage;
this.imagePreview.classList.remove('hidden');
this.cropBtn.classList.remove('hidden');
this.sendToClaudeBtn.classList.remove('hidden');
this.extractTextBtn.classList.remove('hidden');
window.showToast('Image cropped successfully');
} 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');
} finally {
// Always clean up the cropper instance
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
}
}
});
// Crop cancel button
document.getElementById('cropCancel').addEventListener('click', () => {
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.cropContainer.classList.add('hidden');
this.sendToClaudeBtn.classList.add('hidden');
this.extractTextBtn.classList.add('hidden');
document.querySelector('.crop-area').innerHTML = '';
});
}
setupAnalysisEvents() {
// Extract Text button
this.extractTextBtn.addEventListener('click', () => {
if (!this.croppedImage) {
window.showToast('Please crop the image first', 'error');
return;
}
this.extractTextBtn.disabled = true;
this.sendExtractedTextBtn.disabled = true; // Disable the send button while extracting
this.extractTextBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Extracting...</span>';
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');
document.getElementById('settingsPanel').classList.remove('hidden');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
return;
}
// Show text editor and prepare UI
this.textEditor.classList.remove('hidden');
if (this.extractedText) {
this.extractedText.value = 'Extracting text...';
this.extractedText.disabled = true;
}
try {
// 设置超时时间10秒以避免长时间无响应
this.emitTimeout = setTimeout(() => {
window.showToast('文本提取超时,请重试或手动输入文本', 'error');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
this.extractedText.disabled = false;
}, 10000);
this.socket.emit('extract_text', {
image: this.croppedImage.split(',')[1],
settings: {
mathpixApiKey: `${mathpixAppId}:${mathpixAppKey}`
}
}, (ackResponse) => {
// 如果服务器确认收到请求,清除超时
clearTimeout(this.emitTimeout);
console.log('服务器确认收到文本提取请求', ackResponse);
});
} catch (error) {
window.showToast('Failed to extract text: ' + error.message, 'error');
this.extractTextBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false; // Re-enable send button on error
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
}
});
// Send Extracted Text button
this.sendExtractedTextBtn.addEventListener('click', () => {
const text = this.extractedText.value.trim();
if (!text) {
window.showToast('Please enter some text', 'error');
return;
}
const settings = window.settingsManager.getSettings();
const apiKeys = {};
Object.entries(window.settingsManager.apiKeyInputs).forEach(([model, input]) => {
if (input.value) {
apiKeys[model] = input.value;
}
});
this.claudePanel.classList.remove('hidden');
this.responseContent.textContent = '';
this.sendExtractedTextBtn.disabled = true;
try {
this.socket.emit('analyze_text', {
text: text,
settings: {
...settings,
api_keys: apiKeys,
model: settings.model || 'claude-3-7-sonnet-20250219',
}
});
} 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');
}
});
// 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;
}
});
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`);
// 如果图片大小超过8MBWebSocket默认限制则显示错误
if (imageSize > 8 * 1024 * 1024) {
window.showToast('图片太大,请裁剪更小的区域或使用文本提取功能', 'error');
this.sendToClaudeBtn.disabled = false;
return;
}
// 设置超时时间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');
}
});
}
setupKeyboardShortcuts() {
// Keyboard shortcuts for capture and crop
document.addEventListener('keydown', (e) => {
if (e.ctrlKey || e.metaKey) {
switch(e.key) {
case 'c':
if (!this.captureBtn.disabled) this.captureBtn.click();
break;
case 'x':
if (!this.cropBtn.disabled) this.cropBtn.click();
break;
}
}
});
}
setupThinkingToggle() {
// Toggle thinking content visibility
if (this.thinkingToggle) {
this.thinkingToggle.addEventListener('click', () => {
const isCollapsed = this.thinkingContent.classList.contains('collapsed');
if (isCollapsed) {
this.thinkingContent.classList.remove('collapsed');
this.thinkingContent.classList.add('expanded');
this.thinkingToggle.classList.add('thinking-toggle-active');
const icon = this.thinkingToggle.querySelector('.toggle-btn i');
if (icon) {
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
}
} else {
this.thinkingContent.classList.add('collapsed');
this.thinkingContent.classList.remove('expanded');
this.thinkingToggle.classList.remove('thinking-toggle-active');
const icon = this.thinkingToggle.querySelector('.toggle-btn i');
if (icon) {
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
}
}
});
}
}
// 获取用于显示的图像URL如果原始URL无效则返回占位符
getImageForDisplay(imageUrl) {
return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl();
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
try {
console.log('Initializing application...');
window.app = new SnapSolver();
console.log('Application initialized successfully');
} catch (error) {
console.error('Failed to initialize application:', error);
// 在页面上显示错误信息
const errorDiv = document.createElement('div');
errorDiv.className = 'init-error';
errorDiv.innerHTML = `
<h2>Initialization Error</h2>
<p>${error.message}</p>
<pre>${error.stack}</pre>
`;
document.body.appendChild(errorDiv);
}
});