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,55 @@
1
+ <!doctype html>
2
+ <html lang="pt-BR">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="theme-color" content="#f7f6f2">
7
+ <title>Seu estúdio · SagaDeck</title>
8
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 32 32%22%3E%3Crect width=%2232%22 height=%2232%22 rx=%229%22 fill=%22%23634ce9%22/%3E%3Cpath d=%22M9 8h14L9 24h14%22 stroke=%22white%22 stroke-width=%223%22 fill=%22none%22/%3E%3C/svg%3E">
9
+ <script>
10
+ (function () {
11
+ var pref = "light";
12
+ try { pref = JSON.parse(localStorage.getItem("sagadeck.appTheme")) || "light"; } catch (e) {}
13
+ var dark = pref === "dark" || (pref === "system" && window.matchMedia && matchMedia("(prefers-color-scheme: dark)").matches);
14
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
15
+ })();
16
+ </script>
17
+ <link rel="stylesheet" href="fonts.css">
18
+ <link rel="stylesheet" href="library.css">
19
+ </head>
20
+ <body class="lib">
21
+ <a href="#main" class="skip-link">Ir para o conteúdo</a>
22
+ <a class="lib-brand" href="biblioteca" aria-label="SagaDeck, início"><span class="lib-logo"><svg class="saga-mark" viewBox="0 0 32 32" aria-hidden="true"><rect x="3.5" y="4.5" width="19" height="14" rx="3" fill="none" stroke="currentColor" stroke-width="2.2" opacity=".55"/><path fill="currentColor" fill-rule="evenodd" d="M12 11h13a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H12a3 3 0 0 1-3-3v-8a3 3 0 0 1 3-3zM16.5 14.8v6.4l5.3-3.2z"/></svg></span><span>Saga<span class="brand-light">Deck</span><small>CREATIVE STUDIO</small></span></a>
23
+ <header class="lib-top">
24
+ <div class="breadcrumb">Seu estúdio <span>/</span> <b id="location-name">Visão geral</b></div>
25
+ <div class="spacer"></div>
26
+ <label class="lib-search"><i class="ic" data-ic="search"></i><input id="q" aria-label="Buscar apresentações" placeholder="Buscar apresentações" autocomplete="off"><kbd>/</kbd></label>
27
+ <button class="lib-btn primary" id="btn-new"><i class="ic" data-ic="plus"></i><span class="lbl">Nova apresentação</span></button>
28
+ <button class="lib-icon-btn" id="btn-theme" title="Alternar tema claro e escuro" aria-label="Alternar tema claro e escuro"><i class="ic" data-ic="sun-moon"></i></button>
29
+ <div class="lib-user" id="user" hidden><div class="lib-avatar" id="avatar"></div><span class="uname" id="uname"></span></div>
30
+ </header>
31
+ <div class="lib-body">
32
+ <aside class="lib-side">
33
+ <nav id="side" aria-label="Biblioteca"></nav>
34
+ <a class="lib-btn ghost ai-settings" id="btn-ai" hidden target="_blank" rel="noopener" title="Provedores, chaves e modelos de IA"><i class="ic" data-ic="sliders-horizontal"></i><span class="lbl">modelrelay</span></a>
35
+ </aside>
36
+ <main class="lib-main" id="main" tabindex="-1" aria-live="polite"><div class="initial-loading"><span class="loading-orbit"></span>Preparando seu estúdio…</div></main>
37
+ </div>
38
+ <div class="lmenu" id="card-menu" role="menu"></div>
39
+ <!-- Nova: três caminhos (o resto fica na vitrine "Modelo pronto"), para quem não quer olhar mil opções -->
40
+ <div class="lmenu tall" id="new-menu" role="menu">
41
+ <span class="menu-label">COMO VAMOS COMEÇAR?</span>
42
+ <button class="mi" data-new="ai"><i class="ic" data-ic="sparkles"></i><span><b>Criar com IA</b><span class="d">Descreva o assunto e/ou anexe arquivos e links (PDF, Word, PowerPoint, Excel, página da web)</span></span></button>
43
+ <button class="mi" data-new="gallery"><i class="ic" data-ic="layout-grid"></i><span><b>Modelo pronto</b><span class="d">Coleções, estilos, demonstrações e exemplos para editar</span></span></button>
44
+ <div class="sep"></div>
45
+ <button class="mi" data-new="blank"><i class="ic" data-ic="file-plus"></i><span><b>Em branco</b></span></button>
46
+ <button class="mi" data-new="import"><i class="ic" data-ic="upload"></i><span><b>Importar apresentação</b><span class="d">PowerPoint (.pptx), do jeito que está, ou .sagadeck</span></span></button>
47
+ </div>
48
+ <div class="lmenu" id="topic-menu" role="menu"></div>
49
+ <input type="file" id="import-input" accept=".sagadeck,.zip,.pptx" hidden>
50
+ <div class="ldlg-back" id="dlg"><div class="ldlg" role="dialog" aria-modal="true" aria-labelledby="dialog-heading"></div></div>
51
+ <div class="ltoast" id="toast" role="status"></div>
52
+ <script src="ui-icons.js"></script>
53
+ <script src="library.js"></script>
54
+ </body>
55
+ </html>
@@ -0,0 +1,471 @@
1
+ // Biblioteca: tópicos (pastas) e apresentações. Os dados vêm de /api/library (a pasta da biblioteca no disco).
2
+ (function () {
3
+ "use strict";
4
+ const $ = (s) => document.querySelector(s);
5
+ const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
6
+ const COLORS = ["#d33a2c", "#0f6cbd", "#e5a50a", "#8b5cf6", "#0e9f6e", "#e8590c", "#d6336c", "#495057"];
7
+ const H = 3600e3, D = 24 * H;
8
+ const store = {
9
+ get(k, d) { try { const v = localStorage.getItem("sagadeck." + k); return v === null ? d : JSON.parse(v); } catch { return d; } },
10
+ set(k, v) { try { localStorage.setItem("sagadeck." + k, JSON.stringify(v)); } catch {} },
11
+ };
12
+ let data = { topics: [], decks: [], trash: [], root: "" };
13
+ let view = new URLSearchParams(location.search).get("topic") || store.get("libView", "recentes");
14
+ let sortBy = store.get("libSort", "editada");
15
+
16
+ function hydrate(root = document) {
17
+ const icons = window.UI_ICONS || {};
18
+ root.querySelectorAll(".ic[data-ic]").forEach((el) => {
19
+ if (el.dataset.done === el.dataset.ic || !icons[el.dataset.ic]) return;
20
+ el.innerHTML = `<svg viewBox="0 0 24 24" aria-hidden="true">${icons[el.dataset.ic]}</svg>`;
21
+ el.dataset.done = el.dataset.ic;
22
+ });
23
+ }
24
+ const ic = (n) => `<i class="ic" data-ic="${n}"></i>`;
25
+ const ago = (t) => { const d = Date.now() - t; return d < H ? "agora há pouco" : d < D ? `há ${Math.round(d / H)} h` : d < 2 * D ? "ontem" : d < 60 * D ? `há ${Math.round(d / D)} dias` : new Date(t).toLocaleDateString("pt-BR"); };
26
+ const plural = (n) => `${n} apresentaç${n === 1 ? "ão" : "ões"}`;
27
+ const topicOf = (id) => data.topics.find((t) => t.id === id);
28
+ // .yaml deixados direto na pasta da biblioteca (pelo Explorer): não estão em tópico nenhum. Não confundir
29
+ // com o tópico "Sem tópico", que é uma pasta de verdade, para onde vai o que é criado sem escolher tópico.
30
+ const NO_TOPIC = { id: "", name: "Soltas na pasta", color: "#8a8a8a" };
31
+
32
+ function toast(msg, ms = 2800) { const t = $("#toast"); t.textContent = msg; t.classList.add("show"); clearTimeout(t._h); t._h = setTimeout(() => t.classList.remove("show"), ms); }
33
+
34
+ async function api(path, body) {
35
+ const r = await fetch(path, body === undefined ? {} : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
36
+ const j = await r.json().catch(() => ({}));
37
+ if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
38
+ return j;
39
+ }
40
+ async function load() {
41
+ data = await api("api/library");
42
+ if (data.user) {
43
+ $("#user").hidden = false;
44
+ $("#avatar").textContent = String(data.user).slice(0, 1);
45
+ $("#uname").textContent = data.user;
46
+ }
47
+ if (view !== "recentes" && view !== "todas" && view !== "lixeira" && !topicOf(view)) view = "recentes";
48
+ render();
49
+ }
50
+ // botão IA: a tela de configuração do modelrelay local, quando existe
51
+ api("api/ai/setup").then(({ url }) => { if (url) { $("#btn-ai").href = url; $("#btn-ai").hidden = false; } }).catch(() => {});
52
+ const openEditor = (id, present) => { location.href = `editor?deck=${encodeURIComponent(id)}${present ? "&present=1" : ""}`; };
53
+
54
+ // ---------------------------------------------------------------- barra lateral
55
+ function renderSide() {
56
+ const loose = data.decks.filter((d) => !d.topic).length;
57
+ $("#side").innerHTML = `
58
+ <button class="nav ${view === "recentes" ? "active" : ""}" data-view="recentes">${ic("clock")}<span class="name">Recentes</span></button>
59
+ <button class="nav ${view === "todas" ? "active" : ""}" data-view="todas">${ic("layout-grid")}<span class="name">Todas</span><span class="count">${data.decks.length || ""}</span></button>
60
+ <div class="side-title">TÓPICOS<button title="Novo tópico" id="new-topic" aria-label="Novo tópico">${ic("plus")}</button></div>
61
+ ${data.topics.map((t) => `<button class="nav ${view === t.id ? "active" : ""}" data-view="${esc(t.id)}" data-topic="${esc(t.id)}"><span class="dot" style="background:${esc(t.color)}"></span><span class="name">${esc(t.name)}</span><span class="count">${t.count || ""}</span></button>`).join("")}
62
+ ${loose ? `<button class="nav ${view === "" ? "active" : ""}" data-view="" title="Arquivos .yaml deixados direto na pasta da biblioteca"><span class="dot" style="background:${NO_TOPIC.color}"></span><span class="name">${NO_TOPIC.name}</span><span class="count">${loose}</span></button>` : ""}
63
+ <div class="grow"></div>
64
+ <div class="side-foot">
65
+ <button class="nav ${view === "lixeira" ? "active" : ""}" data-view="lixeira">${ic("trash-2")}<span class="name">Lixeira</span><span class="count">${data.trash.length || ""}</span></button>
66
+ </div>`;
67
+ hydrate($("#side"));
68
+ $("#side").querySelectorAll("[data-view]").forEach((b) => b.onclick = () => { view = b.dataset.view; store.set("libView", view); render(); });
69
+ $("#new-topic").onclick = () => topicDialog();
70
+ $("#side").querySelectorAll("[data-topic]").forEach((b) => {
71
+ b.ondragover = (e) => { if (e.dataTransfer.types.includes("application/x-sagadeck-deck")) { e.preventDefault(); b.classList.add("drop"); } };
72
+ b.ondragleave = () => b.classList.remove("drop");
73
+ b.ondrop = async (e) => {
74
+ e.preventDefault(); b.classList.remove("drop");
75
+ const id = e.dataTransfer.getData("application/x-sagadeck-deck");
76
+ const deck = data.decks.find((d) => d.id === id);
77
+ if (!deck || deck.topic === b.dataset.topic) return;
78
+ await act(() => api("api/library/decks/move", { id, topic: b.dataset.topic }), `"${deck.title}" movida para ${(topicOf(b.dataset.topic) || NO_TOPIC).name}`);
79
+ };
80
+ b.oncontextmenu = (e) => { if (b.dataset.topic) { e.preventDefault(); openTopicMenu(b, b.dataset.topic); } };
81
+ });
82
+ }
83
+
84
+ // ---------------------------------------------------------------- área principal
85
+ function card(d, withTopic) {
86
+ const t = topicOf(d.topic) || NO_TOPIC;
87
+ return `<div class="card" draggable="true" data-id="${esc(d.id)}" tabindex="0">
88
+ <div class="thumb"><div class="loading">${esc(d.slides)} slides</div>
89
+ <img loading="lazy" alt="" src="api/library/cover?id=${encodeURIComponent(d.id)}&v=${Math.round(d.edited)}" onerror="this.remove()">
90
+ <div class="over"><button class="pill" data-present>${ic("play")}Apresentar</button><button class="pill" data-edit>${ic("pencil")}Editar</button></div></div>
91
+ <div class="meta"><div class="t"><div class="name">${esc(d.title)}</div>
92
+ <div class="info">${withTopic ? `<span class="chip"><span class="dot" style="background:${esc(t.color)}"></span>${esc(t.name)}</span>·` : ""}<span>${d.slides} slides</span>·<span>${ago(d.edited)}</span></div></div>
93
+ <button class="more" data-more title="Mais ações" aria-label="Mais ações">${ic("ellipsis")}</button></div>
94
+ </div>`;
95
+ }
96
+
97
+ function render() {
98
+ renderSide();
99
+ const q = $("#q").value.trim().toLowerCase();
100
+ const match = (d) => !q || d.title.toLowerCase().includes(q);
101
+ const main = $("#main");
102
+ if (view === "lixeira") {
103
+ main.innerHTML = `<div class="head"><div><h1>Lixeira</h1><div class="sub">Fica aqui por 30 dias; depois é apagado de vez.</div></div>${data.trash.length ? `<button class="lib-btn ghost danger" data-empty-trash>${ic("trash-2")}Esvaziar lixeira</button>` : ""}</div>` +
104
+ (data.trash.length ? data.trash.map((t) => `<div class="trash-row"><div class="t"><b>${esc(t.title)}</b><div class="sub">Era de ${esc(t.topic || NO_TOPIC.name)} · ${t.slides || 0} slides · excluída ${ago(t.deleted)}</div></div>
105
+ <button class="lib-btn ghost" data-restore="${esc(t.id)}">${ic("rotate-ccw")}Restaurar</button><button class="lib-btn ghost danger" data-purge="${esc(t.id)}">Excluir de vez</button></div>`).join("")
106
+ : `<div class="empty"><div class="big">${ic("trash-2")}</div><h2>Lixeira vazia</h2></div>`);
107
+ } else {
108
+ const t = view === "" ? NO_TOPIC : topicOf(view);
109
+ const all = t ? data.decks.filter((d) => d.topic === t.id) : data.decks;
110
+ const list = all.filter(match).sort((a, b) => (sortBy === "nome" ? a.title.localeCompare(b.title, "pt-BR") : b.edited - a.edited));
111
+ const items = view === "recentes" && !q ? list.slice(0, 12) : list;
112
+ const title = t ? `<span class="dot" style="background:${esc(t.color)}"></span>${esc(t.name)}` : view === "recentes" ? "Recentes" : "Todas as apresentações";
113
+ const sub = q ? `${plural(list.length)} com "${esc(q)}"` : t ? plural(all.length) : view === "recentes" ? "As que você mexeu por último" : `${plural(all.length)} em ${data.topics.length} tópico${data.topics.length === 1 ? "" : "s"}`;
114
+ const emptyTopic = t && !all.length;
115
+ main.innerHTML = `<div class="head"><div><h1>${title}</h1><div class="sub">${sub}</div></div><div class="spacer"></div>
116
+ ${t && t.id ? `<button class="lib-icon-btn" id="topic-more" title="Tópico: renomear, cor, excluir" aria-label="Opções do tópico">${ic("ellipsis")}</button>` : ""}
117
+ ${emptyTopic || !all.length ? "" : `<select class="sort" id="sort" aria-label="Ordenar"><option value="editada">Editadas recentemente</option><option value="nome">Nome (A–Z)</option></select>`}</div>`;
118
+ if (emptyTopic) {
119
+ main.innerHTML += `<div class="empty"><div class="big">${ic("folder")}</div><h2>Nenhuma apresentação em ${esc(t.name)}</h2><p>Crie uma aqui ou arraste uma de outro tópico para cá.</p>
120
+ <div class="actions"><button class="lib-btn primary" data-empty-new>${ic("plus")}Nova apresentação</button></div></div>`;
121
+ } else if (!data.decks.length && !q) {
122
+ main.innerHTML += `<div class="empty"><div class="big">${ic("library")}</div><h2>Sua biblioteca está vazia</h2>
123
+ <p>Crie um tópico (ex.: Palestras, Trabalho) e comece uma apresentação — em branco, com IA ou importando um .sagadeck ou um PowerPoint (.pptx).</p>
124
+ <div class="actions"><button class="lib-btn ghost" data-empty-topic>${ic("folder-plus")}Novo tópico</button><button class="lib-btn primary" data-empty-new>${ic("plus")}Nova apresentação</button></div></div>`;
125
+ } else if (!list.length) {
126
+ main.innerHTML += `<div class="empty"><div class="big">${ic("search")}</div><h2>Nada encontrado</h2><p>Nenhuma apresentação com "${esc(q)}".</p></div>`;
127
+ } else {
128
+ main.innerHTML += `<div class="grid">${t && !q ? `<div class="card new-card" id="card-new" tabindex="0"><div class="thumb"><div class="plus">${ic("plus")}Nova em ${esc(t.name)}</div></div></div>` : ""}${items.map((d) => card(d, !t)).join("")}</div>`;
129
+ }
130
+ const sortSel = $("#sort");
131
+ if (sortSel) { sortSel.value = sortBy; sortSel.onchange = () => { sortBy = sortSel.value; store.set("libSort", sortBy); render(); }; }
132
+ const tm = $("#topic-more"); if (tm) tm.onclick = () => openTopicMenu(tm, t.id);
133
+ }
134
+ hydrate(main);
135
+ wire(main);
136
+ }
137
+
138
+ function wire(main) {
139
+ main.querySelectorAll(".card[data-id]").forEach((c) => {
140
+ const id = c.dataset.id;
141
+ c.ondragstart = (e) => { e.dataTransfer.setData("application/x-sagadeck-deck", id); c.classList.add("dragging"); };
142
+ c.ondragend = () => c.classList.remove("dragging");
143
+ c.onclick = (e) => {
144
+ if (e.target.closest("[data-more]")) return openCardMenu(e.target.closest("[data-more]"), id);
145
+ openEditor(id, !!e.target.closest("[data-present]"));
146
+ };
147
+ c.onkeydown = (e) => { if (e.key === "Enter") openEditor(id); };
148
+ });
149
+ main.querySelectorAll("[data-restore]").forEach((b) => b.onclick = () => act(() => api("api/library/decks/restore", { slot: b.dataset.restore }), "Restaurada"));
150
+ const empty = main.querySelector("[data-empty-trash]");
151
+ if (empty) empty.onclick = () => { if (confirm(`Apagar de vez ${data.trash.length === 1 ? "a apresentação" : `as ${data.trash.length} apresentações`} da lixeira? Não dá para desfazer.`)) act(() => api("api/library/trash/empty", {}), "Lixeira esvaziada"); };
152
+ main.querySelectorAll("[data-purge]").forEach((b) => b.onclick = () => { if (confirm("Apagar de vez? Não dá para desfazer.")) act(() => api("api/library/decks/purge", { slot: b.dataset.purge }), "Apagada de vez"); });
153
+ main.querySelectorAll("[data-empty-new], #card-new").forEach((b) => b.onclick = () => openNewMenu(b));
154
+ main.querySelectorAll("[data-empty-topic]").forEach((b) => b.onclick = () => topicDialog());
155
+ }
156
+
157
+ async function act(fn, msg) {
158
+ try { await fn(); if (msg) toast(msg); await load(); }
159
+ catch (e) { toast("Não deu: " + e.message, 5000); }
160
+ }
161
+
162
+ // ---------------------------------------------------------------- menus
163
+ function place(menu, anchor) {
164
+ closeMenus();
165
+ const r = anchor.getBoundingClientRect();
166
+ menu.style.maxHeight = ""; menu.style.overflowY = "";
167
+ menu.classList.add("open");
168
+ // janela baixa (ou menu comprido, como o Nova com os modelos): cabe na tela, com rolagem, em vez de vazar
169
+ if (menu.offsetHeight > innerHeight - 16) { menu.style.maxHeight = innerHeight - 16 + "px"; menu.style.overflowY = "auto"; }
170
+ const w = menu.offsetWidth, h = menu.offsetHeight;
171
+ menu.style.left = Math.max(8, Math.min(r.right - w, innerWidth - w - 8)) + "px";
172
+ menu.style.top = (r.bottom + 6 + h <= innerHeight ? r.bottom + 6 : r.top - h - 6 >= 8 ? r.top - h - 6 : Math.max(8, innerHeight - h - 8)) + "px";
173
+ }
174
+ function closeMenus() { document.querySelectorAll(".lmenu").forEach((m) => m.classList.remove("open")); }
175
+
176
+ function openCardMenu(anchor, id) {
177
+ const d = data.decks.find((x) => x.id === id);
178
+ const m = $("#card-menu");
179
+ const others = [...data.topics.filter((t) => t.id !== d.topic)];
180
+ m.innerHTML = `
181
+ <button class="mi" data-a="open">${ic("pencil")}Abrir</button>
182
+ <button class="mi" data-a="present">${ic("play")}Apresentar</button>
183
+ <div class="sep"></div>
184
+ <button class="mi" data-a="rename">Renomear</button>
185
+ <button class="mi" data-a="dup">${ic("copy")}Duplicar</button>
186
+ ${others.map((t) => `<button class="mi" data-move="${esc(t.id)}"><span class="dot" style="background:${esc(t.color)}"></span>Mover para ${esc(t.name)}</button>`).join("")}
187
+ <div class="sep"></div>
188
+ <button class="mi" data-dl="tudo">${ic("download")}Baixar tudo<small>PowerPoint, PDF e roteiro</small></button>
189
+ <button class="mi" data-dl="sagadeck">${ic("download")}Baixar apresentação<small>.sagadeck</small></button>
190
+ <button class="mi" data-dl="pptx">Baixar PowerPoint<small>.pptx</small></button>
191
+ <button class="mi" data-dl="pptx" data-notas="0">Baixar PowerPoint sem as notas<small>.pptx</small></button>
192
+ <button class="mi" data-dl="pdf">Baixar PDF</button>
193
+ <div class="sep"></div>
194
+ <button class="mi danger" data-a="trash">${ic("trash-2")}Mover para a lixeira</button>`;
195
+ hydrate(m);
196
+ place(m, anchor);
197
+ m.querySelectorAll("[data-a]").forEach((b) => b.onclick = () => {
198
+ closeMenus();
199
+ const a = b.dataset.a;
200
+ if (a === "open" || a === "present") return openEditor(id, a === "present");
201
+ if (a === "rename") return nameDialog("Renomear apresentação", d.title, (title) => act(() => api("api/library/decks/rename", { id, title }), "Renomeada"));
202
+ if (a === "dup") return act(() => api("api/library/decks/duplicate", { id }), "Cópia criada");
203
+ if (a === "trash") return act(() => api("api/library/decks/trash", { id }), "Na lixeira — dá para restaurar por 30 dias");
204
+ });
205
+ m.querySelectorAll("[data-move]").forEach((b) => b.onclick = () => { closeMenus(); act(() => api("api/library/decks/move", { id, topic: b.dataset.move }), `Movida para ${topicOf(b.dataset.move).name}`); });
206
+ m.querySelectorAll("[data-dl]").forEach((b) => b.onclick = () => {
207
+ closeMenus();
208
+ if (b.dataset.dl !== "sagadeck") toast("Gerando… o download começa em alguns segundos.", 6000);
209
+ location.href = `api/library/download?id=${encodeURIComponent(id)}&kind=${b.dataset.dl}${b.dataset.notas === "0" ? "&notas=0" : ""}`;
210
+ });
211
+ }
212
+
213
+ function openTopicMenu(anchor, id) {
214
+ const t = topicOf(id), m = $("#topic-menu");
215
+ m.innerHTML = `<button class="mi" data-t="edit">${ic("pencil")}Renomear ou trocar a cor</button>
216
+ <button class="mi danger" data-t="del">${ic("trash-2")}Excluir tópico${t.count ? " (precisa estar vazio)" : ""}</button>`;
217
+ hydrate(m);
218
+ place(m, anchor);
219
+ m.querySelector('[data-t="edit"]').onclick = () => { closeMenus(); topicDialog(t); };
220
+ m.querySelector('[data-t="del"]').onclick = () => { closeMenus(); act(async () => { await api("api/library/topics/delete", { id }); view = "recentes"; }, "Tópico excluído"); };
221
+ }
222
+
223
+ function openNewMenu(anchor) {
224
+ const m = $("#new-menu");
225
+ place(m, anchor);
226
+ const topic = topicOf(view) ? view : "";
227
+ m.querySelectorAll("[data-new]").forEach((b) => b.onclick = () => { closeMenus(); startNew(b.dataset.new, topic); });
228
+ }
229
+
230
+ // Um jeito de começar (menu Nova ou cartão da vitrine) → a apresentação nova, já aberta no editor
231
+ function startNew(key, topic) {
232
+ const create = async (path, body) => {
233
+ try { const { id } = await api(path, { topic, ...body }); openEditor(id); } catch (e) { toast("Não deu: " + e.message, 5000); }
234
+ };
235
+ if (key === "blank") return nameDialog("Nova apresentação", "", (title) => create("api/library/decks", { title }), "Título");
236
+ if (key === "ai" || key === "ai-file") return aiDialog(topic);
237
+ if (key === "gallery") return galleryDialog(topic);
238
+ // tudo da vitrine abre em prévia: a cópia só nasce quando a pessoa muda algo ou clica "Usar como base".
239
+ // Modelo de fábrica cai em "Modelos" se não veio de um tópico; estilo e exemplo, no tópico atual (ou sem tópico).
240
+ if (/^(model|exp|example)-/.test(key)) {
241
+ const dest = topic || (key.startsWith("model-") ? "Modelos" : "");
242
+ location.href = `editor?model=${encodeURIComponent(key.startsWith("model-") ? key.slice(6) : key)}&topic=${encodeURIComponent(dest)}`;
243
+ return;
244
+ }
245
+ $("#import-input").dataset.topic = topic;
246
+ $("#import-input").click();
247
+ }
248
+
249
+ // Vitrine "Modelo pronto": tudo o que já vem pronto num lugar só, em dois grupos pelo que a pessoa quer fazer:
250
+ // "visual" (um visual completo para trocar o texto) e "recurso" (mostra do que o SagaDeck é capaz).
251
+ // O cartão mostra a capa desenhada de verdade (api/library/gallery-cover); as cores são só o fundo enquanto carrega.
252
+ const GALLERY = [
253
+ ["visual", "model-perspectiva", "Perspectiva", "Corporativo com fotografia, agenda e contraste", ["#1F2A44", "#E9E4DA", "#C8553D"]],
254
+ ["visual", "model-essencial", "Essencial", "Minimalismo, espaço e detalhes em preto e branco", ["#F5F5F2", "#1A1A1A", "#9A9A94"]],
255
+ ["visual", "model-revista", "Revista", "Serifas, capítulos e fotografia editorial", ["#FAF7F0", "#1C1C1C", "#B23A2E"]],
256
+ ["visual", "model-cromatico", "Cromático", "Cor, fotos e painéis sobrepostos", ["#F2B33D", "#1F4AA8", "#D93A2B"]],
257
+ ["visual", "model-tracos", "Traços", "Geometria, molduras e caminhos visuais", ["#F3EEE3", "#1A1A1A", "#D93A2B"]],
258
+ ["visual", "model-lavanda", "Estúdio lavanda", "Mosaicos, cápsulas e composições editoriais", ["#EFE9FB", "#5B3FA8", "#D8C8F5"]],
259
+ ["visual", "model-executivo", "Relatório executivo", "Resumo, indicadores e plano de trabalho", ["#0F1115", "#E4B660", "#EDEBE6"]],
260
+ ["visual", "model-workshop", "Workshop visual", "Perguntas, código guiado e consulta", ["#FFF7E6", "#1A1A1A", "#E8590C"]],
261
+ ["recurso", "model-explorar", "Explore uma ideia", "Prever, comparar cenários, curvas conectadas e acompanhar o mesmo objeto entre cenas", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
262
+ ["recurso", "model-novidades", "Novidades", "Carrossel, gráfico do Excel e da planilha do projeto, fórmulas vivas, duelo de commits, terminais e zoom", ["#F4F1EA", "#1B1F3B", "#E8590C"]],
263
+ ["recurso", "model-hidraulica", "Aula de hidráulica", "Navier–Stokes, curva que se mexe, calculadora de Reynolds ao vivo e exercício resolvido passo a passo", ["#EAF4F8", "#0B5C7A", "#1B998B"]],
264
+ ["recurso", "model-algoritmos", "Aula de algoritmos", "Ordenação e busca rodando de verdade: barras, código, contadores e o botão Tocar", ["#0F1426", "#7C8CFF", "#F2B33D"]],
265
+ ["recurso", "model-compacto", "Material de consulta", "Código completo, JSON e páginas compactas", ["#F7F6F2", "#20242B", "#0F6CBD"]],
266
+ ["recurso", "model-avancado", "Recursos avançados", "Conteúdo denso, avisos, diagramas e tipografia cinética", ["#F7F6F2", "#20242B", "#E8590C"]],
267
+ ["recurso", "model-diagramas", "Diagramas vivos", "Fluxo, sequência, estados e mapa mental", ["#F4F7FB", "#1F6FB2", "#5DBB86"]],
268
+ ["recurso", "example-api", "Aula de APIs ao vivo", "Slides que executam requisições de verdade. Roda sem configurar nada", ["#111B2B", "#5B8DEF", "#ECF1FF"]],
269
+ ["recurso", "example-cenario", "Texto no cenário", "As 13 composições: fundo, transparência, recorte na frente das letras", ["#0A0D17", "#00F2FE", "#FF007A"]],
270
+ ];
271
+ const GALLERY_KINDS = [["", "Todos"], ["visual", "Visuais para começar"], ["recurso", "Recursos do SagaDeck"]];
272
+ async function galleryDialog(topic, kind = "") {
273
+ if (kind === "demo") kind = "recurso"; // link antigo: biblioteca?galeria=demo
274
+ let items = GALLERY;
275
+ try {
276
+ const { experiences } = await api("api/experiences");
277
+ items = [...GALLERY.slice(0, 8), ...experiences.map((x) => ["visual", `exp-${x.id}`, x.name, x.description, [x.background, x.accent, x.color]]), ...GALLERY.slice(8)];
278
+ } catch { /* sem os estilos prontos, a vitrine segue com o resto */ }
279
+ dialog(`<h3 id="dialog-heading">Modelo pronto</h3>
280
+ <p class="vit-intro">Escolha um ponto de partida. Tudo abre em prévia: você navega e experimenta à vontade, e a sua cópia só é criada quando você muda algo.</p>
281
+ <div class="vit-filters" role="tablist">${GALLERY_KINDS.map(([k, l], i) => `<button class="vit-filter ${i ? "" : "active"}" data-kind="${k}">${l}</button>`).join("")}</div>
282
+ <div class="vit-grid">${items.map(([kind, key, name, desc, sw]) => `<button class="vit-card" data-new="${esc(key)}" data-kind="${kind}">
283
+ <span class="vit-thumb" style="background:linear-gradient(90deg,${sw.map(esc).join(",")})"><img loading="lazy" alt="" src="api/library/gallery-cover?key=${encodeURIComponent(key)}" onerror="this.remove()"></span>
284
+ <b>${esc(name)}</b><span class="d">${esc(desc)}</span></button>`).join("")}</div>
285
+ <div class="row"><button class="lib-btn ghost" data-cancel>Fechar</button></div>`, (box, close) => {
286
+ box.classList.add("wide");
287
+ box.querySelectorAll(".vit-filter").forEach((f) => f.onclick = () => {
288
+ box.querySelectorAll(".vit-filter").forEach((x) => x.classList.toggle("active", x === f));
289
+ box.querySelectorAll(".vit-card").forEach((c) => { c.hidden = !!f.dataset.kind && c.dataset.kind !== f.dataset.kind; });
290
+ });
291
+ box.querySelectorAll(".vit-card").forEach((c) => c.onclick = () => { close(); startNew(c.dataset.new, topic); });
292
+ if (kind) box.querySelector(`.vit-filter[data-kind="${kind}"]`)?.click(); // biblioteca?galeria=demo
293
+ });
294
+ }
295
+
296
+ // ---------------------------------------------------------------- diálogos
297
+ function dialog(html, onOpen) {
298
+ const back = $("#dlg"), box = back.firstElementChild;
299
+ box.className = "ldlg"; // tira o "wide" de um diálogo anterior
300
+ box.innerHTML = html;
301
+ hydrate(box);
302
+ back.classList.add("open");
303
+ const close = () => back.classList.remove("open");
304
+ back.onclick = (e) => { if (e.target === back) close(); };
305
+ box.querySelectorAll("[data-cancel]").forEach((b) => b.onclick = close);
306
+ onOpen(box, close);
307
+ box.querySelector("input, textarea")?.focus();
308
+ }
309
+ function nameDialog(title, value, onOk, label = "Nome") {
310
+ dialog(`<h3>${esc(title)}</h3><label>${esc(label)}</label><input type="text" id="dlg-name" value="${esc(value)}">
311
+ <div class="row"><button class="lib-btn ghost" data-cancel>Cancelar</button><button class="lib-btn primary" id="dlg-ok">OK</button></div>`, (box, close) => {
312
+ const go = () => { const v = box.querySelector("#dlg-name").value.trim(); if (!v) return; close(); onOk(v); };
313
+ box.querySelector("#dlg-ok").onclick = go;
314
+ box.querySelector("#dlg-name").onkeydown = (e) => { if (e.key === "Enter") go(); };
315
+ });
316
+ }
317
+ function topicDialog(t) {
318
+ let color = t?.color || COLORS[data.topics.length % COLORS.length];
319
+ dialog(`<h3>${t ? "Tópico" : "Novo tópico"}</h3><label>Nome</label><input type="text" id="dlg-name" value="${esc(t?.name || "")}" placeholder="Ex.: Palestras, Trabalho, Aulas">
320
+ <label>Cor</label><div class="swatches">${COLORS.map((c) => `<button class="sw ${c === color ? "on" : ""}" data-c="${c}" style="background:${c}" aria-label="cor ${c}"></button>`).join("")}</div>
321
+ <div class="row"><button class="lib-btn ghost" data-cancel>Cancelar</button><button class="lib-btn primary" id="dlg-ok">${t ? "Salvar" : "Criar"}</button></div>`, (box, close) => {
322
+ box.querySelectorAll(".sw").forEach((s) => s.onclick = () => { color = s.dataset.c; box.querySelectorAll(".sw").forEach((x) => x.classList.toggle("on", x === s)); });
323
+ const go = async () => {
324
+ const name = box.querySelector("#dlg-name").value.trim();
325
+ if (!name) return;
326
+ close();
327
+ await act(async () => {
328
+ const r = t ? await api("api/library/topics/update", { id: t.id, name, color }) : await api("api/library/topics", { name, color });
329
+ view = r.id; store.set("libView", view);
330
+ }, t ? "Tópico atualizado" : `Tópico "${name}" criado`);
331
+ };
332
+ box.querySelector("#dlg-ok").onclick = go;
333
+ box.querySelector("#dlg-name").onkeydown = (e) => { if (e.key === "Enter") go(); };
334
+ });
335
+ }
336
+ // Estilos = as coleções (COLLECTION_STYLE em src/studio/template-collections.js decide tema e direção criativa)
337
+ const STYLES = [["perspectiva", "Perspectiva: corporativo fotográfico"], ["essencial", "Essencial: minimalismo"], ["revista", "Revista: editorial"], ["cromatico", "Cromático: cor e fotografia"], ["tracos", "Traços: geometria criativa"], ["manual", "Documentação técnica: clara (boa para imprimir)"], ["manual-noite", "Documentação técnica: escura (descansa a vista)"]];
338
+ // mesma conta de slidesForMinutes (src/ai/deck-ai.js): ~1 slide a cada 1,5 min, entre 3 e 40
339
+ const slidesFor = (min) => Math.min(40, Math.max(3, Math.round(Number(min) / 1.5)));
340
+ const readAsDataURL = (f) => new Promise((ok, bad) => { const r = new FileReader(); r.onload = () => ok(r.result); r.onerror = () => bad(r.error); r.readAsDataURL(f); });
341
+
342
+ // "Descrever com IA" e "A partir de um arquivo ou link": o mesmo diálogo, numa tela só (assunto, tempo, estilo,
343
+ // material de apoio). O material vira texto no servidor (/api/ai/context) e vai para a IA junto com o pedido.
344
+ function aiDialog(topic) {
345
+ const docs = [];
346
+ let reading = 0;
347
+ dialog(`<h3 id="dialog-heading">Nova apresentação com IA</h3>
348
+ <label for="dlg-brief">Sobre o que é, para quem e com que objetivo? <span class="muted">Ou só anexe o material abaixo.</span></label>
349
+ <textarea id="dlg-brief" placeholder="Ex.: palestra para gestores sobre golpes no Pix, tom leve, começando com um caso real. Ou: resumo deste relatório para a diretoria, focando nos números do ano."></textarea>
350
+ <div class="dlg-two">
351
+ <div><label for="dlg-min">Quanto tempo você tem?</label><div class="dlg-min"><input type="number" id="dlg-min" min="1" max="120" value="10"><span>min</span><span class="hint" id="dlg-slides"></span></div></div>
352
+ <div><label for="dlg-style">Estilo</label><select id="dlg-style"><option value="">Automático (a IA escolhe pelo assunto)</option>${STYLES.map(([v, l]) => `<option value="${v}">${esc(l)}</option>`).join("")}</select></div>
353
+ </div>
354
+ <label>Material de apoio <span class="muted">(opcional: arquivos ou links; dá para arrastar arquivos para cá)</span></label>
355
+ <div class="dlg-att"><button class="lib-btn ghost" id="dlg-file" type="button">${ic("paperclip")}Anexar arquivo</button>
356
+ <input type="file" id="dlg-files" multiple hidden accept=".pdf,.doc,.docx,.pptx,.xlsx,.txt,.md,.csv">
357
+ <input type="text" id="dlg-link" placeholder="ou cole um link (https://…)"><button class="lib-btn ghost" id="dlg-link-add" type="button">Adicionar</button></div>
358
+ <div class="dlg-chips" id="dlg-chips"></div>
359
+ <div class="status" id="dlg-status"></div>
360
+ <div class="row"><button class="lib-btn ghost" data-cancel>Cancelar</button><button class="lib-btn primary" id="dlg-ok">${ic("sparkles")}Gerar</button></div>`, (box) => {
361
+ box.classList.add("wide");
362
+ const status = box.querySelector("#dlg-status"), btn = box.querySelector("#dlg-ok"), min = box.querySelector("#dlg-min");
363
+ const showSlides = () => { box.querySelector("#dlg-slides").textContent = Number(min.value) > 0 ? `≈ ${slidesFor(min.value)} slides` : ""; };
364
+ min.oninput = showSlides; showSlides();
365
+ const chips = () => {
366
+ box.querySelector("#dlg-chips").innerHTML = docs.map((d, i) => `<span class="dlg-chip">${ic(d.url ? "link" : "file-text")}<span>${esc(d.name)}${d.detail ? ` · ${esc(d.detail)}` : ""}</span><button type="button" data-rm="${i}" aria-label="Tirar ${esc(d.name)}">${ic("x")}</button></span>`).join("")
367
+ + (reading ? `<span class="dlg-chip reading">lendo ${reading} material(is)…</span>` : "");
368
+ hydrate(box.querySelector("#dlg-chips"));
369
+ box.querySelectorAll("[data-rm]").forEach((b) => b.onclick = () => { docs.splice(+b.dataset.rm, 1); chips(); });
370
+ btn.disabled = reading > 0;
371
+ };
372
+ const add = async (body, label) => {
373
+ reading++; chips();
374
+ try { docs.push({ ...(await api("api/ai/context", body)), url: !!body.url }); status.textContent = ""; }
375
+ catch (e) { status.textContent = `Não deu para ler ${label}: ${e.message}`; }
376
+ reading--; chips();
377
+ };
378
+ const files = box.querySelector("#dlg-files");
379
+ box.querySelector("#dlg-file").onclick = () => files.click();
380
+ files.onchange = async () => {
381
+ for (const f of [...files.files]) add({ name: f.name, dataUrl: await readAsDataURL(f) }, f.name);
382
+ files.value = "";
383
+ };
384
+ const link = box.querySelector("#dlg-link");
385
+ const addLink = () => { const u = link.value.trim(); if (!/^https?:\/\//i.test(u)) { status.textContent = "Cole um link que comece com http:// ou https://"; return; } link.value = ""; add({ url: u }, u); };
386
+ box.querySelector("#dlg-link-add").onclick = addLink;
387
+ link.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); addLink(); } };
388
+ // arrastar arquivos para dentro do modal também anexa
389
+ box.addEventListener("dragover", (e) => { if ([...(e.dataTransfer?.types || [])].includes("Files")) { e.preventDefault(); box.classList.add("drop-over"); } });
390
+ box.addEventListener("dragleave", (e) => { if (!box.contains(e.relatedTarget)) box.classList.remove("drop-over"); });
391
+ box.addEventListener("drop", async (e) => {
392
+ if (!e.dataTransfer?.files?.length) return;
393
+ e.preventDefault(); box.classList.remove("drop-over");
394
+ for (const f of [...e.dataTransfer.files]) add({ name: f.name, dataUrl: await readAsDataURL(f) }, f.name);
395
+ });
396
+ let answer = "";
397
+ // a IA perguntou para que serve o material: opções clicáveis + campo livre; responder gera de novo
398
+ const showQuestion = (q) => {
399
+ status.innerHTML = `<div class="dlg-ask"><b></b><div class="dlg-ask-opts"></div><div class="dlg-ask-free"><input type="text" id="dlg-answer" placeholder="Ou responda com as suas palavras"><button class="lib-btn primary" type="button" id="dlg-answer-go">Responder</button></div></div>`;
400
+ status.querySelector("b").textContent = q.question;
401
+ const opts = status.querySelector(".dlg-ask-opts");
402
+ for (const o of q.options || []) { const b = document.createElement("button"); b.type = "button"; b.className = "lib-btn ghost"; b.textContent = o; b.onclick = () => { answer = o; btn.click(); }; opts.append(b); }
403
+ status.querySelector("#dlg-answer-go").onclick = () => { answer = status.querySelector("#dlg-answer").value.trim(); if (answer) btn.click(); };
404
+ btn.disabled = false;
405
+ };
406
+ btn.onclick = async () => {
407
+ let briefing = box.querySelector("#dlg-brief").value.trim();
408
+ if (!briefing && !docs.length) { status.textContent = "Conte sobre o que é a apresentação ou anexe um arquivo ou link."; return; }
409
+ if (!briefing) briefing = "Transforme o material anexado numa apresentação clara e bem redigida, para quem não leu o material.";
410
+ btn.disabled = true;
411
+ status.textContent = "Gerando… pode levar um minuto.";
412
+ const payload = { topic, briefing, answer: answer || undefined, stream: true, duration: Number(min.value) || undefined, style: box.querySelector("#dlg-style").value || undefined, materials: docs.map((d) => d.id) };
413
+ try {
414
+ const res = await fetch("api/library/decks/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
415
+ if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
416
+ const reader = res.body.getReader(), dec = new TextDecoder();
417
+ let buf = "", result = null;
418
+ for (;;) {
419
+ const { value, done } = await reader.read();
420
+ if (done) break;
421
+ buf += dec.decode(value, { stream: true });
422
+ let nl;
423
+ while ((nl = buf.indexOf("\n")) >= 0) {
424
+ const line = buf.slice(0, nl); buf = buf.slice(nl + 1);
425
+ if (!line.trim()) continue;
426
+ const ev = JSON.parse(line);
427
+ if (ev.type === "progress" && ev.text) status.textContent = ev.text;
428
+ if (ev.type === "result") result = ev.data;
429
+ if (ev.type === "error") throw new Error(ev.error || "falhou");
430
+ }
431
+ }
432
+ if (result?.question) { showQuestion(result.question); return; }
433
+ if (!result?.id) throw new Error("a geração não terminou");
434
+ openEditor(result.id);
435
+ } catch (e) {
436
+ status.textContent = "Não deu: " + e.message;
437
+ btn.disabled = false;
438
+ }
439
+ };
440
+ });
441
+ }
442
+
443
+ // ---------------------------------------------------------------- topo
444
+ $("#btn-new").onclick = (e) => { e.stopPropagation(); openNewMenu($("#btn-new")); };
445
+ $("#import-input").onchange = async (e) => {
446
+ const f = e.target.files[0];
447
+ e.target.value = "";
448
+ if (!f) return;
449
+ try {
450
+ const r = await fetch(`api/library/import?topic=${encodeURIComponent(e.target.dataset.topic || "")}&name=${encodeURIComponent(f.name)}`, { method: "POST", body: f });
451
+ const j = await r.json();
452
+ if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`);
453
+ toast(/\.pptx$/i.test(f.name) ? `"${f.name}" importada: ${j.slides} slides${j.snapshots ? `, com a foto de cada slide do original (${j.snapBy})` : ""}` : `"${f.name}" importada`, 6000);
454
+ await load();
455
+ } catch (err) { toast("Não deu para importar: " + err.message, 6000); }
456
+ };
457
+ $("#q").oninput = () => { if (view === "lixeira") view = "todas"; render(); };
458
+ $("#btn-theme").onclick = () => {
459
+ const dark = document.documentElement.dataset.theme !== "dark";
460
+ document.documentElement.dataset.theme = dark ? "dark" : "light";
461
+ store.set("appTheme", dark ? "dark" : "light");
462
+ };
463
+ document.addEventListener("click", (e) => { if (!e.target.closest(".lmenu, [data-more], #btn-new, #card-new, [data-empty-new], #topic-more")) closeMenus(); });
464
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") { closeMenus(); $("#dlg").classList.remove("open"); } });
465
+ hydrate();
466
+ load().then(() => {
467
+ // biblioteca?galeria=demo: abre a vitrine Modelo pronto já filtrada (link "Abrir demos completos" do editor)
468
+ const g = new URLSearchParams(location.search).get("galeria");
469
+ if (g != null) galleryDialog(topicOf(view) ? view : "", g === "1" ? "" : g);
470
+ }).catch((e) => { $("#main").innerHTML = `<div class="empty"><h2>Não deu para abrir a biblioteca</h2><p>${esc(e.message)}</p></div>`; });
471
+ })();