class VoiceModule { constructor() { this.commands = {}; this.terminal = null; this.recognition = null; this.listening = false; this._lastError = ''; } requirements() { if (typeof SpeechRecognition !== 'undefined' || typeof webkitSpeechRecognition !== 'undefined') { return { met: true, missing: [] }; } return { met: false, missing: ['SpeechRecognition not available in this browser'] }; } getHelpInfo() { return { 'voice': 'voice [stop] — Start voice recognition, or type "voice stop" to cancel' }; } registerCommands() { this.commands['voice'] = (args) => this.handle(args); } handle(args) { const sub = args[0] && args[0].toLowerCase(); if (sub === 'stop') return this.stopListening(); if (this.listening) return this.terminal.print('Already listening. Say something or type "voice stop"', 'warning'); this.startListening(); } _updateButton(listening, error) { const btn = document.getElementById('voice-toggle-btn'); if (!btn) return; if (listening) { btn.style.borderColor = 'var(--success-color)'; btn.style.color = 'var(--success-color)'; btn.style.animation = 'pulse 1.5s ease-in-out infinite'; btn.textContent = '🔴 Listening'; } else { btn.style.borderColor = ''; btn.style.color = ''; btn.style.animation = ''; if (error) { btn.style.borderColor = 'var(--error-color)'; btn.style.color = 'var(--error-color)'; } else { btn.style.borderColor = ''; btn.style.color = ''; } btn.textContent = '🎤 Mic'; } } startListening() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { this._updateButton(false, true); return this.terminal.print('Speech recognition not supported in this browser', 'error'); } this.recognition = new SpeechRecognition(); this.recognition.lang = 'en-US'; this.recognition.interimResults = false; this.recognition.maxAlternatives = 1; this.recognition.continuous = false; this.listening = true; this._updateButton(true); this.terminal.print('🎤 Listening... (say a command, or tap mic button to cancel)', 'info'); this.recognition.onresult = (event) => { const transcript = event.results[0][0].transcript; this.terminal.print(`🎤 Recognized: "${transcript}"`, 'success'); const input = this.terminal.inputField; input.value = transcript; input.focus(); this.listening = false; this._updateButton(false); }; this.recognition.onerror = (event) => { this.terminal.print(`🎤 Error: ${event.error}`, 'error'); this.listening = false; this._updateButton(false, true); }; this.recognition.onend = () => { if (this.listening) { this.listening = false; this._updateButton(false); this.terminal.print('🎤 Voice input ended', 'info'); } }; try { this.recognition.start(); } catch (e) { this.terminal.print(`🎤 Failed to start: ${e.message}`, 'error'); this.listening = false; this._updateButton(false, true); } } stopListening() { if (this.recognition && this.listening) { this.recognition.stop(); this.listening = false; this._updateButton(false); this.terminal.print('🎤 Listening stopped', 'info'); } } } if (typeof window.webx11 !== 'undefined' && window.webx11) { webx11.moduleManager.registerModule(new VoiceModule(), 'https://webx11.duckdns.org/spaces/newos4/modules/ext/voice.js'); }