sagadeck 1.2.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/LICENSE +21 -0
- package/README.md +305 -0
- package/SKILL.md +148 -0
- package/bin/sagadeck.js +511 -0
- package/docs/ANDAMENTO.md +1255 -0
- package/docs/EXPERIENCIAS.md +39 -0
- package/docs/PLANO-EXPERIENCIAS.md +39 -0
- package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
- package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
- package/docs/REFERENCIA.md +1131 -0
- package/package.json +59 -0
- package/src/adaptive-layouts.js +18 -0
- package/src/ai/art-direction.js +8 -0
- package/src/ai/commands.js +130 -0
- package/src/ai/context.js +245 -0
- package/src/ai/deck-ai.js +1135 -0
- package/src/ai/directions.js +74 -0
- package/src/ai/document-materials.js +49 -0
- package/src/ai/document-visuals.js +215 -0
- package/src/ai/evaluation.js +31 -0
- package/src/ai/ground.js +55 -0
- package/src/ai/llm.js +292 -0
- package/src/ai/quality.js +51 -0
- package/src/ai/relay.js +45 -0
- package/src/ai/staged-generation.js +24 -0
- package/src/ai/transform.js +858 -0
- package/src/ai/usage.js +17 -0
- package/src/ai/variety.js +80 -0
- package/src/ai/video-frames.js +24 -0
- package/src/ai/video-generation.js +126 -0
- package/src/ai/web-operation.js +31 -0
- package/src/algo-catalog.js +153 -0
- package/src/algo-trace.js +139 -0
- package/src/api-client.js +514 -0
- package/src/api-collections.js +14 -0
- package/src/api-demo.js +222 -0
- package/src/aspect.js +40 -0
- package/src/build.js +323 -0
- package/src/carousel.js +56 -0
- package/src/chrome.js +65 -0
- package/src/code-highlight.js +68 -0
- package/src/code-lab-browser.js +22 -0
- package/src/code-lab.js +3 -0
- package/src/code-language.js +52 -0
- package/src/csv.js +74 -0
- package/src/decision-lab.js +20 -0
- package/src/deck-file.js +119 -0
- package/src/diagram/napkin-examples.js +13 -0
- package/src/diagram/napkin.js +539 -0
- package/src/docx.js +128 -0
- package/src/dynamics/git-sim.js +200 -0
- package/src/dynamics/layouts.js +112 -0
- package/src/elements.js +554 -0
- package/src/experiences.js +124 -0
- package/src/exploration.js +31 -0
- package/src/export/browser.js +53 -0
- package/src/export/estudo.js +67 -0
- package/src/export/pptx.js +475 -0
- package/src/export/roteiro.js +40 -0
- package/src/export/shots.js +182 -0
- package/src/figures/charts.js +383 -0
- package/src/figures/diagrams.js +117 -0
- package/src/figures/icons.js +166 -0
- package/src/figures/material-pictos.js +20 -0
- package/src/figures/pictos.js +255 -0
- package/src/figures/points.js +106 -0
- package/src/figures/qr.js +29 -0
- package/src/figures/ufmap.js +48 -0
- package/src/fiscal/autofix.js +377 -0
- package/src/fiscal/normalize.js +115 -0
- package/src/graph-lab.js +24 -0
- package/src/hologram.js +30 -0
- package/src/identity.js +86 -0
- package/src/import/crop.js +59 -0
- package/src/import/index.js +132 -0
- package/src/import/legacy-word.js +42 -0
- package/src/import/merge.js +45 -0
- package/src/import/office.js +123 -0
- package/src/import/omml.js +69 -0
- package/src/import/pdf-render.js +56 -0
- package/src/import/pptx.js +701 -0
- package/src/import/xml.js +42 -0
- package/src/infographic.js +401 -0
- package/src/layouts.js +723 -0
- package/src/lessons.js +171 -0
- package/src/library.js +399 -0
- package/src/markup.js +116 -0
- package/src/master.js +214 -0
- package/src/mcp/server.js +371 -0
- package/src/meeting.js +42 -0
- package/src/motion.js +56 -0
- package/src/package.js +116 -0
- package/src/playground.js +19 -0
- package/src/portal-scene.js +20 -0
- package/src/preferences.js +61 -0
- package/src/protect.js +51 -0
- package/src/purpose.js +24 -0
- package/src/pytrace.js +1092 -0
- package/src/research/duckduckgo.js +73 -0
- package/src/research/research.js +202 -0
- package/src/research/svg-image.js +17 -0
- package/src/research/visuals.js +52 -0
- package/src/runtime/api-collection-ui.js +5 -0
- package/src/runtime/api-collections.js +28 -0
- package/src/runtime/api-core.js +530 -0
- package/src/runtime/api-ui.js +966 -0
- package/src/runtime/base.css +1774 -0
- package/src/runtime/calc.js +139 -0
- package/src/runtime/code-lab.js +371 -0
- package/src/runtime/decision-lab.js +49 -0
- package/src/runtime/diagram.js +341 -0
- package/src/runtime/fit.js +268 -0
- package/src/runtime/fonts/caveat.css +2 -0
- package/src/runtime/fonts/comic-neue.css +3 -0
- package/src/runtime/fonts/fredoka.css +2 -0
- package/src/runtime/fonts/index.json +10 -0
- package/src/runtime/fonts/inter.css +2 -0
- package/src/runtime/fonts/patrick-hand.css +2 -0
- package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
- package/src/runtime/fonts/press-start-2p.css +2 -0
- package/src/runtime/fonts/vt323.css +2 -0
- package/src/runtime/formula.js +148 -0
- package/src/runtime/graph-lab.js +72 -0
- package/src/runtime/meeting.css +7 -0
- package/src/runtime/meeting.js +10 -0
- package/src/runtime/motion.js +15 -0
- package/src/runtime/playground.js +11 -0
- package/src/runtime/points.js +19 -0
- package/src/runtime/portal-scene.css +6 -0
- package/src/runtime/portal-scene.js +22 -0
- package/src/runtime/runtime.js +827 -0
- package/src/runtime/science.js +89 -0
- package/src/runtime/skins/aluminio.css +31 -0
- package/src/runtime/skins/arcade.css +25 -0
- package/src/runtime/skins/aurora.css +17 -0
- package/src/runtime/skins/bauhaus.css +19 -0
- package/src/runtime/skins/editorial.css +19 -0
- package/src/runtime/skins/jornal.css +17 -0
- package/src/runtime/skins/manual-noite.css +14 -0
- package/src/runtime/skins/manual.css +14 -0
- package/src/runtime/skins/noite.css +21 -0
- package/src/runtime/skins/oceano.css +17 -0
- package/src/runtime/skins/pop.css +23 -0
- package/src/runtime/skins/prata.css +18 -0
- package/src/runtime/skins/prisma.css +28 -0
- package/src/runtime/skins/rabisco.css +18 -0
- package/src/runtime/skins/relevo.css +33 -0
- package/src/runtime/skins/sinal.css +16 -0
- package/src/runtime/skins/terminal.css +23 -0
- package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
- package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
- package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
- package/src/runtime/vendor/katex.css +1 -0
- package/src/runtime/vendor/mermaid.min.js +3636 -0
- package/src/runtime/vendor/plotly.min.js +3620 -0
- package/src/runtime/video-player.js +35 -0
- package/src/science.js +162 -0
- package/src/studio/ai-response.js +36 -0
- package/src/studio/api-routes.js +152 -0
- package/src/studio/assets/contexto/leia-me.md +12 -0
- package/src/studio/assets/contexto/vendas.csv +7 -0
- package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
- package/src/studio/assets/imagens/colecao-cor.png +0 -0
- package/src/studio/assets/imagens/colecao-equipe.png +0 -0
- package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
- package/src/studio/code-routes.js +32 -0
- package/src/studio/command-approval.js +48 -0
- package/src/studio/demo-decks.js +98 -0
- package/src/studio/direction-routes.js +29 -0
- package/src/studio/errors.js +65 -0
- package/src/studio/exploration-demo.js +17 -0
- package/src/studio/exporting.js +105 -0
- package/src/studio/generation-report.js +10 -0
- package/src/studio/instance.js +48 -0
- package/src/studio/layout-samples.js +152 -0
- package/src/studio/project.js +371 -0
- package/src/studio/public/api-collections.js +28 -0
- package/src/studio/public/app.js +5265 -0
- package/src/studio/public/art-preview.js +10 -0
- package/src/studio/public/calc-fields.js +11 -0
- package/src/studio/public/chat-progress.js +47 -0
- package/src/studio/public/developer-fields.js +22 -0
- package/src/studio/public/explorer.js +443 -0
- package/src/studio/public/history.js +58 -0
- package/src/studio/public/index.html +867 -0
- package/src/studio/public/inspector.js +270 -0
- package/src/studio/public/lab-fields.js +5 -0
- package/src/studio/public/library.css +275 -0
- package/src/studio/public/library.html +55 -0
- package/src/studio/public/library.js +471 -0
- package/src/studio/public/merge-decks.js +155 -0
- package/src/studio/public/review-ui.js +100 -0
- package/src/studio/public/screenshot-editor.js +140 -0
- package/src/studio/public/share-ui.js +72 -0
- package/src/studio/public/slide-clipboard.js +23 -0
- package/src/studio/public/slide-form.js +1263 -0
- package/src/studio/public/studio-awareness.js +154 -0
- package/src/studio/public/studio-next.css +303 -0
- package/src/studio/public/style.css +1262 -0
- package/src/studio/public/ui-icons.js +2 -0
- package/src/studio/public/viewers.js +372 -0
- package/src/studio/public/visual-editor.js +326 -0
- package/src/studio/public/workspace.js +18 -0
- package/src/studio/public-files.js +1 -0
- package/src/studio/request-helpers.js +41 -0
- package/src/studio/server.js +1583 -0
- package/src/studio/share-routes.js +146 -0
- package/src/studio/slide-copy-routes.js +26 -0
- package/src/studio/slide-copy.js +30 -0
- package/src/studio/snapshot.js +160 -0
- package/src/studio/style-routes.js +103 -0
- package/src/studio/template-collections.js +97 -0
- package/src/studio/versions.js +95 -0
- package/src/studio/video-download.js +29 -0
- package/src/studio/visual-keys.js +52 -0
- package/src/studio/workspaces.js +39 -0
- package/src/table.js +93 -0
- package/src/templates/scaffold.js +372 -0
- package/src/themes.js +604 -0
- package/src/trace-view.js +450 -0
- package/src/uid.js +19 -0
- package/src/video-player.js +40 -0
- package/src/visual-edits.js +99 -0
- package/src/ws.js +134 -0
- package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
- package/templates/cenario/imagens/cidade.svg +30 -0
- package/templates/cenario/imagens/pessoa.svg +15 -0
- package/templates/cenario-e-ciencia.yaml +47 -0
- package/templates/ensaio-api.yaml +160 -0
- package/templates/exemplo-alegre.yaml +75 -0
- package/templates/exemplo-keynote.yaml +90 -0
- package/templates/exemplo-napkin.yaml +139 -0
- package/templates/exemplo.yaml +228 -0
package/src/carousel.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Carrossel (layout carousel): um item por clique, com foto (sua) e texto.
|
|
2
|
+
// style: arc — roda em semicírculo com as fotos; gira até o item da vez; o texto atual sai pela esquerda e o
|
|
3
|
+
// novo entra da esquerda para a direita, sobrepondo
|
|
4
|
+
// style: rings — a foto em anéis: o de fora gira no sentido anti-horário, o de dentro no horário, e travam no
|
|
5
|
+
// lugar formando a foto; o texto sobe junto com o anel de fora
|
|
6
|
+
// Sem foto, cada item ganha uma foto de demonstração desenhada (SVG, funciona sem internet).
|
|
7
|
+
import { md, esc } from "./markup.js";
|
|
8
|
+
import { figureHTML } from "./elements.js";
|
|
9
|
+
|
|
10
|
+
// ---- fotos de demonstração: paisagens desenhadas, uma paleta por item ----
|
|
11
|
+
const SCENES = [
|
|
12
|
+
{ sky: ["#ffb36b", "#ff6f61"], sun: "#fff3c4", far: "#b8577a", near: "#5b2a58", kind: "mountains" },
|
|
13
|
+
{ sky: ["#8fd3f4", "#2f80ed"], sun: "#ffffff", far: "#1f5fa8", near: "#0d3b66", kind: "sea" },
|
|
14
|
+
{ sky: ["#1b1f3b", "#51306d"], sun: "#ffe8a3", far: "#2c2350", near: "#120f26", kind: "city" },
|
|
15
|
+
{ sky: ["#ffd29d", "#f08a4b"], sun: "#fff7e0", far: "#d9864a", near: "#9c4f2e", kind: "dunes" },
|
|
16
|
+
{ sky: ["#c8f0d8", "#5fb88b"], sun: "#fbfff0", far: "#2f7d5b", near: "#174a35", kind: "forest" },
|
|
17
|
+
];
|
|
18
|
+
export function demoPhoto(k) {
|
|
19
|
+
const s = SCENES[k % SCENES.length], id = `g${k}`;
|
|
20
|
+
let land = "";
|
|
21
|
+
if (s.kind === "mountains") land = `<path d="M0 560 L180 360 L300 470 L470 280 L640 480 L800 380 L800 800 L0 800Z" fill="${s.far}"/><path d="M0 650 L220 500 L400 620 L600 470 L800 600 L800 800 L0 800Z" fill="${s.near}"/>`;
|
|
22
|
+
else if (s.kind === "sea") land = `<rect y="520" width="800" height="280" fill="${s.far}"/>${[0, 1, 2, 3, 4].map((i) => `<path d="M0 ${560 + i * 50} q100 -24 200 0 t200 0 t200 0 t200 0" stroke="#ffffff55" stroke-width="6" fill="none"/>`).join("")}<rect y="700" width="800" height="100" fill="${s.near}"/>`;
|
|
23
|
+
else if (s.kind === "city") land = [60, 150, 230, 330, 420, 520, 600, 690].map((x, i) => { const h = 180 + ((i * 97) % 220); return `<rect x="${x}" y="${800 - h - 120}" width="${70 + (i % 3) * 14}" height="${h + 120}" fill="${i % 2 ? s.far : s.near}"/>${Array.from({ length: 5 }, (_, j) => `<rect x="${x + 14}" y="${800 - h - 90 + j * 44}" width="12" height="16" fill="#ffd66b" opacity="${(i + j) % 3 ? 0.9 : 0.25}"/>`).join("")}`; }).join("");
|
|
24
|
+
else if (s.kind === "dunes") land = `<path d="M0 560 Q200 460 420 560 T800 520 L800 800 L0 800Z" fill="${s.far}"/><path d="M0 660 Q260 560 520 660 T800 640 L800 800 L0 800Z" fill="${s.near}"/>`;
|
|
25
|
+
else land = `<rect y="600" width="800" height="200" fill="${s.near}"/>${Array.from({ length: 11 }, (_, i) => `<path d="M${i * 78 - 10} 640 l40 -${170 + (i * 53) % 90} l40 ${170 + (i * 53) % 90}Z" fill="${i % 2 ? s.far : s.near}"/>`).join("")}`;
|
|
26
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800"><defs><linearGradient id="${id}" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="${s.sky[0]}"/><stop offset="1" stop-color="${s.sky[1]}"/></linearGradient></defs><rect width="800" height="800" fill="url(#${id})"/><circle cx="${540 - (k % 3) * 120}" cy="${260 + (k % 2) * 40}" r="${80 + (k % 3) * 18}" fill="${s.sun}" opacity=".92"/>${land}</svg>`;
|
|
27
|
+
return `data:image/svg+xml;base64,${Buffer.from(svg).toString("base64")}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// a foto do item: a da pessoa (image: caminho, link ou data:) ou a de demonstração
|
|
31
|
+
function photo(item, k, ctx) {
|
|
32
|
+
if (item.image) {
|
|
33
|
+
const html = figureHTML({ image: item.image, fit: "cover", alt: item.alt || item.title || "" }, ctx);
|
|
34
|
+
const src = html.match(/src="([^"]+)"/)?.[1];
|
|
35
|
+
if (src) return src;
|
|
36
|
+
}
|
|
37
|
+
return demoPhoto(k);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function carouselHTML(s, ctx, head) {
|
|
41
|
+
const items = (s.items || []).map((x) => (typeof x === "string" ? { title: x } : x || {}));
|
|
42
|
+
if (!items.length) items.push({ title: "Primeiro item", text: "Troque pelos seus itens: título, texto e foto." }, { title: "Segundo item" }, { title: "Terceiro item" });
|
|
43
|
+
const n = items.length, style = s.style === "rings" ? "rings" : "arc";
|
|
44
|
+
const text = (it, k) => `<article class="car-text${k === 0 ? " active" : ""}" data-lesson-k="${k}">${it.label ? `<div class="car-kicker t f-label">${md(it.label)}</div>` : `<div class="car-kicker t f-label">${String(k + 1).padStart(2, "0")} / ${String(n).padStart(2, "0")}</div>`}<h3 class="car-title t f-display">${md(it.title || "")}</h3>${it.text ? `<p class="car-body t f-body">${md(it.text)}</p>` : ""}</article>`;
|
|
45
|
+
const srcs = items.map((it, k) => photo(it, k, ctx));
|
|
46
|
+
const alt = (it) => esc(it.alt || it.title || "");
|
|
47
|
+
let visual;
|
|
48
|
+
if (style === "arc") {
|
|
49
|
+
const step = Math.max(24, Math.min(34, 200 / Math.max(1, Math.min(n, 6)))); // fotos grandes: vizinhos sem encostar
|
|
50
|
+
visual = `<div class="car-wheel" style="--step:${step}deg" aria-hidden="true"><div class="car-track"></div>${items.map((it, k) => `<div class="car-item${k === 0 ? " active" : ""}" data-lesson-k="${k}" style="--k:${k}"><div class="car-photo"><img src="${srcs[k]}" alt="${alt(it)}"></div></div>`).join("")}</div>`;
|
|
51
|
+
} else {
|
|
52
|
+
visual = `<div class="car-disc">${items.map((it, k) => `<div class="car-slide${k === 0 ? " active" : ""}" data-lesson-k="${k}"><div class="car-ring-out"><img src="${srcs[k]}" alt="${alt(it)}"></div><div class="car-ring-in"><img src="${srcs[k]}" alt=""></div></div>`).join("")}<div class="car-lock" aria-hidden="true"></div></div>
|
|
53
|
+
<nav class="car-dots" aria-hidden="true">${items.map((it, k) => `<span class="car-dot${k === 0 ? " active" : ""}" data-lesson-k="${k}"></span>`).join("")}</nav>`;
|
|
54
|
+
}
|
|
55
|
+
return `<div class="L-carousel car-${style}" data-lesson="carousel" data-lesson-count="${n}" style="--n:${n}">${s.title || s.kicker ? head(s) : ""}<div class="car-stage">${visual}<div class="car-texts">${items.map(text).join("")}</div></div></div>`;
|
|
56
|
+
}
|
package/src/chrome.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Cabeçalho e rodapé dos slides, com variáveis.
|
|
2
|
+
//
|
|
3
|
+
// footer: "Texto" -> texto à esquerda + número à direita (como sempre foi)
|
|
4
|
+
// footer: false -> sem rodapé
|
|
5
|
+
// footer: { left: "{autor} · {evento}", center: "", right: "{pagina} / {total}" }
|
|
6
|
+
// header: { left: "{depto}", right: "{data:DD/MM/AAAA}" }
|
|
7
|
+
//
|
|
8
|
+
// Variáveis: {titulo} {autor} {evento} {depto} {data} {data:MASCARA} {pagina} {n} {total}
|
|
9
|
+
// {pagina} = 01, 02… · {n} = 1, 2… · MASCARA: DD MM AAAA AA MMM (jan) MMMM (janeiro) — também YYYY/YY
|
|
10
|
+
// Campos do deck usados: title, author, event, department, date (AAAA-MM-DD; sem date, usa a data de hoje).
|
|
11
|
+
import { esc, plain } from "./markup.js";
|
|
12
|
+
|
|
13
|
+
const MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
14
|
+
|
|
15
|
+
export function formatDate(date, mask = "DD/MM/AAAA") {
|
|
16
|
+
const d = date instanceof Date ? date : parseDate(date);
|
|
17
|
+
if (!d) return String(date ?? "");
|
|
18
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
19
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
20
|
+
const yyyy = String(d.getFullYear());
|
|
21
|
+
return mask.replace(/MMMM|MMM|AAAA|YYYY|AA|YY|DD|MM/g, (t) => ({
|
|
22
|
+
DD: dd, MM: mm, AAAA: yyyy, YYYY: yyyy, AA: yyyy.slice(2), YY: yyyy.slice(2), MMM: MESES[d.getMonth()].slice(0, 3), MMMM: MESES[d.getMonth()],
|
|
23
|
+
})[t]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function parseDate(v) {
|
|
27
|
+
if (v == null || v === "" || v === "hoje") return new Date();
|
|
28
|
+
if (v instanceof Date) return v;
|
|
29
|
+
const m = String(v).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
30
|
+
if (m) return new Date(+m[1], +m[2] - 1, +m[3]);
|
|
31
|
+
const br = String(v).match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
|
32
|
+
if (br) return new Date(+br[3], +br[2] - 1, +br[1]);
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function fillTokens(tpl, spec, i, total) {
|
|
37
|
+
const vars = {
|
|
38
|
+
titulo: spec.title, autor: spec.author, evento: spec.event, depto: spec.department,
|
|
39
|
+
pagina: String(i + 1).padStart(2, "0"), n: String(i + 1), total: String(total),
|
|
40
|
+
};
|
|
41
|
+
return String(tpl ?? "").replace(/\{(\w+)(?::([^}]*))?\}/g, (all, k, arg) => {
|
|
42
|
+
if (k === "data") return formatDate(spec.date, arg || "DD/MM/AAAA");
|
|
43
|
+
return k in vars ? String(vars[k] ?? "") : all;
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Normaliza footer/header do deck em { left, center, right } (ou null = não tem)
|
|
48
|
+
export function barSlots(v, spec, kind) {
|
|
49
|
+
if (v === false) return null;
|
|
50
|
+
if (v == null) return kind === "footer" && spec.title ? { left: "{titulo}", right: "{pagina}" } : null;
|
|
51
|
+
if (typeof v === "string") return { left: v, right: "{pagina}" };
|
|
52
|
+
if (typeof v === "object") return { left: v.left || "", center: v.center || "", right: v.right || "" };
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function barHTML(kind, spec, i, total) {
|
|
57
|
+
const slots = barSlots(spec[kind], spec, kind);
|
|
58
|
+
if (!slots) return "";
|
|
59
|
+
const cell = (pos) => {
|
|
60
|
+
const txt = fillTokens(slots[pos], spec, i, total).trim();
|
|
61
|
+
return `<span class="bar-${pos}${/^\d+$/.test(txt) ? " fn" : ""}">${esc(plain(txt))}</span>`;
|
|
62
|
+
};
|
|
63
|
+
if (!["left", "center", "right"].some((p) => String(slots[p] || "").trim())) return "";
|
|
64
|
+
return `<div class="${kind === "footer" ? "foot" : "headbar"} f-label">${cell("left")}${cell("center")}${cell("right")}</div>`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { esc } from "./markup.js";
|
|
2
|
+
|
|
3
|
+
const KEYWORDS = {
|
|
4
|
+
Python: new Set("and as assert async await break class continue def del elif else except finally for from global if import in is lambda nonlocal not or pass raise return try while with yield".split(" ")),
|
|
5
|
+
Java: new Set("abstract assert boolean break byte case catch char class const continue default do double else enum extends final finally float for if implements import instanceof int interface long native new package private protected public return short static strictfp super switch synchronized this throw throws transient try void volatile while var record".split(" ")),
|
|
6
|
+
JavaScript: new Set("async await break case catch class const continue debugger default delete do else export extends finally for from function if import in instanceof let new of return static super switch this throw try typeof var void while with yield".split(" ")),
|
|
7
|
+
TypeScript: new Set("abstract any as asserts async await bigint boolean break case catch class const constructor continue declare default delete do else enum export extends finally for from function get if implements import in infer instanceof interface is keyof let namespace never new number object of package private protected public readonly return set static string super switch symbol this throw try type typeof undefined unique unknown var void while with yield".split(" ")),
|
|
8
|
+
"C#": new Set("abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw try typeof uint ulong unchecked unsafe ushort using virtual void volatile while".split(" ")),
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const TYPES = {
|
|
12
|
+
Python: new Set("bool dict float int list object set str tuple".split(" ")),
|
|
13
|
+
Java: new Set("ArrayList Boolean Byte Character Double Float Integer List Long Map Object Set Short String".split(" ")),
|
|
14
|
+
JavaScript: new Set("Array Boolean Date Map Number Object Promise Set String".split(" ")),
|
|
15
|
+
TypeScript: new Set("Array any bigint boolean never number object string symbol unknown void".split(" ")),
|
|
16
|
+
"C#": new Set("bool byte char DateTime decimal double float int List long object short string Task uint ulong var void".split(" ")),
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const CONSTANTS = new Set(["False", "None", "True", "false", "null", "true", "undefined", "NaN", "Infinity"]);
|
|
20
|
+
|
|
21
|
+
function tokenPattern(language) {
|
|
22
|
+
const comments = language === "Python"
|
|
23
|
+
? "#[^\\n]*"
|
|
24
|
+
: "\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/";
|
|
25
|
+
const strings = language === "Python"
|
|
26
|
+
? "(?:[rRuUbBfF]{0,2})(?:'''[\\s\\S]*?'''|\"\"\"[\\s\\S]*?\"\"\"|'(?:\\\\.|[^'\\\\])*'|\"(?:\\\\.|[^\"\\\\])*\")"
|
|
27
|
+
: "(?:\\$?@?\"(?:\"\"|\\\\.|[^\"\\\\])*\"|@?\"(?:\"\"|[^\"])*\"|'(?:\\\\.|[^'\\\\])*'|`(?:\\\\.|[^`\\\\])*`)";
|
|
28
|
+
const number = "(?:\\b(?:0[xX][\\da-fA-F]+|0[bB][01]+|\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)(?:[fFdDlL])?|\\.\\d+(?:[eE][+-]?\\d+)?)";
|
|
29
|
+
return new RegExp(`(?<comment>${comments})|(?<string>${strings})|(?<number>${number})|(?<identifier>[A-Za-z_$][\\w$]*)|(?<operator>[-+*/%=&|^!<>~?:]+)|(?<punctuation>[{}()\\[\\].,;])`, "g");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function classify(match, language, source, offset) {
|
|
33
|
+
const groups = match.groups;
|
|
34
|
+
if (groups.comment) return "comment";
|
|
35
|
+
if (groups.string) return "string";
|
|
36
|
+
if (groups.number) return "number";
|
|
37
|
+
if (groups.identifier) {
|
|
38
|
+
const word = groups.identifier;
|
|
39
|
+
if (KEYWORDS[language].has(word)) return "keyword";
|
|
40
|
+
if (CONSTANTS.has(word)) return "constant";
|
|
41
|
+
if (TYPES[language].has(word)) return "type";
|
|
42
|
+
if (/^\s*\(/.test(source.slice(offset + match[0].length))) return "function";
|
|
43
|
+
}
|
|
44
|
+
if (groups.operator) return "operator";
|
|
45
|
+
if (groups.punctuation) return "punctuation";
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function highlightCode(source, language) {
|
|
50
|
+
const pattern = tokenPattern(language);
|
|
51
|
+
const lines = [""];
|
|
52
|
+
const append = (value, kind = "") => {
|
|
53
|
+
const parts = value.split("\n");
|
|
54
|
+
parts.forEach((part, index) => {
|
|
55
|
+
if (index) lines.push("");
|
|
56
|
+
if (part) lines[lines.length - 1] += kind ? `<span class="tok tok-${kind}">${esc(part)}</span>` : esc(part);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
let cursor = 0;
|
|
61
|
+
for (const match of source.matchAll(pattern)) {
|
|
62
|
+
append(source.slice(cursor, match.index));
|
|
63
|
+
append(match[0], classify(match, language, source, match.index));
|
|
64
|
+
cursor = match.index + match[0].length;
|
|
65
|
+
}
|
|
66
|
+
append(source.slice(cursor));
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { traceHTML } from './trace-view.js';
|
|
2
|
+
// Mesmo interpretador do algoritmo animado; não há eval, rede nem processo Python no navegador.
|
|
3
|
+
for(const root of document.querySelectorAll('[data-codelab]')){
|
|
4
|
+
const $=s=>root.querySelector(s),initial=JSON.parse($('.clab-model').textContent);let index=0;
|
|
5
|
+
const show=()=>{const frames=[...root.querySelectorAll('.dyn-frame')];index=Math.max(0,Math.min(frames.length-1,index));frames.forEach((e,i)=>{e.classList.toggle('active',i===index);e.setAttribute('aria-hidden',i!==index);});$('.clab-status').textContent=`Passo ${index+1} de ${frames.length}`;};
|
|
6
|
+
async function run(){if(initial.engine!=="trace"){return nativeRun()}try{const warnings=[],program=$('[data-clab-program]').value,call=$('[data-clab-call]').value;if(program.length>16000||call.length>2000)throw new Error('Reduza o programa ou a entrada para uma experiência de aula.');const html=traceHTML({maxSteps:initial.maxSteps},{program,call,view:null,name:'Sua execução'},{warnings},()=>'',(i,body,extra='')=>`<section class="lesson-panel dyn-frame ${extra}" data-lesson-panel="${i}">${body}</section>`);
|
|
7
|
+
if(warnings.length&&html.includes('dyn-error'))throw new Error(warnings.join(' '));$('.clab-stage').innerHTML=html;index=0;show();root.dataset.codelabReady='true';
|
|
8
|
+
}catch(e){$('.clab-status').textContent=`Não executou: ${e.message}`;}}
|
|
9
|
+
async function nativeRun(){
|
|
10
|
+
const button=$('[data-clab-run]');button.disabled=true;$('.clab-status').textContent='Executando '+initial.engine+'…';
|
|
11
|
+
try{
|
|
12
|
+
if(!/^https?:$/.test(location.protocol)||location.href==='about:srcdoc')throw new Error('Abra esta apresentação no Studio para usar o runtime nativo.');
|
|
13
|
+
const program=$('[data-clab-program]').value,call=$('[data-clab-call]').value.trim();
|
|
14
|
+
const code=program+(call?'\n'+(initial.engine==='python'?'print(repr('+call+'))':'console.log(await ('+call+'));'):'');
|
|
15
|
+
const response=await fetch('api/code/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({language:initial.engine,code})}),data=await response.json();
|
|
16
|
+
if(!response.ok)throw new Error(data.error||'Execução indisponível');
|
|
17
|
+
const pre=document.createElement('pre');pre.className='clab-native-output';pre.textContent=[data.stdout,data.stderr].filter(Boolean).join('\n')||'(sem saída)';$('.clab-stage').replaceChildren(pre);
|
|
18
|
+
$('.clab-status').textContent=(data.timedOut?'Tempo excedido':'Saída '+data.exitCode)+' · '+data.elapsedMs+' ms';root.dataset.codelabReady='true';
|
|
19
|
+
}catch(e){$('.clab-status').textContent='Não executou: '+e.message;}finally{button.disabled=false;}
|
|
20
|
+
}
|
|
21
|
+
$('[data-clab-run]').onclick=run;$('[data-clab-next]').onclick=()=>{index++;show();};$('[data-clab-prev]').onclick=()=>{index--;show();};$('[data-clab-reset]').onclick=()=>{$('[data-clab-program]').value=initial.program;$('[data-clab-call]').value=initial.call;run();};if(initial.engine==='trace')run();else{root.dataset.codelabReady='true';$('[data-clab-prev]').hidden=true;$('[data-clab-next]').hidden=true;$('.clab-status').textContent='Execução nativa · clique Executar para iniciar.';}
|
|
22
|
+
}
|
package/src/code-lab.js
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { esc } from './markup.js';
|
|
2
|
+
export const codeLabExample={layout:'codelab',title:'Mude a entrada. Explique a saída.',program:'def lotes(total, tamanho):\n feitos = 0\n while feitos < total:\n feitos += tamanho\n print(min(feitos, total))\n return feitos',call:'lotes(12, 5)',explanation:'Teste uma entrada que termina exatamente no tamanho do lote e outra que deixa um resto. Como você corrigiria o retorno?'};
|
|
3
|
+
export function codeLabHTML(s,ctx,head){return `<div class="L-codelab" data-codelab>${head(s)}<div class="clab-toolbar"><label>Chamada / entrada<input data-clab-call value="${esc(s.call||'')}" spellcheck="false"></label><button data-clab-run>Executar minha versão</button><button data-clab-prev>Voltar passo</button><button data-clab-next>Próximo passo</button><button data-clab-reset>Restaurar</button></div><div class="clab-body"><details class="clab-editor"><summary>Editar o programa</summary><textarea data-clab-program spellcheck="false" aria-label="Programa">${esc(s.program||'')}</textarea><small>${s.engine && s.engine!=="trace" ? "Execução nativa no Studio com o runtime instalado. Clique Executar; imports usam os pacotes da máquina." : "Python de aula, executado offline: listas, dicionários, funções, laços, math, heapq e deque. Para bibliotecas externas, escolha execução nativa."}</small></details><div class="clab-stage" aria-live="polite"></div></div><output class="clab-status"></output>${s.explanation?`<p class="gl-explanation">${esc(s.explanation)}</p>`:''}<script type="application/json" class="clab-model">${JSON.stringify({engine:s.engine||'trace',program:s.program||'',call:s.call||'',maxSteps:s.maxSteps||120}).replace(/</g,'\\u003c')}</script></div>`;}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const EXTENSIONS = new Map([
|
|
2
|
+
[".py", "Python"],
|
|
3
|
+
[".pyw", "Python"],
|
|
4
|
+
[".java", "Java"],
|
|
5
|
+
[".js", "JavaScript"],
|
|
6
|
+
[".jsx", "JavaScript"],
|
|
7
|
+
[".mjs", "JavaScript"],
|
|
8
|
+
[".cjs", "JavaScript"],
|
|
9
|
+
[".ts", "TypeScript"],
|
|
10
|
+
[".tsx", "TypeScript"],
|
|
11
|
+
[".mts", "TypeScript"],
|
|
12
|
+
[".cts", "TypeScript"],
|
|
13
|
+
[".cs", "C#"],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const ALIASES = new Map([
|
|
17
|
+
["python", "Python"],
|
|
18
|
+
["py", "Python"],
|
|
19
|
+
["java", "Java"],
|
|
20
|
+
["javascript", "JavaScript"],
|
|
21
|
+
["js", "JavaScript"],
|
|
22
|
+
["node", "JavaScript"],
|
|
23
|
+
["nodejs", "JavaScript"],
|
|
24
|
+
["typescript", "TypeScript"],
|
|
25
|
+
["ts", "TypeScript"],
|
|
26
|
+
["c#", "C#"],
|
|
27
|
+
["csharp", "C#"],
|
|
28
|
+
["cs", "C#"],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
export function inferCodeLanguage(filename) {
|
|
32
|
+
const basename = String(filename || "").trim().split(/[\\/]/).pop() || "";
|
|
33
|
+
const dot = basename.lastIndexOf(".");
|
|
34
|
+
if (dot <= 0) return "";
|
|
35
|
+
const extension = basename.slice(dot).toLowerCase();
|
|
36
|
+
return EXTENSIONS.get(extension) || "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeCodeLanguage(language) {
|
|
40
|
+
const key = String(language || "").trim().toLowerCase().replace(/[\s._-]/g, "");
|
|
41
|
+
return ALIASES.get(key) || "";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveCodeLanguage(language, filename) {
|
|
45
|
+
const requested = String(language || "").trim();
|
|
46
|
+
return requested ? normalizeCodeLanguage(requested) : inferCodeLanguage(filename);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function displayCodeLanguage(language, filename) {
|
|
50
|
+
const requested = String(language || "").trim();
|
|
51
|
+
return normalizeCodeLanguage(requested) || requested || inferCodeLanguage(filename);
|
|
52
|
+
}
|
package/src/csv.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// CSV como o Excel abre: descobre o separador (vírgula, ponto e vírgula, tab ou barra; a linha "sep=;" do Excel
|
|
2
|
+
// manda), respeita aspas (separador, aspas "" e quebra de linha dentro da célula), guarda BOM e fim de linha para
|
|
3
|
+
// gravar de volta igual. Serve ao motor (tabelas e pontos do deck) e ao Studio (planilha do projeto).
|
|
4
|
+
|
|
5
|
+
const CANDIDATES = [",", ";", "\t", "|"];
|
|
6
|
+
|
|
7
|
+
function splitRows(text, delim, limit = Infinity) {
|
|
8
|
+
const rows = [];
|
|
9
|
+
let row = [], cell = "", quoted = false, i = 0, wasQuoted = false;
|
|
10
|
+
const n = text.length;
|
|
11
|
+
while (i < n && rows.length < limit) {
|
|
12
|
+
const c = text[i];
|
|
13
|
+
if (quoted) {
|
|
14
|
+
if (c === '"') { if (text[i + 1] === '"') { cell += '"'; i += 2; continue; } quoted = false; i++; continue; }
|
|
15
|
+
cell += c; i++; continue;
|
|
16
|
+
}
|
|
17
|
+
if (c === '"' && cell.trim() === "" && !wasQuoted) { quoted = true; wasQuoted = true; cell = ""; i++; continue; }
|
|
18
|
+
if (c === delim) { row.push(cell); cell = ""; wasQuoted = false; i++; continue; }
|
|
19
|
+
if (c === "\r" || c === "\n") {
|
|
20
|
+
row.push(cell); rows.push(row); row = []; cell = ""; wasQuoted = false;
|
|
21
|
+
i += c === "\r" && text[i + 1] === "\n" ? 2 : 1; continue;
|
|
22
|
+
}
|
|
23
|
+
cell += c; i++;
|
|
24
|
+
}
|
|
25
|
+
if (rows.length < limit && (cell !== "" || row.length || wasQuoted)) { row.push(cell); rows.push(row); }
|
|
26
|
+
return rows;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function detectDelimiter(text) {
|
|
30
|
+
const t = String(text ?? "").replace(/^/, "");
|
|
31
|
+
const sep = t.match(/^sep=(.)\r?\n/i);
|
|
32
|
+
if (sep) return sep[1];
|
|
33
|
+
let best = { d: ",", score: -1, width: 0 };
|
|
34
|
+
for (const d of CANDIDATES) {
|
|
35
|
+
const rows = splitRows(t, d, 40).filter((r) => r.some((c) => c.trim() !== ""));
|
|
36
|
+
if (!rows.length) continue;
|
|
37
|
+
const counts = new Map();
|
|
38
|
+
for (const r of rows) counts.set(r.length, (counts.get(r.length) || 0) + 1);
|
|
39
|
+
const [width, freq] = [...counts].sort((a, b) => b[1] - a[1] || b[0] - a[0])[0];
|
|
40
|
+
if (width < 2) continue;
|
|
41
|
+
const score = freq / rows.length;
|
|
42
|
+
if (score > best.score + 1e-9 || (Math.abs(score - best.score) < 1e-9 && width > best.width)) best = { d, score, width };
|
|
43
|
+
}
|
|
44
|
+
return best.d;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseCSV(text, delimiter) {
|
|
48
|
+
let t = String(text ?? "");
|
|
49
|
+
const bom = t.startsWith("");
|
|
50
|
+
if (bom) t = t.slice(1);
|
|
51
|
+
const eol = /\r\n/.test(t) ? "\r\n" : "\n";
|
|
52
|
+
const d = delimiter || detectDelimiter(t);
|
|
53
|
+
let sepLine = false;
|
|
54
|
+
const m = t.match(/^sep=(.)\r?\n/i);
|
|
55
|
+
if (m) { sepLine = true; t = t.slice(m[0].length); }
|
|
56
|
+
let rows = splitRows(t, d);
|
|
57
|
+
while (rows.length && rows.at(-1).every((c) => c === "")) rows.pop();
|
|
58
|
+
const width = Math.max(0, ...rows.map((r) => r.length));
|
|
59
|
+
rows = rows.map((r) => (r.length < width ? [...r, ...Array(width - r.length).fill("")] : r));
|
|
60
|
+
return { rows, delimiter: d, eol, bom, sepLine };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function toCSV(rows, { delimiter = ",", eol = "\n", bom = false, sepLine = false } = {}) {
|
|
64
|
+
const needs = (c) => c.includes(delimiter) || c.includes('"') || c.includes("\n") || c.includes("\r") || /^\s|\s$/.test(c);
|
|
65
|
+
const line = (r) => r.map((c) => { const s = String(c ?? ""); return needs(s) ? `"${s.replace(/"/g, '""')}"` : s; }).join(delimiter);
|
|
66
|
+
let out = (sepLine ? `sep=${delimiter}${eol}` : "") + rows.map(line).join(eol) + (rows.length ? eol : "");
|
|
67
|
+
if (bom) out = "" + out;
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const DELIMITER_NAMES = { ",": "vírgula", ";": "ponto e vírgula", "\t": "tab", "|": "barra" };
|
|
72
|
+
|
|
73
|
+
// no Studio (carregado como módulo): o mesmo leitor para o realce e para colar na planilha
|
|
74
|
+
if (typeof window !== "undefined") window.SagaCSV = { detect: detectDelimiter, parse: parseCSV, stringify: toCSV };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import './runtime/decision-lab.js';
|
|
2
|
+
import { esc } from './markup.js';
|
|
3
|
+
export const calculateDecisionLab = globalThis.SagaDecisionLab.calculate;
|
|
4
|
+
export function decisionLabHTML(input = {}) {
|
|
5
|
+
const v = globalThis.SagaDecisionLab.normalize(input), r = calculateDecisionLab(v);
|
|
6
|
+
const fmt = n => n.toLocaleString('pt-BR', { maximumFractionDigits: 1 });
|
|
7
|
+
const controls = [
|
|
8
|
+
['errorRate', 'Erros da automação (%)', 100, .1],
|
|
9
|
+
['reviewRate', 'Casos revisados (%)', 100, 1],
|
|
10
|
+
['catchRate', 'Erros que o revisor corrige (%)', 100, 1],
|
|
11
|
+
['introducedRate', 'Acertos que o revisor estraga (%)', 100, .1],
|
|
12
|
+
];
|
|
13
|
+
return `<div class="decision-lab" data-decision-lab="${esc(JSON.stringify(v))}">
|
|
14
|
+
<div class="dl-controls">${controls.map(([key,label,max,step]) => `<label>${label}<output data-output="${key}">${fmt(v[key])}</output><input aria-label="${label}" data-param="${key}" type="range" min="0" max="${max}" step="${step}" value="${v[key]}"></label>`).join('')}<button type="button" data-reset>Restaurar premissas</button></div>
|
|
15
|
+
<div class="dl-results" aria-live="polite"><div class="dl-compare"><article><span>Automação sozinha</span><strong data-output="automatic">${fmt(r.automatic)}</strong><small>erros esperados</small></article><article><span>Com revisão</span><strong data-output="reviewed">${fmt(r.reviewed)}</strong><small>erros esperados</small></article></div>
|
|
16
|
+
<p class="dl-verdict" data-verdict>${fmt(Math.abs(r.delta))} erros ${r.delta > 0 ? 'a mais' : 'a menos'} com revisão</p>
|
|
17
|
+
<p class="dl-detail"><b data-output="caught">${fmt(r.caught)}</b> corrigidos · <b data-output="introduced">${fmt(r.introduced)}</b> introduzidos<br><b data-output="hours">${fmt(r.hours)}</b> horas de revisão</p></div>
|
|
18
|
+
<div class="dl-assumptions">Simulação hipotética · ${fmt(v.volume)} decisões · ${fmt(v.seconds)} s/revisão · taxas constantes, revisão sem seleção por risco. Não estima danos nem filas.</div>
|
|
19
|
+
</div>`;
|
|
20
|
+
}
|
package/src/deck-file.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// Gravar o .yaml do deck sem estragar o arquivo.
|
|
2
|
+
//
|
|
3
|
+
// O arquivo é da pessoa: pode ter comentários, estilos ({ a: 1 } numa linha, blocos |) e ordem própria. Regravar tudo
|
|
4
|
+
// com YAML.stringify apaga isso e, a cada salvamento, "mexe" em slides que ninguém tocou. Aqui:
|
|
5
|
+
// 1. o arquivo atual é aberto como documento YAML e só os nós que MUDARAM são trocados (campo a campo, item a item);
|
|
6
|
+
// slides iguais, inclusive movidos de lugar, são reaproveitados com comentários e formatação;
|
|
7
|
+
// 2. linhas intocadas voltam byte a byte (a biblioteca só normaliza o espaço antes de um comentário);
|
|
8
|
+
// 3. o texto final é relido e comparado com o deck: se não bater, grava o deck inteiro (correção primeiro);
|
|
9
|
+
// 4. a troca é atômica: escreve num temporário ao lado e renomeia (nunca fica um arquivo pela metade).
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import { isDeepStrictEqual } from "node:util";
|
|
13
|
+
import YAML from "yaml";
|
|
14
|
+
|
|
15
|
+
const same = (a, b) => isDeepStrictEqual(a, b);
|
|
16
|
+
const isObj = (v) => v && typeof v === "object" && !Array.isArray(v);
|
|
17
|
+
const publicOf = (spec) => Object.fromEntries(Object.entries(JSON.parse(JSON.stringify(spec))).filter(([k]) => !k.startsWith("_")));
|
|
18
|
+
|
|
19
|
+
export const fullYaml = (spec) => YAML.stringify(publicOf(spec), { indent: 2 });
|
|
20
|
+
|
|
21
|
+
// troca o valor de um par chave/valor mantendo o nó (e o comentário) quando os dois lados são escalares
|
|
22
|
+
function setValue(doc, map, key, oldVal, newVal) {
|
|
23
|
+
const node = map.get(key, true);
|
|
24
|
+
if (YAML.isScalar(node) && !isObj(newVal) && !Array.isArray(newVal) && !isObj(oldVal) && !Array.isArray(oldVal)) {
|
|
25
|
+
node.value = newVal;
|
|
26
|
+
if (typeof newVal === "string" && newVal.includes("\n")) node.type = "BLOCK_LITERAL";
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (YAML.isMap(node) && isObj(oldVal) && isObj(newVal)) return syncMap(doc, node, oldVal, newVal);
|
|
30
|
+
if (YAML.isSeq(node) && Array.isArray(oldVal) && Array.isArray(newVal)) return syncSeq(doc, node, oldVal, newVal);
|
|
31
|
+
map.set(key, doc.createNode(newVal));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function syncMap(doc, map, oldObj, newObj) {
|
|
35
|
+
for (const k of Object.keys(oldObj)) if (!(k in newObj)) map.delete(k);
|
|
36
|
+
for (const [k, v] of Object.entries(newObj)) {
|
|
37
|
+
if (!(k in oldObj)) map.set(k, doc.createNode(v));
|
|
38
|
+
else if (!same(oldObj[k], v)) setValue(doc, map, k, oldObj[k], v);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Itens de lista: primeiro reaproveita os iguais (mesmo que tenham mudado de lugar), depois edita no lugar os que
|
|
43
|
+
// estão na mesma posição e são do mesmo tipo (mesmo layout), e só então cria nós novos.
|
|
44
|
+
function syncSeq(doc, seq, oldArr, newArr) {
|
|
45
|
+
const nodes = seq.items, used = new Set(), out = new Array(newArr.length);
|
|
46
|
+
newArr.forEach((v, j) => {
|
|
47
|
+
let best = -1;
|
|
48
|
+
oldArr.forEach((o, i) => { if (!used.has(i) && same(o, v) && (best < 0 || Math.abs(i - j) < Math.abs(best - j))) best = i; });
|
|
49
|
+
if (best >= 0) { used.add(best); out[j] = nodes[best]; }
|
|
50
|
+
});
|
|
51
|
+
newArr.forEach((v, j) => {
|
|
52
|
+
if (out[j]) return;
|
|
53
|
+
const o = oldArr[j];
|
|
54
|
+
if (j < oldArr.length && !used.has(j) && isObj(o) && isObj(v) && o.layout === v.layout && YAML.isMap(nodes[j])) {
|
|
55
|
+
used.add(j); syncMap(doc, nodes[j], o, v); out[j] = nodes[j];
|
|
56
|
+
} else if (j < oldArr.length && !used.has(j) && !isObj(o) && !Array.isArray(o) && !isObj(v) && !Array.isArray(v) && YAML.isScalar(nodes[j])) {
|
|
57
|
+
used.add(j); nodes[j].value = v; out[j] = nodes[j];
|
|
58
|
+
} else out[j] = doc.createNode(v);
|
|
59
|
+
});
|
|
60
|
+
seq.items = out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// A biblioteca normaliza espaços (antes de comentário, dentro de [ ] e { }). Linha cujo conteúdo sem espaços é igual
|
|
64
|
+
// ao de uma linha do arquivo original volta como estava. Seguro: se uma troca dessas mudasse o sentido, a releitura
|
|
65
|
+
// em deckText não bateria com o deck e a gravação cairia na escrita completa.
|
|
66
|
+
function keepOriginalSpacing(original, text) {
|
|
67
|
+
const norm = (l) => l.replace(/\s+/g, "");
|
|
68
|
+
const pool = new Map();
|
|
69
|
+
for (const l of original.split("\n")) { const k = norm(l); if (!pool.has(k)) pool.set(k, []); pool.get(k).push(l); }
|
|
70
|
+
return text.split("\n").map((l) => { const q = pool.get(norm(l)); return q && q.length ? q.shift() : l; }).join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function deckText(spec, currentText) {
|
|
74
|
+
const want = publicOf(spec);
|
|
75
|
+
if (currentText != null) {
|
|
76
|
+
try {
|
|
77
|
+
const doc = YAML.parseDocument(currentText);
|
|
78
|
+
if (!doc.errors.length && YAML.isMap(doc.contents)) {
|
|
79
|
+
const have = doc.toJS() || {};
|
|
80
|
+
if (same(have, want)) return currentText; // nada mudou: o arquivo fica intocado
|
|
81
|
+
syncMap(doc, doc.contents, have, want);
|
|
82
|
+
const text = keepOriginalSpacing(currentText, doc.toString({ lineWidth: 0 }));
|
|
83
|
+
if (same(YAML.parse(text), want)) return text;
|
|
84
|
+
}
|
|
85
|
+
} catch { /* cai na escrita completa */ }
|
|
86
|
+
}
|
|
87
|
+
return fullYaml(want);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Portátil: caminho absoluto que aponta para DENTRO da pasta do deck vira relativo (mudar a pasta de lugar ou mandar
|
|
91
|
+
// para outra pessoa não quebra). Fora da pasta fica como está (o .sagadeck empacotado traz esses para dentro).
|
|
92
|
+
const FILE_KEYS = new Set(["image", "foreground", "src", "file", "audio", "video", "css", "widgets", "poster"]);
|
|
93
|
+
const isAbs = (p) => typeof p === "string" && (path.isAbsolute(p) || /^[A-Za-z]:[\\/]/.test(p)) && !/^[a-z]+:\/\//i.test(p);
|
|
94
|
+
export function relativizePaths(spec, deckDir) {
|
|
95
|
+
const dir = path.resolve(deckDir);
|
|
96
|
+
const fix = (p) => {
|
|
97
|
+
if (!isAbs(p)) return p;
|
|
98
|
+
const rel = path.relative(dir, path.resolve(p));
|
|
99
|
+
return rel && !rel.startsWith("..") && !path.isAbsolute(rel) ? rel.split(path.sep).join("/") : p;
|
|
100
|
+
};
|
|
101
|
+
const walk = (v, key) => {
|
|
102
|
+
if (Array.isArray(v)) return v.map((x) => (FILE_KEYS.has(key) && typeof x === "string" ? fix(x) : walk(x, key)));
|
|
103
|
+
if (v && typeof v === "object") return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, FILE_KEYS.has(k) && typeof x === "string" ? fix(x) : walk(x, k)]));
|
|
104
|
+
return v;
|
|
105
|
+
};
|
|
106
|
+
return walk(spec, "");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// grava de forma atômica: temporário na mesma pasta + rename (no Windows, rename substitui o destino)
|
|
110
|
+
export function writeDeckFile(file, spec) {
|
|
111
|
+
let current = null;
|
|
112
|
+
try { current = fs.readFileSync(file, "utf8"); } catch { /* arquivo novo */ }
|
|
113
|
+
const text = deckText(relativizePaths(publicOf(spec), path.dirname(file)), current);
|
|
114
|
+
if (text === current) return false;
|
|
115
|
+
const tmp = path.join(path.dirname(file), `.${path.basename(file)}.${process.pid}.${Date.now()}.tmp`);
|
|
116
|
+
fs.writeFileSync(tmp, text, "utf8");
|
|
117
|
+
try { fs.renameSync(tmp, file); } catch (e) { fs.rmSync(tmp, { force: true }); throw e; }
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Exemplos do "Diagrama de texto" — usados pelo Studio (cartões de exemplo) e pela suíte de testes.
|
|
2
|
+
// Cada exemplo diz qual layout as regras locais (sem IA) devem escolher.
|
|
3
|
+
export const NAPKIN_EXAMPLES = {
|
|
4
|
+
steps: { label: "Processo", layout: "steps", text: "Etapas do atendimento:\n1. Cliente abre chamado pelo app\n2. Triagem automática classifica a urgência\n3. Especialista responde em até 2 h\n4. Cliente avalia o atendimento" },
|
|
5
|
+
funnel: { label: "Funil", layout: "funnel", text: "Funil de vendas do trimestre:\n- Visitas ao site: 120 mil\n- Cadastros: 18 mil\n- Reuniões: 2.400\n- Contratos: 900" },
|
|
6
|
+
stats: { label: "Números", layout: "stats", text: "Resultados do trimestre:\n- Receita: +42%\n- ARR: R$ 8,4 mi\n- Churn mensal: 0,6%\n- NPS: 91" },
|
|
7
|
+
compare: { label: "Antes × depois", layout: "compare", text: "Antes vs depois da automação:\n- Relatório levava 3 dias vs sai em 2 horas\n- 12 planilhas manuais vs um painel único\n- Erros de digitação vs dados validados na origem" },
|
|
8
|
+
timeline: { label: "Linha do tempo", layout: "timeline", text: "Nossa história:\n2019 — fundação com 3 pessoas\n2021 — primeiro cliente grande\n2023 — expansão para o Chile\n2026 — 200 pessoas e 4 países" },
|
|
9
|
+
cards: { label: "Pilares", layout: "cards", text: "3 pilares da estratégia:\n1. Segurança: criptografia de ponta a ponta\n2. Velocidade: resposta em menos de 1 segundo\n3. Simplicidade: nada de manual" },
|
|
10
|
+
pyramid: { label: "Pirâmide", layout: "pyramid", text: "Pirâmide de necessidades do cliente:\n- Topo: encantamento\n- Meio: conveniência\n- Base: confiança e segurança" },
|
|
11
|
+
matrix: { label: "Matriz 2×2", layout: "matrix", text: "Priorização por impacto e esforço:\n- Alto impacto, pouco esforço: fazer já\n- Alto impacto, muito esforço: planejar\n- Baixo impacto, pouco esforço: delegar\n- Baixo impacto, muito esforço: evitar" },
|
|
12
|
+
agenda: { label: "Agenda", layout: "agenda", text: "Agenda do workshop:\n1. Abertura (10 min)\n2. O problema (20 min)\n3. Mão na massa (40 min)\n4. Próximos passos (10 min)" },
|
|
13
|
+
};
|