@cccarv82/freya 1.0.60 → 1.0.61
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/cli/web-ui.js +24 -20
- package/cli/web.js +143 -0
- package/package.json +5 -2
package/cli/web-ui.js
CHANGED
|
@@ -516,26 +516,30 @@
|
|
|
516
516
|
el.style.height = el.scrollHeight + 'px';
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
-
function downloadReportPdf(item) {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
setTimeout(() =>
|
|
538
|
-
|
|
519
|
+
async function downloadReportPdf(item) {
|
|
520
|
+
try {
|
|
521
|
+
setPill('run', 'gerando pdf…');
|
|
522
|
+
const res = await fetch('/api/reports/pdf', {
|
|
523
|
+
method: 'POST',
|
|
524
|
+
headers: { 'Content-Type': 'application/json' },
|
|
525
|
+
body: JSON.stringify({ dir: dirOrDefault(), relPath: item.relPath })
|
|
526
|
+
});
|
|
527
|
+
if (!res.ok) throw new Error('pdf failed');
|
|
528
|
+
const buf = await res.arrayBuffer();
|
|
529
|
+
const blob = new Blob([buf], { type: 'application/pdf' });
|
|
530
|
+
const url = URL.createObjectURL(blob);
|
|
531
|
+
const a = document.createElement('a');
|
|
532
|
+
a.href = url;
|
|
533
|
+
a.download = (item.name || 'report').replace(/\.md$/i, '') + '.pdf';
|
|
534
|
+
document.body.appendChild(a);
|
|
535
|
+
a.click();
|
|
536
|
+
a.remove();
|
|
537
|
+
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
|
538
|
+
setPill('ok', 'pdf pronto');
|
|
539
|
+
setTimeout(() => setPill('ok', 'pronto'), 800);
|
|
540
|
+
} catch (e) {
|
|
541
|
+
setPill('err', 'pdf falhou');
|
|
542
|
+
}
|
|
539
543
|
}
|
|
540
544
|
|
|
541
545
|
function renderReportsPage() {
|
package/cli/web.js
CHANGED
|
@@ -1207,6 +1207,125 @@ function buildReportsHtml(safeDefault, appVersion) {
|
|
|
1207
1207
|
</html>`;
|
|
1208
1208
|
}
|
|
1209
1209
|
|
|
1210
|
+
async function renderReportPdf(markdown, title) {
|
|
1211
|
+
const { PDFDocument, StandardFonts, rgb } = await import('pdf-lib');
|
|
1212
|
+
const pdfDoc = await PDFDocument.create();
|
|
1213
|
+
const fontRegular = await pdfDoc.embedFont(StandardFonts.Helvetica);
|
|
1214
|
+
const fontBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold);
|
|
1215
|
+
const fontItalic = await pdfDoc.embedFont(StandardFonts.HelveticaOblique);
|
|
1216
|
+
const fontMono = await pdfDoc.embedFont(StandardFonts.Courier);
|
|
1217
|
+
|
|
1218
|
+
const pageSize = [595.28, 841.89];
|
|
1219
|
+
let page = pdfDoc.addPage(pageSize);
|
|
1220
|
+
const { width, height } = page.getSize();
|
|
1221
|
+
const margin = 48;
|
|
1222
|
+
let y = height - margin;
|
|
1223
|
+
|
|
1224
|
+
const drawLine = (segments, size, indent = 0) => {
|
|
1225
|
+
const lineHeight = size * 1.35;
|
|
1226
|
+
if (y - lineHeight < margin) {
|
|
1227
|
+
page = pdfDoc.addPage(pageSize);
|
|
1228
|
+
y = height - margin;
|
|
1229
|
+
}
|
|
1230
|
+
let x = margin + indent;
|
|
1231
|
+
for (const seg of segments) {
|
|
1232
|
+
const font = seg.style === 'bold' ? fontBold : seg.style === 'italic' ? fontItalic : seg.style === 'mono' ? fontMono : fontRegular;
|
|
1233
|
+
page.drawText(seg.text, { x, y, size, font, color: rgb(0.1, 0.1, 0.1) });
|
|
1234
|
+
x += font.widthOfTextAtSize(seg.text, size);
|
|
1235
|
+
}
|
|
1236
|
+
y -= lineHeight;
|
|
1237
|
+
};
|
|
1238
|
+
|
|
1239
|
+
const wrapSegments = (segments, size, indent = 0) => {
|
|
1240
|
+
const maxWidth = width - margin * 2 - indent;
|
|
1241
|
+
let line = [];
|
|
1242
|
+
let lineWidth = 0;
|
|
1243
|
+
|
|
1244
|
+
const pushLine = () => {
|
|
1245
|
+
if (line.length) drawLine(line, size, indent);
|
|
1246
|
+
line = [];
|
|
1247
|
+
lineWidth = 0;
|
|
1248
|
+
};
|
|
1249
|
+
|
|
1250
|
+
const addToken = (token) => {
|
|
1251
|
+
const font = token.style === 'bold' ? fontBold : token.style === 'italic' ? fontItalic : token.style === 'mono' ? fontMono : fontRegular;
|
|
1252
|
+
const w = font.widthOfTextAtSize(token.text, size);
|
|
1253
|
+
if (lineWidth + w > maxWidth && token.text.trim() !== '') {
|
|
1254
|
+
pushLine();
|
|
1255
|
+
if (token.text.trim() === '') return;
|
|
1256
|
+
}
|
|
1257
|
+
line.push(token);
|
|
1258
|
+
lineWidth += w;
|
|
1259
|
+
};
|
|
1260
|
+
|
|
1261
|
+
for (const seg of segments) {
|
|
1262
|
+
const parts = seg.text.split(/(\s+)/);
|
|
1263
|
+
for (const p of parts) {
|
|
1264
|
+
if (p === '') continue;
|
|
1265
|
+
addToken({ text: p, style: seg.style });
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
pushLine();
|
|
1269
|
+
};
|
|
1270
|
+
|
|
1271
|
+
const parseInline = (text) => {
|
|
1272
|
+
const chunks = [];
|
|
1273
|
+
const pattern = /(\*\*[^*]+\*\*|__[^_]+__|\*[^*]+\*|_[^_]+_)/g;
|
|
1274
|
+
let last = 0;
|
|
1275
|
+
const str = String(text || '');
|
|
1276
|
+
let m;
|
|
1277
|
+
while ((m = pattern.exec(str)) !== null) {
|
|
1278
|
+
if (m.index > last) chunks.push({ text: str.slice(last, m.index), style: 'normal' });
|
|
1279
|
+
const token = m[0];
|
|
1280
|
+
if (token.startsWith('**') || token.startsWith('__')) chunks.push({ text: token.slice(2, -2), style: 'bold' });
|
|
1281
|
+
else chunks.push({ text: token.slice(1, -1), style: 'italic' });
|
|
1282
|
+
last = m.index + token.length;
|
|
1283
|
+
}
|
|
1284
|
+
if (last < str.length) chunks.push({ text: str.slice(last), style: 'normal' });
|
|
1285
|
+
return chunks;
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
const lines = String(markdown || '').split(/\r?\n/);
|
|
1289
|
+
let inCode = false;
|
|
1290
|
+
for (const rawLine of lines) {
|
|
1291
|
+
const line = String(rawLine || '');
|
|
1292
|
+
if (line.trim().startsWith('```')) {
|
|
1293
|
+
inCode = !inCode;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if (inCode) {
|
|
1298
|
+
wrapSegments([{ text: line, style: 'mono' }], 10);
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
if (line.trim() === '') {
|
|
1303
|
+
y -= 8;
|
|
1304
|
+
continue;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const h = line.match(/^(#{1,3})\s+(.*)$/);
|
|
1308
|
+
if (h) {
|
|
1309
|
+
const lvl = h[1].length;
|
|
1310
|
+
const size = lvl === 1 ? 18 : lvl === 2 ? 15 : 13;
|
|
1311
|
+
const segs = parseInline(h[2]).map((s) => ({ text: s.text, style: 'bold' }));
|
|
1312
|
+
wrapSegments(segs, size);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const li = line.match(/^[ \t]*[-*]\s+(.*)$/);
|
|
1317
|
+
if (li) {
|
|
1318
|
+
const segs = [{ text: '• ', style: 'normal' }, ...parseInline(li[1])];
|
|
1319
|
+
wrapSegments(segs, 11, 12);
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
wrapSegments(parseInline(line), 11);
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
return await pdfDoc.save();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1210
1329
|
function ensureDir(p) {
|
|
1211
1330
|
fs.mkdirSync(p, { recursive: true });
|
|
1212
1331
|
}
|
|
@@ -1497,6 +1616,30 @@ async function cmdWeb({ port, dir, open, dev }) {
|
|
|
1497
1616
|
return safeJson(res, 200, { relPath: rel, fullPath: full });
|
|
1498
1617
|
}
|
|
1499
1618
|
|
|
1619
|
+
if (req.url === '/api/reports/pdf') {
|
|
1620
|
+
const rel = payload.relPath;
|
|
1621
|
+
if (!rel) return safeJson(res, 400, { error: 'Missing relPath' });
|
|
1622
|
+
const full = path.join(workspaceDir, rel);
|
|
1623
|
+
if (!exists(full)) return safeJson(res, 404, { error: 'Report not found' });
|
|
1624
|
+
|
|
1625
|
+
const reportsDir = path.join(workspaceDir, 'docs', 'reports');
|
|
1626
|
+
const safeReportsDir = path.resolve(reportsDir);
|
|
1627
|
+
const safeFull = path.resolve(full);
|
|
1628
|
+
if (!safeFull.startsWith(safeReportsDir + path.sep)) {
|
|
1629
|
+
return safeJson(res, 400, { error: 'Invalid report path' });
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
const text = fs.readFileSync(safeFull, 'utf8');
|
|
1633
|
+
const pdfBytes = await renderReportPdf(text, path.basename(rel));
|
|
1634
|
+
res.writeHead(200, {
|
|
1635
|
+
'Content-Type': 'application/pdf',
|
|
1636
|
+
'Content-Disposition': `attachment; filename="${path.basename(rel).replace(/\.md$/i, '')}.pdf"`,
|
|
1637
|
+
'Cache-Control': 'no-store'
|
|
1638
|
+
});
|
|
1639
|
+
res.end(Buffer.from(pdfBytes));
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1500
1643
|
if (req.url === '/api/reports/write') {
|
|
1501
1644
|
const rel = payload.relPath;
|
|
1502
1645
|
const text = payload.text;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cccarv82/freya",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.61",
|
|
4
4
|
"description": "Personal AI Assistant with local-first persistence",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"health": "node scripts/validate-data.js",
|
|
@@ -28,5 +28,8 @@
|
|
|
28
28
|
"templates",
|
|
29
29
|
".agent"
|
|
30
30
|
],
|
|
31
|
-
"preferGlobal": true
|
|
31
|
+
"preferGlobal": true,
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"pdf-lib": "^1.17.1"
|
|
34
|
+
}
|
|
32
35
|
}
|