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
|
@@ -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"],
|
|
@@ -48,6 +47,7 @@ export const LAYOUT_INFO = {
|
|
|
48
47
|
timeline: ["Linha do tempo", "Eventos em sequência"],
|
|
49
48
|
chart: ["Gráfico de dados", "Barras, colunas, linhas, rosca: cole a tabela do Excel ou digite os valores"],
|
|
50
49
|
table: ["Tabela", "Tabela de verdade nas cores do tema: cole do Excel, destaque linha e total"],
|
|
50
|
+
map: ["Mapa", "Pontos, linhas e áreas sobre o mapa ao vivo: da planilha do projeto, de GeoJSON/GPX/KML ou valores por estado; cor por categoria, cartão ao clicar, uma camada por clique"],
|
|
51
51
|
compare: ["Comparação", "A × B lado a lado"],
|
|
52
52
|
matrix: ["Matriz 2×2", "Quatro quadrantes"],
|
|
53
53
|
question: ["Pergunta", "Pergunta para a plateia com timer"],
|
|
@@ -57,7 +57,6 @@ export const LAYOUT_INFO = {
|
|
|
57
57
|
codewalk: ["Código guiado", "Explique uma linha por vez, com destaques e saída simulada"],
|
|
58
58
|
api: ["API ao vivo", "Requisição que executa na hora (tipo Postman): síncrono, polling ou streaming, com código curl/Python"],
|
|
59
59
|
spotlight: ["Foco guiado", "Conduza o olhar por detalhes de uma imagem ou diagrama"],
|
|
60
|
-
kinetic: ["Tipografia cinética", "Frases em sequência viram parte da cena com ritmo e estilos tipográficos que mudam"],
|
|
61
60
|
video: ["Vídeo", "Player YouTube online ou MP4 incorporado"],
|
|
62
61
|
blocks: ["Livre (blocos)", "Monte com blocos em linhas e colunas"],
|
|
63
62
|
canvas: ["Livre (posições)", "Posicione cada elemento à mão"],
|
|
@@ -65,7 +64,75 @@ export const LAYOUT_INFO = {
|
|
|
65
64
|
end: ["Encerramento", "Obrigado e contatos"],
|
|
66
65
|
};
|
|
67
66
|
|
|
67
|
+
// Como pedir cada layout à IA, em português comum: aparece no "Conheça o SagaDeck" (src/studio/tour.js), para quem vê
|
|
68
|
+
// o recurso saber a frase que o gera. Exemplos de áreas variadas, nunca de um setor só.
|
|
69
|
+
export const LAYOUT_ASK = {
|
|
70
|
+
portal: "Abra a palestra entrando numa porta que transforma o cenário e revela o tema",
|
|
71
|
+
playground: "Faça um slide interativo em que a plateia mexe num controle e vê a cor do céu mudar",
|
|
72
|
+
codelab: "Monte um exercício de código em que os alunos alteram a entrada e executam passo a passo",
|
|
73
|
+
graphlab: "Mostre a rede de metrô da cidade e deixe testar a rota mais curta tirando uma estação",
|
|
74
|
+
mosaic: "Liste as seis frentes do projeto num mosaico que se ajusta sozinho",
|
|
75
|
+
ribbon: "Mostre as três fases da obra em cápsulas, uma ao lado da outra",
|
|
76
|
+
dossier: "Faça uma página de consulta com os comandos e as respostas da API, para guardar",
|
|
77
|
+
decisionlab: "Compare revisão humana e automação com premissas que eu possa ajustar ao vivo",
|
|
78
|
+
solution: "Resolva passo a passo quanto rende uma aplicação de R$ 1.000 a 1% ao mês em três anos",
|
|
79
|
+
calc: "Faça uma calculadora do consumo de energia de uma casa, com faixas de baixo, médio e alto",
|
|
80
|
+
algo: "Mostre a busca binária rodando de verdade, com o código e as variáveis",
|
|
81
|
+
carousel: "Apresente cinco destinos de viagem, um por clique, com foto e uma frase",
|
|
82
|
+
duel: "Simule duas pessoas editando o mesmo arquivo no Git até dar conflito",
|
|
83
|
+
terminals: "Mostre dois terminais lado a lado: um faz push e o outro puxa a mudança",
|
|
84
|
+
turns: "Encene um diálogo entre cliente e atendente, uma fala por clique",
|
|
85
|
+
science: "Mostre a parábola y = ax² + bx + c com controles para a, b e c",
|
|
86
|
+
infographic: "Faça um infográfico com os cinco pilares da sustentabilidade em volta de um centro",
|
|
87
|
+
diagram: "Desenhe o fluxo de atendimento de um pedido, da chegada à entrega",
|
|
88
|
+
cover: "Faça a capa da palestra sobre energia solar, com o meu nome e a data",
|
|
89
|
+
section: "Abra a segunda parte, sobre os resultados",
|
|
90
|
+
statement: "Uma frase de impacto: a água mais barata é a que não se desperdiça",
|
|
91
|
+
calendar: "Mostre num calendário as datas das oficinas do semestre",
|
|
92
|
+
poster: "Faça um pôster de uma página explicando como funciona uma placa solar",
|
|
93
|
+
definition: "Defina \"sustentabilidade\", com a origem da palavra",
|
|
94
|
+
headline: "Uma manchete enorme: 40% da água tratada se perde no caminho",
|
|
95
|
+
quote: "Ponha a citação de Paulo Freire sobre ensinar, com o autor",
|
|
96
|
+
onepage: "Resuma o projeto numa página: jornada, problema com números, solução e painel",
|
|
97
|
+
status: "Faça o status da semana: feito, em andamento, bloqueios e próximos passos",
|
|
98
|
+
hub: "Monte um menu em que a plateia escolhe o caminho: iniciante, intermediário ou avançado",
|
|
99
|
+
number: "Mostre o número 1,2 milhão de visitantes, contando até ele",
|
|
100
|
+
split: "Explique o ciclo da água com o texto de um lado e uma ilustração do outro",
|
|
101
|
+
full: "Uma foto de floresta ocupando o slide inteiro",
|
|
102
|
+
cards: "Três cartões com os benefícios do programa, cada um com ícone",
|
|
103
|
+
bento: "Monte um mosaico de blocos com os destaques do ano",
|
|
104
|
+
stats: "Mostre quatro indicadores do trimestre com a tendência de cada um",
|
|
105
|
+
steps: "Mostre em etapas como instalar um sensor de umidade",
|
|
106
|
+
funnel: "Faça o funil de inscrições: visitantes, cadastros, inscritos e concluintes",
|
|
107
|
+
pyramid: "Monte a pirâmide de prioridades, do essencial ao desejável",
|
|
108
|
+
list: "Liste os cinco cuidados antes de uma viagem longa",
|
|
109
|
+
agenda: "Faça a agenda do evento com a parte atual destacada",
|
|
110
|
+
timeline: "Faça a linha do tempo da história do cinema brasileiro",
|
|
111
|
+
chart: "Faça um gráfico de linhas com a temperatura média de cada mês",
|
|
112
|
+
table: "Monte a tabela comparando três planos, com o total destacado",
|
|
113
|
+
map: "Mostre no mapa as estações de medição da planilha, coloridas pelo status, com um cartão ao clicar",
|
|
114
|
+
compare: "Compare transporte público e carro próprio lado a lado",
|
|
115
|
+
matrix: "Monte uma matriz 2x2 de esforço e impacto com as ideias do time",
|
|
116
|
+
question: "Faça uma pergunta para a plateia com cronômetro de 30 segundos",
|
|
117
|
+
poll: "Faça uma enquete ao vivo: qual tema vocês querem aprofundar?",
|
|
118
|
+
image: "Ponha esta foto em tela cheia com uma legenda curta",
|
|
119
|
+
code: "Mostre este código Python com as linhas da conexão destacadas",
|
|
120
|
+
codewalk: "Explique este código uma linha por vez, com a saída simulada",
|
|
121
|
+
api: "Faça uma chamada de API ao vivo que consulta o clima e mostra a resposta",
|
|
122
|
+
spotlight: "Conduza o olhar pelas partes desta planta baixa, uma por vez",
|
|
123
|
+
video: "Ponha este vídeo do YouTube no slide",
|
|
124
|
+
blocks: "Monte livremente um slide com título, dois textos e uma imagem em colunas",
|
|
125
|
+
canvas: "Posicione o logotipo, o título e a foto onde eu indicar",
|
|
126
|
+
references: "Liste as referências bibliográficas no fim",
|
|
127
|
+
end: "Feche com um obrigado e os meus contatos",
|
|
128
|
+
};
|
|
129
|
+
|
|
68
130
|
export const LAYOUT_SAMPLES = {
|
|
131
|
+
map: { layout: "map", kicker: "Monitoramento", title: "Estações de ==qualidade do ar==", basemap: "claro", layers: [
|
|
132
|
+
{ name: "Estações", color: "status", label: "nome", points: [
|
|
133
|
+
{ nome: "Centro", lat: -23.5489, lon: -46.6388, status: "Boa", pm25: 12 }, { nome: "Pinheiros", lat: -23.5614, lon: -46.7020, status: "Moderada", pm25: 24 },
|
|
134
|
+
{ nome: "Ibirapuera", lat: -23.5874, lon: -46.6576, status: "Boa", pm25: 9 }, { nome: "Mooca", lat: -23.5596, lon: -46.5996, status: "Ruim", pm25: 41 },
|
|
135
|
+
{ nome: "Santana", lat: -23.5027, lon: -46.6252, status: "Moderada", pm25: 27 }], colors: { Ruim: "alert" } }] },
|
|
69
136
|
portal: portalExample,
|
|
70
137
|
playground: playgroundExample,
|
|
71
138
|
codelab: codeLabExample,
|
|
@@ -74,8 +141,8 @@ export const LAYOUT_SAMPLES = {
|
|
|
74
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."}]},
|
|
75
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"}'}]},
|
|
76
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}},
|
|
77
|
-
infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios==
|
|
78
|
-
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" },
|
|
79
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."}},
|
|
80
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}}]},
|
|
81
148
|
algo: {layout:"algo", title:"Bubble sort passo a passo", algorithm:"bubble", array:[5, 1, 4, 2, 8, 3]},
|
|
@@ -86,15 +153,14 @@ export const LAYOUT_SAMPLES = {
|
|
|
86
153
|
terminals: {layout:"terminals", title:"Dois terminais", file:"README.md", base:"# Projeto\nversão 1", people:["Ana","Beto"],
|
|
87
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}]},
|
|
88
155
|
turns: {layout:"turns", title:"Code review ao vivo", people:[{name:"Ana", role:"autora do PR"}, {name:"Beto", role:"revisor"}],
|
|
89
|
-
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"}]},
|
|
90
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"}}}},
|
|
91
|
-
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"},
|
|
92
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 } },
|
|
93
159
|
section: { layout: "section", number: 2, kicker: "Parte dois", title: "O problema" },
|
|
94
160
|
statement: { layout: "statement", kicker: "A ideia", text: "Uma frase que ==muda tudo==." },
|
|
95
161
|
calendar: { layout: "calendar", kicker: "Agenda", title: "Próximos lançamentos", events: [
|
|
96
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" },
|
|
97
|
-
{ 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" }] },
|
|
98
164
|
poster: { layout: "poster", kicker: "Receita", title: "Pão de fermentação natural", subtitle: "Do fermento ao forno em quatro etapas", flow: true,
|
|
99
165
|
panels: [
|
|
100
166
|
{ title: "Fermento", icon: "wheat", text: "Farinha e água, alimentados por 5 dias", facts: [{ value: "1:1", label: "farinha e água" }] },
|
|
@@ -105,9 +171,9 @@ export const LAYOUT_SAMPLES = {
|
|
|
105
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" },
|
|
106
172
|
headline: { layout: "headline", text: "78% ==desistem==", caption: "no segundo passo" },
|
|
107
173
|
quote: { layout: "quote", quote: "Dados vencem opiniões.", by: "Alguém sábio" },
|
|
108
|
-
onepage: { layout: "onepage", title: "
|
|
109
|
-
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: "
|
|
110
|
-
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" }] },
|
|
111
177
|
number: { layout: "number", value: 42, suffix: "%", label: "mais rápido" },
|
|
112
178
|
split: { layout: "split", title: "Texto e figura", bullets: ["Primeiro ponto", "Segundo ponto"], figure: { icon: "lightbulb", size: 300 } },
|
|
113
179
|
full: { layout: "full", figure: { chart: "donut", value: 72, center: "72%" }, title: "Figura em página inteira", overlay: "left" },
|
|
@@ -138,11 +204,7 @@ export const LAYOUT_SAMPLES = {
|
|
|
138
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." },
|
|
139
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." },
|
|
140
206
|
{ x: 5, y: 52, width: 78, height: 39, title: "Os dados", text: "A estrutura JSON é o contrato que o front-end precisa entender." }] },
|
|
141
|
-
|
|
142
|
-
beats: [{ text: "Uma ideia", style: "clean", position: "left", color: "white", size: "medium", tag: "GANHA FORMA" },
|
|
143
|
-
{ text: "muda o olhar", style: "clean", position: "right", color: "gold", size: "medium" },
|
|
144
|
-
{ text: "e abre caminhos", style: "clean", position: "left", color: "white", size: "medium" }] },
|
|
145
|
-
// 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`)
|
|
146
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" },
|
|
147
209
|
video: { layout: "video", title: "Assista", url: "https://example.com", label: "Assistir" },
|
|
148
210
|
blocks: { layout: "blocks", title: "Blocos livres", content: [{ row: [{ lead: "Esquerda" }, { lead: "Direita" }] }] },
|
|
@@ -103,15 +103,6 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
103
103
|
if (!made) throw new Error("Não há um modelo em prévia aberto.");
|
|
104
104
|
return ok(made);
|
|
105
105
|
}
|
|
106
|
-
case "/api/library/decks/example-cenario": {
|
|
107
|
-
// demonstração do Texto no cenário: o YAML e as imagens de exemplo (fundo e recorte transparente)
|
|
108
|
-
const dir0 = TEMPLATE_DIRS.map((d) => path.join(d, "cenario")).find((d) => fs.existsSync(d));
|
|
109
|
-
if (!dir0) throw new Error("o exemplo não veio no pacote (templates/cenario)");
|
|
110
|
-
const tpl = fs.readdirSync(dir0).find((f) => f.endsWith(".yaml"));
|
|
111
|
-
const id = L.createDeck(b.topic || "", YAML.parse(fs.readFileSync(path.join(dir0, tpl), "utf8")));
|
|
112
|
-
fs.cpSync(path.join(dir0, "imagens"), path.join(path.dirname(L.resolveId(id)), "imagens"), { recursive: true, force: false });
|
|
113
|
-
return ok({ id });
|
|
114
|
-
}
|
|
115
106
|
case "/api/library/decks/example": {
|
|
116
107
|
// exemplo de slides api (aula de APIs de IA): roda no ambiente embutido "ensaio", sem configurar nada
|
|
117
108
|
const tpl = templateFile("ensaio-api.yaml");
|
package/src/studio/project.js
CHANGED
|
@@ -20,7 +20,7 @@ const toNum = (v) => rawNum(String(v ?? "").trim().replace(/^R\$\s*/i, "").repla
|
|
|
20
20
|
export const META = ".sagadeck";
|
|
21
21
|
export const CONTEXT = "contexto";
|
|
22
22
|
const TEXT_EXT = new Set(["md", "markdown", "txt", "csv", "tsv", "json", "yaml", "yml", "css", "js", "mjs", "cjs", "jsx", "ts", "tsx", "html", "htm", "svg", "xml",
|
|
23
|
-
"py", "r", "m", "sql", "sh", "bash", "bat", "ps1", "ini", "toml", "cfg", "java", "c", "h", "cpp", "cs", "go", "rs", "tex", "bib", "log"]);
|
|
23
|
+
"py", "r", "m", "sql", "sh", "bash", "bat", "ps1", "ini", "toml", "cfg", "java", "c", "h", "cpp", "cs", "go", "rs", "tex", "bib", "log", "geojson", "gpx", "kml"]); // geo: as camadas do mapa (map.js) se editam aqui
|
|
24
24
|
const SHEET_EXT = new Set(["csv", "tsv", "xlsx"]);
|
|
25
25
|
const IMAGE_EXT = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg"]);
|
|
26
26
|
const extOf = (f) => (String(f).match(/\.([a-z0-9]+)$/i)?.[1] || "").toLowerCase();
|
|
@@ -226,6 +226,7 @@ export function writeSheet(P, rel, rows) {
|
|
|
226
226
|
if (!Array.isArray(rows) || rows.some((r) => !Array.isArray(r))) throw new Error("Planilha inválida.");
|
|
227
227
|
const old = fs.existsSync(abs) ? parseCSV(fs.readFileSync(abs, "utf8"), ext === "tsv" ? "\t" : undefined) : { delimiter: ext === "tsv" ? "\t" : ",", eol: "\n" };
|
|
228
228
|
const tmp = abs + ".tmp";
|
|
229
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true }); // planilha nova numa pasta que ainda não existe (contexto/)
|
|
229
230
|
const clean = rows.map((r) => r.map((c) => String(c ?? "")));
|
|
230
231
|
while (clean.length > 1 && clean.at(-1).every((c) => c === "")) clean.pop(); // linha vazia no fim (o cursor passou dela) não vai para o arquivo
|
|
231
232
|
fs.writeFileSync(tmp, toCSV(clean, old));
|
|
@@ -8,13 +8,15 @@ 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
|
|
|
16
|
+
// o estilo da caixa serve também ao Configurar mapa (map-settings.js)
|
|
17
|
+
const ensureStyle = () => { if (!document.getElementById("ai-cfg-style")) { const st = document.createElement("style"); st.id = "ai-cfg-style"; st.textContent = css; document.head.append(st); } };
|
|
16
18
|
function build() {
|
|
17
|
-
|
|
19
|
+
ensureStyle();
|
|
18
20
|
dialog = document.createElement("dialog");
|
|
19
21
|
dialog.className = "ai-cfg";
|
|
20
22
|
dialog.id = "ai-settings-dialog";
|
|
@@ -85,6 +87,7 @@ window.SagaAISettings = (() => {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
return {
|
|
90
|
+
ensureStyle,
|
|
88
91
|
async open(saved) {
|
|
89
92
|
onSaved = typeof saved === "function" ? saved : () => {};
|
|
90
93
|
if (!dialog) build();
|
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", "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
|
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
const LAYOUT_LABELS = {
|
|
44
44
|
cover: "Capa", section: "Seção", statement: "Frase de impacto", definition: "Definição", poster: "Pôster (infográfico)", calendar: "Calendário", quote: "Citação", number: "Número grande",
|
|
45
45
|
split: "Texto e figura", cards: "Cartões", stats: "Indicadores", steps: "Etapas", list: "Lista",
|
|
46
|
-
timeline: "Linha do tempo", chart: "Gráfico de dados", table: "Tabela", compare: "Comparação", matrix: "Matriz 2×2",
|
|
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",
|
|
@@ -394,6 +394,7 @@
|
|
|
394
394
|
const idx = state.currentSlideIndex;
|
|
395
395
|
const slide = state.deck.slides[idx];
|
|
396
396
|
if (!slide) return;
|
|
397
|
+
window.SagaProject?.slideChanged?.(slide);
|
|
397
398
|
if (dom.tabPanelVars.classList.contains("active")) renderStudioSavedVars();
|
|
398
399
|
|
|
399
400
|
renderReviewBar(slide, idx);
|
|
@@ -424,8 +425,11 @@
|
|
|
424
425
|
|
|
425
426
|
// Renderizar HTML no palco
|
|
426
427
|
window.SagaScience?.dispose(dom.renderedSlideContainer);
|
|
428
|
+
window.SagaMap?.dispose(dom.renderedSlideContainer, { keepView: true });
|
|
427
429
|
dom.renderedSlideContainer.innerHTML = data.html;
|
|
428
430
|
window.SagaScience?.mount(dom.renderedSlideContainer);
|
|
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, materialized: previewMaterialized });
|
|
429
433
|
window.SagaCalc?.mount(dom.renderedSlideContainer); // calculadora ao vivo: os controles funcionam no editor também
|
|
430
434
|
window.SagaDiagrams?.mount(dom.renderedSlideContainer).then(() => reportDiagram(dom.renderedSlideContainer));
|
|
431
435
|
window.SagaDecisionLab?.mount(dom.renderedSlideContainer);
|
|
@@ -2045,11 +2049,11 @@
|
|
|
2045
2049
|
const SLIDE_GROUPS = [
|
|
2046
2050
|
["estrutura", "Abertura e estrutura", ["cover", "section", "agenda", "end", "references"]],
|
|
2047
2051
|
["texto", "Texto e ideias", ["statement", "definition", "poster", "calendar", "headline", "quote", "list", "cards", "split", "mosaic", "ribbon", "bento", "dossier"]],
|
|
2048
|
-
["dados", "Números e gráficos", ["number", "stats", "chart", "table", "science", "compare", "matrix"]],
|
|
2052
|
+
["dados", "Números e gráficos", ["number", "stats", "chart", "table", "map", "science", "compare", "matrix"]],
|
|
2049
2053
|
["processo", "Processos e diagramas", ["steps", "timeline", "funnel", "pyramid", "diagram", "infographic", "hub"]],
|
|
2050
2054
|
["gestao", "Gestão e status", ["status", "onepage", "decisionlab"]],
|
|
2051
2055
|
["codigo", "Código e API", ["code", "codewalk", "api"]],
|
|
2052
|
-
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "
|
|
2056
|
+
["visual", "Imagem e movimento", ["image", "full", "carousel", "spotlight", "video", "portal"]],
|
|
2053
2057
|
["plateia", "Plateia", ["question", "poll"]],
|
|
2054
2058
|
["dinamicas", "Dinâmicas a dois", ["duel", "terminals", "turns"]],
|
|
2055
2059
|
["aula", "Aula: exercícios e algoritmos", ["solution", "calc", "algo", "graphlab", "codelab", "playground"]],
|
|
@@ -2325,38 +2329,13 @@
|
|
|
2325
2329
|
|
|
2326
2330
|
// Ao trocar de layout, o conteúdo em lista (cartões, etapas, eventos, níveis…) vai junto
|
|
2327
2331
|
const LIST_KEY = { cards: "items", bento: "tiles", stats: "stats", steps: "steps", funnel: "stages", pyramid: "levels", list: "items",
|
|
2328
|
-
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts"
|
|
2332
|
+
agenda: "items", timeline: "events", matrix: "cells", question: "options", poll: "options", split: "bullets", statement: "lines", references: "items", end: "contacts" };
|
|
2329
2333
|
const SOURCE_KEYS = ["items", "tiles", "stats", "kpis", "steps", "process", "flow", "stages", "levels", "events", "cells", "options", "bullets", "lines", "contacts"];
|
|
2330
|
-
function kineticPhrases(value) {
|
|
2331
|
-
const words = String(value || "").match(/==[^=]+==|\*\*.*?\*\*|\*[^*]+\*|~~.*?~~|`[^`]+`|\[[^\]]+\]\([^)]+\)|\S+/g) || [];
|
|
2332
|
-
const connectors = new Set(["a", "as", "de", "do", "da", "dos", "das", "e", "em", "no", "na", "nos", "nas", "para", "por", "com", "o", "os"]);
|
|
2333
|
-
const phrases = [];
|
|
2334
|
-
while (words.length) {
|
|
2335
|
-
let take = Math.ceil(words.length / Math.ceil(words.length / 4));
|
|
2336
|
-
const last = words[take - 1]?.replace(/^[^A-Za-zÀ-ÿ]+|[^A-Za-zÀ-ÿ]+$/g, "").toLowerCase();
|
|
2337
|
-
if (take > 1 && connectors.has(last)) take--;
|
|
2338
|
-
phrases.push(words.splice(0, take).join(" "));
|
|
2339
|
-
}
|
|
2340
|
-
return phrases;
|
|
2341
|
-
}
|
|
2342
2334
|
function carryContent(slide, to) {
|
|
2343
2335
|
const target = LIST_KEY[to];
|
|
2344
2336
|
if (!target) return;
|
|
2345
2337
|
const activeKey = LIST_KEY[slide.layout];
|
|
2346
2338
|
if (activeKey === target && Array.isArray(slide[target]) && slide[target].length) return;
|
|
2347
|
-
if (to === "kinetic") {
|
|
2348
|
-
const source = SOURCE_KEYS.find((key) => Array.isArray(slide[key]) && slide[key].length);
|
|
2349
|
-
if (source) {
|
|
2350
|
-
slide.beats = slide[source].map((item) => {
|
|
2351
|
-
if (typeof item !== "object" || item == null) return { text: String(item ?? "") };
|
|
2352
|
-
return { text: item.title ?? item.label ?? item.text ?? item.name ?? "", ...(item.text && item.title ? { tag: item.text } : {}) };
|
|
2353
|
-
});
|
|
2354
|
-
} else {
|
|
2355
|
-
const phrases = [...kineticPhrases(slide.title || slide.text), ...kineticPhrases(slide.subtitle)];
|
|
2356
|
-
if (phrases.length) slide.beats = phrases.map((text, i) => ({ text, ...(i === 0 && slide.kicker ? { tag: slide.kicker } : {}) }));
|
|
2357
|
-
}
|
|
2358
|
-
return;
|
|
2359
|
-
}
|
|
2360
2339
|
const srcKey = [activeKey, ...SOURCE_KEYS].find((k) => k && Array.isArray(slide[k]) && slide[k].length);
|
|
2361
2340
|
if (!srcKey) return;
|
|
2362
2341
|
const entries = slide[srcKey].map((x) => {
|
|
@@ -2387,7 +2366,7 @@
|
|
|
2387
2366
|
}
|
|
2388
2367
|
if (layoutName === "quote") slide.quote = primary;
|
|
2389
2368
|
if (["question", "poll"].includes(layoutName)) slide.question = primary;
|
|
2390
|
-
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length &&
|
|
2369
|
+
if (LIST_KEY[layoutName] && !slide[LIST_KEY[layoutName]]?.length && layoutName !== "statement") {
|
|
2391
2370
|
const content = [slide.subtitle, slide.body, primary].find(v => typeof v === "string" && v.trim());
|
|
2392
2371
|
if (content) slide[LIST_KEY[layoutName]] = content.split(/\n+/).filter(Boolean).map(text => ["list", "split", "question", "poll", "references", "end"].includes(layoutName) ? text : {title:text});
|
|
2393
2372
|
}
|
|
@@ -2448,6 +2427,23 @@
|
|
|
2448
2427
|
} catch (e) { showToast(`Não deu para atualizar: ${e.message}`); }
|
|
2449
2428
|
},
|
|
2450
2429
|
hydrate: hydrateIcons,
|
|
2430
|
+
splitCalendar: async () => {
|
|
2431
|
+
const at = state.currentSlideIndex, slide = state.deck.slides[at];
|
|
2432
|
+
const { calendarMonth, calendarMonths } = window.SagaCalendarMonths;
|
|
2433
|
+
const MESES = ["janeiro", "fevereiro", "março", "abril", "maio", "junho", "julho", "agosto", "setembro", "outubro", "novembro", "dezembro"];
|
|
2434
|
+
const months = calendarMonths(slide);
|
|
2435
|
+
if (months.length < 2) return;
|
|
2436
|
+
const { uid, events, undated, month, week, ...base } = slide;
|
|
2437
|
+
const slides = months.map((m, k) => ({ ...base, ...(k === 0 && uid ? { uid } : {}), view: "mes", month: m,
|
|
2438
|
+
kicker: base.kicker || `${MESES[+m.slice(5) - 1]} ${m.slice(0, 4)}`,
|
|
2439
|
+
events: events.filter((e) => calendarMonth(e?.date) === m),
|
|
2440
|
+
...(k === 0 && (undated?.length || events.some((e) => !calendarMonth(e?.date))) ? { undated: [...(undated || []), ...events.filter((e) => !calendarMonth(e?.date)).map(({ title, text }) => ({ title, ...(text ? { text } : {}) }))] } : {}) }));
|
|
2441
|
+
state.deck.slides.splice(at, 1, ...slides);
|
|
2442
|
+
await syncDeckToServer();
|
|
2443
|
+
renderThumbnails();
|
|
2444
|
+
await renderCurrentSlide();
|
|
2445
|
+
showToast(`Um slide por mês: ${slides.length} slides, na visão de mês.`, 4000);
|
|
2446
|
+
},
|
|
2451
2447
|
pickIcon: (cb) => openIconPicker(null, cb),
|
|
2452
2448
|
layoutLabel,
|
|
2453
2449
|
generateImage: (element, btn) => {
|
|
@@ -2605,7 +2601,9 @@
|
|
|
2605
2601
|
}
|
|
2606
2602
|
|
|
2607
2603
|
async function startPresentation({ pen = false } = {}) {
|
|
2608
|
-
|
|
2604
|
+
// a apresentação é montada a partir do deck do servidor. Modelo em prévia: o servidor já tem o mesmo deck (qualquer
|
|
2605
|
+
// 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
|
|
2606
|
+
if (!state.preview) await syncDeckToServer();
|
|
2609
2607
|
const start = state.currentSlideIndex + 1;
|
|
2610
2608
|
dom.presModal.classList.remove("hidden");
|
|
2611
2609
|
dom.presFrame.src = scopedURL(`preview?t=${Date.now()}#${start}`);
|
|
@@ -2844,7 +2842,7 @@
|
|
|
2844
2842
|
// outra aba/aparelho abriu outra apresentação neste Studio: nada desta foi gravado (o servidor recusou)
|
|
2845
2843
|
function staleTab(msg) { updateSaveStatus("error"); showToast(msg, 20000, { label: "Recarregar", fn: () => location.reload() }); }
|
|
2846
2844
|
|
|
2847
|
-
async function syncDeckToServer() {
|
|
2845
|
+
async function syncDeckToServer({ throwOnError = false } = {}) {
|
|
2848
2846
|
trackDeck("Edição");
|
|
2849
2847
|
updateSaveStatus("saving");
|
|
2850
2848
|
try {
|
|
@@ -2854,7 +2852,7 @@
|
|
|
2854
2852
|
body: JSON.stringify({ spec: state.deck, expectFile: state.file }),
|
|
2855
2853
|
});
|
|
2856
2854
|
const data = await res.json().catch(() => ({}));
|
|
2857
|
-
if (data.stale)
|
|
2855
|
+
if (data.stale) { staleTab(data.error); if (throwOnError) throw new Error(data.error); return; }
|
|
2858
2856
|
if ("file" in data) state.file = data.file;
|
|
2859
2857
|
if (data.materialized) previewMaterialized(data.materialized);
|
|
2860
2858
|
// o servidor dá identidade a slide novo (uid) e leva o ajuste visual junto quando um texto muda: o Studio adota
|
|
@@ -2868,9 +2866,11 @@
|
|
|
2868
2866
|
if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
|
|
2869
2867
|
updateSaveStatus(res.ok ? "saved" : "error");
|
|
2870
2868
|
if (res.ok) window.SagaProject?.deckChanged();
|
|
2869
|
+
else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
|
|
2871
2870
|
} catch (err) {
|
|
2872
2871
|
console.warn("Erro ao sincronizar com servidor:", err);
|
|
2873
2872
|
updateSaveStatus("error");
|
|
2873
|
+
if (throwOnError) throw err;
|
|
2874
2874
|
}
|
|
2875
2875
|
}
|
|
2876
2876
|
|
|
@@ -3631,9 +3631,8 @@ ${ta.value}`;
|
|
|
3631
3631
|
|
|
3632
3632
|
const isMobile = () => window.matchMedia("(max-width: 900px)").matches;
|
|
3633
3633
|
|
|
3634
|
-
//
|
|
3635
|
-
|
|
3636
|
-
const PANE_TABS = ["props", "inspect", "chat", "vars"];
|
|
3634
|
+
// Formatar, Propriedades e Variáveis ficam agrupados; o Assistente tem acesso fixo separado, por último.
|
|
3635
|
+
const PANE_TABS = ["props", "inspect", "vars", "chat"];
|
|
3637
3636
|
const paneEls = (w) => ({ btn: document.getElementById(`tab-btn-${w}`), panel: document.getElementById(`tab-panel-${w}`) });
|
|
3638
3637
|
const currentPane = () => PANE_TABS.find((w) => paneEls(w).panel?.classList.contains("active")) || "props";
|
|
3639
3638
|
// menu de botão direito genérico (itens: { label, ic, fn, danger, disabled } ou { sep: true })
|
|
@@ -3661,7 +3660,7 @@ ${ta.value}`;
|
|
|
3661
3660
|
const fab = document.getElementById("chat-fab");
|
|
3662
3661
|
if (!fab) return;
|
|
3663
3662
|
const visible = isMobile() ? dom.inspectorSidebar.classList.contains("mobile-open") : !dom.inspectorSidebar.classList.contains("collapsed");
|
|
3664
|
-
fab.classList.toggle("hidden", visible
|
|
3663
|
+
fab.classList.toggle("hidden", visible); // com o painel aberto, o botão Assistente já está sempre à vista
|
|
3665
3664
|
const badge = document.getElementById("chat-badge");
|
|
3666
3665
|
if (visible && currentPane() === "chat" && badge?.classList.contains("show")) badge.classList.remove("show"); // só se precisar: o observador do selo chama esta função
|
|
3667
3666
|
fab.classList.toggle("has-news", !!document.getElementById("chat-badge")?.classList.contains("show"));
|
|
@@ -4310,29 +4309,24 @@ ${ta.value}`;
|
|
|
4310
4309
|
return { res, name };
|
|
4311
4310
|
}
|
|
4312
4311
|
|
|
4313
|
-
// PowerPoint / PDF / roteiro:
|
|
4312
|
+
// PowerPoint / PDF / roteiro: acompanham a fila do servidor, sem prender a página no download.
|
|
4314
4313
|
const EXPORTS = {
|
|
4315
|
-
pptx:
|
|
4316
|
-
|
|
4317
|
-
roteiro: { label: "o roteiro", done: "roteiro com miniaturas, notas e tempos." },
|
|
4318
|
-
estudo: { label: "o material de estudo", done: "material de estudo: cada slide inteiro e o texto de consulta." },
|
|
4319
|
-
"estudo-html": { label: "o material de estudo", done: "material de estudo num HTML só." },
|
|
4320
|
-
tudo: { label: "o PowerPoint, o PDF e o roteiro", done: "um .zip com o PowerPoint (com notas), o PDF e o roteiro." },
|
|
4314
|
+
pptx: "o PowerPoint", pdf: "o PDF", roteiro: "o roteiro", estudo: "o material de estudo",
|
|
4315
|
+
"estudo-html": "o material de estudo", tudo: "o PowerPoint, o PDF e o roteiro",
|
|
4321
4316
|
};
|
|
4322
4317
|
document.querySelectorAll("[data-export]").forEach((btn) => {
|
|
4323
4318
|
btn.onclick = async (e) => {
|
|
4324
4319
|
e.preventDefault();
|
|
4325
4320
|
if (btn.disabled) return;
|
|
4326
4321
|
const kind = btn.dataset.export, clean = btn.dataset.notas === "0";
|
|
4327
|
-
const
|
|
4322
|
+
const label = clean ? "o PowerPoint sem as notas" : EXPORTS[kind];
|
|
4328
4323
|
btn.disabled = true;
|
|
4329
|
-
showToast(`Gerando ${x.label} (${state.deck.slides.length} slides)… pode levar alguns segundos.`, 60000);
|
|
4330
4324
|
try {
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4325
|
+
await window.SagadeckExports.start({ kind, notes: !clean, title: state.deck.title }, { beforeStart: async () => {
|
|
4326
|
+
if (!state.preview) await syncDeckToServer({ throwOnError: true });
|
|
4327
|
+
} });
|
|
4334
4328
|
} catch (err) {
|
|
4335
|
-
showToast(`Não deu para gerar ${
|
|
4329
|
+
showToast(`Não deu para gerar ${label}: ${err.message}`, 8000);
|
|
4336
4330
|
} finally {
|
|
4337
4331
|
btn.disabled = false;
|
|
4338
4332
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
let treeData = null;
|
|
16
16
|
const openTabs = []; // { path, kind, name, dirty, text, sheet }
|
|
17
17
|
let active = "__deck__";
|
|
18
|
+
let slideLayout = "";
|
|
18
19
|
const expanded = new Set(["contexto"]);
|
|
19
20
|
const $ = (s, r = document) => r.querySelector(s);
|
|
20
21
|
const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
@@ -225,7 +226,8 @@
|
|
|
225
226
|
function bindTabs() {
|
|
226
227
|
$("#doc-tabs").addEventListener("click", (ev) => {
|
|
227
228
|
const c = ev.target.closest("[data-close]"); if (c) { ev.stopPropagation(); closeTab(c.dataset.close); return; }
|
|
228
|
-
|
|
229
|
+
// A aba ativa já tem a grade e a seleção da pessoa: reler por clique perderia ambas na resposta tardia.
|
|
230
|
+
const t = ev.target.closest("[data-doc]"); if (t && t.dataset.doc !== active) activate(t.dataset.doc);
|
|
229
231
|
});
|
|
230
232
|
$("#doc-tabs").addEventListener("auxclick", (ev) => { const t = ev.target.closest("[data-doc]"); if (ev.button === 1 && t && t.dataset.doc !== "__deck__") closeTab(t.dataset.doc); });
|
|
231
233
|
}
|
|
@@ -241,16 +243,26 @@
|
|
|
241
243
|
if (!openTabs.some((t) => t.path === p)) openTabs.push({ path: p, kind, name: p.split("/").pop() });
|
|
242
244
|
await activate(p);
|
|
243
245
|
}
|
|
246
|
+
// Com um mapa, a planilha e os pontos precisam estar à vista e receber cliques ao mesmo tempo.
|
|
247
|
+
function syncLayout() {
|
|
248
|
+
const t = openTabs.find((x) => x.path === active);
|
|
249
|
+
const mapSheet = t?.kind === "sheet" && slideLayout === "map";
|
|
250
|
+
const side = t?.kind === "deck" || mapSheet;
|
|
251
|
+
const editor = $("#canvas-stage-wrapper");
|
|
252
|
+
editor.classList.toggle("doc-side", active !== "__deck__" && side);
|
|
253
|
+
editor.classList.toggle("doc-full", active !== "__deck__" && !side);
|
|
254
|
+
if (t?.kind === "sheet") $("#doc-view .sh-body")?.classList.toggle("no-suggest", t.hideSuggest ?? mapSheet);
|
|
255
|
+
}
|
|
256
|
+
function slideChanged(slide) { slideLayout = slide?.layout || ""; syncLayout(); ctx?.relayout(); }
|
|
244
257
|
async function activate(p) {
|
|
245
258
|
active = p;
|
|
246
259
|
renderTabs();
|
|
247
260
|
$("#project-tree")?.querySelectorAll(".ex-file").forEach((r) => r.classList.toggle("active", r.dataset.path === p));
|
|
248
|
-
const
|
|
249
|
-
|
|
261
|
+
const view = $("#doc-view");
|
|
262
|
+
syncLayout();
|
|
250
263
|
if (p === "__deck__") { view.innerHTML = ""; view.classList.add("hidden"); ctx.relayout(); return; }
|
|
251
264
|
const t = openTabs.find((x) => x.path === p);
|
|
252
265
|
view.classList.remove("hidden");
|
|
253
|
-
editor.classList.add(t.kind === "deck" ? "doc-side" : "doc-full");
|
|
254
266
|
try {
|
|
255
267
|
if (t.kind === "deck") await showDeckText(view, t);
|
|
256
268
|
else if (t.kind === "md" || t.kind === "text" || (t.kind === "sheet" && t.asText)) await showText(view, t);
|
|
@@ -349,13 +361,14 @@
|
|
|
349
361
|
const sepInfo = data.delimiter ? ` · separado por ${DELIM[data.delimiter] || data.delimiter}` : "";
|
|
350
362
|
const asText = /\.(csv|tsv)$/i.test(t.name) ? `<button type="button" class="btn btn-small" data-as-text title="Ver o arquivo como texto"><i class="ic" data-ic="file-code"></i> Texto</button>` : "";
|
|
351
363
|
view.innerHTML = `<div class="dv-head"><i class="ic" data-ic="file-spreadsheet"></i><b>${esc(t.name)}</b><span class="dv-status" id="dv-status">${sheet.total} linhas · ${cols.length} colunas${sepInfo}${editable ? "" : " · só leitura"}</span>${asText}<button type="button" class="btn btn-small" data-toggle-suggest title="Gráficos que fazem sentido com estes dados"><i class="ic" data-ic="chart-column"></i> Gráficos</button></div>${tabs}
|
|
352
|
-
<div class="sh-body${t.hideSuggest ? " no-suggest" : ""}"><div class="sh-grid-wrap" id="sh-grid"></div>
|
|
364
|
+
<div class="sh-body${(t.hideSuggest ?? (slideLayout === "map")) ? " no-suggest" : ""}"><div class="sh-grid-wrap" id="sh-grid"></div>
|
|
353
365
|
<aside class="sh-suggest"><div class="sh-suggest-head"><b>Gráficos que fazem sentido</b><button type="button" class="btn btn-small" data-ai-suggest title="A IA confere o tipo das colunas e sugere gráficos, títulos e nomes dos eixos"><i class="ic" data-ic="sparkles"></i> Sugerir com IA</button></div><div class="sh-cards" id="sh-cards"><div class="ex-empty">Olhando as colunas…</div></div></aside></div>`;
|
|
354
366
|
const types = [];
|
|
355
367
|
for (const c of cols) types[c.index] = { type: c.type, label: TYPE_LABEL[c.type] || c.type };
|
|
356
368
|
let timer = 0;
|
|
357
|
-
window.SagaViewers.sheetGrid(view.querySelector("#sh-grid"), {
|
|
369
|
+
const grid = window.SagaViewers.sheetGrid(view.querySelector("#sh-grid"), {
|
|
358
370
|
rows: sheet.rows, editable, hasHeader: sheet.hasHeader, types,
|
|
371
|
+
onSelect: (r) => window.SagaMapData?.selectRow(t.path, r + 1),
|
|
359
372
|
onChange: (rows) => {
|
|
360
373
|
const st = $("#dv-status"); st.textContent = "gravando…"; t.dirty = true; renderTabs();
|
|
361
374
|
clearTimeout(timer);
|
|
@@ -365,9 +378,11 @@
|
|
|
365
378
|
}, 500);
|
|
366
379
|
},
|
|
367
380
|
});
|
|
381
|
+
// planilha com posição ou endereço: "Pôr no mapa" e "Achar coordenadas" (map-data.js)
|
|
382
|
+
window.SagaMapData?.sheetTools(view, { t, rows: sheet.rows, grid, api, ctx, reload: async (p) => { if (p && p !== t.path) return; t.text = undefined; await activate(t.path); } });
|
|
368
383
|
view.querySelectorAll("[data-sheet]").forEach((b) => b.onclick = () => { t.sheet = b.dataset.sheet; activate(t.path); });
|
|
369
384
|
view.querySelector("[data-as-text]")?.addEventListener("click", () => { t.asText = true; t.text = undefined; activate(t.path); });
|
|
370
|
-
view.querySelector("[data-toggle-suggest]").onclick = () => { t.hideSuggest = !
|
|
385
|
+
view.querySelector("[data-toggle-suggest]").onclick = () => { t.hideSuggest = !view.querySelector(".sh-body").classList.contains("no-suggest"); view.querySelector(".sh-body").classList.toggle("no-suggest", t.hideSuggest); };
|
|
371
386
|
view.querySelector("[data-ai-suggest]").onclick = (ev) => loadSuggestions(t, true, ev.currentTarget);
|
|
372
387
|
ctx.hydrate(view);
|
|
373
388
|
loadSuggestions(t, false);
|
|
@@ -439,5 +454,5 @@
|
|
|
439
454
|
// o deck mudou por outro caminho (formulário, IA, arrastar): a aba do texto da apresentação acompanha
|
|
440
455
|
function deckChanged() { openTabs.find((t) => t.kind === "deck")?.refresh?.(); }
|
|
441
456
|
const wantsPaste = () => !$("#project-explorer").classList.contains("hidden") || active !== "__deck__";
|
|
442
|
-
window.SagaProject = { setup, refreshTree, openFile, deckChanged, savePastedImage, uploadFile, wantsPaste, activeTab: () => active };
|
|
457
|
+
window.SagaProject = { setup, refreshTree, openFile, deckChanged, slideChanged, savePastedImage, uploadFile, wantsPaste, activeTab: () => active };
|
|
443
458
|
})();
|