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
package/src/lessons.js
CHANGED
|
@@ -8,6 +8,7 @@ import katex from "katex";
|
|
|
8
8
|
import { md, esc } from "./markup.js";
|
|
9
9
|
import "./runtime/formula.js";
|
|
10
10
|
import "./runtime/calc.js";
|
|
11
|
+
import "./runtime/solution.js";
|
|
11
12
|
|
|
12
13
|
const F = globalThis.SagaFormula;
|
|
13
14
|
const tex = (s, display = true) => katex.renderToString(String(s ?? ""), { displayMode: display, throwOnError: false, strict: "ignore", trust: false, maxExpand: 1000, maxSize: 20 });
|
|
@@ -29,32 +30,92 @@ const panel = (i, inner, cls = "") => `<section class="lesson-panel dyn-frame ${
|
|
|
29
30
|
const PROSE = /\p{L}{3,}/u;
|
|
30
31
|
const isProse = (s) => !/\\[a-zA-Z]|[\^_{}]/.test(s) && PROSE.test(String(s).replace(/\b(sen|cos|tan|tg|log|ln|exp|max|min|lim|mod)\b/g, ""));
|
|
31
32
|
const mathOrText = (s) => (isProse(String(s ?? "")) ? `<span class="sol-txt t f-body">${md(String(s))}</span>` : tex(s, false));
|
|
32
|
-
|
|
33
|
+
// % (e # & _ $) são comandos no LaTeX: "60 %" saía como erro vermelho na conta da criança
|
|
34
|
+
const unitTex = (u) => String(u).replace(/(?<!\\)[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,");
|
|
35
|
+
const unitSuffix = (u) => (u ? `\\ \\mathrm{${unitTex(u)}}` : "");
|
|
36
|
+
function givenTex(g, value = null) {
|
|
33
37
|
if (typeof g === "string") return g;
|
|
34
38
|
if (g.latex) return g.latex;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const unit = g.unit ? String(g.unit).replace(/(?<!\\)[%#&_$]/g, (c) => `\\${c}`).replace(/°/g, "^{\\circ}").replace(/ /g, "\\,") : "";
|
|
38
|
-
return `${g.symbol ?? ""} = ${value}${unit ? `\\ \\mathrm{${unit}}` : ""}`;
|
|
39
|
+
value ??= typeof g.value === "string" && isProse(g.value) ? `\\text{${g.value.replace(/[{}\\$]/g, "")}}` : commaTex(g.value);
|
|
40
|
+
return `${symbolTex(g.symbol)} = ${value}${unitSuffix(g.unit)}`;
|
|
39
41
|
}
|
|
42
|
+
// "p_atm" é p com índice "atm": sem chaves o LaTeX subscrevia só o "a" (aparecia "pₐtm" num exercício de aula)
|
|
43
|
+
const symbolTex = (s) => String(s ?? "").replace(/_([A-Za-z0-9]{2,})/g, "_{$1}");
|
|
44
|
+
// nome da conta em LaTeX, quando o passo não traz nem latex nem symbol: D2 → D_{2}, rho → \rho, fator → \mathrm{fator}
|
|
45
|
+
const GREEK = /^(alpha|beta|gamma|delta|epsilon|theta|lambda|mu|nu|xi|pi|rho|sigma|tau|phi|chi|psi|omega|Delta|Omega|Phi|Sigma)(\d*)$/;
|
|
46
|
+
const nameTex = (n) => (n.startsWith("_") ? "" : GREEK.test(n) ? n.replace(GREEK, (_, g, d) => `\\${g}${d ? `_{${d}}` : ""}`) : /^[A-Za-z]\d*$/.test(n) ? n.replace(/^(.)(\d+)$/, "$1_{$2}") : `\\mathrm{${n}}`);
|
|
47
|
+
// {{nome}} ou {{expressão:casas}}: o valor calculado no lugar
|
|
48
|
+
const PH = /\{\{\s*([^{}:]+?)\s*(?::\s*(\d+)\s*)?\}\}/g;
|
|
49
|
+
const S = globalThis.SagaSolution;
|
|
50
|
+
// \htmlData só para a marca do número que a apresentação troca ao vivo
|
|
51
|
+
const texLive = (s, display = true) => katex.renderToString(String(s ?? ""), { displayMode: display, throwOnError: false, strict: "ignore", trust: (c) => c.command === "\\htmlData" && Object.keys(c.attributes || {}).every((k) => k === "data-solv"), maxExpand: 1000, maxSize: 20 });
|
|
40
52
|
export function solutionHTML(s, ctx, head) {
|
|
41
53
|
const steps = (s.steps || []).map((x) => (typeof x === "string" ? { latex: x } : x || {}));
|
|
42
54
|
const givens = [].concat(s.givens || s.data || []);
|
|
43
55
|
const hasAnswer = !!s.answer;
|
|
56
|
+
const answer = hasAnswer ? (typeof s.answer === "string" ? { latex: s.answer } : s.answer) : null;
|
|
57
|
+
// contas: dados com símbolo e valor viram variáveis; passo (e resposta) com fn é calculado pelo motor
|
|
58
|
+
const m = S.model({ ...s, steps, answer });
|
|
59
|
+
const { env, results } = S.evaluate(m);
|
|
60
|
+
const live = !!s.interactive && m.vars.length > 0;
|
|
61
|
+
const T = live ? texLive : tex;
|
|
62
|
+
const marks = [], markAt = new Map();
|
|
63
|
+
const decOf = (name) => m.vars.find((v) => v.name === name)?.decimals ?? m.calcs.find((c) => c.name === name)?.decimals;
|
|
64
|
+
const mark = (expr, d) => {
|
|
65
|
+
const decimals = d != null && d !== "" ? Number(d) : decOf(expr), key = `${expr}|${decimals ?? ""}`;
|
|
66
|
+
if (!markAt.has(key)) { markAt.set(key, marks.length); marks.push({ expr, ...(decimals != null ? { decimals } : {}) }); }
|
|
67
|
+
return { k: markAt.get(key), decimals, value: S.evalExpr(expr, env) };
|
|
68
|
+
};
|
|
69
|
+
const numTex = (r) => { const t = S.format(r.value, r.decimals, "tex"); return live ? `\\htmlData{solv=${r.k}}{${t}}` : t; };
|
|
70
|
+
const fillTex = (str) => String(str ?? "").replace(PH, (_, e, d) => numTex(mark(e, d)));
|
|
71
|
+
const fillText = (str) => {
|
|
72
|
+
const nums = [];
|
|
73
|
+
const out = md(String(str ?? "").replace(PH, (_, e, d) => {
|
|
74
|
+
const r = mark(e, d), t = esc(S.format(r.value, r.decimals, "text"));
|
|
75
|
+
nums.push(live ? `<span data-solv="${r.k}" data-solv-mode="text">${t}</span>` : t);
|
|
76
|
+
return `\uE000${nums.length - 1}\uE001`;
|
|
77
|
+
}));
|
|
78
|
+
return out.replace(/\uE000(\d+)\uE001/g, (_, i) => nums[i]);
|
|
79
|
+
};
|
|
80
|
+
const errors = m.calcs.filter((c) => results[c.key].error).map((c) => `${c.label}: ${results[c.key].error}`);
|
|
81
|
+
if (errors.length) ctx?.warnings?.push(...errors.map((e) => `exercício resolvido: ${e}`));
|
|
82
|
+
// LaTeX do passo calculado: o valor onde a pessoa pôs {{nome}} (o nome do próprio passo), ou "≈ valor unidade" no fim
|
|
83
|
+
const calcTex = (o, key) => {
|
|
84
|
+
const c = m.calcs.find((x) => x.key === key);
|
|
85
|
+
if (!c) return o.latex ? fillTex(o.latex) : "";
|
|
86
|
+
if ([...String(o.latex ?? "").matchAll(PH)].some((x) => x[1] === c.name)) return fillTex(o.latex);
|
|
87
|
+
const r = mark(c.name, c.decimals);
|
|
88
|
+
const exact = r.value != null && S.parseNum(S.format(r.value, c.decimals, "text")) === r.value;
|
|
89
|
+
const lhs = o.latex ? fillTex(o.latex) : o.symbol ? String(o.symbol) : nameTex(c.name);
|
|
90
|
+
const rel = !lhs || /(=|\\approx|≈)\s*$/.test(lhs) ? "" : exact ? "= " : "\\approx ";
|
|
91
|
+
return `${lhs}${lhs ? " " : ""}${rel}${numTex(r)}${unitSuffix(o.unit)}`;
|
|
92
|
+
};
|
|
93
|
+
const calcError = (key) => { const c = m.calcs.find((x) => x.key === key); return c && results[key].error ? `<div class="sol-error dyn-error f-body">${esc(`${c.label[0].toUpperCase()}${c.label.slice(1)}: ${results[key].error}`)}</div>` : ""; };
|
|
94
|
+
const varOf = (g) => (g && typeof g === "object" ? m.vars.find((v) => v.name === (g.name ? String(g.name) : S.varName(g.symbol))) : null);
|
|
95
|
+
const givenHTML = (g) => {
|
|
96
|
+
if (typeof g === "string") return `<div class="sol-given"><div class="sol-tex">${mathOrText(g)}</div></div>`;
|
|
97
|
+
const v = live && !g.latex ? varOf(g) : null;
|
|
98
|
+
const t = v ? givenTex(g, numTex(mark(v.name, v.decimals))) : g.latex ? fillTex(g.latex) : givenTex(g);
|
|
99
|
+
const slider = v ? `<input type="range" class="sol-slider" data-solv-in="${esc(v.name)}" min="${v.min}" max="${v.max}" step="${v.step}" value="${v.value}" aria-label="${esc(v.label)}">` : "";
|
|
100
|
+
return `<div class="sol-given${slider ? " live" : ""}"><div class="sol-tex">${T(t, false)}</div>${g.label ? `<span class="t f-body">${md(g.label)}</span>` : ""}${slider}</div>`;
|
|
101
|
+
};
|
|
44
102
|
const n = 1 + steps.length + (hasAnswer ? 1 : 0); // enunciado · cada passo · resposta
|
|
45
|
-
const
|
|
46
|
-
|
|
103
|
+
const reset = live ? `<button type="button" class="sol-reset" data-solv-reset>Restaurar</button>` : "";
|
|
104
|
+
const left = `<div class="sol-problem">${s.problem ? `<div class="sol-text t f-body">${fillText(s.problem)}</div>` : ""}
|
|
105
|
+
${givens.length ? `<div class="sol-givens"><div class="sol-label t f-label">Dados${reset}</div>${givens.map(givenHTML).join("")}</div>` : ""}
|
|
47
106
|
${s.find ? `<div class="sol-find"><span class="sol-label t f-label">Pede-se</span><div class="sol-tex">${mathOrText(s.find)}</div></div>` : ""}</div>`;
|
|
48
|
-
const
|
|
49
|
-
const
|
|
107
|
+
const stepTex = steps.map((st, i) => calcTex(st, `p${i}`));
|
|
108
|
+
const stepHTML = (st, i, k) => `<article class="sol-step${i === k - 1 ? " now" : ""}"><span class="sol-n t f-label">${String(i + 1).padStart(2, "0")}</span><div class="sol-body">${st.text ? `<div class="sol-why t f-body">${fillText(st.text)}</div>` : ""}${stepTex[i] ? `<div class="sol-math">${T(stepTex[i])}</div>` : ""}${calcError(`p${i}`)}${st.note ? `<div class="sol-note t f-body">${fillText(st.note)}</div>` : ""}</div></article>`;
|
|
109
|
+
const answerTex = answer ? calcTex(answer, "resp") : "";
|
|
50
110
|
const frames = Array.from({ length: n }, (_, k) => {
|
|
51
111
|
const done = Math.min(k, steps.length);
|
|
52
112
|
const list = steps.slice(0, done).map((st, i) => stepHTML(st, i, done)).join("");
|
|
53
|
-
const final = hasAnswer && k === n - 1 ? `<div class="sol-answer"><span class="sol-label t f-label">${esc(answer.label || "Resposta")}</span>${
|
|
113
|
+
const final = hasAnswer && k === n - 1 ? `<div class="sol-answer"><span class="sol-label t f-label">${esc(answer.label || "Resposta")}</span>${answerTex ? `<div class="sol-math">${T(answerTex)}</div>` : ""}${calcError("resp")}${answer.text ? `<div class="t f-body">${fillText(answer.text)}</div>` : ""}</div>` : "";
|
|
54
114
|
const empty = k === 0 ? `<div class="sol-think t f-display">${md(s.prompt || "Por onde você começaria?")}</div>` : "";
|
|
55
115
|
return panel(k, `<div class="sol-steps">${empty}${list}${final}</div>`);
|
|
56
116
|
}).join("");
|
|
57
|
-
|
|
117
|
+
const payload = live ? `<script type="application/json" class="sol-model">${JSON.stringify({ ...m, marks }).replace(/</g, "\\u003c")}</script>` : "";
|
|
118
|
+
return `<div class="L-solution dyn" data-lesson="solution" data-lesson-count="${n}"${live ? " data-solution" : ""}>${head(s)}<div class="sol-row">${left}<div class="dyn-frames">${frames}</div></div>${payload}</div>`;
|
|
58
119
|
}
|
|
59
120
|
|
|
60
121
|
// ------------------------------------------------------------------------------------------------ calc
|
package/src/mcp/server.js
CHANGED
|
@@ -135,7 +135,7 @@ const TOOLS = [
|
|
|
135
135
|
inputSchema: {
|
|
136
136
|
type: "object",
|
|
137
137
|
properties: {
|
|
138
|
-
so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video"] }, description: "Só estes recursos (padrão: todos)" },
|
|
138
|
+
so: { type: "array", items: { type: "string", enum: ["texto", "visao", "imagem", "pesquisa", "video", "conferencia"] }, description: "Só estes recursos (padrão: todos)" },
|
|
139
139
|
rapido: { type: "boolean", description: "Pula geração de imagem e vídeo (os lentos e cobrados)" },
|
|
140
140
|
video_real: { type: "boolean", description: "Gera um clipe curto de verdade e confere o MP4 (cobrado; leva minutos)" },
|
|
141
141
|
},
|
package/src/package.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// <nome>.yaml a apresentação (sem campos internos da máquina de quem salvou)
|
|
6
6
|
// imagens/, widgets/… os arquivos, nos mesmos caminhos relativos do YAML
|
|
7
7
|
// assets/ arquivos que estavam FORA da pasta do deck (o YAML do pacote aponta para cá)
|
|
8
|
+
// .sagadeck/geracao.json o pedido que originou a apresentação com IA, quando existe
|
|
8
9
|
// FALTANDO.txt o que o deck usa mas não existia ao salvar
|
|
9
10
|
//
|
|
10
11
|
// Também abre um .zip comum com um .yaml dentro (versão sem manifesto).
|
|
@@ -93,6 +94,9 @@ export async function packDeck(spec, { baseDir, name = "apresentacao", generator
|
|
|
93
94
|
if (size <= budget) { root.file(q.rel, fs.readFileSync(q.abs)); files.push(q.rel); budget -= size; }
|
|
94
95
|
else skipped.push(q.rel);
|
|
95
96
|
}
|
|
97
|
+
// o pedido que originou a apresentação (src/studio/generation-request.js) viaja junto, para auditar depois
|
|
98
|
+
const request = path.join(dir, ".sagadeck", "geracao.json");
|
|
99
|
+
if (fs.existsSync(request)) { root.file(".sagadeck/geracao.json", fs.readFileSync(request)); files.push(".sagadeck/geracao.json"); }
|
|
96
100
|
for (const a of assets) {
|
|
97
101
|
if (a.exists) { root.file(a.rel, fs.readFileSync(a.abs)); files.push(a.rel); }
|
|
98
102
|
else missing.push(a.ref);
|
package/src/runtime/base.css
CHANGED
|
@@ -1303,6 +1303,9 @@ html[data-motion="subtle"] .lesson-panel.active{animation:lesson-soft .6s cubic-
|
|
|
1303
1303
|
.sol-label{font-size:20px;color:var(--em);letter-spacing:.08em;white-space:nowrap}
|
|
1304
1304
|
.sol-givens{display:flex;flex-direction:column;gap:10px}
|
|
1305
1305
|
.sol-given{display:flex;align-items:baseline;gap:16px;font-size:28px}.sol-given .sol-tex{min-width:7em}.sol-given>span{font-size:22px;color:var(--muted)}
|
|
1306
|
+
.sol-given.live{flex-wrap:wrap;row-gap:4px}.sol-slider{flex:1 0 100%;height:30px;accent-color:var(--hi);cursor:pointer}
|
|
1307
|
+
.sol-reset{font:inherit;font-size:16px;letter-spacing:normal;color:var(--fg);background:transparent;border:1px solid var(--line);border-radius:8px;padding:4px 12px;margin-left:16px;cursor:pointer}.sol-reset:hover{border-color:var(--fg)}
|
|
1308
|
+
.sol-error{font-size:20px;margin-top:4px;padding:8px 14px;border-radius:12px}
|
|
1306
1309
|
.sol-find{display:flex;align-items:baseline;gap:16px;font-size:30px;padding-top:14px;border-top:1px dashed var(--line)}
|
|
1307
1310
|
.sol-steps{flex:1;display:flex;flex-direction:column;justify-content:flex-end;gap:10px;min-height:0;overflow:hidden;-webkit-mask:linear-gradient(to bottom,transparent 0,#000 60px);mask:linear-gradient(to bottom,transparent 0,#000 60px)}.sol-steps.sol-fit{-webkit-mask:none;mask:none}
|
|
1308
1311
|
/* passos anteriores em tamanho menor só quando não cabem todos no normal (com 3 passos curtos, ficavam miúdos à toa) */
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Exercício resolvido com contas de verdade (layout solution): o número do passo nasce da fórmula, não do texto.
|
|
2
|
+
// Os dados com símbolo e valor viram variáveis; cada passo com `fn` é calculado na ordem (um usa os anteriores pelo
|
|
3
|
+
// nome) com o compilador de src/runtime/formula.js (sem eval), e `{{nome}}` ou `{{expressão:casas}}` no LaTeX ou no
|
|
4
|
+
// texto vira o valor. O mesmo arquivo roda no motor (prévia, PDF) e na apresentação (interactive: o dado vira
|
|
5
|
+
// controle e os passos recalculam). Precisa de SagaFormula carregado antes.
|
|
6
|
+
(function (root) {
|
|
7
|
+
const ID = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
8
|
+
// "L_1" → L1 · "\rho" → rho · "v_{0}" → v0 · "h_f" → hf
|
|
9
|
+
function varName(symbol) {
|
|
10
|
+
const n = String(symbol ?? "").replace(/\\(?:mathrm|mathit|text|bar|vec|hat)\b/g, "").replace(/[\\_{}\s]/g, "");
|
|
11
|
+
return ID.test(n) ? n : "";
|
|
12
|
+
}
|
|
13
|
+
// "0,05" → 0.05 · "1.000" → 1000 · "9.81" → 9.81 · "1{,}5" → 1.5 · "2,5e-3" → 0.0025; texto → null
|
|
14
|
+
function parseNum(v) {
|
|
15
|
+
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
16
|
+
let t = String(v ?? "").replace(/\{,\}/g, ",").replace(/\\,|[\s ]/g, "");
|
|
17
|
+
if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(t)) t = t.replace(/\./g, "");
|
|
18
|
+
t = t.replace(",", ".");
|
|
19
|
+
return /^-?(\d+\.?\d*|\.\d+)(e[+-]?\d+)?$/i.test(t) ? Number(t) : null;
|
|
20
|
+
}
|
|
21
|
+
// casas que a pessoa escreveu ("0,026" → 3): o dado aparece como foi escrito
|
|
22
|
+
const writtenDecimals = (v) => { const m = String(v).replace(/\{,\}/g, ",").match(/[.,](\d+)$/); return m && !/^\d{1,3}(\.\d{3})+$/.test(String(v)) ? m[1].length : 0; };
|
|
23
|
+
|
|
24
|
+
// o modelo: variáveis (dados) e contas (passos e resposta com fn), na ordem
|
|
25
|
+
function model(s) {
|
|
26
|
+
const vars = [];
|
|
27
|
+
for (const g of [].concat(s.givens || s.data || [])) {
|
|
28
|
+
if (!g || typeof g !== "object") continue;
|
|
29
|
+
const name = g.name ? String(g.name) : varName(g.symbol), value = parseNum(g.value);
|
|
30
|
+
if (!ID.test(name) || value == null || vars.some((v) => v.name === name)) continue;
|
|
31
|
+
const min = g.min != null ? Number(g.min) : value > 0 ? value / 10 : value < 0 ? value * 10 : -10;
|
|
32
|
+
const max = g.max != null ? Number(g.max) : value > 0 ? value * 10 : value < 0 ? value / 10 : 10;
|
|
33
|
+
const decimals = g.decimals != null ? Number(g.decimals) : writtenDecimals(g.value);
|
|
34
|
+
// passo do controle nas casas do dado (1000 anda de 1 em 1, 0,026 de 0,001), sem passar de ~20 mil posições
|
|
35
|
+
const fine = 10 ** -decimals, range = Math.abs(max - min) || 1;
|
|
36
|
+
const step = g.step != null ? Number(g.step) : range / fine <= 20000 ? fine : 10 ** Math.floor(Math.log10(range / 1000));
|
|
37
|
+
// mínimo e máximo na grade do passo: o controle só para em múltiplos do mínimo, e 0,01 virava 0,011
|
|
38
|
+
const snap = (x, f) => Number((f(x / step) * step).toPrecision(12));
|
|
39
|
+
vars.push({ name, value, decimals, min: snap(min, Math.floor), max: snap(max, Math.ceil), step, label: String(g.label || g.symbol || name) });
|
|
40
|
+
}
|
|
41
|
+
const calcs = [];
|
|
42
|
+
const add = (o, key, label) => {
|
|
43
|
+
if (!o || typeof o !== "object" || o.fn == null || o.fn === "") return;
|
|
44
|
+
const name = o.name && ID.test(String(o.name)) ? String(o.name) : `_${key}`;
|
|
45
|
+
calcs.push({ key, label, name, fn: String(o.fn), ...(o.decimals != null ? { decimals: Number(o.decimals) } : {}) });
|
|
46
|
+
};
|
|
47
|
+
(s.steps || []).forEach((st, i) => add(st, `p${i}`, `passo ${i + 1}`));
|
|
48
|
+
add(s.answer, "resp", "resposta");
|
|
49
|
+
return { vars, calcs };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// calcula tudo na ordem; cada conta devolve { value, error } (erro em português, para o slide e para a IA)
|
|
53
|
+
function evaluate(m, values = {}) {
|
|
54
|
+
const env = Object.create(null);
|
|
55
|
+
for (const v of m.vars) env[v.name] = Number.isFinite(values[v.name]) ? values[v.name] : v.value;
|
|
56
|
+
const known = [...m.vars.map((v) => v.name), ...m.calcs.map((c) => c.name)];
|
|
57
|
+
const out = {};
|
|
58
|
+
for (const c of m.calcs) {
|
|
59
|
+
let value = null, error = "";
|
|
60
|
+
try {
|
|
61
|
+
const f = root.SagaFormula.compile(c.fn, known);
|
|
62
|
+
const missing = f.params.filter((p) => !(p in env));
|
|
63
|
+
if (missing.length) error = `a fórmula usa ${missing.map((p) => `"${p}"`).join(", ")}, que não está nos dados (símbolo e valor) nem nos passos anteriores (name)`;
|
|
64
|
+
else { value = f.eval(env); if (value == null) error = "a conta não deu um número (divisão por zero ou raiz de negativo?)"; }
|
|
65
|
+
} catch (e) { error = `fórmula "${c.fn}": ${e.message}`; }
|
|
66
|
+
if (value != null) env[c.name] = value;
|
|
67
|
+
out[c.key] = { value, error };
|
|
68
|
+
}
|
|
69
|
+
return { env, results: out };
|
|
70
|
+
}
|
|
71
|
+
// valor de {{expressão}}: nome, ou conta com os nomes já calculados
|
|
72
|
+
function evalExpr(expr, env) {
|
|
73
|
+
if (expr in env) return env[expr];
|
|
74
|
+
try { const f = root.SagaFormula.compile(expr, Object.keys(env)); return f.params.every((p) => p in env) ? f.eval(env) : null; } catch { return null; }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// número para a tela: casas pedidas, ou 4 algarismos (inteiro a partir de 1000); 10 elevado se for extremo.
|
|
78
|
+
// tex: "1\,430{,}77" · math: "1 430,77" (o mesmo, em texto, para trocar dentro da fórmula) · text: "1.430,77"
|
|
79
|
+
const SUP = { "-": "⁻", 0: "⁰", 1: "¹", 2: "²", 3: "³", 4: "⁴", 5: "⁵", 6: "⁶", 7: "⁷", 8: "⁸", 9: "⁹" };
|
|
80
|
+
function format(v, decimals, mode = "tex") {
|
|
81
|
+
if (v == null || !Number.isFinite(v)) return mode === "tex" ? "\\text{—}" : "—";
|
|
82
|
+
const comma = mode === "tex" ? "{,}" : ",", group = mode === "tex" ? "\\," : mode === "math" ? " " : ".";
|
|
83
|
+
const a = Math.abs(v);
|
|
84
|
+
if (v !== 0 && (a < 1e-3 || a >= 1e7) && !(decimals != null && a >= 1e7)) {
|
|
85
|
+
const [mant, e] = v.toExponential(decimals != null ? Math.min(decimals, 4) : 2).split("e");
|
|
86
|
+
const mm = String(Number(mant)).replace(".", comma);
|
|
87
|
+
return mode === "tex" ? `${mm}\\times10^{${Number(e)}}` : `${mm} × 10${String(Number(e)).split("").map((c) => SUP[c] ?? c).join("")}`;
|
|
88
|
+
}
|
|
89
|
+
const s = decimals != null ? v.toFixed(decimals) : a >= 1000 ? v.toFixed(0) : String(Number(v.toPrecision(4)));
|
|
90
|
+
const [int, frac] = s.replace(/^-0(\.0*)?$/, "0$1").split(".");
|
|
91
|
+
return `${int.replace(/\B(?=(\d{3})+(?!\d))/g, group)}${frac ? `${comma}${frac}` : ""}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// na apresentação: o dado vira controle; mexeu, recalcula e troca os números marcados (data-solv)
|
|
95
|
+
function mount(doc) {
|
|
96
|
+
doc.querySelectorAll("[data-solution]:not([data-solution-ready])").forEach((el) => {
|
|
97
|
+
el.dataset.solutionReady = "1";
|
|
98
|
+
const m = JSON.parse(el.querySelector(".sol-model").textContent);
|
|
99
|
+
const inputs = [...el.querySelectorAll("[data-solv-in]")];
|
|
100
|
+
// a cópia em MathML (para leitor de tela) não acompanha o número novo: quem lê passa a ser o próprio desenho
|
|
101
|
+
el.querySelectorAll(".katex").forEach((k) => {
|
|
102
|
+
if (!k.querySelector("[data-solv]")) return;
|
|
103
|
+
k.querySelector(".katex-mathml")?.setAttribute("hidden", "");
|
|
104
|
+
k.querySelector(".katex-html")?.removeAttribute("aria-hidden");
|
|
105
|
+
});
|
|
106
|
+
const redraw = () => {
|
|
107
|
+
const values = Object.fromEntries(inputs.map((i) => [i.dataset.solvIn, Number(i.value)]));
|
|
108
|
+
const { env } = evaluate(m, values);
|
|
109
|
+
el.querySelectorAll("[data-solv]").forEach((n) => {
|
|
110
|
+
const mk = m.marks[+n.dataset.solv];
|
|
111
|
+
if (mk) n.textContent = format(evalExpr(mk.expr, env), mk.decimals, n.dataset.solvMode === "text" ? "text" : "math");
|
|
112
|
+
});
|
|
113
|
+
const slide = el.closest(".slide");
|
|
114
|
+
if (slide && root.SagadeckFit?.fitSteps) root.requestAnimationFrame(() => root.SagadeckFit.fitSteps(el));
|
|
115
|
+
};
|
|
116
|
+
const stop = (e) => e.stopPropagation(); // setas e cliques no controle não trocam de slide nem de passo
|
|
117
|
+
inputs.forEach((i) => { ["keydown", "pointerdown", "click"].forEach((ev) => i.addEventListener(ev, stop)); i.addEventListener("input", redraw); });
|
|
118
|
+
const reset = el.querySelector("[data-solv-reset]");
|
|
119
|
+
if (reset) {
|
|
120
|
+
["keydown", "pointerdown", "click"].forEach((ev) => reset.addEventListener(ev, stop));
|
|
121
|
+
reset.addEventListener("click", () => { inputs.forEach((i) => { i.value = String(m.vars.find((v) => v.name === i.dataset.solvIn)?.value ?? i.value); }); redraw(); });
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
root.SagaSolution = { model, evaluate, evalExpr, format, varName, parseNum, mount };
|
|
127
|
+
if (typeof document !== "undefined") {
|
|
128
|
+
const go = () => mount(document);
|
|
129
|
+
document.readyState === "loading" ? document.addEventListener("DOMContentLoaded", go) : go();
|
|
130
|
+
}
|
|
131
|
+
})(typeof globalThis !== "undefined" ? globalThis : window);
|
|
@@ -8,6 +8,7 @@ import { loadSpec } from "../build.js";
|
|
|
8
8
|
import { writeRecording } from "../api-client.js";
|
|
9
9
|
import { editDeck, coverDocumentVisuals, checkNumbers } from "../ai/deck-ai.js";
|
|
10
10
|
import { reviewExperience } from "../ai/quality.js";
|
|
11
|
+
import { checkMath, hasMath, mathSignature, mathWarning } from "../ai/math-check.js";
|
|
11
12
|
import { maybeResearch } from "../research/chat-research.js";
|
|
12
13
|
import { mapDataPrompt, runMapData, mapDataInstruction } from "../ai/map-data.js";
|
|
13
14
|
import { chatErrorResult } from "./errors.js";
|
|
@@ -17,6 +18,8 @@ import { critiqueMaterials, critiqueMarkdown, saveCritique, loadCritique, critiq
|
|
|
17
18
|
import { styleAction } from "./style-routes.js";
|
|
18
19
|
import * as Project from "./project.js";
|
|
19
20
|
import { carryVisualEdits } from "./visual-keys.js";
|
|
21
|
+
import { conferenceItems, conferencePrompt, recordMath, loadGenReport } from "./conference.js";
|
|
22
|
+
import { saveGenerationRequest } from "./generation-request.js";
|
|
20
23
|
|
|
21
24
|
// Arquivos que a IA pode pedir para ver (ver: […]): imagens DENTRO da pasta da apresentação (os recortes e as
|
|
22
25
|
// páginas do material ficam em contexto/visuais/). Caminho que sai da pasta, que não existe ou que não é imagem fica
|
|
@@ -143,8 +146,9 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
143
146
|
const target = typeof body.targetSlide === "number" ? body.targetSlide : null;
|
|
144
147
|
const visuals = await lookAt(withBase(taskWorkspace, spec), target, prompt, emit);
|
|
145
148
|
// anexos: imagens vão como visão; documentos (id do /api/ai/context) vão como texto
|
|
146
|
-
|
|
149
|
+
const attached = takeMaterials(taskWorkspace, (Array.isArray(body.attachments) ? body.attachments : [])
|
|
147
150
|
.filter((a) => a && typeof a === "object" && a.type === "doc").map((a) => a.id));
|
|
151
|
+
let materials = attached;
|
|
148
152
|
if (!taskWorkspace.file && taskWorkspace.preview && materials.some(m => m.bytes)) imageOptionsFor();
|
|
149
153
|
const materialDir = taskWorkspace.file && !isBundledTemplate(taskWorkspace.file) ? path.dirname(taskWorkspace.file) : null;
|
|
150
154
|
if (materialDir) materials = await prepareDocumentMaterials(materials, materialDir, {signal,onProgress:emit});
|
|
@@ -166,6 +170,8 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
166
170
|
materials = chatResearch.materials;
|
|
167
171
|
if (chatResearch.report?.fontes?.length) linkActions.push(`Pesquisa na web: ${chatResearch.report.fontes.length} fonte(s) (${chatResearch.report.fontes.map((f) => f.site).join(", ")})`);
|
|
168
172
|
const firstInstruction = chatResearch.instruction ? `${chatResearch.instruction}\n${prompt}` : prompt;
|
|
173
|
+
// o que o painel Conferência ainda tem pendente (o ignorado não vai): a IA sabe o que falta sem reler avisos velhos
|
|
174
|
+
const pending = materialDir ? conferencePrompt(conferenceItems(loadGenReport(materialDir), spec, { dir: materialDir })) : "";
|
|
169
175
|
const editOpts = (extra = {}) => ({
|
|
170
176
|
spec: withBase(taskWorkspace, spec), signal,
|
|
171
177
|
instruction: firstInstruction,
|
|
@@ -185,6 +191,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
185
191
|
styles: taskWorkspace.file && !isBundledTemplate(taskWorkspace.file) ? { list: taskWorkspace.library.listStyles(), current: taskWorkspace.spec?.style?.name || null } : null,
|
|
186
192
|
// dados para o mapa (bloco mapa:): só com a pasta da apresentação, que é onde os arquivos ficam
|
|
187
193
|
mapData: P ? mapDataPrompt() : null,
|
|
194
|
+
pending,
|
|
188
195
|
...extra,
|
|
189
196
|
});
|
|
190
197
|
result = await editDeck(editOpts());
|
|
@@ -248,6 +255,8 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
248
255
|
signal.throwIfAborted();
|
|
249
256
|
if (checked.spec !== result.spec) { result.spec = checked.spec; result.actions = [...(result.actions || []), ...checked.actions]; }
|
|
250
257
|
if (checked.facts?.unsupported.length) result.actions = [...(result.actions || []), `Números sem base no material: ${checked.facts.unsupported.map((b) => `${b.number} (slide ${b.slide})`).join(", ")}`];
|
|
258
|
+
// criou a apresentação do documento pelo chat: o pedido fica gravado como na geração (.sagadeck/geracao.json)
|
|
259
|
+
if (materialDir) try { saveGenerationRequest(materialDir, { origem: "chat", pedido: { briefing: prompt }, materials: attached, spec: result.spec, research: chatResearch.report }); } catch { /* o registro não derruba a resposta */ }
|
|
251
260
|
}
|
|
252
261
|
}
|
|
253
262
|
if (linkActions.length) result.actions = [...linkActions, ...(result.actions || [])];
|
|
@@ -289,6 +298,40 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
289
298
|
convo.push({ role: "user", text: instruction });
|
|
290
299
|
result = { ...next, actions: [...result.actions, ...(next.actions || [])], spec: next.spec };
|
|
291
300
|
}
|
|
301
|
+
// Contas: a IA pediu a conferência (contas: [n] ou true) ou mexeu nos números de um slide com conta. Ela escreve
|
|
302
|
+
// cada resultado como fórmula, o código calcula (src/ai/math-check.js) e o que não bate volta para ela corrigir.
|
|
303
|
+
if (!result.talk && !result.transform && !result.style && !result.variants && result.spec?.slides) {
|
|
304
|
+
const all = result.spec.slides.map((_, i) => i);
|
|
305
|
+
// contas pedidas pelo Studio (Corrigir com IA no painel Conferência): conferidas depois da resposta, sempre
|
|
306
|
+
const fromPanel = [].concat(body.contas || []).map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= all.length).map((n) => n - 1);
|
|
307
|
+
const asked = result.contas === "all" ? all : [...new Set([...(result.contas || []), ...fromPanel])];
|
|
308
|
+
const before = new Map((spec.slides || []).map((s) => [s?.uid, s]));
|
|
309
|
+
const touched = (result.changed || []).filter((i) => { const s = result.spec.slides[i]; const old = s?.uid ? before.get(s.uid) : spec.slides?.[i]; return hasMath(s) && mathSignature(s) !== mathSignature(old || {}); });
|
|
310
|
+
const only = [...new Set([...asked, ...touched])];
|
|
311
|
+
if (only.length) {
|
|
312
|
+
emit({ phase: "step", text: "Conferindo as contas…" });
|
|
313
|
+
const counted = await checkMath(result.spec, { materials, only, signal, say: (t) => emit({ phase: "step", text: t }),
|
|
314
|
+
// a conversa vai junto: o que a pessoa pediu com todas as letras ("pode corrigir o material") vale
|
|
315
|
+
edit: (o) => editDeck({ signal, images: false, onProgress: emit, materials, drawCheck: diagramCheck, history: [...history, { role: "user", text: prompt }], ...o, spec: withBase(taskWorkspace, o.spec) }) });
|
|
316
|
+
signal.throwIfAborted();
|
|
317
|
+
if (counted.spec !== result.spec) {
|
|
318
|
+
const fixedChanged = counted.spec.slides.map((s, i) => (JSON.stringify(s) !== JSON.stringify(result.spec.slides[i]) ? i : -1)).filter((i) => i >= 0);
|
|
319
|
+
result = { ...result, spec: counted.spec, changed: [...new Set([...(result.changed || []), ...fixedChanged])].sort((a, b) => a - b) };
|
|
320
|
+
}
|
|
321
|
+
const m = counted.math;
|
|
322
|
+
result.math = m;
|
|
323
|
+
// o painel Conferência passa a mostrar o resultado desta conferência para esses slides
|
|
324
|
+
if (deckDir) try { recordMath(deckDir, only, m); } catch (e) { console.error("[Studio] conferência:", e.message); }
|
|
325
|
+
result.actions = [...(result.actions || []), ...counted.actions,
|
|
326
|
+
m?.failed ? `Conferência das contas falhou: ${m.failed}`
|
|
327
|
+
: m ? `Contas conferidas: ${m.checked + (m.fixed?.length || 0)} ok${m.fixed?.length ? ` (${m.fixed.length} corrigida(s): ${m.fixed.map((d) => `slide ${d.slide}, ${d.afirmado} ${d.unidade}`.trim()).join("; ")})` : ""}${m.unchecked?.length ? `, ${m.unchecked.length} sem como calcular` : ""}` : "Nenhuma conta para conferir nesses slides",
|
|
328
|
+
...(mathWarning(m) ? [mathWarning(m)] : [])];
|
|
329
|
+
if (asked.length && result.reply === "Vou conferir as contas.") result.reply = m?.divergent?.length ? "Conferi as contas; algumas ainda não batem (veja abaixo)." : m?.fixed?.length ? "Conferi as contas e corrigi o que não batia." : "Conferi as contas: estão certas.";
|
|
330
|
+
if (m?.reply) result.reply = `${result.reply}
|
|
331
|
+
|
|
332
|
+
${m.reply}`;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
292
335
|
result.mode = "llm";
|
|
293
336
|
} catch (e) {
|
|
294
337
|
if (signal.aborted) return { reply: "Parado a pedido. Nenhuma resposta pendente foi aplicada.", spec, actions: [], talk: true, mode: "cancelled" };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Rotas do painel Revisar › Conferência (src/studio/conference.js): a lista, a decisão por item e o Conferir de novo.
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { storedDocumentMaterials } from "../ai/document-materials.js";
|
|
4
|
+
import { llmAvailable } from "../ai/llm.js";
|
|
5
|
+
import * as Project from "./project.js";
|
|
6
|
+
import { conferenceItems, decideItem, recheckDeck, loadGenReport } from "./conference.js";
|
|
7
|
+
|
|
8
|
+
const send = (res, data, status = 200) => { res.writeHead(status, { "Content-Type": "application/json" }); res.end(JSON.stringify(data)); };
|
|
9
|
+
|
|
10
|
+
// o material da apresentação: o documento guardado na criação ou no chat; sem ele, os arquivos de contexto/
|
|
11
|
+
export async function deckMaterials(file) {
|
|
12
|
+
const dir = path.dirname(file);
|
|
13
|
+
let materials = storedDocumentMaterials(dir);
|
|
14
|
+
if (!materials.length) { try { materials = await Project.contextMaterials(Project.projectOf(file)); } catch { materials = []; } }
|
|
15
|
+
return materials;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function conferenceRoutes({ req, res, pathname, W, readJSON, isBundledTemplate }) {
|
|
19
|
+
if (!pathname.startsWith("/api/conferencia")) return false;
|
|
20
|
+
const dir = W.file && !isBundledTemplate(W.file) ? path.dirname(W.file) : null;
|
|
21
|
+
const list = (report = loadGenReport(dir)) => ({ items: dir ? conferenceItems(report, W.spec, { dir }) : [], conferidoEm: report?.conferidoEm || null, salvo: !!dir });
|
|
22
|
+
if (pathname === "/api/conferencia" && req.method === "GET") { send(res, list()); return true; }
|
|
23
|
+
if (pathname === "/api/conferencia/item" && req.method === "POST") {
|
|
24
|
+
const body = await readJSON(req);
|
|
25
|
+
try { decideItem(dir, String(body.id || ""), String(body.status || "")); send(res, { ok: true, ...list() }); }
|
|
26
|
+
catch (e) { send(res, { error: e.message }, 400); }
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (pathname === "/api/conferencia/conferir" && req.method === "POST") {
|
|
30
|
+
if (!dir) { send(res, { error: "A apresentação precisa estar salva na biblioteca." }, 400); return true; }
|
|
31
|
+
const notes = [];
|
|
32
|
+
try {
|
|
33
|
+
const materials = await deckMaterials(W.file);
|
|
34
|
+
const ai = await llmAvailable({ force: true });
|
|
35
|
+
// sem IA: números e figuras se conferem do mesmo jeito; as contas precisam do modelo para escrever as fórmulas
|
|
36
|
+
const r = await recheckDeck(dir, W.spec, materials, ai ? {} : { complete: async () => { throw new Error("a IA está desligada"); } });
|
|
37
|
+
notes.push(...r.notes);
|
|
38
|
+
if (r.unchecked) notes.push(`${r.unchecked} conta(s) sem como calcular.`);
|
|
39
|
+
send(res, { ok: true, notes, ...list(r.report) });
|
|
40
|
+
} catch (e) { send(res, { error: e.message }, 500); }
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Painel Revisar › Conferência: junta num lugar o que as checagens automáticas acharam no deck aberto (contas que não
|
|
2
|
+
// batem, contas do material que não fecham, números sem base no material, avisos da revisão visual, figuras do
|
|
3
|
+
// material sem slide, slides que a IA calculou ou incluiu) e o que a pessoa decidiu sobre cada um.
|
|
4
|
+
// Antes os achados ficavam espalhados (relatório da criação, mensagem do chat, selo na miniatura) e a professora
|
|
5
|
+
// entregou a aula com a conta errada: o aviso existia, mas não tinha lugar nem botão.
|
|
6
|
+
// A decisão (ignorado: "está certo assim"; enviado: o pedido de correção já foi ao chat) fica no relatório
|
|
7
|
+
// (.sagadeck/avaliacao-geracao.json, campo `decisoes`): o item some do painel e dos próximos pedidos à IA.
|
|
8
|
+
import { locate, checkMath } from "../ai/math-check.js";
|
|
9
|
+
import { unsupportedNumbers, uncoveredVisuals } from "../ai/document-visuals.js";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import YAML from "yaml";
|
|
13
|
+
import { loadGenReport, writeGenReport, slideMark, slideMarks } from "./generation-report.js";
|
|
14
|
+
|
|
15
|
+
// Como os slides eram quando os avisos da revisão visual nasceram: as marcas do relatório; relatório antigo, sem
|
|
16
|
+
// marcas, usa a versão mais antiga guardada do deck (a primeira gravação depois da criação).
|
|
17
|
+
export function reportBaseline(report, dir) {
|
|
18
|
+
if (report?.slideMarks) return report.slideMarks;
|
|
19
|
+
if (!dir) return null;
|
|
20
|
+
try {
|
|
21
|
+
const vdir = path.join(dir, ".sagadeck", "versoes");
|
|
22
|
+
const first = fs.readdirSync(vdir).filter((f) => /\.ya?ml$/i.test(f)).sort()[0];
|
|
23
|
+
return first ? slideMarks(YAML.parse(fs.readFileSync(path.join(vdir, first), "utf8"))) : null;
|
|
24
|
+
} catch { return null; }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const CONF_KINDS = {
|
|
28
|
+
conta: "Conta que não bate",
|
|
29
|
+
material: "Conta do material que não fecha",
|
|
30
|
+
numero: "Número sem base no material",
|
|
31
|
+
visual: "Aviso da revisão visual",
|
|
32
|
+
figura: "Figura do material sem slide",
|
|
33
|
+
derivado: "Calculado pela IA",
|
|
34
|
+
proprio: "Incluído pela IA",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// id estável para o mesmo achado (o painel e o relatório falam dele pelo id)
|
|
38
|
+
const hash = (s) => { let h = 5381; for (const c of String(s)) h = ((h << 5) + h + c.codePointAt(0)) >>> 0; return h.toString(36); };
|
|
39
|
+
const fmt = (x) => (typeof x === "number" ? Number(x.toPrecision(4)).toLocaleString("pt-BR", { maximumFractionDigits: 6 }) : String(x ?? ""));
|
|
40
|
+
const unit = (d) => (d.unidade ? ` ${d.unidade}` : "");
|
|
41
|
+
|
|
42
|
+
// Os itens pendentes: relatório + origem dos slides do deck atual, menos o que a pessoa já decidiu. Achado que o
|
|
43
|
+
// deck já não tem (o número saiu do slide, o slide não existe mais) não aparece: foi resolvido.
|
|
44
|
+
export function conferenceItems(report, spec, { dir = null } = {}) {
|
|
45
|
+
const slides = spec?.slides || [];
|
|
46
|
+
const baseline = report?.quality?.issues?.length ? reportBaseline(report, dir) : null;
|
|
47
|
+
// o slide mudou depois do aviso (a IA ou a pessoa já mexeu nele): o aviso pode estar resolvido
|
|
48
|
+
const changed = (n) => { if (!baseline) return false; const s = slides[n - 1], was = baseline[s?.uid || `#${n}`]; return was != null && was !== slideMark(s); };
|
|
49
|
+
const decided = report?.decisoes || {};
|
|
50
|
+
const out = [];
|
|
51
|
+
const still = (n, number) => slides[n - 1] && locate(slides[n - 1], number).length > 0;
|
|
52
|
+
for (const d of report?.math?.divergent || []) {
|
|
53
|
+
if (!still(d.slide, d.afirmado)) continue;
|
|
54
|
+
out.push({ id: `conta:${d.slide}:${d.afirmado}`, kind: "conta", slide: d.slide, contas: [d.slide],
|
|
55
|
+
text: `${d.oQue ? `${d.oQue}: ` : ""}o slide diz ${d.afirmado}${unit(d)}; refazendo a conta, dá ${fmt(d.calculado)}${unit(d)}.`,
|
|
56
|
+
fix: `Confira a conta do slide ${d.slide}${d.oQue ? ` (${d.oQue})` : ""}: o slide diz ${d.afirmado}${unit(d)}; refazendo a partir dos dados, dá ${fmt(d.calculado)}${unit(d)}${d.fn ? ` [${d.fn} com ${JSON.stringify(d.vars || {})}]` : ""}. Se o slide estiver errado, corrija o resultado em todos os campos onde ele aparece e o que depende dele (passos, resposta, notes, gráficos e textos de outros slides); se o slide estiver certo, diga por quê.` });
|
|
57
|
+
}
|
|
58
|
+
for (const d of report?.math?.material || []) {
|
|
59
|
+
if (!still(d.slide, d.afirmado)) continue;
|
|
60
|
+
out.push({ id: `material:${d.slide}:${d.afirmado}`, kind: "material", slide: d.slide, contas: [d.slide],
|
|
61
|
+
text: `${d.oQue ? `${d.oQue}: ` : ""}o material traz ${d.afirmado}${unit(d)}, mas a conta com os dados dele dá ${fmt(d.calculado)}${unit(d)}. Confira com quem escreveu.`,
|
|
62
|
+
fix: `No slide ${d.slide}, o número ${d.afirmado}${unit(d)} está no material, mas não fecha com os dados dele: refazendo, dá ${fmt(d.calculado)}${unit(d)}. Siga a autoria do deck: em \`autoria: autor\` (ou sem autoria definida), mantenha o número do material e explique a divergência nas notes ("Confira: o material traz X; refazendo a conta, dá Y"); em \`autoria: livre\`, corrija no slide e diga nas notes que o material traz outro valor.` });
|
|
63
|
+
}
|
|
64
|
+
for (const b of report?.facts?.unsupported || []) {
|
|
65
|
+
if (!still(b.slide, b.number)) continue;
|
|
66
|
+
out.push({ id: `numero:${b.slide}:${b.number}`, kind: "numero", slide: b.slide,
|
|
67
|
+
text: `${b.number} aparece no slide, mas não está escrito no material.`,
|
|
68
|
+
fix: `O número ${b.number} do slide ${b.slide} não está no material anexado. Se for valor do material lido errado (de um gráfico, por exemplo), troque pelo que o texto ou a tabela do material traz; se for conta sua a partir do material, deixe só com a conta no slide ou nas notes; se não houver base, tire o número (reescreva a frase sem ele). Não mexa em mais nada.` });
|
|
69
|
+
}
|
|
70
|
+
for (const i of report?.quality?.issues || []) {
|
|
71
|
+
if (!slides[i.slide - 1]) continue;
|
|
72
|
+
const text = String(i.text || "").trim(), stale = changed(i.slide);
|
|
73
|
+
out.push({ id: `visual:${i.slide}:${hash(text)}`, kind: "visual", slide: i.slide, ...(stale ? { stale: true } : {}),
|
|
74
|
+
text: stale ? `O slide mudou depois deste aviso; confira se ainda vale: ${text}` : text,
|
|
75
|
+
fix: `${stale ? "O slide mudou depois deste aviso da revisão visual; olhe o slide e, se o problema ainda existir, corrija" : "Corrija"} no slide ${i.slide} este aviso: ${text}` });
|
|
76
|
+
}
|
|
77
|
+
for (const caption of report?.coverage?.missing || []) {
|
|
78
|
+
out.push({ id: `figura:${hash(caption)}`, kind: "figura", slide: null, text: String(caption),
|
|
79
|
+
fix: `Esta figura/tabela do material não aparece em nenhum slide: ${caption}. Encaixe-a no ponto da narrativa em que o assunto dela é discutido (no slide que já fala dele ou num slide novo logo ao lado), com a figura dominante e 1–3 frases de leitura do que ela mostra. Não mexa no resto do deck.` });
|
|
80
|
+
}
|
|
81
|
+
slides.forEach((s, k) => {
|
|
82
|
+
const n = k + 1, note = s?.provenanceNote ? String(s.provenanceNote) : "";
|
|
83
|
+
if (s?.provenance === "derivado") out.push({ id: `derivado:${s.uid || n}`, kind: "derivado", slide: n, contas: [n],
|
|
84
|
+
text: note || "Feito pela IA a partir dos números do material: confira as contas.",
|
|
85
|
+
fix: `O slide ${n} foi feito pela IA a partir dos números do material${note ? ` (${note})` : ""}. Refaça as contas dele a partir dos dados e corrija o que não bater, em todos os campos onde o número aparece.` });
|
|
86
|
+
else if (s?.provenance === "proprio") out.push({ id: `proprio:${s.uid || n}`, kind: "proprio", slide: n,
|
|
87
|
+
text: note || "Incluído pela IA: não está no material.",
|
|
88
|
+
fix: `O slide ${n} foi incluído pela IA e não está no material${note ? ` (${note})` : ""}. Confira se o que ele afirma está correto e coerente com o material; corrija o que não estiver e deixe claro na provenanceNote de onde vem.` });
|
|
89
|
+
});
|
|
90
|
+
return out.filter((it) => !decided[it.id]).map((it) => ({ ...it, label: CONF_KINDS[it.kind] }));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// As pendências que vão junto do pedido ao chat (editDeck: pending). O que foi ignorado não vai: a IA não volta a
|
|
94
|
+
// insistir nisso, nem quando a pessoa pede "corrija os avisos da criação" com a mensagem antiga na conversa.
|
|
95
|
+
export function conferencePrompt(items) {
|
|
96
|
+
// aviso de slide que já mudou não vai: a IA refazia a correção de algo que talvez já tenha corrigido
|
|
97
|
+
const list = (items || []).filter((it) => it.kind !== "derivado" && it.kind !== "proprio" && !it.stale);
|
|
98
|
+
if (!list.length) return "";
|
|
99
|
+
return `PENDÊNCIAS DA CONFERÊNCIA AUTOMÁTICA (painel Revisar › Conferência): o que as checagens acharam e a pessoa ainda não resolveu nem ignorou. Avisos antigos da conversa que não estão nesta lista foram resolvidos ou ignorados pela pessoa: não volte a eles. Mexa nestes só se o pedido tiver a ver.\n${list.slice(0, 40).map((it) => `- ${it.slide ? `slide ${it.slide}` : "deck"} · ${it.label}: ${it.text}`).join("\n")}`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A pessoa decidiu sobre um item: ignorado ("está certo assim") ou enviado (o pedido de correção foi ao chat)
|
|
103
|
+
export function decideItem(dir, id, status) {
|
|
104
|
+
if (!dir) throw new Error("A apresentação precisa estar salva na biblioteca.");
|
|
105
|
+
if (!["ignorado", "enviado"].includes(status)) throw new Error("status: ignorado ou enviado");
|
|
106
|
+
if (!/^[a-z]+:[^\s]{1,200}$/.test(String(id || ""))) throw new Error("item da conferência inválido");
|
|
107
|
+
const report = loadGenReport(dir) || { createdAt: new Date().toISOString(), avisado: true };
|
|
108
|
+
report.decisoes = { ...(report.decisoes || {}), [id]: { status, em: new Date().toISOString() } };
|
|
109
|
+
writeGenReport(dir, report);
|
|
110
|
+
return report.decisoes[id];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// O chat conferiu as contas de alguns slides (pedido da pessoa, do painel ou edição que mexeu nos números): o resultado
|
|
114
|
+
// substitui o que o relatório dizia desses slides. only: índices 0-based.
|
|
115
|
+
export function recordMath(dir, only, math) {
|
|
116
|
+
if (!dir || !math || math.failed || !only?.length) return;
|
|
117
|
+
const report = loadGenReport(dir);
|
|
118
|
+
if (!report && !math.divergent?.length && !math.material?.length) return;
|
|
119
|
+
const nums = new Set(only.map((i) => i + 1));
|
|
120
|
+
const keep = (list) => (list || []).filter((d) => !nums.has(d.slide));
|
|
121
|
+
const prev = report?.math || {};
|
|
122
|
+
writeGenReport(dir, { ...(report || { createdAt: new Date().toISOString(), avisado: true }),
|
|
123
|
+
math: { ...prev, divergent: [...keep(prev.divergent), ...(math.divergent || [])], material: [...keep(prev.material), ...(math.material || [])] } });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Conferir de novo (botão do painel): refaz no deck atual a conferência das contas (só confere, não corrige: a
|
|
127
|
+
// correção é pelo item), a checagem de números e a cobertura das figuras do material. A revisão visual não se refaz
|
|
128
|
+
// aqui (precisa fotografar os slides); os avisos dela ficam até a pessoa corrigir ou ignorar.
|
|
129
|
+
export async function recheckDeck(dir, spec, materials = [], { complete, signal } = {}) {
|
|
130
|
+
const report = loadGenReport(dir) || { createdAt: new Date().toISOString(), avisado: true };
|
|
131
|
+
const notes = [];
|
|
132
|
+
const sources = (materials || []).filter((m) => m && m.kind !== "critica" && m.kind !== "pesquisa");
|
|
133
|
+
if (sources.some((m) => String(m.text || "").trim())) report.facts = { checked: 0, unsupported: unsupportedNumbers(spec, sources) };
|
|
134
|
+
if (sources.some((m) => m.inventory?.items?.length)) report.coverage = { missing: uncoveredVisuals(spec, sources).map((m) => m.caption) };
|
|
135
|
+
const r = await checkMath(spec, { materials: sources, ...(complete ? { complete } : {}), signal });
|
|
136
|
+
if (r.math?.failed) notes.push(`A conferência das contas falhou (${r.math.failed}); o resto foi conferido.`);
|
|
137
|
+
else report.math = r.math ? { checked: r.math.checked, fixed: [], unchecked: r.math.unchecked, divergent: r.math.divergent, material: r.math.material } : { checked: 0, fixed: [], unchecked: [], divergent: [], material: [] };
|
|
138
|
+
if (!sources.length) notes.push("Sem material anexado: a checagem de números e de figuras precisa do documento.");
|
|
139
|
+
report.conferidoEm = new Date().toISOString();
|
|
140
|
+
writeGenReport(dir, report);
|
|
141
|
+
return { report, notes, unchecked: r.math?.unchecked?.length || 0 };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { loadGenReport };
|