// ── CHAT HISTORY SEARCH ─────────────────────────────────────────────────────── function openSearchModal() { document.getElementById('searchModal').style.display = 'flex'; document.getElementById('searchInput').focus(); } function closeSearchModal() { document.getElementById('searchModal').style.display = 'none'; document.getElementById('searchResults').innerHTML = '
Type to search your JARVIS conversations
'; document.getElementById('searchInput').value = ''; } async function runSearch() { const q = document.getElementById('searchInput').value.trim(); if (!q) return; const el = document.getElementById('searchResults'); el.innerHTML = '
Searching...
'; try { const d = await api('history?q=' + encodeURIComponent(q)); if (!d.results || !d.results.length) { el.innerHTML = '
No results for "' + q + '"
'; return; } el.innerHTML = d.results.map(r => { const role = r.role === 'user' ? '👤' : '🤖'; const ts = new Date(r.created_at).toLocaleString('en-US', {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'}); const snippet = r.content.length > 200 ? r.content.slice(0,197) + '…' : r.content; return `
${role} ${r.role.toUpperCase()} ${ts}
${snippet.replace(/
`; }).join(''); } catch(e) { el.innerHTML = '
Search failed
'; } } document.getElementById('searchModal')?.addEventListener('click', e => { if (e.target === document.getElementById('searchModal')) closeSearchModal(); }); // ── PROACTIVE SUGGESTIONS ──────────────────────────────────────────────────── const _shownSuggestions = new Set(); async function checkSuggestions() { const d = await api('suggestions').catch(() => null); if (!d || !d.suggestions || !d.suggestions.length) return; for (const s of d.suggestions) { const key = s.intent + ':' + d.hour + ':' + d.dow; if (_shownSuggestions.has(key)) continue; _shownSuggestions.add(key); // Show as a soft suggestion chip in chat const log = document.getElementById('chatLog'); const chip = document.createElement('div'); chip.style.cssText = 'display:flex;justify-content:flex-end;margin:4px 0'; chip.innerHTML = ``; log.appendChild(chip); log.scrollTop = log.scrollHeight; break; // show max one suggestion at a time } } function sendSuggestion(intent, btn) { btn.closest('div').remove(); const prompts = { 'network_scan': 'run a network scan', 'jellyfin_now_playing': 'what is playing on Jellyfin', 'ha_scene': 'what scenes are available', 'planner:briefing': 'daily briefing', 'vm_suggestions': 'VM resource suggestions', 'focus_mode': 'focus mode', }; const msg = prompts[intent] || intent.replace(/_/g,' '); document.getElementById('textInput').value = msg; sendMessage(); } // ── MOBILE PANEL SWITCHER ───────────────────────────────────────────────────── function mobSwitch(which) { if (window.innerWidth > 900) return; const panels = {left:'leftPanel', center:'centerPanel', right:'rightPanel'}; Object.entries(panels).forEach(([k, id]) => { document.getElementById(id)?.classList.toggle('mob-active', k === which); }); document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('mob-btn-' + which)?.classList.add('active'); if (which === 'right') loadNews(); } function initMobile() { if (window.innerWidth > 900) return; ['leftPanel','centerPanel','rightPanel'].forEach(id => document.getElementById(id)?.classList.remove('mob-active')); document.getElementById('leftPanel')?.classList.add('mob-active'); document.querySelectorAll('.mob-nav-btn').forEach(b => b.classList.remove('active')); document.getElementById('mob-btn-left')?.classList.add('active'); } window.addEventListener('resize', initMobile); // ── COMMAND PALETTE (Ctrl+K) ────────────────────────────────────────────── const _PALETTE_COMMANDS = [ { label: 'Run a network scan', q: 'run a network scan', group: 'Network' }, { label: 'Show online devices', q: 'who is online on the network', group: 'Network' }, { label: 'Proxmox status', q: 'proxmox status', group: 'Network' }, { label: 'Check agent status', q: 'check all agents', group: 'Agents' }, { label: 'Restart JARVIS agent', q: 'restart jarvis agent', group: 'Agents' }, { label: 'Check VM resources', q: 'VM resource suggestions', group: 'Agents' }, { label: 'Daily briefing', q: 'daily briefing', group: 'Planner' }, { label: 'My tasks today', q: 'my tasks today', group: 'Planner' }, { label: 'My calendar', q: 'my calendar', group: 'Planner' }, { label: "What's playing on Jellyfin", q: 'what is playing on Jellyfin', group: 'Media' }, { label: 'Pause Jellyfin', q: 'pause Jellyfin', group: 'Media' }, { label: 'Next track on Jellyfin', q: 'next track on Jellyfin', group: 'Media' }, { label: 'Stop Jellyfin', q: 'stop Jellyfin', group: 'Media' }, { label: 'List HA scenes', q: 'show home assistant scenes', group: 'Smart Home'}, { label: 'Activate scene…', q: 'activate scene ', group: 'Smart Home'}, { label: 'Focus mode', q: 'focus mode', group: 'UI' }, { label: 'Show all panels', q: 'show all panels', group: 'UI' }, { label: 'Check alerts', q: 'check alerts', group: 'System' }, { label: 'Site health', q: 'site health', group: 'System' }, { label: 'System status', q: 'system status', group: 'System' }, { label: 'Check inbox', q: 'check inbox', group: 'Comms' }, { label: 'Search history…', q: '', group: 'Chat', search: true }, ]; let _paletteOpen = false; function openPalette() { if (_paletteOpen) return; _paletteOpen = true; const ov = document.getElementById('cmdPalette'); if (!ov) return; ov.style.display = 'flex'; const inp = document.getElementById('cmdPaletteInput'); inp.value = ''; renderPaletteItems(''); requestAnimationFrame(() => { ov.classList.add('open'); inp.focus(); }); } function closePalette() { if (!_paletteOpen) return; _paletteOpen = false; const ov = document.getElementById('cmdPalette'); if (!ov) return; ov.classList.remove('open'); setTimeout(() => { ov.style.display = 'none'; }, 180); } function renderPaletteItems(q) { const list = document.getElementById('cmdPaletteList'); if (!list) return; const low = q.toLowerCase().trim(); const filtered = low ? _PALETTE_COMMANDS.filter(c => c.label.toLowerCase().includes(low) || c.group.toLowerCase().includes(low)) : _PALETTE_COMMANDS; let currentGroup = null; list.innerHTML = ''; filtered.forEach((cmd, i) => { if (cmd.group !== currentGroup) { currentGroup = cmd.group; const g = document.createElement('div'); g.className = 'cp-group'; g.textContent = cmd.group; list.appendChild(g); } const row = document.createElement('div'); row.className = 'cp-item' + (i === 0 ? ' cp-active' : ''); row.dataset.q = cmd.q; row.dataset.search = cmd.search ? '1' : ''; const lbl = cmd.label.replace(new RegExp(low.replace(/[.*+?^${}()|[\]\\]/g,'\\$&'), 'gi'), m => `${m}`); row.innerHTML = `${lbl}`; row.addEventListener('click', () => firePaletteItem(row)); list.appendChild(row); }); } function movePaletteSelection(dir) { const items = Array.from(document.querySelectorAll('#cmdPaletteList .cp-item')); if (!items.length) return; const cur = items.findIndex(el => el.classList.contains('cp-active')); const next = (cur + dir + items.length) % items.length; items.forEach(el => el.classList.remove('cp-active')); items[next].classList.add('cp-active'); items[next].scrollIntoView({ block: 'nearest' }); } function firePaletteItem(el) { if (!el) { const active = document.querySelector('#cmdPaletteList .cp-active'); if (!active) return; el = active; } const q = el.dataset.q; const isSearch = el.dataset.search === '1'; closePalette(); if (isSearch) { if (typeof openSearchModal === 'function') openSearchModal(); return; } if (q) { document.getElementById('textInput').value = q; sendMessage(); } } // Keyboard events document.addEventListener('keydown', e => { if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); _paletteOpen ? closePalette() : openPalette(); return; } if (!_paletteOpen) return; if (e.key === 'Escape') { e.preventDefault(); closePalette(); } if (e.key === 'ArrowDown') { e.preventDefault(); movePaletteSelection(1); } if (e.key === 'ArrowUp') { e.preventDefault(); movePaletteSelection(-1); } if (e.key === 'Enter') { e.preventDefault(); firePaletteItem(null); } }); // Filter on type document.getElementById('cmdPaletteInput')?.addEventListener('input', e => { renderPaletteItems(e.target.value); }); // Close on backdrop click 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(); }