sagadeck 1.9.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/SKILL.md +1 -1
- package/bin/sagadeck.js +1 -1
- package/docs/ANDAMENTO.md +302 -0
- package/docs/REFERENCIA.md +72 -4
- package/package.json +4 -4
- package/src/ai/context.js +7 -2
- package/src/ai/critique.js +8 -1
- package/src/ai/deck-ai.js +36 -12
- package/src/ai/document-visuals.js +6 -2
- package/src/ai/ia-check.js +13 -4
- package/src/ai/ia-config.js +4 -2
- package/src/ai/llm.js +12 -7
- package/src/ai/math-check.js +318 -0
- package/src/ai/quality.js +3 -3
- package/src/ai/units.js +85 -0
- package/src/api-client.js +42 -10
- package/src/build.js +1 -0
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/fs-retry.js +2 -2
- package/src/lessons.js +72 -11
- package/src/mcp/server.js +1 -1
- package/src/package.js +4 -0
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/base.css +3 -0
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/runtime/solution.js +131 -0
- package/src/studio/ai-chat-route.js +69 -7
- package/src/studio/conference-routes.js +44 -0
- package/src/studio/conference.js +144 -0
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/generation-report.js +61 -1
- package/src/studio/generation-request.js +80 -0
- package/src/studio/ia-routes.js +1 -1
- package/src/studio/layout-samples.js +1 -1
- package/src/studio/library-routes.js +7 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/ai-settings.js +4 -4
- package/src/studio/public/app.js +14 -2
- package/src/studio/public/chat.js +11 -3
- package/src/studio/public/conference-ui.js +91 -0
- package/src/studio/public/developer-authoring.css +12 -0
- package/src/studio/public/developer-authoring.js +306 -0
- package/src/studio/public/generation-guide.js +42 -0
- package/src/studio/public/index.html +21 -0
- package/src/studio/public/library.css +36 -0
- package/src/studio/public/library.html +4 -1
- package/src/studio/public/library.js +118 -28
- package/src/studio/public/slide-form.js +17 -17
- package/src/studio/public/style.css +25 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +20 -8
- package/src/themes.js +48 -0
package/src/code-highlight.js
CHANGED
|
@@ -6,6 +6,9 @@ const KEYWORDS = {
|
|
|
6
6
|
JavaScript: new Set("async await break case catch class const continue debugger default delete do else export extends finally for from function if import in instanceof let new of return static super switch this throw try typeof var void while with yield".split(" ")),
|
|
7
7
|
TypeScript: new Set("abstract any as asserts async await bigint boolean break case catch class const constructor continue declare default delete do else enum export extends finally for from function get if implements import in infer instanceof interface is keyof let namespace never new number object of package private protected public readonly return set static string super switch symbol this throw try type typeof undefined unique unknown var void while with yield".split(" ")),
|
|
8
8
|
"C#": new Set("abstract as async await base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw try typeof uint ulong unchecked unsafe ushort using virtual void volatile while".split(" ")),
|
|
9
|
+
Bash: new Set("if then else elif fi for in do done while until case esac function select time export local readonly return exit source".split(" ")),
|
|
10
|
+
JSON: new Set(),
|
|
11
|
+
SQL: new Set("SELECT FROM WHERE JOIN INNER LEFT RIGHT FULL OUTER CROSS ON AS AND OR NOT IN IS NULL DISTINCT GROUP BY ORDER HAVING LIMIT OFFSET ASC DESC INSERT INTO VALUES UPDATE SET DELETE CREATE TABLE INDEX VIEW DROP ALTER WITH UNION ALL EXISTS CASE WHEN THEN ELSE END BETWEEN LIKE RETURNING BEGIN COMMIT ROLLBACK PRIMARY KEY REFERENCES DEFAULT".split(" ")),
|
|
9
12
|
};
|
|
10
13
|
|
|
11
14
|
const TYPES = {
|
|
@@ -14,11 +17,19 @@ const TYPES = {
|
|
|
14
17
|
JavaScript: new Set("Array Boolean Date Map Number Object Promise Set String".split(" ")),
|
|
15
18
|
TypeScript: new Set("Array any bigint boolean never number object string symbol unknown void".split(" ")),
|
|
16
19
|
"C#": new Set("bool byte char DateTime decimal double float int List long object short string Task uint ulong var void".split(" ")),
|
|
20
|
+
Bash: new Set(),
|
|
21
|
+
JSON: new Set(),
|
|
22
|
+
SQL: new Set("INT INTEGER BIGINT TEXT VARCHAR BOOLEAN FLOAT DOUBLE DECIMAL NUMERIC DATE TIMESTAMP JSON JSONB".split(" ")),
|
|
17
23
|
};
|
|
18
24
|
|
|
25
|
+
const SHELL_COMMANDS = new Set("curl echo printf git npm node python python3 pip pytest jq cat grep sed awk ls cd pwd mkdir rm cp mv sleep read test find sort head tail".split(" "));
|
|
26
|
+
|
|
19
27
|
const CONSTANTS = new Set(["False", "None", "True", "false", "null", "true", "undefined", "NaN", "Infinity"]);
|
|
20
28
|
|
|
21
29
|
function tokenPattern(language) {
|
|
30
|
+
if (language === "Bash") return /(?<comment>(?<!\S)#[^\n]*)|(?<string>'[^']*'|"(?:\\[\s\S]|[^"\\])*")|(?<variable>\$(?:\{[^}\n]+\}|[A-Za-z_][\w]*|[\d?@#*!$-]))|(?<flag>(?<!\S)--?[A-Za-z][\w-]*(?:=[^\s]+)?)|(?<number>\b\d+(?:\.\d+)?\b)|(?<identifier>[A-Za-z_][\w]*)|(?<operator>[|&;<>]+)|(?<punctuation>[{}()\[\]])/g;
|
|
31
|
+
if (language === "JSON") return /(?<comment>\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(?<string>"(?:\\.|[^"\\])*")|(?<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(?<identifier>true|false|null)|(?<punctuation>[{}\[\],:])/g;
|
|
32
|
+
if (language === "SQL") return /(?<comment>--[^\n]*|\/\*[\s\S]*?\*\/)|(?<string>'(?:''|[^'])*'|"(?:""|[^"])*"|`[^`]*`)|(?<number>\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)|(?<identifier>[A-Za-z_][\w$]*)|(?<operator>[-+*/%=&|^!<>~]+)|(?<punctuation>[{}()\[\].,;])/g;
|
|
22
33
|
const comments = language === "Python"
|
|
23
34
|
? "#[^\\n]*"
|
|
24
35
|
: "\\/\\/[^\\n]*|\\/\\*[\\s\\S]*?\\*\\/";
|
|
@@ -32,13 +43,18 @@ function tokenPattern(language) {
|
|
|
32
43
|
function classify(match, language, source, offset) {
|
|
33
44
|
const groups = match.groups;
|
|
34
45
|
if (groups.comment) return "comment";
|
|
46
|
+
if (groups.variable) return "constant";
|
|
47
|
+
if (groups.flag) return "keyword";
|
|
48
|
+
if (groups.string && language === "JSON" && /^\s*:/.test(source.slice(offset + match[0].length))) return "type";
|
|
35
49
|
if (groups.string) return "string";
|
|
36
50
|
if (groups.number) return "number";
|
|
37
51
|
if (groups.identifier) {
|
|
38
52
|
const word = groups.identifier;
|
|
39
|
-
|
|
53
|
+
const normalized = language === "SQL" ? word.toUpperCase() : word;
|
|
54
|
+
if (KEYWORDS[language].has(normalized)) return "keyword";
|
|
40
55
|
if (CONSTANTS.has(word)) return "constant";
|
|
41
|
-
if (TYPES[language].has(
|
|
56
|
+
if (TYPES[language].has(normalized)) return "type";
|
|
57
|
+
if (language === "Bash" && SHELL_COMMANDS.has(word)) return "function";
|
|
42
58
|
if (/^\s*\(/.test(source.slice(offset + match[0].length))) return "function";
|
|
43
59
|
}
|
|
44
60
|
if (groups.operator) return "operator";
|
|
@@ -47,6 +63,7 @@ function classify(match, language, source, offset) {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
export function highlightCode(source, language) {
|
|
66
|
+
if (!KEYWORDS[language]) return source.split("\n").map(esc);
|
|
50
67
|
const pattern = tokenPattern(language);
|
|
51
68
|
const lines = [""];
|
|
52
69
|
const append = (value, kind = "") => {
|
package/src/code-language.js
CHANGED
|
@@ -11,6 +11,11 @@ const EXTENSIONS = new Map([
|
|
|
11
11
|
[".mts", "TypeScript"],
|
|
12
12
|
[".cts", "TypeScript"],
|
|
13
13
|
[".cs", "C#"],
|
|
14
|
+
[".sh", "Bash"],
|
|
15
|
+
[".bash", "Bash"],
|
|
16
|
+
[".json", "JSON"],
|
|
17
|
+
[".jsonc", "JSON"],
|
|
18
|
+
[".sql", "SQL"],
|
|
14
19
|
]);
|
|
15
20
|
|
|
16
21
|
const ALIASES = new Map([
|
|
@@ -26,6 +31,13 @@ const ALIASES = new Map([
|
|
|
26
31
|
["c#", "C#"],
|
|
27
32
|
["csharp", "C#"],
|
|
28
33
|
["cs", "C#"],
|
|
34
|
+
["bash", "Bash"],
|
|
35
|
+
["shell", "Bash"],
|
|
36
|
+
["sh", "Bash"],
|
|
37
|
+
["curl", "Bash"],
|
|
38
|
+
["json", "JSON"],
|
|
39
|
+
["jsonc", "JSON"],
|
|
40
|
+
["sql", "SQL"],
|
|
29
41
|
]);
|
|
30
42
|
|
|
31
43
|
export function inferCodeLanguage(filename) {
|
|
@@ -48,5 +60,6 @@ export function resolveCodeLanguage(language, filename) {
|
|
|
48
60
|
|
|
49
61
|
export function displayCodeLanguage(language, filename) {
|
|
50
62
|
const requested = String(language || "").trim();
|
|
63
|
+
if (requested.toLowerCase() === "curl") return "cURL";
|
|
51
64
|
return normalizeCodeLanguage(requested) || requested || inferCodeLanguage(filename);
|
|
52
65
|
}
|
package/src/fs-retry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Renomear tolerante a arquivo travado. A biblioteca mora em Documentos, que no Windows costuma estar no OneDrive:
|
|
2
2
|
// enquanto sincroniza (e também o antivírus ou o indexador), o arquivo fica preso por um instante e o rename falha
|
|
3
3
|
// com EPERM/EBUSY/EACCES. Em vez de perder o salvamento da pessoa, tenta de novo com espera curta e crescente
|
|
4
|
-
// (~
|
|
4
|
+
// (~4 s no total: a CI do Windows já segurou o arquivo por mais de 1 s). Qualquer outro erro (destino inexistente, disco cheio…) sobe na hora.
|
|
5
5
|
//
|
|
6
6
|
// Copiar e remover pasta NÃO usam fs.cpSync/fs.rmSync recursivos: o código nativo deles (std::filesystem) no
|
|
7
7
|
// Windows quebra o processo inteiro (0xC0000409, sem exceção para pegar) ou "faz nada" quando o caminho tem
|
|
@@ -12,7 +12,7 @@ import fs from "node:fs";
|
|
|
12
12
|
import path from "node:path";
|
|
13
13
|
|
|
14
14
|
const LOCKED = new Set(["EPERM", "EBUSY", "EACCES"]);
|
|
15
|
-
export const RENAME_DELAYS = [20, 40, 80, 150, 250, 450];
|
|
15
|
+
export const RENAME_DELAYS = [20, 40, 80, 150, 250, 450, 700, 1000, 1300];
|
|
16
16
|
|
|
17
17
|
const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
18
18
|
|
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/api-core.js
CHANGED
|
@@ -35,11 +35,45 @@
|
|
|
35
35
|
|
|
36
36
|
// {{nome}} em qualquer texto do pedido (URL, cabeçalhos, corpo). Sem valor: fica como está.
|
|
37
37
|
const VAR = /\{\{\s*([\w.-]+)\s*\}\}/g;
|
|
38
|
+
function renderBody(body, vars, encoding) {
|
|
39
|
+
if (typeof body !== "string" || !["json", "urlencoded"].includes(encoding)) return render(body, vars);
|
|
40
|
+
if (encoding === "urlencoded") return body.replace(VAR, (all, key) => vars && vars[key] != null ? encodeURIComponent(String(vars[key])) : all);
|
|
41
|
+
// Interpola no texto original, sem converter números ou reformatar o JSON. Dentro de uma
|
|
42
|
+
// string escapa só o valor; fora dela, insere um literal JSON. Marcador sem valor continua.
|
|
43
|
+
let result = "", quoted = false;
|
|
44
|
+
for (let at = 0; at < body.length; at++) {
|
|
45
|
+
if (body.startsWith("{{", at)) {
|
|
46
|
+
const marker = body.slice(at).match(/^\{\{\s*([\w.-]+)\s*\}\}/);
|
|
47
|
+
if (marker && vars && vars[marker[1]] != null) {
|
|
48
|
+
const value = vars[marker[1]];
|
|
49
|
+
result += quoted ? JSON.stringify(String(value)).slice(1, -1) : JSON.stringify(value);
|
|
50
|
+
at += marker[0].length - 1; continue;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const char = body[at]; result += char;
|
|
54
|
+
if (quoted && char === "\\" && at + 1 < body.length) { result += body[++at]; continue; }
|
|
55
|
+
if (char === '"') quoted = !quoted;
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function renderURL(url, vars, encoding) {
|
|
60
|
+
if (encoding !== "query") return render(url, vars);
|
|
61
|
+
const text = String(url), query = text.indexOf("?"), fragment = text.indexOf("#");
|
|
62
|
+
return text.replace(VAR, (all, key, at) => {
|
|
63
|
+
if (!vars || vars[key] == null) return all;
|
|
64
|
+
const value = String(vars[key]);
|
|
65
|
+
return query >= 0 && at > query && (fragment < 0 || at < fragment) ? encodeURIComponent(value) : value;
|
|
66
|
+
});
|
|
67
|
+
}
|
|
38
68
|
function render(value, vars) {
|
|
39
69
|
if (typeof value === "string") return value.replace(VAR, (all, k) => (vars && vars[k] != null ? String(vars[k]) : all));
|
|
40
70
|
if (Array.isArray(value)) return value.map((x) => render(x, vars));
|
|
41
71
|
if (value && typeof value === "object" && value.__py) return value;
|
|
42
|
-
if (value && typeof value === "object") {
|
|
72
|
+
if (value && typeof value === "object") {
|
|
73
|
+
const o = {};
|
|
74
|
+
for (const [k, v] of Object.entries(value)) o[render(k, vars)] = k === "body" && value.bodyEncoding ? renderBody(v, vars, value.bodyEncoding) : k === "url" && value.urlEncoding ? renderURL(v, vars, value.urlEncoding) : render(v, vars);
|
|
75
|
+
return o;
|
|
76
|
+
}
|
|
43
77
|
return value;
|
|
44
78
|
}
|
|
45
79
|
function missing(value, vars) {
|
|
@@ -82,6 +116,8 @@
|
|
|
82
116
|
url: String(req.url || ""),
|
|
83
117
|
headers: req.headers && typeof req.headers === "object" ? req.headers : {},
|
|
84
118
|
body: req.body,
|
|
119
|
+
...(["json", "urlencoded"].includes(req.bodyEncoding) ? { bodyEncoding: req.bodyEncoding } : {}),
|
|
120
|
+
...(req.urlEncoding === "query" ? { urlEncoding: "query" } : {}),
|
|
85
121
|
form: req.form && typeof req.form === "object" ? req.form : null, // multipart: { campo: "@file" | texto }
|
|
86
122
|
auth: req.auth !== false,
|
|
87
123
|
},
|
|
@@ -202,12 +238,12 @@
|
|
|
202
238
|
}
|
|
203
239
|
|
|
204
240
|
function curlCmd(r, tokenVar, extra, fileName) {
|
|
205
|
-
const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${dq(r.url)}`];
|
|
241
|
+
const parts = [`curl -s${extra || ""}${r.method !== "GET" || r.body != null || r.form ? ` -X ${r.method}` : ""} ${r._urlShell || dq(r.url)}`];
|
|
206
242
|
if (r.auth) parts.push(`-H "Authorization: Bearer $${tokenVar}"`);
|
|
207
|
-
for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${dq(k + ": " + v)}`);
|
|
243
|
+
for (const [k, v] of Object.entries(r.headers || {})) parts.push(`-H ${r._headerShell?.[k] || dq(k + ": " + v)}`);
|
|
208
244
|
if (r.form) for (const [k, v] of Object.entries(r.form)) parts.push(`-F ${dq(k + "=" + (v === "@file" ? "@" + (fileName || "arquivo") : v))}`);
|
|
209
245
|
else if (isJsonBody(r.body)) { parts.push(`-H "Content-Type: application/json"`); parts.push(`-d ${shq(JSON.stringify(r.body, null, 2))}`); }
|
|
210
|
-
else if (r.body != null) parts.push(`-d ${shq(r.body)}`);
|
|
246
|
+
else if (r.body != null) parts.push(r._stdinBody ? "--data-binary @-" : `-d ${r._bodyShell || shq(r.body)}`);
|
|
211
247
|
return parts.join(" \\\n ");
|
|
212
248
|
}
|
|
213
249
|
|
|
@@ -217,7 +253,70 @@
|
|
|
217
253
|
const secretsIn = (v) => { const out = new Set(); JSON.stringify(v || "").replace(/\{\{\s*secret\.([\w-]+)\s*\}\}/g, (_, k) => out.add(k)); return [...out]; };
|
|
218
254
|
const fileName = (a, vars) => (vars && vars["file.name"]) || (a.file ? String(a.file).split(/[\\/]/).pop() : "arquivo");
|
|
219
255
|
const usesB64 = (a) => JSON.stringify(a.request.body || "").includes("{{file.base64}}");
|
|
256
|
+
// Para cURL importado, expressões são montadas por contexto. As partes literais continuam
|
|
257
|
+
// literais no shell, e JSON/form/query escapam o segredo só na hora de rodar o exemplo.
|
|
258
|
+
function secretParts(text, encoding) {
|
|
259
|
+
text = String(text); const parts = []; let last = 0, scan = 0, quoted = false;
|
|
260
|
+
const query = text.indexOf("?"), fragment = text.indexOf("#");
|
|
261
|
+
const pattern = /\{\{\s*secret\.([\w-]+)\s*\}\}/g; let match;
|
|
262
|
+
while ((match = pattern.exec(text))) {
|
|
263
|
+
if (encoding === "json") for (; scan < match.index; scan++) { if (quoted && text[scan] === "\\") scan++; else if (text[scan] === '"') quoted = !quoted; }
|
|
264
|
+
if (match.index > last) parts.push(text.slice(last, match.index));
|
|
265
|
+
const context = encoding === "json" ? (quoted ? "json-string" : "json-literal") : encoding === "urlencoded" || (encoding === "query" && query >= 0 && match.index > query && (fragment < 0 || match.index < fragment)) ? "urlencoded" : "raw";
|
|
266
|
+
parts.push({ name: secretEnv(match[1]), context }); last = pattern.lastIndex; scan = last;
|
|
267
|
+
}
|
|
268
|
+
if (last < text.length || !parts.length) parts.push(text.slice(last));
|
|
269
|
+
return parts;
|
|
270
|
+
}
|
|
271
|
+
function secretExpression(text, encoding, language) {
|
|
272
|
+
return secretParts(text, encoding).map(part => {
|
|
273
|
+
if (typeof part === "string") return JSON.stringify(part);
|
|
274
|
+
const value = language === "python" ? `os.environ[${JSON.stringify(part.name)}]` : `segredo(${JSON.stringify(part.name)})`;
|
|
275
|
+
if (part.context === "json-string") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)[1:-1]` : `JSON.stringify(${value}).slice(1, -1)`;
|
|
276
|
+
if (part.context === "json-literal") return language === "python" ? `json.dumps(${value}, ensure_ascii=False)` : `JSON.stringify(${value})`;
|
|
277
|
+
if (part.context === "urlencoded") return language === "python" ? `quote(${value}, safe="-_.!~*'()")` : `encodeURIComponent(${value})`;
|
|
278
|
+
return value;
|
|
279
|
+
}).join(" + ");
|
|
280
|
+
}
|
|
281
|
+
function curlImported(a, vars) {
|
|
282
|
+
const L = Lines(false), name = fileName(a, vars), helpers = [], helperNames = new Map();
|
|
283
|
+
const cleanVars = Object.fromEntries(Object.entries(vars || {}).filter(([key]) => !key.startsWith("secret.")));
|
|
284
|
+
const shell = (text, encoding) => secretParts(text, encoding).map(part => {
|
|
285
|
+
if (typeof part === "string") return shq(part);
|
|
286
|
+
if (part.context === "raw") return `"$${part.name}"`;
|
|
287
|
+
const key = `${part.name}:${part.context}`;
|
|
288
|
+
let helper = helperNames.get(key);
|
|
289
|
+
if (!helper) {
|
|
290
|
+
helper = `SAGA_SECRET_${helpers.length + 1}`; helperNames.set(key, helper);
|
|
291
|
+
const expression = part.context === "urlencoded" ? "encodeURIComponent(valor)" : part.context === "json-string" ? "JSON.stringify(valor).slice(1,-1)" : "JSON.stringify(valor)";
|
|
292
|
+
const script = `const valor=process.env[${JSON.stringify(part.name)}];if(valor==null)throw new Error(${JSON.stringify("Defina " + part.name)});process.stdout.write(${expression});`;
|
|
293
|
+
helpers.push(`${helper}=$(node -e ${shq(script)})`);
|
|
294
|
+
}
|
|
295
|
+
return `"$${helper}"`;
|
|
296
|
+
}).join("");
|
|
297
|
+
const prepare = original => {
|
|
298
|
+
const r = render(original, { ...cleanVars, "file.name": name });
|
|
299
|
+
r._urlShell = shell(r.url, r.urlEncoding);
|
|
300
|
+
r._headerShell = Object.fromEntries(Object.entries(r.headers || {}).map(([key, value]) => [key, shell(`${key}: ${value}`, "raw")]));
|
|
301
|
+
if (r.body != null) {
|
|
302
|
+
if (typeof r.body === "object") r.body = JSON.stringify(r.body, null, 2);
|
|
303
|
+
r._bodyShell = shell(r.body, r.bodyEncoding);
|
|
304
|
+
r._stdinBody = true; // stdin preserva UTF-8 também com curl.exe no Windows
|
|
305
|
+
}
|
|
306
|
+
return r;
|
|
307
|
+
};
|
|
308
|
+
const req = prepare(a.request), check = a.polling ? prepare(render(a.polling.check, { ...cleanVars, id: "$ID" })) : null;
|
|
309
|
+
if (helpers.length) { L.add("# Bash + Node.js: segredos são escapados sem alterar o JSON ou a query."); helpers.forEach(line => L.add(line)); L.blank(); }
|
|
310
|
+
const command = (r, extra = "") => (r._stdinBody ? `printf '%s' ${r._bodyShell} | \\\n` : "") + curlCmd(r, a.tokenVar, extra, name);
|
|
311
|
+
if (a.mode === "polling") {
|
|
312
|
+
L.add("# 1. inicia", "start").add(command(req), "start");
|
|
313
|
+
L.add(`# guarde ${a.polling.id} da resposta em ID`).blank();
|
|
314
|
+
L.add("# 2. consulta o andamento", "poll").add(command(check), "poll");
|
|
315
|
+
} else L.add(command(req, a.mode === "stream" ? "N" : "") + (a.audio ? ` \\\n --output ${shq(a.audio)}` : ""), "start");
|
|
316
|
+
return L.out();
|
|
317
|
+
}
|
|
220
318
|
function curl(a, vars) {
|
|
319
|
+
if (a.request.bodyEncoding || a.request.urlEncoding) return curlImported(a, vars);
|
|
221
320
|
const L = Lines(false);
|
|
222
321
|
const name = fileName(a, vars);
|
|
223
322
|
const sec = Object.fromEntries(secretsIn(a.request).map((k) => ["secret." + k, MARK(k)]));
|
|
@@ -242,14 +341,14 @@
|
|
|
242
341
|
|
|
243
342
|
function pyCall(name, r, extra, ind) {
|
|
244
343
|
ind = ind || "";
|
|
245
|
-
const args = [
|
|
344
|
+
const args = [r._urlExpr || dq(r.url)];
|
|
246
345
|
if (r.auth || Object.keys(r.headers || {}).length) args.push("headers=HEADERS");
|
|
247
346
|
if (r.form) {
|
|
248
347
|
const files = Object.entries(r.form).filter(([, v]) => v === "@file"), data = Object.entries(r.form).filter(([, v]) => v !== "@file");
|
|
249
348
|
if (files.length) args.push(`files={${files.map(([k]) => `${JSON.stringify(k)}: open(${dq(r._fileName || "arquivo")}, "rb")`).join(", ")}}`);
|
|
250
349
|
if (data.length) args.push(`data={${data.map(([k, v]) => `${JSON.stringify(k)}: ${JSON.stringify(String(v))}`).join(", ")}}`);
|
|
251
350
|
} else if (isJsonBody(r.body)) args.push(`json=${pyLiteral(r.body, ind + " ")}`);
|
|
252
|
-
else if (r.body != null) args.push(`data=${JSON.stringify(String(r.body))}`);
|
|
351
|
+
else if (r.body != null) args.push(`data=${r.bodyEncoding ? `(${r._bodyExpr || JSON.stringify(String(r.body))}).encode("utf-8")` : r._bodyExpr || JSON.stringify(String(r.body))}`);
|
|
253
352
|
args.push(...(extra || []));
|
|
254
353
|
return `${name} = requests.${r.method.toLowerCase()}(\n` + args.map((x) => `${ind} ${x},`).join("\n") + `\n${ind})`;
|
|
255
354
|
}
|
|
@@ -260,7 +359,7 @@
|
|
|
260
359
|
Object.assign(h, r.headers || {});
|
|
261
360
|
if (!Object.keys(h).length) return;
|
|
262
361
|
const val = (v) => { const m = String(v).match(/^\{\{\s*secret\.([\w-]+)\s*\}\}$/); return m ? `os.environ[${JSON.stringify(String(m[1]).toUpperCase().replace(/[^A-Z0-9_]/g, "_"))}]` : JSON.stringify(String(v)); };
|
|
263
|
-
const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : val(v)},`).join("\n");
|
|
362
|
+
const body = Object.entries(h).map(([k, v]) => ` ${JSON.stringify(k)}: ${v === "__TOKEN__" ? 'f"Bearer {TOKEN}"' : r.urlEncoding || r.bodyEncoding ? secretExpression(v, "raw", "python") : val(v)},`).join("\n");
|
|
264
363
|
L.add(`HEADERS = {\n${body}\n}`, null, "cabeçalhos enviados em toda chamada");
|
|
265
364
|
}
|
|
266
365
|
|
|
@@ -271,6 +370,12 @@
|
|
|
271
370
|
const toPy = (v) => (v === "{{file.base64}}" ? { __py: "CONTEUDO" } : typeof v === "string" && /^\{\{\s*secret\.([\w-]+)\s*\}\}$/.test(v) ? { __py: `os.environ[${JSON.stringify(secretEnv(v.match(/secret\.([\w-]+)/)[1]))}]` } : Array.isArray(v) ? v.map(toPy) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toPy(x)])) : v);
|
|
272
371
|
const req = { ...render({ ...a.request, body: toPy(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
|
|
273
372
|
const imports = ["import os"];
|
|
373
|
+
if (req.urlEncoding || req.bodyEncoding) {
|
|
374
|
+
req._urlExpr = secretExpression(req.url, req.urlEncoding, "python");
|
|
375
|
+
if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "python");
|
|
376
|
+
if (req.bodyEncoding === "json") imports.push("import json");
|
|
377
|
+
if (req.urlEncoding || req.bodyEncoding === "urlencoded") imports.push("from urllib.parse import quote");
|
|
378
|
+
}
|
|
274
379
|
if (b64) imports.push("import base64");
|
|
275
380
|
if (a.mode === "polling") imports.push("import time");
|
|
276
381
|
if (a.mode === "stream") imports.push("import json");
|
|
@@ -368,9 +473,9 @@
|
|
|
368
473
|
opts.push(`body: JSON.stringify(${jsLiteral(r.body, " ")})`);
|
|
369
474
|
} else {
|
|
370
475
|
if (hasHeaders) opts.push("headers: HEADERS");
|
|
371
|
-
if (r.body != null) opts.push(`body: ${JSON.stringify(String(r.body))}`);
|
|
476
|
+
if (r.body != null) opts.push(`body: ${r._bodyExpr || JSON.stringify(String(r.body))}`);
|
|
372
477
|
}
|
|
373
|
-
const target = url || dq(r.url);
|
|
478
|
+
const target = url || r._urlExpr || dq(r.url);
|
|
374
479
|
L.add(opts.length ? `const ${name} = await fetch(${target}, {\n${opts.map((o) => " " + o + ",").join("\n")}\n});` : `const ${name} = await fetch(${target});`, tag, why);
|
|
375
480
|
L.add(`if (!${name}.ok) throw new Error(\`HTTP \${${name}.status}: \${await ${name}.text()}\`);`, tag, "para aqui se deu erro (4xx/5xx)");
|
|
376
481
|
}
|
|
@@ -380,15 +485,21 @@
|
|
|
380
485
|
const b64 = usesB64(a);
|
|
381
486
|
const toJs = (v) => (v === "{{file.base64}}" ? { __js: "CONTEUDO" } : jsSecret(v) || (Array.isArray(v) ? v.map(toJs) : v && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, toJs(x)])) : v));
|
|
382
487
|
const req = { ...render({ ...a.request, body: toJs(a.request.body) }, { ...vars, "file.name": name }), _fileName: name };
|
|
488
|
+
const imported = !!(req.urlEncoding || req.bodyEncoding);
|
|
489
|
+
if (imported) {
|
|
490
|
+
req._urlExpr = secretExpression(req.url, req.urlEncoding, "javascript");
|
|
491
|
+
if (typeof req.body === "string") req._bodyExpr = secretExpression(req.body, req.bodyEncoding, "javascript");
|
|
492
|
+
}
|
|
383
493
|
const fsUse = [b64 || req.form ? "readFile" : "", a.audio ? "writeFile" : ""].filter(Boolean);
|
|
384
494
|
L.add("// Node.js 18+ · salve como exemplo.mjs e rode: node exemplo.mjs");
|
|
385
495
|
if (fsUse.length) L.add(`import { ${fsUse.join(", ")} } from "node:fs/promises";`);
|
|
386
496
|
L.blank();
|
|
497
|
+
if (imported && secretsIn(a.request).length) L.add('const segredo = (nome) => { const valor = process.env[nome]; if (valor == null) throw new Error(`Defina ${nome}`); return valor; };').blank();
|
|
387
498
|
const auth = req.auth || !!(a.polling && a.polling.check.auth);
|
|
388
499
|
if (auth) L.add(`const TOKEN = process.env.${a.tokenVar};`, null, "o token de acesso vem de uma variável de ambiente, nunca escrito no código").blank();
|
|
389
500
|
const h = [];
|
|
390
501
|
if (auth) h.push(" Authorization: `Bearer ${TOKEN}`,");
|
|
391
|
-
for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
|
|
502
|
+
for (const [k, v] of Object.entries(req.headers || {})) h.push(` ${JSON.stringify(k)}: ${imported ? secretExpression(v, "raw", "javascript") : jsSecret(v) ? jsSecret(v).__js : JSON.stringify(String(v))},`);
|
|
392
503
|
if (h.length) L.add(`const HEADERS = {\n${h.join("\n")}\n};`, null, "cabeçalhos enviados em toda chamada").blank();
|
|
393
504
|
if (b64) L.add(`const CONTEUDO = (await readFile(${dq(name)})).toString("base64");`, "start", "o arquivo, em base64, vai dentro do JSON").blank();
|
|
394
505
|
const out = (expr, why) => {
|
|
@@ -525,6 +636,6 @@
|
|
|
525
636
|
|
|
526
637
|
// o slide mexe com arquivo? (upload @file ou {{file.…}})
|
|
527
638
|
const usesFile = (s) => { const a = s && s._normalized ? s : normalize(s); return !!a.file || a.mic || !!(a.request.form && Object.values(a.request.form).includes("@file")) || /\{\{\s*file\./.test(JSON.stringify(a.request)); };
|
|
528
|
-
const api = { isType, cosine, usesFile, parsePath, get, set, render, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
|
|
639
|
+
const api = { isType, cosine, usesFile, parsePath, get, set, render, renderBody, renderURL, missing, envKind, mask, normalize, key, code, pyLiteral, pyPath, LANGS };
|
|
529
640
|
g.SagadeckApiCore = api;
|
|
530
641
|
})(typeof window !== "undefined" ? window : globalThis);
|
package/src/runtime/api-ui.js
CHANGED
|
@@ -61,7 +61,8 @@
|
|
|
61
61
|
const bodyTxt = bodyEl ? bodyEl.value : "";
|
|
62
62
|
let body;
|
|
63
63
|
if (bodyTxt.trim() !== "" && !cfg.request.form) { // upload: a aba Corpo só mostra os campos do formulário
|
|
64
|
-
|
|
64
|
+
if (cfg.request.bodyEncoding && typeof cfg.request.body === "string") body = bodyTxt;
|
|
65
|
+
else try { body = JSON.parse(bodyTxt); }
|
|
65
66
|
catch (e) {
|
|
66
67
|
if (typeof cfg.request.body === "string") body = bodyTxt;
|
|
67
68
|
else throw new Error("O corpo não é um JSON válido: " + e.message);
|
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,39 @@
|
|
|
1
|
+
/* Aurora Dev: luzes nos cantos, linha orbital e painéis de leitura opacos. A rede ambiente do runtime respeita
|
|
2
|
+
movimento reduzido, motion: none, troca de slide e exportação sem depender de animação CSS contínua. */
|
|
3
|
+
.th-aurora-dev .orn i:nth-child(1){display:block;right:-220px;top:-300px;width:920px;height:820px;border-radius:50%;background:radial-gradient(ellipse,color-mix(in srgb,var(--hi) 18%,transparent),transparent 66%)}
|
|
4
|
+
.th-aurora-dev .orn i:nth-child(2){display:block;left:-240px;bottom:-340px;width:860px;height:740px;border-radius:50%;background:radial-gradient(ellipse,color-mix(in srgb,var(--em) 12%,transparent),transparent 66%)}
|
|
5
|
+
.th-aurora-dev .ambient{opacity:.4!important;mask-image:linear-gradient(90deg,transparent 15%,#000)}
|
|
6
|
+
.th-aurora-dev .kicker{align-self:flex-start;color:var(--em);border-top:3px double var(--em);padding:14px 0 0;font-size:20px;letter-spacing:.16em}
|
|
7
|
+
.th-aurora-dev .L-cover:not(:has(.cv-fig)) .cv-main,.th-aurora-dev .L-end:not(:has(.en-fig)) .en-main{max-width:1390px;margin:0 auto;width:100%;gap:34px}
|
|
8
|
+
.th-aurora-dev :is(.L-cover,.L-end) .ttl{padding-bottom:32px;background:linear-gradient(90deg,var(--hi),var(--em),transparent) no-repeat left bottom/460px 3px}
|
|
9
|
+
.th-aurora-dev .L-cover .sub{max-width:1200px}
|
|
10
|
+
.th-aurora-dev .L-section{flex-direction:column;justify-content:center;align-items:flex-start;gap:28px}
|
|
11
|
+
.th-aurora-dev .L-section .sc-num{font-family:'IBM Plex Mono',monospace;font-size:72px;line-height:1.2;padding:12px 30px;background:var(--surface);border:1px solid var(--hi);border-radius:100px;color:var(--hi)}
|
|
12
|
+
.th-aurora-dev .L-section .sc-text{flex:none;width:100%;gap:24px}
|
|
13
|
+
.th-aurora-dev .L-section:has(.sc-fig){display:grid;grid-template-columns:1fr 500px;align-content:center;column-gap:50px}
|
|
14
|
+
.th-aurora-dev .L-section .sc-fig{grid-column:2;grid-row:1/span 2;width:500px;height:580px}
|
|
15
|
+
.th-aurora-dev .L-section .sc-num{justify-self:start}
|
|
16
|
+
.th-aurora-dev .hd{padding-bottom:22px;border-bottom:2px solid color-mix(in srgb,var(--hi) 40%,var(--line));gap:18px}
|
|
17
|
+
.th-aurora-dev :is(.card:not(.card-hi),.stat-card,.step-card){background:var(--surface);border:1px solid var(--line);border-radius:24px;box-shadow:inset 0 2px 0 color-mix(in srgb,var(--hi) 22%,transparent),0 16px 45px #0002}
|
|
18
|
+
.th-aurora-dev .cd-ico{border-radius:16px;background:color-mix(in srgb,var(--hi) 13%,var(--surface));color:var(--em)}
|
|
19
|
+
.th-aurora-dev .li-n{color:var(--em);border-bottom:2px solid var(--hi);padding-bottom:8px;font-family:'IBM Plex Mono',monospace}
|
|
20
|
+
.th-aurora-dev .li-b{width:14px;height:14px;background:var(--hi);border:0;border-radius:50%;box-shadow:0 0 0 6px color-mix(in srgb,var(--hi) 15%,transparent)}
|
|
21
|
+
.th-aurora-dev .L-quote{padding:36px 50px;border:1px solid var(--line);background:var(--surface);border-radius:24px}
|
|
22
|
+
.th-aurora-dev .q-mark{color:var(--em)}
|
|
23
|
+
.th-aurora-dev .st-body{padding-left:32px;border-left:3px solid var(--em)}
|
|
24
|
+
.th-aurora-dev .nb-val,.th-aurora-dev .st-val{color:var(--hi);font-family:'Space Grotesk',sans-serif}
|
|
25
|
+
/* É a superfície que separa a sintaxe das luzes; não use transparência no código. */
|
|
26
|
+
.th-aurora-dev :is(.code,.codewalk-editor,.api-req,.api-res){background:#131A30;color:#F1F2FF;border:1px solid #475071;border-radius:20px;box-shadow:0 24px 70px #080B2080}
|
|
27
|
+
.th-aurora-dev .codewalk-bar{background:#202A47;color:#F1F2FF;border-bottom-color:#475071}
|
|
28
|
+
.th-aurora-dev .codewalk-editor .code{color:#F1F2FF;border:0;box-shadow:none}
|
|
29
|
+
.th-aurora-dev :is(.codewalk-language,.codewalk-footer,.cn,.tok-comment,.api-code .cl.cm .cc){color:#B5BBD9}
|
|
30
|
+
.th-aurora-dev .codewalk-footer{border-top-color:#475071}
|
|
31
|
+
.th-aurora-dev .code .cl.hl,.th-aurora-dev .api-code .cl.run{background:#C4ADFF1A;box-shadow:inset 4px 0 #C4ADFF;color:#FFF}
|
|
32
|
+
.th-aurora-dev .tok-keyword,.th-aurora-dev .tok-type{color:#C4ADFF}.th-aurora-dev .tok-string,.th-aurora-dev .tok-operator{color:#83E7EF}.th-aurora-dev .tok-number{color:#FFDCA2}.th-aurora-dev .tok-function{color:#A9C8FF}.th-aurora-dev .tok-constant{color:#FFADCA}.th-aurora-dev .tok-punctuation{color:#D9DCF0}
|
|
33
|
+
.th-aurora-dev :is(.api-run,.lesson-controls button,.nav-back){border-radius:14px;font-family:'Space Grotesk',sans-serif}
|
|
34
|
+
.th-aurora-dev :is(.api-run,.lesson-controls button,.api-tabs button,.nav-back):focus-visible{outline:4px solid var(--em);outline-offset:5px}
|
|
35
|
+
.th-aurora-dev .api-tabs button{color:#B5BBD9}.th-aurora-dev .api-tabs button[aria-selected=true]{color:#F1F2FF}
|
|
36
|
+
.th-aurora-dev :is(.api-hint,.api-meta,.api-auth span,.api-tl-i){color:#B5BBD9}
|
|
37
|
+
.th-aurora-dev .lesson-output{border-top:3px solid var(--hi);border-radius:16px}
|
|
38
|
+
html[data-motion="none"] .th-aurora-dev .ambient,.export .th-aurora-dev .ambient{animation:none}
|
|
39
|
+
@media(prefers-reduced-motion:reduce){.th-aurora-dev .ambient{animation:none}}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* Terminal Pro: cabeçalho compacto, moldura de IDE, menta e azul nas partes do código; animação ambiente
|
|
2
|
+
pela rede do runtime, que só anda no slide atual e fica parada na exportação e com movimento reduzido. */
|
|
3
|
+
.th-terminal-pro .orn i:nth-child(1){display:block;inset:0;background:linear-gradient(90deg,color-mix(in srgb,var(--hi) 6%,transparent) 1px,transparent 1px),linear-gradient(color-mix(in srgb,var(--hi) 6%,transparent) 1px,transparent 1px);background-size:64px 64px;mask-image:linear-gradient(90deg,transparent 38%,#000)}
|
|
4
|
+
.th-terminal-pro .orn i:nth-child(2){display:block;right:48px;top:48px;bottom:48px;width:2px;background:linear-gradient(transparent,var(--hi) 35%,var(--em) 65%,transparent);opacity:.35}
|
|
5
|
+
.th-terminal-pro .orn i:nth-child(3){display:block;right:44px;top:52px;width:10px;height:10px;border:2px solid var(--hi);background:var(--bg)}
|
|
6
|
+
.th-terminal-pro .ambient{opacity:.35!important}
|
|
7
|
+
.th-terminal-pro .kicker{align-self:flex-start;padding:12px 18px;background:var(--surface);color:var(--hi);border:1px solid var(--line);border-radius:7px;font-size:20px;letter-spacing:.1em}
|
|
8
|
+
.th-terminal-pro .L-cover .cv-main,.th-terminal-pro .L-end .en-main{border-left:5px solid var(--hi);padding-left:42px;gap:32px}
|
|
9
|
+
.th-terminal-pro .L-cover .ttl{max-width:1330px}
|
|
10
|
+
.th-terminal-pro .L-cover .sub{max-width:1220px}
|
|
11
|
+
.th-terminal-pro .L-section{gap:48px}
|
|
12
|
+
.th-terminal-pro .sc-num{font-family:'IBM Plex Mono',monospace;font-size:156px;line-height:1.2;padding:20px 26px;border:1px solid var(--hi);border-radius:12px;color:var(--hi);background:var(--surface)}
|
|
13
|
+
.th-terminal-pro .hd{padding:0 0 22px 22px;border-left:4px solid var(--hi);border-bottom:1px solid var(--line);gap:16px}
|
|
14
|
+
.th-terminal-pro :is(.card:not(.card-hi),.stat-card,.step-card){border:1px solid var(--line);border-top:4px solid var(--hi);background:var(--surface);border-radius:12px;box-shadow:0 12px 36px #0002}
|
|
15
|
+
.th-terminal-pro .li-n{font-family:'IBM Plex Mono',monospace;background:var(--surface);padding:8px 12px;border:1px solid var(--line);border-radius:8px;color:var(--hi)}
|
|
16
|
+
.th-terminal-pro .li-b{border:0;border-radius:3px;background:var(--hi);width:14px;height:14px}
|
|
17
|
+
.th-terminal-pro .q-text,.th-terminal-pro .st-body{border-left:4px solid var(--hi);padding-left:32px}
|
|
18
|
+
.th-terminal-pro .nb-val,.th-terminal-pro .st-val{font-family:'IBM Plex Mono',monospace;color:var(--hi)}
|
|
19
|
+
/* Código sempre numa superfície escura estável, inclusive com uma paleta clara aplicada por cima. */
|
|
20
|
+
.th-terminal-pro :is(.code,.codewalk-editor,.api-req,.api-res){background:#0D1720;color:#EAF5F1;border:1px solid #344B58;border-radius:12px;box-shadow:0 22px 60px #0003}
|
|
21
|
+
.th-terminal-pro .codewalk-bar{background:#172530;border-bottom-color:#344B58;color:#EAF5F1}
|
|
22
|
+
.th-terminal-pro .codewalk-editor .code{color:#EAF5F1;border:0;box-shadow:none}
|
|
23
|
+
.th-terminal-pro :is(.codewalk-language,.codewalk-footer,.cn,.tok-comment,.api-code .cl.cm .cc){color:#A4B8B4}
|
|
24
|
+
.th-terminal-pro .codewalk-footer{border-top-color:#344B58}
|
|
25
|
+
.th-terminal-pro .code .cl.hl,.th-terminal-pro .api-code .cl.run{background:#74E8B41A;box-shadow:inset 4px 0 #74E8B4;color:#F4FFF9}
|
|
26
|
+
.th-terminal-pro .tok-keyword,.th-terminal-pro .tok-constant{color:#CEB4FF}.th-terminal-pro .tok-string{color:#74E8B4}.th-terminal-pro .tok-number,.th-terminal-pro .tok-type{color:#FFCB8A}.th-terminal-pro .tok-function{color:#83CCF8}.th-terminal-pro .tok-operator,.th-terminal-pro .tok-punctuation{color:#C1D6E2}
|
|
27
|
+
.th-terminal-pro :is(.api-run,.lesson-controls button,.nav-back){border-radius:8px;font-family:'Space Grotesk',sans-serif}
|
|
28
|
+
.th-terminal-pro :is(.api-run,.lesson-controls button,.api-tabs button,.nav-back):focus-visible{outline:4px solid var(--em);outline-offset:5px}
|
|
29
|
+
.th-terminal-pro .api-tabs button{color:#A4B8B4}.th-terminal-pro .api-tabs button[aria-selected=true]{color:#EAF5F1}
|
|
30
|
+
.th-terminal-pro :is(.api-hint,.api-meta,.api-auth span,.api-tl-i){color:#A4B8B4}
|
|
31
|
+
.th-terminal-pro .lesson-output{border-left:4px solid var(--hi);border-radius:8px}
|
|
32
|
+
html[data-motion="none"] .th-terminal-pro .ambient,.export .th-terminal-pro .ambient{animation:none}
|
|
33
|
+
@media(prefers-reduced-motion:reduce){.th-terminal-pro .ambient{animation:none}}
|