sagadeck 1.6.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.
- package/README.md +14 -8
- package/SKILL.md +7 -6
- package/bin/sagadeck.js +13 -3
- package/docs/ANDAMENTO.md +85 -4
- package/docs/EXPERIENCIAS.md +1 -1
- package/docs/REFERENCIA.md +7 -4
- package/package.json +4 -4
- package/src/ai/ia-config.js +23 -2
- package/src/ai/llm.js +6 -4
- package/src/ai/variety.js +1 -1
- package/src/api-client.js +106 -5
- package/src/experiences.js +1 -1
- package/src/library.js +2 -2
- package/src/mcp/server.js +4 -4
- package/src/runtime/base.css +1 -1
- package/src/runtime/code-lab.js +10 -10
- package/src/runtime/skins/aluminio.css +1 -1
- package/src/runtime/skins/prata.css +1 -1
- package/src/studio/api-routes.js +11 -0
- package/src/studio/banner.js +1 -1
- package/src/studio/ia-routes.js +5 -2
- package/src/studio/open-browser.js +17 -0
- package/src/studio/public/ai-settings.js +22 -2
- package/src/studio/public/api-envs-form.js +148 -0
- package/src/studio/public/app.js +8 -5
- package/src/studio/public/chat.js +1 -1
- package/src/studio/public/index.html +5 -0
- package/src/studio/public/library.css +5 -0
- package/src/studio/public/library.html +1 -0
- package/src/studio/public/library.js +9 -2
- package/src/studio/public/style.css +23 -0
- package/src/studio/server.js +3 -1
- package/src/templates/scaffold.js +5 -5
- package/src/themes.js +5 -5
- package/templates/{exemplo-keynote.yaml → exemplo-prata.yaml} +6 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* Alumínio (lançamento de
|
|
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
|
|
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}
|
package/src/studio/api-routes.js
CHANGED
|
@@ -54,10 +54,21 @@ export async function apiRoutes({ req, res, pathname, url, W, apiEnv, rtSessions
|
|
|
54
54
|
const exists = fs.existsSync(apiEnv.file);
|
|
55
55
|
return reply(200, { file: apiEnv.file, exists, text: exists ? fs.readFileSync(apiEnv.file, "utf8") : AMBIENTES_MODELO, builtin: Object.keys(apiEnv.builtin) });
|
|
56
56
|
}
|
|
57
|
+
// tela Ambientes por campos: o mesmo arquivo como formulário (o valor de segredo nunca vem)
|
|
58
|
+
if (pathname === "/api/http/ambientes/form" && req.method === "GET") {
|
|
59
|
+
try { return reply(200, apiEnv.formState()); } catch (e) { return reply(400, { error: `${apiEnv.file}: ${e.message}` }); }
|
|
60
|
+
}
|
|
57
61
|
if (req.method !== "POST") return reply(405, { error: "use POST" });
|
|
58
62
|
if (!/^application\/json/i.test(req.headers["content-type"] || "")) return reply(415, { error: "envie JSON" });
|
|
59
63
|
let body;
|
|
60
64
|
try { body = await readJSON(req); } catch (e) { return reply(400, { error: e.message }); }
|
|
65
|
+
if (pathname === "/api/http/ambientes/form") {
|
|
66
|
+
if (fs.existsSync(apiEnv.file)) { fs.copyFileSync(apiEnv.file, `${apiEnv.file}.bak`); fs.chmodSync(`${apiEnv.file}.bak`, 0o600); }
|
|
67
|
+
try { return reply(200, apiEnv.saveForm(body.envs, { current: body.current })); } catch (e) { return reply(400, { error: e.message }); }
|
|
68
|
+
}
|
|
69
|
+
if (pathname === "/api/http/ambientes/testar") {
|
|
70
|
+
try { return reply(200, await apiEnv.check(String(body.name || ""))); } catch (e) { return reply(400, { error: e.message }); }
|
|
71
|
+
}
|
|
61
72
|
if (pathname === "/api/http/ambientes" || pathname === "/api/http/ambientes/validar") {
|
|
62
73
|
const text = String(body.text ?? "");
|
|
63
74
|
let data;
|
package/src/studio/banner.js
CHANGED
|
@@ -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)" : ""}`,
|
package/src/studio/ia-routes.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Tela "Configurar IA" do Studio: ler (a chave nunca sai inteira), salvar e testar a configuração da IA desta máquina
|
|
2
2
|
// (src/ai/ia-config.js). No multiusuário quem configura é quem administra o servidor (arquivo ou variáveis), não a tela.
|
|
3
|
-
import { PROVIDERS, ROLES, iaFile, loadIA, saveIA, maskedIA, keyOf } from "../ai/ia-config.js";
|
|
3
|
+
import { PROVIDERS, ROLES, RECOMMENDED, iaFile, loadIA, saveIA, maskedIA, keyOf } from "../ai/ia-config.js";
|
|
4
4
|
import { chat, llmConfig, llmAvailable } from "../ai/llm.js";
|
|
5
5
|
|
|
6
6
|
const json = (res, status, data) => { res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" }); res.end(JSON.stringify(data)); };
|
|
@@ -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
|
-
|
|
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") {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// `sagadeck studio` abre o navegador na página certa: quem acabou de instalar não precisa achar o endereço no meio do
|
|
2
|
+
// que o terminal escreveu. Só quando faz sentido: alguém no terminal (não um serviço, não um teste, não a CI), sem
|
|
3
|
+
// servidor multiusuário e sem --sem-navegador / SAGADECK_NO_BROWSER=1.
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
export function shouldOpenBrowser({ flags = {}, env = process.env, isTTY = process.stdout.isTTY, multiuser = false, platform = process.platform } = {}) {
|
|
7
|
+
if (flags["sem-navegador"] || flags["no-browser"] || env.SAGADECK_NO_BROWSER === "1") return false;
|
|
8
|
+
if (multiuser || env.CI || !isTTY) return false;
|
|
9
|
+
if (platform === "linux" && !env.DISPLAY && !env.WAYLAND_DISPLAY) return false; // servidor sem tela
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function openBrowser(url, { platform = process.platform, spawnFn = spawn } = {}) {
|
|
14
|
+
const [cmd, args] = platform === "win32" ? ["cmd", ["/c", "start", "", url]] : platform === "darwin" ? ["open", [url]] : ["xdg-open", [url]];
|
|
15
|
+
try { const child = spawnFn(cmd, args, { stdio: "ignore", detached: true, windowsHide: true }); child.on?.("error", () => {}); child.unref?.(); return true; }
|
|
16
|
+
catch { return false; }
|
|
17
|
+
}
|
|
@@ -6,6 +6,7 @@ window.SagaAISettings = (() => {
|
|
|
6
6
|
.ai-cfg label{display:block;margin:10px 0 3px;font-weight:600;font-size:12px}.ai-cfg input,.ai-cfg select{width:100%;box-sizing:border-box;padding:7px 9px;border:1px solid var(--border,#c9ced6);border-radius:8px;background:var(--bg,#fff);color:inherit;font:inherit}
|
|
7
7
|
.ai-cfg .grid{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}.ai-cfg small{color:var(--text-3,#777);font-size:12px}.ai-cfg .warn{background:#fef3c7;color:#78350f;border-radius:8px;padding:8px 10px}
|
|
8
8
|
.ai-cfg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:16px;flex-wrap:wrap}.ai-cfg button{padding:7px 14px;border-radius:8px;border:1px solid var(--border,#c9ced6);background:transparent;color:inherit;cursor:pointer;font:inherit}
|
|
9
|
+
.ai-cfg .rec{background:var(--selected,#eef);border-radius:8px;padding:8px 10px;margin:6px 0 4px}.ai-cfg .rec button{margin-top:6px;display:block}
|
|
9
10
|
.ai-cfg button.primary{background:var(--brand,#5b4bdb);border-color:var(--brand,#5b4bdb);color:#fff}.ai-cfg [data-status]{margin-top:10px;min-height:1.4em}.ai-cfg [data-status].ok{color:#15803d}.ai-cfg [data-status].err{color:#b91c1c}`;
|
|
10
11
|
const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web" };
|
|
11
12
|
const HINT = { text: "obrigatório", vision: "opcional; vazio = o de texto", image: "opcional; para gerar ilustrações", search: "opcional; no OpenRouter, um modelo :online" };
|
|
@@ -20,6 +21,15 @@ window.SagaAISettings = (() => {
|
|
|
20
21
|
document.body.append(dialog);
|
|
21
22
|
}
|
|
22
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
|
+
|
|
23
33
|
function form() {
|
|
24
34
|
const c = info.config || {}, providers = info.providers || {};
|
|
25
35
|
const provider = c.provider && (providers[c.provider] || c.provider === "outro") ? c.provider : (c.url ? "outro" : "openrouter");
|
|
@@ -27,13 +37,16 @@ window.SagaAISettings = (() => {
|
|
|
27
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>
|
|
28
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>' : ""}
|
|
29
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>` : ""}
|
|
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>` : ""}
|
|
30
42
|
<label for="ai-cfg-provider">Provedor</label>
|
|
31
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>
|
|
32
44
|
<label for="ai-cfg-url">Endereço da API</label>
|
|
33
45
|
<input id="ai-cfg-url" data-f="url" placeholder="https://…/v1" spellcheck="false">
|
|
34
46
|
<label for="ai-cfg-key">Chave</label>
|
|
35
47
|
<input id="ai-cfg-key" data-f="key" type="password" autocomplete="off" spellcheck="false" placeholder="${c.keySet ? `configurada (${esc(c.keyHint)}${c.keyFrom === "variável" ? `, da variável ${esc(c.keyEnv)}` : ""}); em branco mantém` : "cole a chave do provedor"}">
|
|
36
|
-
<div class="grid">${(info.roles || []).map((r) => `<div><label for="ai-cfg-${r}">${LABEL[r] || r}</label><input id="ai-cfg-${r}" data-model="${r}" value="${esc(c.models?.[r])}" spellcheck="false"><small>${HINT[r] || ""}</small></div>`).join("")}</div>
|
|
48
|
+
<div class="grid">${(info.roles || []).map((r) => `<div><label for="ai-cfg-${r}">${LABEL[r] || r}</label><input id="ai-cfg-${r}" data-model="${r}" value="${esc(c.models?.[r])}" placeholder="${info.recommended?.models?.[r] ? `ex.: ${esc(info.recommended.models[r])}` : ""}" spellcheck="false"><small>${HINT[r] || ""}</small></div>`).join("")}</div>
|
|
49
|
+
<p><small>Texto e visão funcionam em qualquer provedor compatível com a API da OpenAI. Gerar imagem e buscar na web dependem do provedor: funcionam no OpenRouter com modelos de imagem e modelos <code>:online</code>; em outros, a pesquisa usa buscadores diretos e a imagem pode não sair.</small></p>
|
|
37
50
|
<div data-status role="status"></div>
|
|
38
51
|
<div class="row"><button type="button" data-close>Fechar</button><button type="button" data-test>Testar</button><button type="button" class="primary" data-save>Salvar</button></div>`;
|
|
39
52
|
const sel = dialog.querySelector('[data-f="provider"]'), url = dialog.querySelector('[data-f="url"]');
|
|
@@ -44,6 +57,13 @@ window.SagaAISettings = (() => {
|
|
|
44
57
|
sel.onchange = sync;
|
|
45
58
|
if (!info.editable) dialog.querySelectorAll("input,select,[data-save],[data-test]").forEach((el) => (el.disabled = true));
|
|
46
59
|
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
60
|
+
const rec = dialog.querySelector("[data-rec]");
|
|
61
|
+
if (rec) rec.onclick = () => {
|
|
62
|
+
sel.value = info.recommended.provider; sync();
|
|
63
|
+
for (const [r, m] of Object.entries(info.recommended.models)) { const el = dialog.querySelector(`[data-model="${r}"]`); if (el) el.value = m; }
|
|
64
|
+
const st = dialog.querySelector("[data-status]"); st.className = ""; st.textContent = info.config?.keySet ? "Preenchido. Clique em Testar." : "Preenchido. Cole a chave do OpenRouter e clique em Testar.";
|
|
65
|
+
dialog.querySelector('[data-f="key"]').focus();
|
|
66
|
+
};
|
|
47
67
|
dialog.querySelector("[data-test]").onclick = () => submit("api/ia/test");
|
|
48
68
|
dialog.querySelector("[data-save]").onclick = () => submit("api/ia");
|
|
49
69
|
}
|
|
@@ -69,7 +89,7 @@ window.SagaAISettings = (() => {
|
|
|
69
89
|
onSaved = typeof saved === "function" ? saved : () => {};
|
|
70
90
|
if (!dialog) build();
|
|
71
91
|
info = await (await fetch("api/ia")).json();
|
|
72
|
-
form();
|
|
92
|
+
if (info.editable) form(); else serverNotice();
|
|
73
93
|
if (!dialog.open) dialog.showModal();
|
|
74
94
|
},
|
|
75
95
|
};
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Ambientes dos slides de API por campos: quem instala do zero preenche endereço, variáveis, segredos e autenticação
|
|
2
|
+
// sem escrever YAML (o arquivo é o mesmo, ~/.sagadeck/ambientes.yaml; a aba "Como texto" continua para quem prefere).
|
|
3
|
+
// Segredo digitado vai cifrado para o arquivo e nunca volta para a página. No servidor multiusuário (ou Studio aberto
|
|
4
|
+
// para a rede) os slides de API não executam: o botão Ambientes nem aparece.
|
|
5
|
+
window.SagaApiEnvsForm = (() => {
|
|
6
|
+
const $ = (id) => document.getElementById(id);
|
|
7
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
8
|
+
const svg = (n) => `<i class="ic" data-ic="${n}"><svg viewBox="0 0 24 24" aria-hidden="true">${(window.UI_ICONS || {})[n] || ""}</svg></i>`;
|
|
9
|
+
let st = null, tab = "form";
|
|
10
|
+
const status = (text, kind = "") => { const el = $("api-envs-status"); if (el) { el.textContent = text; el.className = `sf-hint api-envs-status ${kind}`; } };
|
|
11
|
+
|
|
12
|
+
// sem execução neste Studio (multiusuário, aberto para a rede): o botão some
|
|
13
|
+
fetch("api/http/state").then((r) => r.json()).then((s) => { if (s && s.live === false) $("btn-api-envs")?.setAttribute("hidden", ""); }).catch(() => {});
|
|
14
|
+
|
|
15
|
+
const row = (kind, item = {}) => kind === "var"
|
|
16
|
+
? `<div class="aef-row" data-row="var"><input data-k="name" placeholder="nome (ex.: modelo)" value="${esc(item.name)}" spellcheck="false" aria-label="Nome da variável"><input data-k="value" placeholder="valor" value="${esc(item.value)}" spellcheck="false" aria-label="Valor da variável"><button type="button" class="icon-btn" data-del title="Tirar">${svg("x")}</button></div>`
|
|
17
|
+
: `<div class="aef-row" data-row="secret"><input data-k="name" placeholder="nome (ex.: senha)" value="${esc(item.name)}" spellcheck="false" aria-label="Nome do segredo"><input data-k="value" type="password" autocomplete="off" placeholder="${item.set ? "guardado; em branco mantém" : "valor (guardado cifrado)"}" aria-label="Valor do segredo"><input data-k="env" placeholder="ou variável de ambiente" value="${esc(item.env)}" spellcheck="false" aria-label="Variável de ambiente do segredo"><button type="button" class="icon-btn" data-del title="Tirar">${svg("x")}</button></div>`;
|
|
18
|
+
|
|
19
|
+
function card(e, current) {
|
|
20
|
+
const a = e.auth || { type: "nenhuma" };
|
|
21
|
+
return `<section class="aef-env" data-env>
|
|
22
|
+
<div class="aef-head"><input data-k="envname" value="${esc(e.name)}" placeholder="nome (dev, hom, prod)" spellcheck="false" aria-label="Nome do ambiente">
|
|
23
|
+
<label class="aef-cur"><input type="radio" name="aef-current" ${e.name === current ? "checked" : ""}> em uso</label>
|
|
24
|
+
<button type="button" class="btn btn-secondary btn-sm" data-test>Testar</button>
|
|
25
|
+
<button type="button" class="icon-btn" data-remove title="Tirar este ambiente">${svg("trash-2")}</button></div>
|
|
26
|
+
<label>Endereço base <small>vira <code>{{base}}</code> nos slides</small></label>
|
|
27
|
+
<input data-k="base" value="${esc(e.base)}" placeholder="https://api.exemplo.com/v1" spellcheck="false">
|
|
28
|
+
<label>Variáveis <small><code>{{nome}}</code> nos slides</small></label>
|
|
29
|
+
<div data-list="var">${(e.vars || []).map((v) => row("var", v)).join("")}</div>
|
|
30
|
+
<button type="button" class="btn-link" data-add="var">+ variável</button>
|
|
31
|
+
<label>Segredos <small><code>{{secret.nome}}</code> nos slides; nunca vão para o deck</small></label>
|
|
32
|
+
<div data-list="secret">${(e.secrets || []).map((v) => row("secret", v)).join("")}</div>
|
|
33
|
+
<button type="button" class="btn-link" data-add="secret">+ segredo</button>
|
|
34
|
+
<label>Autenticação</label>
|
|
35
|
+
<select data-k="auth"><option value="nenhuma">Nenhuma</option><option value="bearer">Token fixo (Authorization: Bearer)</option><option value="token">Token que expira (client credentials)</option></select>
|
|
36
|
+
<div class="aef-auth" data-auth="bearer"><input data-k="bearer" type="password" autocomplete="off" placeholder="${a.type === "bearer" && a.secretSet ? "token guardado; em branco mantém" : "o token"}" aria-label="Token fixo"><input data-k="bearerEnv" placeholder="ou variável de ambiente" value="${esc(a.type === "bearer" ? a.secretEnv : "")}" spellcheck="false" aria-label="Variável do token"></div>
|
|
37
|
+
<div class="aef-auth" data-auth="token"><input data-k="tokenUrl" placeholder="endereço do token (https://…/token)" value="${esc(a.url)}" spellcheck="false" aria-label="Endereço do token"><input data-k="clientId" placeholder="client id" value="${esc(a.client_id)}" spellcheck="false" aria-label="Client id"><input data-k="clientSecret" type="password" autocomplete="off" placeholder="${a.type === "token" && a.secretSet ? "client secret guardado; em branco mantém" : "client secret"}" aria-label="Client secret"><input data-k="clientSecretEnv" placeholder="ou variável de ambiente do secret" value="${esc(a.type === "token" ? a.secretEnv : "")}" spellcheck="false" aria-label="Variável do client secret"><input data-k="field" placeholder="campo do token na resposta (access_token)" value="${esc(a.field)}" spellcheck="false" aria-label="Campo do token"></div>
|
|
38
|
+
${e.advanced ? '<p class="muted aef-note">Este ambiente tem configurações avançadas (certificado, outros cabeçalhos); elas ficam como estão. Veja na aba Como texto.</p>' : ""}
|
|
39
|
+
<div class="aef-result" data-result></div>
|
|
40
|
+
</section>`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function wire(root) {
|
|
44
|
+
root.querySelectorAll("[data-env]").forEach((sec) => {
|
|
45
|
+
const sel = sec.querySelector('[data-k="auth"]');
|
|
46
|
+
const show = () => sec.querySelectorAll("[data-auth]").forEach((d) => { d.hidden = d.dataset.auth !== sel.value; });
|
|
47
|
+
sel.onchange = show;
|
|
48
|
+
sec.querySelector("[data-remove]").onclick = () => { sec.remove(); empty(); };
|
|
49
|
+
sec.querySelector("[data-test]").onclick = () => test(sec);
|
|
50
|
+
sec.querySelectorAll("[data-add]").forEach((b) => (b.onclick = () => { sec.querySelector(`[data-list="${b.dataset.add}"]`).insertAdjacentHTML("beforeend", row(b.dataset.add)); wireRows(sec); }));
|
|
51
|
+
wireRows(sec);
|
|
52
|
+
show();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const wireRows = (sec) => sec.querySelectorAll("[data-del]").forEach((b) => (b.onclick = () => b.closest(".aef-row").remove()));
|
|
56
|
+
function empty() {
|
|
57
|
+
const intro = $("api-envs-form").querySelector("[data-intro]");
|
|
58
|
+
if (intro) intro.hidden = !!$("api-envs-form").querySelector("[data-env]");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function render() {
|
|
62
|
+
const box = $("api-envs-form");
|
|
63
|
+
box.innerHTML = `<div class="aef-intro" data-intro><p><b>Você ainda não tem ambientes seus.</b> O <b>ENSAIO</b> (uma API de mentira que vem com o sagadeck) já funciona nos slides de API, sem configurar nada.</p><p>Para ligar a sua API, adicione um ambiente: o endereço base, as variáveis, os segredos e como ela autentica. Fica gravado nesta máquina, em <code>${esc(st.file)}</code>.</p></div>
|
|
64
|
+
<div data-envs>${st.envs.map((e) => card(e, st.current)).join("")}</div>
|
|
65
|
+
<button type="button" class="btn btn-secondary" data-new>${svg("plus")} Adicionar ambiente</button>`;
|
|
66
|
+
box.querySelectorAll("[data-env]").forEach((sec, i) => { sec.querySelector('[data-k="auth"]').value = st.envs[i].auth?.type || "nenhuma"; });
|
|
67
|
+
box.querySelector("[data-new]").onclick = () => {
|
|
68
|
+
const names = new Set([...box.querySelectorAll('[data-k="envname"]')].map((i) => i.value));
|
|
69
|
+
const name = ["dev", "hom", "prod"].find((n) => !names.has(n)) || `ambiente${names.size + 1}`;
|
|
70
|
+
box.querySelector("[data-envs]").insertAdjacentHTML("beforeend", card({ name, base: "", vars: [], secrets: [], auth: { type: "nenhuma" } }, names.size ? st.current : name));
|
|
71
|
+
wire(box.querySelector("[data-envs]").lastElementChild.parentElement);
|
|
72
|
+
empty();
|
|
73
|
+
box.querySelector("[data-envs]").lastElementChild.querySelector('[data-k="base"]').focus();
|
|
74
|
+
};
|
|
75
|
+
wire(box);
|
|
76
|
+
empty();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function collect() {
|
|
80
|
+
const envs = [], box = $("api-envs-form");
|
|
81
|
+
let current = null;
|
|
82
|
+
for (const sec of box.querySelectorAll("[data-env]")) {
|
|
83
|
+
const v = (k) => sec.querySelector(`[data-k="${k}"]`)?.value.trim() || "";
|
|
84
|
+
const name = v("envname");
|
|
85
|
+
if (sec.querySelector('input[name="aef-current"]').checked) current = name;
|
|
86
|
+
const rows = (kind) => [...sec.querySelectorAll(`[data-row="${kind}"]`)].map((r) => Object.fromEntries([...r.querySelectorAll("[data-k]")].map((i) => [i.dataset.k, i.value.trim()]))).filter((r) => r.name);
|
|
87
|
+
const type = v("auth");
|
|
88
|
+
const auth = type === "bearer" ? { type, value: v("bearer"), secretEnv: v("bearerEnv") }
|
|
89
|
+
: type === "token" ? { type, url: v("tokenUrl"), client_id: v("clientId"), value: v("clientSecret"), secretEnv: v("clientSecretEnv"), field: v("field") }
|
|
90
|
+
: { type: "nenhuma" };
|
|
91
|
+
envs.push({ name, base: v("base"), vars: rows("var"), secrets: rows("secret"), auth });
|
|
92
|
+
}
|
|
93
|
+
return { envs, current };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function save() {
|
|
97
|
+
status("Salvando…");
|
|
98
|
+
const r = await fetch("api/http/ambientes/form", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(collect()) });
|
|
99
|
+
const j = await r.json().catch(() => ({}));
|
|
100
|
+
if (!r.ok) { status(`Não salvou: ${j.error || r.status}`, "err"); return false; }
|
|
101
|
+
st = j;
|
|
102
|
+
render();
|
|
103
|
+
status(`Salvo em ${st.file}. Os slides de API já usam os ambientes novos.`, "ok");
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function test(sec) {
|
|
108
|
+
const out = sec.querySelector("[data-result]");
|
|
109
|
+
out.className = "aef-result"; out.textContent = "Salvando e testando…";
|
|
110
|
+
if (!(await save())) { out.textContent = ""; return; }
|
|
111
|
+
const name = sec.querySelector('[data-k="envname"]')?.value.trim();
|
|
112
|
+
const again = [...$("api-envs-form").querySelectorAll("[data-env]")].find((s) => s.querySelector('[data-k="envname"]').value === name);
|
|
113
|
+
const res = again?.querySelector("[data-result]") || out;
|
|
114
|
+
try {
|
|
115
|
+
const j = await (await fetch("api/http/ambientes/testar", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name }) })).json();
|
|
116
|
+
if (j.error) throw new Error(j.error);
|
|
117
|
+
const parts = [j.base ? (j.base.ok ? `endereço respondeu (HTTP ${j.base.status})` : `endereço não respondeu: ${j.base.error}`) : "sem endereço base",
|
|
118
|
+
...(j.token ? [j.token.ok ? `token obtido (…${j.token.last4})` : `token falhou: ${j.token.error}`] : [])];
|
|
119
|
+
const ok = (!j.base || j.base.ok) && (!j.token || j.token.ok);
|
|
120
|
+
res.className = `aef-result ${ok ? "ok" : "err"}`;
|
|
121
|
+
res.textContent = (ok ? "Funcionou: " : "Problema: ") + parts.join("; ") + ".";
|
|
122
|
+
} catch (e) { res.className = "aef-result err"; res.textContent = `Não testou: ${e.message}`; }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function setTab(t) {
|
|
126
|
+
tab = t;
|
|
127
|
+
document.querySelectorAll("[data-envs-tab]").forEach((b) => b.classList.toggle("active", b.dataset.envsTab === t));
|
|
128
|
+
$("api-envs-form").hidden = t !== "form";
|
|
129
|
+
$("api-envs-textmode").hidden = t !== "text";
|
|
130
|
+
$("btn-api-envs-save").disabled = false;
|
|
131
|
+
status("");
|
|
132
|
+
if (t === "text") $("api-envs-text").dispatchEvent(new Event("input")); // a aba de texto valida antes de liberar o Salvar
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function open() {
|
|
136
|
+
document.querySelectorAll("[data-envs-tab]").forEach((b) => (b.onclick = async () => { if (b.dataset.envsTab === "text") { const j = await (await fetch("api/http/ambientes")).json().catch(() => ({})); if (j.text != null) $("api-envs-text").value = j.text; } else await load(); setTab(b.dataset.envsTab); }));
|
|
137
|
+
await load();
|
|
138
|
+
setTab("form");
|
|
139
|
+
}
|
|
140
|
+
async function load() {
|
|
141
|
+
const r = await fetch("api/http/ambientes/form");
|
|
142
|
+
st = await r.json().catch(() => ({ envs: [] }));
|
|
143
|
+
if (!r.ok) { status(st.error || `HTTP ${r.status}`, "err"); st = { envs: [], file: "" }; }
|
|
144
|
+
render();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return { open, save, active: () => tab === "form" };
|
|
148
|
+
})();
|
package/src/studio/public/app.js
CHANGED
|
@@ -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 =
|
|
3048
|
-
?
|
|
3049
|
-
:
|
|
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 || "
|
|
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
|
};
|
|
@@ -3430,6 +3432,7 @@ ${ta.value}`;
|
|
|
3430
3432
|
renderApiEnvChips(st);
|
|
3431
3433
|
if (!j.exists) apiEnvsStatus("Este arquivo ainda não existe. Validando o modelo antes de habilitar Salvar…");
|
|
3432
3434
|
void validateApiEnvs();
|
|
3435
|
+
await window.SagaApiEnvsForm.open(); // por campos é o padrão (api-envs-form.js); o texto fica na outra aba
|
|
3433
3436
|
}
|
|
3434
3437
|
function closeApiEnvs() { apiEnvsModal().classList.add("hidden"); }
|
|
3435
3438
|
async function saveApiEnvs() {
|
|
@@ -3915,7 +3918,7 @@ ${ta.value}`;
|
|
|
3915
3918
|
document.getElementById("btn-api-envs").addEventListener("click", openApiEnvs);
|
|
3916
3919
|
document.getElementById("btn-close-api-envs").addEventListener("click", closeApiEnvs);
|
|
3917
3920
|
document.getElementById("btn-api-envs-cancel").addEventListener("click", closeApiEnvs);
|
|
3918
|
-
document.getElementById("btn-api-envs-save").addEventListener("click", saveApiEnvs);
|
|
3921
|
+
document.getElementById("btn-api-envs-save").addEventListener("click", async () => { if (!window.SagaApiEnvsForm.active()) return saveApiEnvs(); if (await window.SagaApiEnvsForm.save()) await refreshStudioVars(); });
|
|
3919
3922
|
document.getElementById("btn-api-vars-studio").addEventListener("click", () => openPane("vars"));
|
|
3920
3923
|
document.getElementById("api-envs-text").addEventListener("input", scheduleApiEnvValidation);
|
|
3921
3924
|
// grade de variáveis: grava ao sair da célula (change) ou no Enter; Esc desfaz; a linha em branco do fim cria
|
|
@@ -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');
|
|
@@ -748,11 +748,15 @@
|
|
|
748
748
|
<div class="modal-dialog-body api-envs-body">
|
|
749
749
|
<p class="muted api-envs-intro">Endereços, tokens e segredos ficam na sua máquina, nunca no deck (o deck pode ir para o GitHub).
|
|
750
750
|
Nos slides, <code>{{base}}</code> vira o <code>vars.base</code> do ambiente em uso; segredos entram como <code>{{secret.nome}}</code>.</p>
|
|
751
|
+
<div class="api-envs-tabs" role="tablist"><button type="button" role="tab" data-envs-tab="form" class="active">Por campos</button><button type="button" role="tab" data-envs-tab="text">Como texto (avançado)</button></div>
|
|
752
|
+
<div id="api-envs-form" class="api-envs-form"></div>
|
|
753
|
+
<div id="api-envs-textmode" hidden>
|
|
751
754
|
<p class="muted api-envs-safety">O YAML é validado antes de salvar. Uma cópia da versão anterior fica ao lado do arquivo como <code>.bak</code>.</p>
|
|
752
755
|
<div class="napkin-label">Ambiente em uso <span class="muted">— o selo no slide também troca</span></div>
|
|
753
756
|
<div id="api-envs-list" class="api-envs-list"></div>
|
|
754
757
|
<div class="napkin-label">Arquivo <code id="api-envs-file" class="muted"></code></div>
|
|
755
758
|
<textarea id="api-envs-text" class="form-control mono" rows="20" spellcheck="false" aria-label="Conteúdo do ambientes.yaml"></textarea>
|
|
759
|
+
</div>
|
|
756
760
|
<div id="api-envs-status" class="sf-hint api-envs-status"></div>
|
|
757
761
|
</div>
|
|
758
762
|
<div class="modal-dialog-footer">
|
|
@@ -843,6 +847,7 @@
|
|
|
843
847
|
<script src="ui-icons.js"></script>
|
|
844
848
|
<script src="update-notice.js"></script>
|
|
845
849
|
<script src="ai-settings.js"></script>
|
|
850
|
+
<script src="api-envs-form.js"></script>
|
|
846
851
|
<script src="calc-fields.js"></script>
|
|
847
852
|
<script src="art-preview.js"></script>
|
|
848
853
|
<script src="api-collections.js"></script>
|
|
@@ -28,6 +28,11 @@ body.lib { display: grid; grid-template-columns: 218px minmax(0, 1fr); grid-temp
|
|
|
28
28
|
.lib-search { display: flex; align-items: center; gap: 9px; width: 228px; height: 37px; color: var(--text-3); padding: 0 11px; background: transparent; border: 1px solid var(--ui-line); border-radius: 8px; }
|
|
29
29
|
.lib-search input { background: none; color: var(--text); border: 0; outline: 0; width: 100%; min-width: 0; font-size: 11px; }
|
|
30
30
|
.lib-search kbd { border: 1px solid var(--ui-line); border-radius: 3px; font-size: 10px; padding: 0 4px; }
|
|
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
|
+
.ai-banner[hidden] { display: none; }
|
|
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; }
|
|
35
|
+
.ai-banner .ic { width: 18px; height: 18px; flex: none; }
|
|
31
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; }
|
|
32
37
|
.lib-btn.primary { background: var(--brand-fill); color: var(--on-brand); box-shadow: 0 3px 8px #634ce916; }
|
|
33
38
|
.lib-btn.primary:hover { background: var(--brand-fill-hover); transform: translateY(-1px); }
|
|
@@ -28,6 +28,7 @@
|
|
|
28
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
29
|
<div class="lib-user" id="user" hidden><div class="lib-avatar" id="avatar"></div><span class="uname" id="uname"></span></div>
|
|
30
30
|
</header>
|
|
31
|
+
<div class="ai-banner" id="ai-banner" role="status" hidden><i class="ic" data-ic="sparkles"></i><span>A IA ainda não está configurada. Configure para gerar apresentações e conversar no chat (o resto já funciona).</span><button type="button" class="lib-btn primary" id="ai-banner-btn">Configurar IA</button></div>
|
|
31
32
|
<div class="lib-body">
|
|
32
33
|
<aside class="lib-side">
|
|
33
34
|
<nav id="side" aria-label="Biblioteca"></nav>
|
|
@@ -59,8 +59,15 @@
|
|
|
59
59
|
render();
|
|
60
60
|
}
|
|
61
61
|
// botão IA: provedor, chave e modelos desta máquina (ai-settings.js); no multiusuário quem configura é quem administra
|
|
62
|
-
|
|
63
|
-
$("#
|
|
62
|
+
// e, enquanto a IA não estiver configurada, a faixa "Configure a IA" (some ao salvar)
|
|
63
|
+
const aiSaved = () => { $("#ai-banner").hidden = true; };
|
|
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(() => {});
|
|
69
|
+
$("#btn-ai").onclick = () => window.SagaAISettings.open(aiSaved);
|
|
70
|
+
$("#ai-banner-btn").onclick = () => window.SagaAISettings.open(aiSaved);
|
|
64
71
|
const openEditor = (id, present) => { location.href = `editor?deck=${encodeURIComponent(id)}${present ? "&present=1" : ""}`; };
|
|
65
72
|
|
|
66
73
|
// ---------------------------------------------------------------- barra lateral
|
|
@@ -812,6 +812,29 @@ textarea.form-control.napkin-input { min-height: 220px; resize: vertical; font-f
|
|
|
812
812
|
.bs-option:disabled { opacity: .5; }
|
|
813
813
|
.ai-msg.bs .ai-avatar { background: #8b5cf6; }
|
|
814
814
|
.bs-tag { font-size: 11px; color: var(--text-3); margin-bottom: 4px; }
|
|
815
|
+
/* Ambientes por campos (api-envs-form.js) */
|
|
816
|
+
.api-envs-tabs { display: flex; gap: 6px; margin: 4px 0 10px; }
|
|
817
|
+
.api-envs-tabs button { border: 1px solid var(--border, rgba(127,127,127,.3)); background: transparent; color: inherit; border-radius: 999px; padding: 4px 12px; cursor: pointer; font-size: 12px; }
|
|
818
|
+
.api-envs-tabs button.active { background: var(--selected); border-color: var(--brand); color: var(--brand); }
|
|
819
|
+
.api-envs-form { display: flex; flex-direction: column; gap: 10px; }
|
|
820
|
+
.aef-intro p { margin: 0 0 6px; }
|
|
821
|
+
.aef-env { border: 1px solid var(--border, rgba(127,127,127,.3)); border-radius: 10px; padding: 10px 12px; display: flex; flex-direction: column; gap: 4px; }
|
|
822
|
+
.aef-env label { font-size: 12px; font-weight: 600; margin-top: 6px; }
|
|
823
|
+
.aef-env label small { font-weight: 400; color: var(--text-3); }
|
|
824
|
+
.aef-env input, .aef-env select { width: 100%; box-sizing: border-box; padding: 5px 8px; border: 1px solid var(--border, rgba(127,127,127,.3)); border-radius: 6px; background: var(--bg, transparent); color: inherit; font: inherit; font-size: 13px; }
|
|
825
|
+
.aef-head { display: flex; gap: 8px; align-items: center; }
|
|
826
|
+
.aef-head input[data-k="envname"] { font-weight: 600; max-width: 180px; }
|
|
827
|
+
.aef-head .aef-cur { display: flex; align-items: center; gap: 4px; margin: 0; font-weight: 400; white-space: nowrap; }
|
|
828
|
+
.aef-head .aef-cur input { width: auto; }
|
|
829
|
+
.aef-head [data-test] { margin-left: auto; }
|
|
830
|
+
.aef-row, .aef-auth { display: flex; gap: 6px; align-items: center; }
|
|
831
|
+
.aef-auth { flex-wrap: wrap; margin-top: 4px; }
|
|
832
|
+
.aef-auth input { flex: 1 1 180px; }
|
|
833
|
+
.aef-env .btn-link { align-self: flex-start; background: none; border: 0; color: var(--brand); cursor: pointer; padding: 2px 0; font-size: 12px; }
|
|
834
|
+
.aef-note { font-size: 12px; margin: 6px 0 0; }
|
|
835
|
+
.aef-result { font-size: 12px; min-height: 1em; }
|
|
836
|
+
.aef-result.ok { color: #15803d; } .aef-result.err { color: var(--danger, #b91c1c); }
|
|
837
|
+
|
|
815
838
|
/* leitura crítica: um cartão por ponto, ligado ao slide, com Aplicar e Ignorar */
|
|
816
839
|
.crit-list { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
|
|
817
840
|
.crit-item { border: 1px solid var(--border, rgba(127,127,127,.3)); border-radius: 10px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; }
|
package/src/studio/server.js
CHANGED
|
@@ -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
|
-
|
|
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 = "
|
|
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:
|
|
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: "
|
|
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: ["
|
|
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: "
|
|
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 —
|
|
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
|
|
297
|
-
// cromados, grafite no escuro). O prata segue
|
|
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
|
|
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
|
|
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:
|
|
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 ·
|
|
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
|
|
83
|
-
tags: ["
|
|
82
|
+
ratingLabel: "5.0 Pro"
|
|
83
|
+
tags: ["Lançamento", "Prata", "Clean"]
|
|
84
84
|
|
|
85
85
|
- layout: end
|
|
86
|
-
title:
|
|
86
|
+
title: Simples assim.
|
|
87
87
|
subtitle: Obrigado.
|
|
88
88
|
contacts:
|
|
89
|
-
-
|
|
89
|
+
- design.org
|
|
90
90
|
- contato@design.org
|