sagadeck 1.8.0 → 1.9.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 +19 -8
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +367 -7
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +131 -110
- package/package.json +33 -2
- package/src/ai/commands.js +2 -2
- package/src/ai/deck-ai.js +1 -1
- 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/build.js +46 -14
- 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/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/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/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/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/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 +0 -9
- package/src/studio/project.js +1 -0
- package/src/studio/public/ai-settings.js +2 -2
- package/src/studio/public/app.js +41 -51
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/index.html +13 -10
- package/src/studio/public/library.css +12 -3
- package/src/studio/public/library.html +1 -0
- package/src/studio/public/library.js +36 -14
- 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 +18 -15
- package/src/studio/public/style.css +54 -9
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +27 -12
- 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 +154 -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
|
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,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);
|
|
@@ -22,11 +22,10 @@ export const LAYOUT_INFO = {
|
|
|
22
22
|
science: ["Fórmulas e funções", "Escreva a fórmula e ela vira curva; cada letra vira um controle deslizante. Aceita pontos colados do Excel e superfície 3D"],
|
|
23
23
|
infographic: ["Infográfico", "Arco, ramos, lados, trilhas ou metrô: de 2 a 8 itens coloridos em volta de um centro"],
|
|
24
24
|
diagram: ["Diagrama", "Fluxograma, sequência, estados, UML, jornada, mapa mental (Mermaid) nas cores do tema"],
|
|
25
|
-
scenography: ["Texto no cenário", "13 composições (palco, terminal, café, embarque, pregão, cinema, revista…), sem animação"],
|
|
26
25
|
cover: ["Capa", "Abertura com título, subtítulo e autor"],
|
|
27
26
|
section: ["Seção", "Abre um novo ato ou capítulo"],
|
|
28
27
|
statement: ["Frase de impacto", "Uma ideia em letras grandes"],
|
|
29
|
-
calendar: ["Calendário", "Datas num calendário: um cartão por mês
|
|
28
|
+
calendar: ["Calendário", "Datas num calendário: um cartão por mês, um mês grande de parede, a semana com horários ou a agenda em lista; com muitos eventos, um slide por mês"],
|
|
30
29
|
poster: ["Pôster (infográfico)", "Uma página como figura de revista científica: ilustração principal e painéis com letra (a, b, c), desenho, texto curto e números"],
|
|
31
30
|
definition: ["Definição", "O termo em destaque, a origem da palavra em partes e a definição, com um ícone ilustrativo"],
|
|
32
31
|
headline: ["Manchete", "Uma frase enorme ocupando o slide"],
|
|
@@ -58,7 +57,6 @@ export const LAYOUT_INFO = {
|
|
|
58
57
|
codewalk: ["Código guiado", "Explique uma linha por vez, com destaques e saída simulada"],
|
|
59
58
|
api: ["API ao vivo", "Requisição que executa na hora (tipo Postman): síncrono, polling ou streaming, com código curl/Python"],
|
|
60
59
|
spotlight: ["Foco guiado", "Conduza o olhar por detalhes de uma imagem ou diagrama"],
|
|
61
|
-
kinetic: ["Tipografia cinética", "Frases em sequência viram parte da cena com ritmo e estilos tipográficos que mudam"],
|
|
62
60
|
video: ["Vídeo", "Player YouTube online ou MP4 incorporado"],
|
|
63
61
|
blocks: ["Livre (blocos)", "Monte com blocos em linhas e colunas"],
|
|
64
62
|
canvas: ["Livre (posições)", "Posicione cada elemento à mão"],
|
|
@@ -87,7 +85,6 @@ export const LAYOUT_ASK = {
|
|
|
87
85
|
science: "Mostre a parábola y = ax² + bx + c com controles para a, b e c",
|
|
88
86
|
infographic: "Faça um infográfico com os cinco pilares da sustentabilidade em volta de um centro",
|
|
89
87
|
diagram: "Desenhe o fluxo de atendimento de um pedido, da chegada à entrega",
|
|
90
|
-
scenography: "Abra com a frase \"Além do óbvio\" num cenário de palco",
|
|
91
88
|
cover: "Faça a capa da palestra sobre energia solar, com o meu nome e a data",
|
|
92
89
|
section: "Abra a segunda parte, sobre os resultados",
|
|
93
90
|
statement: "Uma frase de impacto: a água mais barata é a que não se desperdiça",
|
|
@@ -123,7 +120,6 @@ export const LAYOUT_ASK = {
|
|
|
123
120
|
codewalk: "Explique este código uma linha por vez, com a saída simulada",
|
|
124
121
|
api: "Faça uma chamada de API ao vivo que consulta o clima e mostra a resposta",
|
|
125
122
|
spotlight: "Conduza o olhar pelas partes desta planta baixa, uma por vez",
|
|
126
|
-
kinetic: "Faça a abertura com frases curtas aparecendo em ritmo, estilo tipografia cinética",
|
|
127
123
|
video: "Ponha este vídeo do YouTube no slide",
|
|
128
124
|
blocks: "Monte livremente um slide com título, dois textos e uma imagem em colunas",
|
|
129
125
|
canvas: "Posicione o logotipo, o título e a foto onde eu indicar",
|
|
@@ -145,8 +141,8 @@ export const LAYOUT_SAMPLES = {
|
|
|
145
141
|
ribbon: {layout:"ribbon",title:"Do primeiro passo ao resultado",items:[{title:"Explorar",text:"Encontre uma oportunidade."},{title:"Testar",text:"Experimente em pequena escala."},{title:"Evoluir",text:"Repita o que funciona."}]},
|
|
146
142
|
dossier: {layout:"dossier",title:"Referência rápida",items:[{title:"Requisição",text:"GET consulta o recurso.",code:"GET /exemplos/42"},{title:"Resposta",text:"Confira o status antes de usar o corpo.",code:'{"id":42,"status":"ok"}'}]},
|
|
147
143
|
decisionlab: {layout:"decisionlab", title:"A revisão melhora o resultado?", lab:{volume:10000,errorRate:2,reviewRate:100,catchRate:60,introducedRate:0.2,seconds:30}},
|
|
148
|
-
infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios==
|
|
149
|
-
diagram: { layout: "diagram", kicker: "Processo", title: "Da ideia à ==produção==", mermaid: "flowchart LR\n A([:lightbulb: Ideia]):::hi --> B{Vale testar?}\n B -->|sim| C[:flask-conical:
|
|
144
|
+
infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios== da viagem", center: { title: "Desafios" }, items: [{ title: "Roteiro", text: "Três cidades em dez dias" }, { title: "Orçamento", text: "Hospedagem pesa mais" }, { title: "Documentos", text: "Passaporte e visto" }, { title: "Clima", text: "Chuva na segunda semana" }] },
|
|
145
|
+
diagram: { layout: "diagram", kicker: "Processo", title: "Da ideia à ==produção==", mermaid: "flowchart LR\n A([:lightbulb: Ideia]):::hi --> B{Vale testar?}\n B -->|sim| C[:flask-conical: Protótipo]\n B -.->|não| X[Arquivar]:::vazado\n C ==> D[:rocket: Produção]:::em" },
|
|
150
146
|
solution: {layout:"solution", title:"Exercício: quanto rende em 3 anos?", problem:"R$ 1.000 aplicados a 1% ao mês, com juros compostos. Quanto há depois de 36 meses?", givens:[{symbol:"C", value:"1000", unit:"R\\$", label:"capital"}, {symbol:"i", value:"0,01", label:"taxa ao mês"}, {symbol:"n", value:"36", label:"meses"}], find:"M", steps:[{text:"Juros compostos", latex:"M = C\\,(1 + i)^n"}, {text:"Substitui os dados", latex:"M = 1000 \\cdot 1{,}01^{36}"}, {text:"Calcula a potência", latex:"1{,}01^{36} \\approx 1{,}4308"}], answer:{latex:"M \\approx R\\$\\,1.430{,}77", text:"Rende cerca de 43% em 3 anos."}},
|
|
151
147
|
calc: {layout:"calc", title:"Qual é o regime do escoamento?", inputs:{V:{label:"Velocidade", latex:"V", value:1.5, min:0.01, max:4, step:0.01, unit:"m/s"}, D:{label:"Diâmetro", latex:"D", value:0.05, min:0.005, max:0.3, step:0.005, unit:"m"}, nu:{label:"Viscosidade (água, 20 °C)", latex:"\\nu = 1{,}0\\times10^{-6}\\ \\mathrm{m^2/s}", value:0.000001, fixed:true}}, outputs:[{name:"Re", label:"Número de Reynolds", latex:"Re = \\frac{VD}{\\nu}", fn:"V*D/nu", decimals:0}, {name:"regime", label:"Regime", of:"Re", cases:[{below:2300, text:"Laminar", color:"s3"}, {below:4000, text:"Transição", color:"em"}, {text:"Turbulento", color:"hi"}], scale:{min:100, max:1000000, log:true}}]},
|
|
152
148
|
algo: {layout:"algo", title:"Bubble sort passo a passo", algorithm:"bubble", array:[5, 1, 4, 2, 8, 3]},
|
|
@@ -157,15 +153,14 @@ export const LAYOUT_SAMPLES = {
|
|
|
157
153
|
terminals: {layout:"terminals", title:"Dois terminais", file:"README.md", base:"# Projeto\nversão 1", people:["Ana","Beto"],
|
|
158
154
|
turns:[{who:1, edit:{2:"versão 2"}, commit:"sobe a versão"}, {who:1, push:true}, {who:2, pull:true}, {who:2, log:true}]},
|
|
159
155
|
turns: {layout:"turns", title:"Code review ao vivo", people:[{name:"Ana", role:"autora do PR"}, {name:"Beto", role:"revisor"}],
|
|
160
|
-
turns:[{who:1, text:"Abri o PR com a validação
|
|
156
|
+
turns:[{who:1, text:"Abri o PR com a validação das inscrições."}, {who:2, text:"Por que não usa o **schema** que já existe?", tag:"pergunta"}, {who:1, text:"Boa. Troquei e testei assim:", method:"POST", url:"/inscricoes", status:201, code:'{ "nome": "Ana" }', language:"json"}, {who:2, text:"Aprovado.", tag:"aprovado"}]},
|
|
161
157
|
science: {layout:"science", title:"Mexa no a e no b", equations:[{label:"A função",latex:"f(x) = a\\sin(bx)"},{label:"A derivada",latex:"f'(x) = ab\\cos(bx)"}], plot:{functions:["a*sin(b*x)","a*b*cos(b*x)"], x:[-6.3,6.3], params:{a:{value:1.5,min:0,max:3,label:"amplitude"},b:{value:1,min:0.5,max:3,label:"frequência"}}}},
|
|
162
|
-
scenography: {layout:"scenography", scene:"stage", title:"ALÉM DO\nÓBVIO", kicker:"UMA NOVA PERSPECTIVA", subtitle:"Ideias que ocupam espaço.", tone:"dark", deco:"none"},
|
|
163
158
|
cover: { layout: "cover", kicker: "Evento 2026", title: "Título da ==apresentação==", subtitle: "Um subtítulo curto", author: "Seu nome", figure: { icon: "rocket", size: 320 } },
|
|
164
159
|
section: { layout: "section", number: 2, kicker: "Parte dois", title: "O problema" },
|
|
165
160
|
statement: { layout: "statement", kicker: "A ideia", text: "Uma frase que ==muda tudo==." },
|
|
166
161
|
calendar: { layout: "calendar", kicker: "Agenda", title: "Próximos lançamentos", events: [
|
|
167
162
|
{ date: "2026-11-12", title: "Beta aberto", tag: "Produto", text: "Para os 500 primeiros" }, { date: "2026-11-28", title: "Evento de lançamento", tag: "Evento" },
|
|
168
|
-
{ date: "2027-01-15", title: "Versão 2.0", tag: "Produto", text: "Com o app de celular" }], undated: [{ title: "
|
|
163
|
+
{ date: "2027-01-15", title: "Versão 2.0", tag: "Produto", text: "Com o app de celular" }], undated: [{ title: "Loja em outra cidade", text: "Em negociação" }] },
|
|
169
164
|
poster: { layout: "poster", kicker: "Receita", title: "Pão de fermentação natural", subtitle: "Do fermento ao forno em quatro etapas", flow: true,
|
|
170
165
|
panels: [
|
|
171
166
|
{ title: "Fermento", icon: "wheat", text: "Farinha e água, alimentados por 5 dias", facts: [{ value: "1:1", label: "farinha e água" }] },
|
|
@@ -176,9 +171,9 @@ export const LAYOUT_SAMPLES = {
|
|
|
176
171
|
definition: { layout: "definition", kicker: "O que é", term: "Hidrologia", origin: "do grego", parts: [{ word: "hydor", meaning: "água" }, { word: "logos", meaning: "ciência" }], text: "É a ciência que estuda ==a água sobre a Terra==: sua quantidade, distribuição e circulação, e sua relação com o meio ambiente e com os seres vivos.", icon: "droplet" },
|
|
177
172
|
headline: { layout: "headline", text: "78% ==desistem==", caption: "no segundo passo" },
|
|
178
173
|
quote: { layout: "quote", quote: "Dados vencem opiniões.", by: "Alguém sábio" },
|
|
179
|
-
onepage: { layout: "onepage", title: "
|
|
180
|
-
status: { layout: "status", kicker: "Semana 39", title: "Portal do cliente", health: "risco", progress: 65, done: ["Tela de login", "Integração com o cadastro"], doing: ["Recuperação de senha"], blocked: [{ text: "
|
|
181
|
-
hub: { layout: "hub", title: "Qual é o seu caso?", question: "Cada caminho tem o seu
|
|
174
|
+
onepage: { layout: "onepage", title: "Agendamento online", journey: [{ icon: "user", title: "Paciente liga", text: "Espera de 15 min" }, { icon: "file-text", title: "Preenche a ficha" }, { icon: "check", title: "Consulta marcada", text: "Até 5 dias" }], problem: { text: "Marcar por telefone atrasa as consultas", numbers: [{ value: "5 dias", label: "para marcar uma consulta" }] }, solution: { text: "Agendamento pelo celular, com confirmação automática", items: ["Consulta marcada no mesmo dia"] } },
|
|
175
|
+
status: { layout: "status", kicker: "Semana 39", title: "Portal do cliente", health: "risco", progress: 65, done: ["Tela de login", "Integração com o cadastro"], doing: ["Recuperação de senha"], blocked: [{ text: "Aprovação do texto de privacidade", owner: "Jurídico", due: "30/09" }], upcoming: ["Teste com 20 usuários"] },
|
|
176
|
+
hub: { layout: "hub", title: "Qual é o seu caso?", question: "Cada caminho tem o seu ritmo", options: [{ icon: "user", title: "Primeira vez", meta: "4 passos", goto: "inicio" }, { icon: "rocket", title: "Já uso", meta: "Atalhos", goto: "atalhos" }] },
|
|
182
177
|
number: { layout: "number", value: 42, suffix: "%", label: "mais rápido" },
|
|
183
178
|
split: { layout: "split", title: "Texto e figura", bullets: ["Primeiro ponto", "Segundo ponto"], figure: { icon: "lightbulb", size: 300 } },
|
|
184
179
|
full: { layout: "full", figure: { chart: "donut", value: 72, center: "72%" }, title: "Figura em página inteira", overlay: "left" },
|
|
@@ -209,11 +204,7 @@ export const LAYOUT_SAMPLES = {
|
|
|
209
204
|
hotspots: [{ x: 4, y: 18, width: 92, height: 15, title: "O pedido", text: "O verbo GET consulta o recurso. A rota identifica a coleção de aulas." },
|
|
210
205
|
{ x: 4, y: 38, width: 92, height: 10, title: "A confirmação", text: "200 OK confirma o sucesso. O tempo ajuda a investigar a experiência do usuário." },
|
|
211
206
|
{ x: 5, y: 52, width: 78, height: 39, title: "Os dados", text: "A estrutura JSON é o contrato que o front-end precisa entender." }] },
|
|
212
|
-
|
|
213
|
-
beats: [{ text: "Uma ideia", style: "clean", position: "left", color: "white", size: "medium", tag: "GANHA FORMA" },
|
|
214
|
-
{ text: "muda o olhar", style: "clean", position: "right", color: "gold", size: "medium" },
|
|
215
|
-
{ text: "e abre caminhos", style: "clean", position: "left", color: "white", size: "medium" }] },
|
|
216
|
-
// genérico de propósito: troque método, URL e corpo pelos do seu serviço (os exemplos prontos ficam no deck "Exemplo: aula de APIs")
|
|
207
|
+
// genérico de propósito: troque método, URL e corpo pelos do seu serviço (os exemplos prontos ficam na demo "APIs com curl" e no `sagadeck ensaio-api`)
|
|
217
208
|
api: { layout: "api", kicker: "Ao vivo", title: "Minha ==chamada==", text: "Troque o método, a URL e o corpo pelos do seu serviço", request: { method: "POST", url: "{{base}}/echo", body: { mensagem: "Olá, API" } }, answer: "$.recebido.mensagem" },
|
|
218
209
|
video: { layout: "video", title: "Assista", url: "https://example.com", label: "Assistir" },
|
|
219
210
|
blocks: { layout: "blocks", title: "Blocos livres", content: [{ row: [{ lead: "Esquerda" }, { lead: "Direita" }] }] },
|
|
@@ -103,15 +103,6 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
103
103
|
if (!made) throw new Error("Não há um modelo em prévia aberto.");
|
|
104
104
|
return ok(made);
|
|
105
105
|
}
|
|
106
|
-
case "/api/library/decks/example-cenario": {
|
|
107
|
-
// demonstração do Texto no cenário: o YAML e as imagens de exemplo (fundo e recorte transparente)
|
|
108
|
-
const dir0 = TEMPLATE_DIRS.map((d) => path.join(d, "cenario")).find((d) => fs.existsSync(d));
|
|
109
|
-
if (!dir0) throw new Error("o exemplo não veio no pacote (templates/cenario)");
|
|
110
|
-
const tpl = fs.readdirSync(dir0).find((f) => f.endsWith(".yaml"));
|
|
111
|
-
const id = L.createDeck(b.topic || "", YAML.parse(fs.readFileSync(path.join(dir0, tpl), "utf8")));
|
|
112
|
-
fs.cpSync(path.join(dir0, "imagens"), path.join(path.dirname(L.resolveId(id)), "imagens"), { recursive: true, force: false });
|
|
113
|
-
return ok({ id });
|
|
114
|
-
}
|
|
115
106
|
case "/api/library/decks/example": {
|
|
116
107
|
// exemplo de slides api (aula de APIs de IA): roda no ambiente embutido "ensaio", sem configurar nada
|
|
117
108
|
const tpl = templateFile("ensaio-api.yaml");
|
package/src/studio/project.js
CHANGED
|
@@ -226,6 +226,7 @@ export function writeSheet(P, rel, rows) {
|
|
|
226
226
|
if (!Array.isArray(rows) || rows.some((r) => !Array.isArray(r))) throw new Error("Planilha inválida.");
|
|
227
227
|
const old = fs.existsSync(abs) ? parseCSV(fs.readFileSync(abs, "utf8"), ext === "tsv" ? "\t" : undefined) : { delimiter: ext === "tsv" ? "\t" : ",", eol: "\n" };
|
|
228
228
|
const tmp = abs + ".tmp";
|
|
229
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true }); // planilha nova numa pasta que ainda não existe (contexto/)
|
|
229
230
|
const clean = rows.map((r) => r.map((c) => String(c ?? "")));
|
|
230
231
|
while (clean.length > 1 && clean.at(-1).every((c) => c === "")) clean.pop(); // linha vazia no fim (o cursor passou dela) não vai para o arquivo
|
|
231
232
|
fs.writeFileSync(tmp, toCSV(clean, old));
|
|
@@ -8,8 +8,8 @@ window.SagaAISettings = (() => {
|
|
|
8
8
|
.ai-cfg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap}.ai-cfg button{padding:7px 14px;border-radius:8px;border:1px solid var(--border,#c9ced6);background:transparent;color:inherit;cursor:pointer;font:inherit}
|
|
9
9
|
.ai-cfg .rec{background:var(--selected,#eef);border-radius:8px;padding:8px 10px;margin:6px 0 4px}.ai-cfg .rec button{margin-top:6px;display:block}
|
|
10
10
|
.ai-cfg button.primary{background:var(--brand,#5b4bdb);border-color:var(--brand,#5b4bdb);color:#fff}.ai-cfg [data-status]{margin-top:10px;min-height:1.4em}.ai-cfg [data-status].ok{color:#15803d}.ai-cfg [data-status].err{color:#b91c1c}`;
|
|
11
|
-
const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web" };
|
|
12
|
-
const HINT = { text: "obrigatório", vision: "opcional; vazio = o de texto", image: "opcional; para gerar ilustrações", search: "opcional; no OpenRouter, um modelo :online" };
|
|
11
|
+
const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web", video: "Modelo de vídeo" };
|
|
12
|
+
const HINT = { text: "obrigatório", vision: "opcional; vazio = o de texto", image: "opcional; para gerar ilustrações", search: "opcional; no OpenRouter, um modelo :online", video: "opcional; para gerar clipes (cobrado por segundo)" };
|
|
13
13
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
14
14
|
let dialog, info, onSaved = () => {};
|
|
15
15
|
|