summaryrefslogtreecommitdiffstats
path: root/ui/renderer.js
diff options
context:
space:
mode:
Diffstat (limited to 'ui/renderer.js')
-rw-r--r--ui/renderer.js564
1 files changed, 353 insertions, 211 deletions
diff --git a/ui/renderer.js b/ui/renderer.js
index b3f05a6..201eef6 100644
--- a/ui/renderer.js
+++ b/ui/renderer.js
@@ -1,99 +1,220 @@
-// Handle status messages
+// Configuration and form field mappings
+const CONFIG_FIELDS = {
+ // String fields
+ compute_type: { type: 'select', default: 'float16' },
+ language: { type: 'select', default: 'english' },
+ model: { type: 'select', default: 'turbo' },
+ microphone: { type: 'number', default: 0 },
+ user_prompt: { type: 'text', default: '' },
+
+ // Number fields
+ gpu_idx: { type: 'number', default: 0 },
+ max_speech_duration_s: { type: 'number', default: 10 },
+ min_silence_duration_ms: { type: 'number', default: 250 },
+ reset_after_silence_s: { type: 'number', default: 15 },
+ transcription_loop_delay_ms: { type: 'number', default: 100 },
+ block_width: { type: 'number', default: 2 },
+ num_blocks: { type: 'number', default: 40 },
+ rows: { type: 'number', default: 10 },
+ cols: { type: 'number', default: 24 },
+
+ // Boolean fields (stored as 1/0)
+ enable_debug_mode: { type: 'boolean', default: 0 },
+ enable_previews: { type: 'boolean', default: 1 },
+ save_audio: { type: 'boolean', default: 0 },
+ use_cpu: { type: 'boolean', default: 0 }
+};
+
+// Button management system
+class ButtonManager {
+ constructor() {
+ this.buttons = {
+ start: document.getElementById('start-process'),
+ stop: document.getElementById('stop-process'),
+ setupVenv: document.getElementById('setup-venv'),
+ resetVenv: document.getElementById('reset-venv'),
+ refreshMicrophones: document.getElementById('refresh-microphones')
+ };
+ }
+
+ setState(buttonName, disabled) {
+ const button = this.buttons[buttonName];
+ if (!button) return;
+
+ button.disabled = disabled;
+ if (disabled) {
+ button.classList.add('opacity-50', 'cursor-not-allowed');
+ } else {
+ button.classList.remove('opacity-50', 'cursor-not-allowed');
+ }
+ }
+
+ setProcessRunning() {
+ this.setState('start', true);
+ this.setState('stop', false);
+ }
+
+ setProcessStopped() {
+ this.setState('start', false);
+ this.setState('stop', true);
+ }
+
+ async withButtonLoading(buttonName, asyncFn) {
+ this.setState(buttonName, true);
+ try {
+ return await asyncFn();
+ } finally {
+ this.setState(buttonName, false);
+ }
+ }
+}
+
+const buttonManager = new ButtonManager();
+
+// Add loading overlay management
+class LoadingOverlay {
+ constructor() {
+ this.overlay = document.getElementById('loading-overlay');
+ this.form = document.getElementById('config-form');
+ this.messageElement = this.overlay.querySelector('p');
+ this.defaultMessage = 'Environment setup underway - please wait.';
+ }
+
+ show(message = null) {
+ this.messageElement.textContent = message || this.defaultMessage;
+ this.overlay.classList.remove('hidden');
+ // Disable all form inputs and buttons in the entire left panel
+ const leftPanel = this.overlay.parentElement;
+ const inputs = leftPanel.querySelectorAll('input, select, textarea, button');
+ inputs.forEach(input => {
+ input.disabled = true;
+ input.classList.add('opacity-50');
+ });
+ }
+
+ hide() {
+ this.overlay.classList.add('hidden');
+ // Re-enable all form inputs and buttons in the entire left panel
+ const leftPanel = this.overlay.parentElement;
+ const inputs = leftPanel.querySelectorAll('input, select, textarea, button');
+ inputs.forEach(input => {
+ input.disabled = false;
+ input.classList.remove('opacity-50');
+ });
+ // Reset to default message
+ this.messageElement.textContent = this.defaultMessage;
+ }
+}
+
+const loadingOverlay = new LoadingOverlay();
+
+// Add a flag to prevent auto-save during programmatic updates
+let isSettingValues = false;
+
+// Handle status messages with better color management
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');
- }
+
+ // Remove all status classes
+ const statusClasses = ['hidden', 'bg-green-100', 'bg-red-100', 'bg-blue-100', 'text-green-800', 'text-red-800', 'text-blue-800'];
+ statusEl.classList.remove(...statusClasses);
+
+ // Add appropriate classes based on type
+ const typeMap = {
+ success: ['bg-green-100', 'text-green-800'],
+ error: ['bg-red-100', 'text-red-800'],
+ info: ['bg-blue-100', 'text-blue-800']
+ };
+
+ statusEl.classList.add(...(typeMap[type] || typeMap.info));
// Also log to console
appendToConsole(message, type === 'error' ? 'stderr' : 'info');
- setTimeout(() => {
- statusEl.classList.add('hidden');
- }, 5000);
+ setTimeout(() => statusEl.classList.add('hidden'), 5000);
}
-// Get form values
+// Get form values using field mappings
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)
- };
+ const config = {};
+
+ for (const [fieldName, fieldConfig] of Object.entries(CONFIG_FIELDS)) {
+ const element = document.getElementById(fieldName);
+ if (!element) continue;
+
+ switch (fieldConfig.type) {
+ case 'boolean':
+ config[fieldName] = element.checked ? 1 : 0;
+ break;
+ case 'number':
+ config[fieldName] = parseInt(element.value) || fieldConfig.default;
+ break;
+ case 'text':
+ config[fieldName] = element.value || fieldConfig.default;
+ break;
+ default:
+ config[fieldName] = element.value || fieldConfig.default;
+ }
+ }
+
+ return config;
}
-// Add a flag to prevent auto-save during programmatic updates
-let isSettingValues = false;
-
-// Set form values
+// Set form values using field mappings
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;
+ for (const [fieldName, fieldConfig] of Object.entries(CONFIG_FIELDS)) {
+ const element = document.getElementById(fieldName);
+ if (!element) continue;
+
+ const value = config[fieldName] ?? fieldConfig.default;
+
+ switch (fieldConfig.type) {
+ case 'boolean':
+ element.checked = value === 1;
+ break;
+ case 'text':
+ element.value = value || '';
+ break;
+ default:
+ element.value = value;
+ }
+ }
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');
- }
-});
+// 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;
+}
-// Simplify button handlers by extracting common patterns
+// Async action handler with better error handling
async function handleAsyncAction(actionName, actionFn) {
try {
const result = await actionFn();
- if (result && result.message) {
+ if (result?.message) {
showStatus(result.message, 'success');
}
return result;
@@ -103,36 +224,12 @@ async function handleAsyncAction(actionName, actionFn) {
}
}
-// 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
+const SAVE_DELAY = 500;
async function autoSaveConfig() {
- if (isSettingValues) return; // Don't save during programmatic updates
+ if (isSettingValues) return;
clearTimeout(saveTimeout);
saveTimeout = setTimeout(async () => {
@@ -141,28 +238,19 @@ async function autoSaveConfig() {
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
+ // Restart process if running
+ if (!buttonManager.buttons.stop.disabled) {
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();
-
+ buttonManager.setProcessRunning();
appendToConsole('Process restarted with new configuration', 'info');
} catch (error) {
appendToConsole(`Failed to restart process: ${error.message}`, 'stderr');
- // Process is stopped, update button states
- setProcessStoppedState();
+ buttonManager.setProcessStopped();
}
}
} catch (error) {
@@ -171,47 +259,32 @@ async function autoSaveConfig() {
}, SAVE_DELAY);
}
-// Add event listeners to all form inputs for auto-save
+// Auto-save setup
function setupAutoSave() {
- // Get all form inputs
const form = document.getElementById('config-form');
- const inputs = form.querySelectorAll('input, select');
+ const inputs = form.querySelectorAll('input, select, textarea');
- // 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);
- }
+ const eventType = input.type === 'checkbox' ? 'change' :
+ (input.type === 'number' || input.type === 'text' || input.tagName === 'TEXTAREA') ? 'input' : 'change';
+ input.addEventListener(eventType, 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
+// Microphone loading
async function loadMicrophones() {
const microphoneSelect = document.getElementById('microphone');
try {
+ // Check/install requirements during startup
+ appendToConsole('Checking virtual environment and requirements...', 'info');
+ loadingOverlay.show('Setting up environment - this can take several minutes.');
+ try {
+ await handleAsyncAction('Install requirements', () => window.electronAPI.installRequirements());
+ } finally {
+ loadingOverlay.hide(); // Always hide overlay when done
+ }
+
appendToConsole('Loading available microphones...', 'info');
const microphones = await window.electronAPI.getMicrophones();
@@ -232,7 +305,7 @@ async function loadMicrophones() {
appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout');
});
- // Restore previously selected microphone if possible
+ // Restore previously selected microphone
try {
const config = await window.electronAPI.loadConfig();
if (config.microphone) {
@@ -248,11 +321,144 @@ async function loadMicrophones() {
}
}
-// Update window load to include auto-save setup
+// Event handlers setup
+function setupEventHandlers() {
+ // Advanced settings toggle
+ 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');
+ }
+ });
+
+ // Setup virtual environment
+ document.getElementById('setup-venv').addEventListener('click', async () => {
+ loadingOverlay.show('Setting up virtual environment - please wait...'); // Show overlay with custom message
+ try {
+ await buttonManager.withButtonLoading('setupVenv', async () => {
+ await handleAsyncAction('Install requirements', () => window.electronAPI.installRequirements());
+ });
+ } finally {
+ loadingOverlay.hide(); // Always hide overlay when done
+ }
+ });
+
+ // Reset virtual environment
+ document.getElementById('reset-venv').addEventListener('click', async () => {
+ loadingOverlay.show('Resetting virtual environment - please wait...'); // Show overlay with custom message
+ try {
+ await buttonManager.withButtonLoading('resetVenv', async () => {
+ await handleAsyncAction('Reset virtual environment', () => window.electronAPI.resetVenv());
+ });
+ } finally {
+ loadingOverlay.hide(); // Always hide overlay when done
+ }
+ });
+
+ // Reset configuration
+ document.getElementById('reset-config').addEventListener('click', async () => {
+ const confirmReset = confirm('Are you sure you want to reset all settings to defaults? This cannot be undone.');
+ if (!confirmReset) return;
+
+ try {
+ // Stop process if running
+ const wasRunning = !buttonManager.buttons.stop.disabled;
+ if (wasRunning) {
+ appendToConsole('Stopping process before resetting configuration...', 'info');
+ await window.electronAPI.stopProcess();
+ buttonManager.setProcessStopped();
+ await new Promise(resolve => setTimeout(resolve, 500));
+ }
+
+ // Reset configuration
+ appendToConsole('Resetting configuration to defaults...', 'info');
+ const result = await window.electronAPI.resetConfig();
+
+ // Reload configuration with defaults
+ const config = await window.electronAPI.loadConfig();
+ setFormValues(config);
+
+ showStatus(result.message, 'success');
+ appendToConsole('Configuration reset successfully', 'info');
+
+ // Restart process if it was running
+ if (wasRunning) {
+ appendToConsole('Restarting process with default configuration...', 'info');
+ await window.electronAPI.startProcess();
+ buttonManager.setProcessRunning();
+ appendToConsole('Process restarted with default configuration', 'info');
+ }
+ } catch (error) {
+ showStatus(`Failed to reset configuration: ${error.message}`, 'error');
+ appendToConsole(`Failed to reset configuration: ${error.message}`, 'stderr');
+ }
+ });
+
+ // Refresh microphones
+ document.getElementById('refresh-microphones').addEventListener('click', async () => {
+ await buttonManager.withButtonLoading('refreshMicrophones', async () => {
+ await loadMicrophones();
+ });
+ });
+
+ // Start process
+ document.getElementById('start-process').addEventListener('click', async () => {
+ buttonManager.setState('start', true);
+
+ try {
+ // The installRequirements function will now check if venv is set up.
+ loadingOverlay.show('Verifying environment setup - please wait...'); // Show overlay with custom message
+ try {
+ await window.electronAPI.installRequirements();
+ appendToConsole('Virtual environment setup checked/completed', 'info');
+ } finally {
+ loadingOverlay.hide(); // Always hide overlay when done
+ }
+
+ await window.electronAPI.startProcess();
+ buttonManager.setProcessRunning();
+ appendToConsole('Process started successfully', 'info');
+ } catch (error) {
+ appendToConsole(`Failed to start process: ${error.message}`, 'stderr');
+ buttonManager.setState('start', false);
+ }
+ });
+
+ // Stop process
+ document.getElementById('stop-process').addEventListener('click', async () => {
+ buttonManager.setState('stop', true);
+
+ try {
+ await window.electronAPI.stopProcess();
+ appendToConsole('Process stop initiated', 'info');
+ } catch (error) {
+ appendToConsole(`Failed to stop process: ${error.message}`, 'stderr');
+ buttonManager.setState('stop', false);
+ }
+ });
+
+ // Listen for process stopped event
+ window.electronAPI.onProcessStopped(() => {
+ buttonManager.setProcessStopped();
+ });
+}
+
+// Initialize application
window.addEventListener('load', async () => {
appendToConsole('TaSTT Configuration UI initialized', 'info');
- // Load config first
+ // Set up Python output listener first so we capture all output
+ window.electronAPI.onPythonOutput((data) => {
+ appendToConsole(data.message, data.type);
+ });
+
+ // Load configuration
try {
const config = await window.electronAPI.loadConfig();
setFormValues(config);
@@ -264,71 +470,7 @@ window.addEventListener('load', async () => {
// Load microphones
await loadMicrophones();
- // Set up auto-save after everything is loaded
+ // Setup event handlers and auto-save
+ setupEventHandlers();
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();
}); \ No newline at end of file