fix: export issue in chromium

This commit is contained in:
sauravniraula 2025-09-09 05:19:57 +05:45
parent 3dc5a22a62
commit 73c097bd31
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
8 changed files with 671 additions and 562 deletions

View file

@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y \
curl \
libreoffice \
fontconfig \
chromium \
imagemagick
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
@ -22,7 +23,7 @@ WORKDIR /app
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
@ -39,8 +40,6 @@ WORKDIR /app/servers/nextjs
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@136.0.7103.92 --install-deps
# Copy Next.js app
COPY servers/nextjs/ /app/servers/nextjs/

View file

@ -6,6 +6,7 @@ RUN apt-get update && apt-get install -y \
curl \
libreoffice \
fontconfig \
chromium \
imagemagick
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml
@ -24,7 +25,7 @@ RUN ls -a
# Set environment variables
ENV APP_DATA_DIRECTORY=/app_data
ENV TEMP_DIRECTORY=/tmp/presenton
# ENV PYTHONPATH="${PYTHONPATH}:/app/servers/fastapi"
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Install ollama
RUN curl -fsSL http://ollama.com/install.sh | sh
@ -40,9 +41,6 @@ WORKDIR /node_dependencies
COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./
RUN npm install --verbose
# Install chrome for puppeteer
RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps
RUN chmod -R 777 /node_dependencies
# Copy nginx configuration

View file

@ -5,200 +5,241 @@ import GroupLayouts from "./GroupLayouts";
import { LayoutGroup } from "../types/index";
interface LayoutSelectionProps {
selectedLayoutGroup: LayoutGroup | null;
onSelectLayoutGroup: (group: LayoutGroup) => void;
selectedLayoutGroup: LayoutGroup | null;
onSelectLayoutGroup: (group: LayoutGroup) => void;
}
const LayoutSelection: React.FC<LayoutSelectionProps> = ({
selectedLayoutGroup,
onSelectLayoutGroup
selectedLayoutGroup,
onSelectLayoutGroup,
}) => {
const {
getLayoutsByGroup,
getGroupSetting,
getAllGroups,
getFullDataByGroup,
loading
} = useLayout();
const {
getLayoutsByGroup,
getGroupSetting,
getAllGroups,
getFullDataByGroup,
loading,
} = useLayout();
const [summaryMap, setSummaryMap] = React.useState<Record<string, { lastUpdatedAt?: number; name?: string; description?: string }>>({});
const [summaryMap, setSummaryMap] = React.useState<
Record<
string,
{ lastUpdatedAt?: number; name?: string; description?: string }
>
>({});
useEffect(() => {
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch("/api/v1/ppt/template-management/summary")
.then(res => res.json())
.then(data => {
const map: Record<string, { lastUpdatedAt?: number; name?: string; description?: string }> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at ? new Date(p.last_updated_at).getTime() : 0,
name: p.template?.name,
description: p.template?.description,
};
}
}
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
const groups = getAllGroups();
if (groups.length === 0) return [];
const Groups: LayoutGroup[] = groups
.filter(groupName => {
// Filter out groups that contain any errored layouts (from custom templates compile/parse errors)
const fullData = getFullDataByGroup(groupName);
const hasErroredLayouts = fullData.some(fd => (fd as any)?.component?.displayName === "CustomTemplateErrorSlide");
return !hasErroredLayouts;
})
.map(groupName => {
const settings = getGroupSetting(groupName);
const customMeta = summaryMap[groupName];
const isCustom = groupName.toLowerCase().startsWith("custom-");
return {
id: groupName,
name: isCustom && customMeta?.name ? customMeta.name : groupName,
description: (isCustom && customMeta?.description) ? customMeta.description : (settings?.description || `${groupName} presentation templates`),
ordered: settings?.ordered || false,
default: settings?.default || false,
useEffect(() => {
// Fetch custom templates summary to get last_updated_at and template meta for sorting and display
fetch("/api/v1/ppt/template-management/summary")
.then((res) => res.json())
.then((data) => {
const map: Record<
string,
{ lastUpdatedAt?: number; name?: string; description?: string }
> = {};
if (data && Array.isArray(data.presentations)) {
for (const p of data.presentations) {
const slug = `custom-${p.presentation_id}`;
map[slug] = {
lastUpdatedAt: p.last_updated_at
? new Date(p.last_updated_at).getTime()
: 0,
name: p.template?.name,
description: p.template?.description,
};
});
// Sort groups to put default first, then by name
return Groups.sort((a, b) => {
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [getAllGroups, getLayoutsByGroup, getGroupSetting, getFullDataByGroup, summaryMap]);
const inBuiltGroups = React.useMemo(
() => layoutGroups.filter(g => !g.id.toLowerCase().startsWith("custom-")),
[layoutGroups]
);
const customGroups = React.useMemo(() => {
const unsorted = layoutGroups.filter(g => g.id.toLowerCase().startsWith("custom-"));
// Sort by last_updated_at desc using summaryMap keyed by slug id
return unsorted.sort((a, b) => (summaryMap[b.id]?.lastUpdatedAt || 0) - (summaryMap[a.id]?.lastUpdatedAt || 0));
}, [layoutGroups, summaryMap]);
// Auto-select first group when groups are loaded
useEffect(() => {
if (layoutGroups.length > 0 && !selectedLayoutGroup) {
const defaultGroup = layoutGroups.find(g => g.default) || layoutGroups[0];
const slides = getLayoutsByGroup(defaultGroup.id);
onSelectLayoutGroup({
...defaultGroup,
slides: slides,
});
}
}
}, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]);
useEffect(() => {
setSummaryMap(map);
})
.catch(() => setSummaryMap({}));
}, []);
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
const groups = getAllGroups();
console.log("All groups: ", groups);
if (groups.length === 0) return [];
const Groups: LayoutGroup[] = groups
.filter((groupName) => {
// Filter out groups that contain any errored layouts (from custom templates compile/parse errors)
const fullData = getFullDataByGroup(groupName);
const hasErroredLayouts = fullData.some(
(fd) =>
(fd as any)?.component?.displayName === "CustomTemplateErrorSlide"
);
return !hasErroredLayouts;
})
.map((groupName) => {
const settings = getGroupSetting(groupName);
const customMeta = summaryMap[groupName];
const isCustom = groupName.toLowerCase().startsWith("custom-");
return {
id: groupName,
name: isCustom && customMeta?.name ? customMeta.name : groupName,
description:
isCustom && customMeta?.description
? customMeta.description
: settings?.description || `${groupName} presentation templates`,
ordered: settings?.ordered || false,
default: settings?.default || false,
};
});
// Sort groups to put default first, then by name
return Groups.sort((a, b) => {
if (a.default && !b.default) return -1;
if (!a.default && b.default) return 1;
return a.name.localeCompare(b.name);
});
}, [
getAllGroups,
getLayoutsByGroup,
getGroupSetting,
getFullDataByGroup,
summaryMap,
]);
const inBuiltGroups = React.useMemo(
() => layoutGroups.filter((g) => !g.id.toLowerCase().startsWith("custom-")),
[layoutGroups]
);
const customGroups = React.useMemo(() => {
const unsorted = layoutGroups.filter((g) =>
g.id.toLowerCase().startsWith("custom-")
);
// Sort by last_updated_at desc using summaryMap keyed by slug id
return unsorted.sort(
(a, b) =>
(summaryMap[b.id]?.lastUpdatedAt || 0) -
(summaryMap[a.id]?.lastUpdatedAt || 0)
);
}, [layoutGroups, summaryMap]);
// Auto-select first group when groups are loaded
useEffect(() => {
if (layoutGroups.length > 0 && !selectedLayoutGroup) {
const defaultGroup =
layoutGroups.find((g) => g.default) || layoutGroups[0];
const slides = getLayoutsByGroup(defaultGroup.id);
onSelectLayoutGroup({
...defaultGroup,
slides: slides,
});
}
}, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]);
useEffect(() => {
if (loading) {
return;
}
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, []);
if (loading) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded mb-3"></div>
<div className="grid grid-cols-3 gap-2 mb-3">
{[1, 2, 3].map((j) => (
<div key={j} className="aspect-video bg-gray-200 rounded"></div>
))}
</div>
</div>
))}
</div>
</div>
);
}
if (layoutGroups.length === 0) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation templates could be loaded. Please try refreshing the page.
</p>
</div>
</div>
);
}
const handleLayoutGroupSelection = (group: LayoutGroup) => {
const slides = getLayoutsByGroup(group.id);
onSelectLayoutGroup({
...group,
slides: slides,
});
}
return (
<div className="space-y-8 mb-4">
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">In Built Templates</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{inBuiltGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
</div>
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">Custom AI Templates</h3>
</div>
{customGroups.length === 0 ? (
<div className="text-sm text-gray-600 py-2">
No custom templates. Create one from "Create Template" menu.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{customGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
)}
</div>
</div>
const existingScript = document.querySelector(
'script[src*="tailwindcss.com"]'
);
if (!existingScript) {
const script = document.createElement("script");
script.src = "https://cdn.tailwindcss.com";
script.async = true;
document.head.appendChild(script);
}
}, []);
if (loading) {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div
key={i}
className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse"
>
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-3 bg-gray-200 rounded mb-3"></div>
<div className="grid grid-cols-3 gap-2 mb-3">
{[1, 2, 3].map((j) => (
<div
key={j}
className="aspect-video bg-gray-200 rounded"
></div>
))}
</div>
</div>
))}
</div>
</div>
);
}
if (layoutGroups.length === 0) {
return (
<div className="space-y-6">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Templates Available
</h5>
<p className="text-gray-600 text-sm">
No presentation templates could be loaded. Please try refreshing the
page.
</p>
</div>
</div>
);
}
const handleLayoutGroupSelection = (group: LayoutGroup) => {
const slides = getLayoutsByGroup(group.id);
onSelectLayoutGroup({
...group,
slides: slides,
});
};
return (
<div className="space-y-8 mb-4">
{/* In Built Templates */}
<div>
<h3 className="text-lg font-semibold text-gray-900 mb-3">
In Built Templates
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{inBuiltGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
</div>
{/* Custom AI Templates */}
<div>
<div className="flex items-center justify-between mb-3">
<h3 className="text-lg font-semibold text-gray-900">
Custom AI Templates
</h3>
</div>
{customGroups.length === 0 ? (
<div className="text-sm text-gray-600 py-2">
No custom templates. Create one from "Create Template" menu.
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{customGroups.map((group) => (
<GroupLayouts
key={group.id}
group={group}
onSelectLayoutGroup={handleLayoutGroupSelection}
selectedLayoutGroup={selectedLayoutGroup}
/>
))}
</div>
)}
</div>
</div>
);
};
export default LayoutSelection;
export default LayoutSelection;

View file

@ -1,31 +1,45 @@
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';
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 });
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-web-security',
]
"--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);
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0', timeout: 300000 });
await page.goto(`http://localhost/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 300000,
});
await page.waitForFunction('() => document.readyState === "complete"')
await page.waitForFunction('() => document.readyState === "complete"');
try {
await page.waitForFunction(
@ -52,13 +66,11 @@ export async function POST(req: NextRequest) {
{ timeout: 300000 }
);
await new Promise(resolve => setTimeout(resolve, 1000));
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",
@ -68,13 +80,17 @@ export async function POST(req: NextRequest) {
browser.close();
const sanitizedTitle = sanitizeFilename(title ?? 'presentation');
const destinationPath = path.join(process.env.APP_DATA_DIRECTORY!, 'exports', `${sanitizedTitle}.pdf`);
const sanitizedTitle = sanitizeFilename(title ?? "presentation");
const destinationPath = path.join(
process.env.APP_DATA_DIRECTORY!,
"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
path: destinationPath,
});
}

View file

@ -1,79 +0,0 @@
import { NextResponse } from "next/server";
import puppeteer from "puppeteer";
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const groupName = searchParams.get("group");
console.log("API called with group:", groupName);
if (!groupName) {
console.warn("No group name provided in query params");
return NextResponse.json({ error: "Missing group name" }, { status: 400 });
}
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(groupName)}`;
console.log("Fetching client page:", schemaPageUrl);
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-web-security"],
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
page.setDefaultNavigationTimeout(300000);
page.setDefaultTimeout(300000);
await page.goto(schemaPageUrl, {
waitUntil: "networkidle0",
timeout: 300000,
});
await page.waitForSelector("[data-layouts]", { timeout: 300000 });
// Extract both data-layouts and data-group-settings attributes
const { dataLayouts, dataGroupSettings } = await page.$eval(
"[data-layouts]",
(el) => ({
dataLayouts: el.getAttribute("data-layouts"),
dataGroupSettings: el.getAttribute("data-group-settings"),
}),
);
let slides, groupSettings;
try {
slides = JSON.parse(dataLayouts || "[]");
} catch (e) {
console.error("Failed to parse data-layouts JSON:", e);
slides = [];
}
try {
groupSettings = JSON.parse(dataGroupSettings || "null");
} catch (e) {
console.error("Failed to parse data-group-settings JSON:", e);
groupSettings = null;
}
// Compose the response to match PresentationLayoutModel
const response = {
name: groupName,
ordered: groupSettings?.ordered ?? false,
slides: slides.map((slide: any) => ({
id: slide.id,
name: slide.name,
description: slide.description,
json_schema: slide.json_schema,
})),
};
return NextResponse.json(response);
} catch (err) {
console.error("Error fetching or parsing client page:", err);
return NextResponse.json(
{ error: "Failed to fetch or parse client page" },
{ status: 500 },
);
} finally {
if (browser) await browser.close();
}
}

View file

@ -1,76 +0,0 @@
import { NextResponse } from 'next/server'
import { promises as fs } from 'fs'
import path from 'path'
import { GroupSetting } from '@/app/(presentation-generator)/template-preview/types'
export async function GET() {
try {
// Get the path to the presentation-templates directory
const layoutsDirectory = path.join(process.cwd(), 'presentation-templates')
// Read all directories in the presentation-templates directory
const items = await fs.readdir(layoutsDirectory, { withFileTypes: true })
// Filter for directories (layout groups) and exclude files
const groupDirectories = items
.filter(item => item.isDirectory())
.map(dir => dir.name)
const allLayouts: { groupName: string; files: string[]; settings: GroupSetting | null }[] = []
// Scan each group directory for layout files and settings
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName)
const groupFiles = await fs.readdir(groupPath)
// Filter for .tsx files and exclude any non-layout files
const layoutFiles = groupFiles.filter(file =>
file.endsWith('.tsx') &&
!file.startsWith('.') &&
!file.includes('.test.') &&
!file.includes('.spec.') &&
file !== 'settings.json'
)
// Read settings.json if it exists
let settings: GroupSetting | null = null
const settingsPath = path.join(groupPath, 'settings.json')
try {
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
settings = JSON.parse(settingsContent) as GroupSetting
} catch (settingsError) {
console.warn(`No settings.json found for group ${groupName} or invalid JSON`)
// Provide default settings if settings.json is missing or invalid
settings = {
description: `${groupName} presentation layouts`,
ordered: false,
default: false
}
}
if (layoutFiles.length > 0) {
allLayouts.push({
groupName: groupName,
files: layoutFiles,
settings: settings
})
}
} catch (error) {
console.error(`Error reading group directory ${groupName}:`, error)
// Continue with other groups even if one fails
}
}
return NextResponse.json(allLayouts)
} catch (error) {
console.error('Error reading presentation-templates directory:', error)
return NextResponse.json(
{ error: 'Failed to read presentation-templates directory' },
{ status: 500 }
)
}
}

File diff suppressed because it is too large Load diff

View file

@ -9,16 +9,36 @@ export async function GET(request: Request) {
return NextResponse.json({ error: "Missing group name" }, { status: 400 });
}
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(groupName)}`;
const schemaPageUrl = `http://localhost/schema?group=${encodeURIComponent(
groupName
)}`;
let browser;
try {
browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox", "--disable-web-security"] });
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);
await page.goto(schemaPageUrl, { waitUntil: "networkidle0", timeout: 300000 });
await page.goto(schemaPageUrl, {
waitUntil: "networkidle0",
timeout: 300000,
});
await page.waitForSelector("[data-layouts]", { timeout: 300000 });
@ -55,8 +75,11 @@ export async function GET(request: Request) {
return NextResponse.json(response);
} catch (err) {
return NextResponse.json({ error: "Failed to fetch or parse client page" }, { status: 500 });
return NextResponse.json(
{ error: "Failed to fetch or parse client page" },
{ status: 500 }
);
} finally {
if (browser) await browser.close();
}
}
}