sagadeck 1.6.0 → 1.7.1
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/README.md +14 -8
- package/SKILL.md +7 -6
- package/bin/sagadeck.js +13 -3
- package/docs/ANDAMENTO.md +85 -4
- package/docs/EXPERIENCIAS.md +1 -1
- package/docs/REFERENCIA.md +7 -4
- package/package.json +4 -4
- package/src/ai/ia-config.js +23 -2
- package/src/ai/llm.js +6 -4
- package/src/ai/variety.js +1 -1
- package/src/api-client.js +106 -5
- package/src/experiences.js +1 -1
- package/src/library.js +2 -2
- package/src/mcp/server.js +4 -4
- package/src/runtime/base.css +1 -1
- package/src/runtime/code-lab.js +10 -10
- package/src/runtime/skins/aluminio.css +1 -1
- package/src/runtime/skins/prata.css +1 -1
- package/src/studio/api-routes.js +11 -0
- package/src/studio/banner.js +1 -1
- package/src/studio/ia-routes.js +5 -2
- package/src/studio/open-browser.js +17 -0
- package/src/studio/public/ai-settings.js +22 -2
- package/src/studio/public/api-envs-form.js +148 -0
- package/src/studio/public/app.js +8 -5
- package/src/studio/public/chat.js +1 -1
- package/src/studio/public/index.html +5 -0
- package/src/studio/public/library.css +5 -0
- package/src/studio/public/library.html +1 -0
- package/src/studio/public/library.js +9 -2
- package/src/studio/public/style.css +23 -0
- package/src/studio/server.js +3 -1
- package/src/templates/scaffold.js +5 -5
- package/src/themes.js +5 -5
- package/templates/{exemplo-keynote.yaml → exemplo-prata.yaml} +6 -6
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Você (ou uma IA) escreve um arquivo `.yaml` com o conteúdo. O sagadeck gera:
|
|
|
9
9
|
| **`.pdf`** | um slide por página, para mandar depois da palestra |
|
|
10
10
|
| **`- roteiro.pdf`** | roteiro do apresentador: miniatura de cada slide + fala + interações + relógio planejado |
|
|
11
11
|
|
|
12
|
-
Ele vem com 11 temas (incluindo
|
|
12
|
+
Ele vem com 11 temas (incluindo prata limpo e respirável, artesanal desenhado à mão, azul elétrico vivo e pop alegre), 23 layouts (incluindo KPIs e fluxos de processo), 2.100+ ícones, pictogramas e cenas geradas na hora, diagramas, 7 tipos de gráfico, e um **fiscal automático** que detecta texto estourado, sobreposição, contraste baixo e slides com texto demais ("anti-sono").
|
|
13
13
|
|
|
14
14
|
## Instalação
|
|
15
15
|
|
|
@@ -24,11 +24,14 @@ Requer **Node.js 18+** e **Chrome ou Edge** instalados (o PDF, o PNG e as confer
|
|
|
24
24
|
```bash
|
|
25
25
|
sagadeck studio
|
|
26
26
|
```
|
|
27
|
-
|
|
28
|
-
apresentações, o editor (estilo PowerPoint) e o chat com a IA. Não há outra interface
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
O navegador abre sozinho em **http://127.0.0.1:3517** (se não abrir, abra esse endereço). **Essa é a página do
|
|
28
|
+
sagadeck**: a biblioteca das suas apresentações, o editor (estilo PowerPoint) e o chat com a IA. Não há outra interface
|
|
29
|
+
para abrir nem para criar. Deixe o terminal aberto enquanto usa; `--sem-navegador` não abre o navegador.
|
|
30
|
+
|
|
31
|
+
Para usar a IA (gerar apresentações, conversar com o chat), clique em **Configurar IA** (a biblioteca mostra uma faixa
|
|
32
|
+
enquanto ela não está configurada). **Usar a recomendação** preenche o OpenRouter com modelos que funcionam; cole a
|
|
33
|
+
chave, clique em **Testar** e **Salvar**. Se nem a recomendação responder, o problema é a rede ou a chave. Depois,
|
|
34
|
+
troque pelo provedor e pelos modelos que quiser. Detalhes em
|
|
32
35
|
[IA de verdade](#ia-de-verdade-llm).
|
|
33
36
|
|
|
34
37
|
## Como usar
|
|
@@ -252,8 +255,11 @@ Para ver funcionando sem configurar nada:
|
|
|
252
255
|
O painel **Formatar** edita o endereço, o corpo em JSON, o modo (síncrono, polling, streaming, tempo real)
|
|
253
256
|
e o que guardar para os próximos slides.
|
|
254
257
|
- **Inserir → Ambientes:** onde ficam os endereços (`{{base}}`), tokens e segredos do *seu* serviço
|
|
255
|
-
(dev, hom, prod).
|
|
256
|
-
|
|
258
|
+
(dev, hom, prod). Preenche-se por campos: endereço base, variáveis, segredos (guardados cifrados) e a
|
|
259
|
+
autenticação (nenhuma, token fixo ou token que expira), com **Testar**; quem prefere edita o YAML na aba
|
|
260
|
+
"Como texto". É gravado em `~/.sagadeck/ambientes.yaml`, na sua máquina, nunca no deck. Sem ambiente seu, o
|
|
261
|
+
**ENSAIO** (API de mentira) já funciona. O selo no slide (DEV, HOM, ENSAIO…) troca o ambiente na hora. No
|
|
262
|
+
servidor multiusuário o botão não aparece (lá os slides de API não executam).
|
|
257
263
|
|
|
258
264
|
Executar só funciona no Studio local; no HTML exportado e no modo multiusuário, o slide mostra a última
|
|
259
265
|
resposta gravada. Todos os campos estão na [referência](docs/REFERENCIA.md#slide-api-requisição-ao-vivo).
|
package/SKILL.md
CHANGED
|
@@ -12,12 +12,13 @@ O sagadeck gera apresentações profissionais a partir de um arquivo YAML, ofere
|
|
|
12
12
|
Quando a pessoa pede para instalar o sagadeck e deixar pronto:
|
|
13
13
|
|
|
14
14
|
1. `npm install -g sagadeck` (Node 18+; não precisa de Python).
|
|
15
|
-
2. `sagadeck studio
|
|
16
|
-
|
|
15
|
+
2. `sagadeck studio`: o navegador abre sozinho em **http://127.0.0.1:3517** (é também a primeira linha que o
|
|
16
|
+
comando escreve; `--sem-navegador` não abre). **O Studio é a interface do sagadeck**: biblioteca, editor e chat com
|
|
17
|
+
IA. O terminal fica aberto enquanto ela usa.
|
|
17
18
|
3. **Nunca crie uma página, um servidor ou uma interface própria** para o sagadeck, nem "suba as APIs" à mão: tudo
|
|
18
19
|
que a pessoa precisa está no Studio.
|
|
19
20
|
4. Para a IA, mostre onde configurar: **Configurar IA** no alto da biblioteca (ou **IA desligada** no editor):
|
|
20
|
-
|
|
21
|
+
**Usar a recomendação** (OpenRouter com modelos que funcionam), a chave, Testar e Salvar. A configuração fica em `~/.sagadeck/ia.json`; nunca grave a chave no código nem
|
|
21
22
|
na pasta das apresentações.
|
|
22
23
|
5. As apresentações moram na biblioteca (`SAGADECK_HOME`, senão Documentos\sagadeck no Windows e `~/sagadeck` fora
|
|
23
24
|
dele). Não crie decks em outra pasta.
|
|
@@ -119,7 +120,7 @@ O agente no terminal deve seguir o fluxo:
|
|
|
119
120
|
## Decisão de Arquitetura: Mesmo Repositório vs. Repositório Separado
|
|
120
121
|
|
|
121
122
|
### Por que o Estúdio deve ficar no **mesmo repositório** (`sagadeck`):
|
|
122
|
-
1. **Fonte Única da Verdade**: Os layouts (incluindo `stats` para KPIs e `steps` para processos), os 11 temas visuais (incluindo `prata`
|
|
123
|
+
1. **Fonte Única da Verdade**: Os layouts (incluindo `stats` para KPIs e `steps` para processos), os 11 temas visuais (incluindo `prata` limpo e respirável, `rabisco` artesanal pintado à mão, `oceano` azul elétrico vivo, `pop` alegre/chiclete e `aurora` neon), as regras de tipografia e as rotinas de verificação geométrica residem no mesmo código (`src/layouts.js`, `src/themes.js`, `src/fiscal/autofix.js`). Qualquer alteração ou novo layout adicionado ao compilador fica disponível **instantaneamente** no estúdio visual.
|
|
123
124
|
2. **Zero Descompasso de Versões**: Em repositórios separados, quando o formato YAML ou os temas do SagaDeck evoluem, a interface visual quebra ou fica desatualizada até que alguém publique e atualize dependências externas.
|
|
124
125
|
3. **Experiência de Uso (DX) Imediata**: O desenvolvedor ou agente clona o repositório ou roda `npm install -g sagadeck` e tem **tudo num só lugar**: linha de comando (`sagadeck build`), estúdio visual (`sagadeck studio`), servidor de agentes (`sagadeck mcp`) e fiscal (`sagadeck check`).
|
|
125
126
|
4. **Leve e Modular**: A interface web foi construída com tecnologias nativas leves (Vanilla JS, CSS moderno e servidor HTTP embutido do Node.js sem frameworks pesados). Isso mantém o pacote pequeno (~1.1 MB empacotado) sem impactar o desempenho do compilador.
|
|
@@ -129,7 +130,7 @@ O agente no terminal deve seguir o fluxo:
|
|
|
129
130
|
## Novos Temas Visuais & Recursos Anti-Textão
|
|
130
131
|
|
|
131
132
|
### 1. Temas Modernos, Vivos, Jovens e Clean
|
|
132
|
-
- **`prata`**: **
|
|
133
|
+
- **`prata`**: **Limpo e espaçoso**. Jovem, clean, respirável e sofisticado (levado a sério). Fundo cinza prata acetinado (`#F5F5F7`), texto em ardósia/preto profundo (`#1D1D1F`), cinza titânio intermediário (`#86868B`), divisores finos (`#D2D2D7`) e azul vivo (`#0071E3`). Cartelas com efeito vidro fosco / acabamento acetinado com sombra ultra-suave e tipografia do sistema / Inter.
|
|
133
134
|
- **`rabisco`**: Estilo artesanal e lousa com fontes desenhadas à mão (`'Caveat'`, `'Patrick Hand'`, `'Ink Free'`), bordas orgânicas de caderno, sombras desenhadas, fitas adesivas/post-its e ícones em estilo doodle. Ideal quando se quer uma apresentação calorosa, amigável e ilustrada.
|
|
134
135
|
- **`oceano`**: Azul elétrico ultra vibrante (`#0062FF`), ciano neon (`#00C6FF`), tipografia moderna punchy e fundo luminoso de alta energia. Perfeito para tech, startups e produtos dinâmicos.
|
|
135
136
|
- **`pop`**: Alegre, colorido e chiclete com paleta super animada (roxo, rosa chiclete, menta, sol) e cantos ultra-arredondados (26px).
|
|
@@ -145,7 +146,7 @@ Para apresentações de alto impacto sem "muralhas de texto":
|
|
|
145
146
|
- **Sem bloqueios de robô**: Modelos de IA não devem tentar raspar o Google via navegadores headless (o Google bloqueia com CAPTCHA/bot detection). O SagaDeck inclui pesquisa nativa via DuckDuckGo com `sagadeck search "<termo>"` e a ferramenta MCP `sagadeck_web_search`.
|
|
146
147
|
- **Controle Flexível de Fontes**:
|
|
147
148
|
- **Quando o usuário pedir fontes citadas**: Inclua `source: "Fonte: Nome do Relatório, Ano"` nos slides com métricas (`stats`, `number`, `chart`, `split`) e a URL completa no roteiro do apresentador (`notes:`).
|
|
148
|
-
- **Quando o usuário pedir sem fontes (ou estilo clean
|
|
149
|
+
- **Quando o usuário pedir sem fontes (ou estilo clean)**: Omita o campo `source:`, mantendo o slide limpo e desobstruído.
|
|
149
150
|
- **Garantia para Modelos Econômicos**: Qualquer modelo (inclusive os menores como Haiku, Flash ou GPT-4o-mini) deve sempre finalizar com o comando `sagadeck autofix <deck.yaml>`. Isso corrige problemas cobertos pelas regras; depois confira a renderização, porque não garante perfeição geométrica ou visual.
|
|
150
151
|
|
|
151
152
|
## Recursos recentes para autoria por IA
|
package/bin/sagadeck.js
CHANGED
|
@@ -40,13 +40,19 @@ for (let i = 0; i < rawRest.length; i++) {
|
|
|
40
40
|
|
|
41
41
|
const HELP = `sagadeck — YAML -> apresentação (HTML animado + PowerPoint editável + PDF + roteiro)
|
|
42
42
|
|
|
43
|
+
Para começar: sagadeck studio
|
|
44
|
+
abre o Studio no navegador (biblioteca, editor e chat com IA; http://127.0.0.1:3517).
|
|
45
|
+
Deixe o terminal aberto enquanto usa. A IA se configura lá, em Configurar IA.
|
|
46
|
+
|
|
47
|
+
Todos os comandos:
|
|
48
|
+
|
|
43
49
|
sagadeck new <nome> --prompt "briefing" [--slides=10] [--duration=20] [--theme=x] deck inteiro escrito pelo LLM
|
|
44
50
|
(pesquisa na web quando o pedido precisa de informação recente ou de um artigo; --sem-pesquisa desliga)
|
|
45
51
|
(--duration dispensa --slides: ~1 slide a cada 1,5 min)
|
|
46
52
|
(imagens: peça no briefing; --images = "você decide onde ilustrar", --no-images = nenhuma)
|
|
47
53
|
sagadeck napkin <texto|arquivo> [-o deck.yaml] [--rules] texto bruto -> slide visual (LLM se houver; --rules força as regras)
|
|
48
54
|
sagadeck imagens <deck.yaml> gera as imagens pedidas com image_prompt: no YAML (modelo de imagem)
|
|
49
|
-
sagadeck scaffold <nome> [--theme=prata] [--type=pitch|
|
|
55
|
+
sagadeck scaffold <nome> [--theme=prata] [--type=lancamento|pitch|palestra] gera esqueleto narrativo pronto (economiza 80% de tokens)
|
|
50
56
|
sagadeck new <nome> [--topic=T] [--theme=x] cria um deck de exemplo NA BIBLIOTECA (<biblioteca>/<tópico>/<nome>/)
|
|
51
57
|
sagadeck build <deck.yaml> gera <deck>.html (abre no navegador; P = modo apresentador)
|
|
52
58
|
sagadeck check <deck.yaml> procura texto estourado, sobreposição, contraste, excesso de texto
|
|
@@ -56,7 +62,7 @@ const HELP = `sagadeck — YAML -> apresentação (HTML animado + PowerPoint edi
|
|
|
56
62
|
sagadeck roteiro <deck.yaml> gera <deck> - roteiro.pdf (miniaturas + notas + tempos)
|
|
57
63
|
sagadeck estudo <deck.yaml> [--html] gera <deck> - material de estudo.pdf (cada slide + texto de consulta)
|
|
58
64
|
sagadeck all <deck.yaml> build + check + pptx + pdf + roteiro
|
|
59
|
-
sagadeck studio [deck.yaml|x.sagadeck] [--port=3517] [--library=PASTA] sem arquivo: abre a biblioteca; já aberto: usa o aberto
|
|
65
|
+
sagadeck studio [deck.yaml|x.sagadeck] [--port=3517] [--library=PASTA] [--sem-navegador] sem arquivo: abre a biblioteca (no navegador); já aberto: usa o aberto
|
|
60
66
|
(PASTA padrão: SAGADECK_HOME, senão Documentossagadeck no Windows, ~/sagadeck fora dele); com arquivo: abre o editor dele
|
|
61
67
|
--host=0.0.0.0 abre para a rede (padrão: só esta máquina)
|
|
62
68
|
--multiuser: uma biblioteca por usuário, atrás de um proxy que envia X-Sagadeck-User
|
|
@@ -196,7 +202,7 @@ async function main() {
|
|
|
196
202
|
const { generateScaffold } = await import("../src/templates/scaffold.js");
|
|
197
203
|
const YAML = (await import("yaml")).default;
|
|
198
204
|
const theme = flags.theme || "prata";
|
|
199
|
-
const type = flags.type || "
|
|
205
|
+
const type = flags.type || "lancamento";
|
|
200
206
|
const title = flags.title || (args[0] ? path.basename(args[0], path.extname(args[0])) : "Nova Apresentação");
|
|
201
207
|
const author = flags.author || "Seu Nome";
|
|
202
208
|
const spec = generateScaffold({ title, theme, type, author });
|
|
@@ -387,6 +393,7 @@ async function main() {
|
|
|
387
393
|
else { console.error(`O Studio já está aberto em http://${shownHost}:${port} com a biblioteca ${library}, e este deck está fora dela. Use --port=OUTRA para abri-lo à parte.`); process.exit(1); }
|
|
388
394
|
}
|
|
389
395
|
console.log(`✓ O SagaDeck Studio ${found.version} já está aberto (processo ${found.pid}): ${url}`);
|
|
396
|
+
{ const B = await import("../src/studio/open-browser.js"); if (B.shouldOpenBrowser({ flags, multiuser })) B.openBrowser(url); }
|
|
390
397
|
break;
|
|
391
398
|
}
|
|
392
399
|
if (decision.action === "conflict") {
|
|
@@ -403,6 +410,9 @@ async function main() {
|
|
|
403
410
|
server.listen(port, host, async () => {
|
|
404
411
|
const { studioBanner } = await import("../src/studio/banner.js");
|
|
405
412
|
for (const line of studioBanner({ version: VERSION, url: `http://${shownHost}:${port}/`, library, ai: (await import("../src/ai/llm.js")).llmConfig(), multiuser, usersDir: path.join(library, "usuarios"), isLibrary: !deckFile })) console.log(line);
|
|
413
|
+
// depois do endereço (a primeira linha é sempre ele): abre o navegador na página
|
|
414
|
+
const B = await import("../src/studio/open-browser.js");
|
|
415
|
+
if (B.shouldOpenBrowser({ flags, multiuser }) && B.openBrowser(`http://${shownHost}:${port}/`)) console.log(" (abrindo no navegador; --sem-navegador desliga)");
|
|
406
416
|
});
|
|
407
417
|
break;
|
|
408
418
|
}
|
package/docs/ANDAMENTO.md
CHANGED
|
@@ -12,10 +12,10 @@ começar. Trabalho em curso vai para o GitHub (commit + push) a cada etapa, nunc
|
|
|
12
12
|
|
|
13
13
|
**Para quem pega isto numa sessão nova, sem a conversa de origem.** O projeto é o **sagadeck**:
|
|
14
14
|
- Repositório GitHub `naruminho/sagadeck`, clonado em `C:\Users\narum\src\sagadeck`, branch `main`.
|
|
15
|
-
- Publicado no npm como `sagadeck` (1.
|
|
15
|
+
- Publicado no npm como `sagadeck` (1.7.1: a IA diz por que o `ia.json` não serve e, no servidor, não mostra a configuração; o PyPI parou na 1.5.0).
|
|
16
16
|
- Ferramenta Node (ESM) que transforma apresentações escritas em YAML em HTML/PPTX.
|
|
17
17
|
- Tem um editor web, o Studio (`sagadeck studio`, porta 3517).
|
|
18
|
-
- Tem uma IA (
|
|
18
|
+
- Tem uma IA embutida (provedor direto, configurado em `~/.sagadeck/ia.json` pela tela Configurar IA) que cria e edita decks, inclusive a partir de PDFs de
|
|
19
19
|
artigos científicos para apresentar em congresso.
|
|
20
20
|
|
|
21
21
|
Os itens abaixo vieram de duas rodadas de 08/10/2026, ambas descritas nas seções logo abaixo deste diário:
|
|
@@ -42,6 +42,42 @@ datadas abaixo). O resto fica para depois e só entra se for pedido. Antes de co
|
|
|
42
42
|
ler o conteúdo força o download. Conferir se listar a biblioteca (`openLibrary`, `/api/library`) lê o YAML de
|
|
43
43
|
cada deck; se lê, listar só pelo nome/metadado e abrir o conteúdo sob demanda.
|
|
44
44
|
|
|
45
|
+
## Sem citar o programa de apresentações da maçã (1.7.1) — 10/10/2026
|
|
46
|
+
|
|
47
|
+
Pedido de Narumi: o código e a documentação não citam o programa de apresentações da maçã, a empresa nem os
|
|
48
|
+
aparelhos dela. O tema `prata` passa a se descrever como "limpo e espaçoso"; o `aluminio`, como "lançamento de
|
|
49
|
+
aparelho premium". O exemplo virou `templates/exemplo-prata.yaml`, e o tipo padrão do `scaffold` (CLI e MCP) virou
|
|
50
|
+
`lancamento` (tipo desconhecido continua caindo nele). Saiu também o slogan da empresa do esqueleto. Ficam só os nomes
|
|
51
|
+
técnicos (a fonte do sistema no CSS, a identificação de navegador da pesquisa). `guards.test.js` falha se voltar
|
|
52
|
+
(olha os arquivos do repositório, menos bibliotecas de terceiros); regra no `CLAUDE.md`.
|
|
53
|
+
|
|
54
|
+
## No servidor, a configuração da IA não sai para quem usa (1.7.1) — 10/10/2026
|
|
55
|
+
|
|
56
|
+
No multiusuário (portal do Oracle), `/api/ia` e `/api/ai/status` mostravam a qualquer usuário o provedor, o endereço,
|
|
57
|
+
os modelos, o caminho do `ia.json` e os 4 últimos caracteres da chave, embora ele não pudesse editar nada.
|
|
58
|
+
|
|
59
|
+
- `/api/ia` no multiusuário devolve só `{ editable: false, configured }`; `/api/ai/status`, só `{ available,
|
|
60
|
+
configured, server: true }`.
|
|
61
|
+
- O Configurar IA, no servidor, vira um aviso: quem configura é quem administra, e se a IA está configurada. O
|
|
62
|
+
indicador do editor diz "A IA deste servidor está ligada" (ou "não está respondendo / não está configurada. Avise
|
|
63
|
+
quem administra"), sem endereço nem modelo; o progresso do chat diz "Enviando para a IA".
|
|
64
|
+
- Teste (`test/library-ui.test.js`): com a IA configurada no servidor, nada de chave, modelo, endereço ou arquivo nas
|
|
65
|
+
respostas nem na página do editor; o diálogo não tem campos. Falha sem a mudança.
|
|
66
|
+
|
|
67
|
+
## ia.json ilegível diz o motivo — 10/10/2026
|
|
68
|
+
|
|
69
|
+
Na migração do Oracle (o modelrelay saiu; os serviços `sagadeck` e `sagadeck-dev` falam direto com o OpenRouter, cada
|
|
70
|
+
um com seu `ia.json`), o arquivo foi criado como root e o serviço, que roda com usuário próprio, não conseguia lê-lo:
|
|
71
|
+
o sagadeck dizia só "IA não configurada", como se o arquivo nem existisse.
|
|
72
|
+
|
|
73
|
+
- `readIA` (`src/ai/ia-config.js`) separa "não existe" (só não configurada) de "existe e não serve": sem permissão,
|
|
74
|
+
ilegível, JSON quebrado, sem `url`. O motivo vai em `llmConfig().problem`.
|
|
75
|
+
- Aparece ao subir (`studioBanner`, a linha "IA: não configurada. …", que é o que quem administra vê no log), no erro
|
|
76
|
+
da chamada à IA, em `/api/ia` e `/api/ai/status`, na faixa da biblioteca, no Configurar IA e no indicador do editor.
|
|
77
|
+
- A faixa da biblioteca quebra o texto longo (o caminho do arquivo empurrava o botão para baixo de outro elemento).
|
|
78
|
+
- Testes: `test/ia-config.test.js` (pasta no lugar do arquivo, sem permissão fora do Windows, JSON quebrado, sem
|
|
79
|
+
endereço) e `test/library-ui.test.js` (faixa e Configurar IA dizem "Não deu para ler"). Falham sem a correção.
|
|
80
|
+
|
|
45
81
|
## CI verde de novo (e a regra: merge só com a CI verde) — 09/10/2026
|
|
46
82
|
|
|
47
83
|
A CI do GitHub estava vermelha havia dias (desde antes da 1.4.0) e 13 PRs foram mergeados e a 1.5.0 publicada assim.
|
|
@@ -108,6 +144,51 @@ os achados para os slides").
|
|
|
108
144
|
`missed`, elas entram no inventário com a caixa exata do texto e passam pela mesma releitura do recorte
|
|
109
145
|
(`transcribeEquation`) que escreve o LaTeX das outras. Saiu da lista.
|
|
110
146
|
|
|
147
|
+
## Primeiro uso: navegador abre sozinho, "Para começar" e a recomendação de IA — 09/10/2026
|
|
148
|
+
|
|
149
|
+
Pergunta de Narumi: quem instala pelo npm sabe pôr no ar, sabe a URL, precisa editar arquivo para a IA? Antes: um
|
|
150
|
+
comando (`sagadeck studio`), mas o navegador não abria, o `sagadeck` sozinho mostrava o `studio` como 19º item de uma
|
|
151
|
+
lista longa, e os campos de modelo do Configurar IA vinham vazios (ninguém sabe o nome exato de um modelo).
|
|
152
|
+
|
|
153
|
+
- `sagadeck studio` **abre o navegador** na página (`src/studio/open-browser.js`), inclusive quando já está aberto;
|
|
154
|
+
só para quem está no terminal: nunca em serviço/teste (sem TTY), CI, multiusuário, Linux sem tela ou
|
|
155
|
+
`--sem-navegador` / `SAGADECK_NO_BROWSER=1`. A primeira linha continua sendo o endereço.
|
|
156
|
+
- `sagadeck` sozinho começa com **"Para começar: sagadeck studio"** (o que faz, o endereço, deixar o terminal aberto).
|
|
157
|
+
- **Configurar IA** com a recomendação (`RECOMMENDED` em `src/ai/ia-config.js`: OpenRouter, DeepSeek para texto e
|
|
158
|
+
visão, Gemini para imagem, `:online` para busca) e **Usar a recomendação**, que preenche provedor e modelos. Ela é
|
|
159
|
+
também o teste da instalação: se nem com ela a IA responde, o problema é rede ou chave. Os campos trazem exemplos e
|
|
160
|
+
a tela diz que imagem e busca dependem do provedor. Sem listar os modelos do provedor (decisão: endereço próprio ou
|
|
161
|
+
de empresa pode não ter a lista, ou ser bloqueado).
|
|
162
|
+
- **Faixa "Configure a IA"** na biblioteca enquanto não está configurada (só onde se pode configurar; some ao salvar).
|
|
163
|
+
- Testes: `test/instance.test.js` (quando abre o navegador e o comando de cada sistema; `sagadeck` sozinho),
|
|
164
|
+
`test/library-ui.test.js` (faixa, recomendação preenche, salvar some a faixa).
|
|
165
|
+
- **Testes isolados de verdade**: a suíte completa falhou só nesta máquina num teste da lixeira, que leu as
|
|
166
|
+
Preferências reais (lixeira em 1 dia). 41 arquivos de teste não carregavam o isolamento (só os que importavam o
|
|
167
|
+
`helpers.js`). Agora `test/isolate.js` (biblioteca, ambientes, Preferências, IA e registro de comandos em pasta
|
|
168
|
+
temporária) é carregado antes de tudo pelo `npm test` (`--import`) e importado por todo arquivo de teste;
|
|
169
|
+
`guards.test.js` reprova arquivo de teste que não carregue.
|
|
170
|
+
- A avaliação ao vivo (`test/bench-live.test.js`) não fechava o navegador da revisão dos slides: o processo ficava
|
|
171
|
+
pendurado (dias). Fecha no fim; `guards.test.js` exige isso de todo teste que gera pelo caminho do Studio.
|
|
172
|
+
## Ambientes por campos e sem o botão no servidor — 09/10/2026
|
|
173
|
+
|
|
174
|
+
Pergunta de Narumi: quem instala do zero sabe preencher o `ambientes.yaml` (slides de API)? O lugar já era automático
|
|
175
|
+
(o Studio grava em `~/.sagadeck/ambientes.yaml`), mas preencher era editar YAML numa caixa de texto. E no servidor
|
|
176
|
+
(multiusuário) o botão abria a janela com "O serviço recusou o acesso…", parecendo defeito.
|
|
177
|
+
|
|
178
|
+
- **Por campos** (`src/studio/public/api-envs-form.js`, aba padrão): um cartão por ambiente com endereço base,
|
|
179
|
+
variáveis, segredos (digitado vai cifrado; em branco mantém; ou o nome de uma variável de ambiente), autenticação
|
|
180
|
+
(nenhuma; token fixo, que vira `Authorization: Bearer {{secret.token}}`; token que expira, client credentials) e
|
|
181
|
+
**Testar** (salva, chama o endereço base e pede o token). Sem ambiente próprio, a tela explica que o ENSAIO já
|
|
182
|
+
funciona. A aba **Como texto** é o editor de YAML de antes.
|
|
183
|
+
- Motor (`src/api-client.js`): `formState`/`saveForm` (o mesmo arquivo, preservando comentários e o que o
|
|
184
|
+
formulário não conhece: `ca`, outros cabeçalhos), `check` (o Testar), cabeçalhos do ambiente com `{{secret.…}}` e
|
|
185
|
+
client secret do token cifrado. Rotas `/api/http/ambientes/form` e `/api/http/ambientes/testar`.
|
|
186
|
+
- **No servidor o botão some** (o slide de API não executa lá; `/api/http/state` diz `live: false`).
|
|
187
|
+
- Testes: `test/api-client.test.js` (salvar e ler de volta sem o valor dos segredos, comentários e `ca` preservados,
|
|
188
|
+
token fixo no pedido, segredo mantido em branco, Testar), `test/studio.test.js` (instalação do zero pela tela, até o
|
|
189
|
+
Testar confirmar com o token chegando na API), `test/studio-scripts.test.js` (botão some no multiusuário; falhava
|
|
190
|
+
sem o módulo).
|
|
191
|
+
|
|
111
192
|
## IA embutida, só npm e o Studio como a página do sagadeck — 09/10/2026
|
|
112
193
|
|
|
113
194
|
Pedido de Narumi, depois de um agente (Copilot) instalar o sagadeck, abrir a página do modelrelay e, sem achar a
|
|
@@ -743,8 +824,8 @@ raciocínio, a mesma conferência sai em 1–2 s e acha o que importa (título r
|
|
|
743
824
|
resposta parava no meio perdia os itens → os que faltaram vão de novo, um por vez. Respostas que não serviram
|
|
744
825
|
ficam em `.sagadeck/transform/falhas/` (as 40 últimas).
|
|
745
826
|
- Casos de 02/10 (decks em `Pedidos de teste/`):
|
|
746
|
-
- **
|
|
747
|
-
|
|
827
|
+
- **Prateado**: o `prata` achado sem graça. Tema novo **`aluminio`**: a cara
|
|
828
|
+
de lançamento de aparelho premium (prata metálico com brilho de estúdio, alumínio escovado, título
|
|
748
829
|
cromado com a cor sólida em `color` para o fiscal medir, disco de metal polido na capa e na seção, cartões de
|
|
749
830
|
alumínio com chanfro, seção em azul-pacífico, escuro em grafite).
|
|
750
831
|
- **história dos videogames em 5 slides, temática, o último como mapa de Mario Kart visto de cima**: saiu no tema
|
package/docs/EXPERIENCIAS.md
CHANGED
|
@@ -6,7 +6,7 @@ Na biblioteca, escolha uma experiência para abrir um roteiro pronto e editável
|
|
|
6
6
|
|---|---|---|
|
|
7
7
|
| Sala de decisão | Estratégia e diretoria | Grafite e ouro, gráficos limpos, recomendação → evidências → riscos → decisão |
|
|
8
8
|
| Matéria de capa | História e investigação | Serifas, papel claro, enquadramentos amplos, pistas e virada narrativa |
|
|
9
|
-
| Modo espetáculo | Palco e
|
|
9
|
+
| Modo espetáculo | Palco e palestras | Contraste luminoso, escala monumental, pausas, escolhas da plateia e revelação por clique |
|
|
10
10
|
| Clube criativo | Workshop | Formas primárias, composição geométrica, desafios e timer |
|
|
11
11
|
| Laboratório vivo | Programação | Código por etapas, saída ilustrativa, screenshot com focos e previsão antes da resposta |
|
|
12
12
|
| Essencial | Produto e comunicação objetiva | Espaço vazio, uma ideia por vez, tipografia discreta e comparação |
|
package/docs/REFERENCIA.md
CHANGED
|
@@ -282,7 +282,9 @@ No Studio, a aba **Variáveis** mostra o ambiente atual em uma tabela **Nome / V
|
|
|
282
282
|
normais ali; nomes e valores são salvos no arquivo de ambientes, nunca no deck. Arraste um nome da tabela
|
|
283
283
|
para um campo do slide para inserir `{{nome}}`. Segredos são listados pelo nome, mas seu valor nunca aparece.
|
|
284
284
|
Os campos `save` do deck também aparecem com sua origem e disponibilidade.
|
|
285
|
-
O editor de ambientes
|
|
285
|
+
O editor de ambientes abre **por campos** (endereço base, variáveis, segredos, autenticação: nenhuma, token fixo
|
|
286
|
+
`Authorization: Bearer {{secret.token}}` ou token que expira; **Testar** chama o endereço base e pede o token). A aba
|
|
287
|
+
**Como texto** valida o YAML antes de habilitar **Salvar**. Os dois preservam a versão anterior em `ambientes.yaml.bak`.
|
|
286
288
|
O botão **Variáveis** (na barra do slide) é o inspect/watch da apresentação, tudo num lugar só:
|
|
287
289
|
- **Ambiente**: as variáveis (criar, editar, apagar) e as **protegidas** (marque "protegida" ao criar): na tela só o nome
|
|
288
290
|
e ••••; o olhinho mostra o valor por 15 segundos. Nos slides e no código gerado aparece o nome (`{{secret.chave}}`),
|
|
@@ -334,8 +336,9 @@ texto) para o modo sem Studio.
|
|
|
334
336
|
|
|
335
337
|
Endereços, credenciais e segredos ficam **na máquina**, nunca no deck (o deck pode ir para o GitHub).
|
|
336
338
|
Outro lugar: variável `SAGADECK_AMBIENTES`. O selo no slide (DEV, HOM…) troca o ambiente. No Studio,
|
|
337
|
-
**Inserir → Ambientes**
|
|
338
|
-
antes de gravar e troca o ambiente em uso.
|
|
339
|
+
**Inserir → Ambientes** preenche o arquivo por campos (ou mostra o YAML na aba Como texto, com um modelo comentado
|
|
340
|
+
se ele ainda não existe), confere antes de gravar e troca o ambiente em uso. Os cabeçalhos do ambiente (`headers`)
|
|
341
|
+
aceitam `{{secret.nome}}`; o client secret do token pode ficar cifrado (`client_secret: "dpapi:…"` no Windows).
|
|
339
342
|
|
|
340
343
|
```yaml
|
|
341
344
|
current: hom
|
|
@@ -560,7 +563,7 @@ Linhas que começam com PALAVRAS EM MAIÚSCULAS seguidas de `:` viram etiquetas
|
|
|
560
563
|
|
|
561
564
|
Como no PowerPoint, são duas escolhas independentes:
|
|
562
565
|
|
|
563
|
-
- **Tema** (`theme`): fontes, raio das bordas, textura e uma **pele** própria que rearruma capa, seção e encerramento, muda o jeito dos slides de conteúdo (título, cartões, marcadores da lista, citação, número grande, frase) e põe ornamentos (`sinal`: faixa zebrada e selo no chapéu; `editorial`: fios e capa centralizada; `noite`: moldura fina; `bauhaus`: círculo, quadrado e triângulo; `terminal`: janela com barra e prompt; `jornal`: fios de jornal e manchete sublinhada; `rabisco`: fitas adesivas e títulos inclinados; `oceano`: ondas e chapéu em pílula; `pop`: adesivos e sombra chapada; `aurora`: brilhos e traço em gradiente; `prata`: limpo e centralizado
|
|
566
|
+
- **Tema** (`theme`): fontes, raio das bordas, textura e uma **pele** própria que rearruma capa, seção e encerramento, muda o jeito dos slides de conteúdo (título, cartões, marcadores da lista, citação, número grande, frase) e põe ornamentos (`sinal`: faixa zebrada e selo no chapéu; `editorial`: fios e capa centralizada; `noite`: moldura fina; `bauhaus`: círculo, quadrado e triângulo; `terminal`: janela com barra e prompt; `jornal`: fios de jornal e manchete sublinhada; `rabisco`: fitas adesivas e títulos inclinados; `oceano`: ondas e chapéu em pílula; `pop`: adesivos e sombra chapada; `aurora`: brilhos e traço em gradiente; `prata`: limpo e centralizado; `relevo`: ficção científica sóbria (sala de controle de filme, sem néon): grafite profundo, curvas de nível ao fundo, cantoneiras de mira, título leve embaixo à esquerda, rótulos técnicos em monoespaçada; combina com ilustração 3D realista (terreno, cidade, água) e gráfico 3D (`science` com superfície); `arcade`: videogame e fliperama, tela de tubo com linhas de varredura, neon, título em pixel no meio e chão de tijolos de jogo de plataforma; `aluminio`: prata metálico de lançamento de aparelho premium, título cromado embaixo à esquerda e um disco de metal polido; `manual` e `manual-noite`: documentação técnica, sem ornamento, código e texto corrido legíveis — o par claro/escuro do mesmo arranjo, para material de consulta, tutorial e apostila: `manual` imprime bem, `manual-noite` descansa a vista). Nos slides de conteúdo: `sinal` barra de aviso e placas; `editorial` fios finos e citação centralizada; `noite` tudo centralizado e cartões vazados; `bauhaus` faixas de cor primária e círculos; `terminal` título como comando e cartões-janela; `jornal` fios duplos e colunas; `oceano` cartões flutuando e balões; `pop` contorno grosso e sombra dura; `prata` centralizado e sem caixa; `aluminio` cartões de alumínio com chanfro de luz; `relevo` painéis de vidro fosco com traço gelo; `arcade` cartões de 8 bits com borda de pixel e sombra dura; `rabisco` tracejados e números circulados; `aurora` vidro com brilho. Trocar o tema já muda o arranjo: não reescreva slides só para "combinar" com o tema.
|
|
564
567
|
- **Paleta** (`palette`): só as cores, em qualquer tema. `tinta`, `floresta`, `mar`, `entardecer`, `lavanda`, `grafite`, `neon`, `areia`, `cereja`, `corporativo`, `luzquente`, ou as suas: `palette: { paper: "F4F1EA", ink: "161616", accent: "D7263D", alert: "1B998B" }`. Os quatro tons saem dessas quatro cores, com contraste garantido. A paleta troca também o fundo: em tema escuro por natureza (`relevo`, `arcade`, `aurora`, `noite`, `terminal`), uma paleta de papel claro (`safira`, `corporativo`, `tinta`…) deixa o tema claro; para manter o visual escuro que o pedido quer, não ponha paleta (ou use uma de papel escuro, como `neon`).
|
|
565
568
|
- **Paletas de família** (para identidade de marca sem cansar a vista): a cor forte da marca fica só no detalhe (`alert`, a ênfase pontual) e a página trabalha com os parentes mais agradáveis dela (`accent` e `family`). `rubi` (rosas, magenta e vinho; vermelho só no detalhe), `ametista` (roxos e lilases), `tangerina` (laranja com azul-marinho), `safira` (azuis com um toque âmbar), `esmeralda` (verdes). Na sua: `palette: { paper: "FFFFFF", ink: "3B2B33", accent: "B83A6E", alert: "CC092F", family: ["F9DCE5", "EFA3BC", "D9668F", "7E2349"] }` (de 2 a 8 parentes). `family` pinta as séries extras dos gráficos e os grupos e ramos dos diagramas. Pedido de "cores da empresa X": monte uma paleta de família assim, com a cor forte em `alert`. Para clima futurista de bom gosto (luz quente de prédios e shows de drones, nunca neon barato): `luzquente` — brancos quentes e âmbar que funcionam no claro e, no `tone: dark`, viram HUD âmbar sobre carvão quente.
|
|
566
569
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sagadeck",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Gerador de apresentações: YAML -> HTML animado + PowerPoint editável, com temas e figuras geradas na hora.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"dev": "node bin/sagadeck.js studio",
|
|
40
40
|
"build:code-lab": "node scripts/bundle-code-lab.mjs",
|
|
41
41
|
"test:live": "node tools/test-live.mjs",
|
|
42
|
-
"test": "node --test --test-concurrency=1 \"test/*.test.js\"",
|
|
43
|
-
"test:unit": "node --test test/engine.test.js",
|
|
44
|
-
"test:ui": "node --test --test-concurrency=1 test/runtime.test.js test/studio.test.js"
|
|
42
|
+
"test": "node --import ./test/isolate.js --test --test-concurrency=1 \"test/*.test.js\"",
|
|
43
|
+
"test:unit": "node --import ./test/isolate.js --test test/engine.test.js",
|
|
44
|
+
"test:ui": "node --import ./test/isolate.js --test --test-concurrency=1 test/runtime.test.js test/studio.test.js"
|
|
45
45
|
},
|
|
46
46
|
"repository": {
|
|
47
47
|
"type": "git",
|
package/src/ai/ia-config.js
CHANGED
|
@@ -13,6 +13,12 @@ export const PROVIDERS = {
|
|
|
13
13
|
openrouter: { label: "OpenRouter", url: "https://openrouter.ai/api/v1", keyEnv: "OPENROUTER_API_KEY" },
|
|
14
14
|
openai: { label: "OpenAI", url: "https://api.openai.com/v1", keyEnv: "OPENAI_API_KEY" },
|
|
15
15
|
};
|
|
16
|
+
// Recomendação para começar, mostrada na tela Configurar IA. Serve também de teste da instalação: se nem com ela a IA
|
|
17
|
+
// responde, o problema é o ambiente (rede, chave), não o modelo escolhido. Depois, cada um põe o modelo que quiser.
|
|
18
|
+
export const RECOMMENDED = {
|
|
19
|
+
provider: "openrouter",
|
|
20
|
+
models: { text: "deepseek/deepseek-v4.1-flash", vision: "deepseek/deepseek-v4.1-flash", image: "google/gemini-3.1-flash-image", search: "deepseek/deepseek-v4.1-flash:online" },
|
|
21
|
+
};
|
|
16
22
|
// os papéis que o sagadeck usa; cada um aponta para um modelo do provedor
|
|
17
23
|
export const ROLES = ["text", "vision", "image", "search"];
|
|
18
24
|
|
|
@@ -34,10 +40,25 @@ export function cleanIA(raw) {
|
|
|
34
40
|
return out.url || out.adaptador ? out : null;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// lê o arquivo e, quando ele existe mas não serve, diz por quê (sem isso, um ia.json de outro dono, que o serviço não
|
|
44
|
+
// consegue ler, aparecia só como "IA não configurada"). Sem arquivo não é problema: é só não configurada.
|
|
45
|
+
export function readIA(file = iaFile()) {
|
|
46
|
+
let text;
|
|
47
|
+
try { text = fs.readFileSync(file, "utf8"); }
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e.code === "ENOENT") return { config: null, problem: "" };
|
|
50
|
+
if (e.code === "EACCES" || e.code === "EPERM") return { config: null, problem: `O arquivo ${file} existe, mas este processo não tem permissão para lê-lo. Confira o dono e as permissões dele.` };
|
|
51
|
+
return { config: null, problem: `Não deu para ler ${file} (${e.code || e.message}).` };
|
|
52
|
+
}
|
|
53
|
+
let raw;
|
|
54
|
+
try { raw = JSON.parse(text); } catch (e) { return { config: null, problem: `O arquivo ${file} não é um JSON válido (${e.message}).` }; }
|
|
55
|
+
const config = cleanIA(raw);
|
|
56
|
+
return config ? { config, problem: "" } : { config: null, problem: `O arquivo ${file} não tem o endereço do provedor ("url").` };
|
|
39
57
|
}
|
|
40
58
|
|
|
59
|
+
export const loadIA = (file = iaFile()) => readIA(file).config;
|
|
60
|
+
export const iaProblem = (file = iaFile()) => readIA(file).problem;
|
|
61
|
+
|
|
41
62
|
// grava de forma atômica, só para quem é dono da conta (a chave mora aqui)
|
|
42
63
|
export function saveIA(cfg, file = iaFile()) {
|
|
43
64
|
const clean = cleanIA(cfg);
|
package/src/ai/llm.js
CHANGED
|
@@ -17,7 +17,7 @@ import { mediaPrompt } from './art-direction.js';
|
|
|
17
17
|
import { currentUsage, recordUsage } from './usage.js';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { pathToFileURL } from 'node:url';
|
|
20
|
-
import { iaFile,
|
|
20
|
+
import { iaFile, readIA, keyOf, migrateModelrelay } from './ia-config.js';
|
|
21
21
|
import {setTimeout as delay} from 'node:timers/promises';
|
|
22
22
|
|
|
23
23
|
export class LLMError extends Error {
|
|
@@ -38,7 +38,7 @@ let migrated = false;
|
|
|
38
38
|
export function llmConfig(env = process.env) {
|
|
39
39
|
const fromEnv = !!env.SAGADECK_LLM_URL;
|
|
40
40
|
if (!fromEnv && !migrated) { migrated = true; try { migrateModelrelay({ env, file: iaFile(env) }); } catch { /* segue sem */ } }
|
|
41
|
-
const ia = fromEnv ? null :
|
|
41
|
+
const { config: ia, problem } = fromEnv ? { config: null, problem: "" } : readIA(iaFile(env));
|
|
42
42
|
const m = ia?.models || {};
|
|
43
43
|
return {
|
|
44
44
|
url: (env.SAGADECK_LLM_URL || ia?.url || "").replace(/\/+$/, ""),
|
|
@@ -51,6 +51,8 @@ export function llmConfig(env = process.env) {
|
|
|
51
51
|
headers: ia?.headers || {},
|
|
52
52
|
adaptador: ia?.adaptador || "",
|
|
53
53
|
source: fromEnv ? "variável" : ia ? "arquivo" : "nenhuma",
|
|
54
|
+
// o ia.json existe mas não serve (sem permissão, JSON quebrado): o motivo, para não dizer só "não configurada"
|
|
55
|
+
problem,
|
|
54
56
|
timeoutMs: Number(env.SAGADECK_LLM_TIMEOUT || 180) * 1000,
|
|
55
57
|
firstTimeoutMs: Number(env.SAGADECK_LLM_FIRST_TIMEOUT || 600) * 1000,
|
|
56
58
|
};
|
|
@@ -79,7 +81,7 @@ async function send(cfg, url, init) {
|
|
|
79
81
|
if (typeof fn !== "function") throw new LLMError(`O adaptador ${cfg.adaptador} não exporta uma função padrão (url, init) => Response.`);
|
|
80
82
|
return fn(url, init);
|
|
81
83
|
}
|
|
82
|
-
const notConfigured = () => new LLMError("A IA não está configurada: no Studio, abra Configurar IA (ou crie ~/.sagadeck/ia.json).", { code: "AI_NOT_CONFIGURED" });
|
|
84
|
+
const notConfigured = (cfg) => new LLMError(cfg?.problem ? `A IA não está configurada. ${cfg.problem}` : "A IA não está configurada: no Studio, abra Configurar IA (ou crie ~/.sagadeck/ia.json).", { code: "AI_NOT_CONFIGURED" });
|
|
83
85
|
const howToFix = "Confira o provedor e a chave em Configurar IA, no Studio.";
|
|
84
86
|
|
|
85
87
|
// Disponibilidade com cache curto, para o Studio decidir entre LLM e as regras determinísticas.
|
|
@@ -182,7 +184,7 @@ async function chatOnce(messages, opts = {}) {
|
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
async function chatOnceRaw(messages, { model, temperature, maxTokens, onDelta, signal, reasoningOff, cfg = llmConfig() } = {}) {
|
|
185
|
-
if (!llmConfigured(cfg)) throw notConfigured();
|
|
187
|
+
if (!llmConfigured(cfg)) throw notConfigured(cfg);
|
|
186
188
|
const body = { model: resolveModel(cfg, model || cfg.textModel), messages };
|
|
187
189
|
if (reasoningOff) body.reasoning = { enabled: false };
|
|
188
190
|
if (temperature !== undefined) body.temperature = temperature;
|
package/src/ai/variety.js
CHANGED
|
@@ -38,7 +38,7 @@ export function varietyReport(spec) {
|
|
|
38
38
|
// Direções criativas: cada deck gerado sorteia um jeito de contar (e de se parecer), para dois decks sobre
|
|
39
39
|
// assuntos parecidos não saírem iguais.
|
|
40
40
|
export const CREATIVE_DIRECTIONS = [
|
|
41
|
-
"
|
|
41
|
+
"Palco minimalista: uma frase de abertura sem ilustração, grandes espaços vazios, composição central, um número por ideia. Considere prata; destaque em cor apenas na virada.",
|
|
42
42
|
"Investigação editorial: abra com uma cena concreta, revele pistas por enquadramentos e comparações, entregue a conclusão no final. Considere editorial; serifas, linhas finas, papel claro e um único contraste escuro.",
|
|
43
43
|
"Workshop geométrico: abra com uma escolha da plateia, alterne desafios curtos, diagramas e timer. Considere bauhaus ou pop; formas primárias, assimetria e revelação por clique.",
|
|
44
44
|
"Estudo de caso documental: contexto → problema → tentativa → resultado → lição. Considere jornal; manchetes, evidências visuais, cronologia e legendas curtas. Só use dados do briefing ou dados verificados.",
|
package/src/api-client.js
CHANGED
|
@@ -25,6 +25,7 @@ import { connectWs } from "./ws.js";
|
|
|
25
25
|
import { protect, unprotect, unprotectAll, PROTECTION } from "./protect.js";
|
|
26
26
|
|
|
27
27
|
const C = globalThis.SagadeckApiCore;
|
|
28
|
+
const BEARER = "Bearer {{secret.token}}"; // o "token fixo" do formulário: cabeçalho em todo pedido do ambiente
|
|
28
29
|
|
|
29
30
|
// As variáveis e segredos dos slides de API moram FORA das apresentações: ~/.sagadeck/ambientes.yaml (no Windows,
|
|
30
31
|
// C:\Users\<você>\.sagadeck\ambientes.yaml) ou SAGADECK_AMBIENTES. Mandar um deck para alguém nunca leva segredo
|
|
@@ -128,7 +129,7 @@ export class ApiEnvironments {
|
|
|
128
129
|
catch (e) { throw new ApiError(`${this.file}: YAML inválido: ${e.message}`, "config"); }
|
|
129
130
|
const envs = data.environments && typeof data.environments === "object" ? data.environments : {};
|
|
130
131
|
// segredos cifrados (dpapi:…): decifra todos de uma vez (um PowerShell só)
|
|
131
|
-
try { unprotectAll(Object.values(envs).flatMap((e) => Object.values((e && e.secrets) || {}))); }
|
|
132
|
+
try { unprotectAll(Object.values(envs).flatMap((e) => [...Object.values((e && e.secrets) || {}), e?.token?.client_secret].filter((v) => typeof v === "string"))); }
|
|
132
133
|
catch (e) { throw new ApiError(`${this.file}: ${e.message}`, "config"); }
|
|
133
134
|
return { current: data.current || null, environments: envs };
|
|
134
135
|
}
|
|
@@ -236,6 +237,106 @@ export class ApiEnvironments {
|
|
|
236
237
|
return this.state();
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
// ---- tela Ambientes por campos (src/studio/public/api-envs-form.js) ----------------------------------------------
|
|
241
|
+
// O mesmo arquivo, visto como formulário: endereço base, variáveis, segredos (só o nome e de onde vêm; o valor nunca
|
|
242
|
+
// sai) e a autenticação (nenhuma, token fixo ou token que expira). O que o formulário não conhece (ca, insecure,
|
|
243
|
+
// outros cabeçalhos) fica no arquivo como estava.
|
|
244
|
+
formState() {
|
|
245
|
+
const raw = fs.existsSync(this.file) ? (YAML.parse(fs.readFileSync(this.file, "utf8")) || {}) : {};
|
|
246
|
+
const envs = raw.environments && typeof raw.environments === "object" ? raw.environments : {};
|
|
247
|
+
return {
|
|
248
|
+
file: this.file, exists: fs.existsSync(this.file), protection: PROTECTION, current: raw.current || null,
|
|
249
|
+
builtin: Object.keys(this.builtin),
|
|
250
|
+
envs: Object.entries(envs).map(([name, e = {}]) => {
|
|
251
|
+
const bearer = e.headers?.Authorization === BEARER;
|
|
252
|
+
const auth = e.token?.url ? { type: "token", url: e.token.url, client_id: e.token.client_id || "", field: e.token.field || "", secretEnv: e.token.client_secret_env || "", secretSet: e.token.client_secret != null }
|
|
253
|
+
: bearer ? { type: "bearer", secretEnv: typeof e.secrets?.token === "object" ? e.secrets.token.env || "" : "", secretSet: typeof e.secrets?.token === "string" }
|
|
254
|
+
: { type: "nenhuma" };
|
|
255
|
+
const otherHeaders = Object.keys(e.headers || {}).filter((h) => !(bearer && h === "Authorization"));
|
|
256
|
+
return {
|
|
257
|
+
name, base: e.vars?.base != null ? String(e.vars.base) : "",
|
|
258
|
+
vars: Object.entries(e.vars || {}).filter(([k]) => k !== "base").map(([k, v]) => ({ name: k, value: String(v) })),
|
|
259
|
+
secrets: Object.entries(e.secrets || {}).filter(([k]) => !(bearer && k === "token")).map(([k, v]) => ({ name: k, env: typeof v === "object" && v ? v.env || "" : "", set: typeof v === "string" })),
|
|
260
|
+
auth, advanced: !!(e.ca || e.insecure || otherHeaders.length),
|
|
261
|
+
};
|
|
262
|
+
}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
saveForm(list, { current } = {}) {
|
|
267
|
+
if (!Array.isArray(list)) throw new ApiError("Mande a lista de ambientes.", "config");
|
|
268
|
+
const NAME = /^[A-Za-z0-9_-]{1,40}$/, VAR = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
269
|
+
const seen = new Set();
|
|
270
|
+
for (const f of list) {
|
|
271
|
+
if (!NAME.test(String(f?.name || ""))) throw new ApiError(`Nome de ambiente inválido: "${f?.name || ""}". Use letras, números, - e _ (ex.: dev, hom, prod).`, "config");
|
|
272
|
+
if (seen.has(f.name)) throw new ApiError(`O ambiente "${f.name}" aparece duas vezes.`, "config");
|
|
273
|
+
seen.add(f.name);
|
|
274
|
+
if (f.base && !/^https?:\/\//i.test(f.base)) throw new ApiError(`Ambiente "${f.name}": o endereço base começa com http:// ou https://.`, "config");
|
|
275
|
+
for (const v of [...(f.vars || []), ...(f.secrets || [])]) if (!VAR.test(String(v?.name || ""))) throw new ApiError(`Ambiente "${f.name}": nome inválido "${v?.name || ""}". Use letras, números e _ (ex.: path_id).`, "config");
|
|
276
|
+
if (f.auth?.type === "token" && !/^https?:\/\//i.test(f.auth.url || "")) throw new ApiError(`Ambiente "${f.name}": falta o endereço do token (https://…).`, "config");
|
|
277
|
+
}
|
|
278
|
+
this.editFile((doc) => {
|
|
279
|
+
const old = doc.toJS()?.environments || {};
|
|
280
|
+
for (const name of Object.keys(old)) if (!seen.has(name)) doc.deleteIn(["environments", name]);
|
|
281
|
+
for (const f of list) {
|
|
282
|
+
const at = (...k) => ["environments", f.name, ...k];
|
|
283
|
+
const prev = old[f.name] || {};
|
|
284
|
+
if (!doc.hasIn(at())) doc.setIn(at(), doc.createNode({}));
|
|
285
|
+
// variáveis: base + as da tabela; a que saiu da tabela sai do arquivo
|
|
286
|
+
const vars = { ...(f.base ? { base: f.base } : {}), ...Object.fromEntries((f.vars || []).map((v) => [v.name, String(v.value ?? "")])) };
|
|
287
|
+
for (const k of Object.keys(prev.vars || {})) if (!(k in vars)) doc.deleteIn(at("vars", k));
|
|
288
|
+
for (const [k, v] of Object.entries(vars)) doc.setIn(at("vars", k), v);
|
|
289
|
+
// segredos: valor novo (cifrado), variável de ambiente, ou fica o que já estava
|
|
290
|
+
const keepToken = f.auth?.type === "bearer";
|
|
291
|
+
const wanted = new Set([...(f.secrets || []).map((x) => x.name), ...(keepToken ? ["token"] : [])]);
|
|
292
|
+
for (const k of Object.keys(prev.secrets || {})) if (!wanted.has(k)) doc.deleteIn(at("secrets", k));
|
|
293
|
+
const putSecret = (name, { value, env } = {}) => {
|
|
294
|
+
if (env) doc.setIn(at("secrets", name), doc.createNode({ env }));
|
|
295
|
+
else if (value) doc.setIn(at("secrets", name), protect(String(value)));
|
|
296
|
+
else if (!doc.hasIn(at("secrets", name))) throw new ApiError(`Ambiente "${f.name}": falta o valor do segredo "${name}".`, "config");
|
|
297
|
+
};
|
|
298
|
+
for (const x of f.secrets || []) putSecret(x.name, x);
|
|
299
|
+
// autenticação
|
|
300
|
+
const ourBearer = doc.getIn(at("headers", "Authorization")) === BEARER;
|
|
301
|
+
if (f.auth?.type === "bearer") {
|
|
302
|
+
putSecret("token", { value: f.auth.value, env: f.auth.secretEnv });
|
|
303
|
+
doc.setIn(at("headers", "Authorization"), BEARER);
|
|
304
|
+
} else if (ourBearer) doc.deleteIn(at("headers", "Authorization"));
|
|
305
|
+
if (doc.getIn(at("headers")) && !Object.keys(doc.getIn(at("headers")).toJSON?.() || {}).length) doc.deleteIn(at("headers"));
|
|
306
|
+
if (f.auth?.type === "token") {
|
|
307
|
+
const t = { url: f.auth.url, ...(f.auth.client_id ? { client_id: f.auth.client_id } : {}), ...(f.auth.field ? { field: f.auth.field } : {}) };
|
|
308
|
+
for (const k of ["url", "client_id", "field"]) if (!(k in t)) doc.deleteIn(at("token", k)); else doc.setIn(at("token", k), t[k]);
|
|
309
|
+
if (f.auth.secretEnv) { doc.setIn(at("token", "client_secret_env"), f.auth.secretEnv); doc.deleteIn(at("token", "client_secret")); }
|
|
310
|
+
else if (f.auth.value) { doc.setIn(at("token", "client_secret"), protect(String(f.auth.value))); doc.deleteIn(at("token", "client_secret_env")); }
|
|
311
|
+
else if (!doc.hasIn(at("token", "client_secret")) && !doc.hasIn(at("token", "client_secret_env"))) throw new ApiError(`Ambiente "${f.name}": falta o segredo do token (client secret).`, "config");
|
|
312
|
+
} else if (doc.hasIn(at("token"))) doc.deleteIn(at("token"));
|
|
313
|
+
}
|
|
314
|
+
const names = list.map((f) => f.name);
|
|
315
|
+
doc.set("current", names.includes(current) ? current : names[0] || null);
|
|
316
|
+
});
|
|
317
|
+
this.tokens.clear();
|
|
318
|
+
return this.formState();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Testar um ambiente salvo: o endereço base responde? o token sai? (só diz o que aconteceu; nada vai para a tela)
|
|
322
|
+
async check(name) {
|
|
323
|
+
const env = this.env(name);
|
|
324
|
+
const out = { name: env.name };
|
|
325
|
+
const base = env.vars?.base;
|
|
326
|
+
if (base) {
|
|
327
|
+
try {
|
|
328
|
+
const res = await open({ method: "GET", url: String(base), headers: { Accept: "*/*", ...C.render(env.headers || {}, this.secretVars(env)) }, ca: this.ca(env), insecure: !!env.insecure, timeout: 10000 });
|
|
329
|
+
res.resume();
|
|
330
|
+
out.base = { ok: true, status: res.statusCode };
|
|
331
|
+
} catch (e) { out.base = { ok: false, error: this.maskText(env, e.message) }; }
|
|
332
|
+
}
|
|
333
|
+
if (env.token?.url) {
|
|
334
|
+
try { const v = await this.token(env, { renew: true }); out.token = { ok: true, last4: v.slice(-4) }; }
|
|
335
|
+
catch (e) { out.token = { ok: false, error: this.maskText(env, e.message) }; }
|
|
336
|
+
}
|
|
337
|
+
return out;
|
|
338
|
+
}
|
|
339
|
+
|
|
239
340
|
env(name) {
|
|
240
341
|
const data = this.load();
|
|
241
342
|
const n = name || this.currentName(data);
|
|
@@ -257,7 +358,7 @@ export class ApiEnvironments {
|
|
|
257
358
|
|
|
258
359
|
secrets(env) {
|
|
259
360
|
const t = env.token || {};
|
|
260
|
-
const out = [this.tokens.get(env.name)?.value, t.client_secret, t.client_secret_env && process.env[t.client_secret_env], ...Object.values(env.headers || {}), ...Object.values(this.secretVars(env))];
|
|
361
|
+
const out = [this.tokens.get(env.name)?.value, t.client_secret != null ? unprotect(t.client_secret) : null, t.client_secret_env && process.env[t.client_secret_env], ...Object.values(env.headers || {}), ...Object.values(this.secretVars(env))];
|
|
261
362
|
return out.filter((s) => typeof s === "string" && s.length >= 6);
|
|
262
363
|
}
|
|
263
364
|
|
|
@@ -273,7 +374,7 @@ export class ApiEnvironments {
|
|
|
273
374
|
const cached = this.tokens.get(env.name);
|
|
274
375
|
if (!renew && cached && Date.now() < cached.renewAt) return cached.value;
|
|
275
376
|
const id = t.client_id ?? (t.client_id_env && process.env[t.client_id_env]);
|
|
276
|
-
const secret = t.client_secret ?? (t.client_secret_env && process.env[t.client_secret_env]);
|
|
377
|
+
const secret = (t.client_secret != null ? unprotect(t.client_secret) : null) ?? (t.client_secret_env && process.env[t.client_secret_env]);
|
|
277
378
|
if (!id || !secret) throw new ApiError(`Ambiente "${env.name}": falta client_id/client_secret do token (ou a variável ${t.client_secret_env || t.client_id_env || "…"})`, "config");
|
|
278
379
|
const fields = { [t.id_field || "client_id"]: id, [t.secret_field || "client_secret"]: secret, ...(t.extra || {}) };
|
|
279
380
|
const form = t.format === "form";
|
|
@@ -320,7 +421,7 @@ export class ApiEnvironments {
|
|
|
320
421
|
throw new ApiError(`Ambiente "${env.name}" não tem ${names.join(", ")} configurado em secrets:`, "config");
|
|
321
422
|
}
|
|
322
423
|
req = { ...req, url: C.render(req.url, sv), headers: C.render(req.headers || {}, sv), body: C.render(req.body, sv) };
|
|
323
|
-
const headers = { Accept: "application/json, text/event-stream, */*", ...(env.headers || {}), ...(req.headers || {}) };
|
|
424
|
+
const headers = { Accept: "application/json, text/event-stream, */*", ...C.render(env.headers || {}, sv), ...(req.headers || {}) };
|
|
324
425
|
let body = req.body;
|
|
325
426
|
const file = req.file || null; // { name, type, data: Buffer } — o arquivo do slide (upload ou base64)
|
|
326
427
|
const needsFile = (req.form && Object.values(req.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(body ?? ""));
|
|
@@ -488,7 +589,7 @@ export class ApiEnvironments {
|
|
|
488
589
|
const sv = this.secretVars(env);
|
|
489
590
|
let url = C.render(String(rt.url || ""), { ...(env.vars || {}), ...sv });
|
|
490
591
|
const t = env.token || {};
|
|
491
|
-
const headers = { ...(env.headers || {}) };
|
|
592
|
+
const headers = { ...C.render(env.headers || {}, sv) };
|
|
492
593
|
const auth = String(rt.auth || "header");
|
|
493
594
|
if (auth !== "none") {
|
|
494
595
|
const tok = await this.token(env);
|