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:
@@ -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>
|
||||
Reference in New Issue
Block a user