Add JARVIS improvements: mobile UI, sparklines, suggestions, multi-step commands, Arc Reactor health, tier badges

- Mobile UI: 3-button bottom nav with panel switcher
- Chat history search: search modal with keyword query
- News filtering: category filter with localStorage persistence
- Proactive reminders: planner/appointment alerts at login and every 5 min
- Proactive alerts: polls every 60s, speaks new critical/warning alerts
- Agent sparklines: 2h CPU+MEM sparkline on each online agent card
- Tier source badge: KB/GROQ/CLAUDE/OLLAMA pill shown after each reply
- VM suggestions: 24h resource analysis via voice command
- HA scene control: fuzzy-match scene activation via voice
- Jellyfin control: pause/stop/next/previous via voice and KB
- Pattern suggestions: usage_patterns table + proactive chips every 30 min
- Multi-step commands: compound "X and Y" command parsing (Tier 0.5)
- Arc Reactor health: warning=amber/1.2s, critical=red/0.6s pulse encoding
- Cross-session history: last 6 turns loaded from prior session
- Restart agent: voice command to restart any JARVIS agent
- New endpoints: history.php, metrics.php, suggestions.php, jellyfin.php

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 02:49:05 +00:00
parent bde8909490
commit 9f92e4d5e4
6 changed files with 221 additions and 1 deletions
+73 -1
View File
@@ -771,6 +771,23 @@ body::after{
.thinking-dot:nth-child(3){animation-delay:0.3s}
@keyframes thinkBounce{0%,100%{transform:translateY(0);opacity:0.5}50%{transform:translateY(-6px);opacity:1}}
/* ── ARC REACTOR HEALTH STATES ──────────────────────────────────── */
#arcReactor.health-warning .arc-ring.r3{border-color:rgba(245,166,35,0.8);box-shadow:0 0 8px #f5a623}
#arcReactor.health-warning .arc-ring.r5{border-color:rgba(245,166,35,0.6)}
#arcReactor.health-warning .arc-ring.r7{border-color:rgba(245,166,35,0.5)}
#arcReactor.health-warning .arc-core{
background:radial-gradient(circle,#fff 0%,#f5a623 35%,#c97b00 65%,transparent 100%);
box-shadow:0 0 15px #f5a623,0 0 30px #f5a623,0 0 60px rgba(245,166,35,0.4);
animation:corePulse 1.2s ease-in-out infinite;
}
#arcReactor.health-critical .arc-ring.r3{border-color:rgba(255,34,68,0.9);box-shadow:0 0 10px var(--red)}
#arcReactor.health-critical .arc-ring.r5{border-color:rgba(255,34,68,0.7)}
#arcReactor.health-critical .arc-ring.r7{border-color:rgba(255,34,68,0.6)}
#arcReactor.health-critical .arc-core{
background:radial-gradient(circle,#fff 0%,var(--red) 35%,#8b0000 65%,transparent 100%);
box-shadow:0 0 15px var(--red),0 0 30px var(--red),0 0 60px rgba(255,34,68,0.5);
animation:corePulse 0.6s ease-in-out infinite;
}
/* ── SPEAKING ANIMATION ──────────────────────────────────────────── */
@keyframes speakPulse{
0%,100%{opacity:0.85;transform:translate(-50%,-50%) scale(1);box-shadow:0 0 15px var(--cyan),0 0 30px var(--cyan),0 0 50px rgba(0,212,255,0.3)}
@@ -1655,6 +1672,21 @@ function setAlertState(hasAlerts) {
if (vg) vg.classList.toggle('alert-vignette', hasAlerts);
}
function setSystemHealth(level) {
// level: 'ok' | 'warning' | 'critical'
const reactor = document.getElementById('arcReactor');
if (!reactor) return;
reactor.classList.remove('health-warning', 'health-critical');
if (level === 'warning') reactor.classList.add('health-warning');
if (level === 'critical') reactor.classList.add('health-critical');
// Also update topbar logo dot
const dot = document.querySelector('.tb-logo-dot');
if (dot) {
dot.style.background = level === 'critical' ? 'var(--red)' : level === 'warning' ? '#f5a623' : 'var(--cyan)';
dot.style.boxShadow = level === 'critical' ? '0 0 8px var(--red)' : level === 'warning' ? '0 0 8px #f5a623' : '0 0 8px var(--cyan)';
}
}
// ── FACE TRACKING — reactor follows face position ─────────────────────
let _faceTargetX = 0, _faceTargetY = 0; // normalized -0.5 to 0.5
let _faceCurrX = 0, _faceCurrY = 0;
@@ -2595,7 +2627,9 @@ function showApp(name, greeting, silent = false) {
initMobile();
setTimeout(checkPlannerReminder, 3000);
setInterval(checkUpcomingAppts, 300000);
setTimeout(pollAlertsProactive, 8000); // baseline on load
setTimeout(pollAlertsProactive, 8000);
setTimeout(checkSuggestions, 15000);
setInterval(checkSuggestions, 1800000); // every 30 min // baseline on load
setInterval(pollAlertsProactive, 60000); // poll every 60s
// Guardian Mode — badge refresh + proactive chat
setTimeout(() => {
@@ -3278,12 +3312,15 @@ async function loadAlerts() {
el.innerHTML = '<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--green);text-align:center;margin-top:20px">✓ NO ACTIVE ALERTS</div>';
tb.textContent='NO ALERTS'; tb.className='text-green';
setAlertState(false);
setSystemHealth('ok');
return;
}
tb.textContent=alerts.length+' ALERT'+(alerts.length>1?'S':'');
tb.className='text-red';
setAlertState(true);
const hasCritical = alerts.some(a => a.severity === 'critical');
setSystemHealth(hasCritical ? 'critical' : 'warning');
el.innerHTML = alerts.map(a => {
const ctxKey = 'alert_' + a.id;
@@ -5252,6 +5289,41 @@ document.getElementById('searchModal')?.addEventListener('click', e => {
if (e.target === document.getElementById('searchModal')) closeSearchModal();
});
// ── PROACTIVE SUGGESTIONS ────────────────────────────────────────────────────
const _shownSuggestions = new Set();
async function checkSuggestions() {
const d = await api('suggestions').catch(() => null);
if (!d || !d.suggestions || !d.suggestions.length) return;
for (const s of d.suggestions) {
const key = s.intent + ':' + d.hour + ':' + d.dow;
if (_shownSuggestions.has(key)) continue;
_shownSuggestions.add(key);
// Show as a soft suggestion chip in chat
const log = document.getElementById('chatLog');
const chip = document.createElement('div');
chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0';
chip.innerHTML = `<button onclick="sendSuggestion('${s.intent}',this)" style="background:rgba(0,212,255,0.06);border:1px solid rgba(0,212,255,0.25);border-radius:12px;color:var(--cyan);font-family:var(--font-display);font-size:0.52rem;letter-spacing:1px;padding:4px 12px;cursor:pointer;transition:all 0.2s" onmouseover="this.style.background='rgba(0,212,255,0.12)'" onmouseout="this.style.background='rgba(0,212,255,0.06)'">◈ ${s.prompt}</button>`;
log.appendChild(chip);
log.scrollTop = log.scrollHeight;
break; // show max one suggestion at a time
}
}
function sendSuggestion(intent, btn) {
btn.closest('div').remove();
const prompts = {
'network_scan': 'run a network scan',
'jellyfin_now_playing': 'what is playing on Jellyfin',
'ha_scene': 'what scenes are available',
'planner:briefing': 'daily briefing',
'vm_suggestions': 'VM resource suggestions',
'focus_mode': 'focus mode',
};
const msg = prompts[intent] || intent.replace(/_/g,' ');
document.getElementById('textInput').value = msg;
sendMessage();
}
// ── MOBILE PANEL SWITCHER ─────────────────────────────────────────────────────
function mobSwitch(which) {
if (window.innerWidth > 900) return;