- Fix PPTX/PDF export: Puppeteer URL port mismatch (80 → 3000) - Fix backend export_utils to use NEXT_INTERNAL_URL env var - Add Chromium to frontend Dockerfile for Docker-based export - Fix slide edit socket hang up with asyncio.wait_for() timeouts - Add FastAPI StaticFiles mounts for /static and /app_data - Add Next.js rewrite for /static/ to proxy to backend - Show template thumbnail in master decks admin page - Add error logging to ReviewWorkflow component - Add Docker env vars for web service (APP_DATA_DIRECTORY, app_data volume) - Add project README in English Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
104 lines
3 KiB
TypeScript
104 lines
3 KiB
TypeScript
import path from "path";
|
|
import fs from "fs";
|
|
import puppeteer from "puppeteer";
|
|
|
|
import { sanitizeFilename } from "@/app/(presentation-generator)/utils/others";
|
|
import { NextResponse, NextRequest } from "next/server";
|
|
|
|
export async function POST(req: NextRequest) {
|
|
const { id, title } = await req.json();
|
|
if (!id) {
|
|
return NextResponse.json(
|
|
{ error: "Missing Presentation ID" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
const browser = await puppeteer.launch({
|
|
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH,
|
|
headless: true,
|
|
args: [
|
|
"--no-sandbox",
|
|
"--disable-setuid-sandbox",
|
|
"--disable-dev-shm-usage",
|
|
"--disable-gpu",
|
|
"--disable-web-security",
|
|
"--disable-background-timer-throttling",
|
|
"--disable-backgrounding-occluded-windows",
|
|
"--disable-renderer-backgrounding",
|
|
"--disable-features=TranslateUI",
|
|
"--disable-ipc-flooding-protection",
|
|
],
|
|
});
|
|
const page = await browser.newPage();
|
|
await page.setViewport({ width: 1280, height: 720 });
|
|
page.setDefaultNavigationTimeout(300000);
|
|
page.setDefaultTimeout(300000);
|
|
|
|
const baseUrl = process.env.PUPPETEER_BASE_URL || `http://localhost:${process.env.PORT || 3000}`;
|
|
await page.goto(`${baseUrl}/pdf-maker?id=${id}`, {
|
|
waitUntil: "networkidle0",
|
|
timeout: 300000,
|
|
});
|
|
|
|
await page.waitForFunction('() => document.readyState === "complete"');
|
|
|
|
try {
|
|
await page.waitForFunction(
|
|
`
|
|
() => {
|
|
const allElements = document.querySelectorAll('*');
|
|
let loadedElements = 0;
|
|
let totalElements = allElements.length;
|
|
|
|
for (let el of allElements) {
|
|
const style = window.getComputedStyle(el);
|
|
const isVisible = style.display !== 'none' &&
|
|
style.visibility !== 'hidden' &&
|
|
style.opacity !== '0';
|
|
|
|
if (isVisible && el.offsetWidth > 0 && el.offsetHeight > 0) {
|
|
loadedElements++;
|
|
}
|
|
}
|
|
|
|
return (loadedElements / totalElements) >= 0.99;
|
|
}
|
|
`,
|
|
{ timeout: 300000 }
|
|
);
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
} catch (error) {
|
|
console.log("Warning: Some content may not have loaded completely:", error);
|
|
}
|
|
|
|
const pdfBuffer = await page.pdf({
|
|
width: "1280px",
|
|
height: "720px",
|
|
printBackground: true,
|
|
margin: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
});
|
|
|
|
browser.close();
|
|
|
|
const sanitizedTitle = sanitizeFilename(title ?? "presentation");
|
|
const appDataDirectory = process.env.APP_DATA_DIRECTORY!;
|
|
if (!appDataDirectory) {
|
|
return NextResponse.json({
|
|
error: "App data directory not found",
|
|
status: 500,
|
|
});
|
|
}
|
|
const destinationPath = path.join(
|
|
appDataDirectory,
|
|
"exports",
|
|
`${sanitizedTitle}.pdf`
|
|
);
|
|
await fs.promises.mkdir(path.dirname(destinationPath), { recursive: true });
|
|
await fs.promises.writeFile(destinationPath, pdfBuffer);
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
path: destinationPath,
|
|
});
|
|
}
|