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>
|
||||
@@ -18,6 +18,9 @@ export interface ItemCriado {
|
||||
receita: any;
|
||||
ia: IaCriacao;
|
||||
pedido: string;
|
||||
origem?: 'pedido' | 'dia';
|
||||
motivo?: string;
|
||||
aprendi?: string;
|
||||
}
|
||||
|
||||
export interface Provedor {
|
||||
|
||||
@@ -117,6 +117,8 @@ const tratadores: Record<string, (d: any) => void> = {
|
||||
expressao: (d: { ordem: string }) => {
|
||||
// 'humor x' é o modo vida avisando o celular; aparece no cartão Vida, não no feed
|
||||
if (d.ordem === 'calma' || /^(humor|ritmo|zanga|sono) /.test(d.ordem)) return;
|
||||
// contar o que aprendeu já aparece no diário do cartão Vida
|
||||
if (d.ordem.startsWith('aprendi ')) return;
|
||||
const criada = /^(expressao|atividade)_criada (.*)$/.exec(d.ordem);
|
||||
if (criada) {
|
||||
let nome = '';
|
||||
@@ -183,7 +185,7 @@ function abrirEventos() {
|
||||
if (estado.conexao !== 'token') estado.conexao = 'caiu';
|
||||
};
|
||||
for (const tipo of ['expressao', 'telemetria', 'app_config', 'aparencia', 'motores', 'estop', 'conversa', 'ia',
|
||||
'reacao', 'treinamento', 'historico', 'pir', 'vida', 'criacao']) {
|
||||
'reacao', 'treinamento', 'historico', 'pir', 'vida', 'criacao', 'inspiracao']) {
|
||||
fonte.addEventListener(tipo, (e) => {
|
||||
const dados = JSON.parse((e as MessageEvent).data);
|
||||
tratadores[tipo]?.(dados);
|
||||
|
||||
+16
-1
@@ -196,6 +196,21 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
{ emoji: '⭐', nome: 'Favoritos', texto: 'A estrela põe expressões e atividades criadas na grade de favoritos do Ao vivo (e nas teclas 1 a 9).' },
|
||||
],
|
||||
},
|
||||
ideias: {
|
||||
titulo: 'Ideias do dia',
|
||||
emoji: '💡',
|
||||
onde: 'Criar com IA › Ideias do dia',
|
||||
resumo:
|
||||
'Todo dia, a partir do horário escolhido, ele procura inspiração na internet e o Gemini cria atividades novas (3 por padrão). A IA descarta tragédias, política e afins: só entram temas leves. As novidades entram na rotina do modo vida, as de hoje com mais chance, e ficam na grade do estúdio para tocar, favoritar ou apagar.',
|
||||
itens: [
|
||||
{ emoji: '🌦️', nome: 'Tempo da cidade', texto: 'Chuva, sol, frio e calor de hoje na cidade escolhida (Open-Meteo). Ex.: dia de chuva vira "lendo com chá".' },
|
||||
{ emoji: '📅', nome: 'Datas comemorativas', texto: 'Feriados (BrasilAPI) e datas como Dia das Crianças, Dia da Pizza, Halloween e Natal, de hoje e dos próximos dias.' },
|
||||
{ emoji: '📜', nome: 'Neste dia na história', texto: 'Fatos que aconteceram neste dia, da Wikipédia em português.' },
|
||||
{ emoji: '📈', nome: 'Assuntos em alta', texto: 'O que o pessoal está procurando hoje no Google Brasil. Política e notícias tristes são ignoradas.' },
|
||||
{ emoji: '🤓', nome: 'Olha o que eu aprendi hoje', texto: 'Cada ideia vem com uma curiosidade. Quando tem alguém perto, ele conta, com legenda e voz de robô (até 2 vezes por dia cada uma). O botão "Contar" faz ele contar na hora.' },
|
||||
{ emoji: '💳', nome: 'Custo', texto: 'Uma chamada ao Gemini para escolher as ideias e uma por atividade: 4 por dia com o padrão de 3.' },
|
||||
],
|
||||
},
|
||||
personalidade: {
|
||||
titulo: 'IA local (Qwen)',
|
||||
emoji: '🧠',
|
||||
@@ -319,7 +334,7 @@ export const TOPICOS: Record<string, TopicoGuia> = {
|
||||
|
||||
/** Ordem do tutorial (página Guia). */
|
||||
export const ORDEM_GUIA = [
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'aparencia',
|
||||
'inicio', 'vida', 'cuidados', 'humores', 'ritmo', 'sono', 'celular', 'camera', 'gestos', 'atividades', 'criar', 'ideias', 'aparencia',
|
||||
'personalidade', 'acontecimentos', 'eventos', 'presenca', 'tela', 'sensores', 'sensibilidade',
|
||||
'reacoes', 'conversa', 'movimento', 'atalhos',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Ideias do dia (/criacao/dia): todo dia o Carinha cria atividades novas inspiradas no tempo, nas
|
||||
* datas, no "neste dia" da Wikipédia e no que está em alta, e conta o que aprendeu com cada uma.
|
||||
* O estado chega por SSE ('inspiracao') e é recarregado ao abrir a tela.
|
||||
*/
|
||||
import { pedir, postar } from './api';
|
||||
import { avisos, tentar } from './avisos.svelte';
|
||||
import { ouvir } from './estado.svelte';
|
||||
|
||||
export type Fonte = 'tempo' | 'datas' | 'historia' | 'alta';
|
||||
|
||||
export interface ConfigInspiracao {
|
||||
ligado: boolean;
|
||||
por_dia: number;
|
||||
hora: number;
|
||||
aprendi: boolean;
|
||||
cidade: { nome: string; lat: number; lon: number };
|
||||
fontes: Record<Fonte, boolean>;
|
||||
}
|
||||
|
||||
export interface IdeiaDoDia {
|
||||
id: number;
|
||||
nome: string;
|
||||
emoji: string;
|
||||
motivo: string;
|
||||
aprendi: string;
|
||||
horario: number;
|
||||
}
|
||||
|
||||
export interface StatusInspiracao {
|
||||
config: ConfigInspiracao;
|
||||
fontes: Record<Fonte, string>;
|
||||
ocupado: boolean;
|
||||
pode: boolean;
|
||||
dia: string;
|
||||
erro: string;
|
||||
tentativas: number;
|
||||
feito_em: number | null;
|
||||
sinais: Partial<Record<Fonte, string | string[]>>;
|
||||
contadas: Record<string, number>;
|
||||
ideias: IdeiaDoDia[];
|
||||
}
|
||||
|
||||
class Inspiracao {
|
||||
status = $state<StatusInspiracao | null>(null);
|
||||
|
||||
async carregar() {
|
||||
try {
|
||||
this.status = await pedir<StatusInspiracao>('/criacao/dia');
|
||||
} catch (e) {
|
||||
avisos.erro(e);
|
||||
}
|
||||
}
|
||||
|
||||
async configurar(dados: Partial<Omit<ConfigInspiracao, 'cidade'>> & { cidade?: string }, aviso?: string) {
|
||||
const r = await tentar(() => postar<StatusInspiracao>('/criacao/dia/config', dados), aviso);
|
||||
if (r) this.status = r;
|
||||
return !!r;
|
||||
}
|
||||
|
||||
agora() {
|
||||
return tentar(() => postar('/criacao/dia/agora', {}), 'Procurando ideias na internet…');
|
||||
}
|
||||
|
||||
contar(id: number) {
|
||||
return tentar(() => postar('/criacao/dia/contar', { id }));
|
||||
}
|
||||
}
|
||||
|
||||
export const inspiracao = new Inspiracao();
|
||||
ouvir('inspiracao', (d: StatusInspiracao) => {
|
||||
const antes = inspiracao.status;
|
||||
inspiracao.status = d;
|
||||
if (antes?.ocupado && !d.ocupado) {
|
||||
if (d.erro) avisos.erro(new Error(d.erro));
|
||||
else if (d.ideias.length > (antes.ideias?.length ?? 0)) avisos.ok(`Ideias novas: ${d.ideias.slice(antes.ideias.length).map((i) => i.emoji + ' ' + i.nome).join(', ')}`);
|
||||
}
|
||||
});
|
||||
@@ -3,6 +3,7 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { favoritos } from '../lib/comandos.svelte';
|
||||
import Card from '../lib/componentes/Card.svelte';
|
||||
import IdeiasDoDia from '../lib/componentes/IdeiasDoDia.svelte';
|
||||
import RostoPrevia from '../lib/componentes/RostoPrevia.svelte';
|
||||
import Selo from '../lib/componentes/Selo.svelte';
|
||||
import { criacao, TIPOS, type IaCriacao, type TipoCriacao } from '../lib/criacao.svelte';
|
||||
@@ -153,6 +154,8 @@
|
||||
<div class="previa"><RostoPrevia titulo="Prévia das criações" espelharSono={false} /></div>
|
||||
</div>
|
||||
|
||||
<IdeiasDoDia />
|
||||
|
||||
<Card titulo="Criações" descricao="Toque para mostrar no celular. A estrela fixa expressões e atividades nos favoritos do Ao vivo.">
|
||||
{#snippet acoes()}
|
||||
<div class="segmentado pequeno" role="radiogroup" aria-label="Filtrar">
|
||||
@@ -174,7 +177,7 @@
|
||||
<button class="cartao" onclick={() => criacao.tocar(c.id)} title={c.pedido}>
|
||||
<span class="emoji" aria-hidden="true">{c.emoji}</span>
|
||||
<span class="nome">{c.nome}</span>
|
||||
<span class="meta">{nomeTipo(c.tipo)} · {st.provedores[c.ia]?.nome ?? c.ia}</span>
|
||||
<span class="meta">{c.origem === 'dia' ? `💡 ${c.motivo || 'Ideia do dia'}` : `${nomeTipo(c.tipo)} · ${st.provedores[c.ia]?.nome ?? c.ia}`}</span>
|
||||
</button>
|
||||
{#if podeFavoritar(c.tipo)}
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user