diff options
| author | yum <yum.food.vr@gmail.com> | 2025-07-23 17:41:49 -0700 |
|---|---|---|
| committer | yum <yum.food.vr@gmail.com> | 2025-07-23 17:41:49 -0700 |
| commit | 790c91d7ad515c3c0a22ca1341316265b8f0d779 (patch) | |
| tree | 28527bbcf87e8fab1d27eb76a1f5ea325b94d599 /ui | |
| parent | 73de7cb2d8fb964e7f76ab55420e9bc331bf7bea (diff) | |
bugfixes
* fix model acquisition
* fix local beepsnd
* fix volume control
Diffstat (limited to 'ui')
| -rw-r--r-- | ui/config-schema.js | 2 | ||||
| -rw-r--r-- | ui/index.html | 13 | ||||
| -rw-r--r-- | ui/index.js | 17 | ||||
| -rw-r--r-- | ui/preload.js | 1 | ||||
| -rw-r--r-- | ui/renderer.js | 198 |
5 files changed, 129 insertions, 102 deletions
diff --git a/ui/config-schema.js b/ui/config-schema.js index 6b11277..bf91fce 100644 --- a/ui/config-schema.js +++ b/ui/config-schema.js @@ -23,6 +23,7 @@ const CONFIG_SCHEMA = { cols: { type: 'number', default: 24 }, beam_size: { type: 'number', default: 5 }, best_of: { type: 'number', default: 5 }, + volume: { type: 'number', default: 30 }, // Boolean fields (stored as 1/0) enable_debug_mode: { type: 'boolean', default: 0 }, @@ -34,7 +35,6 @@ const CONFIG_SCHEMA = { enable_profanity_filter: { type: 'boolean', default: 0 }, remove_trailing_period: { type: 'boolean', default: 0 }, reset_on_toggle: { type: 'boolean', default: 0 }, - enable_local_beep: { type: 'boolean', default: 1 }, use_builtin: { type: 'boolean', default: 1 } }; diff --git a/ui/index.html b/ui/index.html index 99e64dd..19c41ce 100644 --- a/ui/index.html +++ b/ui/index.html @@ -248,10 +248,13 @@ <input type="checkbox" id="reset_on_toggle" class="mr-2"> <span class="checkbox-text">Reset transcript on toggle</span> </label> - <label for="enable_local_beep" class="checkbox-label"> - <input type="checkbox" id="enable_local_beep" checked class="mr-2"> - <span class="checkbox-text">Enable local beep sounds</span> - </label> + <div> + <label for="volume" class="form-label"> + Local Beep Volume + <span id="volume-display" class="text-gray-500 text-sm ml-2">30%</span> + </label> + <input type="range" id="volume" min="0" max="100" step="10" value="30" class="form-input w-full"> + </div> </div> </section> @@ -314,7 +317,7 @@ <button type="button" id="start-process" class="btn btn-green flex-1"> Start </button> - <button type="button" id="stop-process" class="btn btn-red flex-1" disabled> + <button type="button" id="stop-process" class="btn btn-red flex-1"> Stop </button> </div> diff --git a/ui/index.js b/ui/index.js index 24a7e13..5a5d0a6 100644 --- a/ui/index.js +++ b/ui/index.js @@ -530,19 +530,20 @@ ipcMain.handle('start-process', async () => { }); ipcMain.handle('stop-process', async () => { + if (!runningProcess) { + sendPythonOutput('No process to stop', 'info'); + return { success: true, forcefullyKilled: false }; + } + return new Promise((resolve) => { let forcefullyKilled = false; - - if (!runningProcess) { - resolve({ success: true, forcefullyKilled }); - } // Set up a timeout to force kill after 10 seconds const killTimeout = setTimeout(() => { if (runningProcess) { sendPythonOutput('Process did not stop gracefully, forcing termination...', 'stderr'); forcefullyKilled = true; - runningProcess.kill(); + runningProcess.kill('SIGKILL'); } }, 10000); @@ -562,10 +563,14 @@ ipcMain.handle('stop-process', async () => { // Send termination signal sendPythonOutput('Stopping process gracefully...', 'info'); - runningProcess.kill(); + runningProcess.kill('SIGTERM'); }); }); +ipcMain.handle('get-process-state', () => { + return { isRunning: runningProcess !== null }; +}); + // Clean up on app quit app.on('before-quit', () => { if (runningProcess) { diff --git a/ui/preload.js b/ui/preload.js index f2e0a81..6f6e54f 100644 --- a/ui/preload.js +++ b/ui/preload.js @@ -10,6 +10,7 @@ contextBridge.exposeInMainWorld('electronAPI', { resetVenv: () => ipcRenderer.invoke('reset-venv'), startProcess: () => ipcRenderer.invoke('start-process'), stopProcess: () => ipcRenderer.invoke('stop-process'), + getProcessState: () => ipcRenderer.invoke('get-process-state'), onPythonOutput: (callback) => ipcRenderer.on('python-output', (event, data) => callback(data)), onProcessStopped: (callback) => ipcRenderer.on('process-stopped', () => callback()) }); diff --git a/ui/renderer.js b/ui/renderer.js index 2f4c8f1..008e0da 100644 --- a/ui/renderer.js +++ b/ui/renderer.js @@ -1,6 +1,21 @@ // Import configuration schema const CONFIG_FIELDS = window.CONFIG_SCHEMA; +// Process state tracking +let isProcessRunning = false; +let buttonManager; +let loadingOverlay; + +// Auto-save functionality with debouncing +let saveTimeout; +const SAVE_DELAY = 500; +let isSettingValues = false; + +// Console management +const consoleContent = document.getElementById('console-content'); +const MAX_CONSOLE_LINES = 512; +let consoleLineCount = 0; + // Button management system class ButtonManager { constructor() { @@ -11,33 +26,30 @@ class ButtonManager { resetVenv: document.getElementById('reset-venv'), refreshMicrophones: document.getElementById('refresh-microphones') }; - - // Initialize button states on construction + + // Initialize button states - process is not running at startup this.setProcessStopped(); } - + 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); + isProcessRunning = true; } - + setProcessStopped() { this.setState('start', false); this.setState('stop', true); + isProcessRunning = false; } - + async withButtonLoading(buttonName, asyncFn) { this.setState(buttonName, true); try { @@ -48,8 +60,6 @@ class ButtonManager { } } -const buttonManager = new ButtonManager(); - // Add loading overlay management class LoadingOverlay { constructor() { @@ -57,8 +67,9 @@ class LoadingOverlay { this.form = document.getElementById('config-form'); this.messageElement = this.overlay.querySelector('p'); this.defaultMessage = 'Environment setup underway - please wait.'; + this.originalStates = new Map(); // Track original disabled states } - + show(message = null) { this.messageElement.textContent = message || this.defaultMessage; this.overlay.classList.remove('hidden'); @@ -66,68 +77,69 @@ class LoadingOverlay { const leftPanel = this.overlay.parentElement; const inputs = leftPanel.querySelectorAll('input, select, textarea, button'); inputs.forEach(input => { + // Store original disabled state before disabling + this.originalStates.set(input, input.disabled); 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 + // Restore original states of form inputs and buttons const leftPanel = this.overlay.parentElement; const inputs = leftPanel.querySelectorAll('input, select, textarea, button'); inputs.forEach(input => { - input.disabled = false; + // Restore original disabled state + input.disabled = this.originalStates.get(input) || false; input.classList.remove('opacity-50'); }); + // Clear the stored states + this.originalStates.clear(); // 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; - + // 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); } // Get form values using field mappings function getFormValues() { 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; + const numValue = parseInt(element.value); + config[fieldName] = isNaN(numValue) ? fieldConfig.default : numValue; break; case 'text': config[fieldName] = element.value || fieldConfig.default; @@ -136,20 +148,20 @@ function getFormValues() { config[fieldName] = element.value || fieldConfig.default; } } - + return config; } // Set form values using field mappings function setFormValues(config) { isSettingValues = true; // Disable auto-save temporarily - + 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; @@ -161,7 +173,7 @@ function setFormValues(config) { element.value = value; } } - + // Handle use_builtin toggle state const useBuiltin = config.use_builtin === 1; const customChatboxInputs = ['block_width', 'num_blocks', 'rows', 'cols']; @@ -176,53 +188,54 @@ function setFormValues(config) { } } }); - + + // Update volume display + if (config.volume !== undefined) { + const volumePercent = Math.round(config.volume); + document.getElementById('volume-display').textContent = `${volumePercent}%`; + } + isSettingValues = false; // Re-enable auto-save } -// Console management -const consoleContent = document.getElementById('console-content'); -const MAX_CONSOLE_LINES = 512; -let consoleLineCount = 0; - 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); consoleLineCount++; - + // Remove old lines if we exceed the limit if (consoleLineCount > MAX_CONSOLE_LINES) { // Calculate how many lines to remove (remove 10% to avoid frequent trimming) const linesToRemove = Math.floor(MAX_CONSOLE_LINES * 0.1); - + // Remove the oldest lines for (let i = 0; i < linesToRemove; i++) { if (consoleContent.firstChild) { consoleContent.removeChild(consoleContent.firstChild); } } - + consoleLineCount -= linesToRemove; - + // Add a notice that lines were trimmed const trimNotice = document.createElement('div'); trimNotice.className = 'console-info'; trimNotice.innerHTML = '<span class="console-timestamp">[System] </span><span class="console-info">... older lines removed to maintain performance ...</span>'; consoleContent.insertBefore(trimNotice, consoleContent.firstChild); } - + // Auto-scroll to bottom const pythonConsole = document.getElementById('python-console'); pythonConsole.scrollTop = pythonConsole.scrollHeight; @@ -242,24 +255,20 @@ async function handleAsyncAction(actionName, actionFn) { } } -// Auto-save functionality with debouncing -let saveTimeout; -const SAVE_DELAY = 500; - async function autoSaveConfig() { if (isSettingValues) return; - + clearTimeout(saveTimeout); saveTimeout = setTimeout(async () => { try { const config = getFormValues(); await window.electronAPI.saveConfig(config); showStatus('Configuration saved', 'success'); - + // Restart process if running - if (!buttonManager.buttons.stop.disabled) { + if (isProcessRunning) { appendToConsole('Restarting process with new configuration...', 'info'); - + try { await window.electronAPI.stopProcess(); await new Promise(resolve => setTimeout(resolve, 1000)); @@ -281,9 +290,9 @@ async function autoSaveConfig() { function setupAutoSave() { const form = document.getElementById('config-form'); const inputs = form.querySelectorAll('input, select, textarea'); - + inputs.forEach(input => { - const eventType = input.type === 'checkbox' ? 'change' : + const eventType = input.type === 'checkbox' ? 'change' : (input.type === 'number' || input.type === 'text' || input.tagName === 'TEXTAREA') ? 'input' : 'change'; input.addEventListener(eventType, autoSaveConfig); }); @@ -292,7 +301,7 @@ function setupAutoSave() { // Microphone loading async function loadMicrophones() { const microphoneSelect = document.getElementById('microphone'); - + try { // Check/install requirements during startup appendToConsole('Checking virtual environment and requirements...', 'info'); @@ -305,15 +314,15 @@ async function loadMicrophones() { appendToConsole('Loading available microphones...', 'info'); const microphones = await window.electronAPI.getMicrophones(); - + microphoneSelect.innerHTML = ''; - + if (microphones.length === 0) { microphoneSelect.innerHTML = '<option value="" disabled>No microphones found</option>'; appendToConsole('No microphones found', 'stderr'); return; } - + appendToConsole(`Found ${microphones.length} microphone(s)`, 'info'); microphones.forEach(mic => { const option = document.createElement('option'); @@ -322,7 +331,7 @@ async function loadMicrophones() { microphoneSelect.appendChild(option); appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout'); }); - + // Restore previously selected microphone try { const config = await window.electronAPI.loadConfig(); @@ -332,7 +341,7 @@ async function loadMicrophones() { } catch (error) { // Ignore config load errors here } - + } catch (error) { appendToConsole(`Failed to load microphones: ${error.message}`, 'stderr'); microphoneSelect.innerHTML = '<option value="" disabled>Error loading microphones</option>'; @@ -345,7 +354,7 @@ function setupEventHandlers() { 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'); @@ -354,12 +363,12 @@ function setupEventHandlers() { chevron.classList.remove('rotate-90'); } }); - + // Use builtin chatbox toggle document.getElementById('use_builtin').addEventListener('change', (e) => { const customChatboxInputs = ['block_width', 'num_blocks', 'rows', 'cols']; const isBuiltin = e.target.checked; - + customChatboxInputs.forEach(inputId => { const input = document.getElementById(inputId); if (input) { @@ -372,7 +381,13 @@ function setupEventHandlers() { } }); }); - + + // Volume slider update + document.getElementById('volume').addEventListener('input', (e) => { + const volumePercent = Math.round(e.target.value); + document.getElementById('volume-display').textContent = `${volumePercent}%`; + }); + // Setup virtual environment document.getElementById('setup-venv').addEventListener('click', async () => { loadingOverlay.show('Setting up virtual environment - please wait...'); // Show overlay with custom message @@ -385,7 +400,7 @@ function setupEventHandlers() { 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 @@ -397,33 +412,33 @@ function setupEventHandlers() { 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; + const wasRunning = isProcessRunning; 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'); @@ -436,18 +451,18 @@ function setupEventHandlers() { 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 @@ -457,7 +472,7 @@ function setupEventHandlers() { } finally { loadingOverlay.hide(); // Always hide overlay when done } - + await window.electronAPI.startProcess(); buttonManager.setProcessRunning(); appendToConsole('Process started successfully', 'info'); @@ -466,11 +481,11 @@ function setupEventHandlers() { 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'); @@ -479,7 +494,7 @@ function setupEventHandlers() { buttonManager.setState('stop', false); } }); - + // Listen for process stopped event window.electronAPI.onProcessStopped(() => { buttonManager.setProcessStopped(); @@ -489,12 +504,15 @@ function setupEventHandlers() { // Initialize application window.addEventListener('load', async () => { appendToConsole('TaSTT Configuration UI initialized', 'info'); - + + loadingOverlay = new LoadingOverlay(); + buttonManager = new ButtonManager(); + // 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(); @@ -503,11 +521,11 @@ window.addEventListener('load', async () => { } catch (error) { appendToConsole(`Failed to load configuration: ${error.message}`, 'stderr'); } - + // Load microphones await loadMicrophones(); - + // Setup event handlers and auto-save setupEventHandlers(); setupAutoSave(); -});
\ No newline at end of file +}); |
