diff --git a/public_html/index.html b/public_html/index.html index 1268743..e6a71f0 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -445,6 +445,7 @@ body::after{ animation:micPulse 0.8s ease-in-out infinite; } @keyframes micPulse{0%,100%{box-shadow:0 0 25px rgba(255,34,68,0.5)}50%{box-shadow:0 0 40px rgba(255,34,68,0.8),0 0 60px rgba(255,34,68,0.3)}} +#micBtn.muted{border-color:var(--text-dim);background:radial-gradient(circle,rgba(200,230,255,0.05),rgba(0,8,22,0.9));box-shadow:0 0 8px rgba(200,230,255,0.1);} #micIcon{font-size:20px} /* WAVEFORM ─────────────────────────────────────────────────────────── */ @@ -915,6 +916,11 @@ let autoMicCooldown = 0; let faceApiReady = false; let lastActivity = Date.now(); const IDLE_RELOAD_MS = 5 * 60 * 1000; // 5 min inactivity → full reload +let voiceMode = false; // true = JARVIS awake (listening for commands) +let voiceMuted = false; // true = awake but mic muted +let voiceLastCmd = 0; +const VOICE_SLEEP_MS = 30 * 60 * 1000; // 30 min voice inactivity → sleep +const WAKE_WORDS = ['jarvis', 'hey jarvis', "daddy's home", 'wake up']; const FACE_MODEL_URL = 'https://cdn.jsdelivr.net/gh/justadudewhohacks/face-api.js@0.22.2/weights'; // ── INIT ───────────────────────────────────────────────────────────── @@ -928,11 +934,13 @@ window.addEventListener("load", () => { loadVoices(); // Check if already logged in - const saved = sessionStorage.getItem('jarvis_token'); + const saved = sessionStorage.getItem('jarvis_token'); + const autoReload = sessionStorage.getItem('jarvis_autoreload') === '1'; + sessionStorage.removeItem('jarvis_autoreload'); if (saved) { sessionToken = saved; sessionUser = sessionStorage.getItem('jarvis_user') || ''; - showApp(sessionUser); + showApp(sessionUser, null, autoReload); } }); @@ -968,24 +976,37 @@ document.getElementById('loginForm').addEventListener('submit', async (e) => { } }); -function showApp(name, greeting) { +function showApp(name, greeting, silent = false) { document.getElementById('loginScreen').style.display = 'none'; const app = document.getElementById('app'); app.style.display = 'flex'; - if (greeting) { - addMessage('jarvis', greeting); - speak(greeting); - } else { - const g = `Welcome back, ${name}. All systems online and standing by.`; - addMessage('jarvis', g); - speak(g); + if (!silent) { + if (greeting) { + addMessage('jarvis', greeting); + speak(greeting); + } else { + const g = `Welcome back, ${name}. All systems online and standing by.`; + addMessage('jarvis', g); + speak(g); + } } // Start data refresh refreshAll(); refreshTimer = setInterval(refreshAll, 10000); // every 10s - setInterval(() => { if (Date.now() - lastActivity > IDLE_RELOAD_MS) location.reload(); }, 30000); + setInterval(() => { + if (Date.now() - lastActivity > IDLE_RELOAD_MS) { + sessionStorage.setItem('jarvis_autoreload', '1'); + location.reload(); + } + }, 30000); + setInterval(() => { + if (voiceMode && voiceLastCmd > 0 && Date.now() - voiceLastCmd > VOICE_SLEEP_MS) { + exitVoiceMode(); + } + }, 60000); + startListening(); loadNetwork(); loadHA(); checkAgentStatus(); @@ -1090,15 +1111,15 @@ async function startCamera() { const {width, height} = detection.box; const ratio = (width * height) / (320 * 240); // Trigger if face fills >3% of frame and mic not already on and cooldown passed - if (ratio > 0.03 && !isListening && now > autoMicCooldown) { + if (ratio > 0.03 && !voiceMode && now > autoMicCooldown) { autoMicCooldown = now + 9000; // 9s between auto-triggers document.getElementById('cameraBtn').classList.add('cam-sensing'); - startListening(); + enterVoiceMode(); } } else { - // No face — stop if auto-triggered and face gone >3s - if (isListening && now - lastFaceSeen > 3000) { - stopListening(); + // No face — exit voice mode if camera-triggered and face gone >3s + if (voiceMode && now - lastFaceSeen > 3000) { + exitVoiceMode(); } document.getElementById('cameraBtn').classList.remove('cam-sensing'); } @@ -1682,44 +1703,101 @@ async function sendMessage() { function initVoice() { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { - // Chrome blocks speech API on untrusted HTTPS (self-signed certs) if (window.isSecureContext === false) { - console.warn('Speech Recognition blocked: page is not a secure context (self-signed cert?)'); + console.warn('Speech Recognition blocked: not a secure context'); } else { console.warn('Speech Recognition not supported in this browser'); } return; } recognition = new SR(); - recognition.continuous = false; + recognition.continuous = true; recognition.interimResults = false; recognition.lang = 'en-US'; recognition.onresult = (e) => { - const transcript = e.results[0][0].transcript; - document.getElementById('textInput').value = transcript; - stopListening(); - sendMessage(); + const result = e.results[e.results.length - 1]; + if (!result.isFinal) return; + const transcript = result[0].transcript.trim(); + if (!transcript) return; + if (!voiceMode) { + // Sleeping — check for wake word + const lc = transcript.toLowerCase(); + if (WAKE_WORDS.some(w => lc.includes(w))) enterVoiceMode(); + } else if (!voiceMuted) { + // Awake — process as command + voiceLastCmd = Date.now(); + document.getElementById('textInput').value = transcript; + sendMessage(); + } }; recognition.onend = () => { - if (isListening) stopListening(); + // Always restart for continuous wake word / command listening + if (isListening) setTimeout(() => { try { recognition.start(); } catch(_) {} }, 100); }; recognition.onerror = (e) => { - stopListening(); if (e.error === 'not-allowed') { + isListening = false; + updateMicBtn(); addMessage('system', 'Microphone access denied. Please allow microphone permission in your browser, then reload.'); } else if (e.error === 'audio-capture') { + isListening = false; + updateMicBtn(); addMessage('system', 'No microphone detected. Please connect a microphone and try again.'); - } else if (e.error !== 'no-speech') { - addMessage('system', 'Voice error: ' + e.error); } + // no-speech and aborted are normal — onend will restart }; } +function enterVoiceMode() { + voiceMode = true; + voiceMuted = false; + voiceLastCmd = Date.now(); + updateMicBtn(); + speak('Yes, ' + (sessionUser || 'Sir') + '?'); +} + +function exitVoiceMode() { + voiceMode = false; + voiceMuted = false; + updateMicBtn(); +} + +function updateMicBtn() { + const btn = document.getElementById('micBtn'); + const icon = document.getElementById('micIcon'); + const wave = document.getElementById('waveform'); + if (!btn) return; + if (!voiceMode) { + btn.classList.remove('listening', 'muted'); + btn.title = 'Click to activate or say "Hey JARVIS"'; + icon.textContent = '🎤'; + wave.classList.remove('active'); + } else if (voiceMuted) { + btn.classList.remove('listening'); + btn.classList.add('muted'); + btn.title = 'Muted — click to unmute'; + icon.textContent = '🔇'; + wave.classList.remove('active'); + } else { + btn.classList.add('listening'); + btn.classList.remove('muted'); + btn.title = 'Listening — click to mute'; + icon.textContent = '🟢'; + wave.classList.add('active'); + } +} + function toggleVoice() { - if (isListening) stopListening(); else startListening(); + if (!voiceMode) { + enterVoiceMode(); + } else { + voiceMuted = !voiceMuted; + if (!voiceMuted) voiceLastCmd = Date.now(); + updateMicBtn(); + } } function startListening() { @@ -1732,23 +1810,15 @@ function startListening() { return; } isListening = true; - document.getElementById('micBtn').classList.add('listening'); - document.getElementById('micIcon').textContent = '🔴'; - document.getElementById('waveform').classList.add('active'); - try { - recognition.start(); - } catch(e) { - stopListening(); - addMessage('system', 'Could not start microphone: ' + e.message); - } + try { recognition.start(); } catch(_) {} } function stopListening() { isListening = false; - document.getElementById('micBtn').classList.remove('listening'); - document.getElementById('micIcon').textContent = '🎤'; - document.getElementById('waveform').classList.remove('active'); - try { recognition.stop(); } catch(e) {} + voiceMode = false; + voiceMuted = false; + updateMicBtn(); + try { recognition.abort(); } catch(_) {} } // ── SPEECH SYNTHESIS ──────────────────────────────────────────────────