This commit is contained in:
Zylan
2025-02-04 20:52:02 +08:00
parent 75d6ff2c40
commit 65830eaea3
11 changed files with 2368 additions and 25 deletions

146
static/base.css Normal file
View File

@@ -0,0 +1,146 @@
/* CSS Variables */
:root {
--primary-color: #007bff;
--primary-color-rgb: 0, 123, 255;
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--bg-tertiary: #ffffff;
--bg-input: #ffffff;
--bg-button-secondary: #e9ecef;
--bg-toast: #333333;
--bg-toast-error: #dc3545;
--text-primary: #212529;
--text-secondary: #6c757d;
--text-toast: #ffffff;
--border-color: #dee2e6;
--status-idle: #6c757d;
--status-thinking: #ffc107;
--status-done: #28a745;
--error-color: #dc3545;
--highlight-color: rgba(0, 123, 255, 0.1);
--tooltip-bg: rgba(0, 0, 0, 0.8);
--tooltip-text: #ffffff;
}
/* Dark Theme */
[data-theme="dark"] {
--bg-primary: #212529;
--bg-secondary: #343a40;
--bg-tertiary: #2b3035;
--bg-input: #1a1d20;
--bg-button-secondary: #495057;
--text-primary: #f8f9fa;
--text-secondary: #adb5bd;
--border-color: #495057;
--tooltip-bg: rgba(255, 255, 255, 0.9);
--tooltip-text: #000000;
}
/* App Container */
.app-container {
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
background-color: var(--bg-primary);
color: var(--text-primary);
}
/* Header Styles */
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
background-color: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
}
.header-left {
display: flex;
align-items: center;
gap: 1rem;
}
.header-right {
display: flex;
gap: 0.5rem;
}
/* Main Content */
.app-main {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.content-panel {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Utility Classes */
.hidden {
display: none !important;
}
/* Animations */
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes highlight {
0% { background-color: var(--highlight-color); }
100% { background-color: transparent; }
}
/* Touch Interactions */
@media (hover: none) {
.btn-icon:active {
background-color: var(--bg-button-secondary);
}
.section-header:active {
background-color: var(--bg-tertiary);
}
}
/* Responsive Design */
@media (max-width: 768px) {
html {
font-size: 16px;
height: 100%;
overflow: hidden;
}
body {
height: 100%;
overflow: hidden;
}
.app-header {
padding: 0.5rem;
}
.app-header h1 {
font-size: 1.25rem;
}
.header-right {
gap: 0.25rem;
}
.btn-icon {
padding: 0.75rem;
}
}

717
static/components.css Normal file
View File

@@ -0,0 +1,717 @@
/* Capture Section */
.capture-section {
flex: 1;
display: flex;
flex-direction: column;
padding: 1rem;
gap: 1rem;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
}
.toolbar-buttons {
display: flex;
gap: 1rem;
}
/* Image Preview */
.image-preview {
flex: 1;
display: flex;
flex-direction: column;
gap: 1rem;
overflow: hidden;
}
.image-container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
background-color: var(--bg-secondary);
border-radius: 8px;
position: relative;
min-height: 200px;
max-height: 60vh;
}
.image-container img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
touch-action: pinch-zoom;
-webkit-user-select: none;
user-select: none;
}
/* Analysis Section */
.analysis-button {
display: flex;
flex-direction: column;
gap: 1rem;
}
.text-editor {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.text-editor textarea {
width: 100%;
resize: vertical;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.text-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
/* Claude Panel */
.claude-panel {
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
background-color: var(--bg-secondary);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.header-title {
display: flex;
align-items: center;
gap: 0.5rem;
}
.analysis-status {
display: flex;
align-items: center;
}
.status-light {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: var(--status-idle);
}
.status-light.thinking {
background-color: var(--status-thinking);
}
.status-light.done {
background-color: var(--status-done);
}
.response-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
/* Settings Panel */
.settings-panel {
position: absolute;
top: 0;
right: 0;
width: 400px;
height: 100%;
background-color: var(--bg-secondary);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.settings-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.settings-section {
margin-bottom: 1.5rem;
padding: 1rem;
border-radius: 8px;
background: var(--bg-tertiary);
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
margin: -0.75rem -0.75rem 0 -0.75rem;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
position: relative;
z-index: 1;
}
.section-header i {
font-size: 0.8em;
transition: transform 0.3s ease;
margin-left: 0.5rem;
}
.settings-section:not(.expanded) .section-header i {
transform: rotate(-90deg);
}
.section-content {
transition: all 0.3s ease;
overflow: hidden;
padding: 0 1rem;
}
.settings-section:not(.expanded) .section-content {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}
.settings-section.expanded .section-content {
max-height: 2000px;
padding-top: 1rem;
padding-bottom: 1rem;
opacity: 1;
}
/* Form Elements */
.setting-group {
margin-bottom: 1rem;
}
.setting-group label {
display: block;
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.setting-group label i {
color: var(--text-secondary);
cursor: help;
}
.setting-group input[type="text"],
.setting-group input[type="password"],
.setting-group input[type="number"],
.setting-group select,
.setting-group textarea {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border-color);
border-radius: 4px;
background-color: var(--bg-input);
color: var(--text-primary);
}
.setting-group textarea {
resize: vertical;
min-height: 100px;
}
.input-group {
position: relative;
display: flex;
align-items: center;
}
.input-group.highlight {
animation: highlight 0.2s ease-in-out;
}
.input-group input {
flex: 1;
padding-right: 2.5rem;
}
.input-group .btn-icon {
position: absolute;
right: 0.5rem;
}
/* History Panel */
.history-panel {
position: absolute;
top: 0;
right: 0;
width: 300px;
height: 100%;
background-color: var(--bg-secondary);
border-left: 1px solid var(--border-color);
display: flex;
flex-direction: column;
transition: transform 0.3s ease;
}
.history-content {
flex: 1;
padding: 1rem;
overflow-y: auto;
}
.history-item {
margin-bottom: 1rem;
background: var(--bg-tertiary);
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
border: 1px solid var(--border-color);
}
.history-item-header {
padding: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
user-select: none;
-webkit-tap-highlight-color: transparent;
position: relative;
background: var(--bg-secondary);
}
.history-image {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
}
.history-image img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
.history-empty {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 1rem;
color: var(--text-secondary);
padding: 2rem;
}
.history-empty i {
font-size: 3rem;
opacity: 0.5;
}
/* Crop Container */
.crop-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
display: flex;
flex-direction: column;
z-index: 1100;
}
.crop-wrapper {
flex: 1;
position: relative;
overflow: hidden;
background-color: var(--bg-secondary);
display: flex;
justify-content: center;
align-items: center;
}
.crop-area {
position: relative;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
background-color: var(--bg-secondary);
}
.crop-area img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
/* Cropper.js Customization */
.cropper-container {
direction: ltr;
font-size: 0;
line-height: 0;
position: relative;
touch-action: none;
user-select: none;
}
.cropper-wrap-box,
.cropper-canvas,
.cropper-drag-box,
.cropper-crop-box,
.cropper-modal {
bottom: 0;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.cropper-wrap-box {
overflow: hidden;
}
.cropper-drag-box {
background-color: #fff;
opacity: 0;
}
.cropper-modal {
background-color: var(--bg-primary);
opacity: 0.5;
}
.cropper-view-box {
display: block;
height: 100%;
outline: 1px solid var(--primary-color);
outline-color: var(--primary-color);
overflow: hidden;
width: 100%;
}
.cropper-dashed {
border: 0 dashed #eee;
display: block;
opacity: 0.5;
position: absolute;
}
.cropper-center {
display: block;
height: 0;
left: 50%;
opacity: 0.75;
position: absolute;
top: 50%;
width: 0;
}
.cropper-center::before,
.cropper-center::after {
background-color: #eee;
content: " ";
display: block;
position: absolute;
}
.cropper-face {
background-color: #fff;
left: 0;
opacity: 0.1;
position: absolute;
top: 0;
}
.cropper-line {
background-color: var(--primary-color);
display: block;
height: 100%;
opacity: 0.1;
position: absolute;
width: 100%;
}
.cropper-point {
background-color: var(--primary-color);
height: 5px;
opacity: 0.75;
position: absolute;
width: 5px;
}
.crop-actions {
height: 80px;
padding: 1rem;
display: flex;
justify-content: center;
gap: 1rem;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}
.crop-actions button {
flex: 1;
max-width: 160px;
padding: 0.75rem;
font-size: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
/* Toast Container */
.toast-container {
position: fixed;
bottom: 1rem;
right: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
z-index: 2000;
}
.toast {
padding: 0.75rem 1rem;
border-radius: 4px;
background-color: var(--bg-toast);
color: var(--text-toast);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
animation: slideIn 0.3s ease;
}
.toast.error {
background-color: var(--bg-toast-error);
}
/* Button Styles */
.btn-primary,
.btn-secondary {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-secondary {
background-color: var(--bg-button-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
}
.btn-icon {
padding: 0.5rem;
background: none;
border: none;
color: var(--text-primary);
cursor: pointer;
transition: color 0.2s;
}
.btn-icon:hover {
color: var(--primary-color);
}
.button-group {
display: flex;
gap: 0.5rem;
}
/* Tooltip styles */
[title] {
position: relative;
}
[title]:hover::after {
content: attr(title);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
padding: 0.5rem;
background: var(--tooltip-bg);
color: var(--tooltip-text);
border-radius: 4px;
font-size: 0.8em;
white-space: nowrap;
z-index: 1000;
}
/* Responsive Design for Components */
@media (max-width: 768px) {
.settings-content,
.history-content,
.response-content {
-webkit-overflow-scrolling: touch;
}
.capture-section {
padding: 0.5rem;
}
.toolbar {
flex-wrap: wrap;
}
.button-group {
width: 100%;
}
.button-group button {
flex: 1;
padding: 0.75rem;
font-size: 0.9rem;
}
.image-container {
margin: 0 -0.5rem;
border-radius: 0;
background-color: var(--bg-primary);
}
.image-container img {
width: 100%;
height: auto;
object-fit: contain;
}
.settings-panel,
.history-panel,
.claude-panel {
width: 100%;
position: fixed;
bottom: 0;
right: 0;
height: 90vh;
transform: translateY(100%);
border-top-left-radius: 16px;
border-top-right-radius: 16px;
border-left: none;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.settings-panel:not(.hidden),
.history-panel:not(.hidden),
.claude-panel:not(.hidden) {
transform: translateY(0);
}
.settings-content,
.history-content {
padding: 0.75rem;
}
.settings-section {
margin-bottom: 1rem;
padding: 0.75rem;
}
.setting-group {
margin-bottom: 0.75rem;
}
.setting-group input[type="text"],
.setting-group input[type="password"],
.setting-group input[type="number"],
.setting-group select,
.setting-group textarea {
padding: 0.75rem;
font-size: 1rem;
}
.crop-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: var(--bg-primary);
z-index: 1100;
}
.crop-wrapper {
height: calc(100% - 80px);
overflow: hidden;
background-color: var(--bg-secondary);
}
.crop-area {
max-width: 100%;
max-height: 100%;
}
.crop-actions {
height: 80px;
padding: 1rem;
background-color: var(--bg-secondary);
border-top: 1px solid var(--border-color);
}
.crop-actions button {
padding: 0.75rem 1.5rem;
font-size: 1rem;
}
.text-editor textarea {
padding: 0.75rem;
font-size: 1rem;
min-height: 120px;
}
.text-actions {
flex-direction: column;
gap: 0.75rem;
align-items: stretch;
}
.text-actions button {
width: 100%;
padding: 0.75rem;
}
.toast-container {
left: 1rem;
right: 1rem;
bottom: 1rem;
}
.toast {
width: 100%;
padding: 1rem;
font-size: 0.9rem;
text-align: center;
}
}

603
static/js/core.js Normal file
View File

@@ -0,0 +1,603 @@
class SnapSolver {
constructor() {
// Initialize managers first
window.uiManager = new UIManager();
window.settingsManager = new SettingsManager();
this.initializeElements();
this.initializeState();
this.initializeConnection();
this.setupAutoScroll();
this.setupEventListeners();
}
initializeElements() {
// Capture elements
this.captureBtn = document.getElementById('captureBtn');
this.cropBtn = document.getElementById('cropBtn');
this.connectionStatus = document.getElementById('connectionStatus');
this.screenshotImg = document.getElementById('screenshotImg');
this.cropContainer = document.getElementById('cropContainer');
this.imagePreview = document.getElementById('imagePreview');
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.responseContent = document.getElementById('responseContent');
this.claudePanel = document.getElementById('claudePanel');
this.statusLight = document.querySelector('.status-light');
// Verify all elements are found
const elements = [
this.captureBtn, this.cropBtn, this.connectionStatus, this.screenshotImg,
this.cropContainer, this.imagePreview, this.sendToClaudeBtn, this.extractTextBtn,
this.textEditor, this.extractedText, this.sendExtractedTextBtn, this.responseContent,
this.claudePanel, this.statusLight
];
elements.forEach((element, index) => {
if (!element) {
console.error(`Failed to initialize element at index ${index}`);
}
});
}
initializeState() {
this.socket = null;
this.cropper = null;
this.croppedImage = null;
this.history = JSON.parse(localStorage.getItem('snapHistory') || '[]');
this.heartbeatInterval = null;
this.connectionCheckInterval = null;
this.isReconnecting = false;
this.lastConnectionAttempt = 0;
}
resetConnection() {
const now = Date.now();
const timeSinceLastAttempt = now - this.lastConnectionAttempt;
// Prevent multiple reset attempts within 2 seconds
if (timeSinceLastAttempt < 2000) {
console.log('Skipping reset - too soon since last attempt');
return;
}
console.log('Resetting connection...');
this.isReconnecting = true;
this.lastConnectionAttempt = now;
// Clear existing intervals
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
this.connectionCheckInterval = null;
}
// Clean up existing socket
if (this.socket) {
this.socket.removeAllListeners();
this.socket.disconnect();
this.socket = null;
}
// Small delay before reconnecting
setTimeout(() => {
this.initializeConnection();
this.isReconnecting = false;
}, 100);
}
startConnectionCheck() {
// Clear any existing interval
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
}
// Check connection status every 5 seconds
this.connectionCheckInterval = setInterval(() => {
if (!this.isReconnecting && (!this.socket || !this.socket.connected)) {
console.log('Connection check failed, attempting reset...');
this.resetConnection();
}
}, 5000);
}
initializeCropper() {
try {
// Clean up existing cropper if any
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
// Show crop container and prepare crop area
this.cropContainer.classList.remove('hidden');
const cropArea = document.querySelector('.crop-area');
cropArea.innerHTML = '';
// Create a new image element for cropping
const cropImage = document.createElement('img');
cropImage.src = this.screenshotImg.src;
cropArea.appendChild(cropImage);
// Initialize Cropper.js
this.cropper = new Cropper(cropImage, {
aspectRatio: NaN,
viewMode: 1,
dragMode: 'move',
autoCropArea: 0.8,
restore: false,
modal: true,
guides: true,
highlight: true,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
ready: () => {
console.log('Cropper initialized successfully');
},
error: (error) => {
console.error('Cropper initialization error:', error);
window.showToast('Failed to initialize image cropper', 'error');
}
});
} catch (error) {
console.error('Error initializing cropper:', error);
window.showToast('Failed to initialize image cropper', 'error');
}
}
setupAutoScroll() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'characterData' || mutation.type === 'childList') {
this.responseContent.scrollTo({
top: this.responseContent.scrollHeight,
behavior: 'smooth'
});
}
});
});
observer.observe(this.responseContent, {
childList: true,
characterData: true,
subtree: true
});
}
updateConnectionStatus(connected) {
if (!this.connectionStatus || !this.captureBtn) {
console.error('Required elements not initialized');
return;
}
this.connectionStatus.textContent = connected ? 'Connected' : 'Disconnected';
this.connectionStatus.className = `status ${connected ? 'connected' : 'disconnected'}`;
// Enable/disable capture button
if (this.captureBtn) {
this.captureBtn.disabled = !connected;
}
if (!connected) {
// Hide UI elements when disconnected
const elements = [
this.imagePreview,
this.cropBtn,
this.sendToClaudeBtn,
this.extractTextBtn,
this.textEditor
];
elements.forEach(element => {
if (element) {
element.classList.add('hidden');
}
});
}
}
updateStatusLight(status) {
if (!this.statusLight) return;
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:
break;
}
}
initializeConnection() {
// Clear any existing heartbeat interval
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
try {
// Clean up existing socket if any
if (this.socket) {
this.socket.disconnect();
this.socket = null;
}
console.log('Initializing socket connection...');
this.socket = io(window.location.origin, {
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 100, // Very fast initial reconnection
reconnectionDelayMax: 1000, // Shorter max delay
timeout: 120000,
autoConnect: true, // Enable auto-connect
transports: ['websocket'],
forceNew: true, // Force a new connection on refresh
closeOnBeforeunload: false, // Prevent auto-close on page refresh
reconnectionAttempts: Infinity, // Never stop trying to reconnect
extraHeaders: {
'X-Client-Version': '1.0'
}
});
// Setup heartbeat with monitoring
this.heartbeatInterval = setInterval(() => {
if (this.socket && this.socket.connected) {
const heartbeatTimeout = setTimeout(() => {
console.log('Heartbeat timeout, resetting connection...');
if (!this.isReconnecting) {
this.resetConnection();
}
}, 5000); // Wait 5 seconds for heartbeat response
this.socket.emit('heartbeat');
// Clear timeout when heartbeat is acknowledged
this.socket.once('heartbeat_response', () => {
clearTimeout(heartbeatTimeout);
});
}
}, 10000);
this.socket.on('connect', () => {
console.log('Connected to server');
this.updateConnectionStatus(true);
// Re-enable capture button on reconnection
if (this.captureBtn) {
this.captureBtn.disabled = false;
}
// Start connection check after successful connection
this.startConnectionCheck();
});
this.socket.on('disconnect', (reason) => {
console.log('Disconnected from server:', reason);
this.updateConnectionStatus(false);
// Always attempt to reconnect regardless of reason
console.log('Attempting reconnection...');
if (!this.socket.connected && !this.isReconnecting) {
this.resetConnection();
}
// Clean up resources but maintain reconnection ability
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
});
// Add reconnecting event handler
this.socket.on('reconnecting', (attemptNumber) => {
console.log(`Reconnection attempt ${attemptNumber}...`);
if (!this.isReconnecting) {
this.resetConnection();
}
});
// Add reconnect_failed event handler
this.socket.on('reconnect_failed', () => {
console.log('Reconnection failed, trying again...');
if (!this.isReconnecting) {
this.resetConnection();
}
});
this.socket.on('connect_error', (error) => {
console.error('Connection error:', error);
this.updateConnectionStatus(false);
window.showToast('Connection error: ' + error.message, 'error');
// Enhanced exponential backoff with jitter
const attempts = this.socket.io.backoff?.attempts || 0;
const baseDelay = 1000;
const maxDelay = 10000;
const jitter = Math.random() * 1000;
const delay = Math.min(baseDelay * Math.pow(1.5, attempts) + jitter, maxDelay);
console.log(`Scheduling reconnection attempt in ${Math.round(delay)}ms...`);
setTimeout(() => {
if (!this.socket.connected) {
console.log(`Attempting to reconnect (attempt ${attempts + 1})...`);
this.socket.connect();
}
}, delay);
});
this.socket.on('heartbeat_response', () => {
console.debug('Heartbeat acknowledged');
// Reset connection if we were in a disconnected state
if (this.connectionStatus && this.connectionStatus.textContent === 'Disconnected' && !this.isReconnecting) {
this.resetConnection();
}
});
this.socket.on('error', (error) => {
console.error('Socket error:', error);
window.showToast('Socket error occurred', 'error');
});
this.setupSocketEventHandlers();
} catch (error) {
console.error('Connection initialization error:', error);
this.updateConnectionStatus(false);
}
}
setupSocketEventHandlers() {
if (!this.socket) {
console.error('Socket not initialized');
return;
}
// 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>';
}
});
// Mathpix text extraction response handler
this.socket.on('mathpix_response', (data) => {
console.log('Received mathpix_response:', data);
this.updateStatusLight(data.status);
switch (data.status) {
case 'started':
console.log('Text extraction started');
this.extractedText.value = '';
this.extractTextBtn.disabled = true;
break;
case 'completed':
if (data.content) {
console.log('Received extracted text:', data.content);
const confidenceMatch = data.content.match(/Confidence: (\d+\.\d+)%/);
if (confidenceMatch) {
const confidence = confidenceMatch[1];
document.getElementById('confidenceDisplay').textContent = confidence + '%';
this.extractedText.value = data.content.replace(/Confidence: \d+\.\d+%\n\n/, '');
} else {
this.extractedText.value = data.content;
document.getElementById('confidenceDisplay').textContent = '';
}
this.textEditor.classList.remove('hidden');
}
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
window.showToast('Text extracted successfully');
break;
case 'error':
console.error('Text extraction error:', data.error);
const errorMessage = data.error || 'Unknown error occurred';
window.showToast('Failed to extract text: ' + errorMessage, 'error');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
break;
default:
console.warn('Unknown mathpix response status:', data.status);
if (data.error) {
window.showToast('Text extraction failed: ' + data.error, 'error');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
}
}
});
// AI analysis response handler
this.socket.on('claude_response', (data) => {
console.log('Received claude_response:', data);
this.updateStatusLight(data.status);
switch (data.status) {
case 'started':
console.log('AI analysis started');
this.responseContent.textContent = '';
this.sendToClaudeBtn.disabled = true;
this.sendExtractedTextBtn.disabled = true;
break;
case 'streaming':
if (data.content) {
console.log('Received AI content:', data.content);
this.responseContent.textContent += data.content;
}
break;
case 'completed':
if (data.content) {
console.log('Received final AI content:', data.content);
this.responseContent.textContent += data.content;
}
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
this.addToHistory(this.croppedImage, this.responseContent.textContent);
window.showToast('Analysis completed successfully');
break;
case 'error':
console.error('AI analysis error:', data.error);
const errorMessage = data.error || 'Unknown error occurred';
this.responseContent.textContent += '\nError: ' + errorMessage;
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
window.showToast('Analysis failed: ' + errorMessage, 'error');
break;
default:
console.warn('Unknown claude response status:', data.status);
if (data.error) {
this.responseContent.textContent += '\nError: ' + data.error;
this.sendToClaudeBtn.disabled = false;
this.sendExtractedTextBtn.disabled = false;
window.showToast('Unknown error occurred', 'error');
}
}
});
}
setupEventListeners() {
// Add click handler for app title
const appTitle = document.getElementById('appTitle');
if (appTitle) {
appTitle.addEventListener('click', () => {
this.resetInterface();
});
}
// Handle page visibility changes
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
console.log('Page became visible, checking connection...');
// Check connection status and reset if needed
if (this.socket && !this.socket.connected && !this.isReconnecting) {
console.log('Connection lost while page was hidden, resetting...');
this.resetConnection();
}
}
});
// Handle before unload to clean up properly
window.addEventListener('beforeunload', () => {
if (this.socket) {
console.log('Page unloading, cleaning up socket...');
// Store connection state in sessionStorage
sessionStorage.setItem('wasConnected', 'true');
this.socket.disconnect();
}
});
// Check if we need to reconnect after a page reload
if (sessionStorage.getItem('wasConnected') === 'true') {
console.log('Page reloaded, initiating immediate reconnection...');
sessionStorage.removeItem('wasConnected');
// Force an immediate connection attempt
setTimeout(() => {
if (!this.socket?.connected && !this.isReconnecting) {
this.resetConnection();
}
}, 500);
}
this.setupCaptureEvents();
this.setupCropEvents();
this.setupAnalysisEvents();
this.setupKeyboardShortcuts();
}
resetInterface() {
if (!this.captureBtn) return;
// Clear all intervals
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (this.connectionCheckInterval) {
clearInterval(this.connectionCheckInterval);
this.connectionCheckInterval = null;
}
// Clean up cropper if it exists
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
// Clean up socket if it exists
if (this.socket) {
this.socket.removeAllListeners(); // Remove all event listeners
this.socket.disconnect();
this.socket = null;
}
// Show capture button
this.captureBtn.classList.remove('hidden');
// Hide all panels
const panels = ['historyPanel', 'settingsPanel'];
panels.forEach(panelId => {
const panel = document.getElementById(panelId);
if (panel) panel.classList.add('hidden');
});
// Reset image preview and related buttons
const elements = [
this.imagePreview,
this.cropBtn,
this.sendToClaudeBtn,
this.extractTextBtn,
this.textEditor
];
elements.forEach(element => {
if (element) element.classList.add('hidden');
});
// Clear text areas
if (this.extractedText) this.extractedText.value = '';
if (this.responseContent) this.responseContent.textContent = '';
const confidenceDisplay = document.getElementById('confidenceDisplay');
if (confidenceDisplay) confidenceDisplay.textContent = '';
// Hide Claude panel
if (this.claudePanel) this.claudePanel.classList.add('hidden');
}
}
// Initialize the application
window.addEventListener('DOMContentLoaded', () => {
window.app = new SnapSolver();
});

385
static/js/events.js Normal file
View File

@@ -0,0 +1,385 @@
// Events handling extension for SnapSolver class
Object.assign(SnapSolver.prototype, {
setupCaptureEvents() {
if (!this.captureBtn) {
console.error('Capture button not initialized');
return;
}
// Capture button
this.captureBtn.addEventListener('click', async () => {
if (!this.socket) {
console.error('Socket not initialized');
window.showToast('Connection not initialized. Please refresh the page.', 'error');
return;
}
if (!this.socket.connected) {
console.error('Socket not connected');
window.showToast('Server connection lost. Attempting to reconnect...', 'error');
this.socket.connect();
return;
}
try {
this.captureBtn.disabled = true;
this.captureBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Capturing...</span>';
// Set a timeout to re-enable the button if no response is received
const timeout = setTimeout(() => {
if (this.captureBtn.disabled) {
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
window.showToast('Screenshot capture timed out. Please try again.', 'error');
}
}, 10000);
this.socket.emit('request_screenshot', null, (error) => {
if (error) {
clearTimeout(timeout);
console.error('Screenshot error:', error);
window.showToast('Error capturing screenshot: ' + error, 'error');
this.captureBtn.disabled = false;
this.captureBtn.innerHTML = '<i class="fas fa-camera"></i><span>Capture</span>';
}
});
} catch (error) {
console.error('Capture error:', 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() {
if (!this.cropBtn || !this.screenshotImg) {
console.error('Required elements not initialized');
return;
}
// Crop button
this.cropBtn.addEventListener('click', () => {
if (this.screenshotImg.src) {
this.initializeCropper();
}
});
// Crop confirm button
const cropConfirm = document.getElementById('cropConfirm');
if (cropConfirm) {
cropConfirm.addEventListener('click', () => {
if (this.cropper) {
try {
console.log('Starting crop operation...');
if (!this.cropper) {
throw new Error('Cropper not initialized');
}
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).');
}
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');
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.');
}
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
this.cropContainer.classList.add('hidden');
const cropArea = document.querySelector('.crop-area');
if (cropArea) cropArea.innerHTML = '';
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 {
if (this.cropper) {
this.cropper.destroy();
this.cropper = null;
}
}
}
});
}
// Crop cancel button
const cropCancel = document.getElementById('cropCancel');
if (cropCancel) {
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');
const cropArea = document.querySelector('.crop-area');
if (cropArea) cropArea.innerHTML = '';
});
}
},
setupAnalysisEvents() {
// Set up text extraction socket event listener once
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;
}
} else if (data.content) {
if (this.extractedText) {
this.extractedText.value = data.content;
this.extractedText.disabled = false;
// Scroll to make text editor visible
this.extractedText.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
window.showToast('Text extracted successfully');
}
if (this.extractTextBtn) {
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
}
});
// Extract Text button
if (this.extractTextBtn) {
this.extractTextBtn.addEventListener('click', () => {
if (!this.croppedImage) {
window.showToast('Please crop the image first', 'error');
return;
}
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');
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) settingsPanel.classList.remove('hidden');
return;
}
this.extractTextBtn.disabled = true;
this.extractTextBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i><span>Extracting...</span>';
try {
// Show text editor and prepare UI
const textEditor = document.getElementById('textEditor');
if (textEditor) {
textEditor.classList.remove('hidden');
// Scroll to make text editor visible
textEditor.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
// Clear any previous text and show loading indicator
if (this.extractedText) {
this.extractedText.value = 'Extracting text...';
this.extractedText.disabled = true;
}
// Set up timeout to re-enable button if no response
const timeout = setTimeout(() => {
if (this.extractTextBtn && this.extractTextBtn.disabled) {
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
if (this.extractedText) {
this.extractedText.value = '';
this.extractedText.disabled = false;
}
window.showToast('Text extraction timed out. Please try again.', 'error');
}
}, 30000); // 30 second timeout
this.socket.emit('extract_text', {
image: this.croppedImage.split(',')[1],
settings: {
mathpixApiKey: `${mathpixAppId}:${mathpixAppKey}`
}
}, (error) => {
// Clear timeout on acknowledgement
clearTimeout(timeout);
if (error) {
console.error('Text extraction error:', error);
window.showToast('Failed to start text extraction: ' + error, 'error');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
if (this.extractedText) {
this.extractedText.value = '';
this.extractedText.disabled = false;
}
}
});
} catch (error) {
console.error('Text extraction error:', error);
window.showToast('Failed to extract text: ' + error.message, 'error');
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
if (this.extractedText) {
this.extractedText.value = '';
this.extractedText.disabled = false;
}
}
});
}
// Send Extracted Text button
if (this.sendExtractedTextBtn && this.extractedText) {
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 apiKey = window.settingsManager.getApiKey();
if (!apiKey) {
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) settingsPanel.classList.remove('hidden');
window.showToast('Please configure API key in settings', 'error');
return;
}
if (this.claudePanel) this.claudePanel.classList.remove('hidden');
if (this.responseContent) this.responseContent.textContent = '';
this.sendExtractedTextBtn.disabled = true;
try {
this.socket.emit('analyze_text', {
text: text,
settings: {
apiKey: apiKey,
model: settings.model || 'claude-3-5-sonnet-20241022',
temperature: parseFloat(settings.temperature) || 0.7,
systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.',
proxyEnabled: settings.proxyEnabled || false,
proxyHost: settings.proxyHost || '127.0.0.1',
proxyPort: settings.proxyPort || '4780'
}
});
} catch (error) {
console.error('Text analysis error:', error);
if (this.responseContent) {
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
if (this.sendToClaudeBtn) {
this.sendToClaudeBtn.addEventListener('click', () => {
if (!this.croppedImage) {
window.showToast('Please crop the image first', 'error');
return;
}
const settings = window.settingsManager.getSettings();
const apiKey = window.settingsManager.getApiKey();
if (!apiKey) {
const settingsPanel = document.getElementById('settingsPanel');
if (settingsPanel) settingsPanel.classList.remove('hidden');
window.showToast('Please configure API key in settings', 'error');
return;
}
if (this.claudePanel) this.claudePanel.classList.remove('hidden');
if (this.responseContent) this.responseContent.textContent = '';
this.sendToClaudeBtn.disabled = true;
try {
this.socket.emit('analyze_image', {
image: this.croppedImage.split(',')[1],
settings: {
apiKey: apiKey,
model: settings.model || 'claude-3-5-sonnet-20241022',
temperature: parseFloat(settings.temperature) || 0.7,
systemPrompt: settings.systemPrompt || 'You are an expert at analyzing questions and providing detailed solutions.',
proxyEnabled: settings.proxyEnabled || false,
proxyHost: settings.proxyHost || '127.0.0.1',
proxyPort: settings.proxyPort || '4780'
}
});
} catch (error) {
console.error('Image analysis error:', error);
if (this.responseContent) {
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 && !this.captureBtn.disabled) {
this.captureBtn.click();
}
break;
case 'x':
if (this.cropBtn && !this.cropBtn.disabled) {
this.cropBtn.click();
}
break;
}
}
});
}
});

93
static/js/history.js Normal file
View File

@@ -0,0 +1,93 @@
// History management extension for SnapSolver class
Object.assign(SnapSolver.prototype, {
addToHistory(imageData, response) {
const historyItem = {
id: Date.now(),
timestamp: new Date().toISOString(),
image: imageData,
extractedText: this.extractedText.value || null,
response: response
};
this.history.unshift(historyItem);
if (this.history.length > 10) this.history.pop();
localStorage.setItem('snapHistory', JSON.stringify(this.history));
window.renderHistory();
}
});
// Global function for history rendering
window.renderHistory = function() {
const content = document.querySelector('.history-content');
const history = JSON.parse(localStorage.getItem('snapHistory') || '[]');
if (history.length === 0) {
content.innerHTML = `
<div class="history-empty">
<i class="fas fa-history"></i>
<p>No history yet</p>
</div>
`;
return;
}
content.innerHTML = history.map(item => `
<div class="history-item" data-id="${item.id}">
<div class="history-item-header">
<span>${new Date(item.timestamp).toLocaleString()}</span>
<button class="btn-icon delete-history" data-id="${item.id}">
<i class="fas fa-trash"></i>
</button>
</div>
<img src="${item.image}" alt="Historical screenshot" class="history-image">
</div>
`).join('');
// Add click handlers for history items
content.querySelectorAll('.delete-history').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = parseInt(btn.dataset.id);
const updatedHistory = history.filter(item => item.id !== id);
localStorage.setItem('snapHistory', JSON.stringify(updatedHistory));
window.renderHistory();
window.showToast('History item deleted');
});
});
content.querySelectorAll('.history-item').forEach(item => {
item.addEventListener('click', () => {
const historyItem = history.find(h => h.id === parseInt(item.dataset.id));
if (historyItem) {
// Display the image
window.app.screenshotImg.src = historyItem.image;
window.app.imagePreview.classList.remove('hidden');
document.getElementById('historyPanel').classList.add('hidden');
// Hide all action buttons in history view
window.app.cropBtn.classList.add('hidden');
window.app.captureBtn.classList.add('hidden');
window.app.sendToClaudeBtn.classList.add('hidden');
window.app.extractTextBtn.classList.add('hidden');
window.app.sendExtractedTextBtn.classList.add('hidden');
// Reset confidence display
document.getElementById('confidenceDisplay').textContent = '';
// Only show text editor if there was extracted text
if (historyItem.extractedText) {
window.app.textEditor.classList.remove('hidden');
window.app.extractedText.value = historyItem.extractedText;
} else {
window.app.textEditor.classList.add('hidden');
window.app.extractedText.value = '';
}
// Show response if it exists
if (historyItem.response) {
window.app.claudePanel.classList.remove('hidden');
window.app.responseContent.textContent = historyItem.response;
}
}
});
});
};

View File

@@ -136,14 +136,24 @@ class SnapSolver {
});
// Text extraction response handler
this.socket.on('text_extraction_response', (data) => {
if (data.success) {
this.extractedText.value = data.text;
this.textEditor.classList.remove('hidden');
window.showToast('Text extracted successfully');
} else {
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;
}
} else if (data.content) {
if (this.extractedText) {
this.extractedText.value = data.content;
this.extractedText.disabled = false;
// Scroll to make text editor visible
this.extractedText.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
window.showToast('Text extracted successfully');
}
this.extractTextBtn.disabled = false;
this.extractTextBtn.innerHTML = '<i class="fas fa-font"></i><span>Extract Text</span>';
});
@@ -405,9 +415,31 @@ class SnapSolver {
this.extractTextBtn.disabled = true;
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 {
this.socket.emit('extract_text', {
image: this.croppedImage.split(',')[1]
image: this.croppedImage.split(',')[1],
settings: {
mathpixApiKey: `${mathpixAppId}:${mathpixAppKey}`
}
});
} catch (error) {
window.showToast('Failed to extract text: ' + error.message, 'error');

View File

@@ -18,6 +18,10 @@ class SettingsManager {
this.proxyPortInput = document.getElementById('proxyPort');
this.proxySettings = document.getElementById('proxySettings');
// Initialize Mathpix inputs
this.mathpixAppIdInput = document.getElementById('mathpixAppId');
this.mathpixAppKeyInput = document.getElementById('mathpixAppKey');
// API Key elements
this.apiKeyInputs = {
'claude-3-5-sonnet-20241022': document.getElementById('claudeApiKey'),
@@ -47,6 +51,14 @@ class SettingsManager {
loadSettings() {
const settings = JSON.parse(localStorage.getItem('aiSettings') || '{}');
// Load Mathpix credentials
if (settings.mathpixAppId) {
this.mathpixAppIdInput.value = settings.mathpixAppId;
}
if (settings.mathpixAppKey) {
this.mathpixAppKeyInput.value = settings.mathpixAppKey;
}
// Load API keys
if (settings.apiKeys) {
Object.entries(this.apiKeyInputs).forEach(([model, input]) => {
@@ -89,6 +101,8 @@ class SettingsManager {
saveSettings() {
const settings = {
apiKeys: {},
mathpixAppId: this.mathpixAppIdInput.value,
mathpixAppKey: this.mathpixAppKeyInput.value,
model: this.modelSelect.value,
temperature: this.temperatureInput.value,
language: this.languageInput.value,
@@ -129,7 +143,9 @@ class SettingsManager {
systemPrompt: this.systemPromptInput.value + ` Please respond in ${this.languageInput.value}.`,
proxyEnabled: this.proxyEnabledInput.checked,
proxyHost: this.proxyHostInput.value,
proxyPort: this.proxyPortInput.value
proxyPort: this.proxyPortInput.value,
mathpixAppId: this.mathpixAppIdInput.value,
mathpixAppKey: this.mathpixAppKeyInput.value
};
}
@@ -139,6 +155,10 @@ class SettingsManager {
input.addEventListener('change', () => this.saveSettings());
});
// Save Mathpix settings on change
this.mathpixAppIdInput.addEventListener('change', () => this.saveSettings());
this.mathpixAppKeyInput.addEventListener('change', () => this.saveSettings());
this.modelSelect.addEventListener('change', (e) => {
this.updateVisibleApiKey(e.target.value);
this.saveSettings();