/** * dashboard.js — Shell global da Central de Controle do Carinha * Injeta barra lateral unificada, telemetria em tempo real no topo, * controle de áudio robótico e dock de comandos rápidos. */ (() => { if (window.location.search.includes('clean=1')) { document.documentElement.classList.add('is-clean-embed'); return; } // Carrega dinamicamente o sintetizador de áudio caso não esteja presente if (!window.RobotAudio) { const audioScript = document.createElement('script'); audioScript.src = '/audio-robot.js'; audioScript.defer = true; document.head.appendChild(audioScript); } const sections = [ { title: 'Principal', items: [ ['/dashboard', 'Visão geral', 'M3 3h7v7H3z M14 3h7v7h-7z M3 14h7v7H3z M14 14h7v7h-7z'], ['/painel', 'Câmera e rosto', 'M3 6h12v12H3z M15 10l6-4v12l-6-4'] ] }, { title: 'Inteligência & IA', items: [ ['/conversa', 'Conversar com IA', 'M3 4h18v13H9l-6 4z'], ['/emojis', 'Emojis & Reações', 'M12 3a9 9 0 1 0 0 18a9 9 0 1 0 0-18 M8 9h1 M15 9h1 M7 14q5 5 10 0'], ['/treinamento', 'Treinamento', 'M3 8l9-5 9 5-9 5z M6 11v6q6 5 12 0v-6'] ] }, { title: 'Robótica & Hardware', items: [ ['/braco', 'Braço robótico', 'M3 21h18 M8 21v-5l7-5-3-6 M9 4h6v4H9z M14 10h4v4h-4z'], ['/servos', 'Servos PCA9685', 'M5 3v18 M12 3v18 M19 3v18 M2 8h6 M9 16h6 M16 7h6'] ] }, { title: 'Configurações', items: [ ['/api', 'API Gemini & Modelo', 'M8 5L2 12l6 7 M16 5l6 7-6 7 M14 3l-4 18'] ] } ]; const pathname = location.pathname === '/' ? '/dashboard' : location.pathname; document.body.classList.add('dashboard'); const frame = document.createElement('div'); frame.className = 'app-frame'; const main = document.createElement('main'); main.className = 'app-content'; main.id = 'appContent'; // Move elementos existentes do body para o container principal for (const child of [...document.body.childNodes]) { if (!(child.nodeType === 1 && ['SCRIPT', 'STYLE', 'LINK'].includes(child.tagName))) { main.append(child); } } // Sidebar moderna e estilizada const sidebar = document.createElement('aside'); sidebar.className = 'app-sidebar'; sidebar.innerHTML = ` Carinha IA `; const nav = sidebar.querySelector('#sidebarNav'); for (const sec of sections) { const title = document.createElement('div'); title.className = 'nav-section-title'; title.textContent = sec.title; nav.append(title); for (const [url, label, path] of sec.items) { const a = document.createElement('a'); a.href = url; a.innerHTML = `${label}`; if (url === pathname) { a.setAttribute('aria-current', 'page'); } a.addEventListener('click', () => { if (window.RobotAudio) window.RobotAudio.click(); }); nav.append(a); } } // Barra de ferramentas superior (Topbar) const top = document.createElement('header'); top.className = 'app-topbar'; top.innerHTML = `
Central de Controle
Verificando celular…
`; // Mini-Monitor Flutuante (Picture-in-Picture) const pipWidget = document.createElement('div'); pipWidget.id = 'floatingPipWidget'; pipWidget.className = 'floating-pip-widget'; pipWidget.style.display = 'none'; pipWidget.innerHTML = `
Carinha Monitor
`; document.body.appendChild(pipWidget); const pipToggleBtn = top.querySelector('#pipToggleBtn'); const pipMinBtn = pipWidget.querySelector('#pipMinimizeBtn'); const pipCloseBtn = pipWidget.querySelector('#pipCloseBtn'); function togglePip(force) { const isVisible = pipWidget.style.display === 'flex'; const next = force !== undefined ? force : !isVisible; pipWidget.style.display = next ? 'flex' : 'none'; pipToggleBtn.style.color = next ? 'var(--ciano)' : 'var(--texto)'; pipToggleBtn.style.borderColor = next ? 'var(--ciano)' : 'var(--borda)'; localStorage.setItem('carinha_pip', next ? 'true' : 'false'); if (next && window.RobotAudio) window.RobotAudio.click(); } pipToggleBtn.onclick = () => togglePip(); pipCloseBtn.onclick = () => togglePip(false); pipMinBtn.onclick = () => { const isMin = pipWidget.classList.toggle('minimized'); pipMinBtn.textContent = isMin ? '□' : '_'; }; // Se o usuário já deixou o PiP ativo anteriormente if (localStorage.getItem('carinha_pip') === 'true') { pipWidget.style.display = 'flex'; pipToggleBtn.style.color = 'var(--ciano)'; pipToggleBtn.style.borderColor = 'var(--ciano)'; } // Menu móvel const menuBtn = top.querySelector('.menu-button'); menuBtn.onclick = () => { const open = document.body.classList.toggle('menu-open'); menuBtn.setAttribute('aria-expanded', String(open)); }; document.addEventListener('keydown', e => { if (e.key === 'Escape') { document.body.classList.remove('menu-open'); menuBtn.setAttribute('aria-expanded', 'false'); fecharQuickRemote(); } }); main.addEventListener('click', () => { document.body.classList.remove('menu-open'); menuBtn.setAttribute('aria-expanded', 'false'); }); // Modal / Dropdown de Gestos Rápidos const quickRemote = document.createElement('div'); quickRemote.id = 'quickRemoteBar'; quickRemote.style.cssText = ` position: fixed; top: 72px; right: 28px; z-index: 90; background: rgba(14, 24, 38, 0.95); backdrop-filter: blur(16px); border: 1px solid var(--borda-brilho); border-radius: 14px; padding: 16px; box-shadow: var(--sombra); display: none; flex-direction: column; gap: 10px; width: 290px; `; quickRemote.innerHTML = `
⚡ Disparo Rápido
`; document.body.appendChild(quickRemote); function toggleQuickRemote() { const isVisible = quickRemote.style.display === 'flex'; quickRemote.style.display = isVisible ? 'none' : 'flex'; if (!isVisible && window.RobotAudio) window.RobotAudio.click(); } function fecharQuickRemote() { quickRemote.style.display = 'none'; } top.querySelector('#quickRemoteBtn').onclick = toggleQuickRemote; quickRemote.querySelector('#closeQuickRemote').onclick = fecharQuickRemote; quickRemote.addEventListener('click', async (e) => { const btn = e.target.closest('button[data-gesto]'); if (!btn) return; const gesto = btn.getAttribute('data-gesto'); try { if (window.RobotAudio) window.RobotAudio.tocarGesto(gesto); await fetch('/gesto/' + encodeURIComponent(gesto)); btn.style.transform = 'scale(0.95)'; setTimeout(() => btn.style.transform = '', 150); } catch (err) { console.error('Falha ao enviar gesto:', err); } }); // Botão de Áudio const sfxBtn = top.querySelector('#sfxToggleBtn'); function atualizarBotaoSfx() { if (!window.RobotAudio) return; sfxBtn.textContent = window.RobotAudio.enabled ? '🔊 Som: Ativo' : '🔇 Som: Mudo'; sfxBtn.style.color = window.RobotAudio.enabled ? 'var(--ciano)' : 'var(--fraco)'; } sfxBtn.onclick = () => { if (window.RobotAudio) { window.RobotAudio.toggle(); atualizarBotaoSfx(); } }; setTimeout(atualizarBotaoSfx, 400); frame.append(top, main); document.body.prepend(sidebar, frame); // Monitoramento periódico de status do celular e servidor async function checkStatus() { try { const r = await fetch('/app/status', { cache: 'no-store', signal: AbortSignal.timeout(3000) }); if (!r.ok) throw Error(); const s = await r.json(); const badge = document.getElementById('shellConnection'); const text = document.getElementById('shellConnectionText') || badge; const bat = s.estado?.bateria?.nivel; const carregando = s.estado?.bateria?.carregando; let batInfo = ''; if (bat != null) { batInfo = ` · ${bat}%${carregando ? ' ⚡' : ''}`; } if (s.online) { text.textContent = `S10 Online${batInfo}`; badge.className = 'badge ativo'; } else { text.textContent = 'S10 Desconectado'; badge.className = 'badge offline'; } } catch (e) { const badge = document.getElementById('shellConnection'); if (badge) { badge.className = 'badge danger'; const text = document.getElementById('shellConnectionText') || badge; text.textContent = 'Servidor Offline'; } } } checkStatus(); setInterval(checkStatus, 4000); // ========================================================================= // BARRA DE MONITORAMENTO NO TOPO — câmera e rosto sempre visíveis // Com botão de expandir/recolher cada painel individualmente // ========================================================================= const watchBar = document.createElement('div'); watchBar.id = 'watchBar'; watchBar.className = 'watch-bar'; // Lê estado salvo (expandido ou não) e tamanho const watchState = (() => { try { return JSON.parse(localStorage.getItem('carinha_watch') || '{}'); } catch { return {}; } })(); function saveWatchState() { localStorage.setItem('carinha_watch', JSON.stringify(watchState)); } // Altura dos painéis: compacto ou expandido const HEIGHTS = { compact: 90, medium: 200, full: 340 }; function makePanelHTML(id, icon, label, src) { const h = watchState[id + '_size'] || 'compact'; const hidden = watchState[id + '_hidden'] === true; return `
${icon} ${label}
`; } watchBar.innerHTML = `
MONITOR
${makePanelHTML('camera', '📷', 'Câmera ao Vivo', '/painel?clean=1')} ${makePanelHTML('rosto', '🤖', 'Rosto do Carinha', '/carinha.html?espelho=1&audio=0&clean=1')}
`; document.body.appendChild(watchBar); // Aplica estado inicial de visibilidade dos painéis ['camera', 'rosto'].forEach(id => { const panel = watchBar.querySelector('#wp_' + id); if (!panel) return; if (watchState[id + '_hidden'] === true) panel.classList.add('watch-panel-hidden'); }); // Oculta a barra inteira se o usuário fechou antes if (watchState['bar_hidden'] === true) { watchBar.classList.add('watch-bar-collapsed'); } // Toggle da barra inteira watchBar.querySelector('#watchBarToggle').addEventListener('click', () => { const collapsed = watchBar.classList.toggle('watch-bar-collapsed'); watchState['bar_hidden'] = collapsed; saveWatchState(); if (window.RobotAudio) window.RobotAudio.click(); }); // Toggle de visibilidade dos painéis individuais watchBar.querySelector('#watchAddCamera').addEventListener('click', () => { const panel = watchBar.querySelector('#wp_camera'); const hidden = panel.classList.toggle('watch-panel-hidden'); watchState['camera_hidden'] = hidden; saveWatchState(); }); watchBar.querySelector('#watchAddRosto').addEventListener('click', () => { const panel = watchBar.querySelector('#wp_rosto'); const hidden = panel.classList.toggle('watch-panel-hidden'); watchState['rosto_hidden'] = hidden; saveWatchState(); }); // Expandir / Recolher tamanho dos painéis watchBar.addEventListener('click', e => { const btn = e.target.closest('.wp-expand, .wp-shrink, .wp-hide'); if (!btn) return; const id = btn.dataset.id; const panel = watchBar.querySelector('#wp_' + id); const body = panel.querySelector('.watch-panel-body'); const sizes = Object.keys(HEIGHTS); if (btn.classList.contains('wp-expand')) { const cur = sizes.indexOf(panel.dataset.size); const next = sizes[Math.min(cur + 1, sizes.length - 1)]; panel.dataset.size = next; body.style.height = HEIGHTS[next] + 'px'; watchState[id + '_size'] = next; } else if (btn.classList.contains('wp-shrink')) { const cur = sizes.indexOf(panel.dataset.size); const next = sizes[Math.max(cur - 1, 0)]; panel.dataset.size = next; body.style.height = HEIGHTS[next] + 'px'; watchState[id + '_size'] = next; } else if (btn.classList.contains('wp-hide')) { const hidden = panel.classList.toggle('watch-panel-hidden'); watchState[id + '_hidden'] = hidden; } saveWatchState(); if (window.RobotAudio) window.RobotAudio.click(); }); // Ajusta padding do conteúdo principal para não sobrepor a watch-bar const appContent = document.getElementById('appContent'); function syncWatchBarOffset() { const collapsed = watchBar.classList.contains('watch-bar-collapsed'); const h = collapsed ? 0 : watchBar.offsetHeight; if (appContent) appContent.style.paddingTop = h > 0 ? (h + 12) + 'px' : ''; document.body.classList.toggle('watch-bar-active', !collapsed); } const ro = new ResizeObserver(syncWatchBarOffset); ro.observe(watchBar); syncWatchBarOffset(); // Também atualiza o offset ao colapsar/expandir a barra const origToggle = watchBar.querySelector('#watchBarToggle'); if (origToggle) { const prevHandler = origToggle.onclick; origToggle.addEventListener('click', () => { setTimeout(syncWatchBarOffset, 320); // após animação }); } })();