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>
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
/** Ações que qualquer tela pode disparar: gestos, rosto, cor, E-Stop. */
|
||||
import { postar } from './api';
|
||||
import { avisos, tentar } from './avisos.svelte';
|
||||
import { estado } from './estado.svelte';
|
||||
|
||||
/** Nome amigável e emoji de cada gesto do rosto ('parar' fecha o kiosk: não vira botão). */
|
||||
export const GESTOS_INFO: Record<string, { nome: string; emoji: string }> = {
|
||||
feliz: { nome: 'Feliz', emoji: '😊' },
|
||||
alegre: { nome: 'Alegre', emoji: '😄' },
|
||||
coracao: { nome: 'Coração', emoji: '❤️' },
|
||||
piscar: { nome: 'Piscar', emoji: '😌' },
|
||||
piscar2: { nome: 'Piscar duplo', emoji: '😑' },
|
||||
piscadinha: { nome: 'Piscadinha', emoji: '😉' },
|
||||
surpreso: { nome: 'Surpreso', emoji: '😮' },
|
||||
triste: { nome: 'Triste', emoji: '😢' },
|
||||
bravo: { nome: 'Bravo', emoji: '😠' },
|
||||
olhar_esq: { nome: 'Olhar à esquerda', emoji: '👈' },
|
||||
olhar_dir: { nome: 'Olhar à direita', emoji: '👉' },
|
||||
olhar_baixo: { nome: 'Olhar para baixo', emoji: '👇' },
|
||||
dormir: { nome: 'Dormir', emoji: '😴' },
|
||||
acordar: { nome: 'Acordar', emoji: '🌅' },
|
||||
aleatorio: { nome: 'Aleatório', emoji: '🎲' },
|
||||
};
|
||||
|
||||
export const gestosDisponiveis = () => estado.gestos.filter((g) => g in GESTOS_INFO);
|
||||
|
||||
const CHAVE_FAVORITOS = 'carinha_favoritos';
|
||||
const PADRAO_FAVORITOS = ['feliz', 'coracao', 'piscadinha', 'surpreso', 'triste', 'olhar_esq', 'olhar_dir', 'dormir', 'acordar'];
|
||||
|
||||
function lerFavoritos(): string[] {
|
||||
try {
|
||||
const v = JSON.parse(localStorage.getItem(CHAVE_FAVORITOS) || 'null');
|
||||
if (Array.isArray(v) && v.every((x) => typeof x === 'string')) return v.slice(0, 9);
|
||||
} catch {
|
||||
/* sem armazenamento */
|
||||
}
|
||||
return PADRAO_FAVORITOS;
|
||||
}
|
||||
|
||||
class Favoritos {
|
||||
lista = $state<string[]>(lerFavoritos());
|
||||
alternar(g: string) {
|
||||
this.lista = this.lista.includes(g) ? this.lista.filter((x) => x !== g) : [...this.lista, g].slice(0, 9);
|
||||
try {
|
||||
localStorage.setItem(CHAVE_FAVORITOS, JSON.stringify(this.lista));
|
||||
} catch {
|
||||
/* ok */
|
||||
}
|
||||
}
|
||||
}
|
||||
export const favoritos = new Favoritos();
|
||||
|
||||
export const gesto = (nome: string) => tentar(() => postar('/comando', { tipo: 'gesto', valor: nome }));
|
||||
export const rosto = (nome: string) => tentar(() => postar('/comando', { tipo: 'rosto', valor: nome }));
|
||||
export const cor = (rgb: [number, number, number]) => tentar(() => postar('/comando', { tipo: 'cor', valor: rgb }));
|
||||
export const recarregarCelular = () =>
|
||||
tentar(async () => {
|
||||
const r = await postar<{ mensagem: string }>('/comando', { tipo: 'recarregar' });
|
||||
avisos.ok(r.mensagem);
|
||||
});
|
||||
|
||||
export async function pararTudo() {
|
||||
try {
|
||||
await postar('/parar-tudo');
|
||||
avisos.mostrar('E-Stop acionado: motores desligados e travados.', 'erro');
|
||||
} catch (e) {
|
||||
avisos.erro(e);
|
||||
}
|
||||
}
|
||||
|
||||
export const rearmar = () => tentar(() => postar('/rearmar'), 'Motores rearmados.');
|
||||
|
||||
export function hex(rgb: number[]) {
|
||||
return '#' + rgb.map((v) => v.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
export function rgb(hexCor: string): [number, number, number] {
|
||||
const n = parseInt(hexCor.slice(1), 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
export const CORES: { nome: string; rgb: [number, number, number] }[] = [
|
||||
{ nome: 'Ciano', rgb: [92, 214, 255] },
|
||||
{ nome: 'Verde', rgb: [74, 222, 128] },
|
||||
{ nome: 'Amarelo', rgb: [251, 191, 36] },
|
||||
{ nome: 'Laranja', rgb: [255, 140, 50] },
|
||||
{ nome: 'Rosa', rgb: [255, 110, 190] },
|
||||
{ nome: 'Roxo', rgb: [167, 139, 250] },
|
||||
{ nome: 'Vermelho', rgb: [248, 90, 90] },
|
||||
{ nome: 'Branco', rgb: [235, 240, 245] },
|
||||
];
|
||||
Reference in New Issue
Block a user