feat(vida): página "Dia a dia" com as decisões e o porquê

Cada decisão tomada com base no que ele aprendeu passa a ser guardada
com o motivo (aprendizado.decidir, até 800, persistidas):
- acordou do cochilo antes da hora: "às 18h de quarta costuma ter
  gente, pelo plano que você contou";
- cochilo com a casa vazia, com a duração e o porquê;
- sensor segurado numa hora em que acordou à toa (no máximo 1 nota a
  cada 10 min);
- gesto escolhido porque costuma agradar (chance ≥ ×1,3);
- fechamento do dia: horas com gente, carinhos e quanto o dia conferiu
  com o plano;
- análise da IA (adotada ou sugestão, com a porcentagem) e rotina
  contada por você.

esperado_e_fonte diz de onde veio a expectativa (plano seu, plano da
IA, o que viu naquele dia da semana ou média). GET /aprendizado/dia
devolve um dia: medido × plano por hora, conferência do dia, resumo e
decisões.

Painel: tela "Dia a dia" (#/dia-a-dia) com navegação entre dias, os
números do dia, as faixas "Viu" × "Esperava" e a linha do tempo das
decisões; atalho no cartão "O que ele aprendeu", no Ctrl+K e no guia.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-09 17:34:30 -03:00
co-authored by Claude Opus 5.5
parent 935bf8d586
commit b4cf13df11
10 changed files with 553 additions and 13 deletions
+2
View File
@@ -20,6 +20,7 @@
import Eventos from './telas/Eventos.svelte';
import Guia from './telas/Guia.svelte';
import Criar from './telas/Criar.svelte';
import DiaADia from './telas/DiaADia.svelte';
import Ajuda from './lib/componentes/Ajuda.svelte';
import { TOPICO_DA_TELA } from './lib/guia';
@@ -33,6 +34,7 @@
rosto: Rosto,
guia: Guia,
criar: Criar,
'dia-a-dia': DiaADia,
};
const ICONES: Record<string, Component<any>> = {
'ao-vivo': Video,
@@ -1,5 +1,5 @@
<script lang="ts">
import { CalendarClock, GraduationCap } from '@lucide/svelte';
import { CalendarClock, CalendarDays, GraduationCap } from '@lucide/svelte';
import {
analisarSemana,
apagarPlano,
@@ -9,6 +9,7 @@
usarSugestao,
} from '../aprendizado.svelte';
import { estado } from '../estado.svelte';
import { nav } from '../rotas.svelte';
import type { PlanoSemana, TipoPeriodo } from '../tipos';
import Grupo from './Grupo.svelte';
import Interruptor from './Interruptor.svelte';
@@ -97,6 +98,15 @@
</Linha>
{#if a.config.ligado}
<Linha
icone={CalendarDays}
cor="#5e5ce6"
rotulo="Ver dia a dia"
detalhe={a.decisoes_hoje
? `${a.decisoes_hoje} ${a.decisoes_hoje === 1 ? 'decisão' : 'decisões'} hoje, com o porquê`
: 'O que ele viu, o que esperava e as decisões de cada dia'}
onclick={() => nav.ir('dia-a-dia')}
/>
<section class="bloco" aria-labelledby="ap-semana">
<div class="cabeca">
<h3 id="ap-semana">Semana da casa</h3>
+2 -1
View File
@@ -69,6 +69,7 @@ export const TOPICOS: Record<string, TopicoGuia> = {
{ emoji: '🏠', nome: 'O que muda', texto: 'Quando a próxima hora é de gente em casa e a atual não, ele acorda do cochilo 15 minutos antes, para estar de pé quando você chegar. Com a casa vazia, aproveita para tirar cochilos longos e guardar energia. Sono pesado e madrugada continuam respeitados.' },
{ emoji: '☾', nome: 'Acordar à toa', texto: 'Se o PIR acorda o rosto e ninguém fica nem interage em 3 minutos, foi à toa. Quando isso se repete na mesma hora em dias diferentes, ali ele passa a precisar de 1 ou 2 disparos a mais para acordar.' },
{ emoji: '📖', nome: 'Lições', texto: 'O que ele concluiu em cada dia, em primeira pessoa. Também aparecem no diário do cartão Vida.' },
{ emoji: '📅', nome: 'Dia a dia', texto: 'Página com um dia por vez: o que ele viu em cada hora, o que esperava pelo plano, quanto o dia conferiu, e cada decisão tomada com base no que aprendeu, com o porquê ("acordou às 17h45: às 18h de quarta costuma ter gente"). Abre pelo botão "Ver dia a dia" no cartão ou pelo Ctrl+K.' },
{ emoji: '🧽', nome: 'Esquecer', texto: 'Apaga a semana, o plano, os gostos e as lições, e ele começa do zero. Desligar o aprendizado só para de usar o que aprendeu, sem apagar.' },
],
dica: 'Para ensinar rápido: quando ele fizer algo que você gostou, toque na tela do celular (vale como carinho) ou fale com ele logo em seguida.',
@@ -360,5 +361,5 @@ export const ORDEM_GUIA = [
/** Tópico de cada tela, para o ⓘ do topo. */
export const TOPICO_DA_TELA: Record<string, string> = {
'ao-vivo': 'inicio', eventos: 'eventos', conversa: 'conversa', reacoes: 'reacoes',
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar',
movimento: 'movimento', ajustes: 'tela', rosto: 'aparencia', guia: 'inicio', criar: 'criar', 'dia-a-dia': 'aprendizado',
};
+1
View File
@@ -37,6 +37,7 @@ export function itensDaPaleta(): ItemPaleta[] {
registrarNaPaleta(() => [
...ROTAS.map((r) => ({ id: 'ir-' + r.id, grupo: 'Ir para', titulo: r.titulo, fazer: () => nav.ir(r.id) })),
{ id: 'ir-rosto', grupo: 'Ir para', titulo: 'Rosto (estilo, formato e cor)', fazer: () => nav.ir('rosto') },
{ id: 'ir-dia-a-dia', grupo: 'Ir para', titulo: 'Dia a dia (rotina e decisões do Carinha)', fazer: () => nav.ir('dia-a-dia') },
{ id: 'ir-criar', grupo: 'Ir para', titulo: 'Criar com IA (expressões, atividades, figurinhas)', fazer: () => nav.ir('criar') },
...Object.entries(ATIVIDADES_INFO).map(([id, a]) => ({
id: 'atividade-' + id, grupo: 'Atividades', titulo: a.emoji + ' ' + a.nome, fazer: () => atividade(id),
+2 -1
View File
@@ -1,6 +1,6 @@
/** Rotas por hash (#/conversa): o servidor só precisa entregar index.html em "/". */
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar';
export type Rota = 'ao-vivo' | 'eventos' | 'conversa' | 'reacoes' | 'movimento' | 'ajustes' | 'rosto' | 'guia' | 'criar' | 'dia-a-dia';
export const ROTAS: { id: Rota; titulo: string; atalho: string }[] = [
{ id: 'ao-vivo', titulo: 'Ao vivo', atalho: 'g v' },
@@ -16,6 +16,7 @@ export const SECUNDARIAS: { id: Rota; titulo: string; volta: Rota }[] = [
{ id: 'rosto', titulo: 'Rosto', volta: 'ao-vivo' },
{ id: 'guia', titulo: 'Guia', volta: 'ajustes' },
{ id: 'criar', titulo: 'Criar com IA', volta: 'reacoes' },
{ id: 'dia-a-dia', titulo: 'Dia a dia', volta: 'ao-vivo' },
];
export const tituloDa = (id: Rota) => [...ROTAS, ...SECUNDARIAS].find((r) => r.id === id)?.titulo ?? '';
+19
View File
@@ -124,6 +124,7 @@ export interface Aprendizado {
/** 7 dias × 24 horas: chance de ter gente por perto (null = sem dados). */
semana: (number | null)[][];
historico_dias: number;
decisoes_hoje: number;
plano: PlanoSemana | null;
/** Plano da IA que não bateu com os dados (ou que não substitui o que você contou). */
sugestao: PlanoSemana | null;
@@ -139,3 +140,21 @@ export interface Aprendizado {
licoes: { dia: string; texto: string }[];
hoje: { dia: string; atos: number; respostas: Record<string, number>; acordou: number; a_toa: number };
}
/** Um dia visto pelo aprendizado: o que mediu, o que esperava pelo plano e as decisões com o porquê. */
export interface DiaADia {
dia: string;
sem: number;
nome_dia: string;
hoje: boolean;
/** 24 horas: fração do tempo com gente (null = sem dados). */
medido: (number | null)[];
/** Períodos do plano para esse dia da semana (null = sem plano). */
plano: { ini: number; fim: number; tipo: TipoPeriodo }[] | null;
origem_plano: 'ia' | 'voce' | null;
confere: number | null;
horas_com_gente: number;
resumo: { atos: number; respostas: number; a_toa: number } | null;
decisoes: { horario: number; tipo: 'acordar' | 'cochilo' | 'sono' | 'gosto' | 'dia' | 'ia' | 'voce'; texto: string; motivo: string }[];
dias: string[];
}
+329
View File
@@ -0,0 +1,329 @@
<script lang="ts">
import { ChevronLeft, ChevronRight } from '@lucide/svelte';
import { onMount } from 'svelte';
import { pedir } from '../lib/api';
import { avisos } from '../lib/avisos.svelte';
import Card from '../lib/componentes/Card.svelte';
import { estado, ouvir } from '../lib/estado.svelte';
import type { DiaADia, TipoPeriodo } from '../lib/tipos';
// Um dia por vez: o que ele viu em cada hora, o que esperava pelo plano, quanto conferiu
// e cada decisão tomada com base no que aprendeu, com o porquê.
let dia = $state<string | null>(null);
let dados = $state<DiaADia | null>(null);
let carregando = $state(false);
const ICONE: Record<DiaADia['decisoes'][number]['tipo'], string> = {
acordar: '☀️', cochilo: '💤', sono: '☾', gosto: '❤️', dia: '📅', ia: '🧠', voce: '✍️',
};
const NOME_TIPO: Record<TipoPeriodo, string> = { casa: 'em casa', fora: 'casa vazia', dormindo: 'dormindo' };
async function carregar(alvo: string | null = dia) {
carregando = true;
try {
dados = await pedir<DiaADia>('/aprendizado/dia' + (alvo ? '?dia=' + alvo : ''));
dia = dados.dia;
} catch (e) {
avisos.erro(e);
} finally {
carregando = false;
}
}
onMount(() => {
carregar(null);
// o dia de hoje vai mudando: recarrega quando o aprendizado avisa
return ouvir('aprendizado', () => {
if (dados?.hoje) carregar(dados.dia);
});
});
const indice = $derived(dados ? dados.dias.indexOf(dados.dia) : -1);
const anterior = $derived(dados && indice > 0 ? dados.dias[indice - 1] : null);
const proximo = $derived(dados && indice >= 0 && indice < dados.dias.length - 1 ? dados.dias[indice + 1] : null);
const decisoes = $derived(dados ? [...dados.decisoes].sort((a, b) => a.horario - b.horario) : []);
const horaAgora = $derived(new Date(estado.agora).getHours());
function titulo(d: DiaADia) {
const [, m, dd] = d.dia.split('-');
return `${d.nome_dia[0].toUpperCase() + d.nome_dia.slice(1)}, ${dd}/${m}`;
}
function hora(s: number) {
return new Date(s * 1000).toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
}
function tipoNaHora(h: number): TipoPeriodo | null {
return dados?.plano?.find((p) => p.ini <= h && h < p.fim)?.tipo ?? null;
}
function plural(n: number, um: string, varios: string) {
return `${n} ${n === 1 ? um : varios}`;
}
</script>
<div class="pagina">
<Card>
<nav class="dias" aria-label="Escolher o dia">
<button class="btn fantasma icone" onclick={() => carregar(anterior)} disabled={!anterior || carregando} aria-label="Dia anterior">
<ChevronLeft size={18} />
</button>
<div class="qual">
{#if dados}
<strong>{titulo(dados)}</strong>
<span>{dados.hoje ? 'hoje, até agora' : 'dia fechado'}</span>
{:else}
<strong>Carregando…</strong>
{/if}
</div>
<button class="btn fantasma icone" onclick={() => carregar(proximo)} disabled={!proximo || carregando} aria-label="Próximo dia">
<ChevronRight size={18} />
</button>
</nav>
{#if dados}
<dl class="numeros">
<div>
<dt>Com gente por perto</dt>
<dd class="mono">{plural(dados.horas_com_gente, 'hora', 'horas')}</dd>
</div>
<div>
<dt>Carinhos e conversas</dt>
<dd class="mono">{dados.resumo ? dados.resumo.respostas : '—'}</dd>
</div>
<div>
<dt>Conferiu com o plano</dt>
<dd class="mono" class:bom={dados.confere !== null && dados.confere >= 0.7} class:ruim={dados.confere !== null && dados.confere < 0.5}>
{dados.confere === null ? '—' : Math.round(dados.confere * 100) + '%'}
</dd>
</div>
</dl>
<section class="faixas" aria-label="O que ele viu e o que esperava, hora a hora">
<span class="rotulo">Viu</span>
<div class="faixa">
{#each dados.medido as v, h (h)}
<span
class="celula"
class:sem={v === null}
class:agora={dados.hoje && h === horaAgora}
style={v === null ? '' : `--v: ${v}`}
title={`${h}h: ${v === null ? 'sem dados' : Math.round(v * 100) + '% do tempo com gente'}`}
></span>
{/each}
</div>
<span class="rotulo">Esperava</span>
<div class="faixa">
{#each Array(24) as _, h (h)}
{@const t = tipoNaHora(h)}
<span class="celula plano {t ?? 'nada'}" title={`${h}h: ${t ? NOME_TIPO[t] : 'sem plano'}`}></span>
{/each}
</div>
<span></span>
<div class="eixo mono" aria-hidden="true">
{#each Array(24) as _, h (h)}<span>{h % 6 === 0 ? h + 'h' : ''}</span>{/each}
</div>
</section>
<p class="legenda">
<span class="ponto visto"></span> gente por perto
<span class="ponto casa"></span> em casa
<span class="ponto fora"></span> casa vazia
<span class="ponto dormindo"></span> dormindo
{#if !dados.plano}<em>· sem plano ainda: conte sua rotina no cartão "O que ele aprendeu"</em>{/if}
</p>
{/if}
</Card>
<Card titulo="Decisões" ajuda="aprendizado" descricao="O que ele fez por causa do que aprendeu, e por quê.">
{#if decisoes.length}
<ol class="decisoes">
{#each decisoes as d (d.horario + d.texto)}
<li class={d.tipo}>
<time class="mono">{hora(d.horario)}</time>
<span class="icone" aria-hidden="true">{ICONE[d.tipo] ?? '•'}</span>
<div>
<strong>{d.texto}</strong>
<span>{d.motivo}</span>
</div>
</li>
{/each}
</ol>
{:else}
<p class="vazio">
Nenhuma decisão neste dia. Elas aparecem quando ele usa o que aprendeu: acordar antes de você chegar, cochilar
com a casa vazia, segurar o sensor numa hora em que acorda à toa, escolher um gesto que vocês gostam. À
meia-noite ele fecha o dia aqui.
</p>
{/if}
</Card>
</div>
<style>
.pagina {
display: grid;
gap: var(--e-4);
max-width: 760px;
}
.dias {
display: flex;
align-items: center;
gap: var(--e-2);
}
.qual {
display: grid;
flex: 1;
text-align: center;
}
.qual strong {
font-size: var(--t-md);
}
.qual span {
color: var(--texto-3);
font-size: var(--t-xs);
}
.numeros {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--e-2);
margin: var(--e-3) 0 0;
}
.numeros div {
display: grid;
gap: 2px;
padding: var(--e-2) var(--e-3);
border-radius: var(--raio-sm);
background: var(--superficie-2);
}
.numeros dt {
color: var(--texto-3);
font-size: var(--t-xs);
}
.numeros dd {
margin: 0;
font-size: var(--t-md);
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.numeros dd.bom {
color: var(--ok);
}
.numeros dd.ruim {
color: var(--perigo);
}
.faixas {
display: grid;
grid-template-columns: 4.6em 1fr;
align-items: center;
gap: 4px var(--e-2);
margin-top: var(--e-3);
}
.rotulo {
color: var(--texto-3);
font-size: var(--t-xs);
}
.faixa,
.eixo {
display: grid;
grid-template-columns: repeat(24, minmax(0, 1fr));
gap: 2px;
}
.celula {
height: 18px;
border-radius: 2px;
background: color-mix(in srgb, #30b0c7 calc(var(--v, 0) * 100%), var(--superficie-2));
}
.celula.sem {
background: var(--superficie-2);
opacity: 0.5;
}
.celula.agora {
outline: 1.5px solid var(--texto);
outline-offset: 1px;
}
.celula.plano.casa,
.ponto.casa {
background: #30b0c7;
}
.celula.plano.fora,
.ponto.fora {
background: color-mix(in srgb, var(--texto-3) 35%, var(--superficie-2));
}
.celula.plano.dormindo,
.ponto.dormindo {
background: #5e5ce6;
}
.celula.plano.nada {
background: var(--superficie-2);
opacity: 0.5;
}
.ponto.visto {
background: color-mix(in srgb, #30b0c7 60%, var(--superficie-2));
}
.eixo span {
font-size: 9px;
color: var(--texto-3);
white-space: nowrap;
}
.legenda {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 4px var(--e-2);
margin: var(--e-2) 0 0;
color: var(--texto-3);
font-size: var(--t-xs);
}
.legenda em {
font-style: normal;
}
.ponto {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 2px;
}
.decisoes {
display: grid;
gap: var(--e-3);
margin: 0;
padding: 0;
list-style: none;
}
.decisoes li {
display: grid;
grid-template-columns: 3.2em 1.4em 1fr;
align-items: start;
gap: var(--e-2);
}
.decisoes time {
padding-top: 2px;
color: var(--texto-3);
font-size: var(--t-xs);
}
.decisoes .icone {
text-align: center;
}
.decisoes div {
display: grid;
gap: 2px;
min-width: 0;
}
.decisoes strong {
font-size: var(--t-sm);
font-weight: 600;
}
.decisoes div span {
color: var(--texto-2);
font-size: var(--t-sm);
}
.decisoes li.dia strong,
.decisoes li.ia strong {
color: #30b0c7;
}
.vazio {
margin: 0;
color: var(--texto-3);
font-size: var(--t-sm);
}
@media (max-width: 480px) {
.numeros dd {
font-size: var(--t-sm);
}
}
</style>