feat: initial commit - Carinha Control Panel v1.0 com UI glassmorphism, watch-bar, emojis, braco robotico e conversa IA

This commit is contained in:
julio
2026-10-05 16:21:57 -03:00
commit 8217292893
114 changed files with 13145 additions and 0 deletions
+215
View File
@@ -0,0 +1,215 @@
"use strict";
const $ = (id) => document.getElementById(id);
let recargaPendente = null;
let recargaInicio = 0;
let appConfig = null;
let imagemURL = null;
let recebendoImagem = false;
let salvandoConfig = false;
let sequencia = -1;
let ultimoStatus = null;
let consultandoStatus = false;
async function api(rota, dados) {
const r = await fetch(rota, {cache: 'no-store', signal: AbortSignal.timeout(rota === '/app/connect' ? 65000 : 3500),
...(dados === undefined ? {} : {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(dados)})});
const body = await r.json();
if (!r.ok) throw new Error(body.erro || 'Servidor respondeu HTTP ' + r.status);
return body;
}
function ajustarCamera() {
const ligada = appConfig && appConfig.preview;
$('toggleCamera').textContent = ligada ? 'Desligar câmera' : 'Ligar câmera';
if (!ligada) esconderImagem('Transmissão desligada. O rosto e os sensores continuam funcionando.');
if (window.CarinhaVideo && !ligada) window.CarinhaVideo.parar();
}
function esconderImagem(texto) {
$('cameraImagem').hidden = true;
$('cameraImagem').removeAttribute('src');
$('cameraMensagem').hidden = false;
$('cameraMensagem').textContent = texto;
if (imagemURL) URL.revokeObjectURL(imagemURL);
imagemURL = null;
sequencia = -1;
}
async function salvarApp(parte) {
if (salvandoConfig) return;
salvandoConfig = true;
CAMPOS.forEach(([chave]) => { $('ajuste-' + chave).disabled = true; });
$('configEstado').textContent = 'Salvando…';
$('toggleCamera').disabled = true;
try {
appConfig = (await api('/app/config', parte)).config;
atualizarCampos();
ajustarCamera();
$('configEstado').textContent = 'Salvo no servidor. Aguardando o celular aplicar.';
} catch (e) {
$('configEstado').textContent = 'Não foi possível salvar: ' + e.message;
atualizarCampos();
} finally {
salvandoConfig = false; $('toggleCamera').disabled = false;
CAMPOS.forEach(([chave]) => { $('ajuste-' + chave).disabled = false; });
}
}
const CAMPOS = [
['rastrearRosto', 'Rastrear rosto'], ['rastrearMaos', 'Rastrear mãos'],
['seguirRastreamento', 'Olhos acompanharem o alvo'], ['mostrarRastreamento', 'Mostrar rastreamento no vídeo'],
['sensorMic', 'Microfone ligado'], ['sensorCamera', 'Câmera ligada'],
['micSono', 'Permitir microfone durante o sono'], ['cameraSono', 'Permitir câmera durante o sono'],
['cameraLado', 'Câmera', [['user','Frontal'],['environment','Traseira']]],
['videoResolucao', 'Resolução do vídeo', [['auto','Automática (padrão)'],['max','Máxima disponível'],['480','480p'],['720','720p'],['1080','1080p'],['2160','4K'],['4320','8K']]],
['videoFPS', 'FPS do vídeo', [[0,'Automático (padrão)'],[-1,'Máximo disponível'],[15,'15'],[24,'24'],[30,'30'],[60,'60'],[120,'120'],[240,'240']]],
['boca', 'Mostrar boca'], ['sobrancelhas', 'Mostrar sobrancelhas'],
['auto', 'Gestos espontâneos'], ['audio', 'Sons da Carinha'],
['ouvir', 'Reagir aos sons'], ['reagir', 'Reagir ao movimento'],
['volume', 'Volume dos sons', 0, 1, 0.05],
['sompausa', 'Silêncio para dormir (s)', 1, 120, 1],
['economia', 'Reduzir brilho em repouso'],
['brilho', 'Brilho normal', 0.01, 1, 0.01],
['brilhoRepouso', 'Brilho em repouso', 0.01, 1, 0.01],
['repousoTela', 'Espera para reduzir brilho (s)', 5, 300, 5],
['somlim', 'Nível para perceber som', 0, 1, 0.01],
['somganho', 'Aumento de som para susto', 0, 1, 0.01],
['somaltolim', 'Som mínimo para susto', 0, 1, 0.01],
['somcooldown', 'Intervalo entre sustos por som (s)', 0, 120, 1],
['movlim', 'Movimento mínimo', 0, 1, 0.01],
['movforte', 'Movimento para susto', 0, 1, 0.01],
['movcooldown', 'Intervalo entre sustos por movimento (s)', 0, 120, 1],
['movolhar', 'Quanto os olhos acompanham', 0, 100, 1],
];
function montarCampos() {
CAMPOS.forEach(([chave, texto, min, max, passo], i) => {
const area = document.createElement('div');
const label = document.createElement('label');
const select = Array.isArray(min);
const input = document.createElement(select ? 'select' : 'input');
input.id = 'ajuste-' + chave; label.htmlFor = input.id;
if (!select) input.type = min === undefined ? 'checkbox' : 'range';
label.textContent = texto;
if (select) {
min.forEach(([v,t]) => input.add(new Option(t, String(v))));
area.className = 'ajuste'; area.append(label, input);
} else if (input.type === 'range') {
input.min = min; input.max = max; input.step = passo;
area.className = 'ajuste-range';
const output = document.createElement('output'); output.id = 'valor-' + chave;
label.appendChild(output); area.append(label, input);
input.oninput = () => { output.textContent = input.value; };
} else { area.className = 'ajuste'; area.append(label, input); }
input.onchange = () => salvarApp({[chave]: input.type === 'checkbox' ? input.checked : select && chave !== 'videoFPS' ? input.value : Number(input.value)});
$(i < 4 ? 'ajustesRastreamento' : i < 23 ? 'ajustesBasicos' : 'ajustesAvancados').appendChild(area);
});
}
function atualizarCampos() {
if (!appConfig) return;
CAMPOS.forEach(([chave]) => {
const input = $('ajuste-' + chave);
if (input.type === 'checkbox') input.checked = appConfig[chave];
else if (document.activeElement !== input) {
input.value = appConfig[chave]; if ($('valor-' + chave)) $('valor-' + chave).textContent = appConfig[chave];
}
});
}
async function statusApp() {
if (consultandoStatus) return;
consultandoStatus = true;
try {
const s = await api('/app/status'); ultimoStatus = s;
if (!salvandoConfig) { appConfig = await api('/app/config'); atualizarCampos(); ajustarCamera(); }
$('conexaoEstado').textContent = s.online ? 'Conectado' : 'Sem conexão';
$('conexaoEstado').classList.toggle('ativo', s.online);
if(s.online && s.celular_ip && document.activeElement !== $('celularIP')) $('celularIP').value=s.celular_ip;
if(s.servidor_ip && document.activeElement !== $('pcIP')) $('pcIP').value=s.servidor_ip;
if(recargaPendente && s.recarga?.id===recargaPendente && s.recarga.confirmada){$('conectarEstado').textContent='Tela atualizada: confirmação recebida do celular.';recargaPendente=null;}
else if(recargaPendente && Date.now()-recargaInicio>12000){$('conectarEstado').textContent='O celular não confirmou a recarga. Feche e reabra o Carinha uma vez para carregar o suporte atualizado.';recargaPendente=null;}
const e = s.estado || {};
const bateria = e.bateria;
const nivel = bateria?.nivel;
$('bateriaCelular').textContent = s.online && typeof nivel === 'number' && nivel >= 0 && nivel <= 100 ? `Bateria do celular: ${Math.round(nivel)}%${bateria.carregando ? ' · carregando' : ''}` : 'Bateria do celular: aguardando dados do app.';
$('rostoConfirmado').textContent = s.online ? 'No celular: ' + (e.rosto || '?') + (e.dormindo ? ', dormindo.' : ', acordado.') : 'Aguardando confirmação do celular.';
['mic', 'mov'].forEach((nome) => {
const chave = nome === 'mic' ? 'microfone' : 'movimento';
const ativo = s.online && e[nome === 'mic' ? 'microfone_ativo' : 'camera_ativa'];
const v = ativo ? Number(e[chave] || 0) : 0;
$(nome + 'Nivel').value = v;
$(nome + 'Valor').textContent = ativo ? Math.round(v * 100) + '%' : '—';
});
$('sensorEstado').textContent = s.online ? (e.camera_ativa ? ('Câmera ' + (appConfig?.cameraLado === 'environment' ? 'traseira' : 'frontal')) : 'Câmera desativada') + (e.centro ? ' · centro do movimento: ' + e.centro.map(v => Math.round(v * 100) + '%').join(', ') : '') + (typeof e.brilho === 'number' ? ' · brilho: ' + Math.round(e.brilho * 100) + '%' : '') : 'Abra o app no celular ou use Conectar e abrir o app.';
if (appConfig && !salvandoConfig && e.config && Object.keys(appConfig).every(k => e.config[k] === appConfig[k]))
$('configEstado').textContent = s.online ? 'Configurações aplicadas no celular.' : 'Configurações salvas no servidor. Celular sem conexão.';
if (appConfig && appConfig.videoRTC && window.CarinhaVideo) window.CarinhaVideo.atualizar(s, appConfig);
if (appConfig && window.CarinhaRastreamento) window.CarinhaRastreamento.atualizar(s, appConfig);
if (e.video && e.video.aviso) $('sensorEstado').textContent += ' · ' + e.video.aviso;
if (appConfig && !appConfig.videoRTC && appConfig.preview && (!s.online || !s.camera_recente))
esconderImagem(s.online ? 'Aguardando imagem da câmera. Confira a permissão de câmera no celular.' : 'Celular sem conexão. A imagem antiga foi removida.');
} catch (e) {
ultimoStatus = null;
$('bateriaCelular').textContent = 'Bateria do celular: sem conexão.';
$('conexaoEstado').textContent = 'Servidor indisponível';
$('conexaoEstado').classList.remove('ativo');
esconderImagem('O servidor não respondeu. Confira o serviço no Orange Pi.');
if (window.CarinhaVideo) window.CarinhaVideo.parar();
if (window.CarinhaRastreamento) window.CarinhaRastreamento.parar();
} finally { consultandoStatus = false; }
}
async function imagem() {
if (appConfig && appConfig.videoRTC) return;
if (recebendoImagem || !appConfig || !appConfig.preview || !ultimoStatus || !ultimoStatus.camera_recente) return;
recebendoImagem = true;
try {
const r = await fetch('/camera/frame?t=' + Date.now(), {cache:'no-store', signal:AbortSignal.timeout(2500)});
if (!r.ok || r.status === 204) return;
const blob = await r.blob();
if (!appConfig.preview) return;
const nova = URL.createObjectURL(blob);
const anterior = imagemURL; imagemURL = nova;
$('cameraImagem').src = nova;
$('cameraImagem').hidden = false; $('cameraMensagem').hidden = true;
if (anterior) URL.revokeObjectURL(anterior);
sequencia = ultimoStatus.frame_seq;
} catch (e) { esconderImagem('Imagem indisponível. Aguardando o próximo quadro.'); }
finally { recebendoImagem = false; }
}
$('toggleCamera').onclick = () => salvarApp({preview: !appConfig.preview});
$('conectarForm').onsubmit = async (ev) => {
ev.preventDefault(); $('conectarBotao').disabled = true;
$('conectarEstado').textContent = 'Conectando ao celular…';
try {
const r = await api('/app/connect', {ip: $('celularIP').value.trim(), pc: $('pcIP').value.trim(), porta:5555});
$('conectarEstado').textContent = r.mensagem;
} catch (e) { $('conectarEstado').textContent = e.message; }
finally { $('conectarBotao').disabled = false; }
};
montarCampos();
(async () => {
try { appConfig = await api('/app/config'); atualizarCampos(); ajustarCamera(); $('toggleCamera').disabled = false; await statusApp(); }
catch (e) { $('configEstado').textContent = 'Falha ao carregar: ' + e.message; }
})();
setInterval(statusApp, 1000);
setInterval(imagem, 250);
(function(){
const iaActive=document.getElementById('iaAtiva'),iaState=document.getElementById('iaEstado'),iaTest=document.getElementById('iaTestar');
let updating=false;
async function request(path,data){const r=await fetch('/ia/'+path,data===undefined?{}:{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});const d=await r.json();if(!r.ok)throw Error(d.erro);return d;}
async function refresh(){if(updating)return;try{const s=await request('status');iaActive.checked=s.ativo;iaTest.disabled=!s.ativo||s.ocupado;iaState.textContent=s.erro?'IA: '+s.erro:s.ocupado?'IA local pensando…':s.ultima?`${s.modelo} · ${s.executor} · última expressão: ${s.ultima.gesto} (${s.ultima.segundos} s)`:`${s.modelo} · ${s.executor} · ${s.ativo?'aguardando eventos':'desativado'}`;}catch(e){iaState.textContent=e.message;}}
iaActive.onchange=async()=>{updating=true;try{await request('config',{ativo:iaActive.checked});}catch(e){iaState.textContent=e.message;}finally{updating=false;refresh();}};
iaTest.onclick=async()=>{iaTest.disabled=true;try{await request('evento',{evento:document.getElementById('iaEvento').value});iaState.textContent='Evento enviado para a IA local…';}catch(e){iaState.textContent=e.message;}};
refresh();setInterval(refresh,2000);
})();
document.getElementById('recarregarTela').onclick=async()=>{
const button=document.getElementById('recarregarTela');button.disabled=true;
try{const r=await api('/app/recarregar',{});document.getElementById('conectarEstado').textContent=r.mensagem;recargaPendente=(await api('/app/status')).recarga.id;recargaInicio=Date.now();}
catch(e){document.getElementById('conectarEstado').textContent=e.message;}
finally{button.disabled=false;}
};