sagadeck 1.9.0 → 1.11.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.
- package/README.md +9 -2
- package/SKILL.md +1 -1
- package/bin/sagadeck.js +1 -1
- package/docs/ANDAMENTO.md +302 -0
- package/docs/REFERENCIA.md +72 -4
- package/package.json +4 -4
- package/src/ai/context.js +7 -2
- package/src/ai/critique.js +8 -1
- package/src/ai/deck-ai.js +36 -12
- package/src/ai/document-visuals.js +6 -2
- package/src/ai/ia-check.js +13 -4
- package/src/ai/ia-config.js +4 -2
- package/src/ai/llm.js +12 -7
- package/src/ai/math-check.js +318 -0
- package/src/ai/quality.js +3 -3
- package/src/ai/units.js +85 -0
- package/src/api-client.js +42 -10
- package/src/build.js +1 -0
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/fs-retry.js +2 -2
- package/src/lessons.js +72 -11
- package/src/mcp/server.js +1 -1
- package/src/package.js +4 -0
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/base.css +3 -0
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/runtime/solution.js +131 -0
- package/src/studio/ai-chat-route.js +69 -7
- package/src/studio/conference-routes.js +44 -0
- package/src/studio/conference.js +144 -0
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/generation-report.js +61 -1
- package/src/studio/generation-request.js +80 -0
- package/src/studio/ia-routes.js +1 -1
- package/src/studio/layout-samples.js +1 -1
- package/src/studio/library-routes.js +7 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/ai-settings.js +4 -4
- package/src/studio/public/app.js +14 -2
- package/src/studio/public/chat.js +11 -3
- package/src/studio/public/conference-ui.js +91 -0
- package/src/studio/public/developer-authoring.css +12 -0
- package/src/studio/public/developer-authoring.js +306 -0
- package/src/studio/public/generation-guide.js +42 -0
- package/src/studio/public/index.html +21 -0
- package/src/studio/public/library.css +36 -0
- package/src/studio/public/library.html +4 -1
- package/src/studio/public/library.js +118 -28
- package/src/studio/public/slide-form.js +17 -17
- package/src/studio/public/style.css +25 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +20 -8
- package/src/themes.js +48 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { llmConfig } from '../ai/llm.js';
|
|
5
|
+
import { loadPreferences } from '../preferences.js';
|
|
6
|
+
import { renameRetry } from '../fs-retry.js';
|
|
7
|
+
|
|
8
|
+
// O pedido que originou a apresentação (.sagadeck/geracao.json), para dar para auditar depois: foi um prompt ou um
|
|
9
|
+
// material? o que a pessoa pediu? Grava o briefing exatamente como foi para a IA (e se era o texto padrão da tela),
|
|
10
|
+
// a resposta à pergunta da IA, duração, estilo, os anexos (nome, tipo, tamanho, onde ficaram em contexto/), as
|
|
11
|
+
// Preferências que mudam a geração e os modelos. Nada de chave: do provedor, só o endereço sem usuário e senha.
|
|
12
|
+
// Recriar pelo chat grava por cima; o pedido anterior vai para `anteriores` (os 10 últimos).
|
|
13
|
+
export const REQUEST_FILE = path.join('.sagadeck', 'geracao.json');
|
|
14
|
+
|
|
15
|
+
const text = (v, max = 20000) => (v == null || v === '' ? undefined : String(v).slice(0, max));
|
|
16
|
+
const num = (v) => (Number(v) > 0 ? Number(v) : undefined);
|
|
17
|
+
const clean = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined));
|
|
18
|
+
|
|
19
|
+
// onde o anexo ficou guardado (mesma pasta de prepareDocumentMaterials: contexto/documentos/<sha256 12>/<nome>)
|
|
20
|
+
function storedPath(dir, name, bytes) {
|
|
21
|
+
if (!bytes) return undefined;
|
|
22
|
+
const hash = crypto.createHash('sha256').update(bytes).digest('hex').slice(0, 12);
|
|
23
|
+
const rel = ['contexto', 'documentos', hash, path.basename(name)].join('/');
|
|
24
|
+
return fs.existsSync(path.join(dir, ...rel.split('/'))) ? rel : undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function material(dir, m) {
|
|
28
|
+
const name = String(m.name || '');
|
|
29
|
+
const ext = path.extname(name).slice(1).toLowerCase();
|
|
30
|
+
return clean({
|
|
31
|
+
nome: name,
|
|
32
|
+
tipo: /^https?:/i.test(name) ? 'link' : ext || 'texto',
|
|
33
|
+
detalhe: text(m.detail, 200),
|
|
34
|
+
tamanho: m.bytes ? m.bytes.length : undefined, // bytes do arquivo anexado
|
|
35
|
+
caracteres: String(m.text || '').length || undefined, // texto que a IA leu
|
|
36
|
+
caminho: storedPath(dir, name, m.bytes),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// o endereço do provedor, sem usuário, senha nem parâmetros (onde às vezes vai uma chave)
|
|
41
|
+
function provider(cfg) {
|
|
42
|
+
if (cfg.adaptador) return 'adaptador';
|
|
43
|
+
try { const u = new URL(cfg.url); return `${u.protocol}//${u.host}${u.pathname.replace(/\/+$/, '')}`; } catch { return undefined; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// origem: "biblioteca" | "editor" | "chat"; pedido: o corpo do pedido (briefing, answer, duration, style…);
|
|
47
|
+
// materials: os anexos como vieram (com bytes); spec: a apresentação gerada (tema e título finais)
|
|
48
|
+
export function saveGenerationRequest(dir, { origem, pedido = {}, materials = [], spec = null, research = null, prefs = loadPreferences().ia, cfg = llmConfig() } = {}) {
|
|
49
|
+
if (!dir) return null;
|
|
50
|
+
const guide = pedido.guide && typeof pedido.guide === 'object' ? pedido.guide : {};
|
|
51
|
+
const entry = clean({
|
|
52
|
+
data: new Date().toISOString(),
|
|
53
|
+
origem,
|
|
54
|
+
briefing: String(pedido.briefing || ''),
|
|
55
|
+
briefingPadrao: !!guide.padrao,
|
|
56
|
+
guia: clean({ intencao: text(guide.intent, 40), publico: text(guide.audience, 500), objetivo: text(guide.goal, 500), roteiro: text(guide.route, 40) }),
|
|
57
|
+
resposta: text(pedido.answer, 500),
|
|
58
|
+
duracao: num(pedido.duration),
|
|
59
|
+
slides: num(pedido.slides),
|
|
60
|
+
tema: text(pedido.theme, 80),
|
|
61
|
+
estilo: text(pedido.style, 80),
|
|
62
|
+
direcao: text(pedido.direction && typeof pedido.direction === 'object' ? JSON.stringify(pedido.direction) : pedido.direction, 4000),
|
|
63
|
+
materiais: materials.map((m) => material(dir, m)),
|
|
64
|
+
preferencias: clean({ idioma: prefs?.idioma || 'auto', imagens: prefs?.imagens !== false, pesquisa: prefs?.pesquisa !== false, perguntar: prefs?.perguntar !== false, autor: text(prefs?.autor, 120) }),
|
|
65
|
+
pesquisa: research?.fontes?.length ? research.fontes.map((f) => clean({ site: text(f.site, 200), url: text(f.url, 500) })) : undefined,
|
|
66
|
+
modelos: clean({ texto: cfg.textModel, visao: cfg.visionModel, imagem: cfg.imageModel, busca: cfg.searchModel || undefined }),
|
|
67
|
+
provedor: provider(cfg),
|
|
68
|
+
resultado: spec ? clean({ titulo: text(spec.title, 200), tema: text(spec.theme, 80), slides: Array.isArray(spec.slides) ? spec.slides.length : undefined }) : undefined,
|
|
69
|
+
});
|
|
70
|
+
if (!Object.keys(entry.guia).length) delete entry.guia;
|
|
71
|
+
const file = path.join(dir, REQUEST_FILE);
|
|
72
|
+
let anteriores = [];
|
|
73
|
+
try { const old = JSON.parse(fs.readFileSync(file, 'utf8')); anteriores = [{ ...old, anteriores: undefined }, ...(old.anteriores || [])].slice(0, 10); } catch { /* primeiro pedido */ }
|
|
74
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
75
|
+
const out = { ...entry, ...(anteriores.length ? { anteriores: JSON.parse(JSON.stringify(anteriores)) } : {}) };
|
|
76
|
+
const tmp = `${file}.tmp-${process.pid}`;
|
|
77
|
+
fs.writeFileSync(tmp, JSON.stringify(out, null, 2), 'utf8');
|
|
78
|
+
renameRetry(tmp, file);
|
|
79
|
+
return out;
|
|
80
|
+
}
|
package/src/studio/ia-routes.js
CHANGED
|
@@ -52,7 +52,7 @@ export async function iaRoutes({ req, res, pathname, opts, readJSON }) {
|
|
|
52
52
|
// testar: uma pergunta curta com o que está na tela (antes de salvar)
|
|
53
53
|
const base = llmConfig();
|
|
54
54
|
const cfg = { ...base, url: next.url.replace(/\/+$/, ""), key: keyOf(next), headers: next.headers || {}, adaptador: next.adaptador || "",
|
|
55
|
-
textModel: next.models.text, visionModel: next.models.vision || next.models.text, imageModel: next.models.image || "image", searchModel: next.models.search || "",
|
|
55
|
+
textModel: next.models.text, visionModel: next.models.vision || next.models.text, imageModel: next.models.image || "image", searchModel: next.models.search || "", checkModel: next.models.check || next.models.text,
|
|
56
56
|
timeoutMs: 60_000, firstTimeoutMs: 60_000 };
|
|
57
57
|
const t0 = Date.now();
|
|
58
58
|
try {
|
|
@@ -143,7 +143,7 @@ export const LAYOUT_SAMPLES = {
|
|
|
143
143
|
decisionlab: {layout:"decisionlab", title:"A revisão melhora o resultado?", lab:{volume:10000,errorRate:2,reviewRate:100,catchRate:60,introducedRate:0.2,seconds:30}},
|
|
144
144
|
infographic: { layout: "infographic", shape: "arco", title: "Os ==desafios== da viagem", center: { title: "Desafios" }, items: [{ title: "Roteiro", text: "Três cidades em dez dias" }, { title: "Orçamento", text: "Hospedagem pesa mais" }, { title: "Documentos", text: "Passaporte e visto" }, { title: "Clima", text: "Chuva na segunda semana" }] },
|
|
145
145
|
diagram: { layout: "diagram", kicker: "Processo", title: "Da ideia à ==produção==", mermaid: "flowchart LR\n A([:lightbulb: Ideia]):::hi --> B{Vale testar?}\n B -->|sim| C[:flask-conical: Protótipo]\n B -.->|não| X[Arquivar]:::vazado\n C ==> D[:rocket: Produção]:::em" },
|
|
146
|
-
solution: {layout:"solution", title:"Exercício: quanto rende em 3 anos?", problem:"R$ 1.000 aplicados a 1% ao mês, com juros compostos. Quanto há depois de 36 meses?", givens:[{symbol:"C", value:"1000", unit:"R\\$", label:"capital"}, {symbol:"i", value:"0,01", label:"taxa ao mês"}, {symbol:"n", value:"36", label:"meses"}], find:"M", steps:[{text:"Juros compostos", latex:"M = C\\,(1 + i)^n"}, {text:"
|
|
146
|
+
solution: {layout:"solution", title:"Exercício: quanto rende em 3 anos?", problem:"R$ 1.000 aplicados a 1% ao mês, com juros compostos. Quanto há depois de 36 meses?", givens:[{symbol:"C", value:"1000", unit:"R\\$", label:"capital"}, {symbol:"i", value:"0,01", label:"taxa ao mês"}, {symbol:"n", value:"36", label:"meses"}], find:"M", steps:[{text:"Juros compostos", latex:"M = C\\,(1 + i)^n"}, {text:"Calcula o fator", latex:"(1 + i)^n = (1 + {{i}})^{ {{n}} }", fn:"(1+i)^n", name:"fator", decimals:4}, {text:"Multiplica pelo capital", latex:"M = {{C}} \\cdot {{fator}}", fn:"C*fator", name:"M", decimals:2}], answer:{latex:"M \\approx R\\$\\,{{M}}", text:"Rende cerca de {{(M/C - 1)*100:0}}% em {{n}} meses."}, interactive:true},
|
|
147
147
|
calc: {layout:"calc", title:"Qual é o regime do escoamento?", inputs:{V:{label:"Velocidade", latex:"V", value:1.5, min:0.01, max:4, step:0.01, unit:"m/s"}, D:{label:"Diâmetro", latex:"D", value:0.05, min:0.005, max:0.3, step:0.005, unit:"m"}, nu:{label:"Viscosidade (água, 20 °C)", latex:"\\nu = 1{,}0\\times10^{-6}\\ \\mathrm{m^2/s}", value:0.000001, fixed:true}}, outputs:[{name:"Re", label:"Número de Reynolds", latex:"Re = \\frac{VD}{\\nu}", fn:"V*D/nu", decimals:0}, {name:"regime", label:"Regime", of:"Re", cases:[{below:2300, text:"Laminar", color:"s3"}, {below:4000, text:"Transição", color:"em"}, {text:"Turbulento", color:"hi"}], scale:{min:100, max:1000000, log:true}}]},
|
|
148
148
|
algo: {layout:"algo", title:"Bubble sort passo a passo", algorithm:"bubble", array:[5, 1, 4, 2, 8, 3]},
|
|
149
149
|
carousel: {layout:"carousel", title:"Cinco lugares, cinco lições", style:"arc", items:[{title:"Montanha", text:"Subir devagar é o jeito mais rápido de chegar.", label:"Capítulo 1"}, {title:"Mar", text:"Tudo o que é fundo começa raso."}, {title:"Cidade", text:"Muita gente, muita luz, pouco silêncio."}, {title:"Deserto", text:"Onde o pouco vira muito."}, {title:"Floresta", text:"Cada árvore conta uma história."}]},
|
|
@@ -9,6 +9,7 @@ import { writeDeckFile } from "../deck-file.js";
|
|
|
9
9
|
import { createExperienceDeck } from "../experiences.js";
|
|
10
10
|
import { demoDeck } from "./demo-decks.js";
|
|
11
11
|
import { DEMO_FILES } from "../api-demo.js";
|
|
12
|
+
import { developerStarter } from "./developer-starters.js";
|
|
12
13
|
|
|
13
14
|
export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON, readBody, slideSnapshots, sendExport, galleryKey, galleryEntry, materializePreview, copyModelAssets, generateIntoLibrary, llmAvailable, llmConfig, respond, templateFile, TEMPLATE_DIRS }) {
|
|
14
15
|
const L = W.library;
|
|
@@ -74,6 +75,12 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
74
75
|
case "/api/library/topics/update": return ok({ id: L.updateTopic(b.id, b) });
|
|
75
76
|
case "/api/library/topics/delete": L.deleteTopic(b.id); return ok();
|
|
76
77
|
case "/api/library/decks": {
|
|
78
|
+
if (b.starter) {
|
|
79
|
+
const spec = developerStarter(b.starter, { title: b.title });
|
|
80
|
+
if (b.theme) spec.theme = b.theme;
|
|
81
|
+
buildHTML(spec);
|
|
82
|
+
return ok({ id: L.createDeck(b.topic || "", spec) });
|
|
83
|
+
}
|
|
77
84
|
// nova: em branco (uma capa) — "com IA" usa /api/library/decks/ai
|
|
78
85
|
const title = String(b.title || "Nova apresentação").trim();
|
|
79
86
|
const id = L.createDeck(b.topic || "", { title, theme: b.theme || "bauhaus", duration: 10,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// "Sobre o SagaDeck" (Ajuda › Sobre, no editor; botão Sobre, na biblioteca): a versão que está rodando e o essencial
|
|
2
|
+
// para dar suporte (commit, Node, desde quando, a biblioteca, se a IA está configurada), com Copiar informações.
|
|
3
|
+
window.SagaAbout = (() => {
|
|
4
|
+
// as cores vêm das variáveis que o editor e a biblioteca definem nos dois temas (claro e escuro); fundo e texto
|
|
5
|
+
// sempre do mesmo par, senão o tema escuro dá texto claro sobre fundo branco
|
|
6
|
+
const css = `.about-dlg{margin:auto;max-width:min(520px,calc(100vw - 32px));width:100%;border:1px solid var(--ui-line,#c9ced6);border-radius:12px;padding:20px 22px;background:var(--chrome,#fff);color:var(--text,#1d2433);font:14px/1.45 system-ui,sans-serif;box-shadow:0 20px 60px #0004}
|
|
7
|
+
.about-dlg::backdrop{background:rgba(0,0,0,.45)}.about-dlg h2{margin:0;font-size:20px;color:var(--text,#1d2433)}.about-dlg .ver{margin:2px 0 14px;font-size:15px;color:var(--text-2,#555)}
|
|
8
|
+
.about-dlg .ver b{color:var(--text,#1d2433);font-size:22px}.about-dlg dl{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0 0 14px}
|
|
9
|
+
.about-dlg dt{color:var(--text-2,#555)}.about-dlg dd{margin:0;overflow-wrap:anywhere;color:var(--text,#1d2433)}.about-dlg code{font:12px ui-monospace,Consolas,monospace;color:inherit}
|
|
10
|
+
.about-dlg .links{display:flex;gap:14px;flex-wrap:wrap;margin:0 0 6px}.about-dlg .links a{color:var(--brand,#5b4bdb)}
|
|
11
|
+
.about-dlg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}.about-dlg button{padding:7px 14px;border-radius:8px;border:1px solid var(--ui-line,#c9ced6);background:transparent;color:var(--text,#1d2433);cursor:pointer;font:inherit}
|
|
12
|
+
.about-dlg button.primary{background:var(--brand-fill,#5b4bdb);border-color:var(--brand-fill,#5b4bdb);color:#fff}.about-dlg [data-copied]{color:var(--text-2,#555);align-self:center;margin-right:auto}`;
|
|
13
|
+
const REPO = "https://github.com/naruminho/sagadeck";
|
|
14
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
15
|
+
let dialog;
|
|
16
|
+
|
|
17
|
+
function build() {
|
|
18
|
+
if (!document.getElementById("about-style")) { const st = document.createElement("style"); st.id = "about-style"; st.textContent = css; document.head.append(st); }
|
|
19
|
+
dialog = document.createElement("dialog");
|
|
20
|
+
dialog.className = "about-dlg";
|
|
21
|
+
dialog.id = "about-dialog";
|
|
22
|
+
dialog.setAttribute("aria-labelledby", "about-title");
|
|
23
|
+
document.body.append(dialog);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// o que o servidor diz de si (versão, commit, Node, desde quando) e se a IA está configurada
|
|
27
|
+
async function facts() {
|
|
28
|
+
const get = async (u) => { try { const r = await fetch(u, { cache: "no-store" }); return r.ok ? r.json() : {}; } catch { return {}; } };
|
|
29
|
+
const [inst, ia] = await Promise.all([get("api/instance"), get("api/ia")]);
|
|
30
|
+
return { ...inst, ia: ia.configured == null ? null : !!ia.configured };
|
|
31
|
+
}
|
|
32
|
+
const text = (f) => [`SagaDeck ${f.version || "?"} (commit ${f.commit || "?"})`, `Node ${f.node || "?"} · ${f.platform || "?"}`,
|
|
33
|
+
f.started ? `No ar desde ${new Date(f.started).toLocaleString("pt-BR")} (processo ${f.pid || "?"})` : "",
|
|
34
|
+
f.multiuser ? "Servidor multiusuário" : f.library ? `Biblioteca: ${f.library}` : "",
|
|
35
|
+
f.ia == null ? "" : `IA: ${f.ia ? "configurada" : "não configurada"}`].filter(Boolean).join("\n");
|
|
36
|
+
|
|
37
|
+
async function open() {
|
|
38
|
+
if (!dialog) build();
|
|
39
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2><p class="ver">Carregando…</p><div class="row"><button type="button" data-close>Fechar</button></div>`;
|
|
40
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
41
|
+
if (!dialog.open) dialog.showModal();
|
|
42
|
+
const f = await facts();
|
|
43
|
+
const row = (k, v) => (v ? `<dt>${k}</dt><dd>${v}</dd>` : "");
|
|
44
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2>
|
|
45
|
+
<p class="ver">Versão <b data-version>${esc(f.version || "?")}</b></p>
|
|
46
|
+
<dl>
|
|
47
|
+
${row("Commit", f.commit ? `<code>${esc(f.commit)}</code>` : "")}
|
|
48
|
+
${row("Node", f.node ? `${esc(f.node)} · ${esc(f.platform || "")}` : "")}
|
|
49
|
+
${row("No ar desde", f.started ? esc(new Date(f.started).toLocaleString("pt-BR")) : "")}
|
|
50
|
+
${row("Biblioteca", !f.multiuser && f.library ? `<code>${esc(f.library)}</code>` : f.multiuser ? "neste servidor (multiusuário)" : "")}
|
|
51
|
+
${row("IA", f.ia == null ? "" : f.ia ? "configurada (para testar cada recurso: <code>sagadeck ia-check</code>)" : "não configurada")}
|
|
52
|
+
</dl>
|
|
53
|
+
<div class="links">
|
|
54
|
+
<a href="${REPO}/blob/main/CHANGELOG.md" target="_blank" rel="noopener">O que mudou em cada versão</a>
|
|
55
|
+
<a href="${REPO}" target="_blank" rel="noopener">Código e problemas</a>
|
|
56
|
+
<a href="https://www.npmjs.com/package/sagadeck" target="_blank" rel="noopener">Pacote no npm</a>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="row"><span data-copied hidden>Copiado.</span><button type="button" data-copy>Copiar informações</button><button type="button" class="primary" data-close>Fechar</button></div>`;
|
|
59
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
60
|
+
dialog.querySelector("[data-copy]").onclick = async () => {
|
|
61
|
+
try { await navigator.clipboard.writeText(text(f)); dialog.querySelector("[data-copied]").hidden = false; }
|
|
62
|
+
catch { dialog.querySelector("[data-copied]").textContent = "Não deu para copiar."; dialog.querySelector("[data-copied]").hidden = false; }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// qualquer botão ou link com data-about abre (Ajuda › Sobre no editor, o botão da biblioteca)
|
|
66
|
+
document.addEventListener("click", (e) => { const b = e.target.closest?.("[data-about]"); if (b) { e.preventDefault(); open(); } });
|
|
67
|
+
return { open };
|
|
68
|
+
})();
|
|
@@ -4,12 +4,12 @@ window.SagaAISettings = (() => {
|
|
|
4
4
|
const css = `.ai-cfg{max-width:min(560px,calc(100vw - 32px));width:100%;border:1px solid var(--border,#c9ced6);border-radius:12px;padding:18px 20px;background:var(--panel,var(--bg,#fff));color:var(--text,#1d2433);font:14px/1.45 system-ui,sans-serif}
|
|
5
5
|
.ai-cfg::backdrop{background:rgba(0,0,0,.35)}.ai-cfg h2{margin:0 0 6px;font-size:18px}.ai-cfg p{margin:4px 0 10px;color:var(--text-2,#555)}
|
|
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
|
-
.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}
|
|
7
|
+
.ai-cfg .grid{display:grid;grid-template-columns:1fr 1fr;gap:0 12px}.ai-cfg .grid .wide{grid-column:1/-1}.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
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}
|
|
10
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}`;
|
|
11
|
-
const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web", video: "Modelo de vídeo" };
|
|
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", video: "opcional; para gerar clipes (cobrado por segundo)" };
|
|
11
|
+
const LABEL = { text: "Modelo de texto", vision: "Modelo que vê imagens", image: "Modelo de imagem", search: "Modelo de busca na web", video: "Modelo de vídeo", check: "Modelo para conferir" };
|
|
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", video: "opcional; para gerar clipes (cobrado por segundo)", check: "opcional; vazio = o de texto. Um modelo de raciocínio mais forte só onde a conta precisa estar certa (contas, leitura crítica, revisão)" };
|
|
13
13
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
14
14
|
let dialog, info, onSaved = () => {};
|
|
15
15
|
|
|
@@ -47,7 +47,7 @@ window.SagaAISettings = (() => {
|
|
|
47
47
|
<input id="ai-cfg-url" data-f="url" placeholder="https://…/v1" spellcheck="false">
|
|
48
48
|
<label for="ai-cfg-key">Chave</label>
|
|
49
49
|
<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"}">
|
|
50
|
-
<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>
|
|
50
|
+
<div class="grid">${(info.roles || []).map((r) => `<div${r === "check" ? ' class="wide"' : ""}><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>
|
|
51
51
|
<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>
|
|
52
52
|
<div data-status role="status"></div>
|
|
53
53
|
<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>`;
|
package/src/studio/public/app.js
CHANGED
|
@@ -378,6 +378,7 @@
|
|
|
378
378
|
renderThumbnails();
|
|
379
379
|
selectSlide(0);
|
|
380
380
|
loadChatHistory();
|
|
381
|
+
conference.refresh(); // o selo da Conferência: uma consulta ao abrir; depois, a cada salvamento
|
|
381
382
|
} catch (err) {
|
|
382
383
|
showToast("Erro ao carregar apresentação: " + err.message);
|
|
383
384
|
}
|
|
@@ -431,6 +432,7 @@
|
|
|
431
432
|
window.SagaMap?.mount(dom.renderedSlideContainer); // só aqui (prévia grande); miniaturas e galeria ficam na prévia em SVG
|
|
432
433
|
window.SagaMapData?.attach(dom.renderedSlideContainer, slide, () => { syncDeckToServer(); renderCurrentSlide(); }, { toast: showToast, hydrate: hydrateIcons, materialized: previewMaterialized });
|
|
433
434
|
window.SagaCalc?.mount(dom.renderedSlideContainer); // calculadora ao vivo: os controles funcionam no editor também
|
|
435
|
+
window.SagaSolution?.mount(dom.renderedSlideContainer); // exercício interativo: idem
|
|
434
436
|
window.SagaDiagrams?.mount(dom.renderedSlideContainer).then(() => reportDiagram(dom.renderedSlideContainer));
|
|
435
437
|
window.SagaDecisionLab?.mount(dom.renderedSlideContainer);
|
|
436
438
|
applyEditorStep(idx);
|
|
@@ -2156,6 +2158,7 @@
|
|
|
2156
2158
|
showToast("Screenshot pronto. Use Apresentar para percorrer os destaques.");
|
|
2157
2159
|
}
|
|
2158
2160
|
function setupCreativeTools() {
|
|
2161
|
+
window.SagaDeveloperAuthoring.bind({ state, syncDeckToServer, renderThumbnails, renderCurrentSlide, openPane, showToast });
|
|
2159
2162
|
const shotButton = document.createElement("button");
|
|
2160
2163
|
shotButton.id = "btn-screenshot";
|
|
2161
2164
|
shotButton.className = "rbtn rbtn-lg";
|
|
@@ -2806,6 +2809,13 @@
|
|
|
2806
2809
|
state, showToast: (...a) => showToast(...a), renderThumbnails: () => renderThumbnails(), renderCurrentSlide: () => renderCurrentSlide(),
|
|
2807
2810
|
trackDeck: (l) => trackDeck(l), hydrateIcons: (el) => hydrateIcons(el), plainTitle: (sl, i) => plainTitle(sl, i),
|
|
2808
2811
|
});
|
|
2812
|
+
// Revisar › Conferência (conference-ui.js): o que as checagens acharam, com Corrigir com IA (pedido pontual ao chat)
|
|
2813
|
+
const conference = window.SagaConference.create({
|
|
2814
|
+
state, showToast: (...a) => showToast(...a), renderThumbnails: () => renderThumbnails(), renderCurrentSlide: () => renderCurrentSlide(),
|
|
2815
|
+
hydrateIcons: (el) => hydrateIcons(el), plainTitle: (sl, i) => plainTitle(sl, i),
|
|
2816
|
+
chatBusy: () => !!chatApi.getChatJob(),
|
|
2817
|
+
askChat: (text, extra) => { openPane("chat"); dom.chatInput.value = text; chatApi.handleChatSubmit(null, extra); },
|
|
2818
|
+
});
|
|
2809
2819
|
// Desfazer / refazer único (history.js): todo caminho que muda o deck passa por trackDeck, com o rótulo da origem
|
|
2810
2820
|
const deckHistory = window.SagaHistory.createHistory();
|
|
2811
2821
|
function trackDeck(label = "Edição") {
|
|
@@ -2865,7 +2875,8 @@
|
|
|
2865
2875
|
});
|
|
2866
2876
|
if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
|
|
2867
2877
|
updateSaveStatus(res.ok ? "saved" : "error");
|
|
2868
|
-
|
|
2878
|
+
// o selo da Conferência acompanha o que mudou no deck (navegar entre slides não muda nada: não consulta)
|
|
2879
|
+
if (res.ok) { window.SagaProject?.deckChanged(); conference.refreshSoon(); }
|
|
2869
2880
|
else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
|
|
2870
2881
|
} catch (err) {
|
|
2871
2882
|
console.warn("Erro ao sincronizar com servidor:", err);
|
|
@@ -3706,7 +3717,7 @@ ${ta.value}`;
|
|
|
3706
3717
|
|
|
3707
3718
|
// Chat lateral com IA (chat.js): mesma API, agora em módulo próprio.
|
|
3708
3719
|
const chatApi = window.SagaChat({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
3709
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus });
|
|
3720
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized });
|
|
3710
3721
|
const { handleChatSubmit, loadChatHistory, appendChatMessage, createProgressBubble, autoGrowChat, applyBrainstorm, addChatFile } = chatApi;
|
|
3711
3722
|
|
|
3712
3723
|
function setNotesVisible(on) {
|
|
@@ -3909,6 +3920,7 @@ ${ta.value}`;
|
|
|
3909
3920
|
document.getElementById("btn-header-footer").addEventListener("click", openHeaderFooter);
|
|
3910
3921
|
document.getElementById("btn-styles")?.addEventListener("click", openStyles);
|
|
3911
3922
|
document.getElementById("btn-review-changes")?.addEventListener("click", openReviewList);
|
|
3923
|
+
document.getElementById("btn-conference")?.addEventListener("click", () => conference.open());
|
|
3912
3924
|
document.getElementById("btn-close-hf").addEventListener("click", closeHeaderFooter);
|
|
3913
3925
|
document.getElementById("btn-hf-cancel").addEventListener("click", closeHeaderFooter);
|
|
3914
3926
|
document.getElementById("btn-hf-apply").addEventListener("click", applyHeaderFooter);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Saiu do app.js pela trava de tamanho dos monolitos; o comportamento é o mesmo.
|
|
3
3
|
// Fábrica no molde de slide-select.js: recebe o estado e os ajudantes, devolve a API.
|
|
4
4
|
window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
5
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus }) {
|
|
5
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized }) {
|
|
6
6
|
let chatJob = null;
|
|
7
7
|
const getChatJob = () => chatJob;
|
|
8
8
|
// ==========================================================================
|
|
@@ -96,7 +96,8 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
96
96
|
box.scrollIntoView({ block: "nearest" });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
// extra: o que o Studio manda junto do pedido (contas: [n] do painel Conferência, conferidas depois da resposta)
|
|
100
|
+
async function handleChatSubmit(e, extra = {}) {
|
|
100
101
|
if (e) e.preventDefault();
|
|
101
102
|
if (chatJob) { if(e?.type === "submit") chatJob.stop(); return; }
|
|
102
103
|
const message = dom.chatInput.value.trim();
|
|
@@ -156,9 +157,14 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
156
157
|
renderNotes: state.renderNotes || [],
|
|
157
158
|
autoRun: !!state.autoRunCommands, // a pessoa liberou os comandos desta conversa (só enquanto a página está aberta)
|
|
158
159
|
expectFile: state.file,
|
|
160
|
+
...(Array.isArray(extra?.contas) ? { contas: extra.contas } : {}),
|
|
159
161
|
}, (ev) => { if (!commandEvent(ev)) work.update(ev); }, job.controller.signal);
|
|
160
162
|
if (state.file !== job.file) { work.done(); showToast("O pedido terminou na apresentação em que começou; a apresentação atual foi preservada.", 6000); return; }
|
|
161
163
|
if (!data.spec) throw new Error(data.error || "resposta sem deck");
|
|
164
|
+
if (data.materialized) {
|
|
165
|
+
state.file = data.materialized.file;
|
|
166
|
+
previewMaterialized(data.materialized);
|
|
167
|
+
}
|
|
162
168
|
// conversa salva antes de dispensar o indicador: quem espera o fim da resposta já encontra o arquivo gravado
|
|
163
169
|
const finishWork = async () => { await saveChatHistory(); work.done(); };
|
|
164
170
|
if (data.variants) {
|
|
@@ -241,6 +247,7 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
241
247
|
dom.chatInput.disabled = false;
|
|
242
248
|
dom.chatInput.focus();
|
|
243
249
|
await saveChatHistory();
|
|
250
|
+
window.dispatchEvent(new Event("sagadeck:chat-done")); // o painel Conferência confere o selo de novo
|
|
244
251
|
}
|
|
245
252
|
}
|
|
246
253
|
|
|
@@ -317,7 +324,8 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
317
324
|
async function loadChatHistory() {
|
|
318
325
|
state.autoRunCommands = false; // outra apresentação: nada fica liberado
|
|
319
326
|
try {
|
|
320
|
-
const { history } = await (await fetch("api/chat/history")).json();
|
|
327
|
+
const { history, notice } = await (await fetch("api/chat/history")).json();
|
|
328
|
+
if (notice) showToast("A criação com IA deixou avisos sem resolver: veja no chat da IA.", 8000);
|
|
321
329
|
state.chatHistory = Array.isArray(history) ? history : [];
|
|
322
330
|
state.loadingHistory = true; // conversa guardada não é resposta nova (o atalho do chat não acende)
|
|
323
331
|
setTimeout(() => { state.loadingHistory = false; });
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Painel Revisar › Conferência: tudo o que as checagens automáticas acharam no deck aberto (contas, números sem base
|
|
2
|
+
// no material, avisos da revisão visual, figuras do material sem slide, slides que a IA calculou ou incluiu), por
|
|
3
|
+
// slide, com Ir ao slide, Corrigir com IA (um pedido pontual ao chat) e Ignorar (gravado no relatório). A lista vem
|
|
4
|
+
// do servidor (src/studio/conference.js); o app passa o que o painel usa (create(ctx)).
|
|
5
|
+
(function () {
|
|
6
|
+
const KIND_ICON = { conta: "calculator", material: "file-text", numero: "hash", visual: "scan-eye", figura: "image-off", derivado: "calculator", proprio: "sparkles" };
|
|
7
|
+
function create(ctx) {
|
|
8
|
+
const { state, showToast, renderThumbnails, renderCurrentSlide, hydrateIcons, plainTitle, askChat, chatBusy } = ctx;
|
|
9
|
+
let items = [], salvo = false, conferidoEm = null, timer = null, checking = false;
|
|
10
|
+
const json = async (r) => { const j = await r.json().catch(() => ({})); if (!r.ok || j.error) throw new Error(j.error || `HTTP ${r.status}`); return j; };
|
|
11
|
+
function take(j) {
|
|
12
|
+
items = j.items || []; salvo = !!j.salvo; conferidoEm = j.conferidoEm || null;
|
|
13
|
+
const b = document.getElementById("conference-badge");
|
|
14
|
+
if (b) { b.textContent = items.length; b.classList.toggle("clean", !items.length); }
|
|
15
|
+
document.getElementById("conference-dialog")?.dispatchEvent(new Event("refresh"));
|
|
16
|
+
}
|
|
17
|
+
async function refresh() {
|
|
18
|
+
try { take(await json(await fetch("api/conferencia"))); } catch { /* sem servidor: o selo fica como estava */ }
|
|
19
|
+
}
|
|
20
|
+
// o deck muda o tempo todo (formulário, IA): o selo acompanha sem uma consulta por tecla
|
|
21
|
+
const refreshSoon = () => { clearTimeout(timer); timer = setTimeout(refresh, 700); };
|
|
22
|
+
function goTo(n) {
|
|
23
|
+
if (!(n >= 1 && n <= (state.deck?.slides?.length || 0))) return;
|
|
24
|
+
state.currentSlideIndex = n - 1;
|
|
25
|
+
renderThumbnails(); renderCurrentSlide();
|
|
26
|
+
}
|
|
27
|
+
async function decide(id, status) {
|
|
28
|
+
try { take(await json(await fetch("api/conferencia/item", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, status }) }))); }
|
|
29
|
+
catch (e) { showToast(e.message); }
|
|
30
|
+
}
|
|
31
|
+
async function recheck(btn) {
|
|
32
|
+
if (checking) return;
|
|
33
|
+
checking = true; btn.disabled = true;
|
|
34
|
+
btn.innerHTML = '<i class="ic" data-ic="loader-circle"></i> Conferindo…'; hydrateIcons(btn);
|
|
35
|
+
try {
|
|
36
|
+
const j = await json(await fetch("api/conferencia/conferir", { method: "POST", headers: { "Content-Type": "application/json" }, body: "{}" }));
|
|
37
|
+
take(j);
|
|
38
|
+
showToast(`Conferido: ${items.length ? `${items.length} pendência(s)` : "nada pendente"}.${j.notes?.length ? ` ${j.notes.join(" ")}` : ""}`, 6000);
|
|
39
|
+
} catch (e) { showToast(`A conferência não saiu: ${e.message}`); }
|
|
40
|
+
finally { checking = false; document.getElementById("conference-dialog")?.dispatchEvent(new Event("refresh")); }
|
|
41
|
+
}
|
|
42
|
+
function fix(it, dlg) {
|
|
43
|
+
if (chatBusy()) { showToast("Espere a IA terminar o pedido atual.", 3000); return; }
|
|
44
|
+
if (it.slide) goTo(it.slide);
|
|
45
|
+
dlg.close();
|
|
46
|
+
// conta que não bate: o Studio confere de novo depois da resposta (contas: [n]) e o resultado decide se ela
|
|
47
|
+
// sai do painel; o resto (sem como conferir sozinho) sai ao ir para o chat
|
|
48
|
+
if (it.kind !== "conta" && it.kind !== "material") decide(it.id, "enviado");
|
|
49
|
+
askChat(it.fix, it.contas ? { contas: it.contas } : {});
|
|
50
|
+
}
|
|
51
|
+
function open() {
|
|
52
|
+
let dlg = document.getElementById("conference-dialog");
|
|
53
|
+
const fill = () => {
|
|
54
|
+
const groups = new Map();
|
|
55
|
+
for (const it of items) { const k = it.slide || 0; if (!groups.has(k)) groups.set(k, []); groups.get(k).push(it); }
|
|
56
|
+
const order = [...groups.keys()].sort((a, b) => (a || 1e9) - (b || 1e9));
|
|
57
|
+
dlg.innerHTML = `<header><i class="ic" data-ic="list-checks"></i><b>Conferência</b><span class="rv-count">${items.length}</span><span class="grow"></span>${salvo ? `<button type="button" class="btn btn-sm" data-recheck title="Refaz no deck atual a conferência das contas, a checagem dos números e das figuras do material"><i class="ic" data-ic="${checking ? "loader-circle" : "refresh-cw"}"></i> ${checking ? "Conferindo…" : "Conferir de novo"}</button>` : ""}<button type="button" class="btn btn-sm" data-close title="Fechar"><i class="ic" data-ic="x"></i></button></header>
|
|
58
|
+
<div class="rv-list conf-list">${items.length ? order.map((k) => `<section class="conf-group" data-slide="${k}"><div class="conf-group-head">${k ? `<b class="rv-num">${k}</b><span class="conf-slide-title"></span><button type="button" class="btn btn-sm" data-go="${k}">Ir ao slide</button>` : "<b>Deck todo</b>"}</div>${groups.get(k).map((it) => `<div class="conf-item conf-${it.kind}" data-id="${it.id}"><span class="conf-kind"><i class="ic" data-ic="${KIND_ICON[it.kind] || "circle-alert"}"></i><span></span></span><span class="conf-text"></span><span class="conf-actions"><button type="button" class="btn btn-primary btn-sm" data-fix><i class="ic" data-ic="wand-sparkles"></i> Corrigir com IA</button><button type="button" class="btn btn-sm" data-ignore title="Está certo assim: sai do painel e dos próximos pedidos à IA"><i class="ic" data-ic="eye-off"></i> Ignorar</button></span></div>`).join("")}</section>`).join("")
|
|
59
|
+
: `<div class="rv-empty">${salvo ? "Nada pendente: as checagens automáticas não deixaram avisos neste deck." : "A conferência vale para apresentações salvas na biblioteca."}</div>`}</div>
|
|
60
|
+
${conferidoEm ? `<div class="conf-foot">Conferido em ${new Date(conferidoEm).toLocaleString("pt-BR")}</div>` : ""}`;
|
|
61
|
+
dlg.querySelectorAll(".conf-group").forEach((g) => {
|
|
62
|
+
const n = +g.dataset.slide;
|
|
63
|
+
if (n && state.deck?.slides?.[n - 1]) g.querySelector(".conf-slide-title").textContent = plainTitle(state.deck.slides[n - 1], n - 1);
|
|
64
|
+
g.querySelector("[data-go]")?.addEventListener("click", () => { goTo(n); dlg.close(); });
|
|
65
|
+
});
|
|
66
|
+
dlg.querySelectorAll(".conf-item").forEach((row) => {
|
|
67
|
+
const it = items.find((x) => x.id === row.dataset.id);
|
|
68
|
+
row.querySelector(".conf-kind span").textContent = it.label;
|
|
69
|
+
row.querySelector(".conf-text").textContent = it.text;
|
|
70
|
+
row.querySelector("[data-fix]").onclick = () => fix(it, dlg);
|
|
71
|
+
row.querySelector("[data-ignore]").onclick = () => decide(it.id, "ignorado");
|
|
72
|
+
});
|
|
73
|
+
dlg.querySelector("[data-recheck]")?.addEventListener("click", (e) => recheck(e.currentTarget));
|
|
74
|
+
dlg.querySelector("[data-close]").onclick = () => dlg.close();
|
|
75
|
+
hydrateIcons(dlg);
|
|
76
|
+
};
|
|
77
|
+
if (!dlg) {
|
|
78
|
+
dlg = document.createElement("dialog");
|
|
79
|
+
dlg.id = "conference-dialog"; dlg.className = "review-dialog conference-dialog";
|
|
80
|
+
document.body.append(dlg);
|
|
81
|
+
dlg.addEventListener("refresh", () => { if (dlg.open) fill(); });
|
|
82
|
+
}
|
|
83
|
+
fill();
|
|
84
|
+
if (!dlg.open) dlg.showModal();
|
|
85
|
+
refresh();
|
|
86
|
+
}
|
|
87
|
+
window.addEventListener("sagadeck:chat-done", refreshSoon);
|
|
88
|
+
return { open, refresh, refreshSoon };
|
|
89
|
+
}
|
|
90
|
+
window.SagaConference = { create };
|
|
91
|
+
})();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Conteúdo técnico em um só lugar: fonte à esquerda, resultado à direita. */
|
|
2
|
+
.dev-author-dialog{margin:auto;width:min(1110px,96vw);max-width:96vw;height:min(810px,94dvh);max-height:94dvh;padding:0;border:1px solid var(--ui-line);border-radius:18px;background:var(--chrome);color:var(--text);box-shadow:0 24px 90px #0005;font:13px var(--font-sans)}
|
|
3
|
+
.dev-author-dialog[open]{display:flex;flex-direction:column}.dev-author-dialog::backdrop{background:#101521aa;backdrop-filter:blur(4px)}
|
|
4
|
+
.dev-author-dialog header{display:flex;justify-content:space-between;align-items:center;padding:20px 25px;border-bottom:1px solid var(--ui-line)}.dev-author-dialog header small{color:var(--brand);font-size:10px;font-weight:700;letter-spacing:.13em}.dev-author-dialog h2{margin:5px 0;font-size:25px;letter-spacing:-.035em}.dev-author-dialog header p{margin:5px 0 0;color:var(--text-2)}
|
|
5
|
+
.dev-author-dialog button{padding:9px 12px;border:1px solid var(--ui-line-strong);border-radius:8px;background:var(--chrome);color:var(--text);font:inherit;cursor:pointer}.dev-author-dialog button:hover{background:var(--hover)}.dev-author-dialog button:disabled{opacity:.45;cursor:default}.dev-author-dialog button:focus-visible,.dev-author-dialog input:focus-visible,.dev-author-dialog select:focus-visible,.dev-author-dialog textarea:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
|
|
6
|
+
.dev-author-work{display:grid;grid-template-columns:minmax(0,1fr) 340px;min-height:0;flex:1}.dev-author-source{display:flex;flex-direction:column;min-height:0;padding:22px 24px;gap:10px}.dev-author-dialog label{display:block;color:var(--text-2);font-size:12px}.dev-author-dialog input:not([type=radio]),.dev-author-dialog textarea,.dev-author-dialog select{display:block;width:100%;margin-top:6px;padding:9px 10px;border:1px solid var(--ui-line-strong);border-radius:7px;background:var(--chrome);color:var(--text);font:13px var(--font-sans)}
|
|
7
|
+
.dev-author-dialog #dev-source{flex:1;min-height:150px;margin-top:0;resize:none;white-space:pre;tab-size:2;font:13px/1.65 var(--font-mono,monospace);background:var(--canvas-bg)}.dev-author-meta{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-meta>label:first-child{grid-column:1/-1}.dev-author-dialog small{font-size:10px}.dev-author-dialog aside{padding:22px;overflow-y:auto;border-left:1px solid var(--ui-line)}.dev-author-dialog fieldset{border:0;padding:0;margin:0 0 22px}.dev-author-dialog legend{font-size:12px;font-weight:700;color:var(--text);margin-bottom:12px}
|
|
8
|
+
.dev-author-modes label{display:flex;align-items:flex-start;gap:9px;padding:12px;margin-bottom:8px;border:1px solid var(--ui-line);border-radius:9px;cursor:pointer}.dev-author-modes label:has(input:checked){background:var(--selected);border-color:var(--brand)}.dev-author-modes input{margin-top:3px;accent-color:var(--brand)}.dev-author-modes b{display:block;color:var(--text);font-size:13px}.dev-author-modes small{display:block;line-height:1.5;margin-top:4px;color:var(--text-2)}
|
|
9
|
+
.dev-author-dialog h3{font-size:13px;margin-bottom:9px}.dev-author-dialog .dev-author-tip{font-size:11px;line-height:1.5;color:var(--text-2);margin:0 0 10px}.dev-author-range{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-dialog [data-guide]>label{margin:10px 0}.dev-author-dialog [data-step-text]{resize:vertical;max-height:150px}.dev-author-dialog [data-add-step]{width:100%;margin:4px 0 12px}.dev-author-steps{display:grid;gap:6px}.dev-author-step{display:flex;gap:4px}.dev-author-step button:first-child{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.dev-author-step button.active{border-color:var(--brand);color:var(--brand)}.dev-author-step button:last-child{font-size:10px;padding:7px}
|
|
10
|
+
.dev-author-dialog [data-request]{max-height:210px;overflow:auto;border:1px solid var(--ui-line);padding:12px;border-radius:8px;background:var(--canvas-bg);font:11px/1.6 var(--font-mono,monospace);white-space:pre-wrap;overflow-wrap:anywhere;margin-bottom:12px}.dev-author-secrets{padding:12px;border:1px solid var(--brand);border-radius:8px;color:var(--text);background:var(--selected);line-height:1.6;font-size:11px;overflow-wrap:anywhere}.dev-author-dialog [hidden]{display:none!important}
|
|
11
|
+
.dev-author-dialog footer{display:flex;align-items:center;justify-content:flex-end;gap:9px;padding:14px 24px;border-top:1px solid var(--ui-line)}.dev-author-dialog [data-status]{flex:1;font-size:11px;line-height:1.5;color:var(--text-2)}.dev-author-dialog .dev-author-primary{background:var(--brand);color:#fff;border-color:var(--brand)}.dev-author-dialog .dev-author-primary:hover{filter:brightness(1.08)}
|
|
12
|
+
@media(max-width:760px){.dev-author-dialog{height:94dvh}.dev-author-work{display:flex;flex-direction:column;overflow-y:auto}.dev-author-source{min-height:390px;flex-shrink:0;padding:16px}.dev-author-dialog aside{border-left:0;border-top:1px solid var(--ui-line);overflow:visible;padding:16px}.dev-author-dialog header{padding:15px 16px}.dev-author-dialog h2{font-size:21px}.dev-author-dialog footer{padding:12px 16px;flex-wrap:wrap}.dev-author-dialog [data-status]{flex-basis:100%}}
|