sagadeck 1.8.0 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -8
- package/SKILL.md +10 -3
- package/bin/sagadeck.js +23 -0
- package/docs/ANDAMENTO.md +482 -7
- package/docs/PLANO-MELHORIAS.md +180 -0
- package/docs/REFERENCIA.md +178 -111
- package/package.json +35 -4
- package/src/ai/commands.js +2 -2
- package/src/ai/context.js +7 -2
- package/src/ai/deck-ai.js +4 -2
- package/src/ai/ia-check.js +127 -0
- package/src/ai/ia-config.js +2 -2
- package/src/ai/llm.js +5 -1
- package/src/ai/map-analysis-run.js +1 -1
- package/src/ai/quality.js +1 -1
- package/src/ai/video-generation.js +70 -25
- package/src/api-client.js +42 -10
- package/src/build.js +46 -14
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/elements.js +39 -1
- package/src/export/pptx.js +7 -2
- package/src/export/shots.js +7 -2
- package/src/fiscal/normalize.js +19 -0
- package/src/fs-retry.js +37 -2
- package/src/import/office.js +5 -4
- package/src/layouts.js +104 -99
- package/src/lessons.js +1 -1
- package/src/library.js +7 -7
- package/src/map-analysis.js +4 -1
- package/src/map-config.js +10 -4
- package/src/map.js +21 -8
- package/src/mcp/server.js +17 -0
- package/src/motion.js +20 -6
- package/src/purpose.js +1 -1
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/base.css +88 -332
- package/src/runtime/fit.js +3 -2
- package/src/runtime/fonts/amatic-sc.css +3 -0
- package/src/runtime/fonts/bebas-neue.css +2 -0
- package/src/runtime/fonts/cabin-sketch.css +2 -0
- package/src/runtime/fonts/fraunces.css +3 -0
- package/src/runtime/fonts/gelasio.css +3 -0
- package/src/runtime/fonts/ibm-plex-mono.css +4 -0
- package/src/runtime/fonts/index.json +10 -1
- package/src/runtime/fonts/itim.css +2 -0
- package/src/runtime/fonts/montserrat.css +2 -0
- package/src/runtime/fonts/space-grotesk.css +2 -0
- package/src/runtime/map-background.js +91 -0
- package/src/runtime/map.js +41 -9
- package/src/runtime/runtime.js +81 -100
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/caderno.css +33 -0
- package/src/runtime/skins/cartaz.css +27 -0
- package/src/runtime/skins/infografico.css +29 -0
- package/src/runtime/skins/lousa.css +22 -0
- package/src/runtime/skins/musgo.css +42 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
- package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
- package/src/runtime/vendor/geo-contornos.json +1 -1
- package/src/runtime/vendor/maplibre-gl.css +1 -0
- package/src/runtime/vendor/maplibre-gl.js +825 -0
- package/src/runtime/vendor/maplibre-leaflet.js +234 -0
- package/src/runtime/vendor/maplibre-worker.js +10 -0
- package/src/studio/ai-chat-route.js +26 -7
- package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
- package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
- package/src/studio/command-approval.js +2 -3
- package/src/studio/demo-decks.js +4 -2
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/export-jobs.js +86 -0
- package/src/studio/exporting.js +36 -18
- package/src/studio/feature-demos.js +144 -0
- package/src/studio/ia-routes.js +9 -0
- package/src/studio/layout-samples.js +9 -18
- package/src/studio/library-routes.js +7 -9
- package/src/studio/project.js +1 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/ai-settings.js +2 -2
- package/src/studio/public/app.js +43 -52
- package/src/studio/public/chat.js +5 -1
- package/src/studio/public/developer-authoring.css +12 -0
- package/src/studio/public/developer-authoring.js +306 -0
- package/src/studio/public/export-status.js +154 -0
- package/src/studio/public/generation-guide.js +42 -0
- package/src/studio/public/index.html +31 -10
- package/src/studio/public/library.css +48 -3
- package/src/studio/public/library.html +5 -1
- package/src/studio/public/library.js +150 -41
- package/src/studio/public/map-data.js +7 -3
- package/src/studio/public/map-editor.js +625 -236
- package/src/studio/public/map-settings.js +3 -1
- package/src/studio/public/slide-form.js +27 -29
- package/src/studio/public/style.css +56 -9
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +29 -14
- package/src/studio/snapshot.js +8 -4
- package/src/studio/template-collections.js +1 -1
- package/src/studio/tour.js +89 -7
- package/src/themes.js +202 -0
- package/templates/cenario-e-ciencia.yaml +1 -21
- package/templates/ensaio-api.yaml +3 -3
- package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
- package/templates/cenario/imagens/cidade.svg +0 -30
- package/templates/cenario/imagens/pessoa.svg +0 -15
|
@@ -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" }] }] },
|
|
@@ -9,6 +9,7 @@ import { writeDeckFile } from "../deck-file.js";
|
|
|
9
9
|
import { createExperienceDeck } from "../experiences.js";
|
|
10
10
|
import { demoDeck } from "./demo-decks.js";
|
|
11
11
|
import { DEMO_FILES } from "../api-demo.js";
|
|
12
|
+
import { developerStarter } from "./developer-starters.js";
|
|
12
13
|
|
|
13
14
|
export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON, readBody, slideSnapshots, sendExport, galleryKey, galleryEntry, materializePreview, copyModelAssets, generateIntoLibrary, llmAvailable, llmConfig, respond, templateFile, TEMPLATE_DIRS }) {
|
|
14
15
|
const L = W.library;
|
|
@@ -74,6 +75,12 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
74
75
|
case "/api/library/topics/update": return ok({ id: L.updateTopic(b.id, b) });
|
|
75
76
|
case "/api/library/topics/delete": L.deleteTopic(b.id); return ok();
|
|
76
77
|
case "/api/library/decks": {
|
|
78
|
+
if (b.starter) {
|
|
79
|
+
const spec = developerStarter(b.starter, { title: b.title });
|
|
80
|
+
if (b.theme) spec.theme = b.theme;
|
|
81
|
+
buildHTML(spec);
|
|
82
|
+
return ok({ id: L.createDeck(b.topic || "", spec) });
|
|
83
|
+
}
|
|
77
84
|
// nova: em branco (uma capa) — "com IA" usa /api/library/decks/ai
|
|
78
85
|
const title = String(b.title || "Nova apresentação").trim();
|
|
79
86
|
const id = L.createDeck(b.topic || "", { title, theme: b.theme || "bauhaus", duration: 10,
|
|
@@ -103,15 +110,6 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
103
110
|
if (!made) throw new Error("Não há um modelo em prévia aberto.");
|
|
104
111
|
return ok(made);
|
|
105
112
|
}
|
|
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
113
|
case "/api/library/decks/example": {
|
|
116
114
|
// exemplo de slides api (aula de APIs de IA): roda no ambiente embutido "ensaio", sem configurar nada
|
|
117
115
|
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));
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// "Sobre o SagaDeck" (Ajuda › Sobre, no editor; botão Sobre, na biblioteca): a versão que está rodando e o essencial
|
|
2
|
+
// para dar suporte (commit, Node, desde quando, a biblioteca, se a IA está configurada), com Copiar informações.
|
|
3
|
+
window.SagaAbout = (() => {
|
|
4
|
+
// as cores vêm das variáveis que o editor e a biblioteca definem nos dois temas (claro e escuro); fundo e texto
|
|
5
|
+
// sempre do mesmo par, senão o tema escuro dá texto claro sobre fundo branco
|
|
6
|
+
const css = `.about-dlg{margin:auto;max-width:min(520px,calc(100vw - 32px));width:100%;border:1px solid var(--ui-line,#c9ced6);border-radius:12px;padding:20px 22px;background:var(--chrome,#fff);color:var(--text,#1d2433);font:14px/1.45 system-ui,sans-serif;box-shadow:0 20px 60px #0004}
|
|
7
|
+
.about-dlg::backdrop{background:rgba(0,0,0,.45)}.about-dlg h2{margin:0;font-size:20px;color:var(--text,#1d2433)}.about-dlg .ver{margin:2px 0 14px;font-size:15px;color:var(--text-2,#555)}
|
|
8
|
+
.about-dlg .ver b{color:var(--text,#1d2433);font-size:22px}.about-dlg dl{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0 0 14px}
|
|
9
|
+
.about-dlg dt{color:var(--text-2,#555)}.about-dlg dd{margin:0;overflow-wrap:anywhere;color:var(--text,#1d2433)}.about-dlg code{font:12px ui-monospace,Consolas,monospace;color:inherit}
|
|
10
|
+
.about-dlg .links{display:flex;gap:14px;flex-wrap:wrap;margin:0 0 6px}.about-dlg .links a{color:var(--brand,#5b4bdb)}
|
|
11
|
+
.about-dlg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}.about-dlg button{padding:7px 14px;border-radius:8px;border:1px solid var(--ui-line,#c9ced6);background:transparent;color:var(--text,#1d2433);cursor:pointer;font:inherit}
|
|
12
|
+
.about-dlg button.primary{background:var(--brand-fill,#5b4bdb);border-color:var(--brand-fill,#5b4bdb);color:#fff}.about-dlg [data-copied]{color:var(--text-2,#555);align-self:center;margin-right:auto}`;
|
|
13
|
+
const REPO = "https://github.com/naruminho/sagadeck";
|
|
14
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
15
|
+
let dialog;
|
|
16
|
+
|
|
17
|
+
function build() {
|
|
18
|
+
if (!document.getElementById("about-style")) { const st = document.createElement("style"); st.id = "about-style"; st.textContent = css; document.head.append(st); }
|
|
19
|
+
dialog = document.createElement("dialog");
|
|
20
|
+
dialog.className = "about-dlg";
|
|
21
|
+
dialog.id = "about-dialog";
|
|
22
|
+
dialog.setAttribute("aria-labelledby", "about-title");
|
|
23
|
+
document.body.append(dialog);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// o que o servidor diz de si (versão, commit, Node, desde quando) e se a IA está configurada
|
|
27
|
+
async function facts() {
|
|
28
|
+
const get = async (u) => { try { const r = await fetch(u, { cache: "no-store" }); return r.ok ? r.json() : {}; } catch { return {}; } };
|
|
29
|
+
const [inst, ia] = await Promise.all([get("api/instance"), get("api/ia")]);
|
|
30
|
+
return { ...inst, ia: ia.configured == null ? null : !!ia.configured };
|
|
31
|
+
}
|
|
32
|
+
const text = (f) => [`SagaDeck ${f.version || "?"} (commit ${f.commit || "?"})`, `Node ${f.node || "?"} · ${f.platform || "?"}`,
|
|
33
|
+
f.started ? `No ar desde ${new Date(f.started).toLocaleString("pt-BR")} (processo ${f.pid || "?"})` : "",
|
|
34
|
+
f.multiuser ? "Servidor multiusuário" : f.library ? `Biblioteca: ${f.library}` : "",
|
|
35
|
+
f.ia == null ? "" : `IA: ${f.ia ? "configurada" : "não configurada"}`].filter(Boolean).join("\n");
|
|
36
|
+
|
|
37
|
+
async function open() {
|
|
38
|
+
if (!dialog) build();
|
|
39
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2><p class="ver">Carregando…</p><div class="row"><button type="button" data-close>Fechar</button></div>`;
|
|
40
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
41
|
+
if (!dialog.open) dialog.showModal();
|
|
42
|
+
const f = await facts();
|
|
43
|
+
const row = (k, v) => (v ? `<dt>${k}</dt><dd>${v}</dd>` : "");
|
|
44
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2>
|
|
45
|
+
<p class="ver">Versão <b data-version>${esc(f.version || "?")}</b></p>
|
|
46
|
+
<dl>
|
|
47
|
+
${row("Commit", f.commit ? `<code>${esc(f.commit)}</code>` : "")}
|
|
48
|
+
${row("Node", f.node ? `${esc(f.node)} · ${esc(f.platform || "")}` : "")}
|
|
49
|
+
${row("No ar desde", f.started ? esc(new Date(f.started).toLocaleString("pt-BR")) : "")}
|
|
50
|
+
${row("Biblioteca", !f.multiuser && f.library ? `<code>${esc(f.library)}</code>` : f.multiuser ? "neste servidor (multiusuário)" : "")}
|
|
51
|
+
${row("IA", f.ia == null ? "" : f.ia ? "configurada (para testar cada recurso: <code>sagadeck ia-check</code>)" : "não configurada")}
|
|
52
|
+
</dl>
|
|
53
|
+
<div class="links">
|
|
54
|
+
<a href="${REPO}/blob/main/CHANGELOG.md" target="_blank" rel="noopener">O que mudou em cada versão</a>
|
|
55
|
+
<a href="${REPO}" target="_blank" rel="noopener">Código e problemas</a>
|
|
56
|
+
<a href="https://www.npmjs.com/package/sagadeck" target="_blank" rel="noopener">Pacote no npm</a>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="row"><span data-copied hidden>Copiado.</span><button type="button" data-copy>Copiar informações</button><button type="button" class="primary" data-close>Fechar</button></div>`;
|
|
59
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
60
|
+
dialog.querySelector("[data-copy]").onclick = async () => {
|
|
61
|
+
try { await navigator.clipboard.writeText(text(f)); dialog.querySelector("[data-copied]").hidden = false; }
|
|
62
|
+
catch { dialog.querySelector("[data-copied]").textContent = "Não deu para copiar."; dialog.querySelector("[data-copied]").hidden = false; }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// qualquer botão ou link com data-about abre (Ajuda › Sobre no editor, o botão da biblioteca)
|
|
66
|
+
document.addEventListener("click", (e) => { const b = e.target.closest?.("[data-about]"); if (b) { e.preventDefault(); open(); } });
|
|
67
|
+
return { open };
|
|
68
|
+
})();
|
|
@@ -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
|
|
package/src/studio/public/app.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
const LAYOUT_NAMES = [
|
|
36
36
|
"cover", "section", "statement", "definition", "poster", "calendar", "headline", "quote", "number", "split", "full",
|
|
37
37
|
"cards", "bento", "mosaic", "ribbon", "stats", "steps", "funnel", "pyramid", "list", "agenda", "timeline", "hub", "status", "onepage",
|
|
38
|
-
"chart", "table", "map", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "
|
|
38
|
+
"chart", "table", "map", "compare", "matrix", "decisionlab", "diagram", "infographic", "question", "poll", "image", "code", "codewalk", "spotlight", "portal", "science", "video", "carousel", "duel", "terminals", "turns", "solution", "calc", "algo", "graphlab", "codelab", "playground",
|
|
39
39
|
"blocks", "dossier", "canvas", "references", "end",
|
|
40
40
|
];
|
|
41
41
|
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
split: "Texto e figura", cards: "Cartões", stats: "Indicadores", steps: "Etapas", list: "Lista",
|
|
46
46
|
timeline: "Linha do tempo", chart: "Gráfico de dados", table: "Tabela", map: "Mapa", compare: "Comparação", matrix: "Matriz 2×2",
|
|
47
47
|
question: "Pergunta", poll: "Enquete", image: "Imagem", code: "Código", video: "Vídeo",
|
|
48
|
-
diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções",
|
|
48
|
+
diagram: "Diagrama", infographic: "Infográfico", mosaic: "Grade adaptável", ribbon: "Cápsulas", dossier: "Página de consulta", decisionlab: "Laboratório de decisões", science: "Fórmulas e funções", codewalk: "Código guiado", spotlight: "Foco guiado",
|
|
49
49
|
duel: "Duelo de commits", terminals: "Dois terminais", turns: "Turnos a dois", carousel: "Carrossel", solution: "Exercício resolvido", calc: "Calculadora ao vivo", algo: "Algoritmo animado",
|
|
50
50
|
graphlab: "Rede explorável", codelab: "Código explorável", playground: "Experiência personalizada", portal: "Portal e transformação",
|
|
51
51
|
blocks: "Livre (blocos)", canvas: "Livre (posições)", end: "Encerramento", references: "Referências",
|
|
@@ -429,7 +429,7 @@
|
|
|
429
429
|
dom.renderedSlideContainer.innerHTML = data.html;
|
|
430
430
|
window.SagaScience?.mount(dom.renderedSlideContainer);
|
|
431
431
|
window.SagaMap?.mount(dom.renderedSlideContainer); // só aqui (prévia grande); miniaturas e galeria ficam na prévia em SVG
|
|
432
|
-
window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons });
|
|
432
|
+
window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons, materialized: previewMaterialized });
|
|
433
433
|
window.SagaCalc?.mount(dom.renderedSlideContainer); // calculadora ao vivo: os controles funcionam no editor também
|
|
434
434
|
window.SagaDiagrams?.mount(dom.renderedSlideContainer).then(() => reportDiagram(dom.renderedSlideContainer));
|
|
435
435
|
window.SagaDecisionLab?.mount(dom.renderedSlideContainer);
|
|
@@ -2053,7 +2053,7 @@
|
|
|
2053
2053
|
["processo", "Processos e diagramas", ["steps", "timeline", "funnel", "pyramid", "diagram", "infographic", "hub"]],
|
|
2054
2054
|
["gestao", "Gestão e status", ["status", "onepage", "decisionlab"]],
|
|
2055
2055
|
["codigo", "Código e API", ["code", "codewalk", "api"]],
|
|
2056
|
-
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "
|
|
2056
|
+
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "portal"]],
|
|
2057
2057
|
["plateia", "Plateia", ["question", "poll"]],
|
|
2058
2058
|
["dinamicas", "Dinâmicas a dois", ["duel", "terminals", "turns"]],
|
|
2059
2059
|
["aula", "Aula: exercícios e algoritmos", ["solution", "calc", "algo", "graphlab", "codelab", "playground"]],
|
|
@@ -2156,6 +2156,7 @@
|
|
|
2156
2156
|
showToast("Screenshot pronto. Use Apresentar para percorrer os destaques.");
|
|
2157
2157
|
}
|
|
2158
2158
|
function setupCreativeTools() {
|
|
2159
|
+
window.SagaDeveloperAuthoring.bind({ state, syncDeckToServer, renderThumbnails, renderCurrentSlide, openPane, showToast });
|
|
2159
2160
|
const shotButton = document.createElement("button");
|
|
2160
2161
|
shotButton.id = "btn-screenshot";
|
|
2161
2162
|
shotButton.className = "rbtn rbtn-lg";
|
|
@@ -2329,38 +2330,13 @@
|
|
|
2329
2330
|
|
|
2330
2331
|
// Ao trocar de layout, o conteúdo em lista (cartões, etapas, eventos, níveis…) vai junto
|
|
2331
2332
|
const LIST_KEY = { cards: "items", bento: "tiles", stats: "stats", steps: "steps", funnel: "stages", pyramid: "levels", list: "items",
|
|
2332
|
-
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts"
|
|
2333
|
+
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts" };
|
|
2333
2334
|
const SOURCE_KEYS = ["items", "tiles", "stats", "kpis", "steps", "process", "flow", "stages", "levels", "events", "cells", "options", "bullets", "lines", "contacts"];
|
|
2334
|
-
function kineticPhrases(value) {
|
|
2335
|
-
const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
|
|
2336
|
-
const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
|
|
2337
|
-
const phrases = [];
|
|
2338
|
-
while (words.length) {
|
|
2339
|
-
let take = Math.ceil(words.length / Math.ceil(words.length / 4));
|
|
2340
|
-
const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
|
|
2341
|
-
if (take > 1 && connectors.has(last)) take--;
|
|
2342
|
-
phrases.push(words.splice(0, take).join(" "));
|
|
2343
|
-
}
|
|
2344
|
-
return phrases;
|
|
2345
|
-
}
|
|
2346
2335
|
function carryContent(slide, to) {
|
|
2347
2336
|
const target = LIST_KEY[to];
|
|
2348
2337
|
if (!target) return;
|
|
2349
2338
|
const activeKey = LIST_KEY[slide.layout];
|
|
2350
2339
|
if (activeKey === target && Array.isArray(slide[target]) && slide[target].length) return;
|
|
2351
|
-
if (to === "kinetic") {
|
|
2352
|
-
const source = SOURCE_KEYS.find((key) => Array.isArray(slide[key]) && slide[key].length);
|
|
2353
|
-
if (source) {
|
|
2354
|
-
slide.beats = slide[source].map((item) => {
|
|
2355
|
-
if (typeof item !== "object" || item == null) return { text: String(item ?? "") };
|
|
2356
|
-
return { text: item.title ?? item.label ?? item.text ?? item.name ?? "", ...(item.text && item.title ? { tag: item.text } : {}) };
|
|
2357
|
-
});
|
|
2358
|
-
} else {
|
|
2359
|
-
const phrases = [...kineticPhrases(slide.title || slide.text), ...kineticPhrases(slide.subtitle)];
|
|
2360
|
-
if (phrases.length) slide.beats = phrases.map((text, i) => ({ text, ...(i === 0 && slide.kicker ? { tag: slide.kicker } : {}) }));
|
|
2361
|
-
}
|
|
2362
|
-
return;
|
|
2363
|
-
}
|
|
2364
2340
|
const srcKey = [activeKey, ...SOURCE_KEYS].find((k) => k && Array.isArray(slide[k]) && slide[k].length);
|
|
2365
2341
|
if (!srcKey) return;
|
|
2366
2342
|
const entries = slide[srcKey].map((x) => {
|
|
@@ -2391,7 +2367,7 @@
|
|
|
2391
2367
|
}
|
|
2392
2368
|
if (layoutName === "quote") slide.quote = primary;
|
|
2393
2369
|
if (["question", "poll"].includes(layoutName)) slide.question = primary;
|
|
2394
|
-
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length &&
|
|
2370
|
+
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length && layoutName !== "statement") {
|
|
2395
2371
|
const content = [slide.subtitle, slide.body, primary].find(v => typeof v === "string" && v.trim());
|
|
2396
2372
|
if (content) slide[LIST_KEY[layoutName]] = content.split(/\n+/).filter(Boolean).map(text => ["list", "split", "question", "poll", "references", "end"].includes(layoutName) ? text : {title:text});
|
|
2397
2373
|
}
|
|
@@ -2452,6 +2428,23 @@
|
|
|
2452
2428
|
} catch (e) { showToast(`Não deu para atualizar: ${e.message}`); }
|
|
2453
2429
|
},
|
|
2454
2430
|
hydrate: hydrateIcons,
|
|
2431
|
+
splitCalendar: async () => {
|
|
2432
|
+
const at = state.currentSlideIndex, slide = state.deck.slides[at];
|
|
2433
|
+
const { calendarMonth, calendarMonths } = window.SagaCalendarMonths;
|
|
2434
|
+
const MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
2435
|
+
const months = calendarMonths(slide);
|
|
2436
|
+
if (months.length < 2) return;
|
|
2437
|
+
const { uid, events, undated, month, week, ...base } = slide;
|
|
2438
|
+
const slides = months.map((m, k) => ({ ...base, ...(k === 0 && uid ? { uid } : {}), view: "mes", month: m,
|
|
2439
|
+
kicker: base.kicker || `${MESES[+m.slice(5) - 1]} ${m.slice(0, 4)}`,
|
|
2440
|
+
events: events.filter((e) => calendarMonth(e?.date) === m),
|
|
2441
|
+
...(k === 0 && (undated?.length || events.some((e) => !calendarMonth(e?.date))) ? { undated: [...(undated || []), ...events.filter((e) => !calendarMonth(e?.date)).map(({ title, text }) => ({ title, ...(text ? { text } : {}) }))] } : {}) }));
|
|
2442
|
+
state.deck.slides.splice(at, 1, ...slides);
|
|
2443
|
+
await syncDeckToServer();
|
|
2444
|
+
renderThumbnails();
|
|
2445
|
+
await renderCurrentSlide();
|
|
2446
|
+
showToast(`Um slide por mês: ${slides.length} slides, na visão de mês.`, 4000);
|
|
2447
|
+
},
|
|
2455
2448
|
pickIcon: (cb) => openIconPicker(null, cb),
|
|
2456
2449
|
layoutLabel,
|
|
2457
2450
|
generateImage: (element, btn) => {
|
|
@@ -2609,7 +2602,9 @@
|
|
|
2609
2602
|
}
|
|
2610
2603
|
|
|
2611
2604
|
async function startPresentation({ pen = false } = {}) {
|
|
2612
|
-
|
|
2605
|
+
// a apresentação é montada a partir do deck do servidor. Modelo em prévia: o servidor já tem o mesmo deck (qualquer
|
|
2606
|
+
// mudança teria criado a cópia e saído da prévia), e enviar de novo contaria como mudança: apresentar não grava nada
|
|
2607
|
+
if (!state.preview) await syncDeckToServer();
|
|
2613
2608
|
const start = state.currentSlideIndex + 1;
|
|
2614
2609
|
dom.presModal.classList.remove("hidden");
|
|
2615
2610
|
dom.presFrame.src = scopedURL(`preview?t=${Date.now()}#${start}`);
|
|
@@ -2848,7 +2843,7 @@
|
|
|
2848
2843
|
// outra aba/aparelho abriu outra apresentação neste Studio: nada desta foi gravado (o servidor recusou)
|
|
2849
2844
|
function staleTab(msg) { updateSaveStatus("error"); showToast(msg, 20000, { label: "Recarregar", fn: () => location.reload() }); }
|
|
2850
2845
|
|
|
2851
|
-
async function syncDeckToServer() {
|
|
2846
|
+
async function syncDeckToServer({ throwOnError = false } = {}) {
|
|
2852
2847
|
trackDeck("Edição");
|
|
2853
2848
|
updateSaveStatus("saving");
|
|
2854
2849
|
try {
|
|
@@ -2858,7 +2853,7 @@
|
|
|
2858
2853
|
body: JSON.stringify({ spec: state.deck, expectFile: state.file }),
|
|
2859
2854
|
});
|
|
2860
2855
|
const data = await res.json().catch(() => ({}));
|
|
2861
|
-
if (data.stale)
|
|
2856
|
+
if (data.stale) { staleTab(data.error); if (throwOnError) throw new Error(data.error); return; }
|
|
2862
2857
|
if ("file" in data) state.file = data.file;
|
|
2863
2858
|
if (data.materialized) previewMaterialized(data.materialized);
|
|
2864
2859
|
// o servidor dá identidade a slide novo (uid) e leva o ajuste visual junto quando um texto muda: o Studio adota
|
|
@@ -2872,9 +2867,11 @@
|
|
|
2872
2867
|
if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
|
|
2873
2868
|
updateSaveStatus(res.ok ? "saved" : "error");
|
|
2874
2869
|
if (res.ok) window.SagaProject?.deckChanged();
|
|
2870
|
+
else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
|
|
2875
2871
|
} catch (err) {
|
|
2876
2872
|
console.warn("Erro ao sincronizar com servidor:", err);
|
|
2877
2873
|
updateSaveStatus("error");
|
|
2874
|
+
if (throwOnError) throw err;
|
|
2878
2875
|
}
|
|
2879
2876
|
}
|
|
2880
2877
|
|
|
@@ -3635,9 +3632,8 @@ ${ta.value}`;
|
|
|
3635
3632
|
|
|
3636
3633
|
const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
|
|
3637
3634
|
|
|
3638
|
-
//
|
|
3639
|
-
|
|
3640
|
-
const PANE_TABS = ["props", "inspect", "chat", "vars"];
|
|
3635
|
+
// Formatar, Propriedades e Variáveis ficam agrupados; o Assistente tem acesso fixo separado, por último.
|
|
3636
|
+
const PANE_TABS = ["props", "inspect", "vars", "chat"];
|
|
3641
3637
|
const paneEls = (w) => ({ btn: document.getElementById(`tab-btn-${w}`), panel: document.getElementById(`tab-panel-${w}`) });
|
|
3642
3638
|
const currentPane = () => PANE_TABS.find((w) => paneEls(w).panel?.classList.contains("active")) || "props";
|
|
3643
3639
|
// menu de botão direito genérico (itens: { label, ic, fn, danger, disabled } ou { sep: true })
|
|
@@ -3665,7 +3661,7 @@ ${ta.value}`;
|
|
|
3665
3661
|
const fab = document.getElementById("chat-fab");
|
|
3666
3662
|
if (!fab) return;
|
|
3667
3663
|
const visible = isMobile() ? dom.inspectorSidebar.classList.contains("mobile-open") : !dom.inspectorSidebar.classList.contains("collapsed");
|
|
3668
|
-
fab.classList.toggle("hidden", visible
|
|
3664
|
+
fab.classList.toggle("hidden", visible); // com o painel aberto, o botão Assistente já está sempre à vista
|
|
3669
3665
|
const badge = document.getElementById("chat-badge");
|
|
3670
3666
|
if (visible && currentPane() === "chat" && badge?.classList.contains("show")) badge.classList.remove("show"); // só se precisar: o observador do selo chama esta função
|
|
3671
3667
|
fab.classList.toggle("has-news", !!document.getElementById("chat-badge")?.classList.contains("show"));
|
|
@@ -3711,7 +3707,7 @@ ${ta.value}`;
|
|
|
3711
3707
|
|
|
3712
3708
|
// Chat lateral com IA (chat.js): mesma API, agora em módulo próprio.
|
|
3713
3709
|
const chatApi = window.SagaChat({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
3714
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus });
|
|
3710
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized });
|
|
3715
3711
|
const { handleChatSubmit, loadChatHistory, appendChatMessage, createProgressBubble, autoGrowChat, applyBrainstorm, addChatFile } = chatApi;
|
|
3716
3712
|
|
|
3717
3713
|
function setNotesVisible(on) {
|
|
@@ -4314,29 +4310,24 @@ ${ta.value}`;
|
|
|
4314
4310
|
return { res, name };
|
|
4315
4311
|
}
|
|
4316
4312
|
|
|
4317
|
-
// PowerPoint / PDF / roteiro:
|
|
4313
|
+
// PowerPoint / PDF / roteiro: acompanham a fila do servidor, sem prender a página no download.
|
|
4318
4314
|
const EXPORTS = {
|
|
4319
|
-
pptx:
|
|
4320
|
-
|
|
4321
|
-
roteiro: { label: "o roteiro", done: "roteiro com miniaturas, notas e tempos." },
|
|
4322
|
-
estudo: { label: "o material de estudo", done: "material de estudo: cada slide inteiro e o texto de consulta." },
|
|
4323
|
-
"estudo-html": { label: "o material de estudo", done: "material de estudo num HTML só." },
|
|
4324
|
-
tudo: { label: "o PowerPoint, o PDF e o roteiro", done: "um .zip com o PowerPoint (com notas), o PDF e o roteiro." },
|
|
4315
|
+
pptx: "o PowerPoint", pdf: "o PDF", roteiro: "o roteiro", estudo: "o material de estudo",
|
|
4316
|
+
"estudo-html": "o material de estudo", tudo: "o PowerPoint, o PDF e o roteiro",
|
|
4325
4317
|
};
|
|
4326
4318
|
document.querySelectorAll("[data-export]").forEach((btn) => {
|
|
4327
4319
|
btn.onclick = async (e) => {
|
|
4328
4320
|
e.preventDefault();
|
|
4329
4321
|
if (btn.disabled) return;
|
|
4330
4322
|
const kind = btn.dataset.export, clean = btn.dataset.notas === "0";
|
|
4331
|
-
const
|
|
4323
|
+
const label = clean ? "o PowerPoint sem as notas" : EXPORTS[kind];
|
|
4332
4324
|
btn.disabled = true;
|
|
4333
|
-
showToast(`Gerando ${x.label} (${state.deck.slides.length} slides)… pode levar alguns segundos.`, 60000);
|
|
4334
4325
|
try {
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4326
|
+
await window.SagadeckExports.start({ kind, notes: !clean, title: state.deck.title }, { beforeStart: async () => {
|
|
4327
|
+
if (!state.preview) await syncDeckToServer({ throwOnError: true });
|
|
4328
|
+
} });
|
|
4338
4329
|
} catch (err) {
|
|
4339
|
-
showToast(`Não deu para gerar ${
|
|
4330
|
+
showToast(`Não deu para gerar ${label}: ${err.message}`, 8000);
|
|
4340
4331
|
} finally {
|
|
4341
4332
|
btn.disabled = false;
|
|
4342
4333
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Saiu do app.js pela trava de tamanho dos monolitos; o comportamento é o mesmo.
|
|
3
3
|
// Fábrica no molde de slide-select.js: recebe o estado e os ajudantes, devolve a API.
|
|
4
4
|
window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
5
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus }) {
|
|
5
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized }) {
|
|
6
6
|
let chatJob = null;
|
|
7
7
|
const getChatJob = () => chatJob;
|
|
8
8
|
// ==========================================================================
|
|
@@ -159,6 +159,10 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
159
159
|
}, (ev) => { if (!commandEvent(ev)) work.update(ev); }, job.controller.signal);
|
|
160
160
|
if (state.file !== job.file) { work.done(); showToast("O pedido terminou na apresentação em que começou; a apresentação atual foi preservada.", 6000); return; }
|
|
161
161
|
if (!data.spec) throw new Error(data.error || "resposta sem deck");
|
|
162
|
+
if (data.materialized) {
|
|
163
|
+
state.file = data.materialized.file;
|
|
164
|
+
previewMaterialized(data.materialized);
|
|
165
|
+
}
|
|
162
166
|
// conversa salva antes de dispensar o indicador: quem espera o fim da resposta já encontra o arquivo gravado
|
|
163
167
|
const finishWork = async () => { await saveChatHistory(); work.done(); };
|
|
164
168
|
if (data.variants) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Conteúdo técnico em um só lugar: fonte à esquerda, resultado à direita. */
|
|
2
|
+
.dev-author-dialog{margin:auto;width:min(1110px,96vw);max-width:96vw;height:min(810px,94dvh);max-height:94dvh;padding:0;border:1px solid var(--ui-line);border-radius:18px;background:var(--chrome);color:var(--text);box-shadow:0 24px 90px #0005;font:13px var(--font-sans)}
|
|
3
|
+
.dev-author-dialog[open]{display:flex;flex-direction:column}.dev-author-dialog::backdrop{background:#101521aa;backdrop-filter:blur(4px)}
|
|
4
|
+
.dev-author-dialog header{display:flex;justify-content:space-between;align-items:center;padding:20px 25px;border-bottom:1px solid var(--ui-line)}.dev-author-dialog header small{color:var(--brand);font-size:10px;font-weight:700;letter-spacing:.13em}.dev-author-dialog h2{margin:5px 0;font-size:25px;letter-spacing:-.035em}.dev-author-dialog header p{margin:5px 0 0;color:var(--text-2)}
|
|
5
|
+
.dev-author-dialog button{padding:9px 12px;border:1px solid var(--ui-line-strong);border-radius:8px;background:var(--chrome);color:var(--text);font:inherit;cursor:pointer}.dev-author-dialog button:hover{background:var(--hover)}.dev-author-dialog button:disabled{opacity:.45;cursor:default}.dev-author-dialog button:focus-visible,.dev-author-dialog input:focus-visible,.dev-author-dialog select:focus-visible,.dev-author-dialog textarea:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
|
|
6
|
+
.dev-author-work{display:grid;grid-template-columns:minmax(0,1fr) 340px;min-height:0;flex:1}.dev-author-source{display:flex;flex-direction:column;min-height:0;padding:22px 24px;gap:10px}.dev-author-dialog label{display:block;color:var(--text-2);font-size:12px}.dev-author-dialog input:not([type=radio]),.dev-author-dialog textarea,.dev-author-dialog select{display:block;width:100%;margin-top:6px;padding:9px 10px;border:1px solid var(--ui-line-strong);border-radius:7px;background:var(--chrome);color:var(--text);font:13px var(--font-sans)}
|
|
7
|
+
.dev-author-dialog #dev-source{flex:1;min-height:150px;margin-top:0;resize:none;white-space:pre;tab-size:2;font:13px/1.65 var(--font-mono,monospace);background:var(--canvas-bg)}.dev-author-meta{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-meta>label:first-child{grid-column:1/-1}.dev-author-dialog small{font-size:10px}.dev-author-dialog aside{padding:22px;overflow-y:auto;border-left:1px solid var(--ui-line)}.dev-author-dialog fieldset{border:0;padding:0;margin:0 0 22px}.dev-author-dialog legend{font-size:12px;font-weight:700;color:var(--text);margin-bottom:12px}
|
|
8
|
+
.dev-author-modes label{display:flex;align-items:flex-start;gap:9px;padding:12px;margin-bottom:8px;border:1px solid var(--ui-line);border-radius:9px;cursor:pointer}.dev-author-modes label:has(input:checked){background:var(--selected);border-color:var(--brand)}.dev-author-modes input{margin-top:3px;accent-color:var(--brand)}.dev-author-modes b{display:block;color:var(--text);font-size:13px}.dev-author-modes small{display:block;line-height:1.5;margin-top:4px;color:var(--text-2)}
|
|
9
|
+
.dev-author-dialog h3{font-size:13px;margin-bottom:9px}.dev-author-dialog .dev-author-tip{font-size:11px;line-height:1.5;color:var(--text-2);margin:0 0 10px}.dev-author-range{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-dialog [data-guide]>label{margin:10px 0}.dev-author-dialog [data-step-text]{resize:vertical;max-height:150px}.dev-author-dialog [data-add-step]{width:100%;margin:4px 0 12px}.dev-author-steps{display:grid;gap:6px}.dev-author-step{display:flex;gap:4px}.dev-author-step button:first-child{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.dev-author-step button.active{border-color:var(--brand);color:var(--brand)}.dev-author-step button:last-child{font-size:10px;padding:7px}
|
|
10
|
+
.dev-author-dialog [data-request]{max-height:210px;overflow:auto;border:1px solid var(--ui-line);padding:12px;border-radius:8px;background:var(--canvas-bg);font:11px/1.6 var(--font-mono,monospace);white-space:pre-wrap;overflow-wrap:anywhere;margin-bottom:12px}.dev-author-secrets{padding:12px;border:1px solid var(--brand);border-radius:8px;color:var(--text);background:var(--selected);line-height:1.6;font-size:11px;overflow-wrap:anywhere}.dev-author-dialog [hidden]{display:none!important}
|
|
11
|
+
.dev-author-dialog footer{display:flex;align-items:center;justify-content:flex-end;gap:9px;padding:14px 24px;border-top:1px solid var(--ui-line)}.dev-author-dialog [data-status]{flex:1;font-size:11px;line-height:1.5;color:var(--text-2)}.dev-author-dialog .dev-author-primary{background:var(--brand);color:#fff;border-color:var(--brand)}.dev-author-dialog .dev-author-primary:hover{filter:brightness(1.08)}
|
|
12
|
+
@media(max-width:760px){.dev-author-dialog{height:94dvh}.dev-author-work{display:flex;flex-direction:column;overflow-y:auto}.dev-author-source{min-height:390px;flex-shrink:0;padding:16px}.dev-author-dialog aside{border-left:0;border-top:1px solid var(--ui-line);overflow:visible;padding:16px}.dev-author-dialog header{padding:15px 16px}.dev-author-dialog h2{font-size:21px}.dev-author-dialog footer{padding:12px 16px;flex-wrap:wrap}.dev-author-dialog [data-status]{flex-basis:100%}}
|