sagadeck 1.9.0 → 1.10.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 +7 -1
- package/SKILL.md +1 -1
- package/docs/ANDAMENTO.md +115 -0
- package/docs/REFERENCIA.md +48 -2
- package/package.json +4 -4
- package/src/ai/context.js +7 -2
- package/src/ai/deck-ai.js +3 -1
- package/src/api-client.js +42 -10
- package/src/code-highlight.js +19 -2
- package/src/code-language.js +13 -0
- package/src/runtime/api-core.js +122 -11
- package/src/runtime/api-ui.js +2 -1
- package/src/runtime/skins/aurora-dev.css +39 -0
- package/src/runtime/skins/terminal-pro.css +33 -0
- package/src/studio/ai-chat-route.js +26 -7
- package/src/studio/developer-starters.js +46 -0
- package/src/studio/library-routes.js +7 -0
- package/src/studio/public/about.js +68 -0
- package/src/studio/public/app.js +2 -1
- package/src/studio/public/chat.js +5 -1
- 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 +18 -0
- package/src/studio/public/library.css +36 -0
- package/src/studio/public/library.html +4 -1
- package/src/studio/public/library.js +115 -28
- package/src/studio/public/slide-form.js +9 -14
- package/src/studio/public/style.css +2 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/server.js +2 -2
- package/src/themes.js +48 -0
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);
|
|
@@ -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}}
|
|
@@ -105,8 +105,23 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
105
105
|
res.on('close', onClose);
|
|
106
106
|
// Capture o arquivo da tarefa: abrir outro deck não redireciona a escrita.
|
|
107
107
|
const taskWorkspace = { ...owner, spec: structuredClone(owner.spec) };
|
|
108
|
+
const originFile = owner.file, originPreview = owner.preview;
|
|
109
|
+
let materialized = null;
|
|
108
110
|
const prompt = body.message || "";
|
|
109
111
|
const spec = body.spec || taskWorkspace.spec;
|
|
112
|
+
// Conversar com uma prévia não cria arquivo. Edição/imagem ou documento binário precisam da cópia pessoal.
|
|
113
|
+
const imageOptionsFor = (deck = spec) => {
|
|
114
|
+
const options = imageOptions(taskWorkspace, withBase(taskWorkspace, deck));
|
|
115
|
+
if (originPreview && !originFile && taskWorkspace.file && !materialized) {
|
|
116
|
+
materialized = { id: taskWorkspace.library.idOf(taskWorkspace.file), title: taskWorkspace.spec.title,
|
|
117
|
+
topic: originPreview.topic, file: taskWorkspace.file };
|
|
118
|
+
if (owner.file === originFile && owner.preview === originPreview) {
|
|
119
|
+
owner.file = taskWorkspace.file;
|
|
120
|
+
owner.preview = null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return materialized ? { ...options, baseDir: path.dirname(taskWorkspace.file) } : options;
|
|
124
|
+
};
|
|
110
125
|
const issues = body.issues || [];
|
|
111
126
|
// a IA vê (e testa) os slides api no ambiente atual, que pode ser o embutido "ensaio"
|
|
112
127
|
if (!apiBlocked(req) && (spec.slides || []).some((sl) => sl && sl.layout === "api")) await ensureEnsaio();
|
|
@@ -128,8 +143,11 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
128
143
|
const target = typeof body.targetSlide === "number" ? body.targetSlide : null;
|
|
129
144
|
const visuals = await lookAt(withBase(taskWorkspace, spec), target, prompt, emit);
|
|
130
145
|
// anexos: imagens vão como visão; documentos (id do /api/ai/context) vão como texto
|
|
131
|
-
let materials =
|
|
132
|
-
.filter((a) => a && typeof a === "object" && a.type === "doc").map((a) => a.id))
|
|
146
|
+
let materials = takeMaterials(taskWorkspace, (Array.isArray(body.attachments) ? body.attachments : [])
|
|
147
|
+
.filter((a) => a && typeof a === "object" && a.type === "doc").map((a) => a.id));
|
|
148
|
+
if (!taskWorkspace.file && taskWorkspace.preview && materials.some(m => m.bytes)) imageOptionsFor();
|
|
149
|
+
const materialDir = taskWorkspace.file && !isBundledTemplate(taskWorkspace.file) ? path.dirname(taskWorkspace.file) : null;
|
|
150
|
+
if (materialDir) materials = await prepareDocumentMaterials(materials, materialDir, {signal,onProgress:emit});
|
|
133
151
|
for (const [i, url] of (Array.isArray(body.attachments) ? body.attachments : []).entries()) {
|
|
134
152
|
if (typeof url === "string" && url.startsWith("data:image/")) visuals.push({ label: `imagem colada pelo usuário ${i + 1}`, dataUrl: url });
|
|
135
153
|
}
|
|
@@ -154,7 +172,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
154
172
|
targetSlide: target,
|
|
155
173
|
issues,
|
|
156
174
|
images: true, // a IA decide (regra no prompt: só quando pedirem ou aceitarem)
|
|
157
|
-
imageOptions:
|
|
175
|
+
imageOptions: () => imageOptionsFor(),
|
|
158
176
|
history,
|
|
159
177
|
onProgress: emit,
|
|
160
178
|
visuals,
|
|
@@ -212,6 +230,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
212
230
|
result.actions = [...(before.actions || []), ...r.feitos.map((f) => `Mapa: gravei ${f.arquivo} (${f.linhas} ${f.tipo === "contorno" ? "contorno" : "itens"}${f.fonte ? `; fonte: ${f.fonte}` : ""})`), ...r.falhas.map((x) => `Mapa: não deu (${x})`), ...(result.actions || [])];
|
|
213
231
|
}
|
|
214
232
|
if (result.geo?.length) result = { ...result, geo: undefined, talk: true, reply: P ? `${result.reply}\n(Já busquei os dados duas vezes; me diga o que ainda falta.)` : "Os dados do mapa ficam na pasta da apresentação: abra-a pela biblioteca e peça de novo." };
|
|
233
|
+
if (!taskWorkspace.file && taskWorkspace.preview && !result.talk && !result.variants) imageOptionsFor(result.spec);
|
|
215
234
|
// documento anexado NESTA mensagem e a resposta montou/reescreveu a maior parte do deck (criar a apresentação
|
|
216
235
|
// do paper pelo chat): a mesma conferência de cobertura da geração. Pedido pontual ("use a figura 3 no slide
|
|
217
236
|
// 5") não dispara: mexe em poucos slides.
|
|
@@ -220,12 +239,12 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
220
239
|
const rewritten = Array.isArray(result.changed) ? result.changed.length : 0;
|
|
221
240
|
if (rewritten >= Math.max(5, result.spec.slides.length / 2)) {
|
|
222
241
|
const covered = await coverDocumentVisuals(result.spec, { materials, briefing: prompt, say: (s) => emit({ phase: "step", text: s }),
|
|
223
|
-
edit: (o) => editDeck({ signal, images: true, imageOptions:
|
|
242
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: () => imageOptionsFor(), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
224
243
|
signal.throwIfAborted();
|
|
225
244
|
if (covered.spec !== result.spec) { result.spec = covered.spec; result.actions = [...(result.actions || []), ...covered.actions]; }
|
|
226
245
|
if (covered.coverage?.missing.length) result.actions = [...(result.actions || []), `Sem slide (figuras/tabelas do material): ${covered.coverage.missing.join("; ")}`];
|
|
227
246
|
const checked = await checkNumbers(result.spec, { materials, say: (s) => emit({ phase: "step", text: s }),
|
|
228
|
-
edit: (o) => editDeck({ signal, images: true, imageOptions:
|
|
247
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: () => imageOptionsFor(), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
229
248
|
signal.throwIfAborted();
|
|
230
249
|
if (checked.spec !== result.spec) { result.spec = checked.spec; result.actions = [...(result.actions || []), ...checked.actions]; }
|
|
231
250
|
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(", ")}`];
|
|
@@ -264,7 +283,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
264
283
|
emit({ phase: "test", text: reports.every((x) => x.ok) ? "Os testes passaram; conferindo…" : "Corrigindo com base no resultado…" });
|
|
265
284
|
const next = await editDeck({
|
|
266
285
|
spec: withBase(taskWorkspace, result.spec), signal, instruction, targetSlide: target, images: false,
|
|
267
|
-
imageOptions:
|
|
286
|
+
imageOptions: () => imageOptionsFor(result.spec), history: convo, onProgress: emit, apiContext: apiContextFor(req, taskWorkspace),
|
|
268
287
|
drawCheck: diagramCheck, runCommand: commandRunner(req, emit, body, taskWorkspace, signal),
|
|
269
288
|
});
|
|
270
289
|
convo.push({ role: "user", text: instruction });
|
|
@@ -286,7 +305,7 @@ _${c.naoConfirmados.length} observação(ões) descartada(s): o trecho citado n
|
|
|
286
305
|
persist(taskWorkspace);
|
|
287
306
|
if (owner.file === taskWorkspace.file) owner.spec = taskWorkspace.spec;
|
|
288
307
|
return { ...result, spec: taskWorkspace.spec, conflicts: merged.conflicts, kept: merged.kept };
|
|
289
|
-
}, signal)); } finally { owner.aiChats.delete(requestId); res.off("close", onClose); }
|
|
308
|
+
}, signal).then(result => materialized ? { ...result, materialized } : result)); } finally { owner.aiChats.delete(requestId); res.off("close", onClose); }
|
|
290
309
|
return true;
|
|
291
310
|
}
|
|
292
311
|
return false;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Bases curtas para começar uma apresentação técnica sem esperar pela IA.
|
|
2
|
+
// São modelos de fábrica; cada uso cria uma instância pela biblioteca.
|
|
3
|
+
export function developerStarter(kind, { title } = {}) {
|
|
4
|
+
const code = 'function normalizar(nome) {\n return nome.trim().toLowerCase();\n}\n\nconst usuario = normalizar(" Ada ");\nconsole.log(usuario);';
|
|
5
|
+
const curl = ['curl --request POST "$BASE/echo" \\', ' --header "Content-Type: application/json" \\', ' --data \'{"mensagem":"Olá, API"}\''].join("\n");
|
|
6
|
+
if (!["programacao", "apis"].includes(kind)) throw new Error("Escolha uma base de programação ou APIs com cURL.");
|
|
7
|
+
const programming = kind === "programacao";
|
|
8
|
+
const name = String(title || (programming ? "Meu código, passo a passo" : "Minha primeira API com cURL")).trim();
|
|
9
|
+
return {
|
|
10
|
+
title: name, theme: programming ? "terminal-pro" : "aurora-dev", duration: 6, purpose: "palestra", maxWords: 70,
|
|
11
|
+
...(!programming ? { env: "ensaio" } : {}),
|
|
12
|
+
slides: [
|
|
13
|
+
{ layout: "cover", kicker: "Exemplo para editar", title: name,
|
|
14
|
+
subtitle: programming ? "Entrada, transformação e resultado" : "Entenda o comando e experimente uma chamada",
|
|
15
|
+
figure: { motion: { type: "console", caption: programming ? "normalizar.js · saída de exemplo" : "curl · saída de exemplo", scenes: [{ cmd: programming ? "node normalizar.js" : 'curl --request POST "$BASE/echo"', out: programming ? ["ada"] : ['200 OK', '{ "mensagem": "Olá, API" }'] }] } },
|
|
16
|
+
notes: "Este é um exemplo de fábrica para você editar. Use Inserir › Código / cURL para colar seu conteúdo. A animação da capa mostra uma saída ilustrativa." },
|
|
17
|
+
programming ? {
|
|
18
|
+
layout: "codewalk", title: "Uma transformação em ==três passos==", filename: "normalizar.js", language: "JavaScript", code,
|
|
19
|
+
steps: [
|
|
20
|
+
{ title: "Definir a regra", text: "A função recebe um nome. trim remove os espaços nas pontas; toLowerCase converte as letras para minúsculas.", highlight: [1, 2, 3] },
|
|
21
|
+
{ title: "Aplicar à entrada", text: "Os espaços e as letras maiúsculas entram no mesmo exemplo.", highlight: [5] },
|
|
22
|
+
{ title: "Conferir a saída", text: "O resultado pode ser copiado e usado no restante do programa.", highlight: [6], output: "ada" },
|
|
23
|
+
], notes: "Troque o código em Formatar. Selecione as linhas e crie uma etapa para explicar o trecho. A saída deste guia é ilustrativa, sem executar o código."
|
|
24
|
+
} : {
|
|
25
|
+
layout: "codewalk", title: "O cURL em ==três partes==", filename: "pedido.sh", language: "Bash", code: curl,
|
|
26
|
+
steps: [
|
|
27
|
+
{ title: "Método e destino", text: "POST envia os dados para o endpoint. BASE é o endereço do ambiente.", highlight: [1] },
|
|
28
|
+
{ title: "Formato dos dados", text: "Content-Type avisa ao servidor que o corpo é JSON.", highlight: [2] },
|
|
29
|
+
{ title: "O corpo enviado", text: "A mensagem é o dado que vamos conferir na resposta.", highlight: [3] },
|
|
30
|
+
], notes: "Este comando explica a chamada, sem executá-la. O próximo slide chama a API de ensaio do Studio local."
|
|
31
|
+
},
|
|
32
|
+
programming ? {
|
|
33
|
+
layout: "diagram", title: "Veja o ==caminho== dos dados", mermaid: 'flowchart LR\n A["Entrada: espaços e maiúsculas"] --> B["trim + toLowerCase"] --> C["Saída: ada"]',
|
|
34
|
+
notes: "Troque os nós pelo fluxo do seu programa. Use a mesma entrada e a mesma saída em toda a explicação."
|
|
35
|
+
} : {
|
|
36
|
+
layout: "api", title: "Experimente ==ao vivo==", text: "No Studio local, clique em Executar. Troque a mensagem no Corpo e execute novamente.", tab: "curl",
|
|
37
|
+
request: { method: "POST", url: "{{base}}/echo", auth: false, body: { mensagem: "Olá, API" } }, answer: "$.recebido",
|
|
38
|
+
notes: "Ambiente ENSAIO: API de mentira que o Studio local fornece. No servidor multiusuário, chamadas não são executadas. Para usar seu serviço, troque o ambiente e importe seu cURL por Inserir › Código / cURL."
|
|
39
|
+
},
|
|
40
|
+
{ layout: "code", title: programming ? "Conferir a ==entrada e saída==" : "Ler a ==resposta==", filename: "resposta.json", language: "JSON",
|
|
41
|
+
code: programming ? '{\n "entrada": " Ada ",\n "saida": "ada"\n}' : '{\n "metodo": "POST",\n "recebido": { "mensagem": "Olá, API" },\n "consulta": {}\n}',
|
|
42
|
+
note: programming ? "Mantenha um exemplo concreto do começo ao fim da explicação." : "O campo recebido contém os mesmos dados enviados no corpo." },
|
|
43
|
+
{ layout: "end", title: "Agora, o ==seu exemplo==", subtitle: "Cole seu conteúdo em Inserir › Código / cURL ou peça ao assistente para explicar os trechos.", notes: "Você pode aplicar outro tema em Design. O exemplo é seu para editar; cada uso cria uma apresentação independente." },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -9,6 +9,7 @@ import { writeDeckFile } from "../deck-file.js";
|
|
|
9
9
|
import { createExperienceDeck } from "../experiences.js";
|
|
10
10
|
import { demoDeck } from "./demo-decks.js";
|
|
11
11
|
import { DEMO_FILES } from "../api-demo.js";
|
|
12
|
+
import { developerStarter } from "./developer-starters.js";
|
|
12
13
|
|
|
13
14
|
export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON, readBody, slideSnapshots, sendExport, galleryKey, galleryEntry, materializePreview, copyModelAssets, generateIntoLibrary, llmAvailable, llmConfig, respond, templateFile, TEMPLATE_DIRS }) {
|
|
14
15
|
const L = W.library;
|
|
@@ -74,6 +75,12 @@ export async function libraryRoutes({ req, res, pathname, url, W, opts, readJSON
|
|
|
74
75
|
case "/api/library/topics/update": return ok({ id: L.updateTopic(b.id, b) });
|
|
75
76
|
case "/api/library/topics/delete": L.deleteTopic(b.id); return ok();
|
|
76
77
|
case "/api/library/decks": {
|
|
78
|
+
if (b.starter) {
|
|
79
|
+
const spec = developerStarter(b.starter, { title: b.title });
|
|
80
|
+
if (b.theme) spec.theme = b.theme;
|
|
81
|
+
buildHTML(spec);
|
|
82
|
+
return ok({ id: L.createDeck(b.topic || "", spec) });
|
|
83
|
+
}
|
|
77
84
|
// nova: em branco (uma capa) — "com IA" usa /api/library/decks/ai
|
|
78
85
|
const title = String(b.title || "Nova apresentação").trim();
|
|
79
86
|
const id = L.createDeck(b.topic || "", { title, theme: b.theme || "bauhaus", duration: 10,
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// "Sobre o SagaDeck" (Ajuda › Sobre, no editor; botão Sobre, na biblioteca): a versão que está rodando e o essencial
|
|
2
|
+
// para dar suporte (commit, Node, desde quando, a biblioteca, se a IA está configurada), com Copiar informações.
|
|
3
|
+
window.SagaAbout = (() => {
|
|
4
|
+
// as cores vêm das variáveis que o editor e a biblioteca definem nos dois temas (claro e escuro); fundo e texto
|
|
5
|
+
// sempre do mesmo par, senão o tema escuro dá texto claro sobre fundo branco
|
|
6
|
+
const css = `.about-dlg{margin:auto;max-width:min(520px,calc(100vw - 32px));width:100%;border:1px solid var(--ui-line,#c9ced6);border-radius:12px;padding:20px 22px;background:var(--chrome,#fff);color:var(--text,#1d2433);font:14px/1.45 system-ui,sans-serif;box-shadow:0 20px 60px #0004}
|
|
7
|
+
.about-dlg::backdrop{background:rgba(0,0,0,.45)}.about-dlg h2{margin:0;font-size:20px;color:var(--text,#1d2433)}.about-dlg .ver{margin:2px 0 14px;font-size:15px;color:var(--text-2,#555)}
|
|
8
|
+
.about-dlg .ver b{color:var(--text,#1d2433);font-size:22px}.about-dlg dl{display:grid;grid-template-columns:auto 1fr;gap:6px 14px;margin:0 0 14px}
|
|
9
|
+
.about-dlg dt{color:var(--text-2,#555)}.about-dlg dd{margin:0;overflow-wrap:anywhere;color:var(--text,#1d2433)}.about-dlg code{font:12px ui-monospace,Consolas,monospace;color:inherit}
|
|
10
|
+
.about-dlg .links{display:flex;gap:14px;flex-wrap:wrap;margin:0 0 6px}.about-dlg .links a{color:var(--brand,#5b4bdb)}
|
|
11
|
+
.about-dlg .row{display:flex;gap:8px;justify-content:flex-end;margin-top:14px}.about-dlg button{padding:7px 14px;border-radius:8px;border:1px solid var(--ui-line,#c9ced6);background:transparent;color:var(--text,#1d2433);cursor:pointer;font:inherit}
|
|
12
|
+
.about-dlg button.primary{background:var(--brand-fill,#5b4bdb);border-color:var(--brand-fill,#5b4bdb);color:#fff}.about-dlg [data-copied]{color:var(--text-2,#555);align-self:center;margin-right:auto}`;
|
|
13
|
+
const REPO = "https://github.com/naruminho/sagadeck";
|
|
14
|
+
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
15
|
+
let dialog;
|
|
16
|
+
|
|
17
|
+
function build() {
|
|
18
|
+
if (!document.getElementById("about-style")) { const st = document.createElement("style"); st.id = "about-style"; st.textContent = css; document.head.append(st); }
|
|
19
|
+
dialog = document.createElement("dialog");
|
|
20
|
+
dialog.className = "about-dlg";
|
|
21
|
+
dialog.id = "about-dialog";
|
|
22
|
+
dialog.setAttribute("aria-labelledby", "about-title");
|
|
23
|
+
document.body.append(dialog);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// o que o servidor diz de si (versão, commit, Node, desde quando) e se a IA está configurada
|
|
27
|
+
async function facts() {
|
|
28
|
+
const get = async (u) => { try { const r = await fetch(u, { cache: "no-store" }); return r.ok ? r.json() : {}; } catch { return {}; } };
|
|
29
|
+
const [inst, ia] = await Promise.all([get("api/instance"), get("api/ia")]);
|
|
30
|
+
return { ...inst, ia: ia.configured == null ? null : !!ia.configured };
|
|
31
|
+
}
|
|
32
|
+
const text = (f) => [`SagaDeck ${f.version || "?"} (commit ${f.commit || "?"})`, `Node ${f.node || "?"} · ${f.platform || "?"}`,
|
|
33
|
+
f.started ? `No ar desde ${new Date(f.started).toLocaleString("pt-BR")} (processo ${f.pid || "?"})` : "",
|
|
34
|
+
f.multiuser ? "Servidor multiusuário" : f.library ? `Biblioteca: ${f.library}` : "",
|
|
35
|
+
f.ia == null ? "" : `IA: ${f.ia ? "configurada" : "não configurada"}`].filter(Boolean).join("\n");
|
|
36
|
+
|
|
37
|
+
async function open() {
|
|
38
|
+
if (!dialog) build();
|
|
39
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2><p class="ver">Carregando…</p><div class="row"><button type="button" data-close>Fechar</button></div>`;
|
|
40
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
41
|
+
if (!dialog.open) dialog.showModal();
|
|
42
|
+
const f = await facts();
|
|
43
|
+
const row = (k, v) => (v ? `<dt>${k}</dt><dd>${v}</dd>` : "");
|
|
44
|
+
dialog.innerHTML = `<h2 id="about-title">Sobre o SagaDeck</h2>
|
|
45
|
+
<p class="ver">Versão <b data-version>${esc(f.version || "?")}</b></p>
|
|
46
|
+
<dl>
|
|
47
|
+
${row("Commit", f.commit ? `<code>${esc(f.commit)}</code>` : "")}
|
|
48
|
+
${row("Node", f.node ? `${esc(f.node)} · ${esc(f.platform || "")}` : "")}
|
|
49
|
+
${row("No ar desde", f.started ? esc(new Date(f.started).toLocaleString("pt-BR")) : "")}
|
|
50
|
+
${row("Biblioteca", !f.multiuser && f.library ? `<code>${esc(f.library)}</code>` : f.multiuser ? "neste servidor (multiusuário)" : "")}
|
|
51
|
+
${row("IA", f.ia == null ? "" : f.ia ? "configurada (para testar cada recurso: <code>sagadeck ia-check</code>)" : "não configurada")}
|
|
52
|
+
</dl>
|
|
53
|
+
<div class="links">
|
|
54
|
+
<a href="${REPO}/blob/main/CHANGELOG.md" target="_blank" rel="noopener">O que mudou em cada versão</a>
|
|
55
|
+
<a href="${REPO}" target="_blank" rel="noopener">Código e problemas</a>
|
|
56
|
+
<a href="https://www.npmjs.com/package/sagadeck" target="_blank" rel="noopener">Pacote no npm</a>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="row"><span data-copied hidden>Copiado.</span><button type="button" data-copy>Copiar informações</button><button type="button" class="primary" data-close>Fechar</button></div>`;
|
|
59
|
+
dialog.querySelector("[data-close]").onclick = () => dialog.close();
|
|
60
|
+
dialog.querySelector("[data-copy]").onclick = async () => {
|
|
61
|
+
try { await navigator.clipboard.writeText(text(f)); dialog.querySelector("[data-copied]").hidden = false; }
|
|
62
|
+
catch { dialog.querySelector("[data-copied]").textContent = "Não deu para copiar."; dialog.querySelector("[data-copied]").hidden = false; }
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// qualquer botão ou link com data-about abre (Ajuda › Sobre no editor, o botão da biblioteca)
|
|
66
|
+
document.addEventListener("click", (e) => { const b = e.target.closest?.("[data-about]"); if (b) { e.preventDefault(); open(); } });
|
|
67
|
+
return { open };
|
|
68
|
+
})();
|
package/src/studio/public/app.js
CHANGED
|
@@ -2156,6 +2156,7 @@
|
|
|
2156
2156
|
showToast("Screenshot pronto. Use Apresentar para percorrer os destaques.");
|
|
2157
2157
|
}
|
|
2158
2158
|
function setupCreativeTools() {
|
|
2159
|
+
window.SagaDeveloperAuthoring.bind({ state, syncDeckToServer, renderThumbnails, renderCurrentSlide, openPane, showToast });
|
|
2159
2160
|
const shotButton = document.createElement("button");
|
|
2160
2161
|
shotButton.id = "btn-screenshot";
|
|
2161
2162
|
shotButton.className = "rbtn rbtn-lg";
|
|
@@ -3706,7 +3707,7 @@ ${ta.value}`;
|
|
|
3706
3707
|
|
|
3707
3708
|
// Chat lateral com IA (chat.js): mesma API, agora em módulo próprio.
|
|
3708
3709
|
const chatApi = window.SagaChat({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
3709
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus });
|
|
3710
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized });
|
|
3710
3711
|
const { handleChatSubmit, loadChatHistory, appendChatMessage, createProgressBubble, autoGrowChat, applyBrainstorm, addChatFile } = chatApi;
|
|
3711
3712
|
|
|
3712
3713
|
function setNotesVisible(on) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Saiu do app.js pela trava de tamanho dos monolitos; o comportamento é o mesmo.
|
|
3
3
|
// Fábrica no molde de slide-select.js: recebe o estado e os ajudantes, devolve a API.
|
|
4
4
|
window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, openPane, currentPane,
|
|
5
|
-
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus }) {
|
|
5
|
+
streamAI, syncDeckToServer, renderThumbnails, renderCurrentSlide, trackDeck, fitRendered, refreshAIStatus, previewMaterialized }) {
|
|
6
6
|
let chatJob = null;
|
|
7
7
|
const getChatJob = () => chatJob;
|
|
8
8
|
// ==========================================================================
|
|
@@ -159,6 +159,10 @@ window.SagaChat = function ({ state, dom, escHtml, hydrateIcons, showToast, open
|
|
|
159
159
|
}, (ev) => { if (!commandEvent(ev)) work.update(ev); }, job.controller.signal);
|
|
160
160
|
if (state.file !== job.file) { work.done(); showToast("O pedido terminou na apresentação em que começou; a apresentação atual foi preservada.", 6000); return; }
|
|
161
161
|
if (!data.spec) throw new Error(data.error || "resposta sem deck");
|
|
162
|
+
if (data.materialized) {
|
|
163
|
+
state.file = data.materialized.file;
|
|
164
|
+
previewMaterialized(data.materialized);
|
|
165
|
+
}
|
|
162
166
|
// conversa salva antes de dispensar o indicador: quem espera o fim da resposta já encontra o arquivo gravado
|
|
163
167
|
const finishWork = async () => { await saveChatHistory(); work.done(); };
|
|
164
168
|
if (data.variants) {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/* Conteúdo técnico em um só lugar: fonte à esquerda, resultado à direita. */
|
|
2
|
+
.dev-author-dialog{margin:auto;width:min(1110px,96vw);max-width:96vw;height:min(810px,94dvh);max-height:94dvh;padding:0;border:1px solid var(--ui-line);border-radius:18px;background:var(--chrome);color:var(--text);box-shadow:0 24px 90px #0005;font:13px var(--font-sans)}
|
|
3
|
+
.dev-author-dialog[open]{display:flex;flex-direction:column}.dev-author-dialog::backdrop{background:#101521aa;backdrop-filter:blur(4px)}
|
|
4
|
+
.dev-author-dialog header{display:flex;justify-content:space-between;align-items:center;padding:20px 25px;border-bottom:1px solid var(--ui-line)}.dev-author-dialog header small{color:var(--brand);font-size:10px;font-weight:700;letter-spacing:.13em}.dev-author-dialog h2{margin:5px 0;font-size:25px;letter-spacing:-.035em}.dev-author-dialog header p{margin:5px 0 0;color:var(--text-2)}
|
|
5
|
+
.dev-author-dialog button{padding:9px 12px;border:1px solid var(--ui-line-strong);border-radius:8px;background:var(--chrome);color:var(--text);font:inherit;cursor:pointer}.dev-author-dialog button:hover{background:var(--hover)}.dev-author-dialog button:disabled{opacity:.45;cursor:default}.dev-author-dialog button:focus-visible,.dev-author-dialog input:focus-visible,.dev-author-dialog select:focus-visible,.dev-author-dialog textarea:focus-visible{outline:2px solid var(--brand);outline-offset:2px}
|
|
6
|
+
.dev-author-work{display:grid;grid-template-columns:minmax(0,1fr) 340px;min-height:0;flex:1}.dev-author-source{display:flex;flex-direction:column;min-height:0;padding:22px 24px;gap:10px}.dev-author-dialog label{display:block;color:var(--text-2);font-size:12px}.dev-author-dialog input:not([type=radio]),.dev-author-dialog textarea,.dev-author-dialog select{display:block;width:100%;margin-top:6px;padding:9px 10px;border:1px solid var(--ui-line-strong);border-radius:7px;background:var(--chrome);color:var(--text);font:13px var(--font-sans)}
|
|
7
|
+
.dev-author-dialog #dev-source{flex:1;min-height:150px;margin-top:0;resize:none;white-space:pre;tab-size:2;font:13px/1.65 var(--font-mono,monospace);background:var(--canvas-bg)}.dev-author-meta{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-meta>label:first-child{grid-column:1/-1}.dev-author-dialog small{font-size:10px}.dev-author-dialog aside{padding:22px;overflow-y:auto;border-left:1px solid var(--ui-line)}.dev-author-dialog fieldset{border:0;padding:0;margin:0 0 22px}.dev-author-dialog legend{font-size:12px;font-weight:700;color:var(--text);margin-bottom:12px}
|
|
8
|
+
.dev-author-modes label{display:flex;align-items:flex-start;gap:9px;padding:12px;margin-bottom:8px;border:1px solid var(--ui-line);border-radius:9px;cursor:pointer}.dev-author-modes label:has(input:checked){background:var(--selected);border-color:var(--brand)}.dev-author-modes input{margin-top:3px;accent-color:var(--brand)}.dev-author-modes b{display:block;color:var(--text);font-size:13px}.dev-author-modes small{display:block;line-height:1.5;margin-top:4px;color:var(--text-2)}
|
|
9
|
+
.dev-author-dialog h3{font-size:13px;margin-bottom:9px}.dev-author-dialog .dev-author-tip{font-size:11px;line-height:1.5;color:var(--text-2);margin:0 0 10px}.dev-author-range{display:grid;grid-template-columns:1fr 1fr;gap:10px}.dev-author-dialog [data-guide]>label{margin:10px 0}.dev-author-dialog [data-step-text]{resize:vertical;max-height:150px}.dev-author-dialog [data-add-step]{width:100%;margin:4px 0 12px}.dev-author-steps{display:grid;gap:6px}.dev-author-step{display:flex;gap:4px}.dev-author-step button:first-child{flex:1;min-width:0;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.dev-author-step button.active{border-color:var(--brand);color:var(--brand)}.dev-author-step button:last-child{font-size:10px;padding:7px}
|
|
10
|
+
.dev-author-dialog [data-request]{max-height:210px;overflow:auto;border:1px solid var(--ui-line);padding:12px;border-radius:8px;background:var(--canvas-bg);font:11px/1.6 var(--font-mono,monospace);white-space:pre-wrap;overflow-wrap:anywhere;margin-bottom:12px}.dev-author-secrets{padding:12px;border:1px solid var(--brand);border-radius:8px;color:var(--text);background:var(--selected);line-height:1.6;font-size:11px;overflow-wrap:anywhere}.dev-author-dialog [hidden]{display:none!important}
|
|
11
|
+
.dev-author-dialog footer{display:flex;align-items:center;justify-content:flex-end;gap:9px;padding:14px 24px;border-top:1px solid var(--ui-line)}.dev-author-dialog [data-status]{flex:1;font-size:11px;line-height:1.5;color:var(--text-2)}.dev-author-dialog .dev-author-primary{background:var(--brand);color:#fff;border-color:var(--brand)}.dev-author-dialog .dev-author-primary:hover{filter:brightness(1.08)}
|
|
12
|
+
@media(max-width:760px){.dev-author-dialog{height:94dvh}.dev-author-work{display:flex;flex-direction:column;overflow-y:auto}.dev-author-source{min-height:390px;flex-shrink:0;padding:16px}.dev-author-dialog aside{border-left:0;border-top:1px solid var(--ui-line);overflow:visible;padding:16px}.dev-author-dialog header{padding:15px 16px}.dev-author-dialog h2{font-size:21px}.dev-author-dialog footer{padding:12px 16px;flex-wrap:wrap}.dev-author-dialog [data-status]{flex-basis:100%}}
|