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 ──────────────────────────────────────────────────