feat(pessoas): cartão "Quem é?" com o rosto, no Ao vivo e em Pessoas

Um cartão por pessoa desconhecida (grupo de rostos parecidos), com o rosto
grande: toca no nome de quem já conhece ou digita um novo, e a resposta vale
para todas as fotos do grupo. Não é ninguém / Pular. No Ao vivo só aparece
de quem ele já viu 3 vezes ou mais.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-11 18:13:26 -03:00
co-authored by Claude Opus 5.5
parent 938467e318
commit 92701e8a16
3 changed files with 153 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
<script lang="ts">
import { ChevronRight, UserCheck, UserX } from '@lucide/svelte';
import { onMount } from 'svelte';
import { pessoas, urlRosto } from '../pessoas.svelte';
// "Quem é?": um cartão por pessoa desconhecida (grupo de rostos parecidos), com o rosto grande.
// A resposta vale para todas as fotos do grupo. minimo: só pergunta de quem ele já viu tantas vezes.
let { minimo = 1 }: { minimo?: number } = $props();
onMount(() => {
if (!pessoas.status) pessoas.carregar();
});
const st = $derived(pessoas.status);
let pulados = $state<number[]>([]); // primeiro id de cada grupo pulado nesta tela
let nome = $state('');
let ocupado = $state(false);
const grupo = $derived.by(() => {
if (!st?.disponivel || !st.config.ligado) return null;
return st.grupos.find((ids) => ids.length >= minimo && !pulados.includes(ids[0])) ?? null;
});
const rostos = $derived(grupo && st ? st.sem_nome.filter((r) => grupo.includes(r.id)) : []);
const restantes = $derived(st ? st.grupos.filter((ids) => ids.length >= minimo && !pulados.includes(ids[0])).length : 0);
const capa = $derived(rostos[0] ?? null);
const quando = $derived(capa ? new Date(Math.min(...rostos.map((r) => r.horario)) * 1000).toLocaleDateString('pt-BR') : '');
async function responder(alvo: { pessoa?: number; nome?: string } | null) {
if (!grupo || ocupado) return;
ocupado = true;
const ok = alvo ? await pessoas.marcar([...grupo], alvo) : await pessoas.descartar([...grupo]);
ocupado = false;
if (ok) nome = '';
}
async function salvar(e: SubmitEvent) {
e.preventDefault();
if (nome.trim()) await responder({ nome: nome.trim() });
}
function pular() {
if (grupo) pulados = [...pulados, grupo[0]];
nome = '';
}
</script>
{#if grupo && capa}
<section class="quem" aria-label="Quem é esta pessoa?">
<img class="capa" src={urlRosto(capa.recorte)} alt="Rosto que ele viu e ainda não conhece" />
<div class="corpo">
<h3>Quem é?</h3>
<p>Ele viu esta pessoa {grupo.length} {grupo.length === 1 ? 'vez' : 'vezes'} desde {quando}.</p>
{#if rostos.length > 1}
<div class="mais">
{#each rostos.slice(1, 7) as r (r.id)}<img src={urlRosto(r.recorte)} alt="" loading="lazy" />{/each}
{#if rostos.length > 7}<span>+{rostos.length - 7}</span>{/if}
</div>
{/if}
{#if st?.pessoas.length}
<div class="conhecidos">
{#each st.pessoas as p (p.id)}
<button class="btn pequeno" disabled={ocupado} onclick={() => responder({ pessoa: p.id })}><UserCheck size={14} /> {p.nome}</button>
{/each}
</div>
{/if}
<form class="novo" onsubmit={salvar}>
<input bind:value={nome} placeholder={st?.pessoas.length ? 'Ou alguém novo: nome' : 'Nome'} maxlength="30" aria-label="Nome desta pessoa" />
<button class="btn pequeno primario" type="submit" disabled={!nome.trim() || ocupado}>Salvar</button>
</form>
<div class="rodape">
<button class="btn pequeno fantasma" disabled={ocupado} onclick={() => responder(null)}><UserX size={14} /> Não é ninguém</button>
<button class="btn pequeno fantasma" onclick={pular}>Pular <ChevronRight size={14} /></button>
{#if restantes > 1}<small>mais {restantes - 1} para conhecer</small>{/if}
</div>
</div>
</section>
{/if}
<style>
.quem {
display: grid;
grid-template-columns: 132px minmax(0, 1fr);
gap: var(--e-4);
align-items: start;
padding: var(--e-4);
border: 1px solid var(--destaque);
border-radius: var(--raio-lg);
background: var(--destaque-suave);
}
@media (max-width: 520px) {
.quem {
grid-template-columns: minmax(0, 1fr);
justify-items: center;
text-align: center;
}
.mais, .conhecidos, .rodape {
justify-content: center;
}
}
.capa {
width: 132px;
height: 132px;
border-radius: var(--raio);
object-fit: cover;
background: var(--superficie-3);
}
.corpo {
display: grid;
gap: var(--e-2);
min-width: 0;
}
h3 {
margin: 0;
font-size: var(--t-lg);
}
p {
margin: 0;
color: var(--texto-2);
font-size: var(--t-sm);
}
.mais {
display: flex;
align-items: center;
gap: 4px;
}
.mais img {
width: 34px;
height: 34px;
border-radius: 50%;
object-fit: cover;
}
.mais span {
font-size: var(--t-xs);
color: var(--texto-3);
}
.conhecidos, .novo, .rodape {
display: flex;
flex-wrap: wrap;
gap: var(--e-2);
align-items: center;
}
.novo input {
flex: 1;
min-width: 140px;
}
.rodape small {
color: var(--texto-3);
font-size: var(--t-xs);
}
</style>
+4
View File
@@ -15,6 +15,7 @@
import PresencaPir from '../lib/componentes/PresencaPir.svelte';
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
import VidaCartao from '../lib/componentes/VidaCartao.svelte';
import QuemE from '../lib/componentes/QuemE.svelte';
import AprendizadoCartao from '../lib/componentes/AprendizadoCartao.svelte';
import { estado } from '../lib/estado.svelte';
import { Rastreamento } from '../lib/rastreamento.svelte';
@@ -128,6 +129,7 @@ const pirAtivo = $derived(
<!-- No PC: à direita Vida e o que ele aprendeu; embaixo da câmera, o resto.
No celular as duas colunas se desfazem (display: contents) e a ordem vem do `order`. -->
<div class="coluna direita">
<div class="c-quem"><QuemE minimo={3} /></div>
<div class="c-vida"><VidaCartao /></div>
{#if estado.vida?.config.ligado}<div class="c-aprendizado"><AprendizadoCartao /></div>{/if}
@@ -394,6 +396,8 @@ const pirAtivo = $derived(
min-width: 0;
}
.palco { order: 0; }
.c-quem { order: 1; }
.c-quem:empty { display: none; }
.c-vida { order: 1; }
.c-aprendizado { order: 2; }
.c-celular { order: 3; }
+2
View File
@@ -2,6 +2,7 @@
import { Check, Pencil, ScanFace, ShieldCheck, Trash2, UserPlus, X } from '@lucide/svelte';
import { onMount } from 'svelte';
import Card from '../lib/componentes/Card.svelte';
import QuemE from '../lib/componentes/QuemE.svelte';
import Selo from '../lib/componentes/Selo.svelte';
import Toggle from '../lib/componentes/Toggle.svelte';
import { estado } from '../lib/estado.svelte';
@@ -48,6 +49,7 @@
</script>
<div class="pessoas">
<QuemE />
<Card titulo="Reconhecimento de pessoas" ajuda="pessoas" descricao="Pelas fotos dos eventos ele descobre quem chegou e cumprimenta pelo nome.">
{#snippet acoes()}
<button class="btn pequeno" onclick={() => pessoas.varrer()} disabled={!st?.disponivel || !!st?.varrendo}>