sagadeck 1.7.1 → 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 +55 -4
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +483 -1
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +184 -105
- package/package.json +34 -2
- package/src/ai/commands.js +2 -2
- package/src/ai/deck-ai.js +14 -5
- 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 +119 -0
- package/src/ai/map-data.js +221 -0
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/build.js +51 -15
- package/src/elements.js +39 -1
- package/src/export/browser.js +23 -1
- package/src/export/pptx.js +9 -3
- package/src/export/shots.js +10 -3
- 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 +114 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +109 -0
- package/src/map-config.js +89 -0
- package/src/map-routing.js +82 -0
- package/src/map-services.js +60 -0
- package/src/map.js +274 -0
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/base.css +105 -329
- 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 +266 -0
- 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/LEAFLET-LICENSE.txt +26 -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 -0
- package/src/runtime/vendor/leaflet.css +661 -0
- package/src/runtime/vendor/leaflet.js +6 -0
- package/src/runtime/vendor/maplibre-gl.css +1 -0
- package/src/runtime/vendor/maplibre-gl.js +825 -0
- package/src/runtime/vendor/maplibre-leaflet.js +234 -0
- package/src/runtime/vendor/maplibre-worker.js +10 -0
- package/src/studio/ai-chat-route.js +20 -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 +6 -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/geo-routes.js +138 -0
- package/src/studio/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +78 -16
- package/src/studio/library-routes.js +0 -9
- package/src/studio/project.js +2 -1
- package/src/studio/public/ai-settings.js +6 -3
- package/src/studio/public/app.js +46 -52
- package/src/studio/public/explorer.js +23 -8
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/index.html +17 -10
- package/src/studio/public/library.css +12 -3
- package/src/studio/public/library.html +3 -0
- package/src/studio/public/library.js +39 -12
- package/src/studio/public/map-data.js +158 -0
- package/src/studio/public/map-editor.js +676 -0
- package/src/studio/public/map-fields.js +21 -0
- package/src/studio/public/map-settings.js +86 -0
- package/src/studio/public/slide-form.js +19 -16
- package/src/studio/public/style.css +58 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/public/viewers.js +2 -1
- package/src/studio/public/visual-editor.js +1 -1
- package/src/studio/server.js +34 -9
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +158 -0
- 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
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Planilha × mapa no Studio (aba Arquivos, planilha aberta): "Pôr no mapa" cria o slide de mapa a partir dela,
|
|
2
|
+
// "Achar coordenadas" preenche latitude e longitude pelo endereço (pergunta antes de mandar os endereços para o
|
|
3
|
+
// serviço de fora; mostra o progresso; o que não achou fica listado), e a seleção anda junto: clicar num ponto do
|
|
4
|
+
// mapa seleciona a linha da planilha aberta, e selecionar a linha abre o cartão do ponto.
|
|
5
|
+
window.SagaMapData = (() => {
|
|
6
|
+
const norm = (s) => String(s ?? "").normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "");
|
|
7
|
+
const LAT = ["lat", "latitude"], LON = ["lon", "lng", "long", "longitude"], COORD = ["coordenadas", "coordenada", "coords", "latlon", "latlong", "posicao", "localizacao"];
|
|
8
|
+
const ADDR = ["endereco", "address", "logradouro", "rua", "enderecocompleto"], NAME = ["nome", "name", "titulo", "local", "estacao", "unidade", "ponto", "descricao"];
|
|
9
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
10
|
+
const find = (head, names) => head.findIndex((h) => names.includes(norm(h)));
|
|
11
|
+
|
|
12
|
+
// o que a planilha tem: posição, endereço, uma coluna boa para cor (2 a 8 categorias) e uma para o nome
|
|
13
|
+
function guess(rows) {
|
|
14
|
+
const head = (rows[0] || []).map((h) => String(h).trim()), body = rows.slice(1).filter((r) => r.some((c) => String(c).trim()));
|
|
15
|
+
const lat = find(head, LAT), lon = find(head, LON), coord = find(head, COORD), addr = find(head, ADDR);
|
|
16
|
+
const pos = new Set([lat, lon, coord, addr]);
|
|
17
|
+
const distinct = (i) => new Set(body.map((r) => String(r[i] ?? "").trim()).filter(Boolean)).size;
|
|
18
|
+
const isNum = (i) => body.length && body.every((r) => !String(r[i] ?? "").trim() || /^[-+]?\d+([.,]\d+)?$/.test(String(r[i]).trim()));
|
|
19
|
+
// o nome primeiro (coluna "nome"… ou a de texto com valores quase todos diferentes); a cor vai para uma coluna
|
|
20
|
+
// de categorias de verdade: valores que se repetem, de 2 a 8
|
|
21
|
+
let label = find(head, NAME);
|
|
22
|
+
if (label < 0) label = head.findIndex((h, i) => !pos.has(i) && !isNum(i) && distinct(i) > Math.min(8, body.length / 2));
|
|
23
|
+
const color = head.findIndex((h, i) => !pos.has(i) && i !== label && !isNum(i) && distinct(i) >= 2 && distinct(i) <= 8 && distinct(i) < body.length);
|
|
24
|
+
const withPos = lat >= 0 && lon >= 0 ? body.filter((r) => String(r[lat] ?? "").trim() && String(r[lon] ?? "").trim()).length : coord >= 0 ? body.filter((r) => String(r[coord] ?? "").trim()).length : 0;
|
|
25
|
+
return { head, lat, lon, coord, addr, color, label, total: body.length, withPos };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function dialog(html) {
|
|
29
|
+
const d = document.createElement("dialog");
|
|
30
|
+
d.className = "md-dialog";
|
|
31
|
+
d.innerHTML = html;
|
|
32
|
+
document.body.append(d);
|
|
33
|
+
d.addEventListener("close", () => d.remove());
|
|
34
|
+
d.showModal();
|
|
35
|
+
return d;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function geocode(t, g, { api, ctx, status, reload }) {
|
|
39
|
+
const call = (extra) => fetch("api/mapa/geocode", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ path: t.path, ...extra }) });
|
|
40
|
+
const plano = await call({ planejar: true });
|
|
41
|
+
if (!plano.ok) { const e = await plano.json().catch(() => ({})); ctx.toast(e.error || `Não deu (HTTP ${plano.status}).`, 6000); return; }
|
|
42
|
+
const p = await plano.json();
|
|
43
|
+
let confirmo = false;
|
|
44
|
+
if (p.precisaConfirmar) {
|
|
45
|
+
const d = dialog(`<h3>Achar as coordenadas pelo endereço</h3>
|
|
46
|
+
<p>Vou enviar <b>${p.enviar} endereço${p.enviar === 1 ? "" : "s"}</b> desta planilha (coluna <b>${esc(p.coluna)}</b>) ao serviço <b>${esc(p.servico)}</b> para achar latitude e longitude. Os endereços saem desta máquina.</p>
|
|
47
|
+
<p>Leva cerca de ${p.segundos <= 1 ? "1 segundo" : p.segundos < 90 ? `${p.segundos} segundos` : `${Math.ceil(p.segundos / 60)} minutos`}, no ritmo que o serviço aceita.${p.cache ? ` ${p.cache} já estavam guardados e não serão enviados de novo.` : ""}</p>
|
|
48
|
+
<div class="md-row"><button type="button" class="btn" data-no>Cancelar</button><button type="button" class="btn btn-primary" data-yes>Enviar e achar</button></div>`);
|
|
49
|
+
const ok = await new Promise((r) => { d.querySelector("[data-yes]").onclick = () => { d.close(); r(true); }; d.querySelector("[data-no]").onclick = () => { d.close(); r(false); }; });
|
|
50
|
+
if (!ok) return;
|
|
51
|
+
confirmo = true;
|
|
52
|
+
}
|
|
53
|
+
const res = await call({ confirmo });
|
|
54
|
+
if (!res.ok) { const e = await res.json().catch(() => ({})); ctx.toast(e.error || `Não deu (HTTP ${res.status}).`, 6000); return; }
|
|
55
|
+
// progresso linha a linha (ndjson)
|
|
56
|
+
const reader = res.body.getReader(), dec = new TextDecoder();
|
|
57
|
+
let buf = "", end = null;
|
|
58
|
+
for (;;) {
|
|
59
|
+
const { value, done } = await reader.read();
|
|
60
|
+
if (done) break;
|
|
61
|
+
buf += dec.decode(value, { stream: true });
|
|
62
|
+
let i;
|
|
63
|
+
while ((i = buf.indexOf("\n")) >= 0) {
|
|
64
|
+
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
65
|
+
if (!line.trim()) continue;
|
|
66
|
+
const ev = JSON.parse(line);
|
|
67
|
+
if (ev.type === "plano") status.textContent = `procurando ${ev.total} endereço${ev.total === 1 ? "" : "s"}…`;
|
|
68
|
+
if (ev.type === "progresso") status.textContent = `${ev.feitos} de ${ev.total} · ${ev.ok} achado${ev.ok === 1 ? "" : "s"}`;
|
|
69
|
+
if (ev.type === "fim") end = ev;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!end) return;
|
|
73
|
+
const msg = end.bloqueado ? `O serviço parou de responder: ${end.bloqueado}. Não tentei de novo; o que já tinha achado foi gravado.` : `${end.ok} achado${end.ok === 1 ? "" : "s"}${end.falhas.length ? `, ${end.falhas.length} sem resultado` : ""}.`;
|
|
74
|
+
if (end.falhas.length || end.bloqueado) {
|
|
75
|
+
const d = dialog(`<h3>Coordenadas pelo endereço</h3><p>${esc(msg)}</p>${end.falhas.length ? `<p>Sem resultado (corrija o endereço e tente de novo):</p><ul class="md-list">${end.falhas.slice(0, 50).map((f) => `<li>linha ${f.linha}: ${esc(f.endereco)}</li>`).join("")}</ul>` : ""}<div class="md-row"><button type="button" class="btn btn-primary" data-ok>Fechar</button></div>`);
|
|
76
|
+
d.querySelector("[data-ok]").onclick = () => d.close();
|
|
77
|
+
} else ctx.toast(msg, 5000);
|
|
78
|
+
if (end.path !== t.path) ctx.toast(`Gravei as coordenadas em ${end.path} (o xlsx não se edita aqui).`, 6000);
|
|
79
|
+
reload(end.path);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// botões na cabeça da planilha aberta (explorer.js chama ao mostrar uma planilha)
|
|
83
|
+
function sheetTools(view, { t, rows, grid, api, ctx, reload }) {
|
|
84
|
+
const g = guess(rows);
|
|
85
|
+
current = { path: t.path, grid, g };
|
|
86
|
+
if (g.lat < 0 && g.coord < 0 && g.addr < 0) return;
|
|
87
|
+
const head = view.querySelector(".dv-head");
|
|
88
|
+
const bar = document.createElement("span");
|
|
89
|
+
bar.className = "md-tools";
|
|
90
|
+
const canMap = (g.lat >= 0 && g.lon >= 0) || g.coord >= 0;
|
|
91
|
+
bar.innerHTML = `${canMap ? `<button type="button" class="btn btn-small" data-to-map title="Cria um slide de mapa com os pontos desta planilha"><i class="ic" data-ic="map-pin"></i> Pôr no mapa</button>` : ""}${g.addr >= 0 && g.withPos < g.total ? `<button type="button" class="btn btn-small" data-geocode title="Preenche latitude e longitude pelo endereço"><i class="ic" data-ic="search"></i> Achar coordenadas</button>` : ""}<span class="md-status f-label"></span>`;
|
|
92
|
+
head.append(bar);
|
|
93
|
+
ctx.hydrate?.(bar);
|
|
94
|
+
const status = bar.querySelector(".md-status");
|
|
95
|
+
bar.querySelector("[data-to-map]")?.addEventListener("click", async () => {
|
|
96
|
+
const name = t.name.replace(/\.[a-z0-9]+$/i, "");
|
|
97
|
+
// o mapa lê CSV: uma planilha .xlsx ganha um CSV ao lado (o original fica como está)
|
|
98
|
+
let points = t.path;
|
|
99
|
+
if (!/\.(csv|tsv)$/i.test(t.path)) {
|
|
100
|
+
points = t.path.replace(/\.[^.]+$/, "") + ".csv";
|
|
101
|
+
try { await api("api/project/sheet-write", { path: points, rows }); } catch (e) { ctx.toast(`Não consegui gravar ${points}: ${e.message}`, 6000); return; }
|
|
102
|
+
}
|
|
103
|
+
const layer = { name, points, ...(g.color >= 0 ? { color: g.head[g.color] } : {}), ...(g.label >= 0 ? { label: g.head[g.label] } : {}) };
|
|
104
|
+
ctx.insertSlide({ layout: "map", title: name, layers: [layer] });
|
|
105
|
+
ctx.toast(points === t.path ? "Mapa inserido depois do slide atual, ligado à planilha (mudou a planilha, o mapa acompanha)." : `Mapa inserido, ligado a ${points} (o mapa lê CSV; gravei a cópia ao lado da planilha).`, 6000);
|
|
106
|
+
});
|
|
107
|
+
const geoBtn = bar.querySelector("[data-geocode]");
|
|
108
|
+
geoBtn?.addEventListener("click", async () => {
|
|
109
|
+
geoBtn.disabled = true;
|
|
110
|
+
try { await geocode(t, g, { api, ctx, status, reload }); } finally { geoBtn.disabled = false; }
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// seleção ligada: ponto clicado no mapa -> linha da planilha aberta (se for a mesma planilha)
|
|
115
|
+
let current = null;
|
|
116
|
+
document.addEventListener("sagamap:ponto", (e) => {
|
|
117
|
+
const { path, linha } = e.detail || {};
|
|
118
|
+
if (!current || !linha || path !== current.path) return;
|
|
119
|
+
current.grid?.select(linha - 1, 0);
|
|
120
|
+
});
|
|
121
|
+
// linha selecionada na planilha -> cartão do ponto no mapa da prévia
|
|
122
|
+
const selectRow = (path, linha) => document.dispatchEvent(new CustomEvent("sagamap:linha", { detail: { path, linha } }));
|
|
123
|
+
|
|
124
|
+
// botão "Editar mapa" na prévia grande; a tela do editor (map-editor.js) só carrega no primeiro clique
|
|
125
|
+
let editorLoad;
|
|
126
|
+
const loadEditor = () => (window.SagaMapEditor ? Promise.resolve() : (editorLoad ||= new Promise((res, rej) => {
|
|
127
|
+
const s = document.createElement("script"); s.src = "map-editor.js"; s.onload = res;
|
|
128
|
+
s.onerror = () => { editorLoad = null; s.remove(); rej(new Error("Não foi possível carregar o editor do mapa.")); };
|
|
129
|
+
document.head.append(s);
|
|
130
|
+
})));
|
|
131
|
+
function attach(container, slide, commit, ctx) {
|
|
132
|
+
const E = window.SagaMapEditor;
|
|
133
|
+
if (slide?.layout !== "map") { if (E?.active()) E.stop(); document.querySelectorAll("#canvas-viewport > .me-open").forEach((b) => b.remove()); return; }
|
|
134
|
+
const box = container.querySelector(".map-box");
|
|
135
|
+
if (!box) return;
|
|
136
|
+
// o botão e a barra ficam fora do slide escalado (a prévia do Studio fica a ~40%): no #canvas-viewport
|
|
137
|
+
const host = document.getElementById("canvas-viewport") || box;
|
|
138
|
+
host.querySelectorAll(":scope > .me-open").forEach((b) => b.remove());
|
|
139
|
+
const btn = document.createElement("button");
|
|
140
|
+
btn.type = "button"; btn.className = "me-open"; btn.innerHTML = '<i class="ic" data-ic="pencil"></i> Editar mapa';
|
|
141
|
+
btn.title = "Abre a tela do mapa: camadas, pontos, linhas, áreas e as propriedades de cada um (ou dê dois cliques no mapa)";
|
|
142
|
+
for (const ev of ["pointerdown", "mousedown", "click", "dblclick"]) btn.addEventListener(ev, (e) => e.stopPropagation());
|
|
143
|
+
const openEditor = async () => {
|
|
144
|
+
try { await loadEditor(); btn.hidden = true; window.SagaMapEditor.start(box, slide, commit, ctx); }
|
|
145
|
+
catch (e) { ctx.toast(e.message, 5000); }
|
|
146
|
+
};
|
|
147
|
+
btn.onclick = openEditor;
|
|
148
|
+
// dois cliques no mapa da prévia também abrem a tela (o mapa pequeno, escalado, não serve para editar)
|
|
149
|
+
box.addEventListener("dblclick", (e) => { e.stopPropagation(); e.preventDefault(); openEditor(); });
|
|
150
|
+
host.append(btn);
|
|
151
|
+
ctx.hydrate?.(btn);
|
|
152
|
+
// o slide redesenhou com o editor aberto (gravou algo): volta no mesmo modo
|
|
153
|
+
if (E?.active()) { if (E.key() === (slide.uid || slide.title)) { btn.hidden = true; E.resume(box, slide, commit, ctx); } else E.stop(); }
|
|
154
|
+
}
|
|
155
|
+
const editorStopped = () => { const b = document.querySelector("#canvas-viewport > .me-open"); if (b) b.hidden = false; };
|
|
156
|
+
|
|
157
|
+
return { guess, sheetTools, selectRow, attach, editorStopped };
|
|
158
|
+
})();
|