/**
* 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 = `
`;
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 `
`;
}
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
});
}
})();