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>
381 lines
12 KiB
Svelte
381 lines
12 KiB
Svelte
<script lang="ts">
|
|
import { Cpu, Settings2, Sparkles, Star, Trash2, Wand2 } from '@lucide/svelte';
|
|
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';
|
|
import { estado } from '../lib/estado.svelte';
|
|
|
|
// Estúdio: escolhe o que criar e com qual IA, escreve o pedido (pode partir de algo que já existe)
|
|
// e as criações ficam numa grade igual à dos favoritos.
|
|
onMount(() => criacao.carregar());
|
|
const st = $derived(criacao.status);
|
|
|
|
let tipo = $state<TipoCriacao>('expressao');
|
|
let ia = $state<IaCriacao>('gemini');
|
|
let pedido = $state('');
|
|
let base = $state('');
|
|
let filtro = $state<'todos' | TipoCriacao>('todos');
|
|
let configAberta = $state(false);
|
|
let url = $state('');
|
|
let modelo = $state('');
|
|
let chave = $state('');
|
|
|
|
$effect(() => {
|
|
// base só vale para o tipo atual
|
|
tipo;
|
|
base = '';
|
|
});
|
|
$effect(() => {
|
|
const l = st?.provedores.local;
|
|
if (l && !url && !modelo) {
|
|
url = l.url ?? '';
|
|
modelo = l.modelo ?? '';
|
|
}
|
|
});
|
|
|
|
const IAS: { id: IaCriacao; icone: string }[] = [
|
|
{ id: 'gemini', icone: '✨' },
|
|
{ id: 'local', icone: '🖥️' },
|
|
{ id: 's10', icone: '📱' },
|
|
];
|
|
const tipoInfo = $derived(TIPOS.find((t) => t.id === tipo)!);
|
|
const bases = $derived(
|
|
tipo === 'figurinha' || !st
|
|
? []
|
|
: [
|
|
...st.bases[tipo].map((b) => ({ id: b.id, rotulo: `${b.emoji} ${b.nome}` })),
|
|
...st.criacoes.filter((c) => c.tipo === tipo).map((c) => ({ id: `criado:${c.id}`, rotulo: `${c.emoji} ${c.nome} (criado)` })),
|
|
],
|
|
);
|
|
const lista = $derived((st?.criacoes ?? []).filter((c) => filtro === 'todos' || c.tipo === filtro));
|
|
const ocupado = $derived(st?.ocupado ?? null);
|
|
const segundos = $derived(ocupado ? Math.max(0, Math.round(estado.agora / 1000 - ocupado.desde)) : 0);
|
|
const provedor = $derived(st?.provedores[ia]);
|
|
|
|
async function criar(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
if (await criacao.criar(tipo, pedido.trim(), ia, base)) pedido = '';
|
|
}
|
|
async function salvarLocal(e: SubmitEvent) {
|
|
e.preventDefault();
|
|
const dados: { url: string; modelo: string; chave?: string } = { url: url.trim(), modelo: modelo.trim() };
|
|
if (chave.trim()) dados.chave = chave.trim();
|
|
await criacao.configurar(dados);
|
|
chave = '';
|
|
}
|
|
const nomeTipo = (t: TipoCriacao) => TIPOS.find((x) => x.id === t)?.nome ?? t;
|
|
const podeFavoritar = (t: TipoCriacao) => t !== 'figurinha';
|
|
</script>
|
|
|
|
<div class="criar">
|
|
<div class="topo">
|
|
<Card titulo="Criar com IA" ajuda="criar" descricao="A IA monta a criação só com peças que o Carinha já sabe animar, usando exemplos do que já existe.">
|
|
<form class="form" onsubmit={criar}>
|
|
<div class="campo">
|
|
<span>O que criar</span>
|
|
<div class="segmentado" role="radiogroup" aria-label="O que criar">
|
|
{#each TIPOS as t (t.id)}
|
|
<button type="button" role="radio" aria-checked={tipo === t.id} onclick={() => (tipo = t.id)}>
|
|
<span aria-hidden="true">{t.emoji}</span> {t.nome}
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="campo">
|
|
<span>Qual IA</span>
|
|
<div class="ias" role="radiogroup" aria-label="Qual IA">
|
|
{#each IAS as x (x.id)}
|
|
{@const p = st?.provedores[x.id]}
|
|
<button type="button" role="radio" class="ia" aria-checked={ia === x.id} class:indisponivel={p && !p.pronto} onclick={() => (ia = x.id)}>
|
|
<span class="ia-nome"><span aria-hidden="true">{x.icone}</span> {p?.nome ?? x.id}</span>
|
|
<span class="ia-detalhe">{p ? (p.pronto ? p.detalhe : p.motivo) : '…'}</span>
|
|
</button>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
|
|
<label class="campo">
|
|
<span>Pedido</span>
|
|
<textarea bind:value={pedido} maxlength="600" placeholder={tipoInfo.exemplo} required></textarea>
|
|
</label>
|
|
|
|
{#if bases.length}
|
|
<label class="campo">
|
|
<span>Basear em (opcional)</span>
|
|
<select bind:value={base}>
|
|
<option value="">Nada: criar do zero (com exemplos do que existe)</option>
|
|
{#each bases as b (b.id)}<option value={b.id}>{b.rotulo}</option>{/each}
|
|
</select>
|
|
</label>
|
|
{/if}
|
|
|
|
<div class="linha acoes">
|
|
<button class="btn primario" type="submit" disabled={!!ocupado || !pedido.trim() || !provedor?.pronto}>
|
|
<Wand2 size={16} /> {ocupado ? 'Criando…' : `Criar ${nomeTipo(tipo).toLowerCase()}`}
|
|
</button>
|
|
<button class="btn fantasma pequeno" type="button" onclick={() => (configAberta = !configAberta)}>
|
|
<Settings2 size={14} /> IA local
|
|
</button>
|
|
</div>
|
|
{#if ocupado}
|
|
<Selo tom="aviso">
|
|
{st?.provedores[ocupado.ia]?.nome} criando {nomeTipo(ocupado.tipo).toLowerCase()}… {segundos}s
|
|
{ocupado.ia !== 'gemini' ? '(IA local pode levar alguns minutos)' : ''}
|
|
</Selo>
|
|
{:else if st?.erro}
|
|
<p class="erro pequeno-texto">{st.erro}</p>
|
|
{/if}
|
|
</form>
|
|
|
|
{#if configAberta}
|
|
<form class="form local" onsubmit={salvarLocal}>
|
|
<p class="fraco pequeno-texto">
|
|
Servidor compatível com OpenAI na sua rede (LM Studio com "Serve on Local Network" ou llama-server), por exemplo o ROG Ally.
|
|
</p>
|
|
<label class="campo"><span>Endereço</span><input type="text" bind:value={url} placeholder="http://192.168.1.50:1234" /></label>
|
|
<label class="campo"><span>Modelo</span><input type="text" bind:value={modelo} placeholder="qwen3-coder-30b-a3b-instruct" /></label>
|
|
<label class="campo">
|
|
<span>Senha da API {st?.provedores.local.chave ? '(deixe vazio para manter)' : '(opcional)'}</span>
|
|
<input type="password" bind:value={chave} autocomplete="off" />
|
|
</label>
|
|
<div class="linha">
|
|
<button class="btn pequeno primario" type="submit">Salvar</button>
|
|
<button class="btn pequeno" type="button" onclick={() => criacao.testar('local')} disabled={!st?.provedores.local.url}><Cpu size={14} /> Testar</button>
|
|
</div>
|
|
</form>
|
|
{/if}
|
|
</Card>
|
|
|
|
<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">
|
|
<button role="radio" aria-checked={filtro === 'todos'} onclick={() => (filtro = 'todos')}>Todas</button>
|
|
{#each TIPOS as t (t.id)}
|
|
<button role="radio" aria-checked={filtro === t.id} onclick={() => (filtro = t.id)}>{t.emoji}</button>
|
|
{/each}
|
|
</div>
|
|
{/snippet}
|
|
{#if !st}
|
|
<p class="fraco">Carregando…</p>
|
|
{:else if !lista.length}
|
|
<p class="fraco">Nada criado ainda. Escreva um pedido acima. <Sparkles size={14} /></p>
|
|
{:else}
|
|
<div class="grade">
|
|
{#each lista as c (c.id)}
|
|
{@const fav = favoritos.lista.includes(`criado:${c.id}`)}
|
|
<div class="item">
|
|
<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">{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
|
|
class="canto estrela"
|
|
class:ativa={fav}
|
|
aria-pressed={fav}
|
|
aria-label={fav ? `Tirar ${c.nome} dos favoritos` : `Fixar ${c.nome} nos favoritos`}
|
|
onclick={() => favoritos.alternar(`criado:${c.id}`)}><Star size={14} /></button
|
|
>
|
|
{/if}
|
|
<button class="canto lixo" aria-label={`Apagar ${c.nome}`} onclick={() => criacao.apagar(c.id)}><Trash2 size={13} /></button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</Card>
|
|
</div>
|
|
|
|
<style>
|
|
.criar {
|
|
display: grid;
|
|
gap: var(--e-4);
|
|
}
|
|
.topo {
|
|
display: grid;
|
|
gap: var(--e-4);
|
|
align-items: start;
|
|
}
|
|
@media (min-width: 1024px) {
|
|
.topo {
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
}
|
|
.previa {
|
|
position: sticky;
|
|
top: calc(var(--topo-altura) + var(--e-4));
|
|
}
|
|
}
|
|
.previa {
|
|
aspect-ratio: 16 / 9;
|
|
border-radius: var(--raio-lg);
|
|
overflow: hidden;
|
|
background: #000;
|
|
border: 1px solid var(--borda);
|
|
}
|
|
.form {
|
|
display: grid;
|
|
gap: var(--e-3);
|
|
}
|
|
.form.local {
|
|
margin-top: var(--e-3);
|
|
padding-top: var(--e-3);
|
|
border-top: 1px solid var(--borda);
|
|
}
|
|
textarea {
|
|
min-height: 84px;
|
|
resize: vertical;
|
|
}
|
|
.segmentado {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 2px;
|
|
padding: 2px;
|
|
border-radius: var(--raio-sm);
|
|
background: var(--superficie-3);
|
|
}
|
|
.segmentado button {
|
|
flex: 1;
|
|
min-height: 34px;
|
|
padding: 0 var(--e-3);
|
|
border: 0;
|
|
border-radius: calc(var(--raio-sm) - 2px);
|
|
background: transparent;
|
|
color: var(--texto-2);
|
|
font: 500 var(--t-sm) var(--fonte);
|
|
cursor: pointer;
|
|
white-space: nowrap;
|
|
}
|
|
.segmentado.pequeno button {
|
|
min-height: 28px;
|
|
padding: 0 var(--e-2);
|
|
font-size: var(--t-xs);
|
|
}
|
|
.segmentado button[aria-checked='true'] {
|
|
background: var(--superficie);
|
|
color: var(--texto);
|
|
box-shadow: var(--sombra);
|
|
}
|
|
.ias {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--e-2);
|
|
}
|
|
.ia {
|
|
display: grid;
|
|
gap: 2px;
|
|
padding: var(--e-2) var(--e-3);
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio);
|
|
background: var(--superficie-2);
|
|
color: var(--texto);
|
|
font: inherit;
|
|
text-align: left;
|
|
cursor: pointer;
|
|
}
|
|
.ia[aria-checked='true'] {
|
|
border-color: var(--destaque);
|
|
background: var(--destaque-suave);
|
|
}
|
|
.ia.indisponivel .ia-detalhe {
|
|
color: var(--aviso);
|
|
}
|
|
.ia-nome {
|
|
font-weight: 600;
|
|
font-size: var(--t-sm);
|
|
}
|
|
.ia-detalhe {
|
|
font-size: var(--t-xs);
|
|
color: var(--texto-3);
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
.acoes {
|
|
justify-content: space-between;
|
|
}
|
|
.erro {
|
|
color: var(--perigo);
|
|
}
|
|
.grade {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
|
gap: var(--e-2);
|
|
}
|
|
.item {
|
|
position: relative;
|
|
}
|
|
.cartao {
|
|
width: 100%;
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: 2px;
|
|
padding: var(--e-3) var(--e-2) var(--e-2);
|
|
border: 1px solid var(--borda);
|
|
border-radius: var(--raio);
|
|
background: var(--superficie-2);
|
|
color: var(--texto);
|
|
font: inherit;
|
|
cursor: pointer;
|
|
transition: background var(--transicao), border-color var(--transicao), transform var(--transicao);
|
|
}
|
|
.cartao:hover {
|
|
border-color: var(--destaque);
|
|
background: var(--destaque-suave);
|
|
}
|
|
.cartao:active {
|
|
transform: scale(0.97);
|
|
}
|
|
.emoji {
|
|
font-size: 1.7rem;
|
|
line-height: 1.2;
|
|
}
|
|
.nome {
|
|
font-size: var(--t-sm);
|
|
text-align: center;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.meta {
|
|
font-size: 10px;
|
|
color: var(--texto-3);
|
|
text-align: center;
|
|
}
|
|
.canto {
|
|
position: absolute;
|
|
top: 2px;
|
|
width: 26px;
|
|
height: 26px;
|
|
display: grid;
|
|
place-items: center;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
color: var(--texto-3);
|
|
cursor: pointer;
|
|
}
|
|
.estrela {
|
|
right: 2px;
|
|
}
|
|
.lixo {
|
|
left: 2px;
|
|
}
|
|
.lixo:hover {
|
|
color: var(--perigo);
|
|
}
|
|
.estrela.ativa {
|
|
color: var(--aviso);
|
|
}
|
|
.estrela.ativa :global(svg) {
|
|
fill: currentColor;
|
|
}
|
|
</style>
|