feat(vida): ideias do dia com inspiração da internet e "olha o que eu aprendi"

Todo dia, a partir das 9h, o Carinha cria 3 atividades novas (inspiracao.py):
- sinais de fontes abertas: tempo da cidade (Open-Meteo), datas
  comemorativas da semana (lista fixa + feriados da BrasilAPI), "neste dia"
  da Wikipédia e assuntos em alta no Google Trends Brasil;
- o Gemini escolhe ideias leves (descarta tragédia, política e afins) e
  cada uma vira atividade pelo estúdio (Criacao.gerar), com motivo e uma
  curiosidade;
- até 3 tentativas por dia, 30 min entre elas; botão "Criar mais agora";
- as atividades criadas entram na rotina do modo vida (as de hoje com 40%
  de chance) e, com alguém perto, ele conta a curiosidade: legenda e voz
  de robô (ordem "aprendi", até 2x por dia cada);
- painel: cartão "Ideias do dia" no estúdio (Mostrar, Contar, o que ele
  viu na internet, ajustes: quantas, horário, cidade, fontes) e tópico no
  guia.

Correções achadas no caminho:
- WebView de fábrica do S10 (Chrome 102) não tem AbortSignal.timeout: o
  celular nunca mandava o status e a placa o via offline;
- arquivos estáticos com Cache-Control no-cache e o app com LOAD_NO_CACHE:
  o WebView seguia com uma carinha.html velha depois de deploy;
- status do celular recusado vai para o journal (1 linha por minuto);
- legenda mais estreita para não cobrir a bateria.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-08 08:13:03 -03:00
co-authored by Claude Opus 5.5
parent 43770c6d67
commit 4777ceb023
19 changed files with 1179 additions and 30 deletions
+262
View File
@@ -0,0 +1,262 @@
<script lang="ts">
import { Lightbulb, MessageCircle, Play, Settings2, Wand2 } from '@lucide/svelte';
import { onMount } from 'svelte';
import { criacao } from '../criacao.svelte';
import { inspiracao, type Fonte } from '../inspiracao.svelte';
import Card from './Card.svelte';
import Selo from './Selo.svelte';
import Toggle from './Toggle.svelte';
// Ideias do dia: o que ele criou hoje inspirado na internet, a curiosidade de cada uma e os ajustes.
onMount(() => inspiracao.carregar());
const st = $derived(inspiracao.status);
const cfg = $derived(st?.config);
let ajustesAbertos = $state(false);
let cidade = $state('');
const ICONES: Record<Fonte, string> = { tempo: '🌦️', datas: '📅', historia: '📜', alta: '📈' };
const HORAS = Array.from({ length: 23 }, (_, h) => h);
const vazio = $derived.by(() => {
if (!st || !cfg) return '';
if (!st.pode) return 'Precisa da chave Gemini (Ajustes › Inteligência artificial).';
if (!cfg.ligado) return 'Desligado: ligue para ele ter ideias novas todo dia.';
const h = new Date().getHours();
return h < cfg.hora ? `As ideias de hoje chegam a partir das ${cfg.hora}h.` : 'Já já: ele está procurando inspiração.';
});
const sinais = $derived(
st ? (Object.keys(st.fontes) as Fonte[]).filter((f) => st.sinais[f]).map((f) => ({ f, valor: st.sinais[f]! })) : [],
);
async function salvarCidade(e: SubmitEvent) {
e.preventDefault();
if (cidade.trim() && (await inspiracao.configurar({ cidade: cidade.trim() }, 'Cidade salva.'))) cidade = '';
}
function fonte(f: Fonte, v: boolean) {
if (cfg) inspiracao.configurar({ fontes: { ...cfg.fontes, [f]: v } });
}
</script>
<Card titulo="Ideias do dia" ajuda="ideias" descricao="Todo dia ele procura inspiração na internet e cria atividades novas, que entram na rotina dele.">
{#snippet acoes()}
<button class="btn pequeno" onclick={() => inspiracao.agora()} disabled={!st?.pode || st?.ocupado}>
<Wand2 size={14} /> {st?.ocupado ? 'Criando…' : 'Criar mais agora'}
</button>
{/snippet}
{#if !st || !cfg}
<p class="fraco">Carregando…</p>
{:else}
{#if st.ocupado}
<Selo tom="aviso">Procurando ideias na internet e criando as atividades…</Selo>
{:else if st.erro}
<p class="erro pequeno-texto">{st.erro}{st.tentativas ? ` (tentativa ${st.tentativas} de 3; tenta de novo em 30 min)` : ''}</p>
{/if}
{#if st.ideias.length}
<ul class="ideias">
{#each st.ideias as i (i.id)}
{@const contou = st.contadas[String(i.id)] ?? 0}
<li class="ideia">
<span class="emoji" aria-hidden="true">{i.emoji}</span>
<div class="texto">
<strong>{i.nome}</strong>
{#if i.motivo}<span class="motivo">{i.motivo}</span>{/if}
{#if i.aprendi}<p class="aprendi"><Lightbulb size={13} /> {i.aprendi}</p>{/if}
</div>
<div class="botoes">
<button class="btn pequeno" onclick={() => criacao.tocar(i.id)} title="Mostrar a atividade no celular"><Play size={14} /> Mostrar</button>
{#if i.aprendi}
<button class="btn pequeno fantasma" onclick={() => inspiracao.contar(i.id)} title="Ele conta a curiosidade agora">
<MessageCircle size={14} /> Contar{contou ? ` (${contou}×)` : ''}
</button>
{/if}
</div>
</li>
{/each}
</ul>
{:else}
<p class="fraco">{vazio}</p>
{/if}
{#if sinais.length}
<details class="dobra">
<summary>O que ele viu hoje na internet</summary>
<ul class="sinais">
{#each sinais as s (s.f)}
<li>
<span class="fonte">{ICONES[s.f]} {st.fontes[s.f]}</span>
<span class="valor">{Array.isArray(s.valor) ? s.valor.slice(0, 8).join(' · ') : s.valor}</span>
</li>
{/each}
</ul>
</details>
{/if}
<button class="btn fantasma pequeno ajustes" onclick={() => (ajustesAbertos = !ajustesAbertos)} aria-expanded={ajustesAbertos}>
<Settings2 size={14} /> Ajustes das ideias
</button>
{#if ajustesAbertos}
<div class="config">
<Toggle rotulo="Ter ideias novas todo dia" checked={cfg.ligado} onchange={(v) => inspiracao.configurar({ ligado: v })} />
<Toggle
rotulo="Olha o que eu aprendi hoje"
ajuda="Conta a curiosidade de cada ideia quando tem alguém perto"
checked={cfg.aprendi}
onchange={(v) => inspiracao.configurar({ aprendi: v })}
/>
<div class="linha-campos">
<label class="campo">
<span>Ideias por dia</span>
<select value={cfg.por_dia} onchange={(e) => inspiracao.configurar({ por_dia: Number(e.currentTarget.value) })}>
{#each [1, 2, 3, 4, 5] as n (n)}<option value={n}>{n}</option>{/each}
</select>
</label>
<label class="campo">
<span>A partir de</span>
<select value={cfg.hora} onchange={(e) => inspiracao.configurar({ hora: Number(e.currentTarget.value) })}>
{#each HORAS as h (h)}<option value={h}>{h}h</option>{/each}
</select>
</label>
</div>
<form class="campo" onsubmit={salvarCidade}>
<span>Cidade do tempo: <strong>{cfg.cidade.nome}</strong></span>
<div class="linha">
<input type="text" bind:value={cidade} placeholder="Trocar cidade (ex.: Curitiba)" maxlength="60" />
<button class="btn pequeno" type="submit" disabled={!cidade.trim()}>Salvar</button>
</div>
</form>
<fieldset class="campo">
<legend>De onde tirar inspiração</legend>
{#each Object.entries(st.fontes) as [f, nome] (f)}
<Toggle rotulo={`${ICONES[f as Fonte]} ${nome}`} checked={cfg.fontes[f as Fonte]} onchange={(v) => fonte(f as Fonte, v)} />
{/each}
</fieldset>
</div>
{/if}
{/if}
</Card>
<style>
.ideias {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: var(--e-2);
}
.ideia {
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
gap: var(--e-3);
align-items: center;
padding: var(--e-3);
border: 1px solid var(--borda);
border-radius: var(--raio);
background: var(--superficie-2);
}
@media (max-width: 640px) {
.ideia {
grid-template-columns: auto minmax(0, 1fr);
}
.botoes {
grid-column: 1 / -1;
}
}
.emoji {
font-size: 1.8rem;
line-height: 1;
}
.texto {
display: grid;
gap: 2px;
}
.motivo {
font-size: var(--t-xs);
color: var(--texto-3);
}
.aprendi {
margin: var(--e-1) 0 0;
font-size: var(--t-sm);
color: var(--texto-2);
font-style: italic;
}
.aprendi :global(svg) {
vertical-align: -2px;
color: var(--aviso);
}
.botoes {
display: flex;
flex-wrap: wrap;
gap: var(--e-2);
justify-content: flex-end;
}
.dobra {
margin-top: var(--e-3);
font-size: var(--t-sm);
}
.dobra summary {
cursor: pointer;
color: var(--texto-2);
}
.sinais {
list-style: none;
margin: var(--e-2) 0 0;
padding: 0;
display: grid;
gap: var(--e-2);
}
.sinais li {
display: grid;
gap: 2px;
}
.fonte {
font-weight: 600;
font-size: var(--t-xs);
}
.valor {
font-size: var(--t-xs);
color: var(--texto-3);
overflow-wrap: anywhere;
}
.ajustes {
margin-top: var(--e-3);
}
.config {
display: grid;
gap: var(--e-2);
margin-top: var(--e-2);
padding-top: var(--e-3);
border-top: 1px solid var(--borda);
}
.linha-campos {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: var(--e-3);
}
.campo {
display: grid;
gap: var(--e-1);
font-size: var(--t-sm);
}
fieldset.campo {
border: 0;
margin: 0;
padding: 0;
}
legend {
padding: 0;
margin-bottom: var(--e-1);
}
.linha {
display: flex;
gap: var(--e-2);
}
.linha input {
flex: 1;
min-width: 0;
}
.erro {
color: var(--perigo);
}
</style>