mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
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:
+121
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user