sagadeck 1.7.0 → 1.7.1

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.
@@ -1,4 +1,4 @@
1
- /* Alumínio (lançamento de iPhone e iPad, 2020-2021): prata metálico com brilho de luz de estúdio, alumínio escovado,
1
+ /* Alumínio (lançamento de aparelho premium): prata metálico com brilho de luz de estúdio, alumínio escovado,
2
2
  títulos cromados (a cor continua sólida em "color": o fiscal mede o contraste por ela), cartões de metal com
3
3
  chanfro de luz; capa com o título embaixo à esquerda e um disco de metal polido à direita; o escuro é grafite */
4
4
  /* o metal sai das cores do tom (--bg, --fg, --surface): com uma paleta por cima, ele acompanha */
@@ -1,4 +1,4 @@
1
- /* Prata (Keynote): nada de enfeite; muito respiro, tudo centralizado, título contido, número da seção discreto */
1
+ /* Prata: nada de enfeite; muito respiro, tudo centralizado, título contido, número da seção discreto */
2
2
  .th-prata .L-cover:not(:has(.cv-fig)) .cv-main,.th-prata .L-end:not(:has(.en-fig)) .en-main{align-items:center;text-align:center;gap:28px}
3
3
  .th-prata :is(.L-cover,.L-end) .ttl{max-width:1300px}
4
4
  .th-prata .kicker{color:var(--muted);letter-spacing:.02em}
@@ -7,7 +7,7 @@ const providerName = (ai) => Object.values(PROVIDERS).find((p) => p.url === ai.u
7
7
 
8
8
  export function studioBanner({ version, url, library, ai = null, multiuser = false, usersDir = "", pid = process.pid, isLibrary = true }) {
9
9
  const iaLine = !ai || !(ai.url || ai.adaptador)
10
- ? " IA: não configurada. No Studio, clique em IA desligada (ou Configurar IA na biblioteca) e informe provedor e chave."
10
+ ? (ai?.problem ? ` IA: não configurada. ${ai.problem}` : " IA: não configurada. No Studio, clique em IA desligada (ou Configurar IA na biblioteca) e informe provedor e chave.")
11
11
  : ` IA: ${providerName(ai)} · texto ${ai.textModel}${ai.source === "variável" ? " (pelas variáveis SAGADECK_LLM_*)" : ""}`;
12
12
  return [
13
13
  `✓ SagaDeck Studio ${version} pronto. Abra no navegador: ${url}${isLibrary ? " (biblioteca)" : ""}`,
@@ -23,7 +23,10 @@ function draft(body, current) {
23
23
  export async function iaRoutes({ req, res, pathname, opts, readJSON }) {
24
24
  if (pathname === "/api/ia" && req.method === "GET") {
25
25
  const cfg = llmConfig();
26
- json(res, 200, { config: maskedIA(loadIA()), file: iaFile(), source: cfg.source, configured: !!(cfg.url || cfg.adaptador), providers: PROVIDERS, roles: ROLES, recommended: RECOMMENDED, editable: !opts.multiuser });
26
+ // no servidor, quem usa o portal não configura: só fica sabendo se a IA está configurada (provedor, endereço,
27
+ // modelos e o final da chave são de quem administra)
28
+ if (opts.multiuser) { json(res, 200, { editable: false, configured: !!(cfg.url || cfg.adaptador) }); return true; }
29
+ json(res, 200, { config: maskedIA(loadIA()), file: iaFile(), source: cfg.source, configured: !!(cfg.url || cfg.adaptador), problem: cfg.problem || "", providers: PROVIDERS, roles: ROLES, recommended: RECOMMENDED, editable: !opts.multiuser });
27
30
  return true;
28
31
  }
29
32
  if ((pathname === "/api/ia" || pathname === "/api/ia/test") && req.method === "POST") {
@@ -21,6 +21,15 @@ window.SagaAISettings = (() => {
21
21
  document.body.append(dialog);
22
22
  }
23
23
 
24
+ // no servidor (multiusuário) a configuração é de quem administra e não vem para a página: só se está ligada
25
+ function serverNotice() {
26
+ dialog.innerHTML = `<h2>Configurar IA</h2>
27
+ <p>Neste servidor, quem configura a IA é quem administra.</p>
28
+ <p>${info.configured ? "A IA está configurada." : "A IA não está configurada. Avise quem administra o servidor."}</p>
29
+ <div class="row"><button type="button" data-close>Fechar</button></div>`;
30
+ dialog.querySelector("[data-close]").onclick = () => dialog.close();
31
+ }
32
+
24
33
  function form() {
25
34
  const c = info.config || {}, providers = info.providers || {};
26
35
  const provider = c.provider && (providers[c.provider] || c.provider === "outro") ? c.provider : (c.url ? "outro" : "openrouter");
@@ -28,6 +37,7 @@ window.SagaAISettings = (() => {
28
37
  <p>O sagadeck fala direto com o provedor que você escolher. A chave fica só nesta máquina, em <code>${esc(info.file)}</code>.</p>
29
38
  ${info.source === "variável" ? '<p class="warn">As variáveis SAGADECK_LLM_URL / SAGADECK_LLM_KEY estão definidas e valem por cima desta tela.</p>' : ""}
30
39
  ${info.editable ? "" : '<p class="warn">Neste servidor, quem configura a IA é quem administra.</p>'}
40
+ ${info.problem ? `<p class="warn" data-problem>${esc(info.problem)}</p>` : ""}
31
41
  ${info.recommended ? `<div class="rec"><b>Recomendado para começar:</b> ${esc(providers[info.recommended.provider]?.label || info.recommended.provider)}, com ${esc(info.recommended.models.text)} (texto e visão), ${esc(info.recommended.models.image)} (imagem) e ${esc(info.recommended.models.search)} (busca). Se a IA não responder nem com isso, o problema é a rede ou a chave, não o modelo. Depois, troque pelo modelo que quiser. <button type="button" data-rec>Usar a recomendação</button></div>` : ""}
32
42
  <label for="ai-cfg-provider">Provedor</label>
33
43
  <select id="ai-cfg-provider" data-f="provider">${Object.entries(providers).map(([k, p]) => `<option value="${k}">${esc(p.label)}</option>`).join("")}<option value="outro">Outro compatível com a API da OpenAI</option></select>
@@ -79,7 +89,7 @@ window.SagaAISettings = (() => {
79
89
  onSaved = typeof saved === "function" ? saved : () => {};
80
90
  if (!dialog) build();
81
91
  info = await (await fetch("api/ia")).json();
82
- form();
92
+ if (info.editable) form(); else serverNotice();
83
93
  if (!dialog.open) dialog.showModal();
84
94
  },
85
95
  };
@@ -3044,9 +3044,11 @@
3044
3044
  }
3045
3045
  const on = !!state.ai.available;
3046
3046
  dom.aiStatus.textContent = on ? "IA ligada" : "IA desligada";
3047
- dom.aiStatus.title = on
3048
- ? `LLM em ${state.ai.url} · texto: ${state.ai.textModel} · imagem: ${state.ai.imageModel}`
3049
- : state.ai.configured ? `Nenhum LLM respondendo em ${state.ai.url}. Clique para conferir o provedor e a chave.` : "A IA não está configurada. Clique para informar provedor e chave.";
3047
+ dom.aiStatus.title = state.ai.server // no servidor, sem os detalhes (são de quem administra)
3048
+ ? (on ? "A IA deste servidor está ligada." : state.ai.configured ? "A IA deste servidor não está respondendo. Avise quem administra." : "A IA deste servidor não está configurada. Avise quem administra.")
3049
+ : on ? `LLM em ${state.ai.url} · texto: ${state.ai.textModel} · imagem: ${state.ai.imageModel}`
3050
+ : state.ai.configured ? `Nenhum LLM respondendo em ${state.ai.url}. Clique para conferir o provedor e a chave.`
3051
+ : state.ai.problem ? `A IA não está configurada. ${state.ai.problem}` : "A IA não está configurada. Clique para informar provedor e chave.";
3050
3052
  dom.aiStatus.classList.toggle("on", on);
3051
3053
  dom.aiStatus.classList.toggle("off", !on);
3052
3054
  }
@@ -3154,7 +3156,7 @@
3154
3156
  }
3155
3157
  dom.btnRunAiDeck.disabled = true;
3156
3158
  const started = Date.now();
3157
- let phase = `Enviando para ${state.ai.textModel || "o LLM"}…`;
3159
+ let phase = `Enviando para ${state.ai.textModel || "a IA"}…`;
3158
3160
  const show = () => {
3159
3161
  dom.aiDeckStatus.textContent = `${phase} · ${Math.round((Date.now() - started) / 1000)}s`;
3160
3162
  };
@@ -127,7 +127,7 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
127
127
  await fetch("api/ai/chat/cancel", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ requestId: job.id }) }).catch(() => {});
128
128
  };
129
129
  const work = createProgressBubble(state.ai.available
130
- ? `Enviando para o LLM (${state.ai.textModel})…`
130
+ ? (state.ai.textModel ? `Enviando para o LLM (${state.ai.textModel})…` : "Enviando para a IA…")
131
131
  : "Verificando se a IA está no ar…");
132
132
  dom.chatSend.disabled = false;
133
133
  dom.chatSend.title = 'Parar processamento'; dom.chatSend.setAttribute('aria-label','Parar');
@@ -30,7 +30,8 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
30
30
  .lib-search kbd { border: 1px solid var(--ui-line); border-radius: 3px; font-size: 10px; padding: 0 4px; }
31
31
  .ai-banner { display: flex; align-items: center; gap: 12px; margin: 10px 24px 0; padding: 10px 14px; border-radius: 10px; background: #fef3c7; color: #78350f; border: 1px solid #f59e0b; font-size: 13px; }
32
32
  .ai-banner[hidden] { display: none; }
33
- .ai-banner span { flex: 1; }
33
+ .ai-banner span { flex: 1; min-width: 0; overflow-wrap: anywhere; } /* o motivo pode trazer um caminho longo */
34
+ .ai-banner button { flex: none; }
34
35
  .ai-banner .ic { width: 18px; height: 18px; flex: none; }
35
36
  .lib-btn { display: inline-flex; justify-content: center; align-items: center; gap: 8px; min-height: 38px; padding: 0 15px; border-radius: 8px; border: 1px solid transparent; font-size: 12px; font-weight: 600; text-decoration: none; transition: background .15s, transform .15s; }
36
37
  .lib-btn.primary { background: var(--brand-fill); color: var(--on-brand); box-shadow: 0 3px 8px #634ce916; }
@@ -61,7 +61,11 @@
61
61
  // botão IA: provedor, chave e modelos desta máquina (ai-settings.js); no multiusuário quem configura é quem administra
62
62
  // e, enquanto a IA não estiver configurada, a faixa "Configure a IA" (some ao salvar)
63
63
  const aiSaved = () => { $("#ai-banner").hidden = true; };
64
- api("api/ia").then(({ editable, configured }) => { $("#btn-ai").hidden = !editable; $("#ai-banner").hidden = !editable || configured; hydrate($("#ai-banner")); }).catch(() => {});
64
+ api("api/ia").then(({ editable, configured, problem }) => {
65
+ $("#btn-ai").hidden = !editable; $("#ai-banner").hidden = !editable || configured;
66
+ if (problem) $("#ai-banner span").textContent = `A IA não está configurada. ${problem}`; // o arquivo existe mas não serve
67
+ hydrate($("#ai-banner"));
68
+ }).catch(() => {});
65
69
  $("#btn-ai").onclick = () => window.SagaAISettings.open(aiSaved);
66
70
  $("#ai-banner-btn").onclick = () => window.SagaAISettings.open(aiSaved);
67
71
  const openEditor = (id, present) => { location.href = `editor?deck=${encodeURIComponent(id)}${present ? "&present=1" : ""}`; };
@@ -900,7 +900,9 @@ export function createStudioServer(deckPath = null, opts = {}) {
900
900
  const cfg = llmConfig();
901
901
  const available = await llmAvailable({ force: url.searchParams.has("refresh") });
902
902
  res.writeHead(200, { "Content-Type": "application/json" });
903
- res.end(JSON.stringify({ available, configured: llmConfigured(cfg), source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
903
+ // no servidor, só se está ligada: provedor e modelos são de quem administra (src/studio/ia-routes.js)
904
+ if (opts.multiuser) { res.end(JSON.stringify({ available, configured: llmConfigured(cfg), server: true })); return; }
905
+ res.end(JSON.stringify({ available, configured: llmConfigured(cfg), problem: cfg.problem || "", source: cfg.source, url: cfg.url, textModel: cfg.textModel, imageModel: cfg.imageModel }));
904
906
  return;
905
907
  }
906
908
 
@@ -6,7 +6,7 @@ export function generateScaffold({
6
6
  title = "Minha Apresentação",
7
7
  theme = "prata",
8
8
  author = "Seu Nome",
9
- type = "keynote",
9
+ type = "lancamento",
10
10
  } = {}) {
11
11
  const normType = String(type).toLowerCase();
12
12
 
@@ -243,7 +243,7 @@ export function generateScaffold({
243
243
  };
244
244
  }
245
245
 
246
- // Padrão: Keynote Clean / Apple style
246
+ // Padrão: lançamento, limpo e com muito respiro (tema prata)
247
247
  return {
248
248
  title,
249
249
  author,
@@ -252,7 +252,7 @@ export function generateScaffold({
252
252
  slides: [
253
253
  {
254
254
  layout: "cover",
255
- kicker: "Keynote de Lançamento",
255
+ kicker: "Lançamento",
256
256
  title: "Design com ==Propósito==.",
257
257
  subtitle: "A harmonia entre simplicidade, respiro e precisão",
258
258
  author,
@@ -355,14 +355,14 @@ export function generateScaffold({
355
355
  text: "O padrão definitivo para criadores exigentes.",
356
356
  rating: 5,
357
357
  ratingLabel: "5.0 Pro",
358
- tags: ["Keynote", "Prata"],
358
+ tags: ["Lançamento", "Prata"],
359
359
  },
360
360
  ],
361
361
  notes: "> O tom escuro cria um momento teatral no palco.",
362
362
  },
363
363
  {
364
364
  layout: "end",
365
- title: "Pense Diferente.",
365
+ title: "Simples assim.",
366
366
  subtitle: "Obrigado.",
367
367
  contacts: ["apresentacao@empresa.com", "suporte@empresa.com"],
368
368
  notes: "> Conclusão elegante e contato.",
package/src/themes.js CHANGED
@@ -270,7 +270,7 @@ export const THEMES = {
270
270
  },
271
271
 
272
272
  prata: {
273
- label: "Prata — Keynote clean & espacial: prata acetinado, muito respiro, tipografia SF / Inter limpa e elegante",
273
+ label: "Prata — limpo e espacial: prata acetinado, muito respiro, tipografia do sistema / Inter limpa e elegante",
274
274
  colors: {
275
275
  paper: "F5F5F7", ink: "1D1D1F", accent: "0071E3", alert: "FF3B30",
276
276
  muted: "86868B", line: "D2D2D7", surface: "FFFFFF", surfaceDark: "161617",
@@ -293,10 +293,10 @@ export const THEMES = {
293
293
  },
294
294
  radius: 20, deco: "silver",
295
295
  },
296
- // Alumínio: a cara dos lançamentos de iPhone e iPad de 2020-2021 (prata metálico, alumínio escovado, títulos
297
- // cromados, grafite no escuro). O prata segue fiel ao branco da Apple de hoje.
296
+ // Alumínio: a cara de lançamento de aparelho premium (prata metálico, alumínio escovado, títulos
297
+ // cromados, grafite no escuro). O prata segue o branco limpo.
298
298
  aluminio: {
299
- label: "Alumínio — lançamento de iPhone e iPad (2020): prata metálico, títulos cromados, alumínio escovado e grafite",
299
+ label: "Alumínio — lançamento de aparelho premium: prata metálico, títulos cromados, alumínio escovado e grafite",
300
300
  colors: {
301
301
  paper: "E4E5E9", ink: "1C1C1E", accent: "0066CC", alert: "D70015",
302
302
  muted: "5E5E63", line: "C3C5CA", surface: "F4F5F7", surfaceDark: "2C2C2E",
@@ -305,7 +305,7 @@ export const THEMES = {
305
305
  tones: {
306
306
  light: { bg: "paper", fg: "ink", muted: "muted", line: "line", surface: "surface", hi: "D1D3D8", em: "accent", onHi: "ink" },
307
307
  dark: { bg: "1C1C1E", fg: "F5F5F7", muted: "A1A1A6", line: "3A3A3C", surface: "surfaceDark", hi: "48484A", em: "2997FF", onHi: "FFFFFF" },
308
- // azul-pacífico do iPhone 12 Pro, metálico (a pele põe o brilho)
308
+ // azul-pacífico metálico (a pele põe o brilho)
309
309
  accent: { bg: "1D3A50", fg: "FFFFFF", muted: "C5D6E3", line: "3F6580", surface: "2A4C64", hi: "FFFFFF", em: "8FC1E3", onHi: "1D3A50" },
310
310
  alert: { bg: "alert", fg: "FFFFFF", muted: "FFD1D4", line: "F0525F", surface: "B30012", hi: "FFFFFF", em: "FFFFFF", onHi: "alert" },
311
311
  },
@@ -1,10 +1,10 @@
1
1
  title: Inovação & Design
2
- author: Keynote Clean
2
+ author: Estúdio Prata
3
3
  theme: prata
4
4
  duration: 12
5
5
  slides:
6
6
  - layout: cover
7
- kicker: Apresentação Especial · Keynote
7
+ kicker: Apresentação Especial · Lançamento
8
8
  title: Simplicidade com ==Impacto==.
9
9
  subtitle: A estética do respiro, elegância e precisão visual
10
10
  author: Design Team
@@ -79,12 +79,12 @@ slides:
79
79
  title: "Padrão de Mercado"
80
80
  text: "A referência definitiva para profissionais sérios."
81
81
  rating: 5
82
- ratingLabel: "5.0 Keynote"
83
- tags: ["Keynote", "Prata", "Apple Clean"]
82
+ ratingLabel: "5.0 Pro"
83
+ tags: ["Lançamento", "Prata", "Clean"]
84
84
 
85
85
  - layout: end
86
- title: Pense Diferente.
86
+ title: Simples assim.
87
87
  subtitle: Obrigado.
88
88
  contacts:
89
- - apple.com/keynote
89
+ - design.org
90
90
  - contato@design.org