gen / routes /agent.js
AnesKAM's picture
Update routes/agent.js
946313d verified
Raw
History Blame Contribute Delete
10.2 kB
import express from "express";
import { Document, Packer, Paragraph, HeadingLevel, TextRun, AlignmentType, BorderStyle } from "docx";
import PptxGenJS from "pptxgenjs";
import ExcelJS from "exceljs";
import PDFDocument from "pdfkit";
const router = express.Router();
const PURPLE = "7C3AED";
const LIGHT = "F3E8FF";
function sendBuffer(res, buf, filename, mime) {
res.setHeader("Content-Type", mime);
res.setHeader("Content-Disposition", `attachment; filename="${encodeURIComponent(filename)}"`);
res.send(buf);
}
/* ---- قراءة مرنة للمحتوى ---- */
// يحوّل أي شكل قد يرسله النموذج إلى مصفوفة فقرات/أقسام موحّدة
function normalizeBlocks(content) {
const blocks = [];
if (!content) return blocks;
if (typeof content === "string") { blocks.push({ type: "p", text: content }); return blocks; }
if (content.heading) blocks.push({ type: "h", text: content.heading });
// paragraphs
(content.paragraphs || []).forEach(p => blocks.push({ type: "p", text: asText(p) }));
// sections: {title, body|content|text|bullets}
(content.sections || []).forEach(s => {
if (s.title) blocks.push({ type: "h", text: s.title });
const body = s.body || s.content || s.text;
if (body) blocks.push({ type: "p", text: asText(body) });
(s.bullets || s.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) }));
});
// bullets/points على المستوى الأعلى
(content.bullets || content.points || []).forEach(b => blocks.push({ type: "li", text: asText(b) }));
return blocks;
}
function asText(v) {
if (v == null) return "";
if (typeof v === "string") return v;
if (Array.isArray(v)) return v.map(asText).join("\n");
if (typeof v === "object") return v.text || v.body || v.content || JSON.stringify(v);
return String(v);
}
// للعروض: استخرج شرائح موحّدة {title, bullets[]}
function normalizeSlides(content, fallbackTitle) {
let slides = [];
if (content?.slides?.length) {
slides = content.slides.map(s => ({
title: s.title || s.heading || fallbackTitle || "",
bullets: normalizeBullets(s),
}));
} else {
// اصنع شرائح من sections أو paragraphs
const secs = content?.sections || [];
if (secs.length) {
slides = secs.map(s => ({ title: s.title || "", bullets: normalizeBullets(s) }));
} else {
slides = [{ title: fallbackTitle || "Slide", bullets: (content?.paragraphs || []).map(asText) }];
}
}
return slides.filter(s => s.title || (s.bullets && s.bullets.length));
}
function normalizeBullets(s) {
if (Array.isArray(s.bullets)) return s.bullets.map(asText);
if (Array.isArray(s.points)) return s.points.map(asText);
const body = s.body || s.content || s.text;
if (Array.isArray(body)) return body.map(asText);
if (typeof body === "string") return body.split(/\n|•/).map(x => x.trim()).filter(Boolean);
return [];
}
/* ---- WORD (تصميم جميل) ---- */
async function buildDocx(title, content) {
const blocks = normalizeBlocks(content);
const children = [
new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { after: 300 },
border: { bottom: { color: PURPLE, space: 6, style: BorderStyle.SINGLE, size: 18 } },
children: [new TextRun({ text: title || "Document", bold: true, size: 48, color: PURPLE })],
}),
];
for (const b of blocks) {
if (b.type === "h")
children.push(new Paragraph({ spacing: { before: 200, after: 100 }, children: [new TextRun({ text: b.text, bold: true, size: 30, color: PURPLE })] }));
else if (b.type === "li")
children.push(new Paragraph({ bullet: { level: 0 }, spacing: { after: 60 }, children: [new TextRun({ text: b.text, size: 24 })] }));
else
children.push(new Paragraph({ spacing: { after: 120 }, children: [new TextRun({ text: b.text, size: 24 })] }));
}
if (blocks.length === 0) children.push(new Paragraph({ children: [new TextRun(asText(content))] }));
const doc = new Document({ sections: [{ children }] });
return Packer.toBuffer(doc);
}
/* ---- POWERPOINT (قالب إبداعي + أنيميشن) ---- */
async function buildPptx(title, content) {
const pptx = new PptxGenJS();
pptx.defineLayout({ name: "WIDE", width: 13.333, height: 7.5 });
pptx.layout = "WIDE";
// قالب رئيسي بخلفية متدرّجة وأشكال
pptx.defineSlideMaster({
title: "GENISI",
background: { color: "FFFFFF" },
objects: [
{ rect: { x: 0, y: 0, w: 13.333, h: 0.25, fill: { color: PURPLE } } },
{ rect: { x: 0, y: 7.25, w: 13.333, h: 0.25, fill: { color: "C084FC" } } },
],
});
// شريحة العنوان
const cover = pptx.addSlide({ masterName: "GENISI" });
cover.background = { color: PURPLE };
cover.addShape(pptx.ShapeType.ellipse, { x: 10.5, y: -1.5, w: 4, h: 4, fill: { color: "9333EA" } });
cover.addShape(pptx.ShapeType.ellipse, { x: -1, y: 5, w: 3.5, h: 3.5, fill: { color: "A855F7" } });
cover.addText(title || "Presentation", { x: 0.8, y: 2.8, w: 11.7, h: 1.5, fontSize: 44, bold: true, color: "FFFFFF", align: "center" });
cover.addText("Genisi", { x: 0.8, y: 4.4, w: 11.7, h: 0.6, fontSize: 18, color: "E9D5FF", align: "center" });
const slides = normalizeSlides(content, title);
slides.forEach((s, idx) => {
const slide = pptx.addSlide({ masterName: "GENISI" });
// شكل زخرفي
slide.addShape(pptx.ShapeType.roundRect, { x: 0.5, y: 0.5, w: 0.5, h: 0.9, fill: { color: PURPLE }, rectRadius: 0.1 });
slide.addText(s.title || `Slide ${idx + 1}`, { x: 1.2, y: 0.5, w: 11, h: 0.9, fontSize: 30, bold: true, color: PURPLE });
// نقاط مع أنيميشن (ظهور تدريجي عبر options)
const bullets = (s.bullets || []).map(b => ({ text: b, options: { bullet: { code: "2022" }, fontSize: 20, color: "1F2937", paraSpaceAfter: 10 } }));
if (bullets.length) {
slide.addText(bullets, { x: 1.2, y: 1.7, w: 11, h: 5, valign: "top" });
}
// أنيميشن: تأثير دخول للشريحة (transition)
slide.transition = { type: "fade", duration: 0.6 };
slide.addText(`${idx + 1}`, { x: 12.4, y: 6.7, w: 0.7, h: 0.4, fontSize: 12, color: "9CA3AF", align: "right" });
});
const data = await pptx.write("nodebuffer");
return Buffer.isBuffer(data) ? data : Buffer.from(data);
}
/* ---- EXCEL (تصميم جميل) ---- */
async function buildXlsx(title, content) {
const wb = new ExcelJS.Workbook();
wb.creator = "Genisi";
const sheets = content?.sheets?.length
? content.sheets
: [{ name: title || "Sheet1", headers: content?.headers || [], rows: content?.rows || [] }];
sheets.forEach(sh => {
const ws = wb.addWorksheet((sh.name || "Sheet").slice(0, 31));
if (sh.headers?.length) {
const hr = ws.addRow(sh.headers);
hr.eachCell(c => {
c.font = { bold: true, color: { argb: "FFFFFFFF" } };
c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FF7C3AED" } };
c.alignment = { horizontal: "center", vertical: "middle" };
c.border = { bottom: { style: "thin", color: { argb: "FFCCCCCC" } } };
});
}
(sh.rows || []).forEach((r, i) => {
const row = ws.addRow(r);
if (i % 2) row.eachCell(c => (c.fill = { type: "pattern", pattern: "solid", fgColor: { argb: "FFF6F0FF" } }));
});
ws.columns.forEach(c => (c.width = 22));
});
const buf = await wb.xlsx.writeBuffer();
return Buffer.from(buf);
}
function buildCsv(content) {
const headers = content?.headers || content?.sheets?.[0]?.headers || [];
const rows = content?.rows || content?.sheets?.[0]?.rows || [];
const esc = v => `"${String(v ?? "").replace(/"/g, '""')}"`;
const lines = [];
if (headers.length) lines.push(headers.map(esc).join(","));
rows.forEach(r => lines.push(r.map(esc).join(",")));
return Buffer.from("\uFEFF" + lines.join("\n"), "utf8");
}
/* ---- PDF (تصميم جميل) ---- */
function buildPdf(title, content) {
return new Promise((resolve, reject) => {
const doc = new PDFDocument({ margin: 54, size: "A4" });
const chunks = [];
doc.on("data", c => chunks.push(c));
doc.on("end", () => resolve(Buffer.concat(chunks)));
doc.on("error", reject);
// شريط علوي
doc.rect(0, 0, doc.page.width, 70).fill("#7C3AED");
doc.fillColor("#fff").fontSize(24).text(title || "Document", 54, 26, { width: doc.page.width - 108 });
doc.moveDown(2).fillColor("#000");
const blocks = normalizeBlocks(content);
if (blocks.length === 0) { doc.fontSize(12).text(asText(content)); doc.end(); return; }
let y = 100;
doc.y = y;
for (const b of blocks) {
if (b.type === "h") { doc.moveDown(0.6).fontSize(15).fillColor("#7C3AED").text(b.text); doc.fillColor("#000"); }
else if (b.type === "li") { doc.fontSize(12).fillColor("#000").text("• " + b.text, { indent: 12 }); }
else { doc.fontSize(12).fillColor("#000").text(b.text); }
doc.moveDown(0.3);
}
doc.end();
});
}
router.post("/document", async (req, res) => {
try {
const { format = "pdf", title = "Genisi Document", content = {} } = req.body;
const safe = (title || "document").replace(/[^\w\u0600-\u06FF\- ]/g, "_").slice(0, 60);
switch (format) {
case "docx": return sendBuffer(res, await buildDocx(title, content), `${safe}.docx`, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
case "pptx": return sendBuffer(res, await buildPptx(title, content), `${safe}.pptx`, "application/vnd.openxmlformats-officedocument.presentationml.presentation");
case "xlsx": return sendBuffer(res, await buildXlsx(title, content), `${safe}.xlsx`, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
case "csv": return sendBuffer(res, buildCsv(content), `${safe}.csv`, "text/csv; charset=utf-8");
case "pdf":
default: return sendBuffer(res, await buildPdf(title, content), `${safe}.pdf`, "application/pdf");
}
} catch (err) {
console.error("document error:", err);
res.status(500).json({ error: "document_failed", message: String(err?.message || err) });
}
});
export default router;