sagadeck 1.3.1 → 1.5.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 +4 -4
- package/bin/sagadeck.js +9 -1
- package/docs/ANDAMENTO.md +376 -0
- package/docs/REFERENCIA.md +4 -4
- package/package.json +1 -1
- package/src/ai/bench.js +132 -0
- package/src/ai/context.js +12 -3
- package/src/ai/critique.js +114 -0
- package/src/ai/deck-ai.js +234 -30
- package/src/ai/document-materials.js +2 -2
- package/src/ai/document-visuals.js +400 -20
- package/src/ai/llm.js +19 -5
- package/src/ai/progress.js +23 -0
- package/src/ai/quality.js +60 -0
- package/src/api-client.js +2 -1
- package/src/build.js +5 -1
- package/src/decision-lab.js +2 -1
- package/src/deck-file.js +2 -1
- package/src/elements.js +44 -7
- package/src/figures/charts.js +7 -4
- package/src/figures/diagrams.js +15 -1
- package/src/figures/ufmap.js +2 -1
- package/src/fs-retry.js +19 -0
- package/src/import/crop.js +27 -8
- package/src/layouts.js +22 -2
- package/src/library.js +68 -16
- package/src/locale.js +18 -0
- package/src/mcp/server.js +2 -2
- package/src/motion.js +4 -2
- package/src/preferences.js +2 -1
- package/src/runtime/base.css +3 -0
- package/src/runtime/calc.js +3 -3
- package/src/runtime/decision-lab.js +1 -1
- package/src/runtime/fit.js +20 -1
- package/src/runtime/runtime.js +2 -1
- package/src/studio/ai-chat-route.js +119 -4
- package/src/studio/code-version.js +35 -0
- package/src/studio/generate.js +53 -0
- package/src/studio/project.js +8 -7
- package/src/studio/public/app.js +7 -216
- package/src/studio/public/chat.js +71 -1
- package/src/studio/public/header-footer.js +140 -0
- package/src/studio/public/index.html +5 -0
- package/src/studio/public/library.html +1 -0
- package/src/studio/public/library.js +4 -3
- package/src/studio/public/preferences-ui.js +83 -0
- package/src/studio/public/provenance.js +13 -0
- package/src/studio/public/style.css +17 -0
- package/src/studio/public/ui-icons.js +1 -1
- package/src/studio/public/update-notice.js +25 -0
- package/src/studio/public-files.js +11 -1
- package/src/studio/server.js +20 -29
- package/src/studio/share-routes.js +2 -1
package/src/ai/deck-ai.js
CHANGED
|
@@ -48,6 +48,14 @@ Siga ESTRITAMENTE a referência abaixo: use só layouts, elementos, campos e fig
|
|
|
48
48
|
- Ao reformular composição ou criar uma experiência pelo chat, inclua review: true para a conferência visual. Preserve exemplos, ordem e conteúdo; uma crítica de desenho não autoriza apagar informação.
|
|
49
49
|
- Adapte tom, estilo, tema, densidade e recursos ao conteúdo, público e objetivo. Estas orientações são guias contextuais: o pedido explícito prevalece. Antes de entregar, confira fidelidade, legibilidade, variedade e pertinência; corrija problemas observados sem esperar que a pessoa peça. Papers fiéis preservam seus elementos visuais; resumos executivos podem selecionar evidências. Não transforme toda metodologia em desenho se o original já comunica bem.
|
|
50
50
|
- Paper científico anexado (congresso, defesa, aula): metodologia merece slides de verdade, respeitando a natureza do paper — SE tiver método quantitativo, detalhe modelos com parâmetros e calibração, métricas com valores E fontes (tabela comparativa quando houver); se for teórico, revisão ou qualitativo, apresente a estrutura do argumento sem inventar números. Números sempre do próprio texto com a origem ("n=1.403", "NSE 0,90–0,96"). Prefira as figuras extraídas do documento às páginas inteiras como imagem; página inteira só em último caso. Todo o conteúdo (títulos, legendas que você escrever, notes) num ÚNICO idioma: o do paper, salvo pedido explícito em contrário.
|
|
51
|
+
- Apresentação de paper (congresso, defesa, seminário): siga o arco do artigo, cada etapa com conteúdo de verdade e não uma frase solta — (1) contexto e problema, com um dado do próprio paper que mostre por que importa; (2) lacuna/justificativa: o que falta nos métodos ou estudos atuais; (3) objetivo geral e específicos/hipótese num slide de conteúdo (list, cards ou split; nunca o objetivo inteiro numa manchete); (4) área de estudo e dados (onde, quando, quanto, com que instrumento ou fonte); (5) método: o fluxo de etapas, as equações com o significado de cada variável e unidade, parâmetros e critérios de avaliação; (6) resultados: cada figura e tabela numerada do paper no slide do resultado dela, com título que afirma o achado ("O erro cresce com a altura") e 1–3 frases de leitura do que a figura mostra; (7) discussão: por que deu assim, comparando com o que o paper cita; (8) conclusões e (9) limitações/trabalhos futuros. Slide de frase única (statement/headline/quote) só para a mensagem principal, no máximo 2 no deck.
|
|
52
|
+
- Figuras do inventário trazem width×height do recorte: figura larga (largura ≥ 1,6× a altura: mapa duplo, painéis lado a lado, gráfico largo) vai em composição vertical (split com arrangement: stacked, ou image) com o texto curto embaixo; figura alta ou quadrada vai em split lado a lado com a explicação. Nunca ponha título, legenda ou véu por cima de figura do documento (layout full com overlay): o texto cobre a legenda do mapa e os eixos. Não diminua titleSize para dar espaço à figura (o layout já reserva a área; título em tamanhos diferentes de slide a slide desorganiza o deck): se o título não couber, encurte o texto.
|
|
53
|
+
- Figura mal feita do documento (sourcePx = tamanho nativo da imagem; abaixo de ~1000 px de largura ela fica borrada esticada no slide, ou é escaneada/pixelada) pode ser redesenhada: ESQUEMA de caixas e setas (fluxograma, etapas do método, organograma) vira \`diagram\`/\`flow\` nativo fiel, com os mesmos textos e ligações; GRÁFICO cujos valores estão no paper (tabela, texto, rótulos legíveis) vira \`chart\` nativo com os números exatos; gráfico sem os dados à mão (dispersão com centenas de pontos) e ILUSTRAÇÃO desenhada podem ser redesenhados pelo modelo de imagem a partir do original (\`image_prompt: "Clean up and redraw THIS EXACT figure…"\`, copiando eixos, rótulos e valores letra por letra, + \`image_ref\` com o arquivo do inventário, \`fit: contain\`). Mapa e foto ficam como estão. Toda reconstrução nativa (chart, diagram, table) de uma figura do documento leva no slide \`sourceFigure: <arquivo do inventário>\` (o redesenho pelo modelo de imagem guarda \`original\` sozinho): é o caminho de volta. Se a pessoa pedir para manter ou voltar à figura original, troque a reconstrução por \`figure: { image: <sourceFigure ou original>, fit: contain }\` no mesmo slide, sem mexer no texto.
|
|
54
|
+
- AUTORIA E LIBERDADE: decida pela ocasião e grave em \`context.autoria\`. \`autor\`: a pessoa apresenta o PRÓPRIO trabalho (congresso, simpósio, conferência, defesa, banca, relatório da própria equipe; é o padrão quando o pedido é apresentar o paper anexado numa dessas ocasiões). \`livre\`: todo o resto (aula ou seminário sobre o trabalho de outros, journal club, resenha, divulgação, palestra temática, material sem documento anexado). Em \`autor\`, o deck é FIEL ao material: afirmações, conclusões e ênfases são do autor; achado que está no material e passa batido (uma coluna da tabela, uma comparação que o texto não explora) entra no slide; inconsistência, limitação não declarada e pergunta provável vão nas notes do slide relacionado ("Prepare-se: …"), nunca como afirmação no slide. Em \`livre\`, você tem liberdade de criar conteúdo próprio: contexto, analogias, exemplos, comparações, crítica, perguntas para a plateia, gráficos dos números; sem inventar fatos.
|
|
55
|
+
- QUEM ASSINA o deck feito de um documento: \`author\` (e a capa) são os autores do DOCUMENTO, como estão nele (todos, ou o primeiro "et al." se forem muitos), com a instituição em \`role\`; nunca o "autor padrão das preferências", que é de quem usa o sagadeck, não de quem escreveu o artigo. Em \`autoria: autor\`, quem apresenta é um dos autores do artigo (o primeiro, se o pedido não disser outro). Em \`livre\`, a capa credita os autores do artigo e, se o pedido disser quem apresenta, ele vai em \`role\` ("apresentado por …").
|
|
56
|
+
- Apresentação acadêmica (congresso, simpósio, conferência, defesa, banca, seminário de disciplina, journal club): sem enquete, votação, quiz, cronômetro ou contador de votos, a menos que o pedido peça interação; pergunta para discussão vai como slide de texto (question sem options, statement) ou nas notes.
|
|
57
|
+
- ORIGEM (provenance) de cada slide de conteúdo, quando houver material anexado: \`provenance: material\` (o que o slide afirma está no material), \`derivado\` (reorganização, cálculo ou gráfico seu a partir dos números do material) ou \`proprio\` (inclusão sua: contexto geral, analogia, exemplo, crítica, pergunta); com \`provenanceNote\` curta dizendo o que é seu quando não for \`material\`. A pessoa vê no Studio de onde veio cada slide; não esconda inclusão sua atrás de "material".
|
|
58
|
+
- Iniciativa com os números: dados que o paper dá só no texto ou em tabela (valores por classe, por linha, por período, métricas de modelos, antes × depois) e que ficam mais claros vistos lado a lado viram \`chart\` nativo criado por você (barras para comparar, linha para série, donut para partes de um todo), com os valores exatos do paper e \`source\` dizendo de onde vieram (tabela, seção), mesmo que o paper não tenha esse gráfico. Não invente pontos nem interpole: só os números escritos. Esse gráfico novo entra em slide próprio ou ao lado da figura do paper, nunca no lugar de um mapa ou foto (o mapa mostra onde; o gráfico, quanto).
|
|
51
59
|
- Capacidades reais: quando pesquisa/comandos estiverem disponíveis, use-os antes de alegar falta de acesso à web. Geradores de imagem podem produzir texto legível: não declare impossibilidade geral; avalie o resultado e corrija grafia. Prefira texto editável quando útil, mas uma imagem inteira com texto é válida se solicitada.
|
|
52
60
|
- Direção de arte: se pedirem propostas visuais, use variants com três composições do MESMO conteúdo real; varie hierarquia, enquadramento e tipografia, não só cor. Cada opção pode levar direction: {theme, rationale}. Escolher aplica o tema ao deck, mantendo outros slides intactos.
|
|
53
61
|
- Para integrar texto ao cenário, componha camadas editáveis em canvas (imagem, texto, primeiro plano). continuity no mesmo objeto em slides consecutivos mantém continuidade espacial. Prefira uma imagem estática forte quando movimento não explicar nada.
|
|
@@ -250,12 +258,58 @@ function protectLatex(src) {
|
|
|
250
258
|
return src.replace(/"(?:[^"\\\n]|\\.)*"/g, (q) => (/\\[A-Za-z]{2,}/.test(q) ? q.replace(LATEX_CMD, "\\\\") : q));
|
|
251
259
|
}
|
|
252
260
|
|
|
261
|
+
// Vírgula decimal numa sequência ou mapa entre colchetes/chaves: `[07/04/2017, HEC-HMS, 0,93, 0,97]` — no YAML a
|
|
262
|
+
// vírgula ali SEMPRE separa itens, e "0,93" virava duas células (0 e 93): a tabela do paper saiu com NSE 0 e R² 93,
|
|
263
|
+
// as colunas todas deslocadas, sem erro nenhum. Quando os itens são separados por ", " (vírgula e espaço), um
|
|
264
|
+
// "dígitos,dígitos" sem espaço só pode ser número com vírgula decimal: vai entre aspas. `[10,20,30]` (sem ", ") fica.
|
|
265
|
+
export function quoteFlowDecimals(src) {
|
|
266
|
+
return String(src).split("\n").map((line) => {
|
|
267
|
+
if (!/[[{]/.test(line) || !/\d,\d/.test(line)) return line;
|
|
268
|
+
let out = "", depth = 0, quote = null;
|
|
269
|
+
for (let i = 0; i < line.length; i++) {
|
|
270
|
+
const c = line[i];
|
|
271
|
+
if (quote) { out += c; if (c === quote && line[i - 1] !== "\\") quote = null; continue; }
|
|
272
|
+
if (c === '"' || c === "'") { quote = c; out += c; continue; }
|
|
273
|
+
if (c === "#" && /\s/.test(line[i - 1] || " ")) { out += line.slice(i); break; }
|
|
274
|
+
if (c === "[" || c === "{") depth++;
|
|
275
|
+
if (c === "]" || c === "}") depth = Math.max(0, depth - 1);
|
|
276
|
+
// início de item no fluxo: número com vírgula decimal colada, terminando no separador ", " ou no fecho
|
|
277
|
+
if (depth > 0 && /[\s[{:,]/.test(line[i - 1] || "[")) {
|
|
278
|
+
const m = /^-?\d+,\d+%?(?=\s*(?:,\s|[\]}]|$))/.exec(line.slice(i));
|
|
279
|
+
if (m && /,\s/.test(line)) { out += `"${m[0]}"`; i += m[0].length - 1; continue; }
|
|
280
|
+
}
|
|
281
|
+
out += c;
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}).join("\n");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Número de tabela escrito com zero no fim ("0.90", "7.80", "1.0") virava número no YAML e perdia o zero: a tabela
|
|
288
|
+
// do paper em inglês saiu com "0.9" onde o artigo diz 0.90 (a precisão relatada é dado). Dentro de tabela (rows,
|
|
289
|
+
// head, headers, cells) o número fica como foi escrito; dado de gráfico continua número.
|
|
290
|
+
const TABLE_KEYS = new Set(["rows", "head", "headers", "cells"]);
|
|
291
|
+
function parseKeepingDecimals(src) {
|
|
292
|
+
const doc = YAML.parseDocument(src);
|
|
293
|
+
if (doc.errors.length) throw doc.errors[0];
|
|
294
|
+
YAML.visit(doc, {
|
|
295
|
+
Scalar(_key, node, path) {
|
|
296
|
+
if (typeof node.value !== "number" || typeof node.source !== "string" || node.source === String(node.value)) return;
|
|
297
|
+
if (!/^-?\d+\.\d+$/.test(node.source)) return;
|
|
298
|
+
const keys = path.filter((p) => YAML.isPair(p)).map((p) => String(p.key?.value));
|
|
299
|
+
const inChart = keys.some((k) => ["chart", "data", "parts", "series", "values"].includes(k));
|
|
300
|
+
// valor exibido como texto (número grande do layout number, stats): o contador lê "0.90" com as duas casas
|
|
301
|
+
if (keys.some((k) => TABLE_KEYS.has(k)) || (keys.at(-1) === "value" && !inChart)) node.value = node.source;
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
return doc.toJS();
|
|
305
|
+
}
|
|
306
|
+
|
|
253
307
|
export function parseYaml(src) {
|
|
254
|
-
src = protectLatex(src);
|
|
308
|
+
src = quoteFlowDecimals(protectLatex(src));
|
|
255
309
|
try {
|
|
256
|
-
return rejoinFlowCommas(
|
|
310
|
+
return rejoinFlowCommas(parseKeepingDecimals(src));
|
|
257
311
|
} catch (first) {
|
|
258
|
-
try { return rejoinFlowCommas(
|
|
312
|
+
try { return rejoinFlowCommas(parseKeepingDecimals(repairYaml(src))); } catch { throw new Error(`YAML inválido: ${first.message}`); }
|
|
259
313
|
}
|
|
260
314
|
}
|
|
261
315
|
|
|
@@ -474,6 +528,9 @@ export async function materializeImages(spec, { assetsDir, baseDir, max = Infini
|
|
|
474
528
|
// sem fit escolhido, o encaixe automático decide: a ilustração 16:9 numa caixa em pé cabia cortada ("cover")
|
|
475
529
|
// e a criança da capa saía pela metade
|
|
476
530
|
if (!node.alt) node.alt = prompt.slice(0, 120);
|
|
531
|
+
// a figura de base fica anotada: "volte para a figura original" pelo chat troca image por original (antes o
|
|
532
|
+
// caminho se perdia junto com o image_ref e não havia como voltar)
|
|
533
|
+
if (refFile && !node.original) node.original = node.image_ref;
|
|
477
534
|
delete node.image_prompt;
|
|
478
535
|
delete node.image_ref;
|
|
479
536
|
done.push({ prompt, file, ref: refFile ? path.relative(baseDir, refFile).split(path.sep).join("/") : undefined });
|
|
@@ -531,6 +588,13 @@ const CONVERSATION_RULES = `Como responder (você decide pelo que a pessoa quer
|
|
|
531
588
|
pedido de slide. RESPONDA a pergunta com o que você sabe (e com o material anexado, se houver), sem bloco yaml e sem
|
|
532
589
|
tocar nos slides. Só vira AÇÃO com verbo explícito de fazer ("crie", "adicione", "gere", "monte", "transforme em slides",
|
|
533
590
|
"aplica o que combinamos").
|
|
591
|
+
1c. OPINIÃO SEM FOCO ("o que você acha desse slide?", "está bom?", "como ficou?") não diz se é sobre o CONTEÚDO ou a
|
|
592
|
+
APARÊNCIA, e a foto do slide puxa a resposta para o visual. Responda os dois, CONTEÚDO PRIMEIRO: o que o slide afirma
|
|
593
|
+
está certo e fiel ao material? falta o porquê, o número, a evidência, a conclusão? a ordem conta a história? o que a
|
|
594
|
+
plateia ou a banca vai perguntar? (use o material e a leitura crítica, se houver). Depois a FORMA em uma ou duas
|
|
595
|
+
frases (hierarquia, leitura, o que distrai). Até ~180 palavras; opções incluem "Aprofundar o conteúdo" e "Aprofundar o
|
|
596
|
+
visual". Se a pessoa já disse o foco ("o visual", "o design", "a cor" × "o conteúdo", "o texto", "o argumento",
|
|
597
|
+
"os dados"), fale só dele.
|
|
534
598
|
1b. BRAINSTORM ANTES DE GERAR — a pessoa diz que vai pedir a geração depois ("primeiro vamos discutir", "depois eu peço
|
|
535
599
|
para gerar", "só ideias por enquanto"): NÃO gere nada parcial nem antecipe slides. Discuta, combine e CONFIRME o plano;
|
|
536
600
|
a geração acontece quando ela pedir. Se ela aprovar uma sugestão tua ou vocês fecharem um ponto ("fechado", "isso mesmo",
|
|
@@ -590,6 +654,15 @@ delete: [7] # números (atuais) dos slides a remover
|
|
|
590
654
|
test: [2, 3] # slides "api" para o Studio EXECUTAR agora e te devolver o resultado (números no deck DEPOIS das mudanças)
|
|
591
655
|
review: true # conferir visualmente os slides alterados; use ao criar experiências ou reformular composição
|
|
592
656
|
\`\`\`
|
|
657
|
+
VER O MATERIAL ANEXADO: do documento você recebe o texto inteiro e o INVENTÁRIO (legendas, caminhos dos recortes e
|
|
658
|
+
das páginas inteiras em \`pages\`), mas NÃO as imagens. Quando a resposta depender do que uma figura, tabela ou página
|
|
659
|
+
mostra (ler um valor de gráfico, conferir um rótulo, comparar o recorte com a página, avaliar se a figura presta),
|
|
660
|
+
peça para ver ANTES de responder ou mexer: responda uma frase e um bloco yaml só com
|
|
661
|
+
\`\`\`yaml
|
|
662
|
+
ver: [contexto/visuais/…/page-06.png, contexto/visuais/…/…-figure-….png] # até 6 arquivos do inventário
|
|
663
|
+
\`\`\`
|
|
664
|
+
O Studio devolve as imagens e você continua o pedido já vendo. Nunca diga que viu ou leu uma figura que não pediu
|
|
665
|
+
para ver; e não diga que não tem acesso ao material: você tem o texto e pode ver qualquer página ou recorte assim.
|
|
593
666
|
TRANSFORMAR A APRESENTAÇÃO INTEIRA (só quando o deck atual veio de um PowerPoint importado, com \`import:\` no deck e slides
|
|
594
667
|
com \`importado:\`): se a pessoa pedir para MELHORAR a apresentação toda mantendo o estilo do original, ou para RECRIAR do zero,
|
|
595
668
|
não faça patch: responda uma frase dizendo o que vai fazer e um bloco yaml só com
|
|
@@ -709,7 +782,7 @@ export function applyPatch(base, patch) {
|
|
|
709
782
|
const STUDIO_KEYS = ["layout", "notes", "time", "id", "uid", "review", "original", "master", "visualEdits", "auto", "fiscalOk", "from", "image_prompt", "image_ref", "density", "deco", "theme", "palette", "tone",
|
|
710
783
|
"bg", "fg", "background", "backgroundStyle", "footer", "header", "transition", "steps", "markStyle", "maxWords", "fit", "titleAs", "context"];
|
|
711
784
|
const FREE_TEXT_KEYS = new Set(["code", "mermaid", "svg", "html", "request", "realtime", "body", "headers", "response", "notes", "consulta", "output", "json"]);
|
|
712
|
-
const PATCH_WORDS = new Set(["slides", "insert", "delete", "edit", "deck", "variants", "test"]);
|
|
785
|
+
const PATCH_WORDS = new Set(["slides", "insert", "delete", "edit", "deck", "variants", "test", "ver"]);
|
|
713
786
|
let knownKeysCache = null;
|
|
714
787
|
function knownSlideKeys() {
|
|
715
788
|
if (knownKeysCache) return knownKeysCache;
|
|
@@ -730,6 +803,13 @@ export function sanitizeCheck(spec, indices) {
|
|
|
730
803
|
else if (DUMPED_PATCH.test(v)) problems.push(`${where}: o texto parece um pedaço do patch/resposta (slides:, layout:…), não conteúdo`);
|
|
731
804
|
} else if (Array.isArray(v)) v.forEach((x, i) => scan(x, `${where}[${i}]`, key));
|
|
732
805
|
else if (v && typeof v === "object") {
|
|
806
|
+
// tabela com linha de tamanho diferente do cabeçalho: coluna deslocada (vírgula decimal partindo célula,
|
|
807
|
+
// célula esquecida) — o número aparece na coluna errada sem erro nenhum
|
|
808
|
+
const head = Array.isArray(v.head) ? v.head : Array.isArray(v.headers) ? v.headers : null;
|
|
809
|
+
if (head && Array.isArray(v.rows)) {
|
|
810
|
+
const bad = v.rows.map((r, k) => (Array.isArray(r) && r.length !== head.length ? k + 1 : 0)).filter(Boolean);
|
|
811
|
+
if (bad.length) problems.push(`${where}: linha(s) ${bad.slice(0, 5).join(", ")} da tabela com número de células diferente do cabeçalho (${head.length}); número com vírgula decimal vai entre aspas ("0,93") e cada linha tem uma célula por coluna`);
|
|
812
|
+
}
|
|
733
813
|
if (v.svg) {
|
|
734
814
|
const competing = ["diagram", "chart", "picto", "ufmap", "qr", "points", "image"].filter(k => v[k]);
|
|
735
815
|
if (competing.length) problems.push(`${where}: renderizadores incompatíveis svg e ${competing.join(", ")}; um ocultaria o outro. Ao trocar a figura, remova o tipo anterior com null no edit, ou use elementos separados em row/col/canvas para sobrepor.`);
|
|
@@ -740,6 +820,8 @@ export function sanitizeCheck(spec, indices) {
|
|
|
740
820
|
for (const i of indices) {
|
|
741
821
|
const s = spec.slides[i];
|
|
742
822
|
if (!s || typeof s !== "object") continue;
|
|
823
|
+
// a tabela pode ser o próprio slide (layout table: head + rows)
|
|
824
|
+
if (Array.isArray(s.head) && Array.isArray(s.rows)) scan({ head: s.head, rows: s.rows }, `slide ${i + 1}`, null);
|
|
743
825
|
for (const [k, v] of Object.entries(s)) {
|
|
744
826
|
if (PATCH_WORDS.has(k) && k !== "steps") problems.push(`slide ${i + 1}: "${k}" não existe dentro de um slide (é palavra do patch; o slide ficou com um pedaço da resposta)`);
|
|
745
827
|
else if (!known.has(k)) problems.push(`slide ${i + 1}: o campo "${k}" não existe (veja os campos do layout ${s.layout || "?"} na referência)`);
|
|
@@ -831,7 +913,7 @@ function parseEditText(text, base) {
|
|
|
831
913
|
const { text: body, options } = parseOptions(text);
|
|
832
914
|
text = body;
|
|
833
915
|
// Às vezes o patch vem sem a cerca ```: se há uma linha "slides:"/"deck:"/"insert:"/"delete"/"variants:", é YAML.
|
|
834
|
-
const bare = /^(slides|edit|deck|insert|delete|variants|test)\s*:/m.exec(text);
|
|
916
|
+
const bare = /^(slides|edit|deck|insert|delete|variants|test|ver)\s*:/m.exec(text);
|
|
835
917
|
if (!/```/.test(text) && bare) text = `${text.slice(0, bare.index)}\n\`\`\`yaml\n${text.slice(bare.index)}\n\`\`\``;
|
|
836
918
|
const hasYaml = /```/.test(text);
|
|
837
919
|
if (!hasYaml) {
|
|
@@ -843,6 +925,12 @@ function parseEditText(text, base) {
|
|
|
843
925
|
const raw = parseYaml(yaml);
|
|
844
926
|
if (!raw || typeof raw !== "object") throw new Error("O bloco yaml precisa ser um objeto com deck/slides/insert/delete (ou variants).");
|
|
845
927
|
if (raw.variants) return parseVariants(raw.variants, base, prose);
|
|
928
|
+
// ver: [arquivos] → a IA quer OLHAR o material (recorte, página inteira) antes de responder; quem mostra é o Studio
|
|
929
|
+
if (raw.ver || raw.look) {
|
|
930
|
+
const files = [].concat(raw.ver || raw.look).map((f) => String(f || "").trim()).filter(Boolean);
|
|
931
|
+
if (!files.length) throw new Error("ver: mande a lista de arquivos do inventário (recortes ou páginas) que quer ver.");
|
|
932
|
+
return { spec: base, prose, changed: [], look: files.slice(0, 6) };
|
|
933
|
+
}
|
|
846
934
|
if (raw.estilo || raw.style) {
|
|
847
935
|
const e = raw.estilo || raw.style;
|
|
848
936
|
const act = typeof e === "string" ? { aplicar: e } : e || {};
|
|
@@ -932,7 +1020,7 @@ Antes de responder, verifique (e siga as Regras de edição):
|
|
|
932
1020
|
];
|
|
933
1021
|
let motifObjected = false;
|
|
934
1022
|
const drawWarned = new Set();
|
|
935
|
-
const { spec: edited, prose, attempts, changed = [], talk, options = [], variants, imagesDropped, visionRouted, test = [], commands = [], transform, style, review } =
|
|
1023
|
+
const { spec: edited, prose, attempts, changed = [], talk, options = [], variants, imagesDropped, visionRouted, test = [], commands = [], transform, style, review, look } =
|
|
936
1024
|
await askUntilValid(messages, async (t) => {
|
|
937
1025
|
const parsed = parseEditText(t, spec);
|
|
938
1026
|
if (repairSlides && (parsed.spec.slides.length !== spec.slides.length || parsed.changed?.some(i => !repairSlides.includes(i)))) throw new Error('A revisão só pode corrigir os slides indicados, sem inserir, excluir ou alterar outros slides.');
|
|
@@ -953,6 +1041,8 @@ Antes de responder, verifique (e siga as Regras de edição):
|
|
|
953
1041
|
if (talk) return { reply: prose, spec, actions, targetSlide, talk: true, options };
|
|
954
1042
|
// transformar a apresentação inteira: quem executa é o servidor (src/ai/transform.js), com o andamento no chat
|
|
955
1043
|
if (transform) return { reply: prose || "Vou transformar a apresentação.", spec, actions, targetSlide, transform };
|
|
1044
|
+
// quer ver o material antes de responder: o Studio mostra as imagens e chama de novo (src/studio/ai-chat-route.js)
|
|
1045
|
+
if (look) return { reply: prose || "Vou olhar o material.", spec, actions, targetSlide, look };
|
|
956
1046
|
// estilo (Brand Kit): quem aplica/salva é o servidor, com a biblioteca
|
|
957
1047
|
if (style) return { reply: prose || "Certo.", spec, actions, targetSlide, style };
|
|
958
1048
|
// versões para escolher: nada muda até a pessoa escolher uma
|
|
@@ -994,6 +1084,7 @@ Antes de responder, verifique (e siga as Regras de edição):
|
|
|
994
1084
|
quality,
|
|
995
1085
|
reviewRequested: !!review,
|
|
996
1086
|
test,
|
|
1087
|
+
changed,
|
|
997
1088
|
actions,
|
|
998
1089
|
targetSlide: changed.includes(targetSlide) ? targetSlide : (changed[0] ?? pickTarget(spec, edited, targetSlide)),
|
|
999
1090
|
};
|
|
@@ -1059,9 +1150,84 @@ export function styleFor(kind) {
|
|
|
1059
1150
|
// Gera um deck inteiro a partir de um briefing. É o MESMO caminho do chat (editDeck): o pedido vai como uma conversa
|
|
1060
1151
|
// sobre um deck em branco, com as mesmas regras e a mesma temperatura. Antes havia um caminho próprio (direção
|
|
1061
1152
|
// criativa sorteada, regras rígidas de ritmo, rodadas de reescrita) que saía bem pior que pedir a mesma coisa no chat.
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1153
|
+
// Conferência de cobertura do material (ferramenta de autocrítica da IA): figuras e tabelas numeradas do documento
|
|
1154
|
+
// anexado que nenhum slide usa voltam para a IA encaixar no ponto certo da narrativa. Uma rodada; o que ficar de
|
|
1155
|
+
// fora vai no relatório (coverage.missing), sem slide enfiado no fim à força.
|
|
1156
|
+
export async function coverDocumentVisuals(spec, { materials = [], briefing = "", edit, say = () => {} } = {}) {
|
|
1157
|
+
const { uncoveredVisuals } = await import("./document-visuals.js");
|
|
1158
|
+
const missing = uncoveredVisuals(spec, materials);
|
|
1159
|
+
if (!missing.length) return { spec, coverage: null, actions: [] };
|
|
1160
|
+
say(`${missing.length} figura(s)/tabela(s) do material ainda sem slide; pedindo para encaixar…`);
|
|
1161
|
+
let next = spec, actions = [];
|
|
1162
|
+
try {
|
|
1163
|
+
const r = await edit({ spec, instruction: `Conferência de cobertura: estas figuras e tabelas numeradas do material anexado não aparecem em nenhum slide:
|
|
1164
|
+
${missing.map((m) => `- ${m.caption} (página ${m.page}; arquivo ${m.image}${m.width ? `; ${m.width}×${m.height} px` : ""})`).join("\n")}
|
|
1165
|
+
Encaixe cada uma no ponto da narrativa em que o assunto dela é discutido (no slide que já fala dele ou num slide novo logo ao lado), com a figura dominante e 1–3 frases de leitura do que ela mostra, sem repetir a legenda numerada. Figuras do mesmo resultado podem dividir um slide. Não mexa no resto do deck. Só deixe alguma de fora se o pedido da pessoa limitou explicitamente a seleção (resumo, poucos slides): nesse caso diga qual e por quê.
|
|
1166
|
+
|
|
1167
|
+
Pedido original da pessoa:
|
|
1168
|
+
"""
|
|
1169
|
+
${briefing}
|
|
1170
|
+
"""` });
|
|
1171
|
+
if (!r.talk && r.spec?.slides?.length) { next = r.spec; actions = r.actions || []; }
|
|
1172
|
+
} catch (e) { say(`a conferência de cobertura falhou (${e.message}); sigo com o deck como está`); }
|
|
1173
|
+
const still = uncoveredVisuals(next, materials);
|
|
1174
|
+
if (still.length) say(`Ficaram sem slide: ${still.map((m) => m.caption.slice(0, 60)).join("; ")}.`);
|
|
1175
|
+
return { spec: next, coverage: { total: missing.length, missing: still.map((m) => m.caption) }, actions };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Idioma do deck pelo texto (quando a IA não gravou `lang`): palavras de ligação de cada língua, que todo texto usa.
|
|
1179
|
+
// Deck em inglês sem `lang` saía com os números em português ("1.403", "0,93") e a hifenização errada.
|
|
1180
|
+
const LANG_WORDS = { en: /^(the|of|and|to|in|with|for|is|are|from|by|on|this|that|we|our|was|were)$/, pt: /^(de|da|do|das|dos|que|para|com|não|uma|um|os|as|em|no|na|por|são|é)$/, es: /^(el|la|los|las|del|que|para|con|una|por|es|son|en|y)$/ };
|
|
1181
|
+
export function guessDeckLang(spec) {
|
|
1182
|
+
const text = JSON.stringify((spec?.slides || []).map(({ notes, consulta, ...s }) => s)).toLowerCase().replace(/"[a-z_]+":/g, " ");
|
|
1183
|
+
const words = text.match(/\p{L}+/gu) || [];
|
|
1184
|
+
const count = Object.fromEntries(Object.entries(LANG_WORDS).map(([k, re]) => [k, words.filter((w) => re.test(w)).length]));
|
|
1185
|
+
const [best, n] = Object.entries(count).sort((a, b) => b[1] - a[1])[0];
|
|
1186
|
+
const second = Object.entries(count).filter(([k]) => k !== best).reduce((m, [, v]) => Math.max(m, v), 0);
|
|
1187
|
+
return n >= 8 && n >= second * 1.5 ? best : null;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
// Slide idêntico a outro (mesmo conteúdo, só o uid diferente) é cópia: o deck novo do ICFM10 saiu com duas capas
|
|
1191
|
+
// iguais, até nas notes (a IA trocou o slide em branco pela capa e inseriu a capa de novo). Fica o primeiro.
|
|
1192
|
+
export function dropDuplicateSlides(spec) {
|
|
1193
|
+
const seen = new Set(), removed = [];
|
|
1194
|
+
const key = (s) => JSON.stringify({ ...s, uid: undefined, auto: undefined });
|
|
1195
|
+
const slides = (spec.slides || []).filter((s, i) => {
|
|
1196
|
+
const k = key(s);
|
|
1197
|
+
if (seen.has(k)) { removed.push(i + 1); return false; }
|
|
1198
|
+
seen.add(k); return true;
|
|
1199
|
+
});
|
|
1200
|
+
return { spec: removed.length ? { ...spec, slides } : spec, removed };
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Checagem dos números (ferramenta de autocrítica): todo número visível do deck que não está no material volta para a
|
|
1204
|
+
// IA conferir. Uma rodada; o que sobrar vai no relatório (facts.unsupported) e no aviso.
|
|
1205
|
+
export async function checkNumbers(spec, { materials = [], edit, say = () => {} } = {}) {
|
|
1206
|
+
const { unsupportedNumbers } = await import("./document-visuals.js");
|
|
1207
|
+
const bad = unsupportedNumbers(spec, materials);
|
|
1208
|
+
if (!bad.length) return { spec, facts: null, actions: [] };
|
|
1209
|
+
say(`${bad.length} número(s) no deck sem base no material; pedindo para conferir…`);
|
|
1210
|
+
let next = spec, actions = [];
|
|
1211
|
+
try {
|
|
1212
|
+
const r = await edit({ spec, instruction: `Checagem de números: estes números aparecem nos slides, mas não estão escritos no material anexado:\n${bad.map((b) => `- slide ${b.slide}: ${b.number}`).join("\n")}\nPara cada um: se for o valor do material com erro de leitura (por exemplo, lido de um gráfico), troque pelo valor que o texto ou a tabela do material traz; se for uma conta sua a partir de valores do material, deixe só se a conta aparecer no slide ou nas notes; se não houver base, tire o número (reescreva a frase sem ele). Não mexa em mais nada.`, repairSlides: [...new Set(bad.map((b) => b.slide - 1))] });
|
|
1213
|
+
if (!r.talk && r.spec?.slides?.length) { next = r.spec; actions = r.actions || []; }
|
|
1214
|
+
} catch (e) { say(`a checagem de números falhou (${e.message}); sigo com o deck como está`); }
|
|
1215
|
+
const still = unsupportedNumbers(next, materials);
|
|
1216
|
+
if (still.length) say(`Números sem base no material ainda no deck: ${still.map((b) => `${b.number} (slide ${b.slide})`).join(", ")}.`);
|
|
1217
|
+
return { spec: next, facts: { checked: bad.length, unsupported: still }, actions };
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
export async function generateDeck(briefing, { theme, slides, duration, style, direction, materials = [], images = true, imageOptions = {}, onProgress, onEvent, drawCheck = null, reviewCheck = null, ask = false, answer = "", author = "", language = "", research = "auto", web = null, researchDir = null, board = null, pendingMaterials = null } = {}) {
|
|
1221
|
+
// onProgress(texto): marcos (CLI) · onEvent({ phase, text, chars, stages }): tudo, inclusive o texto chegando (Studio)
|
|
1222
|
+
// Cada etapa passa pelo quadro de etapas (src/ai/progress.js): o aviso diz em que etapa está. pendingMaterials: o
|
|
1223
|
+
// inventário do documento ainda em curso (o Studio começa a ler as páginas e a gerar ao mesmo tempo); pesquisa e
|
|
1224
|
+
// leitura crítica usam o texto e só a escrita espera o inventário.
|
|
1225
|
+
const { stageBoard } = await import("./progress.js");
|
|
1226
|
+
board ||= stageBoard((ev) => onEvent?.(ev));
|
|
1227
|
+
let stageEmit = null;
|
|
1228
|
+
const say = (text) => { onProgress?.(text); if (stageEmit) stageEmit(text); else onEvent?.({ phase: "step", text }); };
|
|
1229
|
+
const stage = (label, fn) => board.run(label, async (emit) => { const prev = stageEmit; stageEmit = emit; try { return await fn(emit); } finally { stageEmit = prev; } });
|
|
1230
|
+
const extras = []; // fontes da pesquisa e a leitura crítica: vão junto do material para a escrita
|
|
1065
1231
|
const st = style ? styleFor(style) : null;
|
|
1066
1232
|
if (st && !theme) theme = st.theme;
|
|
1067
1233
|
if (st && !direction) direction = st.direction;
|
|
@@ -1069,15 +1235,14 @@ export async function generateDeck(briefing, { theme, slides, duration, style, d
|
|
|
1069
1235
|
// "perguntar quando não souber" (Preferências): uma chamada curta decide para que serve o material (e se precisa perguntar)
|
|
1070
1236
|
let decided = null;
|
|
1071
1237
|
if (ask && !answer) {
|
|
1072
|
-
say("entendendo para que serve o material…");
|
|
1073
|
-
decided = await decidePurpose(briefing, materials).catch(() => null);
|
|
1238
|
+
decided = await stage("Entendendo o pedido", () => { say("entendendo para que serve o material…"); return decidePurpose(briefing, materials).catch(() => null); });
|
|
1074
1239
|
if (decided?.question) return { question: decided.question };
|
|
1075
1240
|
// ocasião internacional sem idioma informado: confirma antes de gerar no idioma errado
|
|
1076
1241
|
if (decided?.languageQuestion && (language === "auto" || !language)) return { question: decided.languageQuestion };
|
|
1077
1242
|
}
|
|
1078
1243
|
// pesquisa: a IA decide se o que ela sabe basta; se não, busca, escolhe as fontes, lê e anota (src/research)
|
|
1079
1244
|
let researchReport;
|
|
1080
|
-
if (research !== false) {
|
|
1245
|
+
if (research !== false) await stage("Pesquisa", async () => {
|
|
1081
1246
|
const R = await import("../research/research.js");
|
|
1082
1247
|
try {
|
|
1083
1248
|
say("vendo se precisa pesquisar…");
|
|
@@ -1089,13 +1254,34 @@ export async function generateDeck(briefing, { theme, slides, duration, style, d
|
|
|
1089
1254
|
const r = off ? { report: { pesquisou: true, motivo: plan.motivo, buscas: plan.buscas, fontes: [], falhas: ["web desligada"], offline: true, data: new Date().toISOString().slice(0, 10) }, materials: [] }
|
|
1090
1255
|
: await R.runResearch(plan, { briefing, web: web || R.defaultWeb, onProgress: say, saveDir: researchDir });
|
|
1091
1256
|
researchReport = r.report;
|
|
1092
|
-
|
|
1257
|
+
extras.push(...r.materials);
|
|
1093
1258
|
say(r.materials.length ? `pesquisa: ${r.materials.length} fonte(s) lida(s) (${r.report.fontes.map((f) => f.site).join(", ")})`
|
|
1094
1259
|
: r.report.offline ? "Sem acesso à internet: não deu para pesquisar. Sigo com o que eu sei, sem inventar dado recente; cole links ou anexe arquivos para eu usar."
|
|
1095
1260
|
: "a pesquisa não achou fonte que desse para ler; sigo com o que eu sei, sem inventar dado recente.");
|
|
1096
1261
|
}
|
|
1097
1262
|
} catch (e) { say(`a pesquisa falhou (${e.message}); sigo com o que eu sei`); }
|
|
1263
|
+
});
|
|
1264
|
+
// Leitura crítica do documento antes de escrever (src/ai/critique.js): inconsistências, achados escondidos,
|
|
1265
|
+
// perguntas prováveis, limitações; cada item com trecho conferido. Fica na pasta da apresentação e vai no pedido;
|
|
1266
|
+
// o uso (slide × notes) segue a autoria. Falhou: segue sem ela.
|
|
1267
|
+
let critique = null;
|
|
1268
|
+
{
|
|
1269
|
+
const C = await import("./critique.js");
|
|
1270
|
+
if (C.worthCritique([...materials, ...extras])) await stage("Leitura crítica", async () => {
|
|
1271
|
+
say("lendo o material com olho crítico…");
|
|
1272
|
+
try {
|
|
1273
|
+
critique = await C.critiqueMaterials([...materials, ...extras], { briefing });
|
|
1274
|
+
C.saveCritique(imageOptions.baseDir || researchDir, critique);
|
|
1275
|
+
const cm = C.critiqueMaterial(critique);
|
|
1276
|
+
if (cm) extras.push(cm);
|
|
1277
|
+
say(`leitura crítica: ${critique.itens.length} achado(s) com trecho conferido${critique.naoConfirmados.length ? `, ${critique.naoConfirmados.length} descartado(s) sem trecho no material` : ""}`);
|
|
1278
|
+
} catch (e) { say(`a leitura crítica falhou (${e.message}); sigo sem ela`); }
|
|
1279
|
+
});
|
|
1098
1280
|
}
|
|
1281
|
+
// o inventário do documento (figuras, tabelas, equações) corria junto: a escrita precisa dele
|
|
1282
|
+
if (pendingMaterials) materials = await stage("Lendo o documento", async (emit) => { emit("terminando a leitura das páginas…"); return pendingMaterials; });
|
|
1283
|
+
materials = [...materials, ...extras];
|
|
1284
|
+
const fromDocument = (materials || []).some((m) => m?.inventory);
|
|
1099
1285
|
const wishes = [
|
|
1100
1286
|
researchReport ? (await import("../research/research.js")).researchInstruction(researchReport) : "",
|
|
1101
1287
|
theme ? `Use o tema "${theme}".` : "Escolha o tema que combina com o assunto.",
|
|
@@ -1105,7 +1291,10 @@ export async function generateDeck(briefing, { theme, slides, duration, style, d
|
|
|
1105
1291
|
decided?.purpose ? `Para que serve o material (já decidido): purpose: ${decided.purpose}${decided.why ? ` — ${decided.why}` : ""}.` : "",
|
|
1106
1292
|
decided?.texto ? `A pessoa disse quanto texto quer: ${decided.texto} texto na tela (grave maxWords: ${decided.texto === "muito" ? 200 : 35}).` : "",
|
|
1107
1293
|
answer ? `Resposta da pessoa à sua pergunta sobre o material: ${answer}` : "",
|
|
1108
|
-
|
|
1294
|
+
// documento anexado tem autores próprios: o autor das Preferências (quem usa o sagadeck) não assina o artigo dos
|
|
1295
|
+
// outros (a capa do ICFM10 saiu "Narumi Abe" no lugar de Maria Clara Fava)
|
|
1296
|
+
author && !fromDocument ? `Autor padrão das preferências: ${author}. É apenas um padrão; autoria ou organização explicitamente indicada no pedido tem prioridade. Grave a atribuição solicitada em author para não aplicar este padrão.` : "",
|
|
1297
|
+
author && fromDocument ? `Quem usa o sagadeck (Preferências): ${author}. NÃO é o autor do documento anexado: o deck credita os autores do documento; use este nome só se o pedido disser que é quem apresenta.` : "",
|
|
1109
1298
|
// idioma respondido ou extraído do pedido vale como instrução (inclui a resposta à confirmação de idioma)
|
|
1110
1299
|
/english|ingl[eê]s/i.test(answer || "") || (!language || language === "auto") && decided?.idioma === "en" ? "Escreva todo o conteúdo em inglês."
|
|
1111
1300
|
: /portuguese|portugu[eê]s/i.test(answer || "") || (!language || language === "auto") && decided?.idioma === "pt" ? "Escreva todo o conteúdo em português."
|
|
@@ -1121,15 +1310,17 @@ ${briefing}
|
|
|
1121
1310
|
"""
|
|
1122
1311
|
|
|
1123
1312
|
Faça agora, sem oferecer versões. Só pergunte se não der mesmo para saber o que ela quer.`;
|
|
1124
|
-
say("pedindo o deck ao LLM…");
|
|
1125
1313
|
const creationOptions = { images, maxImages: 8, deferImages: true, drawCheck, materials, onProgress: onEvent };
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1314
|
+
const inStage = (o) => ({ ...creationOptions, ...o, onProgress: (ev) => (stageEmit || onEvent || (() => {}))(ev) });
|
|
1315
|
+
const r = await stage("Escrevendo a apresentação", async () => {
|
|
1316
|
+
say("pedindo o deck ao LLM…");
|
|
1317
|
+
try { return await editDeck(inStage({ spec: starter, instruction })); }
|
|
1318
|
+
catch (error) {
|
|
1319
|
+
if (error.code !== 'AI_TOKEN_LIMIT') throw error;
|
|
1320
|
+
const { stagedGeneration } = await import('./staged-generation.js');
|
|
1321
|
+
return stagedGeneration(briefing, { starter, wishes, edit: editDeck, options: inStage({}), say });
|
|
1322
|
+
}
|
|
1323
|
+
});
|
|
1133
1324
|
// a IA preferiu perguntar (como faria no chat): a pergunta volta para a pessoa
|
|
1134
1325
|
if (r.talk) return { question: { question: r.reply, options: r.options || [] } };
|
|
1135
1326
|
let spec = r.spec;
|
|
@@ -1139,18 +1330,30 @@ Faça agora, sem oferecer versões. Só pergunte se não der mesmo para saber o
|
|
|
1139
1330
|
}
|
|
1140
1331
|
if (spec.slides.length < 2 && spec.slides[0]?.title === "Nova apresentação") throw new Error("A IA não criou a apresentação. Tente de novo ou descreva com mais detalhe.");
|
|
1141
1332
|
(r.actions || []).filter((x) => /corrigido|não enxerga/.test(x)).forEach(say);
|
|
1333
|
+
// Cobertura do material: figura/tabela numerada do documento que nenhum slide usa volta para a IA decidir onde
|
|
1334
|
+
// entra (ou dizer por que fica de fora, quando o pedido limitou a seleção). Era o "muitas figuras são ignoradas".
|
|
1335
|
+
const covered = await stage("Conferindo as figuras do material", () => coverDocumentVisuals(spec, { materials, briefing, say, edit: (o) => editDeck(inStage(o)) }));
|
|
1336
|
+
spec = covered.spec;
|
|
1337
|
+
const coverage = covered.coverage;
|
|
1338
|
+
const checked = await stage("Conferindo os números", () => checkNumbers(spec, { materials, say, edit: (o) => editDeck(inStage(o)) }));
|
|
1339
|
+
spec = checked.spec;
|
|
1340
|
+
const facts = checked.facts;
|
|
1341
|
+
|
|
1142
1342
|
// a quantidade de texto que a pessoa pediu com todas as letras vale mesmo se o modelo esquecer de gravar
|
|
1143
1343
|
if (decided?.texto && !spec.maxWords) spec.maxWords = decided.texto === "muito" ? 200 : 35;
|
|
1144
|
-
if (author && !spec.author) spec.author = author;
|
|
1344
|
+
if (author && !spec.author && !fromDocument) spec.author = author;
|
|
1145
1345
|
if (!spec.date) spec.date = new Date().toISOString().slice(0, 10); // a data de criação, nunca uma inventada
|
|
1346
|
+
// idioma do deck: números, hifenização e textos automáticos seguem `lang` (pt-BR é o padrão sem nada gravado)
|
|
1347
|
+
if (!spec.lang) { const g = guessDeckLang(spec); if (g && g !== "pt") spec.lang = g; }
|
|
1146
1348
|
if (spec.title === "Nova apresentação" && spec.slides[0]?.title && spec.slides[0].title !== "Nova apresentação") spec.title = String(spec.slides[0].title).replace(/[=*_`]/g, "");
|
|
1147
|
-
const
|
|
1349
|
+
{ const d = dropDuplicateSlides(spec); if (d.removed.length) { spec = d.spec; say(`slide(s) repetido(s) removido(s): ${d.removed.join(", ")}`); } }
|
|
1350
|
+
const imgs = await stage("Imagens", () => materializeImages(spec, images ? { ...imageOptions, onProgress: say } : { max: 0 }));
|
|
1148
1351
|
// foco guiado sobre uma imagem: a visão põe cada destaque no lugar
|
|
1149
1352
|
try { const { groundSpotlights } = await import("./ground.js"); await groundSpotlights(spec.slides, { baseDir: imageOptions.baseDir || spec._dir, onProgress: say }); } catch {}
|
|
1150
1353
|
let quality;
|
|
1151
|
-
if (reviewCheck) {
|
|
1354
|
+
if (reviewCheck) await stage("Revisão dos slides", async (emit) => {
|
|
1152
1355
|
say('Conferindo a apresentação e os estados interativos…');
|
|
1153
|
-
quality = await reviewCheck(spec, spec.slides.map((_, i) => i));
|
|
1356
|
+
quality = await reviewCheck(spec, spec.slides.map((_, i) => i), { onProgress: emit });
|
|
1154
1357
|
// problema idêntico ao de rodada anterior = tentativa sem efeito: não queima token de novo
|
|
1155
1358
|
const attempted = new Set();
|
|
1156
1359
|
for (let round = 0; quality.issues.length && round < 3; round++) {
|
|
@@ -1158,8 +1361,8 @@ Faça agora, sem oferecer versões. Só pergunte se não der mesmo para saber o
|
|
|
1158
1361
|
if (!fresh.length) { say('Revisão: nada novo para corrigir; encerro as tentativas.'); break; }
|
|
1159
1362
|
fresh.forEach(x => attempted.add(JSON.stringify(x)));
|
|
1160
1363
|
try {
|
|
1161
|
-
const repaired = await editDeck({ spec, instruction: `Corrija somente os problemas observados. Não elimine recursos explicitamente pedidos para resolver uma colisão: reposicione, redimensione ou reorganize a composição mantendo a função. Preserve conteúdo e ordem do briefing (${briefing}): ${JSON.stringify(fresh)}.`, images, imageOptions, materials, drawCheck, reviewDepth: 1, repairSlides: fresh.map(x => x.slide - 1), onProgress:
|
|
1162
|
-
const nextQuality = await reviewCheck(repaired.spec, repaired.spec.slides.map((_, i) => i));
|
|
1364
|
+
const repaired = await editDeck({ spec, instruction: `Corrija somente os problemas observados. Não elimine recursos explicitamente pedidos para resolver uma colisão: reposicione, redimensione ou reorganize a composição mantendo a função. Preserve conteúdo e ordem do briefing (${briefing}): ${JSON.stringify(fresh)}.`, images, imageOptions, materials, drawCheck, reviewDepth: 1, repairSlides: fresh.map(x => x.slide - 1), onProgress: emit });
|
|
1365
|
+
const nextQuality = await reviewCheck(repaired.spec, repaired.spec.slides.map((_, i) => i), { onProgress: emit });
|
|
1163
1366
|
spec = repaired.spec; quality = nextQuality;
|
|
1164
1367
|
} catch (error) {
|
|
1165
1368
|
quality = { ...quality, verified: false, failures: [...(quality.failures || []), { stage: 'repair', reason: error.message }] };
|
|
@@ -1168,8 +1371,9 @@ Faça agora, sem oferecer versões. Só pergunte se não der mesmo para saber o
|
|
|
1168
1371
|
}
|
|
1169
1372
|
}
|
|
1170
1373
|
if (!quality.verified) say(`Revisão incompleta: ${quality.issues.length} problemas e ${quality.unchecked.length} slides sem conferência.`);
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1374
|
+
});
|
|
1375
|
+
spec = dropDuplicateSlides(spec).spec; // a correção visual também pode duplicar
|
|
1376
|
+
return { spec: publicSpec(spec), images: imgs, direction, variety: varietyReport(spec), ...(quality ? { quality } : {}), ...(coverage ? { coverage } : {}), ...(facts ? { facts } : {}), ...(critique ? { critique: { itens: critique.itens.length, naoConfirmados: critique.naoConfirmados.length } } : {}), ...(researchReport ? { research: researchReport } : {}) };
|
|
1173
1377
|
}
|
|
1174
1378
|
|
|
1175
1379
|
export function toYaml(spec) {
|
|
@@ -23,13 +23,13 @@ export async function prepareDocumentMaterials(materials, dir, options = {}) {
|
|
|
23
23
|
if (fs.existsSync(cached)) {
|
|
24
24
|
try {
|
|
25
25
|
const saved = JSON.parse(fs.readFileSync(cached,'utf8'));
|
|
26
|
-
if (saved.visualVersion ===
|
|
26
|
+
if (saved.visualVersion === 6 && !saved.inventory?.items?.some(item => item.needsReview)) { out.push(saved); continue; }
|
|
27
27
|
} catch { /* refaz inventário interrompido */ }
|
|
28
28
|
}
|
|
29
29
|
fs.mkdirSync(folder,{recursive:true});
|
|
30
30
|
fs.writeFileSync(path.join(folder,name),bytes);
|
|
31
31
|
const inventory = /\.(pdf|docx?)$/i.test(name) ? await extractDocumentVisuals(name,bytes,dir,options) : null;
|
|
32
|
-
const metadata = {name,text:material.text,detail:material.detail,visualVersion:
|
|
32
|
+
const metadata = {name,text:material.text,detail:material.detail,visualVersion:6,...(inventory ? {inventory} : {})};
|
|
33
33
|
fs.writeFileSync(path.join(folder,'material.saga.json'),JSON.stringify(metadata));
|
|
34
34
|
out.push(metadata);
|
|
35
35
|
}
|