mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
feat: voice routing, Jellyfin integration, context-aware briefing
- Voice routing (#2): focus_mode and show_panels KB intents → chat.php → ui_action response field → index.html dispatch; removed local JS regex intercepts - Jellyfin (#3): jellyfin.php endpoint (sessions/library/search/recent), JELLYFIN_URL/API_KEY in config.php, api.php router, now_playing/library KB intents in chat.php - Daily briefing (#4): time-of-day greeting (morning/afternoon/evening), weather lead from api_cache, offline agent count summary Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+72
-2
@@ -775,9 +775,35 @@ if (!$reply) {
|
||||
$dp = array_map(fn($d) => $d['title'] . ' (' . round($d['cur'] / max($d['tgt'],1) * 100) . '%)', $active_dirs);
|
||||
$parts[] = count($active_dirs) . ' active directive' . (count($active_dirs) > 1 ? 's' : '') . ': ' . implode(', ', $dp);
|
||||
}
|
||||
// Time-of-day greeting
|
||||
$hour = (int)date('G'); // 0-23, America/Chicago set in config
|
||||
if ($hour >= 5 && $hour < 12) $greet = "Good morning";
|
||||
elseif ($hour >= 12 && $hour < 17) $greet = "Good afternoon";
|
||||
elseif ($hour >= 17 && $hour < 22) $greet = "Good evening";
|
||||
else $greet = "Good evening";
|
||||
|
||||
// Weather lead
|
||||
$wRow = JarvisDB::query("SELECT data FROM api_cache WHERE cache_key='weather' LIMIT 1");
|
||||
if ($wRow && !empty($wRow[0]['data'])) {
|
||||
$wd = json_decode($wRow[0]['data'], true);
|
||||
$c = $wd['current'] ?? [];
|
||||
if (!empty($c['temp']) && !empty($c['desc'])) {
|
||||
$parts = array_merge(["It's {$c['temp']}°F and {$c['desc']} outside"], $parts);
|
||||
}
|
||||
}
|
||||
|
||||
// Offline agents
|
||||
$offline_agents = JarvisDB::query(
|
||||
"SELECT hostname FROM registered_agents WHERE status='offline' AND last_seen > DATE_SUB(NOW(), INTERVAL 7 DAY)"
|
||||
) ?? [];
|
||||
if ($offline_agents) {
|
||||
$names = implode(', ', array_column($offline_agents, 'hostname'));
|
||||
$parts[] = count($offline_agents) . ' agent' . (count($offline_agents) > 1 ? 's' : '') . ' offline: ' . $names;
|
||||
}
|
||||
|
||||
$reply = $parts
|
||||
? "Good morning, {$userAddr}. " . implode('. ', $parts) . '.'
|
||||
: "Good morning, {$userAddr}. Your schedule is clear — no tasks, appointments, or email actions pending today.";
|
||||
? "{$greet}, {$userAddr}. " . implode('. ', $parts) . '.'
|
||||
: "{$greet}, {$userAddr}. Your schedule is clear — all systems nominal, no tasks or appointments pending.";
|
||||
$source = 'planner:briefing';
|
||||
}
|
||||
|
||||
@@ -1607,6 +1633,18 @@ if (!$reply) {
|
||||
if ($matched && $matched['action'] === 'action') {
|
||||
switch ($matched['intent']) {
|
||||
|
||||
case 'focus_mode':
|
||||
$uiAction = 'focus_mode';
|
||||
$reply = "Focus mode activated, {$userAddr}. Side panels hidden.";
|
||||
$source = 'intent:focus_mode';
|
||||
break;
|
||||
|
||||
case 'show_panels':
|
||||
$uiAction = 'show_panels';
|
||||
$reply = "Full view restored, {$userAddr}. All panels visible.";
|
||||
$source = 'intent:show_panels';
|
||||
break;
|
||||
|
||||
case 'network_scan':
|
||||
$online = JarvisDB::single(
|
||||
"SELECT COUNT(*) cnt FROM network_devices WHERE status='online' AND last_seen > DATE_SUB(NOW(), INTERVAL 15 MINUTE)"
|
||||
@@ -1632,6 +1670,36 @@ if (!$reply) {
|
||||
$source = 'intent:network_scan';
|
||||
break;
|
||||
|
||||
case 'jellyfin_now_playing':
|
||||
$jSessions = @json_decode(@file_get_contents(
|
||||
JELLYFIN_URL . '/Sessions?api_key=' . JELLYFIN_API_KEY, false,
|
||||
stream_context_create(['http' => ['timeout' => 4]])
|
||||
), true) ?? [];
|
||||
$active = array_filter($jSessions, fn($s) => isset($s['NowPlayingItem']));
|
||||
if (!$active) {
|
||||
$reply = "Nothing is currently playing on Jellyfin, {$userAddr}.";
|
||||
} else {
|
||||
$lines = [];
|
||||
foreach ($active as $s) {
|
||||
$np = $s['NowPlayingItem'];
|
||||
$title = $np['SeriesName'] ? $np['SeriesName'] . ' — ' . $np['Name'] : $np['Name'];
|
||||
$paused = $s['PlayState']['IsPaused'] ? ' (paused)' : '';
|
||||
$lines[] = "{$s['UserName']} is watching {$title}{$paused} on {$s['DeviceName']}";
|
||||
}
|
||||
$reply = implode('. ', $lines) . '.';
|
||||
}
|
||||
$source = 'intent:jellyfin';
|
||||
break;
|
||||
|
||||
case 'jellyfin_library':
|
||||
$jMovies = @json_decode(@file_get_contents(JELLYFIN_URL . '/Items?IncludeItemTypes=Movie&Recursive=true&Limit=0&api_key=' . JELLYFIN_API_KEY, false, stream_context_create(['http' => ['timeout' => 4]])), true);
|
||||
$jSeries = @json_decode(@file_get_contents(JELLYFIN_URL . '/Items?IncludeItemTypes=Series&Recursive=true&Limit=0&api_key=' . JELLYFIN_API_KEY, false, stream_context_create(['http' => ['timeout' => 4]])), true);
|
||||
$movies = $jMovies['TotalRecordCount'] ?? 0;
|
||||
$series = $jSeries['TotalRecordCount'] ?? 0;
|
||||
$reply = "Jellyfin library: {$movies} movies and {$series} TV series, {$userAddr}.";
|
||||
$source = 'intent:jellyfin';
|
||||
break;
|
||||
|
||||
case 'alerts_show':
|
||||
$activeAlerts = JarvisDB::query(
|
||||
"SELECT title, severity, message FROM alerts WHERE resolved=0 ORDER BY created_at DESC LIMIT 10"
|
||||
@@ -1952,6 +2020,7 @@ if ($reply && !in_array(explode(':', $source)[0], ['intent', 'kb', 'fallback', '
|
||||
memoryExtractAsync($message, $reply, $sessionId);
|
||||
}
|
||||
|
||||
$uiAction = $uiAction ?? null;
|
||||
echo json_encode([
|
||||
'reply' => $reply,
|
||||
'source' => $source,
|
||||
@@ -1959,4 +2028,5 @@ echo json_encode([
|
||||
'timestamp' => date('c'),
|
||||
'arc_job' => $arcJobId,
|
||||
'open_network_map' => ($source === 'intent:network_scan'),
|
||||
'ui_action' => $uiAction,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../config.php';
|
||||
require_once __DIR__ . '/../../includes/auth.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
AuthMiddleware::requireAuth();
|
||||
|
||||
$action = $_GET['action'] ?? 'sessions';
|
||||
|
||||
function jf_get(string $path): array {
|
||||
$url = JELLYFIN_URL . $path . (str_contains($path, '?') ? '&' : '?') . 'api_key=' . JELLYFIN_API_KEY;
|
||||
$ctx = stream_context_create(['http' => ['timeout' => 5, 'ignore_errors' => true]]);
|
||||
$raw = @file_get_contents($url, false, $ctx);
|
||||
return $raw ? (json_decode($raw, true) ?? []) : [];
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'sessions':
|
||||
$sessions = jf_get('/Sessions');
|
||||
$active = array_filter($sessions, fn($s) => isset($s['NowPlayingItem']));
|
||||
$out = [];
|
||||
foreach ($active as $s) {
|
||||
$np = $s['NowPlayingItem'];
|
||||
$pos = $s['PlayState']['PositionTicks'] ?? 0;
|
||||
$dur = $np['RunTimeTicks'] ?? 0;
|
||||
$out[] = [
|
||||
'session_id' => $s['Id'],
|
||||
'user' => $s['UserName'] ?? 'Unknown',
|
||||
'device' => $s['DeviceName'] ?? '',
|
||||
'client' => $s['Client'] ?? '',
|
||||
'title' => $np['Name'] ?? '',
|
||||
'type' => $np['Type'] ?? '',
|
||||
'series' => $np['SeriesName'] ?? null,
|
||||
'year' => $np['ProductionYear'] ?? null,
|
||||
'paused' => $s['PlayState']['IsPaused'] ?? false,
|
||||
'position_pct'=> $dur > 0 ? round($pos / $dur * 100) : 0,
|
||||
];
|
||||
}
|
||||
echo json_encode(['sessions' => array_values($out), 'total_active' => count($out)]);
|
||||
break;
|
||||
|
||||
case 'library':
|
||||
$movies = jf_get('/Items?IncludeItemTypes=Movie&Recursive=true&Limit=0');
|
||||
$series = jf_get('/Items?IncludeItemTypes=Series&Recursive=true&Limit=0');
|
||||
$episodes= jf_get('/Items?IncludeItemTypes=Episode&Recursive=true&Limit=0');
|
||||
echo json_encode([
|
||||
'movies' => $movies['TotalRecordCount'] ?? 0,
|
||||
'series' => $series['TotalRecordCount'] ?? 0,
|
||||
'episodes' => $episodes['TotalRecordCount'] ?? 0,
|
||||
]);
|
||||
break;
|
||||
|
||||
case 'search':
|
||||
$q = trim($_GET['q'] ?? '');
|
||||
if (!$q) { echo json_encode(['results' => []]); break; }
|
||||
$data = jf_get('/Search/Hints?SearchTerm=' . urlencode($q) . '&Limit=8&IncludeItemTypes=Movie,Series,Episode');
|
||||
$hints = $data['SearchHints'] ?? [];
|
||||
$results = array_map(fn($h) => [
|
||||
'id' => $h['ItemId'],
|
||||
'name' => $h['Name'],
|
||||
'type' => $h['Type'],
|
||||
'year' => $h['ProductionYear'] ?? null,
|
||||
'series'=> $h['Series'] ?? null,
|
||||
], $hints);
|
||||
echo json_encode(['results' => $results]);
|
||||
break;
|
||||
|
||||
case 'recent':
|
||||
$data = jf_get('/Items/Latest?Limit=8&IncludeItemTypes=Movie,Episode&Fields=Overview');
|
||||
$out = array_map(fn($i) => [
|
||||
'name' => $i['Name'],
|
||||
'type' => $i['Type'],
|
||||
'series' => $i['SeriesName'] ?? null,
|
||||
'year' => $i['ProductionYear'] ?? null,
|
||||
], is_array($data) ? $data : []);
|
||||
echo json_encode(['recent' => $out]);
|
||||
break;
|
||||
|
||||
default:
|
||||
echo json_encode(['error' => 'Unknown action']);
|
||||
}
|
||||
@@ -71,6 +71,7 @@ $endpoints = [
|
||||
'sites' => 'sites.php',
|
||||
'agent' => 'agent.php',
|
||||
'planner' => 'planner.php',
|
||||
'jellyfin' => 'jellyfin.php',
|
||||
'arc' => 'arc.php',
|
||||
'directives' => 'directives.php',
|
||||
'memory' => 'memory.php',
|
||||
|
||||
+2
-19
@@ -3381,25 +3381,6 @@ async function sendMessage() {
|
||||
else addMessage('jarvis','Network map is not currently active.');
|
||||
return;
|
||||
}
|
||||
// Local panel-toggle voice commands (handled without API call)
|
||||
const t = text.toLowerCase();
|
||||
if (/\b(focus\s*mode|hide\s*(panels?|stats?|statistics)|full\s*screen\s*jarvis)\b/.test(t)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
if (panelsVisible) togglePanels(true);
|
||||
addMessage('jarvis', 'Focus mode activated, Sir. Side panels hidden.');
|
||||
speak('Focus mode activated, Sir. Side panels hidden.');
|
||||
return;
|
||||
}
|
||||
if (/\b(show\s*(panels?|stats?|statistics|full\s*view)|full\s*(view|mode)|restore\s*panels?)\b/.test(t)) {
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
if (!panelsVisible) togglePanels(true);
|
||||
addMessage('jarvis', 'Full view restored, Sir. All panels visible.');
|
||||
speak('Full view restored, Sir. All panels visible.');
|
||||
return;
|
||||
}
|
||||
|
||||
input.value = '';
|
||||
addMessage('user', text);
|
||||
showThinking();
|
||||
@@ -3419,6 +3400,8 @@ async function sendMessage() {
|
||||
speak(data.reply);
|
||||
}
|
||||
if (data.open_network_map) { openNetMap(); }
|
||||
if (data.ui_action === 'focus_mode') { if (panelsVisible) togglePanels(true); }
|
||||
if (data.ui_action === 'show_panels') { if (!panelsVisible) togglePanels(true); }
|
||||
if (data.arc_job) { onArcJobStarted(data.arc_job, data.source || ''); }
|
||||
} catch(e) {
|
||||
const bubble = document.getElementById('thinking-bubble');
|
||||
|
||||
Reference in New Issue
Block a user