sagadeck 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (233) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +305 -0
  3. package/SKILL.md +148 -0
  4. package/bin/sagadeck.js +511 -0
  5. package/docs/ANDAMENTO.md +1255 -0
  6. package/docs/EXPERIENCIAS.md +39 -0
  7. package/docs/PLANO-EXPERIENCIAS.md +39 -0
  8. package/docs/PLANO-TESTE-AULAS-TECNICAS.md +44 -0
  9. package/docs/PLANO-TESTE-DESINFORMACAO.md +38 -0
  10. package/docs/REFERENCIA.md +1131 -0
  11. package/package.json +59 -0
  12. package/src/adaptive-layouts.js +18 -0
  13. package/src/ai/art-direction.js +8 -0
  14. package/src/ai/commands.js +130 -0
  15. package/src/ai/context.js +245 -0
  16. package/src/ai/deck-ai.js +1135 -0
  17. package/src/ai/directions.js +74 -0
  18. package/src/ai/document-materials.js +49 -0
  19. package/src/ai/document-visuals.js +215 -0
  20. package/src/ai/evaluation.js +31 -0
  21. package/src/ai/ground.js +55 -0
  22. package/src/ai/llm.js +292 -0
  23. package/src/ai/quality.js +51 -0
  24. package/src/ai/relay.js +45 -0
  25. package/src/ai/staged-generation.js +24 -0
  26. package/src/ai/transform.js +858 -0
  27. package/src/ai/usage.js +17 -0
  28. package/src/ai/variety.js +80 -0
  29. package/src/ai/video-frames.js +24 -0
  30. package/src/ai/video-generation.js +126 -0
  31. package/src/ai/web-operation.js +31 -0
  32. package/src/algo-catalog.js +153 -0
  33. package/src/algo-trace.js +139 -0
  34. package/src/api-client.js +514 -0
  35. package/src/api-collections.js +14 -0
  36. package/src/api-demo.js +222 -0
  37. package/src/aspect.js +40 -0
  38. package/src/build.js +323 -0
  39. package/src/carousel.js +56 -0
  40. package/src/chrome.js +65 -0
  41. package/src/code-highlight.js +68 -0
  42. package/src/code-lab-browser.js +22 -0
  43. package/src/code-lab.js +3 -0
  44. package/src/code-language.js +52 -0
  45. package/src/csv.js +74 -0
  46. package/src/decision-lab.js +20 -0
  47. package/src/deck-file.js +119 -0
  48. package/src/diagram/napkin-examples.js +13 -0
  49. package/src/diagram/napkin.js +539 -0
  50. package/src/docx.js +128 -0
  51. package/src/dynamics/git-sim.js +200 -0
  52. package/src/dynamics/layouts.js +112 -0
  53. package/src/elements.js +554 -0
  54. package/src/experiences.js +124 -0
  55. package/src/exploration.js +31 -0
  56. package/src/export/browser.js +53 -0
  57. package/src/export/estudo.js +67 -0
  58. package/src/export/pptx.js +475 -0
  59. package/src/export/roteiro.js +40 -0
  60. package/src/export/shots.js +182 -0
  61. package/src/figures/charts.js +383 -0
  62. package/src/figures/diagrams.js +117 -0
  63. package/src/figures/icons.js +166 -0
  64. package/src/figures/material-pictos.js +20 -0
  65. package/src/figures/pictos.js +255 -0
  66. package/src/figures/points.js +106 -0
  67. package/src/figures/qr.js +29 -0
  68. package/src/figures/ufmap.js +48 -0
  69. package/src/fiscal/autofix.js +377 -0
  70. package/src/fiscal/normalize.js +115 -0
  71. package/src/graph-lab.js +24 -0
  72. package/src/hologram.js +30 -0
  73. package/src/identity.js +86 -0
  74. package/src/import/crop.js +59 -0
  75. package/src/import/index.js +132 -0
  76. package/src/import/legacy-word.js +42 -0
  77. package/src/import/merge.js +45 -0
  78. package/src/import/office.js +123 -0
  79. package/src/import/omml.js +69 -0
  80. package/src/import/pdf-render.js +56 -0
  81. package/src/import/pptx.js +701 -0
  82. package/src/import/xml.js +42 -0
  83. package/src/infographic.js +401 -0
  84. package/src/layouts.js +723 -0
  85. package/src/lessons.js +171 -0
  86. package/src/library.js +399 -0
  87. package/src/markup.js +116 -0
  88. package/src/master.js +214 -0
  89. package/src/mcp/server.js +371 -0
  90. package/src/meeting.js +42 -0
  91. package/src/motion.js +56 -0
  92. package/src/package.js +116 -0
  93. package/src/playground.js +19 -0
  94. package/src/portal-scene.js +20 -0
  95. package/src/preferences.js +61 -0
  96. package/src/protect.js +51 -0
  97. package/src/purpose.js +24 -0
  98. package/src/pytrace.js +1092 -0
  99. package/src/research/duckduckgo.js +73 -0
  100. package/src/research/research.js +202 -0
  101. package/src/research/svg-image.js +17 -0
  102. package/src/research/visuals.js +52 -0
  103. package/src/runtime/api-collection-ui.js +5 -0
  104. package/src/runtime/api-collections.js +28 -0
  105. package/src/runtime/api-core.js +530 -0
  106. package/src/runtime/api-ui.js +966 -0
  107. package/src/runtime/base.css +1774 -0
  108. package/src/runtime/calc.js +139 -0
  109. package/src/runtime/code-lab.js +371 -0
  110. package/src/runtime/decision-lab.js +49 -0
  111. package/src/runtime/diagram.js +341 -0
  112. package/src/runtime/fit.js +268 -0
  113. package/src/runtime/fonts/caveat.css +2 -0
  114. package/src/runtime/fonts/comic-neue.css +3 -0
  115. package/src/runtime/fonts/fredoka.css +2 -0
  116. package/src/runtime/fonts/index.json +10 -0
  117. package/src/runtime/fonts/inter.css +2 -0
  118. package/src/runtime/fonts/patrick-hand.css +2 -0
  119. package/src/runtime/fonts/plus-jakarta-sans.css +2 -0
  120. package/src/runtime/fonts/press-start-2p.css +2 -0
  121. package/src/runtime/fonts/vt323.css +2 -0
  122. package/src/runtime/formula.js +148 -0
  123. package/src/runtime/graph-lab.js +72 -0
  124. package/src/runtime/meeting.css +7 -0
  125. package/src/runtime/meeting.js +10 -0
  126. package/src/runtime/motion.js +15 -0
  127. package/src/runtime/playground.js +11 -0
  128. package/src/runtime/points.js +19 -0
  129. package/src/runtime/portal-scene.css +6 -0
  130. package/src/runtime/portal-scene.js +22 -0
  131. package/src/runtime/runtime.js +827 -0
  132. package/src/runtime/science.js +89 -0
  133. package/src/runtime/skins/aluminio.css +31 -0
  134. package/src/runtime/skins/arcade.css +25 -0
  135. package/src/runtime/skins/aurora.css +17 -0
  136. package/src/runtime/skins/bauhaus.css +19 -0
  137. package/src/runtime/skins/editorial.css +19 -0
  138. package/src/runtime/skins/jornal.css +17 -0
  139. package/src/runtime/skins/manual-noite.css +14 -0
  140. package/src/runtime/skins/manual.css +14 -0
  141. package/src/runtime/skins/noite.css +21 -0
  142. package/src/runtime/skins/oceano.css +17 -0
  143. package/src/runtime/skins/pop.css +23 -0
  144. package/src/runtime/skins/prata.css +18 -0
  145. package/src/runtime/skins/prisma.css +28 -0
  146. package/src/runtime/skins/rabisco.css +18 -0
  147. package/src/runtime/skins/relevo.css +33 -0
  148. package/src/runtime/skins/sinal.css +16 -0
  149. package/src/runtime/skins/terminal.css +23 -0
  150. package/src/runtime/vendor/KATEX-LICENSE.txt +21 -0
  151. package/src/runtime/vendor/MERMAID-LICENSE.txt +21 -0
  152. package/src/runtime/vendor/PLOTLY-LICENSE.txt +21 -0
  153. package/src/runtime/vendor/katex.css +1 -0
  154. package/src/runtime/vendor/mermaid.min.js +3636 -0
  155. package/src/runtime/vendor/plotly.min.js +3620 -0
  156. package/src/runtime/video-player.js +35 -0
  157. package/src/science.js +162 -0
  158. package/src/studio/ai-response.js +36 -0
  159. package/src/studio/api-routes.js +152 -0
  160. package/src/studio/assets/contexto/leia-me.md +12 -0
  161. package/src/studio/assets/contexto/vendas.csv +7 -0
  162. package/src/studio/assets/imagens/colecao-arquitetura.png +0 -0
  163. package/src/studio/assets/imagens/colecao-cor.png +0 -0
  164. package/src/studio/assets/imagens/colecao-equipe.png +0 -0
  165. package/src/studio/assets/imagens/lavanda-cover.jpg +0 -0
  166. package/src/studio/code-routes.js +32 -0
  167. package/src/studio/command-approval.js +48 -0
  168. package/src/studio/demo-decks.js +98 -0
  169. package/src/studio/direction-routes.js +29 -0
  170. package/src/studio/errors.js +65 -0
  171. package/src/studio/exploration-demo.js +17 -0
  172. package/src/studio/exporting.js +105 -0
  173. package/src/studio/generation-report.js +10 -0
  174. package/src/studio/instance.js +48 -0
  175. package/src/studio/layout-samples.js +152 -0
  176. package/src/studio/project.js +371 -0
  177. package/src/studio/public/api-collections.js +28 -0
  178. package/src/studio/public/app.js +5265 -0
  179. package/src/studio/public/art-preview.js +10 -0
  180. package/src/studio/public/calc-fields.js +11 -0
  181. package/src/studio/public/chat-progress.js +47 -0
  182. package/src/studio/public/developer-fields.js +22 -0
  183. package/src/studio/public/explorer.js +443 -0
  184. package/src/studio/public/history.js +58 -0
  185. package/src/studio/public/index.html +867 -0
  186. package/src/studio/public/inspector.js +270 -0
  187. package/src/studio/public/lab-fields.js +5 -0
  188. package/src/studio/public/library.css +275 -0
  189. package/src/studio/public/library.html +55 -0
  190. package/src/studio/public/library.js +471 -0
  191. package/src/studio/public/merge-decks.js +155 -0
  192. package/src/studio/public/review-ui.js +100 -0
  193. package/src/studio/public/screenshot-editor.js +140 -0
  194. package/src/studio/public/share-ui.js +72 -0
  195. package/src/studio/public/slide-clipboard.js +23 -0
  196. package/src/studio/public/slide-form.js +1263 -0
  197. package/src/studio/public/studio-awareness.js +154 -0
  198. package/src/studio/public/studio-next.css +303 -0
  199. package/src/studio/public/style.css +1262 -0
  200. package/src/studio/public/ui-icons.js +2 -0
  201. package/src/studio/public/viewers.js +372 -0
  202. package/src/studio/public/visual-editor.js +326 -0
  203. package/src/studio/public/workspace.js +18 -0
  204. package/src/studio/public-files.js +1 -0
  205. package/src/studio/request-helpers.js +41 -0
  206. package/src/studio/server.js +1583 -0
  207. package/src/studio/share-routes.js +146 -0
  208. package/src/studio/slide-copy-routes.js +26 -0
  209. package/src/studio/slide-copy.js +30 -0
  210. package/src/studio/snapshot.js +160 -0
  211. package/src/studio/style-routes.js +103 -0
  212. package/src/studio/template-collections.js +97 -0
  213. package/src/studio/versions.js +95 -0
  214. package/src/studio/video-download.js +29 -0
  215. package/src/studio/visual-keys.js +52 -0
  216. package/src/studio/workspaces.js +39 -0
  217. package/src/table.js +93 -0
  218. package/src/templates/scaffold.js +372 -0
  219. package/src/themes.js +604 -0
  220. package/src/trace-view.js +450 -0
  221. package/src/uid.js +19 -0
  222. package/src/video-player.js +40 -0
  223. package/src/visual-edits.js +99 -0
  224. package/src/ws.js +134 -0
  225. package/templates/cenario/Texto no cen/303/241rio.yaml" +171 -0
  226. package/templates/cenario/imagens/cidade.svg +30 -0
  227. package/templates/cenario/imagens/pessoa.svg +15 -0
  228. package/templates/cenario-e-ciencia.yaml +47 -0
  229. package/templates/ensaio-api.yaml +160 -0
  230. package/templates/exemplo-alegre.yaml +75 -0
  231. package/templates/exemplo-keynote.yaml +90 -0
  232. package/templates/exemplo-napkin.yaml +139 -0
  233. package/templates/exemplo.yaml +228 -0
@@ -0,0 +1,35 @@
1
+ for(const player of document.querySelectorAll('.video-player')){
2
+ const slide=player.closest('.slide'),video=player.querySelector('video'),frame=player.querySelector('iframe');
3
+ const start=player.querySelector('[data-video-start]'),advance=player.querySelector('[data-video-advance]'),status=player.querySelector('[data-video-status]');
4
+ let active=false,finished=false,frameId;
5
+ const handoff=()=>{
6
+ if(finished||!slide?.classList.contains('current'))return;
7
+ if(player.hasAttribute('data-video-reveal')){finished=true;active=false;video.pause();player.dataset.revealed='true';slide.dataset.videoRevealed='true';return;}
8
+ const index=[...document.querySelectorAll('.slide')].indexOf(slide);
9
+ if(index>=0&&index+1<window.sagadeck.n){finished=true;active=false;video?.pause();window.sagadeck.goto(index+1);}
10
+ };
11
+ if(start){
12
+ start.onclick=async()=>{try{if(video.ended)video.currentTime=0;finished=false;delete player.dataset.revealed;delete slide.dataset.videoRevealed;video.style.opacity='1';await video.play();if(!slide?.classList.contains('current')){video.pause();return;}active=true;start.hidden=true;status.textContent='';if(video.requestVideoFrameCallback)frameId=video.requestVideoFrameCallback(tick);}catch{status.textContent='Não foi possível iniciar o vídeo. Você pode avançar.';}};
13
+ advance.onclick=handoff;
14
+ if(player.hasAttribute('data-video-stage')){
15
+ player.addEventListener('click',event=>{if(event.target.closest('button'))return;event.preventDefault();event.stopPropagation();active?handoff():start.click();});
16
+ document.addEventListener('keydown',event=>{
17
+ if(!slide?.classList.contains('current')||finished||event.target.closest('input,textarea,select,[contenteditable=true]'))return;
18
+ if(![' ','Enter','ArrowRight','PageDown'].includes(event.key))return;
19
+ event.preventDefault();event.stopImmediatePropagation();active?handoff():start.click();
20
+ },true);
21
+ }
22
+ const check=time=>{if(!active)return;const cue=player.dataset.videoHandoff!=null?Number(player.dataset.videoHandoff):video.duration;const fade=Number(player.dataset.videoFade);if(player.hasAttribute('data-video-reveal')&&Number.isFinite(cue)){video.style.opacity=fade>0?String(Math.max(0,Math.min(1,(cue-time)/fade))):'1';if(time>=cue)handoff();}else if(player.hasAttribute('data-video-next')&&player.dataset.videoHandoff!=null&&time>=cue)handoff();};
23
+ const tick=(_now,meta)=>{check(meta.mediaTime);if(active&&!finished)frameId=video.requestVideoFrameCallback(tick);};
24
+ video.addEventListener('timeupdate',()=>{if(!video.requestVideoFrameCallback)check(video.currentTime)});
25
+ video.addEventListener('ended',()=>{active=false;start.hidden=false;if(player.hasAttribute('data-video-next')||player.hasAttribute('data-video-reveal'))handoff();});
26
+ }
27
+ if(frame&&/^https?:$/.test(location.protocol)){
28
+ const url=new URL(frame.src);url.searchParams.set('enablejsapi','1');url.searchParams.set('origin',location.origin);frame.src=url.href;
29
+ }
30
+ if(slide)new MutationObserver(()=>{
31
+ if(slide.classList.contains('current')){if(video?.loop&&!matchMedia('(prefers-reduced-motion: reduce)').matches)video.play().catch(()=>{});return;}
32
+ video?.pause();frame?.contentWindow?.postMessage(JSON.stringify({event:'command',func:'pauseVideo',args:[]}),'https://www.youtube-nocookie.com');
33
+ if(start){active=false;finished=false;video.cancelVideoFrameCallback?.(frameId);start.hidden=false;delete player.dataset.revealed;delete slide.dataset.videoRevealed;video.style.opacity="1";}
34
+ }).observe(slide,{attributes:true,attributeFilter:['class']});
35
+ }
package/src/science.js ADDED
@@ -0,0 +1,162 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import katex from 'katex';
4
+ import { esc } from './markup.js';
5
+ import { parseCSV } from './csv.js';
6
+ import './runtime/formula.js';
7
+
8
+ const F = globalThis.SagaFormula;
9
+
10
+ export function mathHTML(equations = []) {
11
+ if (!Array.isArray(equations) || equations.length > 5) throw new Error('Use até cinco equações por slide.');
12
+ return equations.map((eq,i) => {
13
+ const e = typeof eq === 'string' ? {latex:eq} : eq;
14
+ const html = katex.renderToString(String(e.latex || ''), {displayMode:true,throwOnError:false,strict:"ignore",trust:false,maxExpand:1000,maxSize:20});
15
+ return `<article class="science-equation raster"><span class="science-number">${String(i+1).padStart(2,'0')}</span><div>${e.label ? `<p class="t f-label">${esc(e.label)}</p>` : ''}${html}</div></article>`;
16
+ }).join('');
17
+ }
18
+
19
+ // ---- gráfico do slide "Fórmulas e funções" ----
20
+ // plot: { functions: ["a*sin(b*x)", {fn: "cos(x)", name: "cosseno"}], x: [-10, 10], y: [-3, 3],
21
+ // params: {a: {value: 1, min: 0, max: 5, step: 0.1, label: "amplitude"}},
22
+ // points: "x;y\n1;2\n…" | [[1, 2], …] | "dados/medidas.csv", pointsName: "medições",
23
+ // surface: "sin(sqrt(x^2+y^2))", data: [traces Plotly], layout: {…} }
24
+ // Letra da fórmula que não é x (ou y na superfície) vira controle deslizante (valor 1, de -5 a 5, se não vier em params).
25
+ const PRESETS = { wave: {functions:['sin(x)'], x:[-6.3,6.3]}, parabola: {functions:['x^2'], x:[-4,4]}, surface: {surface:'sin(sqrt(x^2+y^2))', x:[-4,4]} };
26
+ const num = (v, d) => (Number.isFinite(Number(v)) && v !== '' && v != null ? Number(v) : d);
27
+ const range = (r, d) => (Array.isArray(r) && r.length === 2 && r.every((v) => Number.isFinite(Number(v))) && Number(r[0]) < Number(r[1]) ? [Number(r[0]), Number(r[1])] : d);
28
+
29
+ // tabela colada do Excel/CSV (tab, ponto e vírgula ou vírgula) -> linhas de células; vírgula decimal vira ponto
30
+ // tabela colada ou lida de arquivo: separador descoberto (vírgula, ponto e vírgula, tab), aspas respeitadas (src/csv.js)
31
+ export function parseTable(text) {
32
+ return parseCSV(text).rows.filter((r) => r.some((c) => c.trim())).map((r) => r.map((c) => c.trim()));
33
+ }
34
+ // número de planilha: 40% · R$ 10 · 1.234,5 · -3
35
+ const toNum = (c) => { const s = String(c ?? '').trim().replace(/^R\$/i, '').replace(/%$/, '').replace(/\s/g, ''); const n = Number(/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(s) ? s.replace(/\./g, '').replace(',', '.') : s.replace(',', '.')); return s !== '' && Number.isFinite(n) ? n : null; };
36
+ export { toNum };
37
+
38
+ function readPoints(points, ctx) {
39
+ if (points == null || points === '') return null;
40
+ let rows;
41
+ if (Array.isArray(points)) rows = points.map((p) => (Array.isArray(p) ? p : [p.x, p.y]));
42
+ else {
43
+ let text = String(points);
44
+ if (/\.(csv|tsv|txt)$/i.test(text.trim()) && !text.includes('\n')) {
45
+ const file = path.resolve(ctx?.baseDir || process.cwd(), text.trim());
46
+ try { text = fs.readFileSync(file, 'utf8'); }
47
+ catch { ctx?.warnings?.push(`pontos do gráfico: "${points}" não encontrado ao lado do deck`); return null; }
48
+ }
49
+ rows = parseTable(text);
50
+ }
51
+ const pts = rows.map((r) => [toNum(r[0]), toNum(r[1])]).filter(([x, y]) => x != null && y != null);
52
+ const head = rows[0] && toNum(rows[0][1]) == null ? String(rows[0][1] || '') : '';
53
+ return pts.length ? { x: pts.map((p) => p[0]), y: pts.map((p) => p[1]), head } : null;
54
+ }
55
+
56
+ // o que a apresentação precisa para desenhar (e redesenhar quando um controle deslizante muda)
57
+ export function plotModel(plot = {}, ctx) {
58
+ const p = { ...(PRESETS[plot.preset] || {}), ...plot };
59
+ if (Array.isArray(p.data) && p.data.length) return { kind: 'plotly', data: p.data, layout: p.layout || {} };
60
+ const errors = [];
61
+ const fns = [].concat(p.functions ?? p.function ?? []).map((f) => (typeof f === 'string' ? { fn: f } : f || {})).filter((f) => String(f.fn ?? '').trim());
62
+ const surface = String(p.surface ?? '').trim();
63
+ const names = new Set();
64
+ const compiled = fns.map((f, i) => {
65
+ try { const c = F.compile(f.fn, ['x']); c.params.forEach((n) => names.add(n)); return c; }
66
+ catch (e) { errors.push(`fórmula ${i + 1} ("${f.fn}"): ${e.message}`); return null; }
67
+ });
68
+ let surf = null;
69
+ if (surface) {
70
+ try { surf = F.compile(surface, ['x', 'y']); surf.params.forEach((n) => names.add(n)); }
71
+ catch (e) { errors.push(`superfície ("${surface}"): ${e.message}`); }
72
+ }
73
+ const given = p.params && typeof p.params === 'object' ? p.params : {};
74
+ const params = {};
75
+ for (const n of [...names]) {
76
+ const g = typeof given[n] === 'number' ? { value: given[n] } : given[n] || {};
77
+ const value = num(g.value, 1);
78
+ params[n] = { value, min: num(g.min, Math.min(-5, value)), max: num(g.max, Math.max(5, value)), step: num(g.step, 0.1), ...(g.label ? { label: String(g.label) } : {}) };
79
+ }
80
+ const points = readPoints(p.points, ctx);
81
+ return {
82
+ kind: 'formula', errors,
83
+ functions: fns.map((f, i) => ({ fn: String(f.fn), name: String(f.name || `y = ${f.fn}`), ...(f.color ? { color: String(f.color) } : {}), ok: !!compiled[i] })),
84
+ surface: surf ? surface : '',
85
+ x: range(p.x, [-10, 10]), ...(range(p.y, null) ? { y: range(p.y, null) } : {}), ...(p.xlog ? { xlog: true } : {}), ...(p.ylog ? { ylog: true } : {}),
86
+ params, ...(points ? { points: { x: points.x, y: points.y, name: String(p.pointsName || points.head || 'dados') } } : {}),
87
+ layout: p.layout || {}, _compiled: compiled, _surf: surf,
88
+ };
89
+ }
90
+
91
+ // traces do Plotly de um modelo (a apresentação usa a mesma conta em src/runtime/science.js)
92
+ export function plotData(plot = {}, ctx) {
93
+ const m = plotModel(plot, ctx);
94
+ if (m.kind === 'plotly') return m.data;
95
+ const vals = Object.fromEntries(Object.entries(m.params).map(([k, v]) => [k, v.value]));
96
+ const out = m._compiled.map((c, i) => c && { type: 'scatter', mode: 'lines', name: m.functions[i].name, ...F.sample(c, m.x, vals, 400, m.xlog) }).filter(Boolean);
97
+ if (m.points) out.push({ type: 'scatter', mode: 'markers', name: m.points.name, x: m.points.x, y: m.points.y });
98
+ return out;
99
+ }
100
+
101
+ // prévia desenhada no próprio slide: aparece na miniatura, na galeria, no PDF e até o Plotly carregar
102
+ function previewSVG(m) {
103
+ const W = 800, H = 480, L = 50, R = 20, T = 20, B = 40;
104
+ const vals = Object.fromEntries(Object.entries(m.params).map(([k, v]) => [k, v.value]));
105
+ if (m._surf) {
106
+ const n = 22, cells = [];
107
+ const zs = [];
108
+ for (let j = 0; j < n; j++) for (let i = 0; i < n; i++) {
109
+ const x = m.x[0] + ((m.x[1] - m.x[0]) * i) / (n - 1), y = m.x[0] + ((m.x[1] - m.x[0]) * j) / (n - 1);
110
+ zs.push(m._surf.eval({ ...vals, x, y }));
111
+ }
112
+ const fin = zs.filter((z) => z != null), lo = Math.min(...fin), hi = Math.max(...fin);
113
+ zs.forEach((z, k) => {
114
+ const t = z == null ? 0 : (z - lo) / ((hi - lo) || 1);
115
+ cells.push(`<rect x="${L + ((k % n) * (W - L - R)) / n}" y="${T + (Math.floor(k / n) * (H - T - B)) / n}" width="${(W - L - R) / n + 0.5}" height="${(H - T - B) / n + 0.5}" style="fill:color-mix(in srgb,var(--s1) ${Math.round(t * 100)}%,var(--s2))"/>`);
116
+ });
117
+ return `<svg class="science-preview" viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" aria-hidden="true">${cells.join('')}</svg>`;
118
+ }
119
+ const series = m._compiled.map((c, i) => c && { i, ...F.sample(c, m.x, vals, 160, m.xlog) }).filter(Boolean);
120
+ const ys = [...series.flatMap((s) => s.y), ...(m.points?.y || [])].filter((v) => v != null).sort((a, b) => a - b);
121
+ let [y0, y1] = m.y || (ys.length ? [ys[Math.floor(ys.length * 0.02)], ys[Math.ceil(ys.length * 0.98) - 1]] : [-1, 1]);
122
+ if (y0 === y1) { y0 -= 1; y1 += 1; }
123
+ const pad = m.y ? 0 : (y1 - y0) * 0.08; y0 -= pad; y1 += pad;
124
+ const lg = (v) => Math.log10(Math.max(v, 1e-12));
125
+ if (m.ylog) { const pos = ys.filter((v) => v > 0); if (pos.length) { y0 = Math.min(...pos); y1 = Math.max(...pos); } }
126
+ const X = (x) => L + (m.xlog ? (lg(x) - lg(m.x[0])) / (lg(m.x[1]) - lg(m.x[0])) : (x - m.x[0]) / (m.x[1] - m.x[0])) * (W - L - R);
127
+ const Y = (y) => T + (1 - (m.ylog ? (lg(y) - lg(y0)) / ((lg(y1) - lg(y0)) || 1) : (y - y0) / (y1 - y0))) * (H - T - B);
128
+ let g = `<line x1="${L}" x2="${W - R}" y1="${H - B}" y2="${H - B}" class="sp-axis"/><line x1="${L}" x2="${L}" y1="${T}" y2="${H - B}" class="sp-axis"/>`;
129
+ if (y0 < 0 && y1 > 0) g += `<line x1="${L}" x2="${W - R}" y1="${Y(0)}" y2="${Y(0)}" class="sp-zero"/>`;
130
+ if (m.x[0] < 0 && m.x[1] > 0) g += `<line x1="${X(0)}" x2="${X(0)}" y1="${T}" y2="${H - B}" class="sp-zero"/>`;
131
+ series.forEach((s, k) => {
132
+ let d = '', pen = false;
133
+ s.x.forEach((x, i) => { const y = s.y[i]; if (y == null || y < y0 - (y1 - y0) || y > y1 + (y1 - y0)) { pen = false; return; } d += `${pen ? 'L' : 'M'}${X(x).toFixed(1)} ${Y(y).toFixed(1)}`; pen = true; });
134
+ const color = m.functions[s.i].color ? `var(--${m.functions[s.i].color})` : `var(--s${(k % 5) + 1})`;
135
+ g += `<path d="${d}" fill="none" style="stroke:${color}" stroke-width="5" stroke-linejoin="round" stroke-linecap="round"/>`;
136
+ });
137
+ (m.points?.x || []).forEach((x, i) => { g += `<circle cx="${X(x).toFixed(1)}" cy="${Y(m.points.y[i]).toFixed(1)}" r="6" style="fill:var(--s${(series.length % 5) + 1})"/>`; });
138
+ const lab = (v) => (Math.abs(v) >= 100 ? Math.round(v) : +v.toFixed(2)).toLocaleString('pt-BR');
139
+ g += `<text x="${L}" y="${H - 12}" class="sp-tick">${lab(m.x[0])}</text><text x="${W - R}" y="${H - 12}" text-anchor="end" class="sp-tick">${lab(m.x[1])}</text>`;
140
+ g += `<text x="${L - 8}" y="${T + 12}" text-anchor="end" class="sp-tick">${lab(y1)}</text><text x="${L - 8}" y="${H - B}" text-anchor="end" class="sp-tick">${lab(y0)}</text>`;
141
+ return `<svg class="science-preview" viewBox="0 0 ${W} ${H}" preserveAspectRatio="xMidYMid meet" aria-hidden="true">${g}</svg>`;
142
+ }
143
+
144
+ function slidersHTML(params) {
145
+ const ks = Object.keys(params);
146
+ if (!ks.length) return '';
147
+ return `<div class="science-params">${ks.map((k) => {
148
+ const p = params[k];
149
+ return `<label class="science-param"><span class="sp-name f-label">${esc(p.label ? `${p.label} (${k})` : k)}</span><input type="range" data-param="${esc(k)}" min="${p.min}" max="${p.max}" step="${p.step}" value="${p.value}" aria-label="${esc(p.label || k)}"><output class="sp-val">${esc(String(+p.value.toFixed(3)).replace('.', ','))}</output></label>`;
150
+ }).join('')}</div>`;
151
+ }
152
+
153
+ export function plotHTML(plot = {}, ctx) {
154
+ const m = plotModel(plot, ctx);
155
+ if (m.kind === 'formula' && m.errors.length) ctx?.warnings?.push(...m.errors.map((e) => `gráfico: ${e}`));
156
+ const { _compiled, _surf, ...pub } = m;
157
+ const payload = JSON.stringify(pub).replace(/</g, '\\u003c');
158
+ const empty = m.kind === 'formula' && !m.functions.length && !m.surface && !m.points;
159
+ const help = m.kind === 'formula' && m.errors.length ? `Fórmula com erro: ${m.errors[0]}` : empty ? 'Escreva uma fórmula, como a*sin(b*x), ou cole pontos (x e y)' : 'Arraste para explorar · passe o mouse para ver valores · duplo clique para restaurar';
160
+ const preview = m.kind === 'formula' && !empty ? previewSVG(m) : '';
161
+ return `<div class="science-plot raster${m.errors?.length ? ' has-error' : ''}" aria-label="Gráfico interativo"><div class="science-plot-target">${preview}</div><script type="application/json" class="science-data">${payload}</script>${m.kind === 'formula' ? slidersHTML(m.params) : ''}<p class="science-help f-label">${esc(help)}</p></div>`;
162
+ }
@@ -0,0 +1,36 @@
1
+ import {errorDiagnostic} from './errors.js';
2
+
3
+ export function cancellable(work, signal) {
4
+ return new Promise((resolve, reject) => {
5
+ const abort = () => reject(Object.assign(new Error('Parado a pedido.'),{aborted:true}));
6
+ signal.addEventListener('abort', abort, { once: true });
7
+ Promise.resolve().then(() => { signal.throwIfAborted(); return work(); }).then(resolve, reject).finally(() => signal.removeEventListener('abort', abort));
8
+ });
9
+ }
10
+ export async function respond(res, stream, work) {
11
+ if (!stream) {
12
+ try {
13
+ const data = await work(() => {});
14
+ res.writeHead(200, { "Content-Type": "application/json" });
15
+ res.end(JSON.stringify(data));
16
+ } catch (e) {
17
+ res.writeHead(500, { "Content-Type": "application/json" });
18
+ res.end(JSON.stringify(errorDiagnostic(e)));
19
+ }
20
+ return;
21
+ }
22
+ res.writeHead(200, { "Content-Type": "application/x-ndjson; charset=utf-8", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" });
23
+ const send = (obj) => res.write(JSON.stringify(obj) + "\n");
24
+ const started = Date.now();
25
+ const heartbeat = setInterval(() => send({ type: "tick", elapsed: Date.now() - started }), 1000);
26
+ try {
27
+ const data = await work((ev) => send({ type: "progress", elapsed: Date.now() - started, ...ev }));
28
+ send({ type: "result", data });
29
+ } catch (e) {
30
+ console.error("[Studio] tarefa de IA falhou:", e.message);
31
+ send({ type: "error", ...errorDiagnostic(e) });
32
+ } finally {
33
+ clearInterval(heartbeat);
34
+ res.end();
35
+ }
36
+ }
@@ -0,0 +1,152 @@
1
+ // Rotas do slide API (/api/http/*): ambientes e variáveis (com segredos protegidos), envio do pedido, tempo real
2
+ // (WebSocket), streaming e gravação da última resposta. Saiu de server.js; o servidor passa o estado (ctx).
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import crypto from "node:crypto";
7
+ import { spawn } from "node:child_process";
8
+ import YAML from "yaml";
9
+ import { writeRecording, readRecordings, mimeOf } from "../api-client.js";
10
+
11
+ // Modelo do ~/.sagadeck/ambientes.yaml, para quem ainda não tem um (tela Ambientes do Studio).
12
+ const AMBIENTES_MODELO = `# Ambientes do slide "api": endereços e credenciais ficam aqui, na sua máquina, nunca no deck.
13
+ # Nos slides, {{base}} vira o valor de vars.base do ambiente escolhido (o selo DEV/HOM no slide troca).
14
+ # Segredo nunca vai em vars: use secrets (e, melhor ainda, uma variável de ambiente com env:).
15
+ current: dev
16
+ environments:
17
+ dev:
18
+ vars: { base: "https://api-dev.exemplo.com/v1" }
19
+ # token: # token que expira (client credentials), renovado sozinho
20
+ # url: "https://identidade-dev.exemplo.com/token"
21
+ # client_id: "meu-id"
22
+ # client_secret_env: MINHA_SECRET # nome da variável de ambiente com o segredo
23
+ # field: "$.access_token"
24
+ # secrets: { client_secret: { env: MINHA_SECRET } } # {{secret.client_secret}} nos slides
25
+ # OpenRouter: o ambiente OPENROUTER embutido usa OPENROUTER_API_KEY para o exemplo gratuito NVIDIA Nemotron.
26
+ # ca: "C:/certs/empresa.pem" # certificado da empresa (inspeção TLS)
27
+ hom:
28
+ vars: { base: "https://api-hom.exemplo.com/v1" }
29
+ `;
30
+
31
+ export async function apiRoutes({ req, res, pathname, url, W, apiEnv, rtSessions, apiBlocked, ensureEnsaio, readJSON, isBundledTemplate }) {
32
+ const reply = (code, obj) => { res.writeHead(code, { "Content-Type": "application/json; charset=utf-8" }); res.end(JSON.stringify(obj)); };
33
+ const blocked = apiBlocked(req);
34
+ if (!blocked) await ensureEnsaio();
35
+ if (pathname === "/api/http/state" && req.method === "GET") {
36
+ let st;
37
+ try { st = apiEnv.state(); } catch (e) { st = { error: e.message, envs: [] }; }
38
+ const tokens = Object.fromEntries((st.envs || []).map((e) => [e.name, apiEnv.tokenInfo(e.name)]));
39
+ return reply(200, { live: !blocked, reason: blocked?.message || null, ...(blocked ? { envs: [], current: null } : st), tokens, recordings: { ...readRecordings(W.file), ...(W.apiRecordings || {}) } });
40
+ }
41
+ if (blocked) return reply(blocked.code, { error: blocked.message, live: false });
42
+ // conversa em tempo real: os eventos do serviço chegam ao navegador por aqui (SSE)
43
+ if (pathname === "/api/http/rt/events" && req.method === "GET") {
44
+ const s = rtSessions.get(url.searchParams.get("sid") || "");
45
+ if (!s) return reply(404, { error: "conversa não existe (já terminou?)" });
46
+ res.writeHead(200, { "Content-Type": "text/event-stream; charset=utf-8", "Cache-Control": "no-cache", "X-Accel-Buffering": "no" });
47
+ s.res = res;
48
+ for (const ev of s.buffer.splice(0)) res.write(`data: ${JSON.stringify(ev)}\n\n`);
49
+ req.on("close", () => { if (s.res === res) { s.conn.close(); rtSessions.delete(s.sid); } });
50
+ return;
51
+ }
52
+ // tela Ambientes: o arquivo como texto (ou um modelo comentado, se ainda não existe)
53
+ if (pathname === "/api/http/ambientes" && req.method === "GET") {
54
+ const exists = fs.existsSync(apiEnv.file);
55
+ return reply(200, { file: apiEnv.file, exists, text: exists ? fs.readFileSync(apiEnv.file, "utf8") : AMBIENTES_MODELO, builtin: Object.keys(apiEnv.builtin) });
56
+ }
57
+ if (req.method !== "POST") return reply(405, { error: "use POST" });
58
+ if (!/^application\/json/i.test(req.headers["content-type"] || "")) return reply(415, { error: "envie JSON" });
59
+ let body;
60
+ try { body = await readJSON(req); } catch (e) { return reply(400, { error: e.message }); }
61
+ if (pathname === "/api/http/ambientes" || pathname === "/api/http/ambientes/validar") {
62
+ const text = String(body.text ?? "");
63
+ let data;
64
+ try { data = YAML.parse(text) || {}; } catch (e) { return reply(400, { error: `YAML inválido: ${e.message}` }); }
65
+ if (typeof data !== "object" || Array.isArray(data)) return reply(400, { error: "O arquivo precisa ter current: e environments:" });
66
+ const envs = data.environments;
67
+ if (envs != null && (typeof envs !== "object" || Array.isArray(envs))) return reply(400, { error: "environments precisa ser uma lista de nomes (dev:, hom:…), não uma lista com traços" });
68
+ for (const [name, e] of Object.entries(envs || {})) {
69
+ if (!e || typeof e !== "object") return reply(400, { error: `O ambiente "${name}" está vazio: ponha pelo menos vars: { base: "…" }` });
70
+ if (e.vars != null && (typeof e.vars !== "object" || Array.isArray(e.vars))) return reply(400, { error: `vars do ambiente "${name}" precisa ser nome: valor` });
71
+ if (e.secrets != null && (typeof e.secrets !== "object" || Array.isArray(e.secrets))) return reply(400, { error: `secrets do ambiente "${name}" precisa ser um mapa de nomes para configurações de segredos` });
72
+ }
73
+ if (data.current != null && (typeof data.current !== "string" || (!Object.hasOwn(envs || {}, data.current) && !Object.hasOwn(apiEnv.builtin, data.current)))) {
74
+ return reply(400, { error: `O ambiente current "${String(data.current)}" não está definido em environments nem é um ambiente embutido.` });
75
+ }
76
+ if (pathname.endsWith("/validar")) return reply(200, { valid: true });
77
+ const backupCreated = fs.existsSync(apiEnv.file);
78
+ if (backupCreated) {
79
+ fs.copyFileSync(apiEnv.file, `${apiEnv.file}.bak`);
80
+ fs.chmodSync(`${apiEnv.file}.bak`, 0o600);
81
+ }
82
+ fs.mkdirSync(path.dirname(apiEnv.file), { recursive: true });
83
+ fs.writeFileSync(apiEnv.file, text.endsWith("\n") ? text : text + "\n", "utf8");
84
+ apiEnv.tokens.clear(); // credencial pode ter mudado: o próximo pedido pega um token novo
85
+ return reply(200, { ...apiEnv.state(), backupCreated });
86
+ }
87
+ // o arquivo do slide: o que foi arrastado na hora, ou o padrão (file:), só de dentro da pasta do deck
88
+ const fileOf = (b) => {
89
+ if (b.file && b.file.base64) return { name: String(b.file.name || "arquivo"), type: String(b.file.type || mimeOf(b.file.name)), data: Buffer.from(b.file.base64, "base64") };
90
+ if (!b.fileRef) return null;
91
+ const dir = W.file ? path.dirname(W.file) : null;
92
+ if (!dir) throw Object.assign(new Error("Salve o deck numa pasta para usar um arquivo padrão (file:)."), { kind: "config" });
93
+ const abs = path.resolve(dir, String(b.fileRef));
94
+ if (!abs.startsWith(dir + path.sep)) throw Object.assign(new Error("O arquivo precisa estar dentro da pasta do deck."), { kind: "config" });
95
+ if (!fs.existsSync(abs)) throw Object.assign(new Error(`Não achei ${b.fileRef} na pasta do deck (${dir}).`), { kind: "config" });
96
+ return { name: path.basename(abs), type: mimeOf(abs), data: fs.readFileSync(abs) };
97
+ };
98
+ try {
99
+ if (pathname === "/api/http/env") return reply(200, apiEnv.use(String(body.name || "")));
100
+ // painel Variáveis: criar/editar (normal ou protegida), revelar pelo olhinho, apagar, abrir a pasta do arquivo
101
+ if (pathname === "/api/http/vars/set") return reply(200, apiEnv.setVar(String(body.name || ""), body.value ?? "", { protected: !!body.protected }));
102
+ if (pathname === "/api/http/vars/reveal") return reply(200, { value: apiEnv.reveal(String(body.name || "")) });
103
+ if (pathname === "/api/http/vars/delete") return reply(200, apiEnv.deleteVar(String(body.name || "")));
104
+ if (pathname === "/api/http/vars/folder") {
105
+ const dir = path.dirname(apiEnv.file);
106
+ fs.mkdirSync(dir, { recursive: true });
107
+ if (process.platform === "win32") spawn("explorer.exe", [dir], { detached: true, stdio: "ignore" }).unref();
108
+ return reply(200, { dir });
109
+ }
110
+ if (pathname === "/api/http/send") return reply(200, await apiEnv.send({ ...(body.request || {}), file: fileOf(body) }));
111
+ if (pathname === "/api/http/rt/open") {
112
+ const r = await apiEnv.openRealtime(body.realtime || {});
113
+ const sid = crypto.randomUUID();
114
+ const s = { sid, conn: r.conn, buffer: [], res: null };
115
+ const push = (ev) => { if (s.res) s.res.write(`data: ${JSON.stringify(ev)}\n\n`); else s.buffer.push(ev); };
116
+ r.conn.on("message", (m, isText) => push(isText ? { dir: "in", text: r.mask(m) } : { dir: "in", bin: m.toString("base64") }));
117
+ r.conn.on("close", (code, why) => { push({ type: "close", code, why: String(why || "") }); if (s.res) s.res.end(); rtSessions.delete(sid); });
118
+ rtSessions.set(sid, s);
119
+ for (const m of [].concat(body.open || [])) r.conn.send(typeof m === "string" ? m : JSON.stringify(m));
120
+ return reply(200, { sid, url: r.url, env: r.env });
121
+ }
122
+ if (pathname === "/api/http/rt/send") {
123
+ const s = rtSessions.get(String(body.sid || ""));
124
+ if (!s) return reply(404, { error: "conversa não existe (já terminou?)" });
125
+ for (const m of [].concat(body.messages || [])) s.conn.send(typeof m === "string" ? m : JSON.stringify(m));
126
+ return reply(200, { ok: true });
127
+ }
128
+ if (pathname === "/api/http/rt/close") {
129
+ const s = rtSessions.get(String(body.sid || ""));
130
+ if (s) { s.conn.close(); rtSessions.delete(s.sid); }
131
+ return reply(200, { ok: true });
132
+ }
133
+ if (pathname === "/api/http/record") {
134
+ if (!body.key || !body.record) return reply(400, { error: "faltou key/record" });
135
+ const f = W.file && !isBundledTemplate(W.file) ? writeRecording(W.file, String(body.key), body.record) : null;
136
+ if (!f) (W.apiRecordings = W.apiRecordings || {})[body.key] = { ...body.record, at: new Date().toISOString() };
137
+ return reply(200, { ok: true, file: f });
138
+ }
139
+ if (pathname === "/api/http/stream") {
140
+ const up = await apiEnv.open({ ...(body.request || {}), file: fileOf(body) });
141
+ res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-cache", "X-Api-Status": String(up.res.statusCode), "X-Api-Type": up.res.headers["content-type"] || "" });
142
+ up.res.on("data", (c) => res.write(up.mask(c.toString("utf8"))));
143
+ up.res.on("end", () => res.end());
144
+ up.res.on("error", () => res.end());
145
+ req.on("close", () => up.res.destroy());
146
+ return;
147
+ }
148
+ } catch (e) {
149
+ return reply(e.kind === "config" ? 400 : 502, { error: e.message, kind: e.kind || "error" });
150
+ }
151
+ return reply(404, { error: "não existe" });
152
+ }
@@ -0,0 +1,12 @@
1
+ # Novidades do SagaDeck
2
+
3
+ Este projeto é a pasta da apresentação. Tudo o que está em `contexto/` vai como material para a IA.
4
+
5
+ ## Para experimentar
6
+
7
+ - Abra `vendas.csv` aqui na aba **Arquivos**: o Studio mostra o tipo de cada coluna e sugere gráficos. **Inserir no slide** cria o gráfico ligado à planilha.
8
+ - Mude um número em `vendas.csv` e use **Atualizar da planilha** no slide do gráfico.
9
+ - Cole um print com **Ctrl+V** na aba Arquivos: ele vai para `contexto/`.
10
+ - Abra o `.yaml` da apresentação: o slide ao lado muda enquanto você digita.
11
+ - No slide de fórmulas, troque `a*sin(b*x)` por outra fórmula no Formatar.
12
+ - No gráfico de dados, cole uma tabela do Excel direto na planilha do Formatar.
@@ -0,0 +1,7 @@
1
+ Mês;Norte;Sul;Nordeste
2
+ jan;10,5;7;6
3
+ fev;12;9;7,5
4
+ mar;15;8;9
5
+ abr;17;11;10
6
+ mai;16;13;12
7
+ jun;19;14;13,5
@@ -0,0 +1,32 @@
1
+ import path from 'node:path';
2
+ import { runCommand, logCommand } from '../ai/commands.js';
3
+ import { downloadYoutube } from './video-download.js';
4
+ import { pollVideoJobs } from '../ai/video-generation.js';
5
+
6
+ // O botão Executar é a autorização do apresentador. Mesmas permissões do chat.
7
+ export async function codeRoutes({ req, res, pathname, W, readJSON, commandsAllowed, commandEnv, isBundledTemplate }) {
8
+ if (!['/api/code/state','/api/code/run','/api/video/download','/api/video/jobs'].includes(pathname)) return false;
9
+ const send = (status, data) => { res.writeHead(status, {'Content-Type':'application/json'});res.end(JSON.stringify(data)); };
10
+ const allowed = commandsAllowed(req, W) && !!W.file && !isBundledTemplate(W.file);
11
+ // vigia de clipes: leitura + download do que já foi pago; nunca pede aprovação
12
+ if (pathname === '/api/video/jobs' && req.method === 'GET') {
13
+ if (!allowed) { send(200, { jobs: [] }); return true; }
14
+ try { send(200, { jobs: await pollVideoJobs(path.dirname(W.file)) }); }
15
+ catch (e) { send(200, { jobs: [], error: e.message }); }
16
+ return true;
17
+ }
18
+ if (pathname.endsWith('/state') && req.method === 'GET') {send(200,{allowed, engines:['python','javascript']});return true;}
19
+ if (req.method !== 'POST') {send(405,{error:'Use POST para executar.'});return true;}
20
+ if (!allowed) {send(403,{error:'Execução nativa indisponível neste workspace.'});return true;}
21
+ try {
22
+ const b = await readJSON(req);
23
+ if (pathname === '/api/video/download') {send(200,await downloadYoutube(b.url,path.dirname(W.file)));return true;}
24
+ if (!['python','javascript'].includes(b.language)) throw new Error('Escolha Python ou JavaScript.');
25
+ const {env,mask} = await commandEnv(W);
26
+ const started = Date.now();
27
+ const result = await runCommand({language:b.language,code:b.code,why:'Execução no laboratório da apresentação'}, {cwd:path.dirname(W.file),env,mask});
28
+ logCommand({language:b.language,code:b.code,output:result.stdout+'\n'+result.stderr,exitCode:result.exitCode,source:'codelab'},{mask});
29
+ send(200,{...result,elapsedMs:Date.now()-started});
30
+ } catch (e) {send(400,{error:e.message});}
31
+ return true;
32
+ }
@@ -0,0 +1,48 @@
1
+ // Política de aprovação dos comandos da IA (quem pede o quê, e o que roda direto).
2
+ // Ações gratuitas (ex.: consultar status do vídeo) nunca pedem; só o que pode
3
+ // custar tempo sensível, gastar dinheiro ou mexer fora da pasta pede clique.
4
+ import crypto from "node:crypto";
5
+ import { videoFreeRun, envName } from "../ai/commands.js";
6
+
7
+ export const needsApproval = ({ autoRun, command }) => !autoRun && !videoFreeRun(command);
8
+
9
+ export const approvalLabel = ({ autoRun, command }) =>
10
+ autoRun ? "liberado" : videoFreeRun(command) ? "livre" : "aprovado";
11
+
12
+ // variáveis, segredos e token do ambiente ativo para o comando (a IA só conhece
13
+ // os nomes; a saída volta mascarada)
14
+ export async function commandEnv(W, apiEnv) {
15
+ try {
16
+ const e = apiEnv.env(), env = {};
17
+ for (const [k, v] of Object.entries({ ...(W.apiVars || {}), ...(e.vars || {}) })) if (v != null && typeof v !== "object") env[`SAGA_VAR_${envName(k)}`] = String(v);
18
+ for (const [k, v] of Object.entries(apiEnv.secretVars(e))) env[`SAGA_SECRET_${envName(k.replace(/^secret\./, ""))}`] = v;
19
+ try { const t = await apiEnv.token(e); if (t) env.SAGA_TOKEN = String(t); } catch {}
20
+ return { env, mask: (s) => apiEnv.maskText(e, s) };
21
+ } catch { return { env: {}, mask: (s) => s }; }
22
+ }
23
+
24
+ // preço estimado de um submit de vídeo (catálogo é de graça; null se não der)
25
+ export async function estimateSubmitCost(command, { commandEnv, W }) {
26
+ try {
27
+ const req = typeof command.code === "object" && command.code !== null ? command.code : JSON.parse(String(command.code || "{}"));
28
+ if (String(command.language || "").toLowerCase() !== "video" || req?.action !== "submit") return null;
29
+ const { estimateVideoCost } = await import("../ai/video-generation.js");
30
+ const { env } = await commandEnv(W).catch(() => ({ env: {} }));
31
+ const key = env.SAGADECK_VIDEO_KEY || env.OPENROUTER_API_KEY || process.env.SAGADECK_VIDEO_KEY || process.env.OPENROUTER_API_KEY;
32
+ return await estimateVideoCost({ model: req.model, duration: req.duration, resolution: req.resolution }, { key });
33
+ } catch { return null; }
34
+ }
35
+
36
+ // espera a pessoa no chat (10 min sem resposta = recusado); aborta junto com o pedido.
37
+ // cost (opcional): texto de custo estimado exibido junto ao pedido ("~$4.00").
38
+ export async function awaitApproval({ command, cost, user, signal, emit, approvals }) {
39
+ const id = crypto.randomUUID();
40
+ const decision = await new Promise((resolve) => {
41
+ const timer = setTimeout(() => { approvals.delete(id); resolve("deny"); }, 10 * 60 * 1000);
42
+ const abort = () => { clearTimeout(timer); approvals.delete(id); resolve("deny"); };
43
+ signal?.addEventListener("abort", abort, { once: true });
44
+ approvals.set(id, { user: user || "", answer: (d) => { clearTimeout(timer); approvals.delete(id); resolve(d); } });
45
+ emit({ phase: "approve", id, command, cost: cost || null, text: "Esperando você autorizar o comando…" });
46
+ });
47
+ return decision;
48
+ }