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:
2026-06-17 03:32:48 +00:00
parent b014fd96ab
commit 2c712a4fc6
4 changed files with 180 additions and 4 deletions
+45
View File
@@ -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();
});
}
});