diff --git a/nginx.conf b/nginx.conf
index 5796df6b..3abcb5d9 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -8,7 +8,7 @@ events {
}
http {
- client_max_body_size 20M;
+ client_max_body_size 100M;
server {
listen 80;
diff --git a/servers/fastapi/api/v1/ppt/endpoints/files.py b/servers/fastapi/api/v1/ppt/endpoints/files.py
index b19e31d0..25936e6a 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/files.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/files.py
@@ -20,7 +20,7 @@ async def upload_files(files: Optional[List[UploadFile]]):
temp_dir = TEMP_FILE_SERVICE.create_temp_dir(get_random_uuid())
- validate_files(files, True, True, 50, UPLOAD_ACCEPTED_FILE_TYPES)
+ validate_files(files, True, True, 100, UPLOAD_ACCEPTED_FILE_TYPES)
temp_files: List[str] = []
if files:
diff --git a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py
index 1acd3b8d..56df3c28 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/pdf_slides.py
@@ -46,6 +46,12 @@ async def process_pdf_slides(
status_code=400,
detail=f"Invalid file type. Expected PDF file, got {pdf_file.content_type}"
)
+ # Enforce 100MB size limit
+ if hasattr(pdf_file, "size") and pdf_file.size and pdf_file.size > (100 * 1024 * 1024):
+ raise HTTPException(
+ status_code=400,
+ detail="PDF file exceeded max upload size of 100 MB",
+ )
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
diff --git a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
index 9b922f3c..473c5dca 100644
--- a/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
+++ b/servers/fastapi/api/v1/ppt/endpoints/pptx_slides.py
@@ -275,6 +275,12 @@ async def process_pptx_slides(
status_code=400,
detail=f"Invalid file type. Expected PPTX file, got {pptx_file.content_type}"
)
+ # Enforce 100MB size limit
+ if hasattr(pptx_file, "size") and pptx_file.size and pptx_file.size > (100 * 1024 * 1024):
+ raise HTTPException(
+ status_code=400,
+ detail="PPTX file exceeded max upload size of 100 MB",
+ )
# Create temporary directory for processing
with tempfile.TemporaryDirectory() as temp_dir:
diff --git a/servers/nextjs/app/(presentation-generator)/components/SlideErrorBoundary.tsx b/servers/nextjs/app/(presentation-generator)/components/SlideErrorBoundary.tsx
new file mode 100644
index 00000000..7ce46351
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/components/SlideErrorBoundary.tsx
@@ -0,0 +1,56 @@
+"use client";
+
+import React from "react";
+
+interface SlideErrorBoundaryProps {
+ children: React.ReactNode;
+ label?: string;
+}
+
+interface SlideErrorBoundaryState {
+ hasError: boolean;
+ errorMessage: string;
+}
+
+export class SlideErrorBoundary extends React.Component<
+ SlideErrorBoundaryProps,
+ SlideErrorBoundaryState
+> {
+ constructor(props: SlideErrorBoundaryProps) {
+ super(props);
+ this.state = { hasError: false, errorMessage: "" };
+ }
+
+ static getDerivedStateFromError(error: unknown): SlideErrorBoundaryState {
+ return {
+ hasError: true,
+ errorMessage: error instanceof Error ? error.message : String(error),
+ };
+ }
+
+ componentDidCatch(error: unknown) {
+ // Optionally log to an error reporting service
+ // eslint-disable-next-line no-console
+ console.error("Slide render error:", error);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+ {this.props.label ? `${this.props.label} render error` : "Slide render error"}
+
+
+ {this.state.errorMessage}
+
+
+ );
+ }
+ return this.props.children;
+ }
+}
+
+export default SlideErrorBoundary;
+
+
diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
index c1ddc47e..6de138e1 100644
--- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
+++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
@@ -378,80 +378,167 @@ export const LayoutProvider: React.FC<{
const groupLayouts: LayoutInfo[] = [];
const groupFullData: FullDataInfo[] = [];
- for (const i of allLayout) {
- /* ---------- 1. compile JSX to plain script ------------------ */
- const module = compileCustomLayout(i.layout_code, React, z);
-
- if (!module.default) {
- toast.error(`Custom Layout has no default export`, {
- description:
- "Please ensure the layout file exports a default component",
- });
- console.warn(`❌ Custom Layout has no default export`);
- continue;
- }
-
- if (!module.Schema) {
- toast.error(`Custom Layout has no Schema export`, {
- description: "Please ensure the layout file exports a Schema",
- });
- console.warn(`❌ Custom Layout has no Schema export`);
- continue;
- }
- const cacheKey = createCacheKey(
- `custom-${presentationId}`,
- i.layout_name
+ // Helper to create an inline error component for this specific slide
+ const createErrorComponent = (title: string, message: string): React.ComponentType<{ data: any }> => {
+ const ErrorSlide: React.FC<{ data: any }> = () => (
+
);
- if (!layoutCache.has(cacheKey)) {
- layoutCache.set(cacheKey, module.default);
+ ErrorSlide.displayName = "CustomTemplateErrorSlide";
+ return ErrorSlide;
+ };
+
+ for (const i of allLayout) {
+ try {
+ /* ---------- 1. compile JSX to plain script ------------------ */
+ const module = compileCustomLayout(i.layout_code, React, z);
+
+ // Determine identifiers even if subsequent steps fail
+ const originalLayoutId =
+ (module && (module as any).layoutId) ||
+ i.layout_name.toLowerCase().replace(/layout$/, "");
+ const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`;
+ const layoutName =
+ (module && (module as any).layoutName) ||
+ i.layout_name.replace(/([A-Z])/g, " $1").trim();
+ const layoutDescription =
+ (module && (module as any).layoutDescription) ||
+ `${layoutName} layout for presentations`;
+
+ let fullData: FullDataInfo | null = null;
+ let jsonSchema: any = null;
+ let componentToUse: React.ComponentType<{ data: any } | any> | null = null;
+ let sampleData: any = {};
+
+ // Validate exports
+ if (!module || !(module as any).default) {
+ const errorComp = createErrorComponent(
+ `Invalid export in ${i.layout_name}`,
+ "Default export not found. Please export a default React component."
+ );
+ componentToUse = errorComp;
+ jsonSchema = {};
+ } else if (!(module as any).Schema) {
+ const errorComp = createErrorComponent(
+ `Schema missing in ${i.layout_name}`,
+ "Schema export not found. Please export a Zod Schema as 'Schema'."
+ );
+ componentToUse = errorComp;
+ jsonSchema = {};
+ } else {
+ // Cache valid component
+ const cacheKey = createCacheKey(
+ `custom-${presentationId}`,
+ i.layout_name
+ );
+ if (!layoutCache.has(cacheKey)) {
+ layoutCache.set(cacheKey, (module as any).default);
+ }
+ componentToUse = (module as any).default;
+
+ // Build schema and sample data with guards
+ try {
+ jsonSchema = z.toJSONSchema((module as any).Schema, {
+ override: (ctx) => {
+ delete ctx.jsonSchema.default;
+ },
+ });
+ } catch (schemaErr: any) {
+ const errorComp = createErrorComponent(
+ `Schema generation failed for ${i.layout_name}`,
+ schemaErr?.message || String(schemaErr)
+ );
+ componentToUse = errorComp;
+ jsonSchema = {};
+ }
+
+ if (componentToUse !== null && componentToUse !== (module as any).default) {
+ // componentToUse already replaced with error component
+ sampleData = {};
+ } else {
+ try {
+ sampleData = (module as any).Schema.parse({});
+ } catch (parseErr: any) {
+ const errorComp = createErrorComponent(
+ `Schema.parse failed for ${i.layout_name}`,
+ parseErr?.message || String(parseErr)
+ );
+ componentToUse = errorComp;
+ sampleData = {};
+ jsonSchema = jsonSchema || {};
+ }
+ }
+ }
+
+ customFonts.set(presentationId, i.fonts);
+
+ const layout: LayoutInfo = {
+ id: uniqueKey,
+ name: layoutName,
+ description: layoutDescription,
+ json_schema: jsonSchema,
+ groupName: groupName,
+ };
+
+ fullData = {
+ name: layoutName,
+ component: componentToUse as React.ComponentType,
+ schema: jsonSchema,
+ sampleData: sampleData,
+ fileName: i.layout_name,
+ groupName: groupName,
+ layoutId: uniqueKey,
+ };
+
+ groupFullData.push(fullData);
+
+ layoutsById.set(uniqueKey, layout);
+ layoutsByGroup.get(groupName)!.add(uniqueKey);
+ fileMap.set(uniqueKey, {
+ fileName: i.layout_name,
+ groupName: groupName,
+ });
+ groupLayouts.push(layout);
+ layouts.push(layout);
+ } catch (e: any) {
+ // Handle compilation/runtime errors during transformation
+ const uniqueKey = `${`custom-${presentationId}`}:${i.layout_name.toLowerCase().replace(/layout$/, "")}`;
+ const layoutName = i.layout_name.replace(/([A-Z])/g, " $1").trim();
+ const errorComp = createErrorComponent(
+ `Compilation error in ${i.layout_name}`,
+ e?.message || String(e)
+ );
+
+ const layout: LayoutInfo = {
+ id: uniqueKey,
+ name: layoutName,
+ description: `Failed to compile ${i.layout_name}`,
+ json_schema: {},
+ groupName: groupName,
+ };
+
+ const fullData: FullDataInfo = {
+ name: layoutName,
+ component: errorComp,
+ schema: {},
+ sampleData: {},
+ fileName: i.layout_name,
+ groupName: groupName,
+ layoutId: uniqueKey,
+ };
+
+ groupFullData.push(fullData);
+ layoutsById.set(uniqueKey, layout);
+ layoutsByGroup.get(groupName)!.add(uniqueKey);
+ fileMap.set(uniqueKey, {
+ fileName: i.layout_name,
+ groupName: groupName,
+ });
+ groupLayouts.push(layout);
+ layouts.push(layout);
}
-
- customFonts.set(presentationId, i.fonts);
-
- const originalLayoutId =
- module.layoutId ||
- i.layout_name.toLowerCase().replace(/layout$/, "");
- const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`;
- const layoutName =
- module.layoutName ||
- i.layout_name.replace(/([A-Z])/g, " $1").trim();
- const layoutDescription =
- module.layoutDescription ||
- `${layoutName} layout for presentations`;
-
- const jsonSchema = z.toJSONSchema(module.Schema, {
- override: (ctx) => {
- delete ctx.jsonSchema.default;
- },
- });
-
- const layout: LayoutInfo = {
- id: uniqueKey,
- name: layoutName,
- description: layoutDescription,
- json_schema: jsonSchema,
- groupName: groupName,
- };
- const sampleData = module.Schema.parse({});
- const fullData: FullDataInfo = {
- name: layoutName,
- component: module.default,
- schema: jsonSchema,
- sampleData: sampleData,
- fileName: i.layout_name,
- groupName: groupName,
- layoutId: uniqueKey,
- };
- groupFullData.push(fullData);
-
- layoutsById.set(uniqueKey, layout);
- layoutsByGroup.get(groupName)!.add(uniqueKey);
- fileMap.set(uniqueKey, {
- fileName: i.layout_name,
- groupName: groupName,
- });
- groupLayouts.push(layout);
- layouts.push(layout);
}
setCustomTemplateFonts(customFonts);
// Cache grouped layouts
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
index a371c9df..0739410e 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
@@ -36,10 +36,10 @@ export const FileUploadSection: React.FC = ({
- Upload PPTX File
+ Upload PDF or PPTX File
- Select a PowerPoint file (.pptx) to process. Maximum file size: 50MB
+ Select a PDF or PowerPoint file (.pdf or .pptx) to process. Maximum file size: 100MB
{slides.length > 0 && (
@@ -56,12 +56,12 @@ export const FileUploadSection: React.FC
= ({