yum/FastTextPager
Compressed text paging over OSC.
git clone https://git.yummers.dev/yum/FastTextPager
790c91d
master
1// Import configuration schema 2const CONFIG_FIELDS = window . CONFIG_SCHEMA ; 3 4// Process state tracking 5let isProcessRunning = false ; 6let buttonManager ; 7let loadingOverlay ; 8 9// Auto-save functionality with debouncing 10let saveTimeout ; 11const SAVE_DELAY = 500 ; 12let isSettingValues = false ; 13 14// Console management 15const consoleContent = document . getElementById ( 'console-content' ); 16const MAX_CONSOLE_LINES = 512 ; 17let consoleLineCount = 0 ; 18 19// Button management system 20class ButtonManager { 21constructor () { 22this . buttons = { 23start :document . getElementById ( 'start-process' ), 24stop :document . getElementById ( 'stop-process' ), 25setupVenv :document . getElementById ( 'setup-venv' ), 26resetVenv :document . getElementById ( 'reset-venv' ), 27refreshMicrophones :document . getElementById ( 'refresh-microphones' ) 28}; 29 30// Initialize button states - process is not running at startup 31this . setProcessStopped (); 32} 33 34setState ( buttonName , disabled ) { 35const button = this . buttons [ buttonName ]; 36if ( ! button ) return ; 37 38button . disabled = disabled ; 39} 40 41setProcessRunning () { 42this . setState ( 'start' , true ); 43this . setState ( 'stop' , false ); 44isProcessRunning = true ; 45} 46 47setProcessStopped () { 48this . setState ( 'start' , false ); 49this . setState ( 'stop' , true ); 50isProcessRunning = false ; 51} 52 53async withButtonLoading ( buttonName , asyncFn ) { 54this . setState ( buttonName , true ); 55try { 56return await asyncFn (); 57} finally { 58this . setState ( buttonName , false ); 59} 60} 61} 62 63// Add loading overlay management 64class LoadingOverlay { 65constructor () { 66this . overlay = document . getElementById ( 'loading-overlay' ); 67this . form = document . getElementById ( 'config-form' ); 68this . messageElement = this . overlay . querySelector ( 'p' ); 69this . defaultMessage = 'Environment setup underway - please wait.' ; 70this . originalStates = new Map (); // Track original disabled states 71} 72 73show ( message = null ) { 74this . messageElement . textContent = message || this . defaultMessage ; 75this . overlay . classList . remove ( 'hidden' ); 76// Disable all form inputs and buttons in the entire left panel 77const leftPanel = this . overlay . parentElement ; 78const inputs = leftPanel . querySelectorAll ( 'input, select, textarea, button' ); 79inputs . forEach ( input => { 80// Store original disabled state before disabling 81this . originalStates . set ( input , input . disabled ); 82input . disabled = true ; 83input . classList . add ( 'opacity-50' ); 84}); 85} 86 87hide () { 88this . overlay . classList . add ( 'hidden' ); 89// Restore original states of form inputs and buttons 90const leftPanel = this . overlay . parentElement ; 91const inputs = leftPanel . querySelectorAll ( 'input, select, textarea, button' ); 92inputs . forEach ( input => { 93// Restore original disabled state 94input . disabled = this . originalStates . get ( input ) || false ; 95input . classList . remove ( 'opacity-50' ); 96}); 97// Clear the stored states 98this . originalStates . clear (); 99// Reset to default message 100this . messageElement . textContent = this . defaultMessage ; 101} 102} 103 104// Handle status messages with better color management 105function showStatus ( message , type = 'info' ) { 106const statusEl = document . getElementById ( 'status-message' ); 107statusEl . textContent = message ; 108 109// Remove all status classes 110const statusClasses = [ 'hidden' , 'bg-green-100' , 'bg-red-100' , 'bg-blue-100' , 'text-green-800' , 'text-red-800' , 'text-blue-800' ]; 111statusEl . classList . remove ( ...statusClasses ); 112 113// Add appropriate classes based on type 114const typeMap = { 115success :[ 'bg-green-100' , 'text-green-800' ], 116error :[ 'bg-red-100' , 'text-red-800' ], 117info :[ 'bg-blue-100' , 'text-blue-800' ] 118}; 119 120statusEl . classList . add ( ...( typeMap [ type ] || typeMap . info )); 121 122// Also log to console 123appendToConsole ( message , type === 'error' ?'stderr' :'info' ); 124 125setTimeout (() => statusEl . classList . add ( 'hidden' ), 5000 ); 126} 127 128// Get form values using field mappings 129function getFormValues () { 130const config = {}; 131 132for ( const [ fieldName , fieldConfig ] of Object . entries ( CONFIG_FIELDS )) { 133const element = document . getElementById ( fieldName ); 134if ( ! element ) continue ; 135 136switch ( fieldConfig . type ) { 137case 'boolean' : 138config [ fieldName ] = element . checked ?1 :0 ; 139break ; 140case 'number' : 141const numValue = parseInt ( element . value ); 142config [ fieldName ] = isNaN ( numValue ) ?fieldConfig . default :numValue ; 143break ; 144case 'text' : 145config [ fieldName ] = element . value || fieldConfig . default ; 146break ; 147default : 148config [ fieldName ] = element . value || fieldConfig . default ; 149} 150} 151 152return config ; 153} 154 155// Set form values using field mappings 156function setFormValues ( config ) { 157isSettingValues = true ; // Disable auto-save temporarily 158 159for ( const [ fieldName , fieldConfig ] of Object . entries ( CONFIG_FIELDS )) { 160const element = document . getElementById ( fieldName ); 161if ( ! element ) continue ; 162 163const value = config [ fieldName ] ?? fieldConfig . default ; 164 165switch ( fieldConfig . type ) { 166case 'boolean' : 167element . checked = value === 1 ; 168break ; 169case 'text' : 170element . value = value || '' ; 171break ; 172default : 173element . value = value ; 174} 175} 176 177// Handle use_builtin toggle state 178const useBuiltin = config . use_builtin === 1 ; 179const customChatboxInputs = [ 'block_width' , 'num_blocks' , 'rows' , 'cols' ]; 180customChatboxInputs . forEach ( inputId => { 181const input = document . getElementById ( inputId ); 182if ( input ) { 183input . disabled = useBuiltin ; 184if ( useBuiltin ) { 185input . classList . add ( 'opacity-50' , 'cursor-not-allowed' ); 186} else { 187input . classList . remove ( 'opacity-50' , 'cursor-not-allowed' ); 188} 189} 190}); 191 192// Update volume display 193if ( config . volume !== undefined ) { 194const volumePercent = Math . round ( config . volume ); 195document . getElementById ( 'volume-display' ). textContent = ` ${ volumePercent } %` ; 196} 197 198isSettingValues = false ; // Re-enable auto-save 199} 200 201function appendToConsole ( message , type = 'stdout' ) { 202const timestamp = new Date (). toLocaleTimeString (); 203const timestampSpan = document . createElement ( 'span' ); 204timestampSpan . className = 'console-timestamp' ; 205timestampSpan . textContent = `[ ${ timestamp } ] ` ; 206 207const messageSpan = document . createElement ( 'span' ); 208messageSpan . className = `console- ${ type } ` ; 209messageSpan . textContent = message ; 210 211const lineDiv = document . createElement ( 'div' ); 212lineDiv . appendChild ( timestampSpan ); 213lineDiv . appendChild ( messageSpan ); 214 215consoleContent . appendChild ( lineDiv ); 216consoleLineCount ++ ; 217 218// Remove old lines if we exceed the limit 219if ( consoleLineCount > MAX_CONSOLE_LINES ) { 220// Calculate how many lines to remove (remove 10% to avoid frequent trimming) 221const linesToRemove = Math . floor ( MAX_CONSOLE_LINES * 0.1 ); 222 223// Remove the oldest lines 224for ( let i = 0 ; i < linesToRemove ; i ++ ) { 225if ( consoleContent . firstChild ) { 226consoleContent . removeChild ( consoleContent . firstChild ); 227} 228} 229 230consoleLineCount -= linesToRemove ; 231 232// Add a notice that lines were trimmed 233const trimNotice = document . createElement ( 'div' ); 234trimNotice . className = 'console-info' ; 235trimNotice . innerHTML = '<span class="console-timestamp">[System] </span><span class="console-info">... older lines removed to maintain performance ...</span>' ; 236consoleContent . insertBefore ( trimNotice , consoleContent . firstChild ); 237} 238 239// Auto-scroll to bottom 240const pythonConsole = document . getElementById ( 'python-console' ); 241pythonConsole . scrollTop = pythonConsole . scrollHeight ; 242} 243 244// Async action handler with better error handling 245async function handleAsyncAction ( actionName , actionFn ) { 246try { 247const result = await actionFn (); 248if ( result ?. message ) { 249showStatus ( result . message , 'success' ); 250} 251return result ; 252} catch ( error ) { 253showStatus ( ` ${ actionName } failed: ${ error . message } ` , 'error' ); 254throw error ; 255} 256} 257 258async function autoSaveConfig () { 259if ( isSettingValues ) return ; 260 261clearTimeout ( saveTimeout ); 262saveTimeout = setTimeout ( async () => { 263try { 264const config = getFormValues (); 265await window . electronAPI . saveConfig ( config ); 266showStatus ( 'Configuration saved' , 'success' ); 267 268// Restart process if running 269if ( isProcessRunning ) { 270appendToConsole ( 'Restarting process with new configuration...' , 'info' ); 271 272try { 273await window . electronAPI . stopProcess (); 274await new Promise ( resolve => setTimeout ( resolve , 1000 )); 275await window . electronAPI . startProcess (); 276buttonManager . setProcessRunning (); 277appendToConsole ( 'Process restarted with new configuration' , 'info' ); 278} catch ( error ) { 279appendToConsole ( `Failed to restart process: ${ error . message } ` , 'stderr' ); 280buttonManager . setProcessStopped (); 281} 282} 283} catch ( error ) { 284showStatus ( `Failed to save configuration: ${ error . message } ` , 'error' ); 285} 286}, SAVE_DELAY ); 287} 288 289// Auto-save setup 290function setupAutoSave () { 291const form = document . getElementById ( 'config-form' ); 292const inputs = form . querySelectorAll ( 'input, select, textarea' ); 293 294inputs . forEach ( input => { 295const eventType = input . type === 'checkbox' ?'change' : 296( input . type === 'number' || input . type === 'text' || input . tagName === 'TEXTAREA' ) ?'input' :'change' ; 297input . addEventListener ( eventType , autoSaveConfig ); 298}); 299} 300 301// Microphone loading 302async function loadMicrophones () { 303const microphoneSelect = document . getElementById ( 'microphone' ); 304 305try { 306// Check/install requirements during startup 307appendToConsole ( 'Checking virtual environment and requirements...' , 'info' ); 308loadingOverlay . show ( 'Setting up environment - this can take several minutes.' ); 309try { 310await handleAsyncAction ( 'Install requirements' , () => window . electronAPI . installRequirements ()); 311} finally { 312loadingOverlay . hide (); // Always hide overlay when done 313} 314 315appendToConsole ( 'Loading available microphones...' , 'info' ); 316const microphones = await window . electronAPI . getMicrophones (); 317 318microphoneSelect . innerHTML = '' ; 319 320if ( microphones . length === 0 ) { 321microphoneSelect . innerHTML = '<option value="" disabled>No microphones found</option>' ; 322appendToConsole ( 'No microphones found' , 'stderr' ); 323return ; 324} 325 326appendToConsole ( `Found ${ microphones . length } microphone(s)` , 'info' ); 327microphones . forEach ( mic => { 328const option = document . createElement ( 'option' ); 329option . value = mic . index . toString (); 330option . textContent = mic . name ; 331microphoneSelect . appendChild ( option ); 332appendToConsole ( ` - ${ mic . name } (Device ${ mic . index } )` , 'stdout' ); 333}); 334 335// Restore previously selected microphone 336try { 337const config = await window . electronAPI . loadConfig (); 338if ( config . microphone ) { 339microphoneSelect . value = config . microphone ; 340} 341} catch ( error ) { 342// Ignore config load errors here 343} 344 345} catch ( error ) { 346appendToConsole ( `Failed to load microphones: ${ error . message } ` , 'stderr' ); 347microphoneSelect . innerHTML = '<option value="" disabled>Error loading microphones</option>' ; 348} 349} 350 351// Event handlers setup 352function setupEventHandlers () { 353// Advanced settings toggle 354document . getElementById ( 'toggle-advanced' ). addEventListener ( 'click' , () => { 355const advancedSettings = document . getElementById ( 'advanced-settings' ); 356const chevron = document . getElementById ( 'chevron' ); 357 358if ( advancedSettings . classList . contains ( 'hidden' )) { 359advancedSettings . classList . remove ( 'hidden' ); 360chevron . classList . add ( 'rotate-90' ); 361} else { 362advancedSettings . classList . add ( 'hidden' ); 363chevron . classList . remove ( 'rotate-90' ); 364} 365}); 366 367// Use builtin chatbox toggle 368document . getElementById ( 'use_builtin' ). addEventListener ( 'change' , ( e ) => { 369const customChatboxInputs = [ 'block_width' , 'num_blocks' , 'rows' , 'cols' ]; 370const isBuiltin = e . target . checked ; 371 372customChatboxInputs . forEach ( inputId => { 373const input = document . getElementById ( inputId ); 374if ( input ) { 375input . disabled = isBuiltin ; 376if ( isBuiltin ) { 377input . classList . add ( 'opacity-50' , 'cursor-not-allowed' ); 378} else { 379input . classList . remove ( 'opacity-50' , 'cursor-not-allowed' ); 380} 381} 382}); 383}); 384 385// Volume slider update 386document . getElementById ( 'volume' ). addEventListener ( 'input' , ( e ) => { 387const volumePercent = Math . round ( e . target . value ); 388document . getElementById ( 'volume-display' ). textContent = ` ${ volumePercent } %` ; 389}); 390 391// Setup virtual environment 392document . getElementById ( 'setup-venv' ). addEventListener ( 'click' , async () => { 393loadingOverlay . show ( 'Setting up virtual environment - please wait...' ); // Show overlay with custom message 394try { 395await buttonManager . withButtonLoading ( 'setupVenv' , async () => { 396await window . electronAPI . deleteVenvIndicatorFile (); 397await handleAsyncAction ( 'Install requirements' , () => window . electronAPI . installRequirements ()); 398}); 399} finally { 400loadingOverlay . hide (); // Always hide overlay when done 401} 402}); 403 404// Reset virtual environment 405document . getElementById ( 'reset-venv' ). addEventListener ( 'click' , async () => { 406loadingOverlay . show ( 'Resetting virtual environment - please wait...' ); // Show overlay with custom message 407try { 408await buttonManager . withButtonLoading ( 'resetVenv' , async () => { 409await handleAsyncAction ( 'Reset virtual environment' , () => window . electronAPI . resetVenv ()); 410}); 411} finally { 412loadingOverlay . hide (); // Always hide overlay when done 413} 414}); 415 416// Reset configuration 417document . getElementById ( 'reset-config' ). addEventListener ( 'click' , async () => { 418const confirmReset = confirm ( 'Are you sure you want to reset all settings to defaults? This cannot be undone.' ); 419if ( ! confirmReset ) return ; 420 421try { 422// Stop process if running 423const wasRunning = isProcessRunning ; 424if ( wasRunning ) { 425appendToConsole ( 'Stopping process before resetting configuration...' , 'info' ); 426await window . electronAPI . stopProcess (); 427buttonManager . setProcessStopped (); 428await new Promise ( resolve => setTimeout ( resolve , 500 )); 429} 430 431// Reset configuration 432appendToConsole ( 'Resetting configuration to defaults...' , 'info' ); 433const result = await window . electronAPI . resetConfig (); 434 435// Reload configuration with defaults 436const config = await window . electronAPI . loadConfig (); 437setFormValues ( config ); 438 439showStatus ( result . message , 'success' ); 440appendToConsole ( 'Configuration reset successfully' , 'info' ); 441 442// Restart process if it was running 443if ( wasRunning ) { 444appendToConsole ( 'Restarting process with default configuration...' , 'info' ); 445await window . electronAPI . startProcess (); 446buttonManager . setProcessRunning (); 447appendToConsole ( 'Process restarted with default configuration' , 'info' ); 448} 449} catch ( error ) { 450showStatus ( `Failed to reset configuration: ${ error . message } ` , 'error' ); 451appendToConsole ( `Failed to reset configuration: ${ error . message } ` , 'stderr' ); 452} 453}); 454 455// Refresh microphones 456document . getElementById ( 'refresh-microphones' ). addEventListener ( 'click' , async () => { 457await buttonManager . withButtonLoading ( 'refreshMicrophones' , async () => { 458await loadMicrophones (); 459}); 460}); 461 462// Start process 463document . getElementById ( 'start-process' ). addEventListener ( 'click' , async () => { 464buttonManager . setState ( 'start' , true ); 465 466try { 467// The installRequirements function will now check if venv is set up. 468loadingOverlay . show ( 'Verifying environment setup - please wait...' ); // Show overlay with custom message 469try { 470await window . electronAPI . installRequirements (); 471appendToConsole ( 'Virtual environment setup checked/completed' , 'info' ); 472} finally { 473loadingOverlay . hide (); // Always hide overlay when done 474} 475 476await window . electronAPI . startProcess (); 477buttonManager . setProcessRunning (); 478appendToConsole ( 'Process started successfully' , 'info' ); 479} catch ( error ) { 480appendToConsole ( `Failed to start process: ${ error . message } ` , 'stderr' ); 481buttonManager . setState ( 'start' , false ); 482} 483}); 484 485// Stop process 486document . getElementById ( 'stop-process' ). addEventListener ( 'click' , async () => { 487buttonManager . setState ( 'stop' , true ); 488 489try { 490await window . electronAPI . stopProcess (); 491appendToConsole ( 'Process stop initiated' , 'info' ); 492} catch ( error ) { 493appendToConsole ( `Failed to stop process: ${ error . message } ` , 'stderr' ); 494buttonManager . setState ( 'stop' , false ); 495} 496}); 497 498// Listen for process stopped event 499window . electronAPI . onProcessStopped (() => { 500buttonManager . setProcessStopped (); 501}); 502} 503 504// Initialize application 505window . addEventListener ( 'load' , async () => { 506appendToConsole ( 'TaSTT Configuration UI initialized' , 'info' ); 507 508loadingOverlay = new LoadingOverlay (); 509buttonManager = new ButtonManager (); 510 511// Set up Python output listener first so we capture all output 512window . electronAPI . onPythonOutput (( data ) => { 513appendToConsole ( data . message , data . type ); 514}); 515 516// Load configuration 517try { 518const config = await window . electronAPI . loadConfig (); 519setFormValues ( config ); 520appendToConsole ( 'Configuration loaded' , 'info' ); 521} catch ( error ) { 522appendToConsole ( `Failed to load configuration: ${ error . message } ` , 'stderr' ); 523} 524 525// Load microphones 526await loadMicrophones (); 527 528// Setup event handlers and auto-save 529setupEventHandlers (); 530setupAutoSave (); 531});