sagadeck 1.3.1 → 1.4.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/docs/ANDAMENTO.md +91 -0
- package/docs/REFERENCIA.md +3 -3
- package/package.json +1 -1
- package/src/ai/context.js +8 -1
- package/src/ai/deck-ai.js +96 -2
- package/src/ai/document-materials.js +2 -2
- package/src/ai/document-visuals.js +396 -20
- package/src/ai/llm.js +19 -5
- package/src/ai/quality.js +32 -0
- package/src/build.js +2 -1
- package/src/elements.js +35 -5
- package/src/import/crop.js +27 -8
- package/src/layouts.js +21 -1
- package/src/runtime/base.css +3 -0
- package/src/runtime/fit.js +20 -1
- package/src/studio/ai-chat-route.js +20 -1
package/src/build.js
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from "node:url";
|
|
|
10
10
|
import YAML from "yaml";
|
|
11
11
|
import { resolveTheme, themeCSS, scopedThemeCSS } from "./themes.js";
|
|
12
12
|
import { identityFor, identitiesFile } from "./identity.js";
|
|
13
|
-
import { LAYOUTS, SCENES } from "./layouts.js";
|
|
13
|
+
import { LAYOUTS, SCENES, fullAsFigure } from "./layouts.js";
|
|
14
14
|
import { applyVisualEdits } from "./visual-edits.js";
|
|
15
15
|
import { el, imageSrc } from "./elements.js";
|
|
16
16
|
import { iconSVG } from "./figures/icons.js";
|
|
@@ -38,6 +38,7 @@ const drawIcon = (name, size = 20) => iconSVG(name, { size, stroke: 2 }).replace
|
|
|
38
38
|
const slug = (s) => String(s || "deck").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 40) || "deck";
|
|
39
39
|
|
|
40
40
|
export function inferLayout(s) {
|
|
41
|
+
if (fullAsFigure(s)) return "image"; // figura do documento em full: sem véu e sem sangrar (src/layouts.js)
|
|
41
42
|
if (s.layout) return s.layout;
|
|
42
43
|
if (s.elements) return "canvas";
|
|
43
44
|
if (s.stats || s.kpis) return "stats";
|
package/src/elements.js
CHANGED
|
@@ -116,13 +116,20 @@ export function figureHTML(el, ctx, w, h) {
|
|
|
116
116
|
const grown = matchCropAspect(el.crop, imageSize(el.image, ctx), w, h);
|
|
117
117
|
const c = { l: +grown.l || 0, t: +grown.t || 0, r: +grown.r || 0, b: +grown.b || 0 };
|
|
118
118
|
const fw = Math.max(0.01, 1 - c.l - c.r), fh = Math.max(0.01, 1 - c.t - c.b);
|
|
119
|
-
|
|
119
|
+
// data-crop: a apresentação mede a caixa de verdade e, se a proporção não bater com a da região (caixa sem
|
|
120
|
+
// tamanho aqui, janela que não pôde crescer), encaixa a região inteira sem esticar (src/runtime/fit.js: fitCrops)
|
|
121
|
+
return `<div${attrs(el, "fig fig-img fig-crop")}><img src="${src}" alt="${esc(el.alt || "")}" data-crop="${[c.l, c.t, c.r, c.b].map((v) => +v.toFixed(5)).join(",")}" style="left:${(-c.l / fw) * 100}%;top:${(-c.t / fh) * 100}%;width:${100 / fw}%;height:${100 / fh}%;${flip}"></div>`;
|
|
120
122
|
}
|
|
121
123
|
// sem fit escolhido: preenche (cover), mas quando a proporção da imagem é muito diferente da caixa (fórmula, esquema
|
|
122
124
|
// largo numa coluna alta; gráfico com eixo e legenda na borda) cortar perderia informação: passou de 1,25× (corte de
|
|
123
125
|
// mais de 20%), cabe inteira (contain). Sem o tamanho da caixa aqui (figura do split, do layout), a mesma conta é feita
|
|
124
126
|
// na apresentação, com a caixa medida (data-autofit; src/runtime/fit.js: fitImages)
|
|
125
127
|
let fit = el.fit;
|
|
128
|
+
// figura recortada do documento (mapa, gráfico, tabela do paper) é evidência: nunca deforma nem perde borda.
|
|
129
|
+
// fill/stretch achatavam o mapa na caixa do slide, e o cover automático (até 20% de corte) comia eixo, legenda e
|
|
130
|
+
// escala; vira contain (inteira, na proporção do original)
|
|
131
|
+
const docImage = /contexto\/visuais\//.test(String(el.image || "").replace(/\\/g, "/"));
|
|
132
|
+
if (docImage && (!fit || /^(fill|stretch|none|cover)$/i.test(String(fit)))) fit = "contain";
|
|
126
133
|
if (!fit) {
|
|
127
134
|
const sz = w && h ? imageSize(el.image, ctx) : null;
|
|
128
135
|
const box = w && h ? w / h : 0, img = sz && sz.h ? sz.w / sz.h : 0;
|
|
@@ -141,7 +148,7 @@ export function figureHTML(el, ctx, w, h) {
|
|
|
141
148
|
if (pct) { box = { ...el }; delete box.w; }
|
|
142
149
|
}
|
|
143
150
|
}
|
|
144
|
-
return `<div${attrs(box, "fig fig-img", flow)}><img src="${src}" alt="${esc(el.alt || "")}"${el.fit ? "" : " data-autofit"} style="object-fit:${fit};${el.radius ? `border-radius:${el.radius}px;` : ""}${flip}"></div>`;
|
|
151
|
+
return `<div${attrs(box, "fig fig-img", flow)}><img src="${src}" alt="${esc(el.alt || "")}"${el.fit || docImage ? "" : " data-autofit"} style="object-fit:${fit};${el.radius ? `border-radius:${el.radius}px;` : ""}${flip}"></div>`;
|
|
145
152
|
}
|
|
146
153
|
return "";
|
|
147
154
|
}
|
|
@@ -374,8 +381,31 @@ export function importedTable(e) {
|
|
|
374
381
|
return `<div${attrs(e, "tbx-table")}><table style="border-collapse:collapse;table-layout:fixed;width:100%"><colgroup>${cols}</colgroup>${rows}</table></div>`;
|
|
375
382
|
}
|
|
376
383
|
|
|
384
|
+
// O contador reescreve o texto no navegador (textContent): marcação de expoente/índice no prefixo/sufixo ("km^2^",
|
|
385
|
+
// "CO~2~") vira o algarismo sobrescrito/subscrito do Unicode, que sobrevive ao textContent (saía "km^2^" cru).
|
|
386
|
+
const SUP = { 0: "⁰", 1: "¹", 2: "²", 3: "³", 4: "⁴", 5: "⁵", 6: "⁶", 7: "⁷", 8: "⁸", 9: "⁹", "-": "⁻", "+": "⁺" };
|
|
387
|
+
const SUB = { 0: "₀", 1: "₁", 2: "₂", 3: "₃", 4: "₄", 5: "₅", 6: "₆", 7: "₇", 8: "₈", 9: "₉", "-": "₋", "+": "₊" };
|
|
388
|
+
export const unicodeScripts = (t) => String(t ?? "").replace(/\^([-+]?\d+)\^/g, (_, d) => [...d].map((c) => SUP[c]).join("")).replace(/~([-+]?\d+)~/g, (_, d) => [...d].map((c) => SUB[c]).join(""));
|
|
389
|
+
// Valor do contador escrito como texto ("37,67%", "1.403", "R$ 2,5 mi", "1521 km^2^"): Number() dava NaN e o slide
|
|
390
|
+
// mostrava "NaN". Separa o que vem antes/depois (prefixo/sufixo) e lê o número no formato brasileiro (milhar com
|
|
391
|
+
// ponto, decimal com vírgula); as casas decimais escritas valem. Sem número nenhum, o texto aparece como está.
|
|
392
|
+
export function parseCounterValue(raw) {
|
|
393
|
+
if (typeof raw === "number") return { value: raw };
|
|
394
|
+
const m = /^(\D*?)(-?\d[\d.,]*)(.*)$/s.exec(String(raw ?? "").trim());
|
|
395
|
+
if (!m) return null;
|
|
396
|
+
let num = m[2].replace(/[.,]$/, ""), dec = 0;
|
|
397
|
+
if (num.includes(",")) { dec = num.split(",").pop().length; num = num.replace(/\./g, "").replace(",", "."); }
|
|
398
|
+
else if (/^-?\d{1,3}(\.\d{3})+$/.test(num)) num = num.replace(/\./g, "");
|
|
399
|
+
else if (num.includes(".")) dec = num.split(".").pop().length;
|
|
400
|
+
const value = Number(num);
|
|
401
|
+
return Number.isFinite(value) ? { value, prefix: m[1], suffix: m[3] + (m[2].length > num.length && /[.,]$/.test(m[2]) ? m[2].slice(-1) : ""), dec } : null;
|
|
402
|
+
}
|
|
403
|
+
|
|
377
404
|
export function counter(e) {
|
|
378
|
-
const
|
|
405
|
+
const parsed = parseCounterValue(e.counter);
|
|
406
|
+
if (!parsed) return `<div${attrs(e, `t f-display r-number pl`, `font-size:${e.size || SIZES[e.as || "number"]}px;`)}${e.fit ? " data-fit" : ""}>${md(String(e.counter ?? ""))}</div>`;
|
|
407
|
+
e = { ...e, prefix: unicodeScripts(`${e.prefix ?? ""}${parsed.prefix ?? ""}`), suffix: unicodeScripts(`${parsed.suffix ?? ""}${e.suffix ?? ""}`), decimals: e.decimals ?? (typeof e.counter === "number" ? undefined : parsed.dec) };
|
|
408
|
+
const v = parsed.value;
|
|
379
409
|
const dec = e.decimals ?? (v % 1 ? 1 : 0);
|
|
380
410
|
const shown = v.toLocaleString("pt-BR", { minimumFractionDigits: dec, maximumFractionDigits: dec });
|
|
381
411
|
const size = e.size || SIZES[e.as || "number"];
|
|
@@ -447,7 +477,7 @@ export function cards(e, ctx) {
|
|
|
447
477
|
${badgeHtml}
|
|
448
478
|
</div>
|
|
449
479
|
${c.title ? `<div class="cd-title t f-heading">${md(c.title)}</div>` : ""}
|
|
450
|
-
${c.text ? `<div class="cd-text t f-body">${md(c.text)}</div>` : ""}
|
|
480
|
+
${c.text ? `<div class="cd-text t f-body${/^\s*\$\$?[^$]+\$\$?\s*$/.test(String(c.text)) ? " cd-math" : ""}">${md(c.text)}</div>` : ""}
|
|
451
481
|
${extraWidgets}
|
|
452
482
|
${c.foot ? `<div class="cd-foot t f-label">${md(c.foot)}</div>` : ""}
|
|
453
483
|
${c.goto != null && c.goto !== "" ? `<span class="goto-mark" aria-hidden="true">${iconSVG("arrow-up-right", { size: 30, stroke: 2 })}</span>` : ""}
|
|
@@ -476,7 +506,7 @@ export function stats(e, ctx) {
|
|
|
476
506
|
${iconHtml}
|
|
477
507
|
${trendHtml}
|
|
478
508
|
</div>
|
|
479
|
-
<div class="st-val t f-display" style="${valColor}">${
|
|
509
|
+
<div class="st-val t f-display" style="${valColor}">${md(String(st.value ?? st.stat ?? ""))}</div>
|
|
480
510
|
${st.label ? `<div class="st-lab t f-heading">${md(st.label)}</div>` : ""}
|
|
481
511
|
${st.text ? `<div class="st-sub t f-body">${md(st.text)}</div>` : ""}
|
|
482
512
|
</div>`;
|
package/src/import/crop.js
CHANGED
|
@@ -45,21 +45,40 @@ export async function cropRegions(jobs) {
|
|
|
45
45
|
const outs = await page.evaluate(async ([data, list]) => {
|
|
46
46
|
const img = new Image(); img.src = data; await img.decode();
|
|
47
47
|
return list.map((j) => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// densidade de tinta (fração de pixels não-papel): recorte quase em branco = caixa errada
|
|
48
|
+
let x = Math.max(0, Math.floor(j.x)), y = Math.max(0, Math.floor(j.y));
|
|
49
|
+
let w = Math.max(1, Math.min(img.width - x, Math.ceil(j.w))), h = Math.max(1, Math.min(img.height - y, Math.ceil(j.h)));
|
|
50
|
+
let c = document.createElement("canvas"); c.width = w; c.height = h;
|
|
51
|
+
let g = c.getContext("2d", { willReadFrequently: true }); g.drawImage(img, x, y, w, h, 0, 0, w, h);
|
|
52
|
+
// densidade de tinta (fração de pixels não-papel): recorte quase em branco = caixa errada (medida antes de aparar)
|
|
53
53
|
let ink = 0;
|
|
54
54
|
try {
|
|
55
55
|
const d = g.getImageData(0, 0, w, h).data;
|
|
56
56
|
for (let i = 0; i < d.length; i += 16) { if ((d[i] + d[i + 1] + d[i + 2]) / 3 < 235) ink++; }
|
|
57
57
|
ink /= d.length / 16;
|
|
58
58
|
} catch { ink = 1; }
|
|
59
|
-
|
|
59
|
+
// trim: apara a margem de papel em volta (linha/coluna inteira branca), com um respiro de 6 px.
|
|
60
|
+
// Só papel puro sai: céu claro numa foto ou fundo cinza de gráfico não é branco puro em toda a linha.
|
|
61
|
+
if (j.trim && w > 24 && h > 24) {
|
|
62
|
+
const d = g.getImageData(0, 0, w, h).data;
|
|
63
|
+
const paper = (i) => d[i] >= 246 && d[i + 1] >= 246 && d[i + 2] >= 246;
|
|
64
|
+
const rowBlank = (r) => { for (let q = 0; q < w; q++) if (!paper((r * w + q) * 4)) return false; return true; };
|
|
65
|
+
const colBlank = (q, t0, t1) => { for (let r = t0; r <= t1; r++) if (!paper((r * w + q) * 4)) return false; return true; };
|
|
66
|
+
let t = 0, b = h - 1, l = 0, r = w - 1;
|
|
67
|
+
while (t < b && rowBlank(t)) t++;
|
|
68
|
+
while (b > t && rowBlank(b)) b--;
|
|
69
|
+
while (l < r && colBlank(l, t, b)) l++;
|
|
70
|
+
while (r > l && colBlank(r, t, b)) r--;
|
|
71
|
+
const pad = 6, nl = Math.max(0, l - pad), nt = Math.max(0, t - pad), nw = Math.min(w, r + pad + 1) - nl, nh = Math.min(h, b + pad + 1) - nt;
|
|
72
|
+
if (r > l && b > t && (nw < w || nh < h)) {
|
|
73
|
+
const c2 = document.createElement("canvas"); c2.width = nw; c2.height = nh;
|
|
74
|
+
const g2 = c2.getContext("2d", { willReadFrequently: true }); g2.drawImage(c, nl, nt, nw, nh, 0, 0, nw, nh);
|
|
75
|
+
c = c2; g = g2; w = nw; h = nh;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return { png: c.toDataURL("image/png"), ink, size: { w, h } };
|
|
60
79
|
});
|
|
61
|
-
}, [data, list.map(({ x, y, w, h }) => ({ x, y, w, h }))]);
|
|
62
|
-
outs.forEach((u, k) => { fs.writeFileSync(list[k].dst, Buffer.from(u.png.split(",")[1], "base64")); list[k].ink = u.ink; done.push(list[k].dst); });
|
|
80
|
+
}, [data, list.map(({ x, y, w, h, trim }) => ({ x, y, w, h, trim }))]);
|
|
81
|
+
outs.forEach((u, k) => { fs.writeFileSync(list[k].dst, Buffer.from(u.png.split(",")[1], "base64")); list[k].ink = u.ink; list[k].size = u.size; done.push(list[k].dst); });
|
|
63
82
|
}
|
|
64
83
|
} finally { await browser.close(); }
|
|
65
84
|
return done;
|
package/src/layouts.js
CHANGED
|
@@ -97,6 +97,20 @@ function chartsSideBySide(content) {
|
|
|
97
97
|
return out;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
// `full` com figura do documento (mapa, gráfico, tabela recortada do paper) ou figura inteira (fit: contain) e texto:
|
|
101
|
+
// o véu por cima cobria a legenda do mapa e os eixos, e a figura sangrava cortando a borda. Evidência não leva texto
|
|
102
|
+
// por cima nem sangra: vira a composição do layout image (título no alto, figura inteira, legenda embaixo). Para
|
|
103
|
+
// figura do documento até overlay: none com título vale assim (senão título e legenda sumiam do slide).
|
|
104
|
+
export function fullAsFigure(s) {
|
|
105
|
+
if (s?.layout !== "full") return false;
|
|
106
|
+
const f = s.figure ?? (s.image ? { image: s.image } : null);
|
|
107
|
+
const fig = typeof f === "string" ? { image: f } : f;
|
|
108
|
+
if (!fig || typeof fig !== "object" || !fig.image || fig.image_prompt || fig.video) return false;
|
|
109
|
+
if (!(s.title || s.caption || s.kicker)) return false;
|
|
110
|
+
const doc = /contexto\/visuais\//.test(String(fig.image).replace(/\\/g, "/"));
|
|
111
|
+
return doc || ((fig.fit || s.fit) === "contain" && s.overlay !== "none");
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
export const LAYOUTS = {
|
|
101
115
|
portal(s,ctx){return portalHTML(s,ctx,head)+src(s)+add(s,ctx);},
|
|
102
116
|
playground(s,ctx){return playgroundHTML(s,ctx,head)+src(s)+add(s,ctx);},
|
|
@@ -439,6 +453,7 @@ export const LAYOUTS = {
|
|
|
439
453
|
},
|
|
440
454
|
|
|
441
455
|
image(s, ctx) {
|
|
456
|
+
if (typeof s.figure === "string") s = { ...s, figure: { image: s.figure } }; // full com figure: caminho (fullAsFigure)
|
|
442
457
|
// figura inteira à vista (fit: contain: gráfico, tabela, esquema): a legenda vai embaixo, sem cobrir; foto (cover)
|
|
443
458
|
// segue sangrando, com o cartão por cima
|
|
444
459
|
const contain = (s.figure?.fit || s.fit) === "contain" || (!s.fit&&!s.figure?.fit&&String(s.figure?.image||s.image||'').replace(/\\/g,'/').includes('contexto/visuais/'));
|
|
@@ -536,6 +551,7 @@ export const LAYOUTS = {
|
|
|
536
551
|
const f = s.figure ?? (s.image || s.image_prompt ? { image: s.image, image_prompt: s.image_prompt } : null);
|
|
537
552
|
const fig = f == null ? "" : typeof f === "object" ? { fit: s.fit || "cover", ...f } : { image: f, fit: s.fit || "cover" };
|
|
538
553
|
const hasText = s.title || s.caption || s.kicker;
|
|
554
|
+
if (fullAsFigure(s)) return LAYOUTS.image({ ...s, figure: { ...fig, fit: "contain" }, fit: undefined }, ctx);
|
|
539
555
|
const pos = s.overlay || (hasText ? "bottom" : "none");
|
|
540
556
|
// figura desenhada (gráfico, ícone, diagrama) com texto à esquerda: vai para a direita em vez de ficar por baixo do texto
|
|
541
557
|
const drawn = fig && !(fig.image || fig.image_prompt || fig.video);
|
|
@@ -545,7 +561,11 @@ export const LAYOUTS = {
|
|
|
545
561
|
|
|
546
562
|
// Manchete: uma frase enorme que ocupa o slide (encolhe para caber)
|
|
547
563
|
headline(s, ctx) {
|
|
548
|
-
|
|
564
|
+
// manchete é para frase curta; frase longa em "hero" de 300 px virava o slide de uma frase gigante (objetivo
|
|
565
|
+
// inteiro do paper na tela). Como no statement, o tamanho segue o comprimento; `as`/`size` explícitos vencem.
|
|
566
|
+
const chars = String(s.text || s.title || "").replace(/[=*_`^~]/g, "").length;
|
|
567
|
+
const role = s.as || (chars > 100 ? "h2" : chars > 60 ? "title" : "hero");
|
|
568
|
+
return `<div class="L-headline">${kicker(s)}${text(s.text || s.title, role, { class: "hl-text e", style: "--d:1;", fit: true, size: s.size || (role === "hero" ? 300 : undefined) })}
|
|
549
569
|
${s.caption ? text(s.caption, "lead", { class: "hl-cap muted e", style: "--d:2;" }) : ""}</div>${src(s)}${add(s, ctx)}`;
|
|
550
570
|
},
|
|
551
571
|
|
package/src/runtime/base.css
CHANGED
|
@@ -247,6 +247,9 @@ code.f-mono{font-size:.86em;background:color-mix(in srgb,var(--fg) 9%,transparen
|
|
|
247
247
|
.cd-num{font-size:110px;line-height:.9}
|
|
248
248
|
.cd-title{font-size:52px}
|
|
249
249
|
.cd-text{font-size:33px;color:var(--muted)}
|
|
250
|
+
/* cartão cujo texto é só uma fórmula: a fórmula é o conteúdo, não nota de rodapé (no tamanho do texto ela sumia) */
|
|
251
|
+
.cd-text.cd-math{font-size:52px;color:var(--fg);margin:.15em 0}
|
|
252
|
+
.card-hi .cd-text.cd-math{color:var(--on-hi)}
|
|
250
253
|
.card-hi .cd-text{color:var(--on-hi)}
|
|
251
254
|
.cd-foot{margin-top:auto;font-size:20px;color:var(--muted)}
|
|
252
255
|
|
package/src/runtime/fit.js
CHANGED
|
@@ -225,6 +225,24 @@
|
|
|
225
225
|
});
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
// Recorte (crop): a região vira a caixa por porcentagem, o que só é proporcional quando a caixa tem a proporção da
|
|
229
|
+
// região. Medida a caixa de verdade, se não bater (mais de 3%), a região inteira é encaixada no centro (como
|
|
230
|
+
// contain), em vez de esticar a figura na largura ou na altura.
|
|
231
|
+
function fitCrops(root) {
|
|
232
|
+
root.querySelectorAll(".fig-crop img[data-crop]").forEach((img) => {
|
|
233
|
+
if (!img.naturalWidth) { if (!img.dataset.cropWait) { img.dataset.cropWait = "1"; img.addEventListener("load", () => fitCrops(img.parentElement || img), { once: true }); } return; }
|
|
234
|
+
const box = img.parentElement, W = box.clientWidth, H = box.clientHeight;
|
|
235
|
+
if (!W || !H) return;
|
|
236
|
+
const [l, t, r, b] = img.dataset.crop.split(",").map(Number);
|
|
237
|
+
const fw = Math.max(0.01, 1 - l - r), fh = Math.max(0.01, 1 - t - b);
|
|
238
|
+
const region = (fw * img.naturalWidth) / (fh * img.naturalHeight);
|
|
239
|
+
if (Math.abs(region - W / H) / (W / H) < 0.03) return;
|
|
240
|
+
const rw = region > W / H ? W : H * region, rh = rw / region; // a região inteira, sem esticar
|
|
241
|
+
const iw = rw / fw, ih = rh / fh;
|
|
242
|
+
Object.assign(img.style, { width: `${iw}px`, height: `${ih}px`, left: `${(W - rw) / 2 - l * iw}px`, top: `${(H - rh) / 2 - t * ih}px` });
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
228
246
|
// Passos do exercício resolvido (solution): a coluna é alinhada pelo fim (o passo novo embaixo) e o que sobra vaza por
|
|
229
247
|
// cima, apagando. Antes de cortar, encolhe (até 60%) para os passos caberem inteiros. Quadro escondido não mede: o
|
|
230
248
|
// runtime chama de novo a cada clique.
|
|
@@ -257,6 +275,7 @@
|
|
|
257
275
|
function fitAllIn(root) {
|
|
258
276
|
fitStages(root);
|
|
259
277
|
fitImages(root);
|
|
278
|
+
fitCrops(root);
|
|
260
279
|
fitSteps(root);
|
|
261
280
|
fitChartText(root);
|
|
262
281
|
fitCode(root);
|
|
@@ -264,5 +283,5 @@
|
|
|
264
283
|
fitText(root);
|
|
265
284
|
}
|
|
266
285
|
|
|
267
|
-
g.SagadeckFit = { fitText: fitAllIn, fitChartText, fitCode, fitWide, fitImages, fitSteps, fitStages, shrink };
|
|
286
|
+
g.SagadeckFit = { fitText: fitAllIn, fitChartText, fitCode, fitWide, fitImages, fitCrops, fitSteps, fitStages, shrink };
|
|
268
287
|
})(typeof window !== "undefined" ? window : globalThis);
|
|
@@ -6,7 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
import crypto from "node:crypto";
|
|
7
7
|
import { loadSpec } from "../build.js";
|
|
8
8
|
import { writeRecording } from "../api-client.js";
|
|
9
|
-
import { editDeck } from "../ai/deck-ai.js";
|
|
9
|
+
import { editDeck, coverDocumentVisuals, checkNumbers } from "../ai/deck-ai.js";
|
|
10
10
|
import { reviewExperience } from "../ai/quality.js";
|
|
11
11
|
import { maybeResearch } from "../research/chat-research.js";
|
|
12
12
|
import { chatErrorResult } from "./errors.js";
|
|
@@ -96,6 +96,25 @@ export async function aiChatRoutes({ req, res, pathname, url, W, readJSON, stale
|
|
|
96
96
|
styles: taskWorkspace.file && !isBundledTemplate(taskWorkspace.file) ? { list: taskWorkspace.library.listStyles(), current: taskWorkspace.spec?.style?.name || null } : null,
|
|
97
97
|
});
|
|
98
98
|
signal.throwIfAborted();
|
|
99
|
+
// documento anexado NESTA mensagem e a resposta montou/reescreveu a maior parte do deck (criar a apresentação
|
|
100
|
+
// do paper pelo chat): a mesma conferência de cobertura da geração. Pedido pontual ("use a figura 3 no slide
|
|
101
|
+
// 5") não dispara: mexe em poucos slides.
|
|
102
|
+
const attachedDocs = (Array.isArray(body.attachments) ? body.attachments : []).some((a) => a && typeof a === "object" && a.type === "doc");
|
|
103
|
+
if (attachedDocs && !result.talk && !result.transform && !result.style && !result.variants && result.spec?.slides) {
|
|
104
|
+
const rewritten = Array.isArray(result.changed) ? result.changed.length : 0;
|
|
105
|
+
if (rewritten >= Math.max(5, result.spec.slides.length / 2)) {
|
|
106
|
+
const covered = await coverDocumentVisuals(result.spec, { materials, briefing: prompt, say: (s) => emit({ phase: "step", text: s }),
|
|
107
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: imageOptions(taskWorkspace, withBase(taskWorkspace, spec)), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
108
|
+
signal.throwIfAborted();
|
|
109
|
+
if (covered.spec !== result.spec) { result.spec = covered.spec; result.actions = [...(result.actions || []), ...covered.actions]; }
|
|
110
|
+
if (covered.coverage?.missing.length) result.actions = [...(result.actions || []), `Sem slide (figuras/tabelas do material): ${covered.coverage.missing.join("; ")}`];
|
|
111
|
+
const checked = await checkNumbers(result.spec, { materials, say: (s) => emit({ phase: "step", text: s }),
|
|
112
|
+
edit: (o) => editDeck({ signal, images: true, imageOptions: imageOptions(taskWorkspace, withBase(taskWorkspace, spec)), onProgress: emit, materials, drawCheck: diagramCheck, ...o }) });
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
if (checked.spec !== result.spec) { result.spec = checked.spec; result.actions = [...(result.actions || []), ...checked.actions]; }
|
|
115
|
+
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(", ")}`];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
99
118
|
if (linkActions.length) result.actions = [...linkActions, ...(result.actions || [])];
|
|
100
119
|
// a IA decidiu transformar a apresentação inteira (transform:): o trabalho em etapas, com o andamento aqui
|
|
101
120
|
if (result.transform) {
|