feat: implement 7 JARVIS UI enhancements

#1  Voice waveform: Web Audio API drives wave-bar heights in real time
#2  Ambient dim mode: panels fade to 12% after 90s idle
#6  Streaming AI replies: Groq tokens via SSE; frontend ReadableStream
#7  Quick-note capture: N key / "note: text" saves to kb_facts instantly
#8  Cancel in-flight request: AbortController + CANCEL button
#9  Accent color themes: Stark Blue / Widow Red / Hulk Green, localStorage
#10 Browser push notifications: critical alerts when tab is backgrounded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:39:45 +00:00
parent 58070c7f06
commit 6195f9bd3b
5 changed files with 368 additions and 29 deletions
+121 -1
View File
@@ -8,6 +8,7 @@ if ($method !== 'POST') {
$message = trim($data['message'] ?? '');
$sessionId = $data['session_id'] ?? session_id();
$panelCtx = $data['context'] ?? null; // Panel item selected by user (VM, device, alert, etc.)
$stream = !empty($data['stream']);
if (!$message) {
echo json_encode(['error' => 'Message required']); exit;
@@ -1632,6 +1633,18 @@ if (!$reply) {
}
}
// ── Tier 0.25: Quick-note capture ────────────────────────────────────────────
if (!$reply && preg_match('/^note:\s*(.+)/iu', $message, $nm)) {
$noteText = trim($nm[1]);
$ts = date('Y-m-d H:i');
JarvisDB::query(
"INSERT INTO kb_facts (category, fact, source, confidence) VALUES (?,?,?,?)",
['notes', "[{$ts}] {$noteText}", 'user-note', 1.0]
);
$reply = "Note saved to Memory Core, {$userAddr}: \"{$noteText}\"";
$source = 'intent:quick_note';
}
// ── Tier 0.5: Multi-step command detection ──────────────────────────────────
// Detect "do X and Y" or "X then Y" compound commands (only when no reply yet)
if (!$reply) {
@@ -1670,7 +1683,7 @@ if (!$reply) {
if ($best && $bestS >= 1) {
@file_get_contents(HA_URL.'/api/services/scene/turn_on', false,
stream_context_create(['http'=>['method'=>'POST','timeout'=>4,
'header'=>"Authorization: Bearer ".HA_TOKEN."
'header'=>"Authorization: Bearer ".HA_TOKEN."
Content-Type: application/json",
'content'=>json_encode(['entity_id'=>$best['entity_id']])]]));
$mReply = ($best['attributes']['friendly_name'] ?? $best['entity_id']) . ' activated';
@@ -1817,6 +1830,44 @@ Content-Type: application/json",
break;
}
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 'restart_agent':
// Extract target hostname from message
$msgLow = strtolower($message);
@@ -2132,6 +2183,75 @@ if (!$reply && defined('GROQ_API_KEY') && GROQ_API_KEY) {
$userMsg = $ctxSnippet ? $ctxSnippet . "\n" . $message : $message;
$groqMessages[] = ['role' => 'user', 'content' => $userMsg];
if ($stream) {
// ── Streaming SSE path ──────────────────────────────────────────
while (ob_get_level()) ob_end_clean();
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('X-Accel-Buffering: no');
header('Connection: keep-alive');
ob_implicit_flush(true);
$streamedReply = '';
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode([
'model' => $groqModel,
'messages' => $groqMessages,
'max_tokens' => 400,
'temperature' => 0.7,
'stream' => true,
]),
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . GROQ_API_KEY,
'Content-Type: application/json',
],
CURLOPT_TIMEOUT => GROQ_TIMEOUT,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_WRITEFUNCTION => function($ch, $rawData) use (&$streamedReply) {
$lines = explode("\n", $rawData);
foreach ($lines as $line) {
$line = trim($line);
if ($line === '' || $line === 'data: [DONE]') continue;
if (!str_starts_with($line, 'data: ')) continue;
$ev = json_decode(substr($line, 6), true);
$tok = $ev['choices'][0]['delta']['content'] ?? null;
if ($tok !== null && $tok !== '') {
echo 'data: ' . json_encode(['type' => 'token', 'token' => $tok]) . "\n\n";
flush();
$streamedReply .= $tok;
}
}
return strlen($rawData);
},
]);
curl_exec($ch);
curl_close($ch);
$reply = $streamedReply ? trim($streamedReply) : "Groq AI is temporarily unavailable, {$userAddr}.";
$source = $streamedReply ? 'groq:' . $groqModel : 'fallback';
JarvisDB::insert(
'INSERT INTO conversations (session_id, role, content) VALUES (?,?,?)',
[$sessionId, 'assistant', $reply]
);
KBEngine::learnFromConversation($message, $reply);
echo 'data: ' . json_encode([
'type' => 'complete',
'reply' => $reply,
'source' => $source,
'session_id' => $sessionId,
'ui_action' => $uiAction ?? null,
'arc_job' => null,
'open_network_map' => false,
]) . "\n\n";
flush();
exit;
}
$ch = curl_init('https://api.groq.com/openai/v1/chat/completions');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,