From 3bf5280a25b366b3cf2a11cf21824f711d0f0621 Mon Sep 17 00:00:00 2001 From: Suraj Jha Date: Tue, 19 Aug 2025 23:12:18 +0545 Subject: [PATCH] fix: parse for error in slide of template beforehand and show eht error while edit --- .../components/SlideErrorBoundary.tsx | 56 +++++ .../context/LayoutContext.tsx | 231 ++++++++++++------ .../hooks/useGroupLayouts.tsx | 12 +- .../outline/components/LayoutSelection.tsx | 12 +- 4 files changed, 234 insertions(+), 77 deletions(-) create mode 100644 servers/nextjs/app/(presentation-generator)/components/SlideErrorBoundary.tsx 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)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx index 24b66bb0..f6652e85 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx @@ -3,6 +3,7 @@ import React, { useMemo } from "react"; import { useDispatch } from "react-redux"; import { useLayout } from "../context/LayoutContext"; import EditableLayoutWrapper from "../components/EditableLayoutWrapper"; +import SlideErrorBoundary from "../components/SlideErrorBoundary"; import TiptapTextReplacer from "../components/TiptapTextReplacer"; import { updateSlideContent } from "../../../store/slices/presentationGeneration"; import { Loader2 } from "lucide-react"; @@ -66,7 +67,6 @@ export const useGroupLayouts = () => { dataPath: string, slideIndex?: number ) => { - // Dispatch Redux action to update slide content if (dataPath && slideIndex !== undefined) { dispatch( updateSlideContent({ @@ -78,12 +78,18 @@ export const useGroupLayouts = () => { } }} > - + + + ); } - return ; + return ( + + + + ); }; }, [getGroupLayout, dispatch]); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx index 3dc97448..5e0c4639 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -17,6 +17,7 @@ const LayoutSelection: React.FC = ({ getLayoutsByGroup, getGroupSetting, getAllGroups, + getFullDataByGroup, loading } = useLayout(); @@ -47,7 +48,14 @@ const LayoutSelection: React.FC = ({ const groups = getAllGroups(); if (groups.length === 0) return []; - const Groups: LayoutGroup[] = groups.map(groupName => { + 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-"); @@ -66,7 +74,7 @@ const LayoutSelection: React.FC = ({ if (!a.default && b.default) return 1; return a.name.localeCompare(b.name); }); - }, [getAllGroups, getLayoutsByGroup, getGroupSetting, summaryMap]); + }, [getAllGroups, getLayoutsByGroup, getGroupSetting, getFullDataByGroup, summaryMap]); const inBuiltGroups = React.useMemo( () => layoutGroups.filter(g => !g.id.toLowerCase().startsWith("custom-")),