1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
|
// 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() {
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')
};
// 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;
}
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 {
return await asyncFn();
} finally {
this.setState(buttonName, false);
}
}
}
// 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.';
this.originalStates = new Map(); // Track original disabled states
}
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 => {
// 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');
// Restore original states of form inputs and buttons
const leftPanel = this.overlay.parentElement;
const inputs = leftPanel.querySelectorAll(
'input, select, textarea, button');
inputs.forEach(input => {
// 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;
}
}
// 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':
const numValue = parseInt(element.value);
config[fieldName] = isNaN(numValue) ? fieldConfig.default : numValue;
break;
case 'text':
config[fieldName] = element.value || fieldConfig.default;
break;
default:
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;
break;
case 'text':
element.value = value || '';
break;
default:
element.value = value;
}
}
// Handle use_builtin toggle state
const useBuiltin = config.use_builtin === 1;
const customChatboxInputs = ['block_width', 'num_blocks', 'rows', 'cols'];
customChatboxInputs.forEach(inputId => {
const input = document.getElementById(inputId);
if (input) {
input.disabled = useBuiltin;
if (useBuiltin) {
input.classList.add('opacity-50', 'cursor-not-allowed');
} else {
input.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
});
// 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
}
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}`;
// Redact usernames from paths.
const redactedMessage = message.replace(/([A-Za-z]:[\/\\]Users[\/\\])([^\/\\]+)/g, '$1*****');
messageSpan.textContent = redactedMessage;
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;
}
// Async action handler with better error handling
async function handleAsyncAction(actionName, actionFn) {
try {
const result = await actionFn();
if (result?.message) {
showStatus(result.message, 'success');
}
return result;
} catch (error) {
showStatus(`${actionName} failed: ${error.message}`, 'error');
throw error;
}
}
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 (isProcessRunning) {
appendToConsole('Restarting process with new configuration...', 'info');
try {
await window.electronAPI.stopProcess();
await new Promise(resolve => setTimeout(resolve, 1000));
await window.electronAPI.startProcess();
buttonManager.setProcessRunning();
appendToConsole('Process restarted with new configuration', 'info');
} catch (error) {
appendToConsole(`Failed to restart process: ${error.message}`, 'stderr');
buttonManager.setProcessStopped();
}
}
} catch (error) {
showStatus(`Failed to save configuration: ${error.message}`, 'error');
}
}, SAVE_DELAY);
}
// Auto-save setup
function setupAutoSave() {
const form = document.getElementById('config-form');
const inputs = form.querySelectorAll('input, select, textarea');
inputs.forEach(input => {
const eventType = input.type === 'checkbox' ? 'change' :
(input.type === 'number' || input.type === 'text' || input.tagName === 'TEXTAREA') ? 'input' : 'change';
input.addEventListener(eventType, autoSaveConfig);
});
}
// 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();
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');
option.value = mic.index.toString();
option.textContent = mic.name;
microphoneSelect.appendChild(option);
appendToConsole(` - ${mic.name} (Device ${mic.index})`, 'stdout');
});
// Restore previously selected microphone
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 = '<option value="" disabled>Error loading microphones</option>';
}
}
// 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');
}
});
// 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) {
input.disabled = isBuiltin;
if (isBuiltin) {
input.classList.add('opacity-50', 'cursor-not-allowed');
} else {
input.classList.remove('opacity-50', 'cursor-not-allowed');
}
}
});
});
// 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
try {
await buttonManager.withButtonLoading('setupVenv', async () => {
await window.electronAPI.deleteVenvIndicatorFile();
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 = 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');
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');
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();
setFormValues(config);
appendToConsole('Configuration loaded', 'info');
} catch (error) {
appendToConsole(`Failed to load configuration: ${error.message}`, 'stderr');
}
// Load microphones
await loadMicrophones();
// Setup event handlers and auto-save
setupEventHandlers();
setupAutoSave();
});
|