mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Add morning briefing, command palette, and boot animations
#11 Smart Morning Briefing: auto-speaks once per day before noon — fetches tasks, appointments, active alerts, and weather, composes a ~2-sentence TTS summary. Stored in localStorage (jarvis_brief_YYYY-MM-DD) to fire only once. #12 Quick Command Palette (Ctrl+K): frosted-glass overlay with 20 pre-loaded commands across 6 groups (Network/Agents/Planner/Media/Smart Home/System). Fuzzy filter as you type, arrow-key navigation, Enter to fire. Matches are highlighted. Backdrop click or Escape to close. #13 Live Boot Animation: stat bars and numbers now count from 0 on first render via tickTo() change. Arc Reactor rings spin in with staggered delays (0.08s per ring) on login using boot-spin CSS class + @keyframes arcBootSpin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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 => `<mark>${m}</mark>`);
|
||||
row.innerHTML = `<span class="cp-icon">◈</span><span class="cp-label">${lbl}</span><kbd class="cp-kbd">↵</kbd>`;
|
||||
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();
|
||||
});
|
||||
|
||||
@@ -339,6 +339,17 @@
|
||||
</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>
|
||||
<!-- COMMAND PALETTE -->
|
||||
<div id="cmdPalette">
|
||||
<div id="cmdPaletteBox">
|
||||
<input id="cmdPaletteInput" type="text" placeholder="TYPE A COMMAND..." autocomplete="off" spellcheck="false"/>
|
||||
<div id="cmdPaletteList"></div>
|
||||
<div id="cmdPaletteFooter">
|
||||
<span>↑↓ navigate</span><span>↵ execute</span><span>ESC close</span><span>CTRL+K toggle</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SLEEP OVERLAY -->
|
||||
<div id="sleepOverlay">
|
||||
<div class="sleep-reactor">
|
||||
|
||||
Reference in New Issue
Block a user