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,62 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Prévia do rosto: o próprio carinha.html em modo ?painel=1 (sem polling).
|
||||
* O painel repassa por postMessage o que chega no SSE, então a prévia mostra
|
||||
* exatamente as mesmas ordens que o celular recebe.
|
||||
*/
|
||||
import { onDestroy } from 'svelte';
|
||||
import { estado, ouvir } from '../estado.svelte';
|
||||
|
||||
let { titulo = 'Prévia do rosto' }: { titulo?: string } = $props();
|
||||
let quadro: HTMLIFrameElement;
|
||||
let pronto = $state(false);
|
||||
|
||||
function enviar(msg: unknown) {
|
||||
if (pronto) quadro?.contentWindow?.postMessage(msg, location.origin);
|
||||
}
|
||||
|
||||
function aoCarregar() {
|
||||
pronto = true;
|
||||
if (estado.appConfig) enviar({ tipo: 'config', config: $state.snapshot(estado.appConfig) });
|
||||
}
|
||||
|
||||
let ultimaConversa = estado.conversa?.ultima?.id;
|
||||
const cancelar = [
|
||||
ouvir('expressao', (d) => enviar({ tipo: 'ordem', ordem: d.ordem })),
|
||||
ouvir('app_config', (c) => enviar({ tipo: 'config', config: c })),
|
||||
ouvir('reacao', (r) => r?.receita && enviar({ tipo: 'previa-reacao', receita: r.receita })),
|
||||
// O celular reage à resposta da conversa com acordar + gesto; a prévia imita.
|
||||
ouvir('conversa', (c) => {
|
||||
if (c.ultima && c.ultima.id !== ultimaConversa) {
|
||||
ultimaConversa = c.ultima.id;
|
||||
enviar({ tipo: 'ordem', ordem: 'acordar' });
|
||||
enviar({ tipo: 'ordem', ordem: c.ultima.gesto });
|
||||
}
|
||||
}),
|
||||
];
|
||||
onDestroy(() => cancelar.forEach((c) => c()));
|
||||
|
||||
/** Para a tela de reações: mostra uma receita sem mandar ao celular. */
|
||||
export function previa(receita: unknown) {
|
||||
enviar({ tipo: 'previa-reacao', receita });
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe
|
||||
bind:this={quadro}
|
||||
src="/carinha.html?espelho=1&painel=1&auto=0&camera=0&audio=0"
|
||||
title={titulo}
|
||||
tabindex="-1"
|
||||
onload={aoCarregar}
|
||||
></iframe>
|
||||
|
||||
<style>
|
||||
iframe {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
background: #000;
|
||||
border-radius: inherit;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user