|
39 | 39 | skippingWhisper: false, |
40 | 40 | modelDownloadChoice: null, // 'now' | 'later' |
41 | 41 | modelDownloading: false, |
| 42 | + modelDownloaded: false, |
| 43 | + whisperTestPassed: false, |
| 44 | + whisperTesting: false, |
42 | 45 | finished: false, |
43 | 46 | }; |
44 | 47 |
|
|
73 | 76 | } |
74 | 77 | refreshStepper(); |
75 | 78 | backBtn.style.visibility = state.step === 0 ? 'hidden' : 'visible'; |
76 | | - // Reset next button state unless we're actively downloading a model |
77 | | - if (name !== 'model-download' || !state.modelDownloading) { |
| 79 | + // Reset next button state unless we're actively downloading a model or awaiting a test |
| 80 | + const awaitingTest = name === 'model-download' && |
| 81 | + state.modelDownloadChoice === 'now' && |
| 82 | + state.modelDownloaded && |
| 83 | + !state.whisperTestPassed; |
| 84 | + if (name !== 'model-download' || (!state.modelDownloading && !state.whisperTesting && !awaitingTest)) { |
78 | 85 | nextBtn.disabled = false; |
79 | 86 | nextBtn.classList.remove('success'); |
80 | 87 | nextBtn.classList.add('primary'); |
|
131 | 138 | // Allow advancing whether whisper is detected OR user skipped |
132 | 139 | return state.whisperDetected || state.skippingWhisper; |
133 | 140 | case 'model-download': |
134 | | - return !!state.modelDownloadChoice && !state.modelDownloading; |
| 141 | + if (!state.modelDownloadChoice || state.modelDownloading) return false; |
| 142 | + // If user chose to download now, require a successful mic test before continuing. |
| 143 | + if (state.modelDownloadChoice === 'now' && state.modelDownloaded && !state.whisperTestPassed) return false; |
| 144 | + return true; |
135 | 145 | case 'finish': |
136 | 146 | return true; |
137 | 147 | default: |
|
407 | 417 | if (value === 'now') { |
408 | 418 | // Start downloading the model immediately |
409 | 419 | startModelDownload(); |
| 420 | + } else { |
| 421 | + // 'later' doesn't need a test |
| 422 | + nextBtn.disabled = false; |
410 | 423 | } |
411 | 424 | }); |
412 | 425 | }); |
| 426 | + |
| 427 | + // Wire up the Whisper test button |
| 428 | + const testBtn = document.getElementById('testWhisperBtn'); |
| 429 | + if (testBtn) { |
| 430 | + testBtn.addEventListener('click', runWhisperTest); |
| 431 | + } |
413 | 432 | } |
414 | 433 |
|
415 | 434 | // Restore selection state when navigating back |
416 | 435 | $$('#modelDownloadChoices .choice-card').forEach((card) => { |
417 | 436 | card.classList.toggle('selected', card.dataset.value === state.modelDownloadChoice); |
418 | 437 | }); |
419 | 438 |
|
420 | | - // Re-enable continue button if a choice has been made (or download already completed) |
421 | | - if (state.modelDownloadChoice && !state.modelDownloading) { |
422 | | - nextBtn.disabled = false; |
| 439 | + // Restore test panel visibility and state |
| 440 | + const testCard = document.getElementById('whisperTestCard'); |
| 441 | + if (testCard && state.modelDownloadChoice === 'now' && state.modelDownloaded) { |
| 442 | + testCard.style.display = 'block'; |
| 443 | + } |
| 444 | + const testBtn = document.getElementById('testWhisperBtn'); |
| 445 | + if (testBtn && state.whisperTestPassed) { |
| 446 | + testBtn.innerHTML = '<i class="fas fa-check-circle"></i> Test passed'; |
| 447 | + testBtn.classList.remove('primary'); |
| 448 | + testBtn.classList.add('success'); |
| 449 | + } |
| 450 | + |
| 451 | + // Re-enable continue button if a choice has been made and (if applicable) test passed |
| 452 | + if (state.modelDownloadChoice && !state.modelDownloading && !state.whisperTesting) { |
| 453 | + if (state.modelDownloadChoice === 'now' && state.modelDownloaded && !state.whisperTestPassed) { |
| 454 | + nextBtn.disabled = true; |
| 455 | + } else { |
| 456 | + nextBtn.disabled = false; |
| 457 | + } |
423 | 458 | } |
424 | 459 | } |
425 | 460 |
|
|
440 | 475 | const r = await window.electronAPI.downloadWhisperModel('turbo'); |
441 | 476 | state.modelDownloading = false; |
442 | 477 | if (r.ok) { |
| 478 | + state.modelDownloaded = true; |
443 | 479 | appendModelLog(`\n✓ Model downloaded successfully: ${r.path}`); |
444 | | - // Re-enable continue so user can proceed to finish |
445 | | - nextBtn.disabled = false; |
446 | | - nextBtn.classList.remove('primary'); |
447 | | - nextBtn.classList.add('success'); |
448 | | - nextBtn.innerHTML = '<i class="fas fa-check-circle"></i> Continue'; |
| 480 | + // Show the Whisper mic test panel |
| 481 | + const testCard = document.getElementById('whisperTestCard'); |
| 482 | + if (testCard) testCard.style.display = 'block'; |
| 483 | + // Keep Continue disabled until test passes |
| 484 | + nextBtn.disabled = true; |
449 | 485 | } else { |
450 | 486 | appendModelLog(`\n✗ Download failed: ${r.message}`); |
451 | 487 | // Let user continue anyway; they'll download on first use |
452 | 488 | nextBtn.disabled = false; |
453 | | - nextBtn.innerHTML = 'Continue <i class="fas fa-arrow-right"></i>'; |
454 | 489 | } |
455 | 490 | } catch (e) { |
456 | 491 | state.modelDownloading = false; |
457 | 492 | appendModelLog(`\n! Error: ${e.message || e}`); |
458 | 493 | nextBtn.disabled = false; |
459 | | - nextBtn.innerHTML = 'Continue <i class="fas fa-arrow-right"></i>'; |
460 | 494 | } finally { |
461 | 495 | if (progressHandler && window.electronAPI.removeAllListeners) { |
462 | 496 | try { window.electronAPI.removeAllListeners('install-progress'); } catch (_) { /* ignore */ } |
463 | 497 | } |
464 | 498 | } |
465 | 499 | } |
466 | 500 |
|
| 501 | + async function runWhisperTest() { |
| 502 | + const btn = document.getElementById('testWhisperBtn'); |
| 503 | + const resultEl = document.getElementById('whisperTestResult'); |
| 504 | + if (!btn || !window.electronAPI || !window.electronAPI.testWhisperRecording) return; |
| 505 | + |
| 506 | + state.whisperTesting = true; |
| 507 | + btn.disabled = true; |
| 508 | + btn.innerHTML = '<span class="spinner"></span> Listening…'; |
| 509 | + if (resultEl) resultEl.textContent = 'Speak now…'; |
| 510 | + |
| 511 | + try { |
| 512 | + const r = await window.electronAPI.testWhisperRecording(); |
| 513 | + if (r.ok) { |
| 514 | + state.whisperTestPassed = true; |
| 515 | + if (resultEl) resultEl.innerHTML = `<span style="color: var(--success);">✓ Heard:</span> “${escapeHtml(r.text)}”`; |
| 516 | + btn.innerHTML = '<i class="fas fa-check-circle"></i> Test passed'; |
| 517 | + btn.classList.remove('primary'); |
| 518 | + btn.classList.add('success'); |
| 519 | + nextBtn.disabled = false; |
| 520 | + nextBtn.classList.remove('primary'); |
| 521 | + nextBtn.classList.add('success'); |
| 522 | + nextBtn.innerHTML = '<i class="fas fa-check-circle"></i> Continue'; |
| 523 | + } else { |
| 524 | + if (resultEl) resultEl.innerHTML = `<span style="color: var(--error);">✗ ${escapeHtml(r.error || 'Test failed')}</span>`; |
| 525 | + btn.disabled = false; |
| 526 | + btn.innerHTML = '<i class="fas fa-redo"></i> Retry test'; |
| 527 | + } |
| 528 | + } catch (e) { |
| 529 | + if (resultEl) resultEl.innerHTML = `<span style="color: var(--error);">✗ ${escapeHtml(e.message || e)}</span>`; |
| 530 | + btn.disabled = false; |
| 531 | + btn.innerHTML = '<i class="fas fa-redo"></i> Retry test'; |
| 532 | + } finally { |
| 533 | + state.whisperTesting = false; |
| 534 | + } |
| 535 | + } |
| 536 | + |
| 537 | + function escapeHtml(text) { |
| 538 | + const div = document.createElement('div'); |
| 539 | + div.textContent = text; |
| 540 | + return div.innerHTML; |
| 541 | + } |
| 542 | + |
467 | 543 | // ── Wire up: Finish screen ──────────────────────────────────────── |
468 | 544 | function populateSummary() { |
469 | 545 | const rows = []; |
|
0 commit comments