diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_config.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_config.tsx
deleted file mode 100644
index bdcb0c9d..00000000
--- a/servers/nextjs/app/(presentation-generator)/components/slide_config.tsx
+++ /dev/null
@@ -1,500 +0,0 @@
-import { Slide } from "../types/slide";
-
-import Type1Layout from "./slide_layouts/Type1Layout";
-import Type2Layout from "./slide_layouts/Type2Layout";
-import Type4Layout from "./slide_layouts/Type4Layout";
-import Type5Layout from "./slide_layouts/Type5Layout";
-import Type6Layout from "./slide_layouts/Type6Layout";
-import Type7Layout from "./slide_layouts/Type7Layout";
-import Type8Layout from "./slide_layouts/Type8Layout";
-import Type9Layout from "./slide_layouts/Type9Layout";
-
-
-import { Chart, ChartSettings } from "@/store/slices/presentationGeneration";
-
-import { Pie, PieChart, Cell, CartesianGrid, Label } from "recharts";
-import {
- LineChart,
- Bar,
- Legend,
- BarChart,
- Tooltip,
- YAxis,
- Line,
- XAxis,
-} from "recharts";
-import { ResponsiveContainer } from "recharts";
-
-import { ThemeColors } from "../store/themeSlice";
-import { isDarkColor } from "../utils/others";
-
-import {
- formatTooltipValue,
- formatYAxisTick,
- transformedData,
-} from "../utils/chart";
-
-export const renderSlideContent = (slide: Slide, language: string) => {
- switch (slide.type) {
- case 1:
- return (
-
- );
- case 2:
- return (
-
- );
-
- case 4:
- return (
-
- );
-
- case 5:
- const isFullSizeGraph =
- slide.content.graph?.data.categories.length > 4 &&
- slide.content.graph.type !== "pie";
- return (
-
- );
-
- case 6:
- return (
-
- );
-
- case 7:
- return (
-
- );
-
- case 8:
- return (
-
- );
-
- case 9:
- return (
-
- );
-
-
- default:
- return null;
- }
-};
-
-
-
-// CHART RENDERING
-export const renderChart = (
- localChartData: Chart,
- isMini: boolean = false,
- theme: ThemeColors,
- chartSettings?: ChartSettings
-) => {
- const chartColors = theme.chartColors || [];
-
- const renderCustomizedLabel = ({
- cx,
- cy,
- midAngle,
- innerRadius,
- outerRadius,
- percent,
- index,
- }: any) => {
- const RADIAN = Math.PI / 180;
- const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
- const x = cx + radius * Math.cos(-midAngle * RADIAN);
- const y = cy + radius * Math.sin(-midAngle * RADIAN);
- const isDark = isDarkColor(theme.chartColors[index % chartColors.length]);
-
- return (
-
cx ? "start" : "end"}
- dominantBaseline="central"
- >
- {`${(percent * 100).toFixed(0)}%`}
-
- );
- };
-
- // New function for outside labels
- const renderOutsideLabel = ({
- cx,
- cy,
- midAngle,
- innerRadius,
- outerRadius,
- percent,
- index,
- name,
- }: any) => {
- const RADIAN = Math.PI / 180;
- // Position the label further outside the pie
- const radius = outerRadius * 1.2;
- const x = cx + radius * Math.cos(-midAngle * RADIAN);
- const y = cy + radius * Math.sin(-midAngle * RADIAN);
-
- return (
-
cx ? "start" : "end"}
- dominantBaseline="central"
- >
- {`${(percent * 100).toFixed(0)}%`}
-
- );
- };
-
- if (!localChartData) return null;
- switch (localChartData.type) {
- case "line":
- return (
-
-
- {chartSettings?.showGrid && (
-
- )}
- {!isMini && chartSettings?.showAxisLabel && (
-
- )}
- {!isMini && chartSettings?.showAxisLabel && (
-
-
-
- )}
-
- {!isMini && chartSettings?.showLegend && (
-
- )}
- {localChartData.data.series.map((serie, index) => (
- formatYAxisTick(value),
- // fill: chartSettings?.dataLabel.dataLabelPosition === "Outside" ? theme.slideTitle : '#ffffff',
- // fontWeight: 'bold',
- // fontSize: '12px',
- // fontFamily: theme.fontFamily
- // } : undefined}
- />
- ))}
-
-
- );
-
- case "pie":
- return (
-
-
-
- {transformedData(localChartData).map((entry: any, index: any) => (
- |
- ))}
-
-
- formatTooltipValue(localChartData, value as number)
- }
- contentStyle={{
- backgroundColor: theme.slideBox,
- color: theme.slideTitle,
- border: "none",
- borderRadius: "6px",
- }}
- itemStyle={{
- color: theme.slideTitle,
- }}
- />
- {!isMini && chartSettings?.showLegend && (
-
- )}
-
-
- );
-
- case "bar":
- default:
- return (
-
-
- {chartSettings?.showGrid && (
-
- )}
- {!isMini && chartSettings?.showAxisLabel && (
-
- )}
- {!isMini && chartSettings?.showAxisLabel && (
-
-
-
- )}
-
- {!isMini && chartSettings?.showLegend && (
-
- )}
- {localChartData &&
- localChartData.data &&
- localChartData.data.series &&
- localChartData.data.series.map((serie, index) => (
- formatYAxisTick(value),
- fill:
- chartSettings?.dataLabel.dataLabelPosition ===
- "Outside"
- ? theme.slideTitle
- : "#ffffff",
- fontWeight: "bold",
- fontSize: "14px",
- fontFamily: theme.fontFamily,
- }
- : undefined
- }
- />
- ))}
-
-
- );
- }
-};
diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
index 8ee17eec..77550392 100644
--- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
+++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx
@@ -70,84 +70,92 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const fileMap = new Map
();
const groupedLayouts = new Map();
+ // Start preloading process
+ setIsPreloading(true);
- for (const groupData of groupedLayoutsData) {
+ try {
+ for (const groupData of groupedLayoutsData) {
- // Initialize group
- if (!layoutsByGroup.has(groupData.groupName)) {
- layoutsByGroup.set(groupData.groupName, new Set());
- }
-
- // group settings or default settings
- const settings = groupData.settings || {
- description: `${groupData.groupName} presentation layouts`,
- ordered: false,
- isDefault: false
- };
-
- groupSettingsMap.set(groupData.groupName, settings);
- const groupLayouts: LayoutInfo[] = [];
-
- for (const fileName of groupData.files) {
- try {
- const file = fileName.replace('.tsx', '').replace('.ts', '');
-
- const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`);
-
-
- if (!module.default) {
- toast({
- title: `${file} has no default export`,
- description: 'Please ensure the layout file exports a default component',
- });
- console.warn(`❌ ${file} has no default export`);
- continue;
- }
-
- if (!module.Schema) {
- toast({
- title: `${file} has no Schema export`,
- description: 'Please ensure the layout file exports a Schema',
- });
- console.warn(`❌ ${file} has no Schema export`);
- continue;
- }
-
- const originalLayoutId = module.layoutId || file.toLowerCase().replace(/layout$/, '');
- const uniqueKey = `${groupData.groupName}:${originalLayoutId}`;
- const layoutName = module.layoutName || file.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: originalLayoutId,
- name: layoutName,
- description: layoutDescription,
- json_schema: jsonSchema,
- groupName: groupData.groupName,
- };
-
-
- layoutsById.set(uniqueKey, layout);
- layoutsByGroup.get(groupData.groupName)!.add(originalLayoutId);
- fileMap.set(uniqueKey, { fileName, groupName: groupData.groupName });
- groupLayouts.push(layout);
- layouts.push(layout);
-
-
- } catch (error) {
- console.error(`💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`, error);
+ // Initialize group
+ if (!layoutsByGroup.has(groupData.groupName)) {
+ layoutsByGroup.set(groupData.groupName, new Set());
}
- }
- // Cache grouped layouts
- groupedLayouts.set(groupData.groupName, groupLayouts);
+ // group settings or default settings
+ const settings = groupData.settings || {
+ description: `${groupData.groupName} presentation layouts`,
+ ordered: false,
+ isDefault: false
+ };
+
+ groupSettingsMap.set(groupData.groupName, settings);
+ const groupLayouts: LayoutInfo[] = [];
+
+ for (const fileName of groupData.files) {
+ try {
+ const file = fileName.replace('.tsx', '').replace('.ts', '');
+
+ const module = await import(`@/presentation-layouts/${groupData.groupName}/${file}`);
+
+ if (!module.default) {
+ toast({
+ title: `${file} has no default export`,
+ description: 'Please ensure the layout file exports a default component',
+ });
+ console.warn(`❌ ${file} has no default export`);
+ continue;
+ }
+
+ if (!module.Schema) {
+ toast({
+ title: `${file} has no Schema export`,
+ description: 'Please ensure the layout file exports a Schema',
+ });
+ console.warn(`❌ ${file} has no Schema export`);
+ continue;
+ }
+
+ // Cache the layout component immediately after import
+ const cacheKey = createCacheKey(groupData.groupName, fileName);
+ if (!layoutCache.has(cacheKey)) {
+ layoutCache.set(cacheKey, module.default);
+ }
+
+ const originalLayoutId = module.layoutId || file.toLowerCase().replace(/layout$/, '');
+ const uniqueKey = `${groupData.groupName}:${originalLayoutId}`;
+ const layoutName = module.layoutName || file.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: groupData.groupName,
+ };
+
+ layoutsById.set(uniqueKey, layout);
+ layoutsByGroup.get(groupData.groupName)!.add(uniqueKey);
+ fileMap.set(uniqueKey, { fileName, groupName: groupData.groupName });
+ groupLayouts.push(layout);
+ layouts.push(layout);
+
+ } catch (error) {
+ console.error(`💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`, error);
+ }
+ }
+
+ // Cache grouped layouts
+ groupedLayouts.set(groupData.groupName, groupLayouts);
+ }
+ } finally {
+ setIsPreloading(false);
}
return {
@@ -185,8 +193,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const data = await buildData(groupedLayoutsData);
setLayoutData(data);
- // Preload layouts after loading schema
- await preloadLayouts(data.fileMap);
+ // The preloading is now handled within buildData
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts';
setError(errorMessage);
@@ -196,33 +203,6 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
}
};
- const preloadLayouts = async (fileMap: Map) => {
- setIsPreloading(true);
- try {
- const layoutPromises = Array.from(fileMap.entries()).map(async ([layoutId, { fileName, groupName }]) => {
- const cacheKey = createCacheKey(groupName, fileName);
- if (!layoutCache.has(cacheKey)) {
- const layoutName = fileName.replace('.tsx', '').replace('.ts', '');
-
- const Layout = dynamic(
- () => import(`@/presentation-layouts/${groupName}/${layoutName}`),
- {
- loading: () => ,
- ssr: false,
- }
- ) as React.ComponentType<{ data: any }>;
-
- layoutCache.set(cacheKey, Layout);
- }
- });
- await Promise.all(layoutPromises);
- } catch (error) {
- console.error('Error preloading layouts:', error);
- } finally {
- setIsPreloading(false);
- }
- };
-
const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => {
if (!layoutData) return null;
@@ -230,9 +210,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
// Search through all fileMap entries to find the layout
for (const [key, info] of Array.from(layoutData.fileMap.entries())) {
- // Extract original layout ID from unique key (format: "groupName:layoutId")
- const originalId = key.split(':')[1];
- if (originalId === layoutId) {
+ if (key === layoutId) {
fileInfo = info;
break;
}
@@ -269,8 +247,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
// Search through all entries to find the layout (since we don't know the group)
for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) {
- const originalId = key.split(':')[1];
- if (originalId === layoutId) {
+ if (key === layoutId) {
return layout;
}
}
@@ -279,8 +256,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const getLayoutByIdAndGroup = (layoutId: string, groupName: string): LayoutInfo | null => {
if (!layoutData) return null;
- const uniqueKey = `${groupName}:${layoutId}`;
- return layoutData.layoutsById.get(uniqueKey) || null;
+ return layoutData.layoutsById.get(layoutId) || null;
};
const getLayoutsByGroup = (groupName: string): LayoutInfo[] => {
diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
index 2dd965a2..84b39f5b 100644
--- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
+++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
@@ -1,10 +1,13 @@
'use client'
import React, { useMemo } from 'react';
+import { useDispatch } from 'react-redux';
import { useLayout } from '../context/LayoutContext';
-import { SmartEditableProvider } from '../components/SmartEditableWrapper';
+import EditableLayoutWrapper from '../components/EditableLayoutWrapper';
import TiptapTextReplacer from '../components/TiptapTextReplacer';
+import { updateSlideContent } from '../../../store/slices/presentationGeneration';
export const useGroupLayouts = () => {
+ const dispatch = useDispatch();
const {
getLayoutByIdAndGroup,
getLayoutsByGroup,
@@ -30,7 +33,7 @@ export const useGroupLayouts = () => {
};
}, [getLayoutsByGroup]);
- // Render slide content with group validation and automatic Tiptap text editing
+ // Render slide content with group validation, automatic Tiptap text editing, and editable images/icons
const renderSlideContent = useMemo(() => {
return (slide: any, isEditMode: boolean = true) => {
const Layout = getGroupLayout(slide.layout, slide.layout_group);
@@ -46,29 +49,37 @@ export const useGroupLayouts = () => {
if (isEditMode) {
return (
-
{
- console.log(`Text content changed at ${dataPath}:`, content);
+ onContentChange={(content: string, dataPath: string, slideIndex?: number) => {
+ console.log(`Text content changed at slide ${slideIndex}, path ${dataPath}:`, content);
+ // Dispatch Redux action to update slide content
+ if (dataPath && slideIndex !== undefined) {
+ dispatch(updateSlideContent({
+ slideIndex: slideIndex,
+ dataPath: dataPath,
+ content: content
+ }));
+ }
}}
>
-
+
);
}
return ;
};
- }, [getGroupLayout]);
+ }, [getGroupLayout, dispatch]);
return {
getGroupLayout,
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx
index 17b6f00f..1c2b4bd5 100644
--- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx
+++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx
@@ -37,7 +37,6 @@ const LayoutSelection: React.FC = ({
const Groups: LayoutGroup[] = groups.map(groupName => {
const layouts = getLayoutsByGroup(groupName);
const settings = getGroupSetting(groupName);
-
return {
id: groupName,
name: groupName,
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx
index 02a338c7..640beb24 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx
@@ -47,7 +47,6 @@ import Modal from "./Modal";
import Announcement from "@/components/Announcement";
import { getFontLink, getStaticFileUrl } from "../../utils/others";
-import JSPowerPointExtractor from "../../components/JSPowerPointExtractor";
const Header = ({
@@ -108,13 +107,7 @@ const Header = ({
themeColors.slideBox
);
- // Save in background
- await PresentationGenerationApi.setThemeColors(presentation_id, {
- name: themeType,
- colors: {
- ...themeColors,
- },
- });
+
} catch (error) {
console.error("Failed to update theme:", error);
toast({
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx
index 18ec7f59..a6bb0976 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx
@@ -8,14 +8,14 @@ import SidePanel from "../components/SidePanel";
import SlideContent from "../components/SlideContent";
import LoadingState from "../../components/LoadingState";
import Header from "../components/Header";
-import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
-import { AlertCircle } from "lucide-react";
+import { AlertCircle, Loader2 } from "lucide-react";
import Help from "./Help";
import {
usePresentationStreaming,
usePresentationData,
- usePresentationNavigation
+ usePresentationNavigation,
+ useAutoSave
} from "../hooks";
import { PresentationPageProps } from "../types";
@@ -26,7 +26,6 @@ const PresentationPage: React.FC = ({ presentation_id })
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
- const [autoSaveLoading, setAutoSaveLoading] = useState(false);
// Redux state
const { currentTheme, currentColors } = useSelector(
@@ -36,13 +35,19 @@ const PresentationPage: React.FC = ({ presentation_id })
(state: RootState) => state.presentationGeneration
);
+ // Auto-save functionality
+ const { isSaving } = useAutoSave({
+ debounceMs: 2000,
+ enabled: !!presentationData && !isStreaming,
+
+ });
+
// Custom hooks
const { fetchUserSlides, handleDeleteSlide } = usePresentationData(
presentation_id,
setLoading,
setError
);
-
const {
isPresentMode,
stream,
@@ -98,33 +103,29 @@ const PresentationPage: React.FC = ({ presentation_id })
role="alert"
>
- Oops!
-
- We encountered an issue loading your presentation.
+
+ Something went wrong
+
+
+ We couldn't load your presentation. Please try again.
-
- Please check your internet connection or try again later.
-
-