sagadeck 1.8.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/README.md +25 -8
  2. package/SKILL.md +10 -3
  3. package/bin/sagadeck.js +23 -0
  4. package/docs/ANDAMENTO.md +482 -7
  5. package/docs/PLANO-MELHORIAS.md +180 -0
  6. package/docs/REFERENCIA.md +178 -111
  7. package/package.json +35 -4
  8. package/src/ai/commands.js +2 -2
  9. package/src/ai/context.js +7 -2
  10. package/src/ai/deck-ai.js +4 -2
  11. package/src/ai/ia-check.js +127 -0
  12. package/src/ai/ia-config.js +2 -2
  13. package/src/ai/llm.js +5 -1
  14. package/src/ai/map-analysis-run.js +1 -1
  15. package/src/ai/quality.js +1 -1
  16. package/src/ai/video-generation.js +70 -25
  17. package/src/api-client.js +42 -10
  18. package/src/build.js +46 -14
  19. package/src/code-highlight.js +19 -2
  20. package/src/code-language.js +13 -0
  21. package/src/elements.js +39 -1
  22. package/src/export/pptx.js +7 -2
  23. package/src/export/shots.js +7 -2
  24. package/src/fiscal/normalize.js +19 -0
  25. package/src/fs-retry.js +37 -2
  26. package/src/import/office.js +5 -4
  27. package/src/layouts.js +104 -99
  28. package/src/lessons.js +1 -1
  29. package/src/library.js +7 -7
  30. package/src/map-analysis.js +4 -1
  31. package/src/map-config.js +10 -4
  32. package/src/map.js +21 -8
  33. package/src/mcp/server.js +17 -0
  34. package/src/motion.js +20 -6
  35. package/src/purpose.js +1 -1
  36. package/src/runtime/api-core.js +122 -11
  37. package/src/runtime/api-ui.js +2 -1
  38. package/src/runtime/base.css +88 -332
  39. package/src/runtime/fit.js +3 -2
  40. package/src/runtime/fonts/amatic-sc.css +3 -0
  41. package/src/runtime/fonts/bebas-neue.css +2 -0
  42. package/src/runtime/fonts/cabin-sketch.css +2 -0
  43. package/src/runtime/fonts/fraunces.css +3 -0
  44. package/src/runtime/fonts/gelasio.css +3 -0
  45. package/src/runtime/fonts/ibm-plex-mono.css +4 -0
  46. package/src/runtime/fonts/index.json +10 -1
  47. package/src/runtime/fonts/itim.css +2 -0
  48. package/src/runtime/fonts/montserrat.css +2 -0
  49. package/src/runtime/fonts/space-grotesk.css +2 -0
  50. package/src/runtime/map-background.js +91 -0
  51. package/src/runtime/map.js +41 -9
  52. package/src/runtime/runtime.js +81 -100
  53. package/src/runtime/skins/aurora-dev.css +39 -0
  54. package/src/runtime/skins/caderno.css +33 -0
  55. package/src/runtime/skins/cartaz.css +27 -0
  56. package/src/runtime/skins/infografico.css +29 -0
  57. package/src/runtime/skins/lousa.css +22 -0
  58. package/src/runtime/skins/musgo.css +42 -0
  59. package/src/runtime/skins/terminal-pro.css +33 -0
  60. package/src/runtime/vendor/MAPLIBRE-LEAFLET-LICENSE.txt +15 -0
  61. package/src/runtime/vendor/MAPLIBRE-LICENSE.txt +116 -0
  62. package/src/runtime/vendor/geo-contornos.json +1 -1
  63. package/src/runtime/vendor/maplibre-gl.css +1 -0
  64. package/src/runtime/vendor/maplibre-gl.js +825 -0
  65. package/src/runtime/vendor/maplibre-leaflet.js +234 -0
  66. package/src/runtime/vendor/maplibre-worker.js +10 -0
  67. package/src/studio/ai-chat-route.js +26 -7
  68. package/src/studio/assets/imagens/tour-mapa-botao.jpg +0 -0
  69. package/src/studio/assets/imagens/tour-mapa-editor.jpg +0 -0
  70. package/src/studio/command-approval.js +2 -3
  71. package/src/studio/demo-decks.js +4 -2
  72. package/src/studio/developer-starters.js +46 -0
  73. package/src/studio/export-jobs.js +86 -0
  74. package/src/studio/exporting.js +36 -18
  75. package/src/studio/feature-demos.js +144 -0
  76. package/src/studio/ia-routes.js +9 -0
  77. package/src/studio/layout-samples.js +9 -18
  78. package/src/studio/library-routes.js +7 -9
  79. package/src/studio/project.js +1 -0
  80. package/src/studio/public/about.js +68 -0
  81. package/src/studio/public/ai-settings.js +2 -2
  82. package/src/studio/public/app.js +43 -52
  83. package/src/studio/public/chat.js +5 -1
  84. package/src/studio/public/developer-authoring.css +12 -0
  85. package/src/studio/public/developer-authoring.js +306 -0
  86. package/src/studio/public/export-status.js +154 -0
  87. package/src/studio/public/generation-guide.js +42 -0
  88. package/src/studio/public/index.html +31 -10
  89. package/src/studio/public/library.css +48 -3
  90. package/src/studio/public/library.html +5 -1
  91. package/src/studio/public/library.js +150 -41
  92. package/src/studio/public/map-data.js +7 -3
  93. package/src/studio/public/map-editor.js +625 -236
  94. package/src/studio/public/map-settings.js +3 -1
  95. package/src/studio/public/slide-form.js +27 -29
  96. package/src/studio/public/style.css +56 -9
  97. package/src/studio/public/ui-icons.js +1 -1
  98. package/src/studio/server.js +29 -14
  99. package/src/studio/snapshot.js +8 -4
  100. package/src/studio/template-collections.js +1 -1
  101. package/src/studio/tour.js +89 -7
  102. package/src/themes.js +202 -0
  103. package/templates/cenario-e-ciencia.yaml +1 -21
  104. package/templates/ensaio-api.yaml +3 -3
  105. package/templates/cenario/Texto no cen/303/241rio.yaml" +0 -171
  106. package/templates/cenario/imagens/cidade.svg +0 -30
  107. package/templates/cenario/imagens/pessoa.svg +0 -15
package/src/motion.js CHANGED
@@ -5,13 +5,17 @@ const number=(x,f,min,max)=>Math.min(max,Math.max(min,Number.isFinite(Number(x))
5
5
  const color=(x,f)=>/^#?[0-9a-f]{6}$/i.test(x||'')?'#'+String(x).replace('#',''):f;
6
6
  export function motionOptions(value,colors={}){
7
7
  const o=typeof value==='string'?{type:value}:value||{};
8
- if(!['wireframe','hologram','trace','requests','flow','chat','terminal'].includes(o.type))throw new Error('motion.type: wireframe, hologram, trace, requests, flow, chat ou terminal.');
8
+ if(!['wireframe','hologram','trace','requests','flow','chat','terminal','console'].includes(o.type))throw new Error('motion.type: wireframe, hologram, trace, requests, flow, chat, terminal ou console.');
9
+ // console: janela de terminal com cenas (comando digitado letra a letra, depois a saída linha a linha), em ciclo
10
+ const con=o.type==='console';
11
+ const scenes=(Array.isArray(o.scenes)?o.scenes:con&&Array.isArray(o.lines)?o.lines:[]).slice(0,12).map(sc=>typeof sc==='string'?{cmd:sc,out:[]}:{cmd:String(sc?.cmd??''),out:(Array.isArray(sc?.out)?sc.out:String(sc?.out??'').split('\n')).map(String).filter(x=>x!=='').slice(0,8)}).filter(sc=>sc.cmd);
12
+ if(con&&!scenes.length)throw new Error('motion console: scenes: [{cmd: "comando", out: ["linha de saída", ...]}].');
9
13
  const aliases={fg:'ink',bg:'paper',hi:'accent',em:'alert'};
10
14
  const resolved=(v,f)=>color(colors[aliases[v]||v]||v,f);
11
15
  const trace=(Array.isArray(o.path)&&o.path.length>=2?o.path:[[.05,.5],[.95,.5]]).slice(0,64).map(p=>[number(Array.isArray(p)?p[0]:p?.x,0,0,1),number(Array.isArray(p)?p[1]:p?.y,0,0,1)]);
12
16
  const transparent=o.background==='transparent';
13
17
  const layers=(Array.isArray(o.layers)&&o.layers.length?o.layers:[{shape:o.shape,x:.5,y:.5,scale:.24}]).slice(0,6).map(l=>({shape:['sphere','cube','plane','ring'].includes(l.shape)?l.shape:'sphere',x:number(l.x,.5,-.5,1.5),y:number(l.y,.5,-.5,1.5),scale:number(l.scale,.24,.03,1),speed:number(l.speed,1,.1,4),rotation:number(l.rotation,0,-10,10)}));
14
- return {type:o.type,path:trace,closed:o.closed===true,pulses:Math.round(number(o.pulses,2,1,8)),lineWidth:number(o.lineWidth,1.2,.5,6),layers,opacity:number(o.opacity,.7,.05,1),lines:Array.isArray(o.lines)?o.lines.map(String):String(o.code||'').split('\n'),fontSize:number(o.fontSize,16,10,40),shape:o.shape==='cube'?'cube':'sphere',speed:number(o.speed,1,.1,8),recognition:number(o.recognition,1,0,10),thinking:number(o.thinking,.8,0,10),read:number(o.read,2,0,15),typing:number(o.typing,.035,.01,.2),caption:String(o.caption||''),user:String(o.user||''),assistant:String(o.assistant||''),background:transparent?'transparent':resolved(o.background,'#111e30'),foreground:resolved(o.foreground,transparent?color(colors.ink,'#182642'):'#b5c9db'),accent:resolved(o.accent,color(colors.alert,'#cc092f')),secondary:resolved(o.secondary,transparent?color(colors.accent,'#3769e8'):'#aa9ccf'),manual:o.start!=='auto',finish:['next','video','hold'].includes(o.finish)?o.finish:'hold'};
18
+ return {type:o.type,path:trace,closed:o.closed===true,pulses:Math.round(number(o.pulses,2,1,8)),lineWidth:number(o.lineWidth,1.2,.5,6),layers,opacity:number(o.opacity,.7,.05,1),lines:Array.isArray(o.lines)?o.lines.map(String):String(o.code||'').split('\n'),fontSize:number(o.fontSize,con?20:16,10,40),shape:o.shape==='cube'?'cube':'sphere',speed:number(o.speed,1,.1,8),recognition:number(o.recognition,1,0,10),thinking:number(o.thinking,.8,0,10),read:number(o.read,2,0,15),typing:number(o.typing,con?.016:.035,.005,.2),scenes,hold:number(o.hold,3.2,.5,30),caption:String(o.caption||''),user:String(o.user||''),assistant:String(o.assistant||''),background:transparent?'transparent':resolved(o.background,con?color(colors.surfaceDark||colors.surface,'#12160f'):'#111e30'),foreground:resolved(o.foreground,con?color(colors.ink,'#ece8db'):transparent?color(colors.ink,'#182642'):'#b5c9db'),accent:resolved(o.accent,con?color(colors.accent,'#f0b24a'):color(colors.alert,'#cc092f')),secondary:resolved(o.secondary,con?color(colors.c2||colors.accent,'#a9c79b'):transparent?color(colors.accent,'#3769e8'):'#aa9ccf'),muted:color(colors.muted,'#9fa89a'),line:color(colors.line,'#2b352c'),alert:color(colors.alert,'#e07a55'),manual:o.start!=='auto',finish:['next','video','hold'].includes(o.finish)?o.finish:'hold'};
15
19
  }
16
20
  function drawTrace(ctx,o,t,w,h){
17
21
  const points=o.path.map(p=>[p[0]*w,p[1]*h]);if(o.closed)points.push(points[0]);
@@ -26,11 +30,21 @@ function scene(o){
26
30
  const message=(kind)=>parent.postMessage({sagaMotion:kind},'*');
27
31
  const chat=document.querySelector('.chat'),input=document.querySelector('.input'),thread=document.querySelector('.thread'),status=document.querySelector('.status');
28
32
  function reset(){started=false;phase='idle';elapsed=0;chat.hidden=true;input.textContent='';thread.replaceChildren();status.textContent='';}
29
- window.__sagaMotion=()=>({active,reduced,started,serial,phase});message('ready');window.addEventListener('message',e=>{if(e.source!==parent)return;if(e.data?.sagaMotion==='poster'){still=e.data.active!==false;recompute();if(reduced&&o.type==='terminal')document.querySelector('.feed').textContent=o.lines.join('\n');}if(e.data?.sagaMotion==='active')active=!!e.data.active;if(e.data?.sagaMotion==='reset')reset();if(e.data?.sagaMotion==='start'){reset();started=true;active=true;phase='recognize';chat.hidden=false;}});
33
+ window.__sagaMotion=()=>({active,reduced,started,serial,phase});message('ready');window.addEventListener('message',e=>{if(e.source!==parent)return;if(e.data?.sagaMotion==='poster'){still=e.data.active!==false;recompute();if(reduced&&o.type==='terminal')document.querySelector('.feed').textContent=o.lines.join('\n');if(reduced&&o.type==='console')consoleFull();}if(e.data?.sagaMotion==='active')active=!!e.data.active;if(e.data?.sagaMotion==='reset')reset();if(e.data?.sagaMotion==='start'){reset();started=true;active=true;phase='recognize';chat.hidden=false;}});
30
34
  if(o.type==='chat'&&o.manual){const request=e=>{if(!active)return;if(e.type==='keydown'&&![' ','Enter','ArrowRight','PageDown'].includes(e.key))return;e.preventDefault();message('start-request');};document.addEventListener('click',request);document.addEventListener('keydown',request);}
31
35
  if(o.type==='chat'&&!o.manual){started=true;phase='recognize';chat.hidden=false;}
32
36
  function bubble(text,kind){const el=document.createElement('div');el.className='bubble '+kind;el.textContent=text;thread.append(el);return el;}
33
37
  let terminalElapsed=0;const terminalText=o.lines.join('\n');
38
+ let ci=0,cphase='wait',ct=0,shown='';const conBody=document.querySelector('.con-body'),conNum=document.querySelector('.con-n');
39
+ function showConsole(cmdLen,outN){const sc=o.scenes[ci],key=ci+':'+cmdLen+':'+outN;if(!sc||key===shown)return;shown=key;const cmd=document.createElement('p');cmd.className='cmd';const pr=document.createElement('span');pr.className='pr';cmd.append(pr,document.createTextNode(sc.cmd.slice(0,cmdLen)));if(cmdLen<sc.cmd.length){const cur=document.createElement('span');cur.className='cur';cmd.append(cur);}const outs=sc.out.slice(0,outN).map((l,i)=>{const e=document.createElement('p');e.className='out'+(i===0?' first':'');const arr=document.createElement('span');arr.className='arr';e.append(arr,document.createTextNode(l));return e;});conBody.replaceChildren(cmd,...outs);conNum.textContent=String(ci+1).padStart(2,'0')+'/'+String(o.scenes.length).padStart(2,'0');}
40
+ function consoleFull(){ci=0;cphase='wait';ct=0;showConsole(o.scenes[0].cmd.length,o.scenes[0].out.length);}
41
+ function tickConsole(dt){const sc=o.scenes[ci];if(reduced){consoleFull();return;}ct+=dt;
42
+ if(cphase==='wait'){showConsole(0,0);if(ct>=.3){cphase='type';ct=0;}}
43
+ else if(cphase==='type'){const n=Math.min(sc.cmd.length,Math.floor(ct/o.typing));showConsole(n,0);if(n>=sc.cmd.length){cphase='gap';ct=0;}}
44
+ else if(cphase==='gap'){showConsole(sc.cmd.length,0);if(ct>=.38){cphase='out';ct=0;}}
45
+ else if(cphase==='out'){const n=Math.min(sc.out.length,1+Math.floor(ct/.26));showConsole(sc.cmd.length,n);if(n>=sc.out.length&&ct>=Math.max(0,sc.out.length-1)*.26){cphase='hold';ct=0;}}
46
+ else if(cphase==='hold'&&ct>=o.hold){ci=(ci+1)%o.scenes.length;cphase='wait';ct=0;}}
47
+ if(o.type==='console'){if(reduced)consoleFull();else showConsole(0,0);}
34
48
  let bot;
35
49
  function tickChat(dt){if(!started||phase==='done')return;elapsed+=dt;
36
50
  if(phase==='recognize'&&elapsed>=o.recognition){phase='input';elapsed=0;}
@@ -40,7 +54,7 @@ function scene(o){
40
54
  else if(phase==='read'&&elapsed>=o.read){phase='done';message('done');}}
41
55
  function requestLine(){serial++;const ops=['GET /silent-cloud','POST /paper-moon','PUT /silver-orbit','PATCH /soft-vector','GET /tiny-comet','DELETE /ghost-frame'];const el=document.createElement('div');el.textContent=String(serial).padStart(6,'0')+' '+ops[(serial*7+Math.floor(serial/9))%ops.length].padEnd(23,' ')+' '+(serial*2654435761>>>0).toString(16)+' '+(17+serial*37%981)+'ms 200 OK';document.querySelector('.feed').append(el);while(document.querySelector('.feed').children.length>14)document.querySelector('.feed').firstChild.remove();document.querySelector('.feed').scrollTop=1e6;}
42
56
  if(o.type==='requests'){for(let i=0;i<8;i++)requestLine();setInterval(()=>{if(active&&!document.hidden&&!reduced){requestLine();requestLine();}},Math.max(35,240/o.speed));}
43
- function draw(ts){requestAnimationFrame(draw);if(!active||document.hidden){previous=ts;return;}const dt=Math.min(.1,(ts-(previous||ts))/1000);previous=ts;if(o.type==='chat'){tickChat(dt);return;}if(o.type==='terminal'){terminalElapsed+=dt*o.speed;const count=reduced?terminalText.length:Math.floor(terminalElapsed/o.typing);document.querySelector('.feed').textContent=terminalText.slice(0,count)+(count<terminalText.length?'_':'');document.querySelector('.feed').scrollTop=1e6;if(count>terminalText.length+3/o.typing)terminalElapsed=0;return;}if(o.type==='requests'||(reduced&&serial))return;serial++;
57
+ function draw(ts){requestAnimationFrame(draw);if(!active||document.hidden){previous=ts;return;}const dt=Math.min(.1,(ts-(previous||ts))/1000);previous=ts;if(o.type==='chat'){tickChat(dt);return;}if(o.type==='console'){tickConsole(dt*o.speed);return;}if(o.type==='terminal'){terminalElapsed+=dt*o.speed;const count=reduced?terminalText.length:Math.floor(terminalElapsed/o.typing);document.querySelector('.feed').textContent=terminalText.slice(0,count)+(count<terminalText.length?'_':'');document.querySelector('.feed').scrollTop=1e6;if(count>terminalText.length+3/o.typing)terminalElapsed=0;return;}if(o.type==='requests'||(reduced&&serial))return;serial++;
44
58
  const w=main.clientWidth,h=main.clientHeight,dpr=devicePixelRatio||1;if(canvas.width!==Math.round(w*dpr)||canvas.height!==Math.round(h*dpr)){canvas.width=Math.round(w*dpr);canvas.height=Math.round(h*dpr);}ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,w,h);const t=reduced?0:(ts-epoch)/1000*o.speed,r=Math.min(w*.25,h*.32),cy=h*.55;if(o.type==='hologram'){drawHologram(ctx,o,t,w,h);return;}if(o.type==='trace'){drawTrace(ctx,o,t,w,h);return;}
45
59
  function p(x,y,z){let a=t*.15+.4,xx=x*Math.cos(a)+z*Math.sin(a),zz=-x*Math.sin(a)+z*Math.cos(a),yy=y*Math.cos(.3)-zz*Math.sin(.3);const f=4/(4+zz);return[w/2+xx*r*f,cy+yy*r*f];}
46
60
  function line(v,c){ctx.beginPath();v.forEach((q,i)=>{let a=p(...q);i?ctx.lineTo(...a):ctx.moveTo(...a);});ctx.strokeStyle=c;ctx.lineWidth=.9;ctx.stroke();}
@@ -53,6 +67,6 @@ function scene(o){
53
67
  }
54
68
  // still: o deck pediu movimento nenhum (motion: none). A cena já nasce parada e completa; antes ela só sabia pela
55
69
  // mensagem da página, que sob carga chegava depois de o terminal começar a digitar.
56
- export function motionHTML(value,colors,{still=false}={}){const o={...motionOptions(value,colors),...(still?{still:true}:{})};const css=`html,body{height:100%;background:${o.type==='chat'?'transparent':o.background};color:${o.foreground};overflow:hidden}main{height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}.caption{position:absolute;top:12px;left:16px;color:${o.secondary};font:11px monospace;letter-spacing:1.5px}.feed{position:absolute;top:36px;left:16px;right:16px;bottom:12px;overflow:hidden;font:${o.type==='terminal'?o.fontSize:12}px/1.5 monospace;white-space:pre}.chat{position:absolute;inset:0;border:1px solid ${o.secondary};border-radius:16px;padding:18px;background:${o.background};display:flex;flex-direction:column;gap:12px}.chat[hidden]{display:none}.thread{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px}.bubble{font:18px/1.45 system-ui;max-width:90%;padding:10px 14px;border-radius:10px;white-space:pre-wrap}.user{align-self:flex-end;background:#263a51}.assistant{align-self:flex-start}.status{font:14px system-ui;color:${o.secondary};min-height:18px}.input{border:1px solid #51617b;border-radius:9px;min-height:48px;padding:12px;font:17px system-ui;white-space:pre-wrap}.chat-title{font:12px system-ui;letter-spacing:2px;color:${o.secondary}}`;
57
- const html=`<main><canvas aria-hidden="true"></canvas><div class="caption">${esc(o.type==='chat'?'':o.caption)}</div><div class="feed" aria-hidden="true"></div><section class="chat" hidden><div class="chat-title">${esc(o.caption||'ASSISTENTE')}</div><div class="thread" aria-live="polite"></div><div class="status"></div><div class="input" aria-label="Mensagem digitada"></div></section></main>`;
70
+ export function motionHTML(value,colors,{still=false}={}){const o={...motionOptions(value,colors),...(still?{still:true}:{})};const css=`html,body{height:100%;background:${['chat','console'].includes(o.type)?'transparent':o.background};color:${o.foreground};overflow:hidden}main{height:100%;position:relative}canvas{position:absolute;inset:0;width:100%;height:100%}.caption{position:absolute;top:12px;left:16px;color:${o.secondary};font:11px monospace;letter-spacing:1.5px}.feed{position:absolute;top:36px;left:16px;right:16px;bottom:12px;overflow:hidden;font:${o.type==='terminal'?o.fontSize:12}px/1.5 monospace;white-space:pre}.chat{position:absolute;inset:0;border:1px solid ${o.secondary};border-radius:16px;padding:18px;background:${o.background};display:flex;flex-direction:column;gap:12px}.chat[hidden]{display:none}.thread{flex:1;overflow:auto;display:flex;flex-direction:column;gap:12px}.bubble{font:18px/1.45 system-ui;max-width:90%;padding:10px 14px;border-radius:10px;white-space:pre-wrap}.user{align-self:flex-end;background:#263a51}.assistant{align-self:flex-start}.status{font:14px system-ui;color:${o.secondary};min-height:18px}.input{border:1px solid #51617b;border-radius:9px;min-height:48px;padding:12px;font:17px system-ui;white-space:pre-wrap}.chat-title{font:12px system-ui;letter-spacing:2px;color:${o.secondary}}`+(o.type==='console'?`.caption{display:none}.con{position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);max-height:100%;display:flex;flex-direction:column;border:1px solid ${o.line};border-radius:14px;overflow:hidden;background:${o.background};font-family:'IBM Plex Mono','Cascadia Mono',Consolas,'SF Mono',Menlo,monospace}.con-bar{flex:none;display:flex;align-items:center;gap:9px;height:44px;padding:0 18px;border-bottom:1px solid ${o.line}}.con-bar i{width:13px;height:13px;border-radius:50%;opacity:.72}.con-bar i:nth-child(1){background:${o.alert}}.con-bar i:nth-child(2){background:${o.accent}}.con-bar i:nth-child(3){background:${o.secondary}}.con-cap{margin-left:10px;font-size:13px;letter-spacing:2.5px;text-transform:uppercase;color:${o.muted};white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.con-n{margin-left:auto;font-size:13px;color:${o.muted}}.con-body{flex:1;min-height:calc(${o.fontSize}px * 1.6 * 6 + 36px);overflow:hidden;padding:18px 22px;font-size:${o.fontSize}px;line-height:1.6}.con-body p{margin:0 0 .4em;white-space:pre-wrap;word-break:break-all}.cmd{color:${o.foreground}}.pr{display:inline-block;width:.42em;height:.42em;margin:0 .85em .12em .1em;border-top:.13em solid ${o.accent};border-right:.13em solid ${o.accent};transform:rotate(45deg)}.cur{display:inline-block;width:.55em;height:1.05em;margin-left:2px;vertical-align:-.18em;background:${o.accent};animation:bl 1s steps(1) infinite}@keyframes bl{50%{opacity:0}}.out{color:${o.muted}}.out.first{color:${o.secondary}}.arr{position:relative;display:inline-block;width:.8em;height:0;margin-right:.7em;vertical-align:.3em;border-top:.09em solid ${o.muted}}.arr::before{content:"";position:absolute;left:0;top:-.24em;width:.4em;height:.4em;border-left:.09em solid ${o.muted};border-bottom:.09em solid ${o.muted};transform:translateY(-.03em) rotate(45deg)}@media(prefers-reduced-motion:reduce){.cur{animation:none}}`:'');
71
+ const html=`<main><canvas aria-hidden="true"></canvas><div class="caption">${esc(o.type==='chat'?'':o.caption)}</div><div class="feed" aria-hidden="true"></div><section class="chat" hidden><div class="chat-title">${esc(o.caption||'ASSISTENTE')}</div><div class="thread" aria-live="polite"></div><div class="status"></div><div class="input" aria-label="Mensagem digitada"></div></section>${o.type==='console'?`<div class="con"><div class="con-bar"><i></i><i></i><i></i><span class="con-cap">${esc(o.caption)}</span><span class="con-n"></span></div><div class="con-body" aria-hidden="true"></div></div>`:''}</main>`;
58
72
  const doc=playgroundDocument({html,css,javascript:`${drawHologram.toString()}\n${drawTrace.toString()}\n(${scene.toString()})(${JSON.stringify(o)});`});return `<iframe data-motion="${o.type}" data-motion-manual="${o.manual}" data-motion-finish="${o.finish}" title="${esc(o.caption||'Detalhe animado')}" sandbox="allow-scripts" style="width:100%;height:100%;border:0" srcdoc="${esc(doc)}"></iframe>`;}
package/src/purpose.js CHANGED
@@ -12,7 +12,7 @@ export const PURPOSES = {
12
12
  executiva: { label: "Executiva (antigo)", hint: "", maxWords: 40, dense: false, legacy: true },
13
13
  };
14
14
 
15
- const LAYOUT_WORDS = { onepage: 120, status: 90, solution: 140, calc: 90, algo: 60 }; // densos por natureza (uma página com tudo; exercício resolvido se revela aos poucos)
15
+ const LAYOUT_WORDS = { onepage: 120, status: 90, solution: 140, calc: 90, algo: 60, calendar: 240 }; // densos por natureza (uma página com tudo; exercício resolvido se revela aos poucos)
16
16
 
17
17
  // Limite de palavras na tela de um slide: o do slide > o do deck > o do propósito (ou do layout, se maior) > 40
18
18
  export function wordLimit(s = {}, spec = {}, layout = s.layout) {
@@ -35,11 +35,45 @@
35
35
 
36
36
  // {{nome}} em qualquer texto do pedido (URL, cabeçalhos, corpo). Sem valor: fica como está.
37
37
  const VAR = /\{\{\s*([\w.-]+)\s*\}\}/g;
38
+ function renderBody(body, vars, encoding) {
39
+ if (typeof body !== "string" || !["json", "urlencoded"].includes(encoding)) return render(body, vars);
40
+ if (encoding === "urlencoded") return body.replace(VAR, (all, key) => vars && vars[key] != null ? encodeURIComponent(String(vars[key])) : all);
41
+ // Interpola no texto original, sem converter números ou reformatar o JSON. Dentro de uma
42
+ // string escapa só o valor; fora dela, insere um literal JSON. Marcador sem valor continua.
43
+ let result = "", quoted = false;
44
+ for (let at = 0; at < body.length; at++) {
45
+ if (body.startsWith("{{", at)) {
46
+ const marker = body.slice(at).match(/^\{\{\s*([\w.-]+)\s*\}\}/);
47
+ if (marker && vars && vars[marker[1]] != null) {
48
+ const value = vars[marker[1]];
49
+ result += quoted ? JSON.stringify(String(value)).slice(1, -1) : JSON.stringify(value);
50
+ at += marker[0].length - 1; continue;
51
+ }
52
+ }
53
+ const char = body[at]; result += char;
54
+ if (quoted && char === "\\" && at + 1 < body.length) { result += body[++at]; continue; }
55
+ if (char === '"') quoted = !quoted;
56
+ }
57
+ return result;
58
+ }
59
+ function renderURL(url, vars, encoding) {
60
+ if (encoding !== "query") return render(url, vars);
61
+ const text = String(url), query = text.indexOf("?"), fragment = text.indexOf("#");
62
+ return text.replace(VAR, (all, key, at) => {
63
+ if (!vars || vars[key] == null) return all;
64
+ const value = String(vars[key]);
65
+ return query >= 0 && at > query && (fragment < 0 || at < fragment) ? encodeURIComponent(value) : value;
66
+ });
67
+ }
38
68
  function render(value, vars) {
39
69
  if (typeof value === "string") return value.replace(VAR, (all, k) => (vars && vars[k] != null ? String(vars[k]) : all));
40
70
  if (Array.isArray(value)) return value.map((x) => render(x, vars));
41
71
  if (value && typeof value === "object" && value.__py) return value;
42
- if (value && typeof value === "object") { const o = {}; for (const [k, v] of Object.entries(value)) o[render(k, vars)] = render(v, vars); return o; }
72
+ if (value && typeof value === "object") {
73
+ const o = {};
74
+ for (const [k, v] of Object.entries(value)) o[render(k, vars)] = k === "body" && value.bodyEncoding ? renderBody(v, vars, value.bodyEncoding) : k === "url" && value.urlEncoding ? renderURL(v, vars, value.urlEncoding) : render(v, vars);
75
+ return o;
76
+ }
43
77
  return value;
44
78
  }
45
79
  function missing(value, vars) {
@@ -82,6 +116,8 @@
82
116
  url: String(req.url || ""),
83
117
  headers: req.headers && typeof req.headers === "object" ? req.headers : {},
84
118
  body: req.body,
119
+ ...(["json", "urlencoded"].includes(req.bodyEncoding) ? { bodyEncoding: req.bodyEncoding } : {}),
120
+ ...(req.urlEncoding === "query" ? { urlEncoding: "query" } : {}),
85
121
  form: req.form && typeof req.form === "object" ? req.form : null, // multipart: { campo: "@file" | texto }
86
122
  auth: req.auth !== false,
87
123
  },
@@ -202,12 +238,12 @@
202
238
  }
203
239
 
204
240
  function curlCmd(r, tokenVar, extra, fileName) {
205
- const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${dq(r.url)}`];
241
+ const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${r._urlShell || dq(r.url)}`];
206
242
  if (r.auth) parts.push(`-H "Authorization: Bearer $${tokenVar}"`);
207
- for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${dq(k + ": " + v)}`);
243
+ for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${r._headerShell?.[k] || dq(k + ": " + v)}`);
208
244
  if (r.form) for (const [k, v] of Object.entries(r.form)) parts.push(`-F ${dq(k + "=" + (v === "@file" ? "@" + (fileName || "arquivo") : v))}`);
209
245
  else if (isJsonBody(r.body)) { parts.push(`-H "Content-Type: application/json"`); parts.push(`-d ${shq(JSON.stringify(r.body, null, 2))}`); }
210
- else if (r.body != null) parts.push(`-d ${shq(r.body)}`);
246
+ else if (r.body != null) parts.push(r._stdinBody ? "--data-binary @-" : `-d ${r._bodyShell || shq(r.body)}`);
211
247
  return parts.join(" \\\n ");
212
248
  }
213
249
 
@@ -217,7 +253,70 @@
217
253
  const secretsIn = (v) => { const out = new Set(); JSON.stringify(v || "").replace(/\{\{\s*secret\.([\w-]+)\s*\}\}/g, (_, k) => out.add(k)); return [...out]; };
218
254
  const fileName = (a, vars) => (vars && vars["file.name"]) || (a.file ? String(a.file).split(/[\\/]/).pop() : "arquivo");
219
255
  const usesB64 = (a) => JSON.stringify(a.request.body || "").includes("{{file.base64}}");
256
+ // Para cURL importado, expressões são montadas por contexto. As partes literais continuam
257
+ // literais no shell, e JSON/form/query escapam o segredo só na hora de rodar o exemplo.
258
+ function secretParts(text, encoding) {
259
+ text = String(text); const parts = []; let last = 0, scan = 0, quoted = false;
260
+ const query = text.indexOf("?"), fragment = text.indexOf("#");
261
+ const pattern = /\{\{\s*secret\.([\w-]+)\s*\}\}/g; let match;
262
+ while ((match = pattern.exec(text))) {
263
+ if (encoding === "json") for (; scan < match.index; scan++) { if (quoted && text[scan] === "\\") scan++; else if (text[scan] === '"') quoted = !quoted; }
264
+ if (match.index > last) parts.push(text.slice(last, match.index));
265
+ const context = encoding === "json" ? (quoted ? "json-string" : "json-literal") : encoding === "urlencoded" || (encoding === "query" && query >= 0 && match.index > query && (fragment < 0 || match.index < fragment)) ? "urlencoded" : "raw";
266
+ parts.push({ name: secretEnv(match[1]), context }); last = pattern.lastIndex; scan = last;
267
+ }
268
+ if (last < text.length || !parts.length) parts.push(text.slice(last));
269
+ return parts;
270
+ }
271
+ function secretExpression(text, encoding, language) {
272
+ return secretParts(text, encoding).map(part => {
273
+ if (typeof part === "string") return JSON.stringify(part);
274
+ const value = language === "python" ? `os.environ[${JSON.stringify(part.name)}]` : `segredo(${JSON.stringify(part.name)})`;
275
+ if (part.context === "json-string") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)[1:-1]` : `JSON.stringify(${value}).slice(1, -1)`;
276
+ if (part.context === "json-literal") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)` : `JSON.stringify(${value})`;
277
+ if (part.context === "urlencoded") return language === "python" ? `quote(${value}, safe="-_.!~*'()")` : `encodeURIComponent(${value})`;
278
+ return value;
279
+ }).join(" + ");
280
+ }
281
+ function curlImported(a, vars) {
282
+ const L = Lines(false), name = fileName(a, vars), helpers = [], helperNames = new Map();
283
+ const cleanVars = Object.fromEntries(Object.entries(vars || {}).filter(([key]) => !key.startsWith("secret.")));
284
+ const shell = (text, encoding) => secretParts(text, encoding).map(part => {
285
+ if (typeof part === "string") return shq(part);
286
+ if (part.context === "raw") return `"$${part.name}"`;
287
+ const key = `${part.name}:${part.context}`;
288
+ let helper = helperNames.get(key);
289
+ if (!helper) {
290
+ helper = `SAGA_SECRET_${helpers.length + 1}`; helperNames.set(key, helper);
291
+ const expression = part.context === "urlencoded" ? "encodeURIComponent(valor)" : part.context === "json-string" ? "JSON.stringify(valor).slice(1,-1)" : "JSON.stringify(valor)";
292
+ const script = `const valor=process.env[${JSON.stringify(part.name)}];if(valor==null)throw new Error(${JSON.stringify("Defina " + part.name)});process.stdout.write(${expression});`;
293
+ helpers.push(`${helper}=$(node -e ${shq(script)})`);
294
+ }
295
+ return `"$${helper}"`;
296
+ }).join("");
297
+ const prepare = original => {
298
+ const r = render(original, { ...cleanVars, "file.name": name });
299
+ r._urlShell = shell(r.url, r.urlEncoding);
300
+ r._headerShell = Object.fromEntries(Object.entries(r.headers || {}).map(([key, value]) => [key, shell(`${key}: ${value}`, "raw")]));
301
+ if (r.body != null) {
302
+ if (typeof r.body === "object") r.body = JSON.stringify(r.body, null, 2);
303
+ r._bodyShell = shell(r.body, r.bodyEncoding);
304
+ r._stdinBody = true; // stdin preserva UTF-8 também com curl.exe no Windows
305
+ }
306
+ return r;
307
+ };
308
+ const req = prepare(a.request), check = a.polling ? prepare(render(a.polling.check, { ...cleanVars, id: "$ID" })) : null;
309
+ if (helpers.length) { L.add("# Bash + Node.js: segredos são escapados sem alterar o JSON ou a query."); helpers.forEach(line => L.add(line)); L.blank(); }
310
+ const command = (r, extra = "") => (r._stdinBody ? `printf '%s' ${r._bodyShell} | \\\n` : "") + curlCmd(r, a.tokenVar, extra, name);
311
+ if (a.mode === "polling") {
312
+ L.add("# 1. inicia", "start").add(command(req), "start");
313
+ L.add(`# guarde ${a.polling.id} da resposta em ID`).blank();
314
+ L.add("# 2. consulta o andamento", "poll").add(command(check), "poll");
315
+ } else L.add(command(req, a.mode === "stream" ? "N" : "") + (a.audio ? ` \\\n --output ${shq(a.audio)}` : ""), "start");
316
+ return L.out();
317
+ }
220
318
  function curl(a, vars) {
319
+ if (a.request.bodyEncoding || a.request.urlEncoding) return curlImported(a, vars);
221
320
  const L = Lines(false);
222
321
  const name = fileName(a, vars);
223
322
  const sec = Object.fromEntries(secretsIn(a.request).map((k) => ["secret." + k, MARK(k)]));
@@ -242,14 +341,14 @@
242
341
 
243
342
  function pyCall(name, r, extra, ind) {
244
343
  ind = ind || "";
245
- const args = [`${dq(r.url)}`];
344
+ const args = [r._urlExpr || dq(r.url)];
246
345
  if (r.auth || Object.keys(r.headers || {}).length) args.push("headers=HEADERS");
247
346
  if (r.form) {
248
347
  const files = Object.entries(r.form).filter(([, v]) => v === "@file"), data = Object.entries(r.form).filter(([, v]) => v !== "@file");
249
348
  if (files.length) args.push(`files={${files.map(([k]) => `${JSON.stringify(k)}: open(${dq(r._fileName || "arquivo")}, "rb")`).join(", ")}}`);
250
349
  if (data.length) args.push(`data={${data.map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(String(v))}`).join(", ")}}`);
251
350
  } else if (isJsonBody(r.body)) args.push(`json=${pyLiteral(r.body, ind + " ")}`);
252
- else if (r.body != null) args.push(`data=${JSON.stringify(String(r.body))}`);
351
+ else if (r.body != null) args.push(`data=${r.bodyEncoding ? `(${r._bodyExpr || JSON.stringify(String(r.body))}).encode("utf-8")` : r._bodyExpr || JSON.stringify(String(r.body))}`);
253
352
  args.push(...(extra || []));
254
353
  return `${name} = requests.${r.method.toLowerCase()}(\n` + args.map((x) => `${ind} ${x},`).join("\n") + `\n${ind})`;
255
354
  }
@@ -260,7 +359,7 @@
260
359
  Object.assign(h, r.headers || {});
261
360
  if (!Object.keys(h).length) return;
262
361
  const val = (v) => { const m = String(v).match(/^\{\{\s*secret\.([\w-]+)\s*\}\}$/); return m ? `os.environ[${JSON.stringify(String(m[1]).toUpperCase().replace(/[^A-Z0-9_]/g, "_"))}]` : JSON.stringify(String(v)); };
263
- const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : val(v)},`).join("\n");
362
+ const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : r.urlEncoding || r.bodyEncoding ? secretExpression(v, "raw", "python") : val(v)},`).join("\n");
264
363
  L.add(`HEADERS = {\n${body}\n}`, null, "cabeçalhos enviados em toda chamada");
265
364
  }
266
365
 
@@ -271,6 +370,12 @@
271
370
  const toPy = (v) => (v === "{{file.base64}}" ? { __py: "CONTEUDO" } : typeof v === "string" && /^\{\{\s*secret\.([\w-]+)\s*\}\}$/.test(v) ? { __py: `os.environ[${JSON.stringify(secretEnv(v.match(/secret\.([\w-]+)/)[1]))}]` } : Array.isArray(v) ? v.map(toPy) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toPy(x)])) : v);
272
371
  const req = { ...render({ ...a.request, body: toPy(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
273
372
  const imports = ["import os"];
373
+ if (req.urlEncoding || req.bodyEncoding) {
374
+ req._urlExpr = secretExpression(req.url, req.urlEncoding, "python");
375
+ if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "python");
376
+ if (req.bodyEncoding === "json") imports.push("import json");
377
+ if (req.urlEncoding || req.bodyEncoding === "urlencoded") imports.push("from urllib.parse import quote");
378
+ }
274
379
  if (b64) imports.push("import base64");
275
380
  if (a.mode === "polling") imports.push("import time");
276
381
  if (a.mode === "stream") imports.push("import json");
@@ -368,9 +473,9 @@
368
473
  opts.push(`body: JSON.stringify(${jsLiteral(r.body, " ")})`);
369
474
  } else {
370
475
  if (hasHeaders) opts.push("headers: HEADERS");
371
- if (r.body != null) opts.push(`body: ${JSON.stringify(String(r.body))}`);
476
+ if (r.body != null) opts.push(`body: ${r._bodyExpr || JSON.stringify(String(r.body))}`);
372
477
  }
373
- const target = url || dq(r.url);
478
+ const target = url || r._urlExpr || dq(r.url);
374
479
  L.add(opts.length ? `const ${name} = await fetch(${target}, {\n${opts.map((o) => " " + o + ",").join("\n")}\n});` : `const ${name} = await fetch(${target});`, tag, why);
375
480
  L.add(`if (!${name}.ok) throw new Error(\`HTTP \${${name}.status}: \${await ${name}.text()}\`);`, tag, "para aqui se deu erro (4xx/5xx)");
376
481
  }
@@ -380,15 +485,21 @@
380
485
  const b64 = usesB64(a);
381
486
  const toJs = (v) => (v === "{{file.base64}}" ? { __js: "CONTEUDO" } : jsSecret(v) || (Array.isArray(v) ? v.map(toJs) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toJs(x)])) : v));
382
487
  const req = { ...render({ ...a.request, body: toJs(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
488
+ const imported = !!(req.urlEncoding || req.bodyEncoding);
489
+ if (imported) {
490
+ req._urlExpr = secretExpression(req.url, req.urlEncoding, "javascript");
491
+ if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "javascript");
492
+ }
383
493
  const fsUse = [b64 || req.form ? "readFile" : "", a.audio ? "writeFile" : ""].filter(Boolean);
384
494
  L.add("// Node.js 18+ · salve como exemplo.mjs e rode: node exemplo.mjs");
385
495
  if (fsUse.length) L.add(`import { ${fsUse.join(", ")} } from "node:fs/promises";`);
386
496
  L.blank();
497
+ if (imported && secretsIn(a.request).length) L.add('const segredo = (nome) => { const valor = process.env[nome]; if (valor == null) throw new Error(`Defina ${nome}`); return valor; };').blank();
387
498
  const auth = req.auth || !!(a.polling && a.polling.check.auth);
388
499
  if (auth) L.add(`const TOKEN = process.env.${a.tokenVar};`, null, "o token de acesso vem de uma variável de ambiente, nunca escrito no código").blank();
389
500
  const h = [];
390
501
  if (auth) h.push(" Authorization: `Bearer ${TOKEN}`,");
391
- for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
502
+ for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${imported ? secretExpression(v, "raw", "javascript") : jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
392
503
  if (h.length) L.add(`const HEADERS = {\n${h.join("\n")}\n};`, null, "cabeçalhos enviados em toda chamada").blank();
393
504
  if (b64) L.add(`const CONTEUDO = (await readFile(${dq(name)})).toString("base64");`, "start", "o arquivo, em base64, vai dentro do JSON").blank();
394
505
  const out = (expr, why) => {
@@ -525,6 +636,6 @@
525
636
 
526
637
  // o slide mexe com arquivo? (upload @file ou {{file.…}})
527
638
  const usesFile = (s) => { const a = s && s._normalized ? s : normalize(s); return !!a.file || a.mic || !!(a.request.form && Object.values(a.request.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(a.request)); };
528
- const api = { isType, cosine, usesFile, parsePath, get, set, render, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
639
+ const api = { isType, cosine, usesFile, parsePath, get, set, render, renderBody, renderURL, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
529
640
  g.SagadeckApiCore = api;
530
641
  })(typeof window !== "undefined" ? window : globalThis);
@@ -61,7 +61,8 @@
61
61
  const bodyTxt = bodyEl ? bodyEl.value : "";
62
62
  let body;
63
63
  if (bodyTxt.trim() !== "" && !cfg.request.form) { // upload: a aba Corpo só mostra os campos do formulário
64
- try { body = JSON.parse(bodyTxt); }
64
+ if (cfg.request.bodyEncoding && typeof cfg.request.body === "string") body = bodyTxt;
65
+ else try { body = JSON.parse(bodyTxt); }
65
66
  catch (e) {
66
67
  if (typeof cfg.request.body === "string") body = bodyTxt;
67
68
  else throw new Error("O corpo não é um JSON válido: " + e.message);