mirror of
https://github.com/myronblair/jarvis
synced 2026-06-30 17:50:23 -05:00
Add live voice transcript, keyboard shortcuts, agent topology map
#3 Live Voice Transcript: Real-time subtitle bar at bottom of screen shows what JARVIS hears as you speak. Interim results appear word-by-word via SpeechRecognition.onresult interim events; bar fades 3.2s after final result. #4 Keyboard Shortcuts: Global keydown handler (skips input fields): F5=refresh all, Esc=close modals/overlays, M=mute mic toggle, Space=focus chat input, 1/2/3/4=switch HOME/ALERTS/NEWS/AGENTS tabs. Shortcut hints added to Ctrl+K palette footer. #5 Agent Topology Map: TOPOLOGY button in AGENTS tab switches from card view to animated ring-based canvas showing all agents by type (Proxmox=green inner ring, HA=yellow mid ring, Linux/Windows=blue outer ring). Live particles flow hub→agents; offline nodes shown in red. Reads from rendered agent cards. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1216,6 +1216,11 @@ function initVoice() {
|
||||
|
||||
recognition.onresult = (e) => {
|
||||
if (isSpeaking) return;
|
||||
let interimText = '';
|
||||
for (let ri = e.resultIndex; ri < e.results.length; ri++) {
|
||||
if (!e.results[ri].isFinal) interimText += e.results[ri][0].transcript;
|
||||
}
|
||||
if (interimText && voiceMode && !voiceMuted) _showInterimTranscript(interimText);
|
||||
const transcript = (e.results[0][0].transcript || '').trim();
|
||||
if (!transcript) return;
|
||||
const lc = transcript.toLowerCase();
|
||||
@@ -1269,9 +1274,23 @@ function initVoice() {
|
||||
};
|
||||
}
|
||||
|
||||
let _transcriptTimer = null;
|
||||
function _showTranscript(text) {
|
||||
const el = document.getElementById('textInput');
|
||||
if (el) { el.placeholder = '▶ ' + text.substring(0, 60); setTimeout(() => { el.placeholder = 'Enter command or speak to JARVIS...'; }, 3000); }
|
||||
const bar = document.getElementById('voiceTranscriptBar');
|
||||
if (!bar) return;
|
||||
bar.textContent = text;
|
||||
bar.classList.add('vt-active');
|
||||
if (_transcriptTimer) clearTimeout(_transcriptTimer);
|
||||
_transcriptTimer = setTimeout(() => { bar.classList.remove('vt-active'); bar.textContent = ''; }, 3200);
|
||||
}
|
||||
function _showInterimTranscript(text) {
|
||||
const bar = document.getElementById('voiceTranscriptBar');
|
||||
if (!bar || !text) return;
|
||||
bar.textContent = text + '…';
|
||||
bar.classList.add('vt-active');
|
||||
if (_transcriptTimer) clearTimeout(_transcriptTimer);
|
||||
}
|
||||
|
||||
function enterVoiceMode(source) {
|
||||
@@ -1527,3 +1546,29 @@ async function triggerMorningBriefing() {
|
||||
speak(msg);
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
// ── KEYBOARD SHORTCUTS ───────────────────────────────────────────────────────────────
|
||||
document.addEventListener('keydown', function(e) {
|
||||
const tag = (document.activeElement?.tagName || '').toLowerCase();
|
||||
const inInput = tag === 'input' || tag === 'textarea' || document.activeElement?.isContentEditable;
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'k') return; // handled by palette
|
||||
if (e.key === 'Escape') {
|
||||
['sitesModal','agentModal','searchModal'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el && (el.style.display === 'flex' || el.style.display === 'block')) el.style.display = 'none';
|
||||
});
|
||||
if (document.getElementById('netMapOverlay')?.classList.contains('nm-open')) closeNetMap();
|
||||
return;
|
||||
}
|
||||
if (inInput) return;
|
||||
if (e.key === 'F5') { e.preventDefault(); refreshAll(); return; }
|
||||
if (e.key === 'm' || e.key === 'M') { toggleVoice(); return; }
|
||||
if (e.key === ' ') { e.preventDefault(); document.getElementById('textInput')?.focus(); return; }
|
||||
const tabMap = {'1':'ha','2':'alerts','3':'news','4':'agents'};
|
||||
if (tabMap[e.key]) {
|
||||
document.querySelectorAll('.tab').forEach(t => {
|
||||
const oc = t.getAttribute('onclick') || '';
|
||||
if (oc.includes("'" + tabMap[e.key] + "'")) t.click();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1543,3 +1543,120 @@ document.getElementById('cmdPaletteInput')?.addEventListener('input', e => {
|
||||
document.getElementById('cmdPalette')?.addEventListener('click', e => {
|
||||
if (e.target.id === 'cmdPalette') closePalette();
|
||||
});
|
||||
|
||||
// ── AGENT TOPOLOGY MAP ─────────────────────────────────────────────────────────────
|
||||
let _agentTopoMode = false, _agentTopoRaf = null, _agentTopoData = [];
|
||||
|
||||
function toggleAgentTopo() {
|
||||
_agentTopoMode = !_agentTopoMode;
|
||||
const btn = document.getElementById('agent-topo-btn');
|
||||
const list = document.getElementById('agents-list');
|
||||
const cvs = document.getElementById('agentTopoCanvas');
|
||||
if (!btn || !list || !cvs) return;
|
||||
btn.classList.toggle('active', _agentTopoMode);
|
||||
if (_agentTopoMode) {
|
||||
list.style.display = 'none'; cvs.style.display = 'block';
|
||||
_buildAgentTopoData(); _drawAgentTopo();
|
||||
} else {
|
||||
list.style.display = 'block'; cvs.style.display = 'none';
|
||||
if (_agentTopoRaf) { cancelAnimationFrame(_agentTopoRaf); _agentTopoRaf = null; }
|
||||
}
|
||||
}
|
||||
|
||||
function _buildAgentTopoData() {
|
||||
// Build node list from rendered agent cards
|
||||
_agentTopoData = [{id:'jarvis',label:'JARVIS',online:true,type:'hub'}];
|
||||
document.querySelectorAll('.agent-card').forEach(el => {
|
||||
const nameEl = el.querySelector('.agent-name, [class*="name"]');
|
||||
if (!nameEl) return;
|
||||
const name = nameEl.textContent.trim();
|
||||
const online = el.classList.contains('online') || !!el.querySelector('.agent-dot.online, .dot.online');
|
||||
const lname = name.toLowerCase();
|
||||
let type = 'linux';
|
||||
if (lname.includes('pve') || lname.includes('proxmox') || el.querySelector('[class*="proxmox"]')) type = 'proxmox';
|
||||
else if (lname.includes('ha') || lname.includes('homeassist')) type = 'homeassistant';
|
||||
else if (lname.includes('windows') || lname.includes('mini')) type = 'windows';
|
||||
_agentTopoData.push({id:name, label:name.substring(0,12), online, type});
|
||||
});
|
||||
// Fallback: use last known registered agent list if cards not rendered
|
||||
if (_agentTopoData.length <= 1 && typeof _lastAgents !== 'undefined') {
|
||||
(_lastAgents || []).forEach(a => {
|
||||
_agentTopoData.push({id:a.agent_id,label:(a.hostname||a.agent_id).substring(0,12),online:a.status==='online',type:a.agent_type||'linux'});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _drawAgentTopo() {
|
||||
const cvs = document.getElementById('agentTopoCanvas');
|
||||
if (!cvs || !_agentTopoMode) return;
|
||||
const ctx = cvs.getContext('2d');
|
||||
const rect = cvs.getBoundingClientRect();
|
||||
const W = rect.width || 280, H = rect.height || 260;
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
cvs.width = W * dpr; cvs.height = H * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
const typeRing = {hub:0, proxmox:0.28, homeassistant:0.48, linux:0.68, windows:0.68};
|
||||
const typeColor = {hub:'0,212,255', proxmox:'0,255,136', homeassistant:'255,215,0', linux:'0,190,255', windows:'180,120,255'};
|
||||
// Assign positions
|
||||
const byType = {};
|
||||
_agentTopoData.slice(1).forEach(n => { (byType[n.type]=byType[n.type]||[]).push(n); });
|
||||
_agentTopoData[0].x = W/2; _agentTopoData[0].y = H/2;
|
||||
Object.entries(byType).forEach(([tp, nodes]) => {
|
||||
const rf = typeRing[tp] || 0.68;
|
||||
const r = Math.min(W, H) / 2 * rf;
|
||||
nodes.forEach((n, i) => {
|
||||
const a = -Math.PI/2 + (i / nodes.length) * Math.PI * 2;
|
||||
n.x = W/2 + Math.cos(a)*r; n.y = H/2 + Math.sin(a)*r;
|
||||
});
|
||||
});
|
||||
let t = 0;
|
||||
function frame() {
|
||||
if (!_agentTopoMode) return;
|
||||
t += 0.007; ctx.clearRect(0, 0, W, H);
|
||||
// Orbit rings
|
||||
[0.28, 0.48, 0.68].forEach(rf => {
|
||||
ctx.beginPath(); ctx.arc(W/2, H/2, Math.min(W,H)/2*rf, 0, Math.PI*2);
|
||||
ctx.strokeStyle = 'rgba(0,212,255,0.05)'; ctx.lineWidth = 0.5; ctx.stroke();
|
||||
});
|
||||
// Edges
|
||||
_agentTopoData.slice(1).forEach(n => {
|
||||
if (!n.x) return;
|
||||
const col = typeColor[n.type] || '0,190,255';
|
||||
ctx.beginPath(); ctx.moveTo(W/2, H/2); ctx.lineTo(n.x, n.y);
|
||||
ctx.strokeStyle = n.online ? 'rgba('+col+',0.18)' : 'rgba(255,50,80,0.08)';
|
||||
ctx.lineWidth = n.online ? 1 : 0.5; ctx.stroke();
|
||||
});
|
||||
// Particles
|
||||
_agentTopoData.slice(1).filter(n=>n.online&&n.x).forEach((n,i) => {
|
||||
const p = ((t*0.35+i*0.41)%1);
|
||||
const col = typeColor[n.type]||'0,190,255';
|
||||
const px = W/2+(n.x-W/2)*p, py = H/2+(n.y-H/2)*p;
|
||||
ctx.beginPath(); ctx.arc(px,py,1.4,0,Math.PI*2);
|
||||
ctx.fillStyle='rgba('+col+',0.75)'; ctx.fill();
|
||||
});
|
||||
// Nodes
|
||||
_agentTopoData.forEach((n,i) => {
|
||||
if (!n.x) return;
|
||||
const col = typeColor[n.type]||'0,190,255';
|
||||
const nr = n.type==='hub' ? 13 : 7;
|
||||
const pulse = Math.sin(t+i*0.9)*0.25+0.75;
|
||||
if (n.online||n.type==='hub') {
|
||||
const g = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,nr*3.5);
|
||||
g.addColorStop(0,'rgba('+col+','+(0.15*pulse)+')');
|
||||
g.addColorStop(1,'transparent');
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,nr*3.5,0,Math.PI*2);
|
||||
ctx.fillStyle=g; ctx.fill();
|
||||
}
|
||||
ctx.beginPath(); ctx.arc(n.x,n.y,nr,0,Math.PI*2);
|
||||
ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.9)' : 'rgba(255,50,80,0.5)';
|
||||
ctx.fill();
|
||||
ctx.strokeStyle='rgba('+col+',0.6)'; ctx.lineWidth=1; ctx.stroke();
|
||||
ctx.fillStyle = n.online||n.type==='hub' ? 'rgba('+col+',0.85)' : 'rgba(255,80,80,0.7)';
|
||||
ctx.font = (n.type==='hub'?'600 8px':'6px')+' "Share Tech Mono",monospace';
|
||||
ctx.textAlign='center';
|
||||
ctx.fillText(n.label, n.x, n.y+nr+9);
|
||||
});
|
||||
_agentTopoRaf = requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user