class SnapSolver {
constructor() {
console.log('Creating SnapSolver instance...');
// 初始化属性
this.socket = null;
this.socketId = null;
this.isProcessing = false;
this.cropper = null;
this.autoScrollInterval = null;
this.capturedImage = null; // 存储截图的base64数据
this.userThinkingExpanded = false; // 用户思考过程展开状态偏好
this.originalImage = null;
this.croppedImage = null;
this.extractedContent = '';
this.eventsSetup = false;
// Cache DOM elements
this.captureBtn = document.getElementById('captureBtn');
// 移除裁剪按钮引用
this.screenshotImg = document.getElementById('screenshotImg');
this.imagePreview = document.getElementById('imagePreview');
this.emptyState = document.getElementById('emptyState');
this.extractedText = document.getElementById('extractedText');
this.cropContainer = document.getElementById('cropContainer');
this.sendToClaudeBtn = document.getElementById('sendToClaude');
this.extractTextBtn = document.getElementById('extractText');
this.sendExtractedTextBtn = document.getElementById('sendExtractedText');
this.claudePanel = document.getElementById('claudePanel');
this.responseContent = document.getElementById('responseContent');
this.thinkingContent = document.getElementById('thinkingContent');
this.thinkingSection = document.getElementById('thinkingSection');
this.thinkingToggle = document.getElementById('thinkingToggle');
this.connectionStatus = document.getElementById('connectionStatus');
this.statusLight = document.querySelector('.status-light');
this.progressLine = document.querySelector('.progress-line');
this.statusText = document.querySelector('.status-text');
this.analysisIndicator = document.querySelector('.analysis-indicator');
this.clipboardTextarea = document.getElementById('clipboardText');
this.clipboardSendButton = document.getElementById('clipboardSend');
this.clipboardReadButton = document.getElementById('clipboardRead');
this.clipboardStatus = document.getElementById('clipboardStatus');
// Crop elements
this.cropCancel = document.getElementById('cropCancel');
this.cropConfirm = document.getElementById('cropConfirm');
this.cropSendToAI = document.getElementById('cropSendToAI');
// 初始化应用
this.initialize();
}
initializeElements() {
// 查找主要UI元素
this.connectionStatus = document.getElementById('connectionStatus');
this.captureBtn = document.getElementById('captureBtn');
this.emptyState = document.getElementById('emptyState');
this.imagePreview = document.getElementById('imagePreview');
this.screenshotImg = document.getElementById('screenshotImg');
this.sendToClaudeBtn = document.getElementById('sendToClaude');
this.extractTextBtn = document.getElementById('extractText');
this.extractedText = document.getElementById('extractedText');
this.sendExtractedTextBtn = document.getElementById('sendExtractedText');
this.claudePanel = document.getElementById('claudePanel');
this.closeClaudePanel = document.getElementById('closeClaudePanel');
this.thinkingSection = document.getElementById('thinkingSection');
this.thinkingToggle = document.getElementById('thinkingToggle');
this.thinkingContent = document.getElementById('thinkingContent');
this.responseContent = document.getElementById('responseContent');
this.cropContainer = document.getElementById('cropContainer');
this.cropCancel = document.getElementById('cropCancel');
this.cropConfirm = document.getElementById('cropConfirm');
this.cropSendToAI = document.getElementById('cropSendToAI');
this.stopGenerationBtn = document.getElementById('stopGenerationBtn');
this.clipboardTextarea = document.getElementById('clipboardText');
this.clipboardSendButton = document.getElementById('clipboardSend');
this.clipboardReadButton = document.getElementById('clipboardRead');
this.clipboardStatus = document.getElementById('clipboardStatus');
// 处理按钮事件
if (this.closeClaudePanel) {
this.closeClaudePanel.addEventListener('click', () => {
this.claudePanel.classList.add('hidden');
});
}
// 处理停止生成按钮点击事件
if (this.stopGenerationBtn) {
this.stopGenerationBtn.addEventListener('click', () => {
this.stopGeneration();
});
}
}
initializeState() {
this.socket = null;
this.cropper = null;
this.croppedImage = 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(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');
}
}
}
updateStatusLight(status) {
// 获取进度指示器元素
const progressLine = document.querySelector('.progress-line');
const statusText = document.querySelector('.status-text');
const analysisIndicator = document.querySelector('.analysis-indicator');
if (!progressLine || !statusText || !analysisIndicator) {
console.error('状态指示器元素未找到:', {
progressLine: !!progressLine,
statusText: !!statusText,
analysisIndicator: !!analysisIndicator
});
return;
}
// 确保指示器可见
analysisIndicator.style.display = 'flex';
// 先移除所有可能的状态类
analysisIndicator.classList.remove('processing', 'completed', 'error');
progressLine.classList.remove('processing', 'completed', 'error');
switch (status) {
case 'started':
case 'thinking':
case 'reasoning':
case 'streaming':
// 添加处理中状态类
analysisIndicator.classList.add('processing');
progressLine.classList.add('processing');
statusText.textContent = '生成中';
break;
case 'completed':
// 添加完成状态类
analysisIndicator.classList.add('completed');
progressLine.classList.add('completed');
statusText.textContent = '完成';
break;
case 'error':
// 添加错误状态类
analysisIndicator.classList.add('error');
progressLine.classList.add('error');
statusText.textContent = '出错';
break;
case 'stopped':
// 添加错误状态类(用于停止状态)
analysisIndicator.classList.add('error');
progressLine.classList.add('error');
statusText.textContent = '已停止';
break;
default:
// 对于未知状态,显示准备中
statusText.textContent = '准备中';
break;
}
}
initializeConnection() {
try {
// 添加日志以便调试
console.log('尝试连接WebSocket服务器...');
// 确保在Safari上使用完整的URL
const socketUrl = window.location.protocol === 'https:'
? `${window.location.origin}`
: window.location.origin;
console.log('WebSocket连接URL:', socketUrl);
this.socket = io(socketUrl, {
reconnection: true,
reconnectionAttempts: 5,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
timeout: 20000,
autoConnect: true,
transports: ['websocket', 'polling'] // 明确指定传输方式,增加兼容性
});
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus('已连接', true);
this.captureBtn.disabled = false;
});
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.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);
// 移除setTimeout,让用户手动刷新页面重连
window.uiManager.showToast('连接服务器失败,请刷新页面重试', 'error');
}
}
setupSocketEventHandlers() {
// 如果已经设置过事件处理器,先移除它们
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('ai_response');
}
// 标记事件处理器已设置
this.eventsSetup = true;
// 添加响应计数器
if (!window.responseCounter) {
window.responseCounter = 0;
}
// 旧版截图响应处理器 (保留兼容性)
this.socket.on('screenshot_response', (data) => {
// 增加计数并记录
window.responseCounter++;
console.log(`DEBUG: 接收到screenshot_response响应,计数 = ${window.responseCounter}`);
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');
// 根据模型类型显示适当的按钮
this.updateImageActionButtons();
// 恢复按钮状态
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '';
// 初始化裁剪器
this.initializeCropper();
window.uiManager.showToast('截图成功', 'success');
} else {
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '';
console.error('截图失败:', data.error);
window.uiManager.showToast('截图失败: ' + data.error, 'error');
}
});
// 新版截图响应处理器
this.socket.on('screenshot_complete', (data) => {
// 增加计数并记录
window.responseCounter++;
console.log(`DEBUG: 接收到screenshot_complete响应,计数 = ${window.responseCounter}`);
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');
// 根据模型类型显示适当的按钮
this.updateImageActionButtons();
// 初始化裁剪工具
this.initializeCropper();
// 显示成功消息
window.uiManager.showToast('截图成功', 'success');
} else {
// 显示错误消息
window.uiManager.showToast('截图失败: ' + (data.error || '未知错误'), 'error');
}
});
// 确认请求处理
this.socket.on('request_acknowledged', (data) => {
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 (data.content) {
this.extractedText.value = data.content;
this.extractedContent = data.content;
this.extractedText.classList.remove('hidden');
this.sendExtractedTextBtn.classList.remove('hidden');
this.sendExtractedTextBtn.disabled = false;
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('ai_response', (data) => {
console.log('Received ai_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');
// 清空显示内容
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;
// 显示进行中状态
if (this.responseContent) {
this.responseContent.innerHTML = '
分析进行中,请稍候...
';
this.responseContent.style.display = 'block';
}
// 显示停止生成按钮
this.showStopGenerationButton();
break;
case 'thinking':
// 处理思考内容
if (data.content && this.thinkingContent && this.thinkingSection) {
console.log('Received thinking content');
this.thinkingSection.classList.remove('hidden');
// 显示动态省略号
this.showThinkingAnimation(true);
// 直接设置完整内容
this.setElementContent(this.thinkingContent, data.content);
// 添加打字动画效果
this.thinkingContent.classList.add('thinking-typing');
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
// 更新切换按钮图标
const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
}
// 确保停止按钮可见
this.showStopGenerationButton();
break;
case 'reasoning':
// 处理推理内容 (QVQ-Max模型使用)
if (data.content && this.thinkingContent && this.thinkingSection) {
console.log('Received reasoning content');
this.thinkingSection.classList.remove('hidden');
// 显示动态省略号
this.showThinkingAnimation(true);
// 直接设置完整内容
this.setElementContent(this.thinkingContent, data.content);
// 添加打字动画效果
this.thinkingContent.classList.add('thinking-typing');
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
// 更新切换按钮图标
const toggleIcon = document.querySelector('#thinkingToggle .toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
}
// 确保停止按钮可见
this.showStopGenerationButton();
break;
case 'thinking_complete':
// 完整的思考内容
if (data.content && this.thinkingContent && this.thinkingSection) {
console.log('思考过程完成');
this.thinkingSection.classList.remove('hidden');
// 停止动态省略号
this.showThinkingAnimation(false);
// 设置完整内容
this.setElementContent(this.thinkingContent, data.content);
// 移除打字动画
this.thinkingContent.classList.remove('thinking-typing');
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
// 确保图标正确显示
const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
}
break;
case 'reasoning_complete':
// 完整的推理内容 (QVQ-Max模型使用)
if (data.content && this.thinkingContent && this.thinkingSection) {
console.log('Reasoning complete');
this.thinkingSection.classList.remove('hidden');
// 停止动态省略号
this.showThinkingAnimation(false);
// 设置完整内容
this.setElementContent(this.thinkingContent, data.content);
// 移除打字动画
this.thinkingContent.classList.remove('thinking-typing');
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
// 确保图标正确显示
const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
}
break;
case 'streaming':
if (data.content && this.responseContent) {
console.log('Received content chunk');
// 使用更安全的方式设置内容,避免HTML解析问题
this.setElementContent(this.responseContent, data.content);
this.responseContent.style.display = 'block';
// 停止省略号动画
this.showThinkingAnimation(false);
// 移除思考部分的打字动画
if (this.thinkingContent) {
this.thinkingContent.classList.remove('thinking-typing');
}
// 平滑滚动到最新内容
this.scrollToBottom();
}
// 确保停止按钮可见
this.showStopGenerationButton();
break;
case 'completed':
console.log('Analysis completed');
// 重新启用按钮
if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false;
if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false;
// 恢复界面
this.updateStatusLight('completed');
// 只有在有思考内容时才显示思考组件
if (this.thinkingSection && this.thinkingContent) {
// 检查思考内容是否为空
const hasThinkingContent = this.thinkingContent.textContent && this.thinkingContent.textContent.trim() !== '';
if (hasThinkingContent) {
// 有思考内容,显示思考组件
this.thinkingSection.classList.remove('hidden');
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
const toggleBtn = document.querySelector('#thinkingToggle .toggle-btn i');
if (toggleBtn) {
toggleBtn.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
// 简化提示信息
window.uiManager.showToast('分析完成', 'success');
} else {
// 没有思考内容,隐藏思考组件
this.thinkingSection.classList.add('hidden');
window.uiManager.showToast('分析完成', 'success');
}
}
// 确保响应内容完整显示
if (data.content && data.content.trim() !== '' && this.responseContent) {
this.setElementContent(this.responseContent, data.content);
}
// 确保结果内容可见
if (this.responseContent) {
this.responseContent.style.display = 'block';
// 滚动到结果内容底部
this.scrollToBottom();
}
// 隐藏停止生成按钮
this.hideStopGenerationButton();
break;
case 'stopped':
// 处理停止生成的响应
console.log('Generation stopped');
// 重新启用按钮
if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false;
if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false;
// 恢复界面
this.updateStatusLight('stopped');
// 隐藏停止生成按钮
this.hideStopGenerationButton();
// 显示提示信息
window.uiManager.showToast('已停止生成', 'info');
break;
case 'error':
console.error('Analysis error:', data.error);
// 安全处理错误消息,确保它是字符串
let errorMessage = 'Unknown error occurred';
if (data.error) {
if (typeof data.error === 'string') {
errorMessage = data.error;
} else if (typeof data.error === 'object') {
// 如果是对象,尝试获取消息字段或转换为JSON
errorMessage = data.error.message || data.error.error || JSON.stringify(data.error);
} else {
errorMessage = String(data.error);
}
}
// 显示错误信息
if (this.responseContent) {
// 不要尝试获取现有内容,直接显示错误信息
this.setElementContent(this.responseContent, 'Error: ' + errorMessage);
}
// 重新启用按钮
if (this.sendToClaudeBtn) this.sendToClaudeBtn.disabled = false;
if (this.sendExtractedTextBtn) this.sendExtractedTextBtn.disabled = false;
window.uiManager.showToast('Analysis failed: ' + errorMessage, 'error');
// 隐藏停止生成按钮
this.hideStopGenerationButton();
break;
default:
console.warn('Unknown response status:', data.status);
// 对于未知状态,尝试显示内容(如果有)
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');
// 隐藏停止生成按钮
this.hideStopGenerationButton();
break;
}
});
// 接收到thinking数据时
this.socket.on('thinking', (data) => {
console.log('收到思考过程数据');
// 显示思考区域
this.thinkingSection.classList.remove('hidden');
// 显示动态省略号
this.showThinkingAnimation(true);
// 使用setElementContent方法处理Markdown
this.setElementContent(this.thinkingContent, data.thinking);
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
});
// 思考过程完成 - Socket事件处理
this.socket.on('thinking_complete', (data) => {
console.log('Socket接收到思考过程完成');
this.thinkingSection.classList.remove('hidden');
// 停止动态省略号动画
this.showThinkingAnimation(false);
// 使用setElementContent方法处理Markdown
this.setElementContent(this.thinkingContent, data.thinking);
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
// 确保图标正确显示
const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
});
// 分析完成
this.socket.on('analysis_complete', (data) => {
console.log('分析完成,接收到结果');
this.updateStatusLight('completed');
this.enableInterface();
// 显示分析结果
if (this.responseContent) {
// 使用setElementContent方法处理Markdown
this.setElementContent(this.responseContent, data.response);
this.responseContent.style.display = 'block';
// 直接滚动到结果区域,不使用setTimeout
this.responseContent.scrollIntoView({ behavior: 'smooth' });
}
// 确保思考部分完全显示(如果有的话)
if (data.thinking && this.thinkingSection && this.thinkingContent) {
this.thinkingSection.classList.remove('hidden');
// 使用setElementContent方法处理Markdown
this.setElementContent(this.thinkingContent, data.thinking);
// 根据用户偏好设置展开/折叠状态
this.thinkingContent.classList.remove('expanded');
this.thinkingContent.classList.remove('collapsed');
if (this.userThinkingExpanded) {
this.thinkingContent.classList.add('expanded');
} else {
this.thinkingContent.classList.add('collapsed');
}
const toggleIcon = this.thinkingToggle.querySelector('.toggle-btn i');
if (toggleIcon) {
toggleIcon.className = this.userThinkingExpanded ?
'fas fa-chevron-up' : 'fas fa-chevron-down';
}
// 弹出提示
window.uiManager.showToast('分析完成', 'success');
}
});
}
// 新方法:安全设置DOM内容的方法(替代updateElementContent)
setElementContent(element, content) {
if (!element) return;
// 首先确保content是字符串
if (typeof content !== 'string') {
if (content === null || content === undefined) {
content = '';
} else if (typeof content === 'object') {
// 对于对象,尝试获取有意义的字符串表示
if (content.error || content.message) {
content = content.error || content.message;
} else if (content.toString && typeof content.toString === 'function' && content.toString() !== '[object Object]') {
content = content.toString();
} else {
// 作为最后手段,使用JSON.stringify
try {
content = JSON.stringify(content, null, 2);
} catch (e) {
content = '[Complex Object]';
}
}
} else {
content = String(content);
}
}
try {
// 检查marked是否已配置
if (typeof marked === 'undefined') {
console.warn('Marked库未加载,回退到纯文本显示');
// 即使回退到纯文本,也要保留换行和基本格式
element.innerHTML = content.replace(/\n/g, '
');
return;
}
// 使用marked库解析Markdown内容
const renderedHtml = marked.parse(content);
// 设置解析后的HTML内容
element.innerHTML = renderedHtml;
// 为未高亮的代码块应用语法高亮
if (window.hljs) {
element.querySelectorAll('pre code:not(.hljs)').forEach((block) => {
hljs.highlightElement(block);
});
}
// 为所有代码块添加复制按钮
this.addCopyButtonsToCodeBlocks(element);
} catch (error) {
console.error('Markdown解析错误:', error);
// 发生错误时也保留换行格式
element.innerHTML = content.replace(/\n/g, '
');
}
// 自动滚动到底部
element.scrollTop = element.scrollHeight;
}
// 为代码块添加复制按钮
addCopyButtonsToCodeBlocks(element) {
const codeBlocks = element.querySelectorAll('pre code');
codeBlocks.forEach((codeBlock) => {
// 检查是否已经有复制按钮
if (codeBlock.parentElement.querySelector('.code-copy-btn')) {
return;
}
// 创建包装器
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
// 将pre元素包装起来
const preElement = codeBlock.parentElement;
preElement.parentNode.insertBefore(wrapper, preElement);
wrapper.appendChild(preElement);
// 创建复制按钮
const copyBtn = document.createElement('button');
copyBtn.className = 'code-copy-btn';
copyBtn.innerHTML = ' 复制';
copyBtn.title = '复制代码';
// 添加点击事件
copyBtn.addEventListener('click', async () => {
const codeText = codeBlock.textContent;
try {
// 尝试使用现代 Clipboard API
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(codeText);
this.showCopySuccess(copyBtn);
return;
}
// 降级方案:使用传统的 document.execCommand
const textArea = document.createElement('textarea');
textArea.value = codeText;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
this.showCopySuccess(copyBtn);
} else {
throw new Error('execCommand failed');
}
} catch (error) {
console.error('复制失败:', error);
// 最后的降级方案:选中文本让用户手动复制
this.selectTextForManualCopy(codeBlock, copyBtn);
}
});
// 将按钮添加到包装器
wrapper.appendChild(copyBtn);
});
}
// 显示复制成功状态
showCopySuccess(copyBtn) {
copyBtn.innerHTML = ' 已复制';
copyBtn.classList.add('copied');
window.uiManager?.showToast('代码已复制到剪贴板', 'success');
// 2秒后恢复原状
setTimeout(() => {
copyBtn.innerHTML = ' 复制';
copyBtn.classList.remove('copied');
}, 2000);
}
// 选中文本供用户手动复制
selectTextForManualCopy(codeBlock, copyBtn) {
// 选中代码文本
const range = document.createRange();
range.selectNodeContents(codeBlock);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
// 更新按钮状态
copyBtn.innerHTML = ' 已选中';
copyBtn.classList.add('copied');
// 显示提示
window.uiManager?.showToast('代码已选中,请按 Ctrl+C 复制', 'info');
// 3秒后恢复原状
setTimeout(() => {
copyBtn.innerHTML = ' 复制';
copyBtn.classList.remove('copied');
selection.removeAllRanges();
}, 3000);
}
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: 'move',
aspectRatio: NaN,
modal: true,
background: true,
ready: function() {
// 如果有上次保存的裁剪框数据,应用它
if (self.lastCropBoxData) {
self.cropper.setCropBoxData(self.lastCropBoxData);
console.log('Applied saved crop box data');
}
}
});
} catch (error) {
console.error('Failed to initialize cropper', error);
window.uiManager.showToast('裁剪器初始化失败', 'error');
// 确保在出错时关闭裁剪界面
if (this.cropContainer) {
this.cropContainer.classList.add('hidden');
}
}
}
setupEventListeners() {
console.log('DEBUG: 设置所有事件监听器(这应该只执行一次)');
this.setupCaptureEvents();
this.setupCropEvents();
this.setupAnalysisEvents();
this.setupKeyboardShortcuts();
this.setupThinkingToggle();
this.setupClipboardFeature();
// 监听模型选择变化,更新界面
if (window.settingsManager && window.settingsManager.modelSelect) {
window.settingsManager.modelSelect.addEventListener('change', () => {
this.updateImageActionButtons();
});
}
}
setupClipboardFeature() {
if (!this.clipboardTextarea || !this.clipboardSendButton || !this.clipboardReadButton) {
console.warn('Clipboard controls not found in DOM');
return;
}
// 读取剪贴板按钮事件
this.clipboardReadButton.addEventListener('click', (event) => {
event.preventDefault();
this.readClipboardText();
});
// 发送到剪贴板按钮事件
this.clipboardSendButton.addEventListener('click', (event) => {
event.preventDefault();
this.sendClipboardText();
});
// 键盘快捷键:Ctrl/Cmd + Enter 发送到剪贴板
this.clipboardTextarea.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
event.preventDefault();
this.sendClipboardText();
}
});
}
setupCaptureEvents() {
// 添加计数器
if (!window.captureCounter) {
window.captureCounter = 0;
}
// 移除现有的事件监听器,防止重复绑定
if (this.captureBtn) {
// 克隆按钮并替换原按钮,这样可以移除所有事件监听器
const newBtn = this.captureBtn.cloneNode(true);
this.captureBtn.parentNode.replaceChild(newBtn, this.captureBtn);
this.captureBtn = newBtn;
console.log('DEBUG: 已清除截图按钮上的事件监听器');
}
// 截图按钮
this.captureBtn.addEventListener('click', () => {
if (!this.checkConnectionBeforeAction()) return;
try {
// 增加计数并记录
window.captureCounter++;
console.log(`DEBUG: 截图按钮点击计数 = ${window.captureCounter}`);
this.captureBtn.disabled = true; // 禁用按钮防止重复点击
this.captureBtn.innerHTML = '';
console.log('DEBUG: 发送capture_screenshot事件到服务器');
this.socket.emit('capture_screenshot', {});
} catch (error) {
console.error('Error starting capture:', error);
window.uiManager.showToast('启动截图失败', 'error');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '';
}
});
}
updateClipboardStatus(message, status = 'neutral') {
if (!this.clipboardStatus) return;
if (!message) {
this.clipboardStatus.textContent = '';
this.clipboardStatus.removeAttribute('data-status');
return;
}
this.clipboardStatus.textContent = message;
this.clipboardStatus.dataset.status = status;
}
async readClipboardText() {
if (!this.clipboardTextarea || !this.clipboardReadButton) return;
this.updateClipboardStatus('读取中...', 'pending');
this.clipboardReadButton.disabled = true;
try {
const response = await fetch('/api/clipboard', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});
const result = await response.json().catch(() => ({}));
if (response.ok && result?.success) {
// 将读取到的内容填入文本框
this.clipboardTextarea.value = result.text || '';
const successMessage = result.text ?
`成功读取剪贴板内容 (${result.text.length} 字符)` :
'剪贴板为空';
this.updateClipboardStatus(successMessage, 'success');
window.uiManager?.showToast(successMessage, 'success');
} else {
const errorMessage = result?.message || '读取失败,请稍后重试';
this.updateClipboardStatus(errorMessage, 'error');
window.uiManager?.showToast(errorMessage, 'error');
}
} catch (error) {
console.error('Failed to read clipboard text:', error);
const networkErrorMessage = '网络错误,读取失败';
this.updateClipboardStatus(networkErrorMessage, 'error');
window.uiManager?.showToast(networkErrorMessage, 'error');
} finally {
this.clipboardReadButton.disabled = false;
}
}
async sendClipboardText() {
if (!this.clipboardTextarea) return;
const text = this.clipboardTextarea.value ?? '';
if (!text.trim()) {
const warningMessage = '请输入要发送到剪贴板的文字';
this.updateClipboardStatus(warningMessage, 'error');
window.uiManager?.showToast(warningMessage, 'warning');
return;
}
this.updateClipboardStatus('发送中...', 'pending');
if (this.clipboardSendButton) {
this.clipboardSendButton.disabled = true;
}
try {
const response = await fetch('/api/clipboard', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ text })
});
const result = await response.json().catch(() => ({}));
if (response.ok && result?.success) {
const successMessage = '已复制到服务端剪贴板';
this.updateClipboardStatus(successMessage, 'success');
window.uiManager?.showToast(successMessage, 'success');
// 清空输入框
this.clipboardTextarea.value = '';
} else {
const errorMessage = result?.message || '发送失败,请稍后重试';
this.updateClipboardStatus(errorMessage, 'error');
window.uiManager?.showToast(errorMessage, 'error');
}
} catch (error) {
console.error('Failed to send clipboard text:', error);
const networkErrorMessage = '网络错误,发送失败';
this.updateClipboardStatus(networkErrorMessage, 'error');
window.uiManager?.showToast(networkErrorMessage, 'error');
} finally {
if (this.clipboardSendButton) {
this.clipboardSendButton.disabled = false;
}
}
}
setupCropEvents() {
// 防止重复绑定事件监听器
if (this.cropConfirm) {
const newCropConfirm = this.cropConfirm.cloneNode(true);
this.cropConfirm.parentNode.replaceChild(newCropConfirm, this.cropConfirm);
this.cropConfirm = newCropConfirm;
}
if (this.cropCancel) {
const newCropCancel = this.cropCancel.cloneNode(true);
this.cropCancel.parentNode.replaceChild(newCropCancel, this.cropCancel);
this.cropCancel = newCropCancel;
}
const cropResetElement = document.getElementById('cropReset');
if (cropResetElement) {
const newCropReset = cropResetElement.cloneNode(true);
cropResetElement.parentNode.replaceChild(newCropReset, cropResetElement);
}
if (this.cropSendToAI) {
const newCropSendToAI = this.cropSendToAI.cloneNode(true);
this.cropSendToAI.parentNode.replaceChild(newCropSendToAI, this.cropSendToAI);
this.cropSendToAI = newCropSendToAI;
}
console.log('DEBUG: 已清除裁剪按钮上的事件监听器,防止重复绑定');
// 存储裁剪框数据
this.lastCropBoxData = null;
// Crop confirm button
this.cropConfirm.addEventListener('click', () => {
if (!this.checkConnectionBeforeAction()) return;
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);
// 保存裁剪框数据以便下次使用
this.lastCropBoxData = 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.updateImageActionButtons();
window.uiManager.showToast('裁剪成功');
// 不再自动发送至AI,由用户手动选择
} catch (error) {
console.error('Cropping error details:', {
message: error.message,
stack: error.stack,
cropperState: this.cropper ? 'initialized' : 'not initialized'
});
window.uiManager.showToast(error.message || '裁剪图像时出错', 'error');
} finally {
// Always clean up the cropper instance
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
}
}
});
// Crop cancel button
this.cropCancel.addEventListener('click', () => {
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.cropContainer.classList.add('hidden');
// 取消裁剪时隐藏图像预览和相关按钮
this.imagePreview.classList.add('hidden');
document.querySelector('.crop-area').innerHTML = '';
});
// Crop reset button
const cropResetBtn = document.getElementById('cropReset');
if (cropResetBtn) {
cropResetBtn.addEventListener('click', () => {
if (this.cropper) {
// 重置裁剪区域到默认状态
this.cropper.reset();
window.uiManager.showToast('已重置裁剪区域');
}
});
}
// Crop send to AI button
this.cropSendToAI.addEventListener('click', () => {
if (!this.checkConnectionBeforeAction()) return;
// 如果有裁剪器,尝试获取裁剪结果;否则使用原始图片
if (this.cropper) {
try {
console.log('Starting crop and send 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);
// 保存裁剪框数据以便下次使用
this.lastCropBoxData = 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
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
console.log('Converting to data URL...');
try {
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.');
}
// Clean up cropper and update UI
this.cropper.destroy();
this.cropper = null;
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.updateImageActionButtons();
// 显示Claude分析面板
this.claudePanel.classList.remove('hidden');
this.emptyState.classList.add('hidden');
// 发送图像到Claude进行分析
this.sendImageToClaude(this.croppedImage);
window.uiManager.showToast('正在发送至AI分析...');
} catch (error) {
console.error('Crop and send error details:', {
message: error.message,
stack: error.stack,
cropperState: this.cropper ? 'initialized' : 'not initialized'
});
window.uiManager.showToast(error.message || '处理图像时出错', 'error');
// Clean up on error
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.cropContainer.classList.add('hidden');
document.querySelector('.crop-area').innerHTML = '';
}
} else if (this.originalImage) {
// 如果没有裁剪器但有原始图片,直接发送原始图片
try {
// 隐藏裁剪容器
this.cropContainer.classList.add('hidden');
// 显示Claude分析面板
this.claudePanel.classList.remove('hidden');
this.emptyState.classList.add('hidden');
// 发送原始图像到Claude进行分析
this.sendImageToClaude(this.originalImage);
window.uiManager.showToast('正在发送至AI分析...');
} catch (error) {
console.error('Send original image error:', error);
window.uiManager.showToast('发送图片失败: ' + error.message, 'error');
}
} else {
window.uiManager.showToast('请先截图', 'error');
}
});
}
setupAnalysisEvents() {
// 防止重复绑定事件监听器
if (this.extractTextBtn) {
const newExtractBtn = this.extractTextBtn.cloneNode(true);
this.extractTextBtn.parentNode.replaceChild(newExtractBtn, this.extractTextBtn);
this.extractTextBtn = newExtractBtn;
}
if (this.sendExtractedTextBtn) {
const newSendBtn = this.sendExtractedTextBtn.cloneNode(true);
this.sendExtractedTextBtn.parentNode.replaceChild(newSendBtn, this.sendExtractedTextBtn);
this.sendExtractedTextBtn = newSendBtn;
}
if (this.sendToClaudeBtn) {
const newClaudeBtn = this.sendToClaudeBtn.cloneNode(true);
this.sendToClaudeBtn.parentNode.replaceChild(newClaudeBtn, this.sendToClaudeBtn);
this.sendToClaudeBtn = newClaudeBtn;
}
console.log('DEBUG: 已清除分析按钮上的事件监听器,防止重复绑定');
// Extract Text button
this.extractTextBtn.addEventListener('click', () => {
if (!this.checkConnectionBeforeAction()) return;
// 优先使用裁剪后的图片,如果没有则使用原始截图
const imageToExtract = this.croppedImage || this.originalImage;
if (!imageToExtract) {
window.uiManager.showToast('请先截图', 'error');
return;
}
this.extractTextBtn.disabled = true;
this.sendExtractedTextBtn.disabled = true; // Disable the send button while extracting
this.extractTextBtn.innerHTML = '提取中...';
const settings = window.settingsManager.getSettings();
// 根据用户设置的OCR源进行选择
const ocrSource = settings.ocrSource || 'auto';
const baiduApiKey = window.settingsManager.apiKeyValues.BaiduApiKey;
const baiduSecretKey = window.settingsManager.apiKeyValues.BaiduSecretKey;
const mathpixApiKey = settings.mathpixApiKey;
const hasBaiduOCR = baiduApiKey && baiduSecretKey;
const hasMathpix = mathpixApiKey && mathpixApiKey !== ':';
// 根据OCR源配置检查可用性
let canProceed = false;
let missingOCRMessage = '';
if (ocrSource === 'baidu') {
canProceed = hasBaiduOCR;
missingOCRMessage = '请在设置中配置百度OCR API密钥';
} else if (ocrSource === 'mathpix') {
canProceed = hasMathpix;
missingOCRMessage = '请在设置中配置Mathpix API密钥';
} else { // auto
canProceed = hasBaiduOCR || hasMathpix;
missingOCRMessage = '请在设置中配置OCR API密钥:百度OCR(推荐)或Mathpix';
}
if (!canProceed) {
window.uiManager.showToast(missingOCRMessage, 'error');
document.getElementById('settingsPanel').classList.add('active');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '提取文本';
return;
}
// 显示文本框和按钮
this.extractedText.classList.remove('hidden');
this.sendExtractedTextBtn.classList.remove('hidden');
if (this.extractedText) {
this.extractedText.value = '正在提取文本...';
this.extractedText.disabled = true;
}
try {
this.socket.emit('extract_text', {
image: imageToExtract.split(',')[1],
settings: {
ocrSource: settings.ocrSource || 'auto'
}
});
// 监听服务器确认请求的响应
this.socket.once('request_acknowledged', (ackResponse) => {
console.log('服务器确认收到文本提取请求', ackResponse);
});
} catch (error) {
window.uiManager.showToast('提取文本失败: ' + error.message, 'error');
this.extractTextBtn.disabled = false;
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;
// 防止重复点击
if (this.sendExtractedTextBtn.disabled) return;
const text = this.extractedText.value.trim();
if (!text) {
window.uiManager.showToast('请输入一些文本', 'error');
return;
}
const settings = window.settingsManager.getSettings();
const apiKeys = {};
Object.keys(window.settingsManager.apiKeyInputs).forEach(keyId => {
const input = window.settingsManager.apiKeyInputs[keyId];
if (input && input.value) {
apiKeys[keyId] = input.value;
}
});
console.log("Debug - 发送文本分析API密钥:", apiKeys);
// 禁用按钮防止重复点击
this.sendExtractedTextBtn.disabled = true;
try {
this.socket.emit('analyze_text', {
text: text,
settings: {
...settings,
apiKeys: apiKeys,
model: settings.model || 'claude-3-7-sonnet-20250219',
modelInfo: settings.modelInfo || {},
modelCapabilities: {
supportsMultimodal: settings.modelInfo?.supportsMultimodal || false,
isReasoning: settings.modelInfo?.isReasoning || false
}
}
});
} catch (error) {
this.setElementContent(this.responseContent, 'Error: Failed to send text for analysis - ' + error.message);
this.sendExtractedTextBtn.disabled = false;
window.uiManager.showToast('发送文本进行分析失败', 'error');
}
});
// Send to Claude button
this.sendToClaudeBtn.addEventListener('click', () => {
if (!this.checkConnectionBeforeAction()) return;
// 防止重复点击
if (this.sendToClaudeBtn.disabled) return;
this.sendToClaudeBtn.disabled = true;
// 获取当前模型设置
const settings = window.settingsManager.getSettings();
const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false;
const modelName = settings.model || '未知';
if (!isMultimodalModel) {
window.uiManager.showToast(`当前选择的模型 ${modelName} 不支持图像分析。请先提取文本或切换到支持多模态的模型。`, 'error');
this.sendToClaudeBtn.disabled = false;
return;
}
// 只发送裁剪后的图片,如果没有裁剪过则提示用户先裁剪
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;
}
});
// 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';
});
}
}
setupThinkingToggle() {
// 确保正确获取DOM元素
const thinkingSection = document.getElementById('thinkingSection');
const thinkingToggle = document.getElementById('thinkingToggle');
const thinkingContent = document.getElementById('thinkingContent');
if (!thinkingToggle || !thinkingContent) {
console.error('思考切换组件未找到必要的DOM元素');
return;
}
// 初始化时隐藏动态省略号
this.showThinkingAnimation(false);
// 存储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';
}
// 更新用户偏好
this.userThinkingExpanded = false;
} else {
console.log('展开思考内容');
// 添加展开状态
thinkingContent.classList.add('expanded');
if (toggleIcon) {
toggleIcon.className = 'fas fa-chevron-up';
}
// 更新用户偏好
this.userThinkingExpanded = true;
// 当展开思考内容时,确保代码高亮生效
if (window.hljs) {
setTimeout(() => {
thinkingContent.querySelectorAll('pre code').forEach((block) => {
hljs.highlightElement(block);
});
}, 100); // 添加一点延迟,确保DOM已完全更新
}
}
};
console.log('思考切换组件初始化完成');
}
// 获取用于显示的图像URL,如果原始URL无效则返回占位符
getImageForDisplay(imageUrl) {
return this.isValidImageDataUrl(imageUrl) ? imageUrl : this.getPlaceholderImageUrl();
}
sendImageToClaude(imageData) {
const settings = window.settingsManager.getSettings();
// 获取API密钥
const apiKeys = {};
Object.keys(window.settingsManager.apiKeyInputs).forEach(keyId => {
const input = window.settingsManager.apiKeyInputs[keyId];
if (input && input.value) {
apiKeys[keyId] = input.value;
}
});
console.log("Debug - 发送API密钥:", apiKeys);
try {
// 处理图像数据,去除base64前缀
let processedImageData = imageData;
if (imageData.startsWith('data:')) {
// 分割数据URL,只保留base64部分
processedImageData = imageData.split(',')[1];
}
this.socket.emit('analyze_image', {
image: processedImageData,
settings: {
...settings,
apiKeys: apiKeys,
model: settings.model || 'claude-3-7-sonnet-20250219',
modelInfo: settings.modelInfo || {},
modelCapabilities: {
supportsMultimodal: settings.modelInfo?.supportsMultimodal || false,
isReasoning: settings.modelInfo?.isReasoning || false
}
}
});
// 注意:Claude面板的显示已经在点击事件中处理,这里不再重复
} catch (error) {
this.setElementContent(this.responseContent, 'Error: ' + error.message);
window.uiManager.showToast('发送图片分析失败', 'error');
this.sendToClaudeBtn.disabled = false;
}
}
async initialize() {
console.log('Initializing SnapSolver...');
// 重置调试计数器
window.captureCounter = 0;
window.responseCounter = 0;
console.log('DEBUG: 重置截图计数器');
// 初始化managers
// 确保UIManager已经初始化,如果没有,等待它初始化
if (!window.uiManager) {
console.log('等待UI管理器初始化...');
window.uiManager = new UIManager();
// 给UIManager一些时间初始化
await new Promise(resolve => setTimeout(resolve, 100));
}
window.settingsManager = new SettingsManager();
window.app = this; // 便于从其他地方访问
// 等待SettingsManager初始化完成
if (window.settingsManager) {
// 如果settingsManager还没初始化完成,等待它
if (!window.settingsManager.isInitialized) {
console.log('等待设置管理器初始化完成...');
// 最多等待5秒
for (let i = 0; i < 50; i++) {
if (window.settingsManager.isInitialized) break;
await new Promise(resolve => setTimeout(resolve, 100));
}
}
}
// 初始化Markdown工具
this.initializeMarkdownTools();
// 建立与服务器的连接
this.connectToServer();
// 初始化UI元素和事件处理
this.initializeElements();
// 设置所有事件监听器(注意:setupEventListeners内部已经调用了setupCaptureEvents,不需要重复调用)
this.setupEventListeners();
this.setupAutoScroll();
// 监听窗口大小变化,调整界面
window.addEventListener('resize', this.handleResize.bind(this));
// 监听document点击事件,处理面板关闭
document.addEventListener('click', (e) => {
// 关闭裁剪器
if (this.cropContainer &&
!this.cropContainer.contains(e.target) &&
!e.target.matches('#cropBtn') &&
!this.cropContainer.classList.contains('hidden')) {
this.cropContainer.classList.add('hidden');
}
});
// 监听页面卸载事件,清除所有计时器
window.addEventListener('beforeunload', this.cleanup.bind(this));
// 设置默认UI状态
this.enableInterface();
// 更新图像操作按钮
this.updateImageActionButtons();
console.log('SnapSolver initialization complete');
}
// 初始化Markdown工具
initializeMarkdownTools() {
// 检查marked是否可用
if (typeof marked === 'undefined') {
console.warn('Marked.js 未加载,Markdown渲染将不可用');
return;
}
// 创建一个备用的hljs对象,以防CDN加载失败
if (typeof hljs === 'undefined') {
console.warn('Highlight.js未加载,创建备用对象');
window.hljs = {
highlight: (code, opts) => ({ value: code }),
highlightAuto: (code) => ({ value: code }),
getLanguage: () => null,
configure: () => {}
};
}
// 初始化marked设置
marked.setOptions({
gfm: true, // 启用GitHub风格的Markdown
breaks: true, // 将换行符转换为
pedantic: false, // 不使用原始markdown规范
sanitize: false, // 不要过滤HTML标签,允许一些HTML
smartLists: true, // 使用比原生markdown更智能的列表行为
smartypants: false, // 不要使用更智能的标点符号
xhtml: false, // 不使用自闭合标签
mangle: false, // 不混淆邮箱地址
headerIds: false, // 不生成header ID
highlight: function(code, lang) {
// 如果highlight.js不可用,直接返回代码
if (typeof hljs === 'undefined') {
return code;
}
// 如果指定了语言且hljs支持
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(code, { language: lang }).value;
} catch (err) {
console.error('代码高亮错误:', err);
}
}
// 尝试自动检测语言
try {
return hljs.highlightAuto(code).value;
} catch (err) {
console.error('自动语言检测错误:', err);
}
return code; // 使用默认编码效果
}
});
// 配置hljs以支持自动语言检测
try {
hljs.configure({
languages: ['javascript', 'python', 'java', 'cpp', 'csharp', 'html', 'css', 'json', 'xml', 'markdown', 'bash']
});
console.log('Markdown工具初始化完成');
} catch (err) {
console.error('配置hljs时出错:', err);
}
}
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');
});
// 设置socket事件处理器
this.setupSocketEventHandlers();
}
isConnected() {
return this.socket && this.socket.connected;
}
checkConnectionBeforeAction(action) {
if (!this.isConnected()) {
window.uiManager.showToast('未连接到服务器,请等待连接建立后再试', 'error');
return false;
}
return true;
}
scrollToBottom() {
if (this.responseContent) {
// 使用平滑滚动效果
this.responseContent.scrollTo({
top: this.responseContent.scrollHeight,
behavior: 'smooth'
});
// 确保Claude面板也滚动到可见区域
if (this.claudePanel) {
this.claudePanel.scrollIntoView({
behavior: 'smooth',
block: 'end'
});
}
}
}
// 新增方法:根据所选模型更新图像操作按钮
updateImageActionButtons() {
if (!window.settingsManager) {
console.error('Settings manager not available');
return;
}
const settings = window.settingsManager.getSettings();
const isMultimodalModel = settings.modelInfo?.supportsMultimodal || false;
const modelName = settings.model || '未知';
console.log(`更新图像操作按钮 - 当前模型: ${modelName}, 是否支持多模态: ${isMultimodalModel}`);
// 对于截图后的操作按钮显示逻辑
if (this.sendToClaudeBtn && this.extractTextBtn) {
if (!isMultimodalModel) {
// 非多模态模型:只显示提取文本按钮,隐藏发送到AI按钮
console.log('非多模态模型:隐藏"发送图片至AI"按钮');
this.sendToClaudeBtn.classList.add('hidden');
this.extractTextBtn.classList.remove('hidden');
} else {
// 多模态模型:显示两个按钮
if (!this.imagePreview.classList.contains('hidden')) {
// 只有在有图像时才显示按钮
console.log('多模态模型:显示全部按钮');
this.sendToClaudeBtn.classList.remove('hidden');
this.extractTextBtn.classList.remove('hidden');
} else {
// 无图像时隐藏所有按钮
console.log('无图像:隐藏所有按钮');
this.sendToClaudeBtn.classList.add('hidden');
this.extractTextBtn.classList.add('hidden');
}
}
} else {
console.warn('按钮元素不可用');
}
}
checkClickOutside() {
// 点击其他区域时自动关闭悬浮窗
document.addEventListener('click', (e) => {
// 检查是否点击在设置面板、设置按钮或其子元素之外
if (
!e.target.closest('#settingsPanel') &&
!e.target.matches('#settingsToggle') &&
!e.target.closest('#settingsToggle') &&
document.getElementById('settingsPanel').classList.contains('active')
) {
document.getElementById('settingsPanel').classList.remove('active');
}
// 检查是否点击在Claude面板、分析按钮或其子元素之外
if (
!e.target.closest('#claudePanel') &&
!e.target.matches('#sendToClaude') &&
!e.target.closest('#sendToClaude') &&
!e.target.matches('#extractText') &&
!e.target.closest('#extractText') &&
!e.target.matches('#sendExtractedText') &&
!e.target.closest('#sendExtractedText') &&
!this.claudePanel.classList.contains('hidden')
) {
// 因为分析可能正在进行,不自动关闭Claude面板
// 但是可以考虑增加一个最小化功能
}
});
}
// 新增清理方法,移除计时器相关代码
cleanup() {
console.log('执行清理操作...');
// 清除所有Socket监听器
if (this.socket) {
this.socket.off('text_extracted');
this.socket.off('screenshot_response');
this.socket.off('screenshot_complete');
this.socket.off('request_acknowledged');
this.socket.off('ai_response');
this.socket.off('thinking');
this.socket.off('thinking_complete');
this.socket.off('analysis_complete');
}
// 销毁裁剪器实例
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
console.log('清理完成');
}
// 空方法替代键盘快捷键实现
setupKeyboardShortcuts() {
// 移动端应用不需要键盘快捷键
console.log('键盘快捷键已禁用(移动端应用)');
}
// 控制思考动态省略号显示
showThinkingAnimation(show) {
const dotsElement = document.querySelector('.thinking-title .dots-animation');
if (dotsElement) {
if (show) {
dotsElement.style.display = 'inline-block';
} else {
dotsElement.style.display = 'none';
}
}
}
// 添加停止生成方法
stopGeneration() {
console.log('停止生成请求');
// 向服务器发送停止生成信号
if (this.socket && this.socket.connected) {
this.socket.emit('stop_generation');
// 显示提示
window.uiManager.showToast('正在停止生成...', 'info');
// 隐藏停止按钮
this.hideStopGenerationButton();
} else {
console.error('无法停止生成: Socket未连接');
window.uiManager.showToast('无法停止生成: 连接已断开', 'error');
}
}
// 显示停止生成按钮
showStopGenerationButton() {
if (this.stopGenerationBtn) {
this.stopGenerationBtn.classList.add('visible');
}
}
// 隐藏停止生成按钮
hideStopGenerationButton() {
if (this.stopGenerationBtn) {
this.stopGenerationBtn.classList.remove('visible');
}
}
}
// Initialize the application when the DOM is loaded
document.addEventListener('DOMContentLoaded', async () => {
try {
console.log('Initializing application...');
window.app = new SnapSolver();
await window.app.initialize();
console.log('Application initialized successfully');
} catch (error) {
console.error('Failed to initialize application:', error);
// 在页面上显示错误信息
const errorDiv = document.createElement('div');
errorDiv.className = 'init-error';
errorDiv.innerHTML = `
Initialization Error
${error.message}
${error.stack}
`;
document.body.appendChild(errorDiv);
}
});