mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Add sleep mode + window focus on wake
Sleep mode: - Commands: good night / go to sleep / shut down / standby / offline / signing off - Works via voice or text - UI dims to 8% brightness, slow-spinning standby reactor overlay appears - Refresh loop pauses (light 2min heartbeat keeps session alive) - Mic stays fully active — only responds to master wake phrases - Idle-reload disabled while sleeping (prevents unwanted reloads overnight) Wake from sleep (master wake phrase): - Detects wake phrase while isAsleep=true, routes to wakeFromSleep() - Full HUD boot sequence animation (panels slide in) - refreshAll() fires immediately to reload all data - JARVIS greets: All systems back online Window focus on any wake: - window.focus() called on every enterVoiceMode - document.title flashes 8x between JARVIS ONLINE and default - Web Notifications API: system popup fires when window is minimized/backgrounded - Notification permission requested 3s after login - Works regardless of sleep/voice mode
This commit is contained in:
+167
-12
@@ -844,6 +844,37 @@ body::after{
|
||||
font-size:0.6rem;display:none;min-width:150px;box-shadow:0 0 18px rgba(0,212,255,0.12)}
|
||||
#nmNodeInfo .ni-title{color:var(--cyan);font-size:0.62rem;letter-spacing:2px;margin-bottom:3px}
|
||||
#nmNodeInfo .ni-row{color:var(--text-dim);margin:2px 0}
|
||||
|
||||
/* ── SLEEP MODE ──────────────────────────────────────────────────────── */
|
||||
#sleepOverlay{
|
||||
position:fixed;inset:0;z-index:500;display:none;
|
||||
flex-direction:column;align-items:center;justify-content:center;
|
||||
background:rgba(0,2,10,0.94);
|
||||
backdrop-filter:blur(6px);
|
||||
}
|
||||
#sleepOverlay.active{display:flex}
|
||||
@keyframes sleepPulse{0%,100%{opacity:0.25;transform:scale(1)}50%{opacity:0.6;transform:scale(1.06)}}
|
||||
@keyframes sleepCoreGlow{0%,100%{box-shadow:0 0 30px rgba(0,212,255,0.15),0 0 60px rgba(0,212,255,0.05)}50%{box-shadow:0 0 50px rgba(0,212,255,0.3),0 0 100px rgba(0,212,255,0.1)}}
|
||||
.sleep-reactor{position:relative;width:120px;height:120px;margin-bottom:40px}
|
||||
.sleep-ring{position:absolute;border-radius:50%;border:1px solid rgba(0,212,255,0.2);top:50%;left:50%;transform:translate(-50%,-50%)}
|
||||
.sleep-ring.sr1{width:120px;height:120px;animation:spinRing 18s linear infinite;border-color:rgba(0,212,255,0.12)}
|
||||
.sleep-ring.sr2{width:85px;height:85px;animation:spinRing 12s linear infinite reverse;border-color:rgba(0,212,255,0.18)}
|
||||
.sleep-ring.sr3{width:52px;height:52px;animation:spinRing 8s linear infinite;border-color:rgba(0,80,160,0.3)}
|
||||
.sleep-core{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);
|
||||
width:24px;height:24px;border-radius:50%;
|
||||
background:radial-gradient(circle,rgba(0,150,220,0.6) 0%,rgba(0,80,160,0.3) 60%,transparent 100%);
|
||||
animation:sleepCoreGlow 4s ease-in-out infinite,sleepPulse 4s ease-in-out infinite}
|
||||
.sleep-label{font-family:var(--font-display);font-size:0.6rem;letter-spacing:6px;
|
||||
color:rgba(0,212,255,0.35);animation:sleepPulse 4s ease-in-out infinite;margin-bottom:12px}
|
||||
.sleep-sub{font-family:var(--font-mono);font-size:0.55rem;letter-spacing:3px;
|
||||
color:rgba(0,212,255,0.2);animation:sleepPulse 4s ease-in-out infinite;animation-delay:0.5s}
|
||||
/* App dims on sleep */
|
||||
#app.sleeping #mainLayout,#app.sleeping #topBar,#app.sleeping #bottomBar{
|
||||
pointer-events:none;
|
||||
filter:brightness(0.08) saturate(0.3);
|
||||
transition:filter 1.2s ease;
|
||||
}
|
||||
#app.sleeping #sleepOverlay{display:flex}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -1118,6 +1149,18 @@ body::after{
|
||||
</div>
|
||||
<div id="nmNodeInfo"><div class="ni-title" id="ni-name">—</div><div class="ni-row" id="ni-ip"></div><div class="ni-row" id="ni-status"></div><div class="ni-row" id="ni-type"></div></div>
|
||||
</div>
|
||||
<!-- SLEEP OVERLAY -->
|
||||
<div id="sleepOverlay">
|
||||
<div class="sleep-reactor">
|
||||
<div class="sleep-ring sr1"></div>
|
||||
<div class="sleep-ring sr2"></div>
|
||||
<div class="sleep-ring sr3"></div>
|
||||
<div class="sleep-core"></div>
|
||||
</div>
|
||||
<div class="sleep-label">JARVIS — STANDBY</div>
|
||||
<div class="sleep-sub">SAY "WAKE UP JARVIS" TO RESUME</div>
|
||||
</div>
|
||||
|
||||
<div id="sitesModal" style="position:fixed;inset:0;background:rgba(0,0,0,0.92);z-index:9999;display:none;align-items:flex-start;justify-content:center;padding:24px;overflow-y:auto">
|
||||
<div style="background:var(--panel-bg);border:1px solid var(--panel-border);width:100%;max-width:960px;font-family:var(--font-mono)">
|
||||
<div style="display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--panel-border)">
|
||||
@@ -1713,6 +1756,93 @@ function _drawTopo() {
|
||||
})();
|
||||
|
||||
|
||||
// ── SLEEP MODE ────────────────────────────────────────────────────────────────
|
||||
var isAsleep = false;
|
||||
var _sleepRefreshTimer = null;
|
||||
|
||||
var SLEEP_CMDS = /\b(good\s*night|go\s*to\s*sleep|sleep\s*(mode|now)?|shut\s*(down|off)\s*(jarvis|for\s*the\s*night)?|offline|stand\s*by|power\s*down|hibernate|signing\s*off)\b/i;
|
||||
|
||||
function enterSleepMode() {
|
||||
if (isAsleep) return;
|
||||
isAsleep = true;
|
||||
|
||||
// Pause voice mode
|
||||
voiceMode = false;
|
||||
voiceMuted = false;
|
||||
updateMicBtn();
|
||||
|
||||
// Slow or pause the refresh loop — keep mic alive for wake word
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
// Light polling every 2 min just to stay alive
|
||||
_sleepRefreshTimer = setInterval(function() {
|
||||
// heartbeat only — keep session alive without hammering APIs
|
||||
try { fetch('/api/auth', {method:'GET', headers:{'Authorization':'Bearer '+sessionToken}}); } catch(e) {}
|
||||
}, 120000);
|
||||
|
||||
// Dim the UI
|
||||
var app = document.getElementById('app');
|
||||
if (app) app.classList.add('sleeping');
|
||||
|
||||
// Flash title to confirm
|
||||
document.title = 'JARVIS — STANDBY';
|
||||
|
||||
addMessage('jarvis', 'Understood. Going offline. Say "wake up JARVIS" when you need me.');
|
||||
}
|
||||
|
||||
function wakeFromSleep() {
|
||||
if (!isAsleep) return;
|
||||
isAsleep = false;
|
||||
|
||||
// Restore full polling
|
||||
clearInterval(_sleepRefreshTimer);
|
||||
_sleepRefreshTimer = null;
|
||||
refreshAll();
|
||||
refreshTimer = setInterval(refreshAll, 10000);
|
||||
|
||||
// Remove dim overlay
|
||||
var app = document.getElementById('app');
|
||||
if (app) app.classList.remove('sleeping');
|
||||
document.title = 'JARVIS — Integrated Defense and Logistics System';
|
||||
|
||||
// Boot sequence
|
||||
var topBar=document.getElementById('topBar'), lp=document.getElementById('leftPanel');
|
||||
var rp=document.getElementById('rightPanel'), cp=document.getElementById('centerPanel');
|
||||
[topBar,lp,rp,cp].forEach(function(el){if(el){el.style.opacity='0';}});
|
||||
requestAnimationFrame(function(){
|
||||
setTimeout(function(){if(topBar){topBar.style.opacity='';topBar.classList.add('boot-top');}},0);
|
||||
setTimeout(function(){if(lp){lp.style.opacity='';lp.classList.add('boot-left');}},140);
|
||||
setTimeout(function(){if(rp){rp.style.opacity='';rp.classList.add('boot-right');}},200);
|
||||
setTimeout(function(){if(cp){cp.style.opacity='';cp.classList.add('boot-center');}},260);
|
||||
setTimeout(function(){[topBar,lp,rp,cp].forEach(function(el){if(el)el.classList.remove('boot-top','boot-left','boot-right','boot-center');});},1400);
|
||||
});
|
||||
|
||||
// Enter voice mode and greet
|
||||
enterVoiceMode('wake');
|
||||
}
|
||||
|
||||
function _focusWindow() {
|
||||
// Attempt to bring browser window to front
|
||||
try { window.focus(); } catch(e) {}
|
||||
|
||||
// Flash title to grab attention if tab is backgrounded
|
||||
var _origTitle = 'JARVIS — Integrated Defense and Logistics System';
|
||||
var _flashCount = 0;
|
||||
var _titleFlash = setInterval(function() {
|
||||
document.title = _flashCount % 2 === 0 ? '⚡ JARVIS — ONLINE' : _origTitle;
|
||||
if (++_flashCount >= 8) { clearInterval(_titleFlash); document.title = _origTitle; }
|
||||
}, 400);
|
||||
|
||||
// Browser Notification API — fires even when window is minimized
|
||||
if ('Notification' in window) {
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('JARVIS', { body: 'Wake word detected — system online.', icon: '/favicon.ico', tag: 'jarvis-wake', requireInteraction: false });
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── NETWORK MAP ──────────────────────────────────────────────────────────────
|
||||
var _nmNodes=[], _nmEdges=[], _nmParticles=[], _nmRaf=null, _nmT=0, _nmHoverNode=null;
|
||||
var _nmRot=[0,0,0,0,0];
|
||||
@@ -2059,7 +2189,7 @@ function showApp(name, greeting, silent = false) {
|
||||
refreshAll();
|
||||
refreshTimer = setInterval(refreshAll, 10000); // every 10s
|
||||
setInterval(() => {
|
||||
if (Date.now() - lastActivity > IDLE_RELOAD_MS) {
|
||||
if (!isAsleep && Date.now() - lastActivity > IDLE_RELOAD_MS) {
|
||||
sessionStorage.setItem('jarvis_autoreload', '1');
|
||||
location.reload();
|
||||
}
|
||||
@@ -2085,6 +2215,10 @@ function showApp(name, greeting, silent = false) {
|
||||
}
|
||||
}, 12000);
|
||||
startListening();
|
||||
// Request notification permission for wake-word alerts when minimized
|
||||
if ('Notification' in window && Notification.permission === 'default') {
|
||||
setTimeout(() => Notification.requestPermission(), 3000);
|
||||
}
|
||||
loadNetwork();
|
||||
loadHA();
|
||||
checkAgentStatus();
|
||||
@@ -2869,6 +3003,15 @@ async function sendMessage() {
|
||||
|
||||
// Local commands — no API round-trip
|
||||
var t2 = text.toLowerCase();
|
||||
|
||||
// Sleep command
|
||||
if (SLEEP_CMDS.test(t2)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
enterSleepMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (NM_OPEN_RE.test(t2)) {
|
||||
input.value=''; addMessage('user',text);
|
||||
addMessage('jarvis','Launching network topology display.');
|
||||
@@ -2948,16 +3091,28 @@ function initVoice() {
|
||||
const transcript = (e.results[0][0].transcript || '').trim();
|
||||
if (!transcript) return;
|
||||
const lc = transcript.toLowerCase();
|
||||
|
||||
// Sleeping: ONLY respond to master wake phrases
|
||||
if (isAsleep) {
|
||||
if (WAKE_PHRASES.some(p => lc.includes(p))) wakeFromSleep();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!voiceMode) {
|
||||
if (WAKE_PHRASES.some(p => lc.includes(p))) enterVoiceMode();
|
||||
} else if (!voiceMuted) {
|
||||
// Awake — any speech is a command; strip optional "jarvis" prefix
|
||||
voiceLastCmd = Date.now();
|
||||
voiceActive = Date.now();
|
||||
const cmd = lc.startsWith(CMD_PREFIX)
|
||||
? transcript.substring(CMD_PREFIX.length).trim()
|
||||
: transcript;
|
||||
if (cmd) {
|
||||
// Check for sleep command by voice
|
||||
if (SLEEP_CMDS.test(cmd)) {
|
||||
addMessage('user', transcript);
|
||||
enterSleepMode();
|
||||
return;
|
||||
}
|
||||
_showTranscript(cmd);
|
||||
document.getElementById('textInput').value = cmd;
|
||||
sendMessage();
|
||||
@@ -2991,21 +3146,21 @@ function _showTranscript(text) {
|
||||
if (el) { el.placeholder = '▶ ' + text.substring(0, 60); setTimeout(() => { el.placeholder = 'Enter command or speak to JARVIS...'; }, 3000); }
|
||||
}
|
||||
|
||||
function enterVoiceMode() {
|
||||
function enterVoiceMode(source) {
|
||||
voiceMode = true;
|
||||
voiceMuted = false;
|
||||
voiceLastCmd = Date.now();
|
||||
voiceActive = Date.now();
|
||||
updateMicBtn();
|
||||
speak('Yes, ' + (sessionUser || 'Sir') + '?');
|
||||
// Bring window to front and maximize when JARVIS wakes
|
||||
try {
|
||||
window.focus();
|
||||
if (!document.fullscreenElement && window.screen) {
|
||||
window.moveTo(0, 0);
|
||||
window.resizeTo(window.screen.availWidth, window.screen.availHeight);
|
||||
}
|
||||
} catch(e) {}
|
||||
// Focus/notify when woken from minimized or sleep
|
||||
_focusWindow();
|
||||
if (source === 'wake') {
|
||||
const g = 'All systems back online, ' + (sessionUser || 'Sir') + '. Good to have you back.';
|
||||
addMessage('jarvis', g);
|
||||
speak(g);
|
||||
} else {
|
||||
speak('Yes, ' + (sessionUser || 'Sir') + '?');
|
||||
}
|
||||
}
|
||||
|
||||
function exitVoiceMode() {
|
||||
|
||||
Reference in New Issue
Block a user