sagadeck 1.10.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 +2 -1
- package/bin/sagadeck.js +1 -1
- package/docs/ANDAMENTO.md +187 -0
- package/docs/REFERENCIA.md +24 -2
- package/package.json +1 -1
- package/src/ai/critique.js +8 -1
- package/src/ai/deck-ai.js +33 -11
- 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/build.js +1 -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/base.css +3 -0
- package/src/runtime/solution.js +131 -0
- package/src/studio/ai-chat-route.js +44 -1
- package/src/studio/conference-routes.js +44 -0
- package/src/studio/conference.js +144 -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/public/ai-settings.js +4 -4
- package/src/studio/public/app.js +12 -1
- package/src/studio/public/chat.js +6 -2
- package/src/studio/public/conference-ui.js +91 -0
- package/src/studio/public/index.html +3 -0
- package/src/studio/public/library.js +4 -1
- package/src/studio/public/slide-form.js +8 -3
- package/src/studio/public/style.css +23 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +18 -6
|
@@ -1,10 +1,70 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { mathWarning } from '../ai/math-check.js';
|
|
4
|
+
import { renameRetry } from '../fs-retry.js';
|
|
5
|
+
|
|
6
|
+
// Relatório da criação com IA (.sagadeck/avaliacao-geracao.json): revisão visual, variedade, números sem base no
|
|
7
|
+
// material, contas e figuras de fora. `avisado` diz se a pessoa já viu os avisos no chat (generationNotice).
|
|
8
|
+
const reportFile = (dir) => path.join(dir, '.sagadeck', 'avaliacao-geracao.json');
|
|
9
|
+
|
|
10
|
+
export function loadGenReport(dir) {
|
|
11
|
+
if (!dir) return null;
|
|
12
|
+
try { return JSON.parse(fs.readFileSync(reportFile(dir), 'utf8')); } catch { return null; }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// grava inteiro (tmp + rename): o painel Conferência e o chat mexem no mesmo arquivo
|
|
16
|
+
export function writeGenReport(dir, report) {
|
|
17
|
+
const file = reportFile(dir), tmp = `${file}.tmp-${process.pid}`;
|
|
18
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
19
|
+
fs.writeFileSync(tmp, JSON.stringify(report, null, 2), 'utf8');
|
|
20
|
+
renameRetry(tmp, file);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// impressão digital de um slide (o que ele mostra, sem uid nem marcas do Studio): o aviso da revisão visual vale para
|
|
24
|
+
// o slide como ele era; mudou, o painel avisa que o aviso pode estar velho
|
|
25
|
+
export function slideMark(slide) {
|
|
26
|
+
const s = JSON.stringify({ ...slide, uid: undefined, auto: undefined, review: undefined });
|
|
27
|
+
let h = 5381; for (const c of s) h = ((h << 5) + h + c.codePointAt(0)) >>> 0;
|
|
28
|
+
return h.toString(36);
|
|
29
|
+
}
|
|
30
|
+
// marcas por slide: pelo uid (sobrevive a inserir e apagar slides) e, sem uid, pela posição
|
|
31
|
+
export const slideMarks = (spec) => Object.fromEntries((spec?.slides || []).map((s, i) => [s?.uid || `#${i + 1}`, slideMark(s)]));
|
|
3
32
|
|
|
4
33
|
export function saveGenReport(dir, generation) {
|
|
5
34
|
const reportDir = path.join(dir, '.sagadeck');
|
|
6
35
|
fs.mkdirSync(reportDir, { recursive: true });
|
|
7
|
-
fs.writeFileSync(
|
|
36
|
+
fs.writeFileSync(reportFile(dir), JSON.stringify({
|
|
8
37
|
createdAt: new Date().toISOString(), quality: generation.quality, variety: generation.variety,
|
|
38
|
+
...(generation.spec ? { slideMarks: slideMarks(generation.spec) } : {}),
|
|
39
|
+
...(generation.facts ? { facts: generation.facts } : {}), ...(generation.math ? { math: generation.math } : {}),
|
|
40
|
+
...(generation.coverage ? { coverage: generation.coverage } : {}), avisado: false,
|
|
9
41
|
}, null, 2), 'utf8');
|
|
10
42
|
}
|
|
43
|
+
|
|
44
|
+
// Os avisos que a conferência automática deixou sem resolver, como texto para a pessoa
|
|
45
|
+
export function generationWarnings(report) {
|
|
46
|
+
const out = [];
|
|
47
|
+
const m = mathWarning(report?.math);
|
|
48
|
+
if (m) out.push(m);
|
|
49
|
+
const facts = report?.facts?.unsupported || [];
|
|
50
|
+
if (facts.length) out.push(`Números sem base no material: ${facts.map((b) => `${b.number} (slide ${b.slide})`).join(', ')}`);
|
|
51
|
+
const missing = report?.coverage?.missing || [];
|
|
52
|
+
if (missing.length) out.push(`Figuras e tabelas do material sem slide: ${missing.join('; ')}`);
|
|
53
|
+
const issues = report?.quality?.issues || [];
|
|
54
|
+
for (const i of issues.slice(0, 12)) out.push(`Slide ${i.slide}: ${String(i.text).slice(0, 240)}`);
|
|
55
|
+
if (issues.length > 12) out.push(`… e mais ${issues.length - 12} aviso(s) da revisão visual.`);
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Na primeira vez que a apresentação abre depois da criação, os avisos viram uma mensagem do chat (uma vez só):
|
|
60
|
+
// antes eles ficavam só no arquivo, e ninguém via que a revisão tinha reclamado.
|
|
61
|
+
export function generationNotice(dir) {
|
|
62
|
+
if (!dir) return null;
|
|
63
|
+
let report;
|
|
64
|
+
try { report = JSON.parse(fs.readFileSync(reportFile(dir), 'utf8')); } catch { return null; }
|
|
65
|
+
if (report.avisado) return null;
|
|
66
|
+
const warnings = generationWarnings(report);
|
|
67
|
+
try { fs.writeFileSync(reportFile(dir), JSON.stringify({ ...report, avisado: true }, null, 2), 'utf8'); } catch { return null; }
|
|
68
|
+
if (!warnings.length) return null;
|
|
69
|
+
return `A conferência automática da criação desta apresentação deixou estes avisos sem resolver:\n${warnings.map((w) => `- ${w}`).join('\n')}\nPeça "corrija os avisos da criação" para eu tentar de novo, ou ajuste à mão.`;
|
|
70
|
+
}
|
|
@@ -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."}]},
|
|
@@ -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);
|
|
@@ -2807,6 +2809,13 @@
|
|
|
2807
2809
|
state, showToast: (...a) => showToast(...a), renderThumbnails: () => renderThumbnails(), renderCurrentSlide: () => renderCurrentSlide(),
|
|
2808
2810
|
trackDeck: (l) => trackDeck(l), hydrateIcons: (el) => hydrateIcons(el), plainTitle: (sl, i) => plainTitle(sl, i),
|
|
2809
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
|
+
});
|
|
2810
2819
|
// Desfazer / refazer único (history.js): todo caminho que muda o deck passa por trackDeck, com o rótulo da origem
|
|
2811
2820
|
const deckHistory = window.SagaHistory.createHistory();
|
|
2812
2821
|
function trackDeck(label = "Edição") {
|
|
@@ -2866,7 +2875,8 @@
|
|
|
2866
2875
|
});
|
|
2867
2876
|
if (adopted && !state.applyingHistory) deckHistory.rebase(state.deck);
|
|
2868
2877
|
updateSaveStatus(res.ok ? "saved" : "error");
|
|
2869
|
-
|
|
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(); }
|
|
2870
2880
|
else if (throwOnError) throw new Error(data.error || 'Não foi possível salvar as mudanças antes de exportar.');
|
|
2871
2881
|
} catch (err) {
|
|
2872
2882
|
console.warn("Erro ao sincronizar com servidor:", err);
|
|
@@ -3910,6 +3920,7 @@ ${ta.value}`;
|
|
|
3910
3920
|
document.getElementById("btn-header-footer").addEventListener("click", openHeaderFooter);
|
|
3911
3921
|
document.getElementById("btn-styles")?.addEventListener("click", openStyles);
|
|
3912
3922
|
document.getElementById("btn-review-changes")?.addEventListener("click", openReviewList);
|
|
3923
|
+
document.getElementById("btn-conference")?.addEventListener("click", () => conference.open());
|
|
3913
3924
|
document.getElementById("btn-close-hf").addEventListener("click", closeHeaderFooter);
|
|
3914
3925
|
document.getElementById("btn-hf-cancel").addEventListener("click", closeHeaderFooter);
|
|
3915
3926
|
document.getElementById("btn-hf-apply").addEventListener("click", applyHeaderFooter);
|
|
@@ -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,6 +157,7 @@ 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");
|
|
@@ -245,6 +247,7 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
245
247
|
dom.chatInput.disabled = false;
|
|
246
248
|
dom.chatInput.focus();
|
|
247
249
|
await saveChatHistory();
|
|
250
|
+
window.dispatchEvent(new Event("sagadeck:chat-done")); // o painel Conferência confere o selo de novo
|
|
248
251
|
}
|
|
249
252
|
}
|
|
250
253
|
|
|
@@ -321,7 +324,8 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
321
324
|
async function loadChatHistory() {
|
|
322
325
|
state.autoRunCommands = false; // outra apresentação: nada fica liberado
|
|
323
326
|
try {
|
|
324
|
-
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);
|
|
325
329
|
state.chatHistory = Array.isArray(history) ? history : [];
|
|
326
330
|
state.loadingHistory = true; // conversa guardada não é resposta nova (o atalho do chat não acende)
|
|
327
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
|
+
})();
|
|
@@ -336,6 +336,7 @@
|
|
|
336
336
|
<i class="ic" data-ic="wand"></i><span>Arrumar layout</span><b id="fiscal-badge" class="fiscal-badge clean">0</b>
|
|
337
337
|
</button>
|
|
338
338
|
<button id="btn-review-changes" class="rbtn rbtn-lg" title="O que mudou em relação ao original: aceitar ou desfazer slide a slide"><i class="ic" data-ic="git-compare"></i><span>Mudanças</span><b id="review-badge" class="fiscal-badge clean">0</b></button>
|
|
339
|
+
<button id="btn-conference" class="rbtn rbtn-lg" title="O que as checagens automáticas acharam (contas, números sem base no material, avisos da revisão, figuras de fora, slides da IA): ir ao slide, corrigir com IA ou ignorar"><i class="ic" data-ic="list-checks"></i><span>Conferência</span><b id="conference-badge" class="fiscal-badge clean">0</b></button>
|
|
339
340
|
<button id="btn-ai-review" class="rbtn rbtn-lg" title="A IA recebe uma imagem do slide, avalia a composição e aplica melhorias. Usa tokens."><i class="ic" data-ic="scan-eye"></i><span>Revisar com IA</span></button>
|
|
340
341
|
</div>
|
|
341
342
|
<div class="rgroup-label">Layout</div>
|
|
@@ -879,6 +880,7 @@
|
|
|
879
880
|
<script src="screenshot-editor.js"></script>
|
|
880
881
|
<script src="formula.js"></script>
|
|
881
882
|
<script src="calc.js"></script>
|
|
883
|
+
<script src="solution.js"></script>
|
|
882
884
|
<script src="science.js"></script>
|
|
883
885
|
<script src="map-background.js"></script>
|
|
884
886
|
<script src="map.js"></script>
|
|
@@ -892,6 +894,7 @@
|
|
|
892
894
|
<script src="merge-decks.js"></script>
|
|
893
895
|
<script src="history.js"></script>
|
|
894
896
|
<script src="review-ui.js"></script>
|
|
897
|
+
<script src="conference-ui.js"></script>
|
|
895
898
|
<script src="share-ui.js"></script>
|
|
896
899
|
<script src="slide-clipboard.js"></script>
|
|
897
900
|
<script src="slide-select.js"></script>
|
|
@@ -539,7 +539,10 @@
|
|
|
539
539
|
generating = true;
|
|
540
540
|
btn.disabled = true;
|
|
541
541
|
status.textContent = "Gerando… pode levar um minuto.";
|
|
542
|
-
|
|
542
|
+
// guide: o que a pessoa escolheu no pedido guiado; padrao = não escreveu nada e foi o texto padrão (fica no registro da criação)
|
|
543
|
+
const v = values();
|
|
544
|
+
const guideInfo = { intent: v.intent, audience: v.audience.trim() || undefined, goal: v.goal.trim() || undefined, route: v.route || undefined, padrao: !v.briefing.trim() };
|
|
545
|
+
const payload = { topic, briefing, answer: answer || undefined, stream: true, duration: Number(min.value) || undefined, style: style.value || undefined, materials: docs.map((d) => d.id), guide: guideInfo };
|
|
543
546
|
try {
|
|
544
547
|
const res = await fetch("api/library/decks/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
|
545
548
|
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || `HTTP ${res.status}`);
|
|
@@ -220,10 +220,15 @@
|
|
|
220
220
|
{ addLabel: "Adicionar item", newItem: () => ({ title: "Novo item", text: "" }) })],
|
|
221
221
|
// aula (src/lessons.js): exercício resolvido, calculadora ao vivo, algoritmo animado
|
|
222
222
|
solution: [f.text("kicker", "Chapéu"), f.text("title", "Título"), f.area("problem", "Enunciado"),
|
|
223
|
-
f.list("givens", "Dados", obj([f.text("symbol", "Símbolo (LaTeX)", { mono: true }), f.text("value", "Valor", { placeholder: "0,05" }), f.text("unit", "Unidade"), f.text("label", "Nome"),
|
|
223
|
+
f.list("givens", "Dados", obj([f.text("symbol", "Símbolo (LaTeX)", { mono: true }), f.text("value", "Valor", { placeholder: "0,05" }), f.text("unit", "Unidade"), f.text("label", "Nome"),
|
|
224
|
+
f.more([f.text("latex", "Ou a linha inteira em LaTeX", { mono: true }), f.text("name", "Nome na conta", { mono: true, hint: "Como as fórmulas chamam este dado. Sem ele, vem do símbolo: L_1 vira L1." }),
|
|
225
|
+
f.num("min", "Mínimo do controle"), f.num("max", "Máximo do controle")])]), { addLabel: "Adicionar dado", newItem: () => ({ symbol: "x", value: "1", unit: "" }) }),
|
|
224
226
|
f.text("find", "Pede-se (LaTeX)", { mono: true }),
|
|
225
|
-
f.list("steps", "Passos (um por clique)", obj([f.text("text", "O que se faz"), f.area("latex", "A conta (LaTeX)", { mono: true, rows: 2
|
|
226
|
-
|
|
227
|
+
f.list("steps", "Passos (um por clique)", obj([f.text("text", "O que se faz"), f.area("latex", "A conta (LaTeX)", { mono: true, rows: 2, hint: "{{nome}} mostra o valor calculado; sem ele, o valor entra no fim." }),
|
|
228
|
+
f.text("fn", "Fórmula que calcula", { mono: true, placeholder: "C*(1+i)^n", hint: "O sagadeck faz a conta com os dados e os passos anteriores (pelo nome)." }),
|
|
229
|
+
f.text("name", "Nome do resultado", { mono: true, placeholder: "M" }), f.text("unit", "Unidade do resultado"), f.num("decimals", "Casas decimais"), f.text("note", "Observação")]), { addLabel: "Adicionar passo", newItem: () => ({ text: "Próximo passo", latex: "" }) }),
|
|
230
|
+
f.obj("answer", "Resposta", [f.text("latex", "Resultado (LaTeX)", { mono: true, hint: "Use {{nome}} para o valor calculado: M \\approx {{M}}" }), f.text("text", "Em palavras"), f.text("label", "Rótulo", { placeholder: "Resposta" })]),
|
|
231
|
+
f.bool("interactive", "Dados ajustáveis na apresentação"),
|
|
227
232
|
f.more([f.text("prompt", "Pergunta antes do primeiro passo", { placeholder: "Por onde você começaria?" })])],
|
|
228
233
|
calc: window.SagaCalcFields(f, obj), map: window.SagaMapFields(f, obj),
|
|
229
234
|
codelab: window.SagaLabFields.codelab(f, obj),
|
|
@@ -1304,6 +1304,29 @@ textarea.form-control.napkin-input { min-height: 220px; resize: vertical; font-f
|
|
|
1304
1304
|
.rv-note { color: var(--text-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1305
1305
|
.rv-empty { padding: 24px; text-align: center; color: var(--text-2); }
|
|
1306
1306
|
|
|
1307
|
+
/* Revisar › Conferência (conference-ui.js): achados das checagens automáticas, por slide */
|
|
1308
|
+
.conference-dialog { width: min(820px, 94vw); }
|
|
1309
|
+
.conf-group { padding: 4px 0 8px; border-bottom: 1px solid var(--ui-line); }
|
|
1310
|
+
.conf-group:last-child { border-bottom: 0; }
|
|
1311
|
+
.conf-group-head { display: flex; align-items: center; gap: 8px; padding: 6px 8px; font-size: 12px; }
|
|
1312
|
+
.conf-group-head .rv-num { width: auto; color: var(--text-2); }
|
|
1313
|
+
.conf-slide-title { flex: 1; min-width: 0; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1314
|
+
.conf-item { display: flex; align-items: flex-start; gap: 10px; margin: 2px 0 2px 34px; padding: 6px 8px; border-left: 3px solid var(--ui-line); border-radius: 0 6px 6px 0; font-size: 12px; }
|
|
1315
|
+
.conf-item:hover { background: var(--hover); }
|
|
1316
|
+
.conf-item.conf-conta, .conf-item.conf-material { border-left-color: #c42b1c; }
|
|
1317
|
+
.conf-item.conf-numero, .conf-item.conf-figura { border-left-color: #bc4b09; }
|
|
1318
|
+
.conf-item.conf-visual { border-left-color: #0063b1; }
|
|
1319
|
+
.conf-item.conf-derivado, .conf-item.conf-proprio { border-left-color: #8764b8; }
|
|
1320
|
+
.conf-kind { display: inline-flex; align-items: center; gap: 4px; flex: 0 0 170px; font-weight: 600; color: var(--text-2); }
|
|
1321
|
+
.conf-kind .ic { width: 14px; height: 14px; }
|
|
1322
|
+
.conf-text { flex: 1; min-width: 0; line-height: 1.4; }
|
|
1323
|
+
.conf-actions { display: flex; gap: 6px; flex-shrink: 0; }
|
|
1324
|
+
.conf-foot { padding: 6px 12px 10px; font-size: 11px; color: var(--text-3); }
|
|
1325
|
+
@media (max-width: 640px) {
|
|
1326
|
+
.conf-item { flex-wrap: wrap; margin-left: 8px; }
|
|
1327
|
+
.conf-kind { flex-basis: 100%; }
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1307
1330
|
/* Arquivo › Compartilhar link (share-ui.js) */
|
|
1308
1331
|
.share-dialog { width: min(620px, 94vw); }
|
|
1309
1332
|
.share-new { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; margin-bottom: 14px; }
|