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)/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-")),