Files
carinha-control-panel/web/src/lib/ajustes.svelte.ts
T
julioandClaude Opus 5.5 c4b53a8976 feat(web): cria painel Svelte com shell, Ao vivo e Ajustes
- web/ com Vite + Svelte 5 + TypeScript; servidor entrega web/dist em "/"
  (telas antigas seguem em /dashboard, /painel, /braco...)
- design system em tokens CSS, tema escuro e claro, fontes locais
- estado unico: foto de /estado + um EventSource em /eventos, sem polling
- shell com navegacao lateral/inferior, selos de status reais e E-Stop fixo
- command palette (Ctrl+K), gestos favoritos com atalhos 1-9, g+letra navega
- Ao vivo: camera WebRTC e rastreamento portados para TS, rosto, aparencia,
  sensores do celular, IA local e feed de acontecimentos
- Ajustes: campos do celular agrupados com busca, salvamento otimista e
  restaurar padrao (GET /app/padrao); motor da conversa, Gemini e token
- carinha.html ganha modo ?painel=1 dirigido por postMessage (sem polling)
- teste Playwright de ponta a ponta do painel novo

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 19:47:18 -03:00

108 lines
5.2 KiB
TypeScript

/** Ajustes do app do celular (/app/config): salvamento otimista e campos agrupados. */
import { pedir, postar } from './api';
import { avisos } from './avisos.svelte';
import { estado } from './estado.svelte';
import type { AppConfig } from './tipos';
/** Atualiza na hora; se o servidor recusar, volta ao valor anterior e avisa. Não trava o formulário. */
export async function salvarApp(parcial: AppConfig) {
const antes = estado.appConfig ? { ...estado.appConfig } : null;
if (estado.appConfig) estado.appConfig = { ...estado.appConfig, ...parcial };
try {
const r = await postar<{ config: AppConfig }>('/app/config', parcial);
estado.appConfig = r.config;
} catch (e) {
estado.appConfig = antes;
avisos.erro(e);
}
}
export async function restaurarPadrao() {
const padrao = await pedir<AppConfig>('/app/padrao');
// A transmissão não é "ajuste": continua como está.
delete padrao.preview;
await salvarApp(padrao);
avisos.ok('Ajustes do celular restaurados para o padrão.');
}
type Opcoes = [string | number, string][];
export interface Campo {
chave: string;
rotulo: string;
ajuda?: string;
tipo: 'toggle' | 'slider' | 'escolha';
min?: number;
max?: number;
passo?: number;
unidade?: string;
opcoes?: Opcoes;
}
export const GRUPOS: { titulo: string; descricao: string; campos: Campo[] }[] = [
{
titulo: 'Tela e personalidade',
descricao: 'Como o rosto aparece e se comporta sozinho.',
campos: [
{ chave: 'auto', rotulo: 'Gestos espontâneos', ajuda: 'Pisca e olha em volta sem comando', tipo: 'toggle' },
{ chave: 'boca', rotulo: 'Mostrar boca', tipo: 'toggle' },
{ chave: 'sobrancelhas', rotulo: 'Mostrar sobrancelhas', tipo: 'toggle' },
{ chave: 'audio', rotulo: 'Sons no celular', ajuda: 'Bipes do próprio robô', tipo: 'toggle' },
{ chave: 'volume', rotulo: 'Volume dos sons', tipo: 'slider', min: 0, max: 1, passo: 0.05 },
{ chave: 'brilho', rotulo: 'Brilho normal', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
{ chave: 'economia', rotulo: 'Reduzir brilho em repouso', tipo: 'toggle' },
{ chave: 'brilhoRepouso', rotulo: 'Brilho em repouso', tipo: 'slider', min: 0.01, max: 1, passo: 0.01 },
{ chave: 'repousoTela', rotulo: 'Espera para reduzir brilho', tipo: 'slider', min: 5, max: 300, passo: 5, unidade: ' s' },
],
},
{
titulo: 'Sensores e reações',
descricao: 'O que o celular percebe e como reage.',
campos: [
{ chave: 'sensorMic', rotulo: 'Microfone', ajuda: 'Só o nível de som é usado, nunca o áudio', tipo: 'toggle' },
{ chave: 'ouvir', rotulo: 'Reagir a sons', tipo: 'toggle' },
{ chave: 'micSono', rotulo: 'Microfone durante o sono', tipo: 'toggle' },
{ chave: 'sompausa', rotulo: 'Silêncio para dormir', tipo: 'slider', min: 1, max: 120, passo: 1, unidade: ' s' },
{ chave: 'sensorCamera', rotulo: 'Câmera como sensor', ajuda: 'Detecta movimento e permite o vídeo', tipo: 'toggle' },
{ chave: 'reagir', rotulo: 'Reagir a movimento', tipo: 'toggle' },
{ chave: 'cameraSono', rotulo: 'Câmera durante o sono', tipo: 'toggle' },
{ chave: 'movolhar', rotulo: 'Quanto os olhos acompanham', tipo: 'slider', min: 0, max: 100, passo: 1, unidade: '%' },
],
},
{
titulo: 'Vídeo e rastreamento',
descricao: 'Qualidade da transmissão e rastreamento feito neste PC.',
campos: [
{ chave: 'cameraLado', rotulo: 'Câmera', tipo: 'escolha', opcoes: [['user', 'Frontal'], ['environment', 'Traseira']] },
{
chave: 'videoResolucao',
rotulo: 'Resolução',
tipo: 'escolha',
opcoes: [['auto', 'Automática'], ['max', 'Máxima'], ['480', '480p'], ['720', '720p'], ['1080', '1080p'], ['2160', '4K'], ['4320', '8K']],
},
{
chave: 'videoFPS',
rotulo: 'Quadros por segundo',
tipo: 'escolha',
opcoes: [[0, 'Automático'], [-1, 'Máximo'], [15, '15'], [24, '24'], [30, '30'], [60, '60'], [120, '120'], [240, '240']],
},
{ chave: 'rastrearRosto', rotulo: 'Rastrear rosto', tipo: 'toggle' },
{ chave: 'rastrearMaos', rotulo: 'Rastrear mãos', tipo: 'toggle' },
{ chave: 'seguirRastreamento', rotulo: 'Olhos seguem o alvo', tipo: 'toggle' },
{ chave: 'mostrarRastreamento', rotulo: 'Desenhar rastreamento no vídeo', tipo: 'toggle' },
],
},
{
titulo: 'Sensibilidade (avançado)',
descricao: 'Limiares dos sustos e intervalos entre reações.',
campos: [
{ chave: 'somlim', rotulo: 'Nível para perceber som', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
{ chave: 'somaltolim', rotulo: 'Som mínimo para susto', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
{ chave: 'somganho', rotulo: 'Aumento súbito para susto', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
{ chave: 'somcooldown', rotulo: 'Intervalo entre sustos por som', tipo: 'slider', min: 0, max: 120, passo: 1, unidade: ' s' },
{ chave: 'movlim', rotulo: 'Movimento mínimo', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
{ chave: 'movforte', rotulo: 'Movimento para susto', tipo: 'slider', min: 0, max: 1, passo: 0.01 },
{ chave: 'movcooldown', rotulo: 'Intervalo entre sustos por movimento', tipo: 'slider', min: 0, max: 120, passo: 1, unidade: ' s' },
],
},
];