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 }> = () => ( +
+
{title}
+
{message}
+
); - 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 = ({