diff --git a/public_html/assets/css/jarvis.css b/public_html/assets/css/jarvis.css index 3a3065b..a20e9a5 100644 --- a/public_html/assets/css/jarvis.css +++ b/public_html/assets/css/jarvis.css @@ -1162,3 +1162,9 @@ body::after{ color:rgba(0,212,255,0.25);padding:8px 20px;border-top:1px solid rgba(0,212,255,0.1); display:flex;gap:16px } + +/* ── VOICE TRANSCRIPT BAR ────────────────────────────────────────── */ +#voiceTranscriptBar.vt-active{opacity:1} +/* ── AGENT TOPOLOGY ────────────────────────────────────────────────── */ +#agentTopoCanvas{background:transparent;border-top:1px solid rgba(0,212,255,0.08);display:block} +#agent-topo-btn.active{background:rgba(0,212,255,0.15);border-color:rgba(0,212,255,0.5)} diff --git a/public_html/assets/js/jarvis-app.js b/public_html/assets/js/jarvis-app.js index db444ca..f6f3092 100644 --- a/public_html/assets/js/jarvis-app.js +++ b/public_html/assets/js/jarvis-app.js @@ -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(); + }); + } +}); diff --git a/public_html/assets/js/jarvis-protocols.js b/public_html/assets/js/jarvis-protocols.js index 28ce31f..72099d1 100644 --- a/public_html/assets/js/jarvis-protocols.js +++ b/public_html/assets/js/jarvis-protocols.js @@ -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(); +} diff --git a/public_html/index.html b/public_html/index.html index 2364811..55a692f 100644 --- a/public_html/index.html +++ b/public_html/index.html @@ -243,8 +243,12 @@
-
-
+
+
+ +
+
+
@@ -271,7 +275,10 @@
- + +
+ +
@@ -345,7 +352,8 @@
- ↑↓ navigate↵ executeESC closeCTRL+K toggle + ↑↓ navigate↵ executeESC close + 1-4 tabs · M mute · F5 refresh · Space→input