sagadeck 1.8.0 → 1.10.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/README.md +25 -8
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +482 -7
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +178 -111
- package/package.json +35 -4
- package/src/ai/commands.js +2 -2
- package/src/ai/context.js +7 -2
- package/src/ai/deck-ai.js +4 -2
- package/src/ai/ia-check.js +127 -0
- package/src/ai/ia-config.js +2 -2
- package/src/ai/llm.js +5 -1
- package/src/ai/map-analysis-run.js +1 -1
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/api-client.js +42 -10
- package/src/build.js +46 -14
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/elements.js +39 -1
- package/src/export/pptx.js +7 -2
- package/src/export/shots.js +7 -2
- package/src/fiscal/normalize.js +19 -0
- package/src/fs-retry.js +37 -2
- package/src/import/office.js +5 -4
- package/src/layouts.js +104 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +4 -1
- package/src/map-config.js +10 -4
- package/src/map.js +21 -8
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/base.css +88 -332
- package/src/runtime/fit.js +3 -2
- package/src/runtime/fonts/amatic-sc.css +3 -0
- package/src/runtime/fonts/bebas-neue.css +2 -0
- package/src/runtime/fonts/cabin-sketch.css +2 -0
- package/src/runtime/fonts/fraunces.css +3 -0
- package/src/runtime/fonts/gelasio.css +3 -0
- package/src/runtime/fonts/ibm-plex-mono.css +4 -0
- package/src/runtime/fonts/index.json +10 -1
- package/src/runtime/fonts/itim.css +2 -0
- package/src/runtime/fonts/montserrat.css +2 -0
- package/src/runtime/fonts/space-grotesk.css +2 -0
- package/src/runtime/map-background.js +91 -0
- package/src/runtime/map.js +41 -9
- package/src/runtime/runtime.js +81 -100
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/caderno.css +33 -0
- package/src/runtime/skins/cartaz.css +27 -0
- package/src/runtime/skins/infografico.css +29 -0
- package/src/runtime/skins/lousa.css +22 -0
- package/src/runtime/skins/musgo.css +42 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
- package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
- package/src/runtime/vendor/geo-contornos.json +1 -1
- package/src/runtime/vendor/maplibre-gl.css +1 -0
- package/src/runtime/vendor/maplibre-gl.js +825 -0
- package/src/runtime/vendor/maplibre-leaflet.js +234 -0
- package/src/runtime/vendor/maplibre-worker.js +10 -0
- package/src/studio/ai-chat-route.js +26 -7
- package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
- package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
- package/src/studio/command-approval.js +2 -3
- package/src/studio/demo-decks.js +4 -2
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/export-jobs.js +86 -0
- package/src/studio/exporting.js +36 -18
- package/src/studio/feature-demos.js +144 -0
- package/src/studio/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +9 -18
- package/src/studio/library-routes.js +7 -9
- package/src/studio/project.js +1 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/ai-settings.js +2 -2
- package/src/studio/public/app.js +43 -52
- package/src/studio/public/chat.js +5 -1
- package/src/studio/public/developer-authoring.css +12 -0
- package/src/studio/public/developer-authoring.js +306 -0
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/generation-guide.js +42 -0
- package/src/studio/public/index.html +31 -10
- package/src/studio/public/library.css +48 -3
- package/src/studio/public/library.html +5 -1
- package/src/studio/public/library.js +150 -41
- package/src/studio/public/map-data.js +7 -3
- package/src/studio/public/map-editor.js +625 -236
- package/src/studio/public/map-settings.js +3 -1
- package/src/studio/public/slide-form.js +27 -29
- package/src/studio/public/style.css +56 -9
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +29 -14
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +89 -7
- package/src/themes.js +202 -0
- package/templates/cenario-e-ciencia.yaml +1 -21
- package/templates/ensaio-api.yaml +3 -3
- package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
- package/templates/cenario/imagens/cidade.svg +0 -30
- package/templates/cenario/imagens/pessoa.svg +0 -15
|
@@ -105,8 +105,23 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
105
105
|
res.on('close', onClose);
|
|
106
106
|
// Capture o arquivo da tarefa: abrir outro deck não redireciona a escrita.
|
|
107
107
|
const taskWorkspace = { ...owner, spec: structuredClone(owner.spec) };
|
|
108
|
+
const originFile = owner.file, originPreview = owner.preview;
|
|
109
|
+
let materialized = null;
|
|
108
110
|
const prompt = body.message || "";
|
|
109
111
|
const spec = body.spec || taskWorkspace.spec;
|
|
112
|
+
// Conversar com uma prévia não cria arquivo. Edição/imagem ou documento binário precisam da cópia pessoal.
|
|
113
|
+
const imageOptionsFor = (deck = spec) => {
|
|
114
|
+
const options = imageOptions(taskWorkspace, withBase(taskWorkspace, deck));
|
|
115
|
+
if (originPreview && !originFile && taskWorkspace.file && !materialized) {
|
|
116
|
+
materialized = { id: taskWorkspace.library.idOf(taskWorkspace.file), title: taskWorkspace.spec.title,
|
|
117
|
+
topic: originPreview.topic, file: taskWorkspace.file };
|
|
118
|
+
if (owner.file === originFile && owner.preview === originPreview) {
|
|
119
|
+
owner.file = taskWorkspace.file;
|
|
120
|
+
owner.preview = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return materialized ? { ...options, baseDir: path.dirname(taskWorkspace.file) } : options;
|
|
124
|
+
};
|
|
110
125
|
const issues = body.issues || [];
|
|
111
126
|
// a IA vê (e testa) os slides api no ambiente atual, que pode ser o embutido "ensaio"
|
|
112
127
|
if (!apiBlocked(req) && (spec.slides || []).some((sl) => sl && sl.layout === "api")) await ensureEnsaio();
|
|
@@ -128,8 +143,11 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
128
143
|
const target = typeof body.targetSlide === "number" ? body.targetSlide : null;
|
|
129
144
|
const visuals = await lookAt(withBase(taskWorkspace, spec), target, prompt, emit);
|
|
130
145
|
// anexos: imagens vão como visão; documentos (id do /api/ai/context) vão como texto
|
|
131
|
-
let materials =
|
|
132
|
-
.filter((a) => a && typeof a === "object" && a.type === "doc").map((a) => a.id))
|
|
146
|
+
let materials = takeMaterials(taskWorkspace, (Array.isArray(body.attachments) ? body.attachments : [])
|
|
147
|
+
.filter((a) => a && typeof a === "object" && a.type === "doc").map((a) => a.id));
|
|
148
|
+
if (!taskWorkspace.file && taskWorkspace.preview && materials.some(m => m.bytes)) imageOptionsFor();
|
|
149
|
+
const materialDir = taskWorkspace.file && !isBundledTemplate(taskWorkspace.file) ? path.dirname(taskWorkspace.file) : null;
|
|
150
|
+
if (materialDir) materials = await prepareDocumentMaterials(materials, materialDir, {signal,onProgress:emit});
|
|
133
151
|
for (const [i, url] of (Array.isArray(body.attachments) ? body.attachments : []).entries()) {
|
|
134
152
|
if (typeof url === "string" && url.startsWith("data:image/")) visuals.push({ label: `imagem colada pelo usuário ${i + 1}`, dataUrl: url });
|
|
135
153
|
}
|
|
@@ -154,7 +172,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
154
172
|
targetSlide: target,
|
|
155
173
|
issues,
|
|
156
174
|
images: true, // a IA decide (regra no prompt: só quando pedirem ou aceitarem)
|
|
157
|
-
imageOptions:
|
|
175
|
+
imageOptions: () => imageOptionsFor(),
|
|
158
176
|
history,
|
|
159
177
|
onProgress: emit,
|
|
160
178
|
visuals,
|
|
@@ -212,6 +230,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
212
230
|
result.actions = [...(before.actions || []), ...r.feitos.map((f) => `Mapa: gravei ${f.arquivo} (${f.linhas} ${f.tipo === "contorno" ? "contorno" : "itens"}${f.fonte ? `; fonte: ${f.fonte}` : ""})`), ...r.falhas.map((x) => `Mapa: não deu (${x})`), ...(result.actions || [])];
|
|
213
231
|
}
|
|
214
232
|
if (result.geo?.length) result = { ...result, geo: undefined, talk: true, reply: P ? `${result.reply}\n(Já busquei os dados duas vezes; me diga o que ainda falta.)` : "Os dados do mapa ficam na pasta da apresentação: abra-a pela biblioteca e peça de novo." };
|
|
233
|
+
if (!taskWorkspace.file && taskWorkspace.preview && !result.talk && !result.variants) imageOptionsFor(result.spec);
|
|
215
234
|
// documento anexado NESTA mensagem e a resposta montou/reescreveu a maior parte do deck (criar a apresentação
|
|
216
235
|
// do paper pelo chat): a mesma conferência de cobertura da geração. Pedido pontual ("use a figura 3 no slide
|
|
217
236
|
// 5") não dispara: mexe em poucos slides.
|
|
@@ -220,12 +239,12 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
220
239
|
const rewritten = Array.isArray(result.changed) ? result.changed.length : 0;
|
|
221
240
|
if (rewritten >= Math.max(5, result.spec.slides.length / 2)) {
|
|
222
241
|
const covered = await coverDocumentVisuals(result.spec, { materials, briefing: prompt, say: (s) => emit({ phase: "step", text: s }),
|
|
223
|
-
edit: (o) => editDeck({ signal, images: true, imageOptions:
|
|
242
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: () => imageOptionsFor(), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
224
243
|
signal.throwIfAborted();
|
|
225
244
|
if (covered.spec !== result.spec) { result.spec = covered.spec; result.actions = [...(result.actions || []), ...covered.actions]; }
|
|
226
245
|
if (covered.coverage?.missing.length) result.actions = [...(result.actions || []), `Sem slide (figuras/tabelas do material): ${covered.coverage.missing.join("; ")}`];
|
|
227
246
|
const checked = await checkNumbers(result.spec, { materials, say: (s) => emit({ phase: "step", text: s }),
|
|
228
|
-
edit: (o) => editDeck({ signal, images: true, imageOptions:
|
|
247
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: () => imageOptionsFor(), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
229
248
|
signal.throwIfAborted();
|
|
230
249
|
if (checked.spec !== result.spec) { result.spec = checked.spec; result.actions = [...(result.actions || []), ...checked.actions]; }
|
|
231
250
|
if (checked.facts?.unsupported.length) result.actions = [...(result.actions || []), `Números sem base no material: ${checked.facts.unsupported.map((b) => `${b.number} (slide ${b.slide})`).join(", ")}`];
|
|
@@ -264,7 +283,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
264
283
|
emit({ phase: "test", text: reports.every((x) => x.ok) ? "Os testes passaram; conferindo…" : "Corrigindo com base no resultado…" });
|
|
265
284
|
const next = await editDeck({
|
|
266
285
|
spec: withBase(taskWorkspace, result.spec), signal, instruction, targetSlide: target, images: false,
|
|
267
|
-
imageOptions:
|
|
286
|
+
imageOptions: () => imageOptionsFor(result.spec), history: convo, onProgress: emit, apiContext: apiContextFor(req, taskWorkspace),
|
|
268
287
|
drawCheck: diagramCheck, runCommand: commandRunner(req, emit, body, taskWorkspace, signal),
|
|
269
288
|
});
|
|
270
289
|
convo.push({ role: "user", text: instruction });
|
|
@@ -286,7 +305,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
286
305
|
persist(taskWorkspace);
|
|
287
306
|
if (owner.file === taskWorkspace.file) owner.spec = taskWorkspace.spec;
|
|
288
307
|
return { ...result, spec: taskWorkspace.spec, conflicts: merged.conflicts, kept: merged.kept };
|
|
289
|
-
}, signal)); } finally { owner.aiChats.delete(requestId); res.off("close", onClose); }
|
|
308
|
+
}, signal).then(result => materialized ? { ...result, materialized } : result)); } finally { owner.aiChats.delete(requestId); res.off("close", onClose); }
|
|
290
309
|
return true;
|
|
291
310
|
}
|
|
292
311
|
return false;
|
|
Binary file
|
|
Binary file
|
|
@@ -26,10 +26,9 @@ export async function estimateSubmitCost(command, { commandEnv, W }) {
|
|
|
26
26
|
try {
|
|
27
27
|
const req = typeof command.code === "object" && command.code !== null ? command.code : JSON.parse(String(command.code || "{}"));
|
|
28
28
|
if (String(command.language || "").toLowerCase() !== "video" || req?.action !== "submit") return null;
|
|
29
|
-
const { estimateVideoCost } = await import("../ai/video-generation.js");
|
|
29
|
+
const { estimateVideoCost, videoSetup } = await import("../ai/video-generation.js");
|
|
30
30
|
const { env } = await commandEnv(W).catch(() => ({ env: {} }));
|
|
31
|
-
|
|
32
|
-
return await estimateVideoCost({ model: req.model, duration: req.duration, resolution: req.resolution }, { key });
|
|
31
|
+
return await estimateVideoCost({ model: req.model, duration: req.duration, resolution: req.resolution }, { setup: videoSetup({ env: { ...process.env, ...env } }) });
|
|
33
32
|
} catch { return null; }
|
|
34
33
|
}
|
|
35
34
|
|
package/src/studio/demo-decks.js
CHANGED
|
@@ -4,16 +4,18 @@ import { COLLECTION_NAMES, COLLECTION_ASSETS, collectionDeck } from './template-
|
|
|
4
4
|
import { LAYOUT_SAMPLES } from './layout-samples.js';
|
|
5
5
|
import { explorationDemo } from './exploration-demo.js';
|
|
6
6
|
import { tourDeck, TOUR_TITLE } from './tour.js';
|
|
7
|
-
|
|
7
|
+
import { featureDemo, FEATURE_DEMO_NAMES } from './feature-demos.js';
|
|
8
|
+
export function demoAssets(kind) { return kind === 'lavanda' || kind === 'novidades' ? ['lavanda-cover.jpg'] : kind === 'conheca' || kind === 'mapas' ? ['tour-mapa-botao.jpg', 'tour-mapa-editor.jpg'] : COLLECTION_ASSETS[kind] || []; }
|
|
8
9
|
// arquivos do projeto que o modelo traz (fora de imagens/): a planilha e as anotações da demo de novidades
|
|
9
10
|
export function demoProjectFiles(kind) { return kind === 'novidades' ? ['contexto/vendas.csv', 'contexto/leia-me.md'] : []; }
|
|
10
11
|
const item=(title,text,extra={})=>({title,text,...extra});
|
|
11
12
|
const route='flowchart LR\n A([Pedido]):::hi --> B{Dados válidos?}\n B -->|sim| C[Executar tarefa]\n B -.->|não| D[Informar erro]:::vazado\n C ==> E[Guardar resultado]:::em';
|
|
12
|
-
export const DEMO_NAMES={conheca:TOUR_TITLE,cinematica:"Demo — mundos que se transformam",explorar:'Demo — explore uma ideia',lavanda:'Estúdio lavanda',executivo:'Relatório executivo',workshop:'Workshop visual',compacto:'Demo — material de consulta',avancado:'Demo — recursos avançados',diagramas:'Demo — diagramas vivos',novidades:'Demo — novidades',hidraulica:'Aula de hidráulica',algoritmos:'Aula de algoritmos',...COLLECTION_NAMES};
|
|
13
|
+
export const DEMO_NAMES={conheca:TOUR_TITLE,...FEATURE_DEMO_NAMES,cinematica:"Demo — mundos que se transformam",explorar:'Demo — explore uma ideia',lavanda:'Estúdio lavanda',executivo:'Relatório executivo',workshop:'Workshop visual',compacto:'Demo — material de consulta',avancado:'Demo — recursos avançados',diagramas:'Demo — diagramas vivos',novidades:'Demo — novidades',hidraulica:'Aula de hidráulica',algoritmos:'Aula de algoritmos',...COLLECTION_NAMES};
|
|
13
14
|
export function demoDeck(kind) {
|
|
14
15
|
if(kind==='conheca')return tourDeck();
|
|
15
16
|
if(kind==='cinematica')return portalDemo();
|
|
16
17
|
if(kind==='explorar')return explorationDemo();
|
|
18
|
+
const feature=featureDemo(kind);if(feature)return feature;
|
|
17
19
|
if(Object.hasOwn(COLLECTION_NAMES,kind))return collectionDeck(kind);
|
|
18
20
|
if(!DEMO_NAMES[kind])throw Error('Modelo não encontrado.');
|
|
19
21
|
// cada demo com a sua cara (tema, tom e figura da capa): na vitrine, as capas não podem parecer a mesma
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Bases curtas para começar uma apresentação técnica sem esperar pela IA.
|
|
2
|
+
// São modelos de fábrica; cada uso cria uma instância pela biblioteca.
|
|
3
|
+
export function developerStarter(kind, { title } = {}) {
|
|
4
|
+
const code = 'function normalizar(nome) {\n return nome.trim().toLowerCase();\n}\n\nconst usuario = normalizar(" Ada ");\nconsole.log(usuario);';
|
|
5
|
+
const curl = ['curl --request POST "$BASE/echo" \\', ' --header "Content-Type: application/json" \\', ' --data \'{"mensagem":"Olá, API"}\''].join("\n");
|
|
6
|
+
if (!["programacao", "apis"].includes(kind)) throw new Error("Escolha uma base de programação ou APIs com cURL.");
|
|
7
|
+
const programming = kind === "programacao";
|
|
8
|
+
const name = String(title || (programming ? "Meu código, passo a passo" : "Minha primeira API com cURL")).trim();
|
|
9
|
+
return {
|
|
10
|
+
title: name, theme: programming ? "terminal-pro" : "aurora-dev", duration: 6, purpose: "palestra", maxWords: 70,
|
|
11
|
+
...(!programming ? { env: "ensaio" } : {}),
|
|
12
|
+
slides: [
|
|
13
|
+
{ layout: "cover", kicker: "Exemplo para editar", title: name,
|
|
14
|
+
subtitle: programming ? "Entrada, transformação e resultado" : "Entenda o comando e experimente uma chamada",
|
|
15
|
+
figure: { motion: { type: "console", caption: programming ? "normalizar.js · saída de exemplo" : "curl · saída de exemplo", scenes: [{ cmd: programming ? "node normalizar.js" : 'curl --request POST "$BASE/echo"', out: programming ? ["ada"] : ['200 OK', '{ "mensagem": "Olá, API" }'] }] } },
|
|
16
|
+
notes: "Este é um exemplo de fábrica para você editar. Use Inserir › Código / cURL para colar seu conteúdo. A animação da capa mostra uma saída ilustrativa." },
|
|
17
|
+
programming ? {
|
|
18
|
+
layout: "codewalk", title: "Uma transformação em ==três passos==", filename: "normalizar.js", language: "JavaScript", code,
|
|
19
|
+
steps: [
|
|
20
|
+
{ title: "Definir a regra", text: "A função recebe um nome. trim remove os espaços nas pontas; toLowerCase converte as letras para minúsculas.", highlight: [1, 2, 3] },
|
|
21
|
+
{ title: "Aplicar à entrada", text: "Os espaços e as letras maiúsculas entram no mesmo exemplo.", highlight: [5] },
|
|
22
|
+
{ title: "Conferir a saída", text: "O resultado pode ser copiado e usado no restante do programa.", highlight: [6], output: "ada" },
|
|
23
|
+
], notes: "Troque o código em Formatar. Selecione as linhas e crie uma etapa para explicar o trecho. A saída deste guia é ilustrativa, sem executar o código."
|
|
24
|
+
} : {
|
|
25
|
+
layout: "codewalk", title: "O cURL em ==três partes==", filename: "pedido.sh", language: "Bash", code: curl,
|
|
26
|
+
steps: [
|
|
27
|
+
{ title: "Método e destino", text: "POST envia os dados para o endpoint. BASE é o endereço do ambiente.", highlight: [1] },
|
|
28
|
+
{ title: "Formato dos dados", text: "Content-Type avisa ao servidor que o corpo é JSON.", highlight: [2] },
|
|
29
|
+
{ title: "O corpo enviado", text: "A mensagem é o dado que vamos conferir na resposta.", highlight: [3] },
|
|
30
|
+
], notes: "Este comando explica a chamada, sem executá-la. O próximo slide chama a API de ensaio do Studio local."
|
|
31
|
+
},
|
|
32
|
+
programming ? {
|
|
33
|
+
layout: "diagram", title: "Veja o ==caminho== dos dados", mermaid: 'flowchart LR\n A["Entrada: espaços e maiúsculas"] --> B["trim + toLowerCase"] --> C["Saída: ada"]',
|
|
34
|
+
notes: "Troque os nós pelo fluxo do seu programa. Use a mesma entrada e a mesma saída em toda a explicação."
|
|
35
|
+
} : {
|
|
36
|
+
layout: "api", title: "Experimente ==ao vivo==", text: "No Studio local, clique em Executar. Troque a mensagem no Corpo e execute novamente.", tab: "curl",
|
|
37
|
+
request: { method: "POST", url: "{{base}}/echo", auth: false, body: { mensagem: "Olá, API" } }, answer: "$.recebido",
|
|
38
|
+
notes: "Ambiente ENSAIO: API de mentira que o Studio local fornece. No servidor multiusuário, chamadas não são executadas. Para usar seu serviço, troque o ambiente e importe seu cURL por Inserir › Código / cURL."
|
|
39
|
+
},
|
|
40
|
+
{ layout: "code", title: programming ? "Conferir a ==entrada e saída==" : "Ler a ==resposta==", filename: "resposta.json", language: "JSON",
|
|
41
|
+
code: programming ? '{\n "entrada": " Ada ",\n "saida": "ada"\n}' : '{\n "metodo": "POST",\n "recebido": { "mensagem": "Olá, API" },\n "consulta": {}\n}',
|
|
42
|
+
note: programming ? "Mantenha um exemplo concreto do começo ao fim da explicação." : "O campo recebido contém os mesmos dados enviados no corpo." },
|
|
43
|
+
{ layout: "end", title: "Agora, o ==seu exemplo==", subtitle: "Cole seu conteúdo em Inserir › Código / cURL ou peça ao assistente para explicar os trechos.", notes: "Você pode aplicar outro tema em Design. O exemplo é seu para editar; cada uso cria uma apresentação independente." },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Gerações pertencem à pessoa, não à aba. O arquivo fica fora da biblioteca/nuvem por até 24 h,
|
|
2
|
+
// enquanto este Studio estiver ligado. Uma geração por vez limita o uso de Chrome e memória.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { generateExport, exportHeaders } from './exporting.js';
|
|
8
|
+
import { removeRetry } from '../fs-retry.js';
|
|
9
|
+
|
|
10
|
+
export const EXPORT_KINDS = ['sagadeck', 'pptx', 'pdf', 'roteiro', 'tudo', 'estudo', 'estudo-html'];
|
|
11
|
+
const TTL = 24 * 3600_000;
|
|
12
|
+
export function createExportJobs({ generate = generateExport, now = () => Date.now(), ttl = TTL } = {}) {
|
|
13
|
+
const jobs = new Map(), queue = [];
|
|
14
|
+
let active = null, closed = false, dir;
|
|
15
|
+
const visible = j => ({ id: j.id, kind: j.kind, name: j.name, title: j.title, status: j.status,
|
|
16
|
+
progress: j.progress, createdAt: j.createdAt, finishedAt: j.finishedAt || null,
|
|
17
|
+
expiresAt: j.finishedAt ? j.finishedAt + ttl : null, error: j.error || null, warnings: j.warnings || [] });
|
|
18
|
+
const remove = j => { if (j.path) fs.rmSync(j.path, { force: true }); jobs.delete(j.id); };
|
|
19
|
+
function prune() {
|
|
20
|
+
for (const j of jobs.values()) if (j.finishedAt && now() - j.finishedAt >= ttl) remove(j);
|
|
21
|
+
}
|
|
22
|
+
const timer = setInterval(prune, 60_000); timer.unref();
|
|
23
|
+
async function run(j) {
|
|
24
|
+
j.status = 'running';
|
|
25
|
+
try {
|
|
26
|
+
const result = await generate(j.kind, j.spec, j.name, { ...j.options, onProgress: p => { j.progress = { ...j.progress, ...p }; } });
|
|
27
|
+
if (closed) return;
|
|
28
|
+
dir ||= fs.mkdtempSync(path.join(os.tmpdir(), 'sagadeck-export-jobs-'));
|
|
29
|
+
j.path = path.join(dir, j.id);
|
|
30
|
+
fs.writeFileSync(j.path, result.body, { mode: 0o600 });
|
|
31
|
+
j.headers = exportHeaders(result);
|
|
32
|
+
j.name = result.file;
|
|
33
|
+
j.warnings = [...(result.warnings || []), ...(result.missing || []).map(file => `Arquivo não encontrado: ${file}`)];
|
|
34
|
+
j.progress = { ...j.progress, phase: 'ready', completed: j.progress.total };
|
|
35
|
+
j.status = 'ready';
|
|
36
|
+
} catch (e) {
|
|
37
|
+
j.status = 'failed'; j.error = e.message || String(e);
|
|
38
|
+
} finally {
|
|
39
|
+
j.finishedAt = now(); delete j.spec; delete j.options;
|
|
40
|
+
// Mantém as dez exportações recentes da pessoa; não guarda arquivos sem limite.
|
|
41
|
+
const done = [...jobs.values()].filter(x => x.owner === j.owner && x.finishedAt).sort((a, b) => b.finishedAt - a.finishedAt);
|
|
42
|
+
for (const old of done.slice(10)) remove(old);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function drain() {
|
|
46
|
+
if (closed || active || !queue.length) return;
|
|
47
|
+
const j = queue.shift();
|
|
48
|
+
active = run(j).finally(() => { active = null; setImmediate(drain); });
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
create(owner, kind, spec, name, options = {}) {
|
|
52
|
+
if (closed) throw new Error('O Studio está encerrando. Tente novamente depois de reiniciar.');
|
|
53
|
+
if (!EXPORT_KINDS.includes(kind)) throw new Error('Formato de exportação inválido.');
|
|
54
|
+
prune();
|
|
55
|
+
const key = crypto.createHash('sha256').update(JSON.stringify([kind, spec, name, options])).digest('hex');
|
|
56
|
+
const pending = [...jobs.values()].filter(j => !j.finishedAt);
|
|
57
|
+
const same = pending.find(j => j.owner === owner && j.key === key);
|
|
58
|
+
if (same) return visible(same);
|
|
59
|
+
if (pending.filter(j => j.owner === owner).length >= 4 || pending.length >= 32) throw new Error('Há muitas exportações em andamento. Espere uma delas terminar.');
|
|
60
|
+
const j = { id: crypto.randomUUID(), owner, key, kind, spec: structuredClone(spec), name, title: spec.title || name,
|
|
61
|
+
options: { ...options }, status: 'queued', createdAt: now(), progress: { phase: 'queued', completed: 0, total: spec.slides?.length || 0 } };
|
|
62
|
+
jobs.set(j.id, j); queue.push(j); setImmediate(drain);
|
|
63
|
+
return visible(j);
|
|
64
|
+
},
|
|
65
|
+
list(owner) { prune(); return [...jobs.values()].filter(j => j.owner === owner).sort((a, b) => b.createdAt - a.createdAt).map(visible); },
|
|
66
|
+
download(owner, id, res) {
|
|
67
|
+
prune();
|
|
68
|
+
const j = jobs.get(id);
|
|
69
|
+
const code = !j || j.owner !== owner ? 404 : j.status !== 'ready' ? 409 : 200;
|
|
70
|
+
if (code !== 200) {
|
|
71
|
+
res.writeHead(code, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
|
|
72
|
+
res.end(JSON.stringify({ error: code === 404 ? 'Exportação não encontrada ou expirada. Gere novamente.' : j.error || 'A exportação ainda não terminou.' }));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
res.writeHead(200, { ...j.headers, 'Content-Length': fs.statSync(j.path).size });
|
|
76
|
+
const stream = fs.createReadStream(j.path);
|
|
77
|
+
stream.on('error', () => res.destroy());
|
|
78
|
+
res.on('close', () => stream.destroy()); stream.pipe(res);
|
|
79
|
+
},
|
|
80
|
+
async close() {
|
|
81
|
+
closed = true; clearInterval(timer); queue.length = 0;
|
|
82
|
+
await active;
|
|
83
|
+
jobs.clear(); if (dir) removeRetry(dir);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
package/src/studio/exporting.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { expandApiCollections } from '../api-collections.js';
|
|
2
|
+
import { removeRetry } from '../fs-retry.js';
|
|
2
3
|
import {meetingSpec} from '../meeting.js';
|
|
3
4
|
// Exportar a apresentação aberta ou da biblioteca: .sagadeck, PowerPoint, PDF, roteiro, material de estudo e "Baixar
|
|
4
5
|
// tudo". PPTX/PDF/roteiro/estudo usam o Chrome invisível (os mesmos exportadores do CLI), numa pasta temporária.
|
|
@@ -21,14 +22,12 @@ export function sendHtml(res,spec,audience){
|
|
|
21
22
|
|
|
22
23
|
// Gera e envia um arquivo da apresentação. PPTX/PDF/roteiro usam o Chrome invisível (os mesmos
|
|
23
24
|
// exportadores de "sagadeck pptx | pdf | roteiro"), numa pasta temporária.
|
|
24
|
-
export async function
|
|
25
|
+
export async function generateExport(kind, spec, name, { notes = true, audience, macFonts = false, onProgress = () => {} } = {}) {
|
|
25
26
|
spec=meetingSpec(spec,audience);
|
|
26
|
-
|
|
27
|
+
onProgress({ phase: 'preparing', completed: 0, total: spec.slides?.length || 0 });
|
|
27
28
|
if (kind === "sagadeck") {
|
|
28
29
|
const { zip, missing } = await packDeck(spec, { baseDir: spec._dir || process.cwd(), name, generator: "sagadeck studio" });
|
|
29
|
-
|
|
30
|
-
res.end(zip);
|
|
31
|
-
return;
|
|
30
|
+
return { body: zip, file: name + EXTENSION, mime: MIME, missing, warnings: [] };
|
|
32
31
|
}
|
|
33
32
|
const kinds = {
|
|
34
33
|
pptx: { file: `${name}.pptx`, mime: "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
|
|
@@ -41,6 +40,7 @@ export async function sendExport(res, kind, spec, name, { notes = true, inline =
|
|
|
41
40
|
tudo: { file: `${name}.zip`, mime: "application/zip" },
|
|
42
41
|
};
|
|
43
42
|
const k = kinds[kind];
|
|
43
|
+
if (!k) throw new Error('Formato de exportação inválido.');
|
|
44
44
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), `sagadeck-${kind}-`));
|
|
45
45
|
try {
|
|
46
46
|
if (["pptx","pdf","tudo"].includes(kind)) spec=expandApiCollections(spec);
|
|
@@ -49,27 +49,31 @@ export async function sendExport(res, kind, spec, name, { notes = true, inline =
|
|
|
49
49
|
fs.writeFileSync(htmlFile, r.html);
|
|
50
50
|
const errors = [];
|
|
51
51
|
const make = async (what) => {
|
|
52
|
+
const progress = event => onProgress({ ...event, format: what });
|
|
53
|
+
progress({ phase: 'opening', completed: 0, total: r.slidesMeta.length });
|
|
52
54
|
const out = path.join(tmp, `saida-${what}`);
|
|
53
55
|
if (what === "pptx") {
|
|
54
56
|
const { exportPptx } = await import("../export/pptx.js");
|
|
55
|
-
errors.push(...(await exportPptx(htmlFile, out, { theme: r.theme, meta: { ...r.meta, slides: r.slidesMeta }, notes, fontFallback: macFonts ? "mac" : undefined })).errors);
|
|
57
|
+
errors.push(...(await exportPptx(htmlFile, out, { theme: r.theme, meta: { ...r.meta, slides: r.slidesMeta }, notes, fontFallback: macFonts ? "mac" : undefined, onProgress: progress })).errors);
|
|
56
58
|
} else if (what === "pdf") {
|
|
57
59
|
const { pdf } = await import("../export/shots.js");
|
|
58
60
|
// Preferências › Exportação: tema escuro com par claro (manual-noite → manual) sai claro no PDF, bom para imprimir
|
|
59
61
|
const light = loadPreferences().exportacao.pdfClaro === true ? lightVariant(spec) : null;
|
|
60
62
|
let file = htmlFile;
|
|
61
63
|
if (light) { file = path.join(tmp, "deck-claro.html"); fs.writeFileSync(file, buildHTML(light).html); }
|
|
62
|
-
await pdf(file, out);
|
|
64
|
+
await pdf(file, out, { onProgress: progress });
|
|
63
65
|
} else if (what === "estudo" || what === "estudo-html") {
|
|
64
66
|
const { shots } = await import("../export/shots.js");
|
|
65
67
|
const { estudoHTML, estudoPDF } = await import("../export/estudo.js");
|
|
66
|
-
const { files } = await shots(htmlFile, path.join(tmp, "fotos"), { scale: what === "estudo" ? 0.6 : 0.75, jpeg: true });
|
|
68
|
+
const { files } = await shots(htmlFile, path.join(tmp, "fotos"), { scale: what === "estudo" ? 0.6 : 0.75, jpeg: true, onProgress: progress });
|
|
69
|
+
progress({ phase: 'packing', completed: files.length, total: files.length });
|
|
67
70
|
const html = estudoHTML({ title: r.meta.title, author: r.meta.author, date: spec.date, slidesMeta: r.slidesMeta, shotFiles: files });
|
|
68
71
|
if (what === "estudo-html") fs.writeFileSync(out, html); else await estudoPDF(html, out);
|
|
69
72
|
} else {
|
|
70
73
|
const { shots } = await import("../export/shots.js");
|
|
71
74
|
const { roteiroPDF } = await import("../export/roteiro.js");
|
|
72
|
-
const { files } = await shots(htmlFile, path.join(tmp, "miniaturas"), { scale: 0.5, jpeg: true });
|
|
75
|
+
const { files } = await shots(htmlFile, path.join(tmp, "miniaturas"), { scale: 0.5, jpeg: true, onProgress: progress });
|
|
76
|
+
progress({ phase: 'packing', completed: files.length, total: files.length });
|
|
73
77
|
await roteiroPDF({ slidesMeta: r.slidesMeta, shotFiles: files, outFile: out, title: r.meta.title, author: r.meta.author, duration: spec.duration });
|
|
74
78
|
}
|
|
75
79
|
return fs.readFileSync(out);
|
|
@@ -78,20 +82,34 @@ export async function sendExport(res, kind, spec, name, { notes = true, inline =
|
|
|
78
82
|
if (kind === "tudo") {
|
|
79
83
|
const zip = new JSZip();
|
|
80
84
|
for (const what of ["pptx", "pdf", "roteiro"]) zip.file(kinds[what].file, await make(what));
|
|
85
|
+
onProgress({ phase: 'packing', format: 'tudo', completed: r.slidesMeta.length, total: r.slidesMeta.length });
|
|
81
86
|
body = await zip.generateAsync({ type: "nodebuffer", compression: "DEFLATE" });
|
|
82
87
|
} else body = await make(kind);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
// só o que afeta o arquivo; o fiscal de conteúdo fica no Revisar
|
|
89
|
+
return { body, file: k.file, mime: k.mime, warnings: [...(r.warnings || []).filter(w => /não encontrado/.test(w)), ...errors].slice(0, 20) };
|
|
90
|
+
} finally {
|
|
91
|
+
removeRetry(tmp);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function exportHeaders(result, inline = false) {
|
|
96
|
+
const cd = file => `attachment; filename="${slugify(file.replace(/\.\w+$/, ''))}${path.extname(file)}"; filename*=UTF-8''${encodeURIComponent(file)}`;
|
|
97
|
+
return { 'Content-Type': result.mime, 'Content-Disposition': inline ? 'inline' : cd(result.file),
|
|
98
|
+
'Cache-Control': 'private, no-store',
|
|
99
|
+
'X-Sagadeck-Warnings': encodeURIComponent(JSON.stringify(result.warnings || [])),
|
|
100
|
+
...(result.missing ? { 'X-Sagadeck-Missing': encodeURIComponent(JSON.stringify(result.missing)) } : {}) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Links de compartilhamento e CLI seguem recebendo o arquivo na própria resposta.
|
|
104
|
+
export async function sendExport(res, kind, spec, name, options = {}) {
|
|
105
|
+
try {
|
|
106
|
+
const result = await generateExport(kind, spec, name, options);
|
|
107
|
+
res.writeHead(200, exportHeaders(result, options.inline));
|
|
108
|
+
res.end(result.body);
|
|
89
109
|
} catch (e) {
|
|
90
110
|
console.error(`[Studio] exportação ${kind} falhou:`, e.message);
|
|
91
|
-
res.writeHead(500, {
|
|
111
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
92
112
|
res.end(JSON.stringify({ error: e.message }));
|
|
93
|
-
} finally {
|
|
94
|
-
fs.rmSync(tmp, { recursive: true, force: true });
|
|
95
113
|
}
|
|
96
114
|
}
|
|
97
115
|
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Demos de um assunto só (mapas, programação, APIs com curl): curtas, para mostrar um recurso a quem vai usá-lo, sem
|
|
2
|
+
// passar pelo Conheça o SagaDeck inteiro. Reaproveitam os exemplos de layout (layout-samples.js) e as demos de mapa do
|
|
3
|
+
// tour; cada slide diz o que é e a frase para pedir à IA (no rodapé e nas notas). Abrem em prévia, como os modelos.
|
|
4
|
+
import { LAYOUT_INFO, LAYOUT_SAMPLES, LAYOUT_ASK } from "./layout-samples.js";
|
|
5
|
+
import { mapDemos } from "./tour.js";
|
|
6
|
+
|
|
7
|
+
export const FEATURE_DEMO_NAMES = { mapas: "Demo — mapas", programacao: "Demo — programação", apis: "Demo — APIs com curl" };
|
|
8
|
+
|
|
9
|
+
const asked = (ask) => `Peça à IA: “${ask}”`;
|
|
10
|
+
const told = (label, desc, ask) => ({ source: `**${label}**: ${desc}. ${asked(ask)}`, notes: [`${label}: ${desc}.`, asked(ask)].join("\n\n") });
|
|
11
|
+
// o exemplo do layout, com o nome do recurso e a frase para a IA; extra troca o conteúdo quando o exemplo é genérico
|
|
12
|
+
const sample = (name, extra = {}) => {
|
|
13
|
+
const [label, desc] = LAYOUT_INFO[name] || [name, ""];
|
|
14
|
+
return { ...structuredClone(LAYOUT_SAMPLES[name]), layout: name, ...told(label, desc, LAYOUT_ASK[name] || ""), ...extra };
|
|
15
|
+
};
|
|
16
|
+
const cover = (title, subtitle, figure) => ({ layout: "cover", kicker: "SAGADECK · DEMO", title, subtitle, figure });
|
|
17
|
+
|
|
18
|
+
// ---- Mapas: mostrar, calcular, editar
|
|
19
|
+
export function mapasDemo() {
|
|
20
|
+
const [bolhas, regioes, linhas, rota, raio, faixa, comIA, ondeEdita, telaMapa, noStudio] = mapDemos();
|
|
21
|
+
return {
|
|
22
|
+
title: FEATURE_DEMO_NAMES.mapas, theme: "oceano", duration: 15, maxWords: 140, slides: [
|
|
23
|
+
cover("Mapas ==ao vivo==", "Pontos, linhas e áreas sobre o mapa de verdade, com as contas feitas pelo código", { icon: "map", size: 320 }),
|
|
24
|
+
{ layout: "statement", kicker: "a ideia", text: "A ==planilha== é a fonte; o mapa é a vista.",
|
|
25
|
+
notes: "Os pontos vêm de uma planilha do projeto (contexto/*.csv), as linhas e áreas de GeoJSON, GPX ou KML. A mesma planilha serve tabela e gráfico. Nada de mapa chapado em imagem: ele é ao vivo." },
|
|
26
|
+
{ layout: "section", number: "01", kicker: "mostrar", title: "Dados no ==mapa==", subtitle: "pontos por categoria, bolhas, regiões por valor, linhas e áreas" },
|
|
27
|
+
sample("map"), bolhas, regioes, linhas,
|
|
28
|
+
{ layout: "section", number: "02", kicker: "calcular", title: "As ==contas==", subtitle: "rota pelas ruas, alcance, raio e faixa: calculados, nunca desenhados à mão" },
|
|
29
|
+
rota, raio, faixa,
|
|
30
|
+
{ layout: "section", number: "03", kicker: "editar", title: "Com a IA e no ==Studio==", subtitle: "a IA busca os dados; o Editar mapa mexe em cada ponto" },
|
|
31
|
+
comIA, ondeEdita, telaMapa, noStudio,
|
|
32
|
+
{ layout: "end", title: "Agora, o seu ==mapa==.", subtitle: "Peça à IA: “Ponha no mapa os endereços da minha planilha”." },
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---- Programação: mostrar código, rodar ao vivo, Git a dois, explicar a arquitetura
|
|
38
|
+
const PEDIDOS = [
|
|
39
|
+
"def total_do_pedido(itens, cupom=None):",
|
|
40
|
+
" subtotal = sum(i.preco * i.qtd for i in itens)",
|
|
41
|
+
" if cupom and cupom.valido():",
|
|
42
|
+
" subtotal -= cupom.desconto(subtotal)",
|
|
43
|
+
" frete = 0 if subtotal >= 200 else 19.90",
|
|
44
|
+
" return round(subtotal + frete, 2)",
|
|
45
|
+
].join("\n");
|
|
46
|
+
export function programacaoDemo() {
|
|
47
|
+
return {
|
|
48
|
+
title: FEATURE_DEMO_NAMES.programacao, theme: "musgo", duration: 15, maxWords: 140, slides: [
|
|
49
|
+
cover("Código no ==palco==", "Do trecho destacado ao algoritmo rodando, com a plateia acompanhando linha a linha", { motion: { type: "console", caption: "projeto · terminal", scenes: [
|
|
50
|
+
{ cmd: "pytest -q", out: ["42 passed in 1.84s"] },
|
|
51
|
+
{ cmd: "git log --oneline -3", out: ["a1f9c2e calcula o frete", "77b03d4 aplica o cupom", "3c0e1aa primeira versão"] },
|
|
52
|
+
{ cmd: "python -m pedidos --demo", out: ["total: R$ 187,40", "frete: R$ 19,90"] },
|
|
53
|
+
] } }),
|
|
54
|
+
{ layout: "section", number: "01", kicker: "mostrar", title: "Código ==legível== de longe", subtitle: "destaque, linha a linha e material de consulta" },
|
|
55
|
+
sample("code", { title: "Destaque o que ==importa==", filename: "pedidos.py", language: "python", code: PEDIDOS, highlight: [3, 4],
|
|
56
|
+
note: "O cupom só vale se for válido: é aqui que o desconto entra." }),
|
|
57
|
+
sample("codewalk"),
|
|
58
|
+
sample("dossier"),
|
|
59
|
+
{ layout: "section", number: "02", kicker: "rodar", title: "Executar na ==frente== de todos", subtitle: "a plateia muda o código e vê o resultado" },
|
|
60
|
+
sample("codelab"),
|
|
61
|
+
sample("algo"),
|
|
62
|
+
{ layout: "section", number: "03", kicker: "colaborar", title: "Git e ==revisão==", subtitle: "duas pessoas, um clique por jogada" },
|
|
63
|
+
sample("terminals"),
|
|
64
|
+
sample("duel"),
|
|
65
|
+
sample("turns"),
|
|
66
|
+
{ layout: "section", number: "04", kicker: "explicar", title: "A ==arquitetura==", subtitle: "fluxos, sequências e redes que dá para explorar" },
|
|
67
|
+
sample("diagram", { title: "Do pedido ao ==pagamento==", mermaid: "sequenceDiagram\n participant App\n participant API\n participant Pagamentos\n App->>API: POST /pedidos\n API->>Pagamentos: cobra o total\n Pagamentos-->>API: aprovado\n API-->>App: 201 Created" }),
|
|
68
|
+
sample("graphlab"),
|
|
69
|
+
{ layout: "end", title: "Agora, o seu ==código==.", subtitle: "Peça à IA: “Explique este arquivo linha a linha, com a saída de cada passo”." },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- APIs com curl: anatomia de um curl, executar de verdade (no ambiente ENSAIO, a API de mentira que o Studio
|
|
75
|
+
// sobe sozinho), polling, streaming, as opções que mais ajudam e o cuidado com o token
|
|
76
|
+
const CURL = [
|
|
77
|
+
'curl -X POST "https://api.exemplo.com/v1/pedidos" \\',
|
|
78
|
+
' -H "Authorization: Bearer $API_TOKEN" \\',
|
|
79
|
+
' -H "Content-Type: application/json" \\',
|
|
80
|
+
` -d '{"item": "livro", "quantidade": 2}' \\`,
|
|
81
|
+
" -i",
|
|
82
|
+
].join("\n");
|
|
83
|
+
const ensaio = "Roda no ambiente ENSAIO, a API de mentira que o Studio sobe sozinho: nada sai da sua máquina. Com o seu serviço, troque o endereço em Inserir › Ambientes.";
|
|
84
|
+
const apiSlide = (s) => {
|
|
85
|
+
const t = told("API ao vivo", "Requisição que executa na hora, com o curl equivalente na aba curl", LAYOUT_ASK.api || "Mostre esta chamada ao vivo com o curl");
|
|
86
|
+
return { layout: "api", tab: "curl", ...s, source: t.source, notes: [s.notes, ensaio, t.notes].filter(Boolean).join("\n\n") };
|
|
87
|
+
};
|
|
88
|
+
export function apisDemo() {
|
|
89
|
+
return {
|
|
90
|
+
title: FEATURE_DEMO_NAMES.apis, theme: "wine", duration: 15, maxWords: 140, slides: [
|
|
91
|
+
cover("APIs com ==curl==", "Uma chamada por slide, executada na frente de todos, com o curl pronto para copiar", { motion: { type: "console", caption: "curl · ao vivo", scenes: [
|
|
92
|
+
{ cmd: `curl -X POST /v1/echo -d '{"mensagem":"Olá, API"}'`, out: ["200 OK", `{ "metodo": "POST", "recebido": { "mensagem": "Olá, API" } }`] },
|
|
93
|
+
{ cmd: "curl /v1/echo?cidade=Recife", out: ["200 OK", `{ "consulta": { "cidade": "Recife" } }`] },
|
|
94
|
+
{ cmd: "curl -N /v1/stream", out: ["200 OK · text/event-stream", "data: a resposta chega aos poucos..."] },
|
|
95
|
+
] } }),
|
|
96
|
+
{ layout: "statement", kicker: "a ideia", text: "O curl é a chamada de API ==mais curta== que existe.",
|
|
97
|
+
notes: "Todo slide de API do SagaDeck mostra o pedido, executa de verdade e traz o curl equivalente na aba curl (e também JavaScript e Python)." },
|
|
98
|
+
{ layout: "codewalk", kicker: "anatomia", title: "Um curl em ==quatro partes==", filename: "pedido.sh", language: "bash", code: CURL,
|
|
99
|
+
steps: [
|
|
100
|
+
{ title: "Verbo e endereço", text: "-X diz o que fazer (POST cria); a URL diz onde.", highlight: [1] },
|
|
101
|
+
{ title: "Quem está pedindo", text: "O token vai no cabeçalho Authorization. Ele vem de uma variável: nunca escrito no comando.", highlight: [2] },
|
|
102
|
+
{ title: "O que vai junto", text: "Content-Type avisa que o corpo é JSON; -d leva o corpo.", highlight: [3, 4] },
|
|
103
|
+
{ title: "Ver a resposta inteira", text: "-i mostra o status e os cabeçalhos antes do corpo.", highlight: [5], output: "HTTP/1.1 201 Created\ncontent-type: application/json\n\n{ \"id\": \"ped-42\", \"status\": \"criado\" }" },
|
|
104
|
+
], ...told("Código guiado", "Explique uma linha por vez, com destaques e saída simulada", "Explique este curl parte por parte") },
|
|
105
|
+
{ layout: "section", number: "01", kicker: "executar", title: "Ao ==vivo==, de verdade", subtitle: "o Executar faz a chamada; a aba curl mostra o comando" },
|
|
106
|
+
apiSlide({ id: "post", kicker: "POST", title: "Mandar um ==corpo==", request: { method: "POST", url: "{{base}}/echo", body: { mensagem: "Olá, API", turma: "noite" } }, answer: "$.recebido",
|
|
107
|
+
notes: "Edite a mensagem na aba Corpo e execute de novo: a aba curl acompanha." }),
|
|
108
|
+
apiSlide({ id: "get", kicker: "GET", title: "Perguntar pela ==URL==", request: { method: "GET", url: "{{base}}/echo?cidade=Recife&dias=3" }, answer: "$.consulta",
|
|
109
|
+
notes: "GET não tem corpo: os parâmetros vão na URL, depois do ?." }),
|
|
110
|
+
apiSlide({ id: "token", kicker: "Token", title: "Com ==autorização==", request: { method: "POST", url: "{{base}}/sync", body: { q: "Qual é o status do pedido 42?" } }, answer: "$.choices[0].message.content",
|
|
111
|
+
notes: "O token sai do ambiente (o Studio pede e renova sozinho). No curl ele aparece como $API_TOKEN: o valor nunca vai para o slide." }),
|
|
112
|
+
{ layout: "section", number: "02", kicker: "esperar", title: "Quando a resposta ==demora==", subtitle: "acompanhar até terminar, ou receber aos poucos" },
|
|
113
|
+
apiSlide({ id: "polling", kicker: "Polling", title: "Inicia e ==acompanha==", mode: "polling",
|
|
114
|
+
request: { url: "{{base}}/start", body: { workflow: "{{wf}}", input: { texto: "Resuma o contrato" } } },
|
|
115
|
+
polling: { id: "$.executionId", check: { url: "{{base}}/status/{{id}}" }, status: "$.status", done: ["FINISHED"], failed: ["ERROR"], interval: 0.8 },
|
|
116
|
+
steps: "$.responses", stepText: "$.output", notes: "O início devolve só o código da execução; a consulta repete até FINISHED. Na aba curl: o laço com sleep." }),
|
|
117
|
+
apiSlide({ id: "stream", kicker: "Streaming", title: "A resposta chega ==aos poucos==", mode: "stream", request: { method: "POST", url: "{{base}}/stream", body: { stream: true } },
|
|
118
|
+
notes: "No curl, -N desliga o buffer e o texto aparece enquanto chega." }),
|
|
119
|
+
{ layout: "section", number: "03", kicker: "dia a dia", title: "As opções que ==salvam== o dia", subtitle: "clique em cada cartão para ver para que serve" },
|
|
120
|
+
// (oito cartões ocupam a área toda: aqui a frase para a IA fica só nas notas, sem a linha de rodapé)
|
|
121
|
+
{ layout: "cards", title: "Clique para ==virar==", cols: 4, notes: told("Cartão que vira", "A frente mostra a opção; o verso explica, no clique", "Faça cartões que viram com a explicação no verso").notes, items: [
|
|
122
|
+
{ icon: "file-text", title: "-i", text: "status e cabeçalhos", flip: "Mostra a linha de status e os cabeçalhos antes do corpo. O primeiro passo de toda investigação." },
|
|
123
|
+
{ icon: "bug", title: "-v", text: "conversa inteira", flip: "Mostra tudo: o pedido que saiu, o certificado, o que voltou. Para quando nada faz sentido." },
|
|
124
|
+
{ icon: "shield-alert", title: "--fail", text: "erro vira erro", flip: "Status 4xx ou 5xx faz o curl sair com erro. Essencial em script, que senão segue feliz." },
|
|
125
|
+
{ icon: "corner-down-right", title: "-L", text: "segue o redirecionamento", flip: "Vai atrás do 301 e do 302 até o endereço final." },
|
|
126
|
+
{ icon: "volume-x", title: "-s", text: "sem barra de progresso", flip: "Silencia o progresso; combine com -S para ainda ver os erros." },
|
|
127
|
+
{ icon: "download", title: "-o arquivo", text: "grava a resposta", flip: "Salva o corpo num arquivo em vez de jogar na tela." },
|
|
128
|
+
{ icon: "timer", title: "-w", text: "tempo e status", flip: "-w '%{http_code} %{time_total}s' imprime o status e quanto demorou." },
|
|
129
|
+
{ icon: "braces", title: "| jq", text: "JSON legível", flip: "Mande a saída para o jq: indenta, colore e filtra campos, como jq '.itens[0]'." },
|
|
130
|
+
] },
|
|
131
|
+
{ layout: "statement", kicker: "cuidado", text: "O token ==nunca== vai para o slide.",
|
|
132
|
+
add: { aviso: { tipo: "perigo", titulo: "Segredo exposto", texto: "No deck, use {{secret.nome}}; no curl, $API_TOKEN. Os valores ficam nos Ambientes da sua máquina." } },
|
|
133
|
+
notes: "Os segredos ficam em ~/.sagadeck/ambientes.yaml (no Windows, cifrados para o seu usuário). O deck pode ir para outras pessoas sem vazar nada." },
|
|
134
|
+
{ layout: "end", title: "Agora, a sua ==API==.", subtitle: "Peça à IA: “Monte um slide que chama o meu serviço e mostra o curl”." },
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function featureDemo(kind) {
|
|
140
|
+
if (kind === "mapas") return mapasDemo();
|
|
141
|
+
if (kind === "programacao") return programacaoDemo();
|
|
142
|
+
if (kind === "apis") return apisDemo();
|
|
143
|
+
return null;
|
|
144
|
+
}
|
package/src/studio/ia-routes.js
CHANGED
|
@@ -29,6 +29,15 @@ export async function iaRoutes({ req, res, pathname, opts, readJSON }) {
|
|
|
29
29
|
json(res, 200, { config: maskedIA(loadIA()), file: iaFile(), source: cfg.source, configured: !!(cfg.url || cfg.adaptador), problem: cfg.problem || "", providers: PROVIDERS, roles: ROLES, recommended: RECOMMENDED, editable: !opts.multiuser });
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
|
+
// diagnóstico de todos os recursos (src/ai/ia-check.js), o mesmo do `sagadeck ia-check`: no multiusuário, só quem
|
|
33
|
+
// administra (diz quais modelos estão configurados)
|
|
34
|
+
if (pathname === "/api/ia/check" && req.method === "POST") {
|
|
35
|
+
if (opts.multiuser) { json(res, 403, { error: "No servidor multiusuário, o diagnóstico da IA é de quem administra: rode sagadeck ia-check no servidor." }); return true; }
|
|
36
|
+
const body = await readJSON(req);
|
|
37
|
+
const { iaCheck } = await import("../ai/ia-check.js");
|
|
38
|
+
json(res, 200, await iaCheck({ only: Array.isArray(body.so) ? body.so : null, rapido: !!body.rapido, videoReal: !!body.video_real }));
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
32
41
|
if ((pathname === "/api/ia" || pathname === "/api/ia/test") && req.method === "POST") {
|
|
33
42
|
if (opts.multiuser) { json(res, 403, { error: "No servidor multiusuário, a IA é configurada por quem administra." }); return true; }
|
|
34
43
|
const body = await readJSON(req);
|