feat(vida): aprendizado com o dia a dia (gosto, rotina da casa e sono)

O Carinha passa a melhorar as interações com o que vive em casa, sem IA
e sem internet (aprendizado.py):
- gosto: gesto ou atividade feito com alguém por perto é uma tentativa;
  carinho, brincadeira ou conversa até 60 s depois contam como "gostaram"
  (cutucada vale meio). A chance de cada ato vai de 0,5x a 2x, relativa à
  média, e só depois de 3 tentativas;
- rotina: fração de minutos com gente em cada hora (média móvel entre
  dias). Antes de uma hora movimentada, acorda do cochilo 15 min antes;
  sono pesado e madrugada continuam respeitados;
- sono: PIR acordou e ninguém ficou nem interagiu em 3 min = à toa. Se
  repete na mesma hora, ali pede 1 ou 2 disparos a mais para acordar;
- à meia-noite fecha o dia: lições em primeira pessoa no diário e o
  passado pesa 15% menos a cada dia (o gosto muda);
- rotas /aprendizado/status|config|esquecer e SSE 'aprendizado';
- painel: cartão "O que ele aprendeu" no Ao vivo (rotina em 24 barras,
  faz mais / faz menos, lições, esquecer com confirmação) e tópico no
  guia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-09 10:13:23 -03:00
co-authored by Claude Opus 5.5
parent 4777ceb023
commit 24625453f7
12 changed files with 1000 additions and 9 deletions
@@ -0,0 +1,283 @@
<script lang="ts">
import { GraduationCap } from '@lucide/svelte';
import { configurarAprendizado, esquecerAprendizado } from '../aprendizado.svelte';
import { estado } from '../estado.svelte';
import Grupo from './Grupo.svelte';
import Interruptor from './Interruptor.svelte';
import Linha from './Linha.svelte';
// O que ele aprendeu com o dia a dia: rotina da casa (24 h), do que gostam, lições e o sono.
const a = $derived(estado.aprendizado);
let confirmar = $state(false);
const horaAgora = $derived(new Date(estado.agora).getHours());
const temRotina = $derived(!!a && a.rotina.some((v) => v !== null));
const faz_mais = $derived(a ? a.gosto.filter((g) => g.peso > 1.05).slice(0, 4) : []);
const faz_menos = $derived(a ? [...a.gosto].reverse().filter((g) => g.peso < 0.95).slice(0, 3) : []);
const respostas = $derived(a ? Object.values(a.hoje.respostas).reduce((s, n) => s + n, 0) : 0);
const detalhe = $derived.by(() => {
if (!a) return '';
if (!a.config.ligado) return 'Desligado: ele faz tudo como veio de fábrica';
const dias = a.dias === 0 ? 'primeiro dia observando' : a.dias === 1 ? '1 dia observado' : `${a.dias} dias observados`;
return `${dias} · hoje: ${a.hoje.atos} ${a.hoje.atos === 1 ? 'gesto' : 'gestos'} com alguém perto, ${respostas} ${respostas === 1 ? 'carinho ou conversa' : 'carinhos ou conversas'}`;
});
const resumoRotina = $derived.by(() => {
if (!a) return '';
const cheias = a.rotina.flatMap((v, h) => (v !== null && v >= 0.5 ? [h] : []));
return cheias.length ? 'Costuma ter gente às ' + cheias.map((h) => h + 'h').join(', ') : 'Ainda sem um horário marcado';
});
function dia(iso: string) {
const [, m, d] = iso.split('-');
return `${d}/${m}`;
}
function vezes(peso: number) {
return '×' + peso.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
}
</script>
<Grupo
titulo="O que ele aprendeu"
ajuda="aprendizado"
rodape="Ele conta gestos feitos com alguém por perto e o que veio logo depois (carinho, conversa). À meia-noite junta o dia e escreve as lições."
>
{#if !a}
<p class="vazio">Carregando…</p>
{:else}
<Linha icone={GraduationCap} cor="#30b0c7" rotulo="Aprender com o dia a dia" {detalhe}>
{#snippet acessorio()}
<Interruptor rotulo="Aprender com o dia a dia" checked={a.config.ligado} onchange={(l) => configurarAprendizado(l)} />
{/snippet}
</Linha>
{#if a.config.ligado}
<section class="bloco" aria-labelledby="ap-rotina">
<h3 id="ap-rotina">Rotina da casa <span>{temRotina ? resumoRotina : 'aparece depois da primeira meia-noite'}</span></h3>
<div class="rotina" role="img" aria-label={resumoRotina}>
{#each a.rotina as v, h (h)}
<span
class="hora"
class:agora={h === horaAgora}
class:sem={v === null}
title={`${h}h: ${v === null ? 'sem dados' : Math.round(v * 100) + '% do tempo com gente'}${a.sono[h] ? ' · acordou à toa aqui' : ''}`}
>
<span class="barra" style="--v: {v ?? 0}"></span>
{#if a.sono[h]}<span class="lua" aria-hidden="true">☾</span>{/if}
</span>
{/each}
</div>
<div class="eixo mono" aria-hidden="true"><span>0h</span><span>6h</span><span>12h</span><span>18h</span><span>24h</span></div>
{#if a.sono.some((n) => n > 0)}
<p class="nota"><span aria-hidden="true">☾</span> Horas em que acordou à toa: ali o sensor precisa de mais movimento para acordá-lo.</p>
{/if}
</section>
{#if faz_mais.length || faz_menos.length}
<section class="bloco gosto" aria-labelledby="ap-gosto">
<h3 id="ap-gosto">Do que vocês gostam</h3>
<div class="colunas">
<ul>
<li class="titulo">Faz mais</li>
{#each faz_mais as g (g.id)}
<li class="mais"><span>{g.nome}</span><b class="mono">{vezes(g.peso)}</b></li>
{:else}
<li class="nada">Nada se destacou ainda</li>
{/each}
</ul>
<ul>
<li class="titulo">Faz menos</li>
{#each faz_menos as g (g.id)}
<li class="menos"><span>{g.nome}</span><b class="mono">{vezes(g.peso)}</b></li>
{:else}
<li class="nada">Nada foi ignorado</li>
{/each}
</ul>
</div>
</section>
{/if}
<section class="bloco" aria-labelledby="ap-licoes">
<h3 id="ap-licoes">Lições</h3>
{#if a.licoes.length}
<ol class="licoes">
{#each a.licoes.slice(0, 5) as l (l.dia + l.texto)}
<li><time class="mono">{dia(l.dia)}</time> <span>{l.texto}</span></li>
{/each}
</ol>
{:else}
<p class="nota">A primeira lição chega à meia-noite. Faça carinho ou converse logo depois de um gesto que você gostou.</p>
{/if}
</section>
{/if}
<div class="esquecer">
{#if confirmar}
<span>Apagar rotina, gostos e lições?</span>
<button class="btn pequeno perigo" onclick={() => { confirmar = false; esquecerAprendizado(); }}>Esquecer tudo</button>
<button class="btn fantasma pequeno" onclick={() => (confirmar = false)}>Cancelar</button>
{:else}
<button class="btn fantasma pequeno" onclick={() => (confirmar = true)} disabled={!a.dias && !a.gosto.length && !a.licoes.length}>
Esquecer o que aprendeu
</button>
{/if}
</div>
{/if}
</Grupo>
<style>
.vazio,
.nota {
margin: 0;
color: var(--texto-3);
font-size: var(--t-xs);
}
.vazio {
padding: var(--e-3) var(--e-4);
}
.bloco {
display: grid;
gap: var(--e-2);
padding: var(--e-3) var(--e-4);
border-top: 1px solid var(--borda);
}
h3 {
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: var(--e-2);
margin: 0;
font-size: var(--t-xs);
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--texto-3);
}
h3 span {
font-weight: 400;
letter-spacing: 0;
text-transform: none;
}
.rotina {
display: grid;
grid-template-columns: repeat(24, minmax(0, 1fr));
align-items: end;
gap: 2px;
height: 44px;
}
.hora {
position: relative;
display: flex;
align-items: flex-end;
height: 100%;
border-radius: 3px;
background: var(--superficie-2);
}
.barra {
display: block;
width: 100%;
height: calc(var(--v) * 100%);
min-height: 2px;
border-radius: 3px;
background: #30b0c7;
transition: height 600ms ease;
}
.sem .barra {
background: transparent;
}
.agora {
outline: 1.5px solid var(--texto-2);
outline-offset: 1px;
}
.lua {
position: absolute;
top: 1px;
left: 50%;
transform: translateX(-50%);
font-size: 10px;
line-height: 1;
color: #5e5ce6;
}
.eixo {
display: flex;
justify-content: space-between;
color: var(--texto-3);
font-size: 10px;
}
.colunas {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
gap: var(--e-2) var(--e-4);
}
.colunas ul {
display: grid;
gap: 4px;
margin: 0;
padding: 0;
list-style: none;
font-size: var(--t-sm);
}
.colunas li {
display: flex;
justify-content: space-between;
gap: var(--e-2);
color: var(--texto-2);
}
.colunas b {
font-weight: 600;
font-size: var(--t-xs);
font-variant-numeric: tabular-nums;
}
.mais b {
color: var(--ok, #34c759);
}
.menos b {
color: var(--texto-3);
}
.colunas .titulo {
color: var(--texto-3);
font-size: var(--t-xs);
}
.nada {
color: var(--texto-3);
font-size: var(--t-xs);
}
.licoes {
display: grid;
gap: 6px;
margin: 0;
padding: 0;
list-style: none;
font-size: var(--t-sm);
}
.licoes li {
display: flex;
gap: var(--e-2);
color: var(--texto);
}
.licoes time {
flex: none;
padding-top: 2px;
color: var(--texto-3);
font-size: var(--t-xs);
}
.esquecer {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--e-2);
padding: var(--e-2) var(--e-3) var(--e-3);
border-top: 1px solid var(--borda);
font-size: var(--t-sm);
color: var(--texto-2);
}
.esquecer span {
padding-left: var(--e-1);
}
@media (prefers-reduced-motion: reduce) {
.barra {
transition: none;
}
}
</style>
@@ -296,6 +296,9 @@
.diario li.emoji {
color: #af52de;
}
.diario li.aprendizado {
color: #30b0c7;
}
.diario li.cuidado {
color: var(--texto);
}