diff --git a/api/endpoints/chat.php b/api/endpoints/chat.php index 4d85852..99bb674 100644 --- a/api/endpoints/chat.php +++ b/api/endpoints/chat.php @@ -72,13 +72,27 @@ JarvisDB::insert( [$sessionId, 'user', $message] ); -// Conversation history +// Conversation history — current session $history = JarvisDB::query( 'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 10', [$sessionId] -); +) ?? []; $history = array_reverse($history); +// Cross-session memory: last 6 turns from the most recent prior session +$priorSession = JarvisDB::query( + "SELECT session_id FROM conversations WHERE session_id != ? AND role='user' + ORDER BY created_at DESC LIMIT 1", + [$sessionId] +); +if ($priorSession && !empty($priorSession[0]['session_id'])) { + $priorHistory = JarvisDB::query( + 'SELECT role, content FROM conversations WHERE session_id=? ORDER BY created_at DESC LIMIT 6', + [$priorSession[0]['session_id']] + ) ?? []; + $history = array_merge(array_reverse($priorHistory), $history); +} + $reply = null; $source = 'unknown'; @@ -1633,6 +1647,44 @@ if (!$reply) { if ($matched && $matched['action'] === 'action') { switch ($matched['intent']) { + case 'restart_agent': + // Extract target hostname from message + $msgLow = strtolower($message); + $agentMap = [ + 'homebridge' => 'homebridge_b57cbaea', + 'jellyfin' => 'jellyfin_7e386833', + 'networkbackup' => 'networkbackup_NetworkB', + 'network backup'=> 'networkbackup_NetworkB', + 'novacpx' => 'novacpx_e3b07264', + 'nova' => 'novacpx_e3b07264', + 'mediastack' => 'MediaStack_2c00b1b8', + 'media stack' => 'MediaStack_2c00b1b8', + 'homeassistant' => 'homeassistant_ha', + 'home assistant'=> 'homeassistant_ha', + ]; + $targetAgentId = null; + $targetName = null; + foreach ($agentMap as $keyword => $agentId) { + if (str_contains($msgLow, $keyword)) { + $targetAgentId = $agentId; + $targetName = ucfirst($keyword); + break; + } + } + if ($targetAgentId) { + JarvisDB::insert( + "INSERT INTO agent_commands (agent_id, command_type, command_data, status, created_at) + VALUES (?, 'restart_service', ?, 'pending', NOW())", + [$targetAgentId, json_encode(['service' => 'jarvis-agent'])] + ); + $reply = "Restart command sent to the {$targetName} agent, {$userAddr}. It should come back online within 15 seconds."; + } else { + // Fall back to listing restartable agents + $reply = "Which agent should I restart, {$userAddr}? I can restart: HomeAssistant, Homebridge, Jellyfin, MediaStack, NetworkBackup, or NovaCPX."; + } + $source = 'intent:restart_agent'; + break; + case 'focus_mode': $uiAction = 'focus_mode'; $reply = "Focus mode activated, {$userAddr}. Side panels hidden."; diff --git a/public_html/index.html b/public_html/index.html index 9e627b8..e2c94f3 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -2584,6 +2584,8 @@ function showApp(name, greeting, silent = false) { loadWeather(); loadNews(); initMobile(); + setTimeout(checkPlannerReminder, 3000); + setInterval(checkUpcomingAppts, 300000); // Guardian Mode — badge refresh + proactive chat setTimeout(() => { _refreshGuardianBadge(); @@ -3157,6 +3159,52 @@ async function toggleHA(entityId, domain, currentState) { } catch(e) {} } +// ── PROACTIVE REMINDERS ────────────────────────────────────────────────────── +let _reminderShown = false; +async function checkPlannerReminder() { + if (_reminderShown || sessionStorage.getItem('reminderShown')) return; + _reminderShown = true; + sessionStorage.setItem('reminderShown', '1'); + const d = await api('planner/today').catch(() => null); + if (!d) return; + const tasks = [...(d.tasks_overdue||[]), ...(d.tasks_today||[])]; + const appts = d.appts_today || []; + const overdue = d.tasks_overdue?.length || 0; + if (!tasks.length && !appts.length) return; + + const parts = []; + if (overdue) parts.push(overdue + ' overdue task' + (overdue > 1 ? 's' : '')); + if (tasks.length - overdue > 0) parts.push((tasks.length - overdue) + ' task' + (tasks.length - overdue > 1 ? 's' : '') + ' due today'); + if (appts.length) { + const nextAppt = appts[0]; + const t = nextAppt.start_at ? new Date(nextAppt.start_at).toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true}) : ''; + parts.push((t ? 'appointment at ' + t : appts.length + ' appointment' + (appts.length > 1 ? 's' : '') + ' today')); + } + + const msg = 'Heads up, ' + (sessionUser||'Sir') + '. You have ' + parts.join(' and ') + '.'; + addMessage('jarvis', msg); + if (typeof speak === 'function' && isVoiceActive) speak(msg); +} + +// Check for upcoming appointments (fires every 5 min after load) +let _apptAlerted = new Set(); +async function checkUpcomingAppts() { + const d = await api('planner/today').catch(() => null); + if (!d) return; + const now = Date.now(); + for (const a of (d.appts_today||[])) { + if (!a.start_at || _apptAlerted.has(a.id)) continue; + const start = new Date(a.start_at).getTime(); + const minsUntil = (start - now) / 60000; + if (minsUntil > 0 && minsUntil <= 15) { + _apptAlerted.add(a.id); + const msg = 'Reminder: ' + a.title + ' starts in ' + Math.round(minsUntil) + ' minutes' + (a.location ? ' at ' + a.location : '') + '.'; + addMessage('jarvis', msg); + if (typeof speak === 'function' && isVoiceActive) speak(msg); + } + } +} + // ── PLANNER SUMMARY (top bar badge only) ───────────────────────────────── async function loadPlannerSummary() { const d = await api('planner/today');