primocode 8.25.0 → 8.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/app.js ADDED
@@ -0,0 +1,334 @@
1
+ /**
2
+ * app.js — o PrimoCode como aplicativo de janela.
3
+ *
4
+ * "Se mandar /app pra ele, ele abre o aplicativo que você vai desenvolver
5
+ * para o Mac e o Windows, já instala junto com o npm. Vai ser um aplicativo
6
+ * desktop bem bonito com as cores do PrimoCode, com aquele azul. Bem estilo
7
+ * Codex. Vai ter o chat em localhost. Um chat estilo terminal para quem não
8
+ * quer o terminal."
9
+ *
10
+ * ── POR QUE NÃO É ELECTRON ───────────────────────────────────────────────
11
+ * O PrimoCode tem ZERO dependência de npm, e isso não é purismo: é o que faz
12
+ * `npm i -g primocode` levar segundos em vez de baixar 200 MB de navegador
13
+ * empacotado. Electron levaria os dois embora — a promessa de "já instala
14
+ * junto com o npm" e a instalação leve.
15
+ *
16
+ * O que Electron entrega, no fundo, é uma janela sem barra de endereço
17
+ * mostrando uma página local. Chrome, Edge e Brave fazem exatamente isso com
18
+ * `--app=<url>`: janela limpa, ícone próprio na barra de tarefas, sem abas.
19
+ * O navegador já está na máquina do usuário — no Windows o Edge vem com o
20
+ * sistema, no Mac o Safari (e quase sempre o Chrome).
21
+ *
22
+ * Então: servidor HTTP local (http do Node, nada instalado) + janela do
23
+ * navegador em modo app. Se não houver nenhum navegador de Chromium, abre
24
+ * numa aba normal, que continua funcionando.
25
+ *
26
+ * ── POR QUE ELE ESPELHA O TERMINAL ───────────────────────────────────────
27
+ * O pedido foi "um chat estilo terminal para quem não quer o terminal" — e o
28
+ * agente já conta o que faz, linha por linha, com cor. Reescrever essa saída
29
+ * para a web daria duas verdades para manter, e uma delas ficaria velha.
30
+ *
31
+ * Aqui a saída é a MESMA: o `process.stdout.write` é espelhado, os códigos
32
+ * ANSI viram cor de verdade na página, e o terminal continua imprimindo
33
+ * normalmente. Quem abrir os dois vê a mesma coisa nos dois.
34
+ *
35
+ * ── SEGURANÇA ────────────────────────────────────────────────────────────
36
+ * Este servidor MANDA NO AGENTE: quem fala com ele executa comando, escreve
37
+ * arquivo e gasta a cota do usuário. Por isso ele só escuta em 127.0.0.1 e
38
+ * exige uma chave sorteada a cada abertura — sem ela, uma página qualquer
39
+ * aberta no mesmo navegador dispararia pedidos para cá.
40
+ */
41
+
42
+ 'use strict';
43
+
44
+ const http = require('http');
45
+ const fs = require('fs');
46
+ const os = require('os');
47
+ const path = require('path');
48
+ const crypto = require('crypto');
49
+ const { execFile } = require('child_process');
50
+
51
+ const WEB = path.join(__dirname, '..', 'app');
52
+ const PORTA_PADRAO = Number(process.env.PRIMOCODE_APP_PORT) || 7788;
53
+
54
+ // ── espelho da saída ─────────────────────────────────────────────────────
55
+
56
+ /* Duplica tudo que vai para o terminal, sem tirar nada de lá.
57
+ *
58
+ * O `write` original continua sendo chamado com os mesmos argumentos e o mesmo
59
+ * retorno: se este espelho mudasse o valor de volta, o Node passaria a achar
60
+ * que o buffer encheu (ou que não encheu) e a saída sairia picada. */
61
+ function espelhar(aoEscrever) {
62
+ const real = process.stdout.write.bind(process.stdout);
63
+ process.stdout.write = function (pedaco, enc, cb) {
64
+ try {
65
+ aoEscrever(typeof pedaco === 'string' ? pedaco : Buffer.from(pedaco).toString('utf8'));
66
+ } catch { /* o espelho nunca pode derrubar a saída */ }
67
+ return real(pedaco, enc, cb);
68
+ };
69
+ return function parar() { process.stdout.write = real; };
70
+ }
71
+
72
+ /* O prompt do readline e os quadros do rodinha não são conteúdo: são a
73
+ interface do TERMINAL se redesenhando. Na página eles viravam lixo piscando
74
+ no meio da conversa. O jeito de reconhecê-los é o que sobra depois de tirar
75
+ o ANSI: pedaço curto e sem quebra de linha é desenho, não texto. */
76
+ function ehRuido(texto) {
77
+ const limpo = texto.replace(/\x1b\[[0-9;?]*[a-zA-Z]/g, '').replace(/\r/g, '');
78
+ if (limpo.includes('\n')) return false;
79
+ return limpo.trim().length < 12;
80
+ }
81
+
82
+ // ── o servidor ───────────────────────────────────────────────────────────
83
+
84
+ function tipoDe(arquivo) {
85
+ if (arquivo.endsWith('.html')) return 'text/html; charset=utf-8';
86
+ if (arquivo.endsWith('.css')) return 'text/css; charset=utf-8';
87
+ if (arquivo.endsWith('.js')) return 'application/javascript; charset=utf-8';
88
+ if (arquivo.endsWith('.svg')) return 'image/svg+xml';
89
+ return 'application/octet-stream';
90
+ }
91
+
92
+ /**
93
+ * Sobe a janela do PrimoCode.
94
+ *
95
+ * @param {object} op
96
+ * @param {(texto:string)=>Promise} op.enviar manda um pedido ao agente
97
+ * @param {(texto:string)=>Promise} op.barra trata um comando /barra
98
+ * @param {()=>void} op.interromper para o que estiver rodando
99
+ * @param {()=>object} op.estado { pasta, modo, esforco, projeto }
100
+ * @param {string} op.versao
101
+ */
102
+ async function abrir(op) {
103
+ const chave = crypto.randomBytes(16).toString('hex');
104
+ const ouvintes = new Set();
105
+ let ocupado = false;
106
+ const fila = [];
107
+
108
+ // O histórico de saída para quem abrir a janela DEPOIS de o agente já ter
109
+ // começado — sem isto a página nasceria vazia no meio de uma tarefa.
110
+ const memoria = [];
111
+ const MEMORIA_MAX = 400;
112
+
113
+ function emitir(evento, dados) {
114
+ const linha = `event: ${evento}\ndata: ${JSON.stringify(dados)}\n\n`;
115
+ for (const res of ouvintes) {
116
+ try { res.write(linha); } catch { ouvintes.delete(res); }
117
+ }
118
+ }
119
+
120
+ const pararEspelho = espelhar((pedaco) => {
121
+ if (ehRuido(pedaco)) return;
122
+ memoria.push(pedaco);
123
+ if (memoria.length > MEMORIA_MAX) memoria.shift();
124
+ emitir('saida', pedaco);
125
+ });
126
+
127
+ async function processar(texto) {
128
+ fila.push(texto);
129
+ if (ocupado) return;
130
+ ocupado = true;
131
+ emitir('ocupado', true);
132
+ while (fila.length) {
133
+ const t = String(fila.shift() || '').trim();
134
+ if (!t) continue;
135
+ emitir('voce', t);
136
+ try {
137
+ if (t.startsWith('/')) await op.barra(t);
138
+ else await op.enviar(t);
139
+ } catch (e) {
140
+ emitir('erro', (e && e.message) || String(e));
141
+ }
142
+ }
143
+ ocupado = false;
144
+ emitir('ocupado', false);
145
+ emitir('estado', op.estado());
146
+ }
147
+
148
+ const servidor = http.createServer((req, res) => {
149
+ const url = new URL(req.url, 'http://127.0.0.1');
150
+ const rota = url.pathname;
151
+
152
+ /* A chave protege a API, que é onde está tudo o que importa: a conversa
153
+ do usuário e a capacidade de dar ordens ao agente. A página em si —
154
+ HTML, CSS, JS — não guarda segredo nenhum e não faz nada sem a
155
+ chave, então ela é servida solta.
156
+
157
+ Não é frouxidão, é o contrário: exigir chave nos estáticos também
158
+ quebrava a própria janela, porque o navegador pede /app.css e
159
+ /app.js sem parâmetro nenhum. A página vinha, ficava sem estilo e
160
+ sem script, e o "app bonito" era um formulário branco. */
161
+ if (rota.startsWith('/api/')) {
162
+ const dada = url.searchParams.get('k') || req.headers['x-primo-chave'];
163
+ if (dada !== chave) {
164
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
165
+ return res.end('chave inválida — abra a janela com o comando /app');
166
+ }
167
+ }
168
+ /* CSRF: uma página de terceiro consegue disparar POST simples para
169
+ 127.0.0.1 sem preflight. Ela não saberia a chave, mas o cinto e o
170
+ suspensório custam três linhas. */
171
+ const origem = req.headers.origin;
172
+ if (origem && !origem.startsWith(`http://127.0.0.1:${servidor.__porta}`)
173
+ && !origem.startsWith(`http://localhost:${servidor.__porta}`)) {
174
+ res.writeHead(403).end();
175
+ return;
176
+ }
177
+
178
+ if (rota === '/api/fluxo') {
179
+ res.writeHead(200, {
180
+ 'Content-Type': 'text/event-stream; charset=utf-8',
181
+ 'Cache-Control': 'no-cache',
182
+ Connection: 'keep-alive',
183
+ // Sem isto um proxy no meio segura os eventos até fechar.
184
+ 'X-Accel-Buffering': 'no',
185
+ });
186
+ res.write(': ligado\n\n');
187
+ ouvintes.add(res);
188
+ res.write(`event: estado\ndata: ${JSON.stringify(op.estado())}\n\n`);
189
+ for (const pedaco of memoria) {
190
+ res.write(`event: saida\ndata: ${JSON.stringify(pedaco)}\n\n`);
191
+ }
192
+ res.write(`event: ocupado\ndata: ${JSON.stringify(ocupado)}\n\n`);
193
+ // Batida a cada 20s: proxy e sistema derrubam conexão parada, e
194
+ // uma janela que "morre sozinha depois de um tempo" seria um bug
195
+ // impossível de achar depois.
196
+ const batida = setInterval(() => { try { res.write(': ok\n\n'); } catch {} }, 20000);
197
+ req.on('close', () => { clearInterval(batida); ouvintes.delete(res); });
198
+ return;
199
+ }
200
+
201
+ if (rota === '/api/enviar' && req.method === 'POST') {
202
+ let corpo = '';
203
+ req.on('data', (c) => { corpo += c; if (corpo.length > 200000) req.destroy(); });
204
+ req.on('end', () => {
205
+ let texto = '';
206
+ try { texto = JSON.parse(corpo).texto || ''; } catch { /* vazio */ }
207
+ res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');
208
+ if (texto.trim()) processar(texto);
209
+ });
210
+ return;
211
+ }
212
+
213
+ if (rota === '/api/parar' && req.method === 'POST') {
214
+ try { op.interromper(); } catch {}
215
+ res.writeHead(200, { 'Content-Type': 'application/json' }).end('{"ok":true}');
216
+ return;
217
+ }
218
+
219
+ if (rota === '/api/estado') {
220
+ res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
221
+ return res.end(JSON.stringify(Object.assign({ versao: op.versao }, op.estado())));
222
+ }
223
+
224
+ // Estáticos. `basename` corta qualquer "../" antes de virar caminho:
225
+ // este servidor fala com o disco do usuário e não pode servir o que
226
+ // estiver fora da pasta do aplicativo.
227
+ const nome = rota === '/' ? 'index.html' : path.basename(rota);
228
+ const arquivo = path.join(WEB, nome);
229
+ fs.readFile(arquivo, (erro, dados) => {
230
+ if (erro) { res.writeHead(404).end('não encontrado'); return; }
231
+ res.writeHead(200, { 'Content-Type': tipoDe(nome) });
232
+ res.end(dados);
233
+ });
234
+ });
235
+
236
+ const porta = await escutar(servidor, PORTA_PADRAO);
237
+ servidor.__porta = porta;
238
+ const url = `http://127.0.0.1:${porta}/?k=${chave}`;
239
+
240
+ const janela = await abrirJanela(url);
241
+ return {
242
+ url, porta, chave, servidor, janela,
243
+ fechar() { try { pararEspelho(); } catch {} try { servidor.close(); } catch {} },
244
+ };
245
+ }
246
+
247
+ /* Porta ocupada não é erro: é outra janela do PrimoCode, ou qualquer coisa
248
+ que pegou a 7788 primeiro. Anda dez casas e desiste — se dez portas
249
+ seguidas estão tomadas, o problema é outro. */
250
+ function escutar(servidor, inicial) {
251
+ return new Promise((ok, falha) => {
252
+ let porta = inicial;
253
+ const tentar = () => {
254
+ servidor.once('error', (e) => {
255
+ if (e.code === 'EADDRINUSE' && porta < inicial + 10) { porta++; tentar(); }
256
+ else falha(e);
257
+ });
258
+ servidor.listen(porta, '127.0.0.1', () => ok(porta));
259
+ };
260
+ tentar();
261
+ });
262
+ }
263
+
264
+ /* ── a janela ────────────────────────────────────────────────────────────
265
+ `--app=` abre sem barra de endereço, sem abas e com entrada própria na
266
+ barra de tarefas: é o que faz parecer aplicativo em vez de site. Chrome,
267
+ Edge, Brave e Vivaldi entendem, porque todos são Chromium.
268
+
269
+ `--user-data-dir` aponta para uma pasta do PrimoCode de propósito: sem ele,
270
+ o Chrome que JÁ ESTÁ ABERTO recebe o pedido e devolve uma aba comum — o
271
+ modo app é ignorado quando há um perfil em uso. Com perfil próprio nasce
272
+ uma janela de verdade, e o histórico do usuário fica fora disto. */
273
+ function candidatosDeNavegador() {
274
+ const p = os.platform();
275
+ if (p === 'darwin') {
276
+ return [
277
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
278
+ '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
279
+ '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser',
280
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
281
+ '/Applications/Arc.app/Contents/MacOS/Arc',
282
+ ];
283
+ }
284
+ if (p === 'win32') {
285
+ const bases = [process.env['PROGRAMFILES'], process.env['PROGRAMFILES(X86)'],
286
+ process.env.LOCALAPPDATA].filter(Boolean);
287
+ const relativos = [
288
+ 'Google\\Chrome\\Application\\chrome.exe',
289
+ 'Microsoft\\Edge\\Application\\msedge.exe',
290
+ 'BraveSoftware\\Brave-Browser\\Application\\brave.exe',
291
+ ];
292
+ const fora = [];
293
+ for (const b of bases) for (const r of relativos) fora.push(path.join(b, r));
294
+ return fora;
295
+ }
296
+ return ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium',
297
+ '/usr/bin/chromium-browser', '/usr/bin/microsoft-edge', '/usr/bin/brave-browser'];
298
+ }
299
+
300
+ function abrirJanela(url) {
301
+ const perfil = path.join(os.homedir(), '.primocode', 'janela');
302
+ try { fs.mkdirSync(perfil, { recursive: true }); } catch {}
303
+
304
+ for (const exe of candidatosDeNavegador()) {
305
+ if (!fs.existsSync(exe)) continue;
306
+ try {
307
+ const filho = execFile(exe, [
308
+ `--app=${url}`,
309
+ `--user-data-dir=${perfil}`,
310
+ '--window-size=1180,820',
311
+ '--no-first-run',
312
+ '--no-default-browser-check',
313
+ ], () => {});
314
+ // A janela é do usuário: se o PrimoCode do terminal terminar, ela
315
+ // continua de pé.
316
+ filho.unref();
317
+ return Promise.resolve({ modo: 'janela', navegador: path.basename(exe) });
318
+ } catch { /* tenta o próximo */ }
319
+ }
320
+
321
+ // Nenhum Chromium instalado: aba comum no navegador padrão. Funciona
322
+ // igual — só não fica com cara de aplicativo.
323
+ const p = os.platform();
324
+ const [cmd, args] = p === 'darwin' ? ['open', [url]]
325
+ : p === 'win32' ? ['cmd.exe', ['/c', 'start', '', url]]
326
+ : ['xdg-open', [url]];
327
+ return new Promise((ok) => {
328
+ execFile(cmd, args, { timeout: 10000 }, (erro) => {
329
+ ok(erro ? { modo: 'nenhum', erro: erro.message } : { modo: 'aba' });
330
+ });
331
+ });
332
+ }
333
+
334
+ module.exports = { abrir, abrirJanela, espelhar, ehRuido, candidatosDeNavegador };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "primocode",
3
- "version": "8.25.0",
3
+ "version": "8.27.0",
4
4
  "description": "PrimoCode — agente de engenharia com IA e cursor próprio. Modelos grátis. Cria arquivos, roda comandos, controla navegador e desktop: abre apps, clica em botões e ícones pelo nome, digita e usa atalhos.",
5
5
  "main": "bin/primocode.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/primocode.js",
11
- "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js"
11
+ "test": "node test/tools.test.js && node test/ui.test.js && node test/modo.test.js && node test/pasta.test.js && node test/catalogo.test.js && node test/desktop.test.js && node test/effort.test.js && node test/claude-engine.test.js && node test/regressao.test.js && node test/memoria-conversa.test.js && node test/repeticao.test.js && node test/referencia.test.js && node test/parar.test.js && node test/continuar.test.js && node test/studio-spec.test.js && node test/studio-versoes.test.js && node test/voz.test.js && node test/editor.test.js && node test/estudio-projeto.test.js && node test/vocabulario.test.js && node test/video-export.test.js && node test/monotonia.test.js && node test/clipe.test.js && node test/midia-envio.test.js && node test/pausas.test.js && node test/app.test.js && node test/elementos.test.js"
12
12
  },
13
13
  "engines": {
14
14
  "node": ">=18.17.0"
@@ -27,6 +27,7 @@
27
27
  "files": [
28
28
  "bin/",
29
29
  "lib/",
30
+ "app/",
30
31
  "studio/",
31
32
  "README.md"
32
33
  ]
@@ -57,7 +57,8 @@ Em dúvida entre duas, escolha a que o usuário vai **mostrar para outra pessoa*
57
57
  - **Ritmo.** Em decks longos, um `layout: "secao"` a cada 3–5 cenas.
58
58
  - **Escolha o tema pelo assunto:** `meia-noite` produto e tecnologia · `claro`
59
59
  corporativo e projetor · `papel` editorial e longo · `neon` técnico e dev ·
60
- `brasa` energia e urgência · `floresta` sustentabilidade e saúde.
60
+ `brasa` energia e urgência · `floresta` sustentabilidade e saúde ·
61
+ `ambar` curso, aula e conteúdo longo · `grafite` técnico sóbrio e relatório.
61
62
  - **Notas do apresentador** (`notas` na cena) em quem vai falar por cima.
62
63
  - **Nunca invente dado com cara de real.** Sem fonte, escreva a ordem de grandeza
63
64
  ou deixe o campo como pergunta explícita ao usuário.
@@ -134,9 +134,10 @@ Sem declarar nada, os elementos já entram em cascata na ordem em que aparecem.
134
134
 
135
135
  ### Temas
136
136
 
137
- `meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce` `mono`
137
+ `meia-noite` `claro` `papel` `neon` `brasa` `floresta` `retro` `oceano` `doce`
138
+ `mono` `ambar` `grafite`
138
139
 
139
- São esses dez, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
140
+ São esses doze, e só. Nome fora da lista é recusado: escuro é `meia-noite`, não
140
141
  `noite`. Vale no documento ou por cena.
141
142
 
142
143
  ### Identidade — o que separa um vídeo genérico de um da marca
@@ -89,6 +89,8 @@ cena. Se o assunto não empurrar para nenhuma, escolha a que você NÃO usou da
89
89
  | explicação, tutorial, passo a passo | `papel` ou `claro` | `lofi` | **Aula** |
90
90
  | conquista, agradecimento, celebração | `doce` ou `neon` | `alegre` | **Brinde** |
91
91
  | memória, história, retrospectiva | `retro` ou `mono` | `lofi` | **Linha** |
92
+ | curso, aula, treinamento, método | `ambar` | `lofi` | **Aula** |
93
+ | relatório, número, análise técnica | `grafite` | `ambiente` | **Manchete** |
92
94
 
93
95
  ### As seis estruturas
94
96
 
@@ -178,6 +180,85 @@ livre de direitos, ou uma das trilhas geradas acima.
178
180
 
179
181
  ---
180
182
 
183
+ ## Elementos que dão cara de produção
184
+
185
+ Estes seis existem porque um vídeo genérico é feito só de título + texto. São
186
+ baratos de escrever e mudam completamente o resultado.
187
+
188
+ ### `selo` — a pílula de estado
189
+
190
+ ```json
191
+ { "tipo": "selo", "texto": "Módulo 2 de 5" }
192
+ { "tipo": "selo", "texto": "Resultado 4", "icone": "★", "estilo": "vivo" }
193
+ ```
194
+
195
+ `estilo`: sem nada (contorno), `"vivo"` (preenchido) ou `"solto"` (só o texto
196
+ em versalete). Vai sozinho para cima do título.
197
+
198
+ ### `progresso` — onde estamos no caminho
199
+
200
+ ```json
201
+ { "tipo": "progresso", "atual": 4, "total": 15 }
202
+ ```
203
+
204
+ Mostra "4/15" e os pontinhos. Use em série de cenas: diz ao espectador que
205
+ existe um percurso, e isso segura ele até o fim. `"numero": false` deixa só
206
+ os pontos.
207
+
208
+ ### `avatar` — a pessoa
209
+
210
+ ```json
211
+ { "tipo": "avatar", "src": "/midia/foto.jpg", "nome": "Cleiton Paris", "tamanho": 280 }
212
+ ```
213
+
214
+ Círculo com anel aceso. Sem `src`, mostra as iniciais do `nome` — melhor que
215
+ um buraco cinza. Vai para cima do nome sozinho.
216
+
217
+ ### `dispositivo` — celular, notebook ou navegador com conteúdo dentro
218
+
219
+ ```json
220
+ { "tipo": "dispositivo", "forma": "celular", "dentro": [ ...elementos... ] }
221
+ ```
222
+
223
+ `forma`: `celular` | `notebook` | `tablet` | `navegador` (este aceita
224
+ `"endereco": "primocode.dev"`). `dentro` é uma lista de elementos comuns — o
225
+ que couber. Nos layouts `imagem-direita` / `imagem-esquerda` o aparelho vai
226
+ para o lado sozinho, com o texto do outro. É o enquadramento mais forte que
227
+ existe aqui: use quando estiver mostrando uma tela.
228
+
229
+ ### `conversa` — balões de pergunta e resposta
230
+
231
+ ```json
232
+ { "tipo": "conversa", "itens": [
233
+ { "de": "eu", "texto": "Como acelero meu workflow?" },
234
+ { "de": "ela", "texto": "Peça o storyboard em seis quadros." } ] }
235
+ ```
236
+
237
+ Entram um de cada vez, como diálogo. `de: "eu"` à esquerda; qualquer outra
238
+ coisa à direita, com a cor do tema. Combina com `dispositivo` — a conversa
239
+ dentro do celular é a cena mais usada em vídeo de IA.
240
+
241
+ ### `passos` e `opcoes`
242
+
243
+ ```json
244
+ { "tipo": "passos", "atual": 2, "itens": ["Passo 1|roteiro", "Passo 2|storyboard", "Passo 3|corte"] }
245
+ { "tipo": "opcoes", "pergunta": "O que garante o look?", "certa": 1,
246
+ "itens": ["LUT uniforme", "Saturação aleatória", "Espaços de cor diferentes"] }
247
+ ```
248
+
249
+ `passos` acende a etapa `atual` e apaga as outras — mude o `atual` a cada
250
+ cena e a série ganha movimento sem esforço. `opcoes` marca a alternativa
251
+ `certa` com ✓ (não escreva o ✓ no texto: ele vem sozinho). Alternativa faz o
252
+ espectador pensar antes da resposta, e pensar é o que segura alguém no vídeo.
253
+
254
+ ### Vidro
255
+
256
+ `"estilo": "vidro"` em `cartoes` dá painel translúcido com bisel de luz em
257
+ cima e sombra embaixo — o "liquid glass". Funciona em tema escuro; em tema
258
+ claro use os cartões normais.
259
+
260
+ ---
261
+
181
262
  ## Editar o vídeo do usuário (`clipe`)
182
263
 
183
264
  Quando a pessoa manda uma gravação dela — o rosto falando, uma tela gravada,
package/studio/server.py CHANGED
@@ -784,7 +784,7 @@ def gravar_analise(ident, dados):
784
784
  # "Ele não alterna entre os temas, usa um tema só pra todos os vídeos, e o
785
785
  # vídeo sempre tem a mesma estrutura. Fica tudo genérico."
786
786
  #
787
- # O estúdio tem dez temas, onze fundos, oito transições e seis trilhas. O
787
+ # O estúdio tem doze temas, onze fundos, oito transições e seis trilhas. O
788
788
  # agente usa uma fatia estreita e repete — porque a spec tem UM exemplo
789
789
  # completo e modelo ancora em exemplo. Conselho escrito na spec ajuda pouco
790
790
  # quando ela nem sempre é relida.
@@ -922,7 +922,7 @@ def _texto_do_doc(doc):
922
922
  # Os temas que o render.css de fato define. Fora desta lista não existe cor
923
923
  # nenhuma — foi o que aconteceu com "noite", que passou como se fosse tema.
924
924
  TEMAS_VALIDOS = {"claro", "meia-noite", "papel", "neon", "brasa", "floresta",
925
- "retro", "oceano", "doce", "mono"}
925
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"}
926
926
 
927
927
 
928
928
  def _tema_invalido(tema, onde=""):
@@ -989,7 +989,13 @@ def validar_doc(modelo, doc):
989
989
  # preto": documento válido na forma, invisível na tela.
990
990
  TIPOS_VALIDOS = {"texto", "lista", "cartoes", "numeros", "tabela", "codigo",
991
991
  "citacao", "forma", "imagem", "icone", "grafico", "no", "conector",
992
- "objeto3d", "clipe"}
992
+ "objeto3d", "clipe",
993
+ "selo", "progresso", "avatar", "dispositivo", "conversa",
994
+ "passos", "opcoes"}
995
+ # Os sete últimos são o vocabulário do vídeo de referência. O comentário
996
+ # mora aqui fora porque test/vocabulario.test.js lê esta lista com
997
+ # expressão regular, comparando com a do render.js — palavra de comentário
998
+ # dentro das chaves entrava na lista como se fosse um tipo válido.
993
999
 
994
1000
  # Tema inventado tinha o mesmo destino silencioso do tipo inventado: o CSS
995
1001
  # não casa com [data-tema="noite"], nenhuma cor é definida e a peça sai
@@ -2,10 +2,10 @@
2
2
  ferramentas.js — o que cada ferramenta oferece na interface.
3
3
  ========================================================================== */
4
4
 
5
- // Os dez que o render.css define. A lista tinha seis: quatro temas existiam
5
+ // Os doze que o render.css define. A lista tinha seis: quatro temas existiam
6
6
  // no CSS, a IA podia usá-los, e o editor não os oferecia a ninguém.
7
7
  var TEMAS = ["meia-noite", "claro", "papel", "neon", "brasa", "floresta",
8
- "retro", "oceano", "doce", "mono"];
8
+ "retro", "oceano", "doce", "mono", "ambar", "grafite"];
9
9
 
10
10
  var PALETA = ["#ff5a36", "#ffb03a", "#f4e04d", "#2fbf7f", "#1f8fe0", "#8b5cf6",
11
11
  "#f472b6", "#ffffff", "#9aa4b2", "#111318"];