diff --git a/public_html/assets/css/jarvis.css b/public_html/assets/css/jarvis.css index 76a8746..3a3065b 100644 --- a/public_html/assets/css/jarvis.css +++ b/public_html/assets/css/jarvis.css @@ -1101,3 +1101,64 @@ body::after{ .intel-new-btn:hover{background:rgba(0,212,255,0.12)} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}} + +/* ── ARC REACTOR BOOT SPIN ───────────────────────────────────────────── */ +@keyframes arcBootSpin{ + 0%{transform:rotate(0deg) scale(0.6);opacity:0.2} + 40%{opacity:1} + 100%{transform:rotate(360deg) scale(1);opacity:1} +} +#arcReactor.boot-spin .arc-ring{animation:arcBootSpin 1.4s cubic-bezier(0.4,0,0.2,1) both!important} +#arcReactor.boot-spin .arc-ring.r3{animation-delay:0s!important} +#arcReactor.boot-spin .arc-ring.r5{animation-delay:0.08s!important} +#arcReactor.boot-spin .arc-ring.r7{animation-delay:0.16s!important} +#arcReactor.boot-spin .arc-core{animation:arcBootSpin 1.0s 0.3s cubic-bezier(0.4,0,0.2,1) both!important} + +/* ── COMMAND PALETTE ─────────────────────────────────────────────────── */ +#cmdPalette{ + position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:10000; + display:none;align-items:flex-start;justify-content:center; + padding-top:12vh;backdrop-filter:blur(4px); + opacity:0;transition:opacity 0.18s ease; +} +#cmdPalette.open{opacity:1} +#cmdPaletteBox{ + width:min(640px,92vw);background:rgba(5,15,25,0.97); + border:1px solid rgba(0,212,255,0.35);border-radius:4px; + box-shadow:0 0 40px rgba(0,212,255,0.15),0 20px 60px rgba(0,0,0,0.8); + overflow:hidden;transform:translateY(-8px);transition:transform 0.18s ease; +} +#cmdPalette.open #cmdPaletteBox{transform:translateY(0)} +#cmdPaletteInput{ + width:100%;background:transparent;border:none;border-bottom:1px solid rgba(0,212,255,0.2); + color:var(--cyan);font-family:var(--font-display);font-size:0.85rem;letter-spacing:1px; + padding:16px 20px;outline:none;caret-color:var(--cyan); +} +#cmdPaletteInput::placeholder{color:rgba(0,212,255,0.3);letter-spacing:2px} +#cmdPaletteList{max-height:360px;overflow-y:auto;padding:8px 0} +#cmdPaletteList .cp-group{ + font-family:var(--font-display);font-size:0.4rem;letter-spacing:3px; + color:rgba(0,212,255,0.4);padding:8px 20px 4px;text-transform:uppercase +} +#cmdPaletteList .cp-item{ + display:flex;align-items:center;gap:10px;padding:8px 20px;cursor:pointer; + font-family:var(--font-mono);font-size:0.68rem;color:rgba(200,230,255,0.7); + transition:background 0.1s,color 0.1s; +} +#cmdPaletteList .cp-item:hover,#cmdPaletteList .cp-item.cp-active{ + background:rgba(0,212,255,0.08);color:var(--cyan) +} +#cmdPaletteList .cp-item .cp-icon{color:rgba(0,212,255,0.4);flex-shrink:0;font-size:0.6rem} +#cmdPaletteList .cp-item .cp-label{flex:1} +#cmdPaletteList .cp-item .cp-label mark{background:none;color:var(--cyan);font-weight:700} +#cmdPaletteList .cp-item .cp-kbd{ + font-family:var(--font-mono);font-size:0.45rem;color:rgba(0,212,255,0.3); + border:1px solid rgba(0,212,255,0.2);border-radius:2px;padding:1px 5px; + opacity:0;transition:opacity 0.1s; +} +#cmdPaletteList .cp-item.cp-active .cp-kbd,#cmdPaletteList .cp-item:hover .cp-kbd{opacity:1} +#cmdPaletteFooter{ + font-family:var(--font-display);font-size:0.4rem;letter-spacing:2px; + color:rgba(0,212,255,0.25);padding:8px 20px;border-top:1px solid rgba(0,212,255,0.1); + display:flex;gap:16px +} diff --git a/public_html/assets/js/jarvis-app.js b/public_html/assets/js/jarvis-app.js index e2a41bd..db444ca 100644 --- a/public_html/assets/js/jarvis-app.js +++ b/public_html/assets/js/jarvis-app.js @@ -135,6 +135,21 @@ function showApp(name, greeting, silent = false) { } } + // Smart morning briefing: auto-speak once per day before noon + const _briefKey = 'jarvis_brief_' + new Date().toISOString().slice(0, 10); + const _briefHour = new Date().getHours(); + if (!silent && _briefHour < 12 && !localStorage.getItem(_briefKey)) { + localStorage.setItem(_briefKey, '1'); + setTimeout(triggerMorningBriefing, 3500); + } + + // Arc Reactor boot spin-up + const _ar = document.getElementById('arcReactor'); + if (_ar) { + _ar.classList.add('boot-spin'); + setTimeout(() => _ar.classList.remove('boot-spin'), 1600); + } + // Start data refresh initCollapsiblePanels(); refreshAll(); @@ -419,7 +434,7 @@ const _prevVals = {}; function tickTo(id, newVal, unit='%', decimals=0) { const el = document.getElementById(id); if (!el) return; - const prev = _prevVals[id] ?? newVal; + const prev = _prevVals[id] !== undefined ? _prevVals[id] : 0; _prevVals[id] = newVal; if (Math.abs(newVal - prev) < 0.5) { el.textContent = newVal.toFixed(decimals) + unit; return; } const start = performance.now(), dur = 700; @@ -1480,3 +1495,35 @@ async function checkAgentStatus() { if (sta) sta.textContent = 'ERROR'; } } + +// ── SMART MORNING BRIEFING ───────────────────────────────────────────────── +async function triggerMorningBriefing() { + try { + const [planner, alerts, weather] = await Promise.all([ + api('planner/today').catch(() => null), + api('alerts').catch(() => null), + api('weather').catch(() => null), + ]); + + const tasks = (planner?.tasks || []).filter(t => t.status !== 'done'); + const appts = planner?.appointments || []; + const active = (alerts?.alerts || alerts || []).filter(a => + a.severity === 'critical' || a.severity === 'warning'); + const temp = weather?.current?.temp_f ?? weather?.current?.temp ?? null; + const cond = weather?.current?.condition?.text ?? weather?.current?.description ?? null; + + const parts = []; + if (tasks.length > 0) parts.push(`${tasks.length} task${tasks.length > 1 ? 's' : ''} due today`); + if (appts.length > 0) parts.push(`${appts.length} appointment${appts.length > 1 ? 's' : ''} on the calendar`); + if (active.length > 0) parts.push(`${active.length} active alert${active.length > 1 ? 's' : ''} requiring attention`); + if (temp !== null) parts.push(`currently ${Math.round(temp)}°${cond ? ' and ' + cond.toLowerCase() : ''}`); + + const name = sessionUser || 'sir'; + const msg = parts.length > 0 + ? `Good morning, ${name}. ${parts.join(', ')}. Systems nominal — ready when you are.` + : `Good morning, ${name}. No tasks or alerts today — clear skies ahead. All systems nominal.`; + + addMessage('jarvis', msg); + speak(msg); + } catch(e) {} +} diff --git a/public_html/assets/js/jarvis-protocols.js b/public_html/assets/js/jarvis-protocols.js index 4399087..28ce31f 100644 --- a/public_html/assets/js/jarvis-protocols.js +++ b/public_html/assets/js/jarvis-protocols.js @@ -1411,3 +1411,135 @@ function initMobile() { document.getElementById('mob-btn-left')?.classList.add('active'); } window.addEventListener('resize', initMobile); + +// ── COMMAND PALETTE (Ctrl+K) ────────────────────────────────────────────── +const _PALETTE_COMMANDS = [ + { label: 'Run a network scan', q: 'run a network scan', group: 'Network' }, + { label: 'Show online devices', q: 'who is online on the network', group: 'Network' }, + { label: 'Proxmox status', q: 'proxmox status', group: 'Network' }, + { label: 'Check agent status', q: 'check all agents', group: 'Agents' }, + { label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' }, + { label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' }, + { label: 'Daily briefing', q: 'daily briefing', group: 'Planner' }, + { label: 'My tasks today', q: 'my tasks today', group: 'Planner' }, + { label: 'My calendar', q: 'my calendar', group: 'Planner' }, + { label: 'What's playing on Jellyfin', q: 'what is playing on Jellyfin', group: 'Media' }, + { label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' }, + { label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' }, + { label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' }, + { label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'}, + { label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'}, + { label: 'Focus mode', q: 'focus mode', group: 'UI' }, + { label: 'Show all panels', q: 'show all panels', group: 'UI' }, + { label: 'Check alerts', q: 'check alerts', group: 'System' }, + { label: 'Site health', q: 'site health', group: 'System' }, + { label: 'System status', q: 'system status', group: 'System' }, + { label: 'Check inbox', q: 'check inbox', group: 'Comms' }, + { label: 'Search history…', q: '', group: 'Chat', search: true }, +]; + +let _paletteOpen = false; + +function openPalette() { + if (_paletteOpen) return; + _paletteOpen = true; + const ov = document.getElementById('cmdPalette'); + if (!ov) return; + ov.style.display = 'flex'; + const inp = document.getElementById('cmdPaletteInput'); + inp.value = ''; + renderPaletteItems(''); + requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); }); +} + +function closePalette() { + if (!_paletteOpen) return; + _paletteOpen = false; + const ov = document.getElementById('cmdPalette'); + if (!ov) return; + ov.classList.remove('open'); + setTimeout(() => { ov.style.display = 'none'; }, 180); +} + +function renderPaletteItems(q) { + const list = document.getElementById('cmdPaletteList'); + if (!list) return; + const low = q.toLowerCase().trim(); + const filtered = low + ? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low)) + : _PALETTE_COMMANDS; + + let currentGroup = null; + list.innerHTML = ''; + filtered.forEach((cmd, i) => { + if (cmd.group !== currentGroup) { + currentGroup = cmd.group; + const g = document.createElement('div'); + g.className = 'cp-group'; + g.textContent = cmd.group; + list.appendChild(g); + } + const row = document.createElement('div'); + row.className = 'cp-item' + (i === 0 ? ' cp-active' : ''); + row.dataset.q = cmd.q; + row.dataset.search = cmd.search ? '1' : ''; + const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), + m => `${m}`); + row.innerHTML = `${lbl}`; + row.addEventListener('click', () => firePaletteItem(row)); + list.appendChild(row); + }); +} + +function movePaletteSelection(dir) { + const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item')); + if (!items.length) return; + const cur = items.findIndex(el => el.classList.contains('cp-active')); + const next = (cur + dir + items.length) % items.length; + items.forEach(el => el.classList.remove('cp-active')); + items[next].classList.add('cp-active'); + items[next].scrollIntoView({ block: 'nearest' }); +} + +function firePaletteItem(el) { + if (!el) { + const active = document.querySelector('#cmdPaletteList .cp-active'); + if (!active) return; + el = active; + } + const q = el.dataset.q; + const isSearch = el.dataset.search === '1'; + closePalette(); + if (isSearch) { + if (typeof openSearchModal === 'function') openSearchModal(); + return; + } + if (q) { + document.getElementById('textInput').value = q; + sendMessage(); + } +} + +// Keyboard events +document.addEventListener('keydown', e => { + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + _paletteOpen ? closePalette() : openPalette(); + return; + } + if (!_paletteOpen) return; + if (e.key === 'Escape') { e.preventDefault(); closePalette(); } + if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); } + if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); } + if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); } +}); + +// Filter on type +document.getElementById('cmdPaletteInput')?.addEventListener('input', e => { + renderPaletteItems(e.target.value); +}); + +// Close on backdrop click +document.getElementById('cmdPalette')?.addEventListener('click', e => { + if (e.target.id === 'cmdPalette') closePalette(); +}); diff --git a/public_html/index.html b/public_html/index.html index 2169d6e..8a69213 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -339,6 +339,17 @@
+ +
+
+ +
+
+ ↑↓ navigate↵ executeESC closeCTRL+K toggle +
+
+
+