@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.
Files changed (3) hide show
  1. package/cli/web-ui.js +24 -20
  2. package/cli/web.js +143 -0
  3. 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
- const text = state.reportTexts[item.relPath] || '';
521
- const html = `<!doctype html><html><head><meta charset="utf-8" /><title>${escapeHtml(item.name)}</title><style>body{font-family:Arial, sans-serif; padding:32px; color:#111; line-height:1.5;} h1,h2,h3{margin:16px 0 8px;} ul{padding-left:18px;} code{font-family:monospace;} pre{background:#f5f5f5; padding:12px; border-radius:8px;}</style></head><body>${renderMarkdown(text)}</body></html>`;
522
- const frame = document.createElement('iframe');
523
- frame.style.position = 'fixed';
524
- frame.style.right = '0';
525
- frame.style.bottom = '0';
526
- frame.style.width = '0';
527
- frame.style.height = '0';
528
- frame.style.border = '0';
529
- document.body.appendChild(frame);
530
- const doc = frame.contentWindow.document;
531
- doc.open();
532
- doc.write(html);
533
- doc.close();
534
- frame.onload = () => {
535
- frame.contentWindow.focus();
536
- frame.contentWindow.print();
537
- setTimeout(() => frame.remove(), 1000);
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.60",
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
  }