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:
@@ -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
@@ -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',
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 ?? '';
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user