// Handle status messages function showStatus(message, type = 'info') { const statusEl = document.getElementById('status-message'); statusEl.textContent = message; statusEl.classList.remove('hidden', 'bg-green-100', 'bg-red-100', 'bg-blue-100', 'text-green-800', 'text-red-800', 'text-blue-800'); if (type === 'success') { statusEl.classList.add('bg-green-100', 'text-green-800'); } else if (type === 'error') { statusEl.classList.add('bg-red-100', 'text-red-800'); } else { statusEl.classList.add('bg-blue-100', 'text-blue-800'); } // Also log to console appendToConsole(message, type === 'error' ? 'stderr' : 'info'); setTimeout(() => { statusEl.classList.add('hidden'); }, 5000); } // Get form values function getFormValues() { const microphoneValue = document.getElementById('microphone').value; // Convert to number if it's a numeric string (device index) const microphoneForConfig = /^\d+$/.test(microphoneValue) ? parseInt(microphoneValue) : microphoneValue; return { compute_type: document.getElementById('compute_type').value, enable_debug_mode: document.getElementById('enable_debug_mode').checked ? 1 : 0, enable_previews: document.getElementById('enable_previews').checked ? 1 : 0, save_audio: document.getElementById('save_audio').checked ? 1 : 0, language: document.getElementById('language').value, gpu_idx: parseInt(document.getElementById('gpu_idx').value), max_speech_duration_s: parseInt(document.getElementById('max_speech_duration_s').value), min_silence_duration_ms: parseInt(document.getElementById('min_silence_duration_ms').value), microphone: microphoneForConfig, model: document.getElementById('model').value, reset_after_silence_s: parseInt(document.getElementById('reset_after_silence_s').value), transcription_loop_delay_ms: parseInt(document.getElementById('transcription_loop_delay_ms').value), use_cpu: document.getElementById('use_cpu').checked ? 1 : 0, block_width: parseInt(document.getElementById('block_width').value), num_blocks: parseInt(document.getElementById('num_blocks').value), rows: parseInt(document.getElementById('rows').value), cols: parseInt(document.getElementById('cols').value) }; } // Add a flag to prevent auto-save during programmatic updates let isSettingValues = false; // Set form values function setFormValues(config) { isSettingValues = true; // Disable auto-save temporarily document.getElementById('compute_type').value = config.compute_type || 'int8'; document.getElementById('enable_debug_mode').checked = config.enable_debug_mode === 1; document.getElementById('enable_previews').checked = config.enable_previews === 1; document.getElementById('save_audio').checked = config.save_audio === 1; document.getElementById('language').value = config.language || 'english'; document.getElementById('gpu_idx').value = config.gpu_idx || 0; document.getElementById('max_speech_duration_s').value = config.max_speech_duration_s || 10; document.getElementById('min_silence_duration_ms').value = config.min_silence_duration_ms || 250; document.getElementById('microphone').value = config.microphone || 'motu'; document.getElementById('model').value = config.model || 'turbo'; document.getElementById('reset_after_silence_s').value = config.reset_after_silence_s || 15; document.getElementById('transcription_loop_delay_ms').value = config.transcription_loop_delay_ms || 100; document.getElementById('use_cpu').checked = config.use_cpu === 1; document.getElementById('block_width').value = config.block_width || 2; document.getElementById('num_blocks').value = config.num_blocks || 40; document.getElementById('rows').value = config.rows || 10; document.getElementById('cols').value = config.cols || 24; isSettingValues = false; // Re-enable auto-save } // Toggle advanced settings document.getElementById('toggle-advanced').addEventListener('click', () => { const advancedSettings = document.getElementById('advanced-settings'); const chevron = document.getElementById('chevron'); if (advancedSettings.classList.contains('hidden')) { advancedSettings.classList.remove('hidden'); chevron.classList.add('rotate-90'); } else { advancedSettings.classList.add('hidden'); chevron.classList.remove('rotate-90'); } }); // Simplify button handlers by extracting common patterns async function handleAsyncAction(actionName, actionFn) { try { const result = await actionFn(); if (result && result.message) { showStatus(result.message, 'success'); } return result; } catch (error) { showStatus(`${actionName} failed: ${error.message}`, 'error'); throw error; } } // Process control buttons const startButton = document.getElementById('start-process'); const stopButton = document.getElementById('stop-process'); // Helper functions for button state management function setButtonState(button, disabled) { button.disabled = disabled; if (disabled) { button.classList.add('opacity-50', 'cursor-not-allowed'); } else { button.classList.remove('opacity-50', 'cursor-not-allowed'); } } function setProcessRunningState() { setButtonState(startButton, true); setButtonState(stopButton, false); } function setProcessStoppedState() { setButtonState(startButton, false); setButtonState(stopButton, true); } // Auto-save functionality with debouncing let saveTimeout; const SAVE_DELAY = 500; // milliseconds async function autoSaveConfig() { if (isSettingValues) return; // Don't save during programmatic updates clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { try { const config = getFormValues(); await window.electronAPI.saveConfig(config); showStatus('Configuration saved', 'success'); // Check if process is running (stop button is enabled means process is running) const stopButton = document.getElementById('stop-process'); if (!stopButton.disabled) { // Process is running, restart it with new config appendToConsole('Restarting process with new configuration...', 'info'); try { await window.electronAPI.stopProcess(); await new Promise(resolve => setTimeout(resolve, 1000)); await window.electronAPI.startProcess(); // Update button states to reflect running process setProcessRunningState(); appendToConsole('Process restarted with new configuration', 'info'); } catch (error) { appendToConsole(`Failed to restart process: ${error.message}`, 'stderr'); // Process is stopped, update button states setProcessStoppedState(); } } } catch (error) { showStatus(`Failed to save configuration: ${error.message}`, 'error'); } }, SAVE_DELAY); } // Add event listeners to all form inputs for auto-save function setupAutoSave() { // Get all form inputs const form = document.getElementById('config-form'); const inputs = form.querySelectorAll('input, select'); // Add change listener to each input inputs.forEach(input => { if (input.type === 'checkbox') { input.addEventListener('change', autoSaveConfig); } else if (input.type === 'number' || input.type === 'text') { input.addEventListener('input', autoSaveConfig); } else if (input.tagName === 'SELECT') { input.addEventListener('change', autoSaveConfig); } }); } // Update the setup-venv handler document.getElementById('setup-venv').addEventListener('click', async () => { const setupButton = document.getElementById('setup-venv'); setupButton.disabled = true; setupButton.classList.add('opacity-50', 'cursor-not-allowed'); try { await handleAsyncAction('Install requirements', async () => { return await window.electronAPI.installRequirements(); }); // Reload microphones after successful installation await loadMicrophones(); } finally { setupButton.disabled = false; setupButton.classList.remove('opacity-50', 'cursor-not-allowed'); } }); // Simplified microphone loading async function loadMicrophones() { const microphoneSelect = document.getElementById('microphone'); try { appendToConsole('Loading available microphones...', 'info'); const microphones = await window.electronAPI.getMicrophones(); microphoneSelect.innerHTML = ''; if (microphones.length === 0) { microphoneSelect.innerHTML = ''; appendToConsole('No microphones found', 'stderr'); return; } appendToConsole(`Found ${microphones.length} microphone(s)`, 'info'); microphones.forEach(mic => { const option = document.createElement('option'); option.value = mic.index.toString(); option.textContent = mic.name; microphoneSelect.appendChild(option); appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout'); }); // Restore previously selected microphone if possible try { const config = await window.electronAPI.loadConfig(); if (config.microphone) { microphoneSelect.value = config.microphone; } } catch (error) { // Ignore config load errors here } } catch (error) { appendToConsole(`Failed to load microphones: ${error.message}`, 'stderr'); microphoneSelect.innerHTML = ''; } } // Update window load to include auto-save setup window.addEventListener('load', async () => { appendToConsole('TaSTT Configuration UI initialized', 'info'); // Load config first try { const config = await window.electronAPI.loadConfig(); setFormValues(config); appendToConsole('Configuration loaded', 'info'); } catch (error) { appendToConsole(`Failed to load configuration: ${error.message}`, 'stderr'); } // Load microphones await loadMicrophones(); // Set up auto-save after everything is loaded setupAutoSave(); }); // Console management const consoleContent = document.getElementById('console-content'); function appendToConsole(message, type = 'stdout') { const timestamp = new Date().toLocaleTimeString(); const timestampSpan = document.createElement('span'); timestampSpan.className = 'console-timestamp'; timestampSpan.textContent = `[${timestamp}] `; const messageSpan = document.createElement('span'); messageSpan.className = `console-${type}`; messageSpan.textContent = message; const lineDiv = document.createElement('div'); lineDiv.appendChild(timestampSpan); lineDiv.appendChild(messageSpan); consoleContent.appendChild(lineDiv); // Auto-scroll to bottom const pythonConsole = document.getElementById('python-console'); pythonConsole.scrollTop = pythonConsole.scrollHeight; } // Clear console button document.getElementById('clear-console').addEventListener('click', () => { consoleContent.innerHTML = ''; appendToConsole('Console cleared', 'info'); }); // Listen for Python output window.electronAPI.onPythonOutput((data) => { appendToConsole(data.message, data.type); }); document.getElementById('start-process').addEventListener('click', async () => { setButtonState(startButton, true); try { await window.electronAPI.startProcess(); setProcessRunningState(); appendToConsole('Process started successfully', 'info'); } catch (error) { appendToConsole(`Failed to start process: ${error.message}`, 'stderr'); setButtonState(startButton, false); } }); document.getElementById('stop-process').addEventListener('click', async () => { setButtonState(stopButton, true); try { const result = await window.electronAPI.stopProcess(); appendToConsole('Process stop initiated', 'info'); } catch (error) { appendToConsole(`Failed to stop process: ${error.message}`, 'stderr'); setButtonState(stopButton, false); } }); // Listen for process stopped event window.electronAPI.onProcessStopped(() => { setProcessStoppedState(); });