diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx new file mode 100644 index 00000000..f6fcac0b --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx @@ -0,0 +1,86 @@ +import React, { useState, useEffect, useRef } from "react"; +import dynamic from "next/dynamic"; +import useLayoutSchema from "./useLayoutSchema"; + +// Global layout cache to persist across component unmounts +const layoutCache = new Map>(); + +const useLayoutCache = () => { + const { idMapFileNames, loading } = useLayoutSchema(); + const [isPreloading, setIsPreloading] = useState(false); + const preloadedRef = useRef(false); + + // Pre-load all layouts when schema is available + useEffect(() => { + if (!loading && idMapFileNames && Object.keys(idMapFileNames).length > 0 && !preloadedRef.current) { + preloadLayouts(); + preloadedRef.current = true; + } + }, [idMapFileNames, loading]); + + const preloadLayouts = async () => { + if (isPreloading) return; + + setIsPreloading(true); + + try { + const layoutPromises = Object.values(idMapFileNames).map(async (layoutName) => { + if (!layoutCache.has(layoutName)) { + const Layout = dynamic( + () => import(`@/components/layouts/${layoutName}`), + { + loading: () =>
, + ssr: false, + } + ) as React.ComponentType<{ data: any }>; + + layoutCache.set(layoutName, 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 => { + const layoutName = idMapFileNames[layoutId]; + if (!layoutName) { + return null; + } + + // Return cached layout if available + if (layoutCache.has(layoutName)) { + return layoutCache.get(layoutName)!; + } + + // Create and cache layout if not available + const Layout = dynamic( + () => import(`@/components/layouts/${layoutName}`), + { + loading: () =>
, + ssr: false, + } + ) as React.ComponentType<{ data: any }>; + + layoutCache.set(layoutName, Layout); + return Layout; + }; + + const clearCache = () => { + layoutCache.clear(); + preloadedRef.current = false; + }; + + return { + getLayout, + isPreloading, + clearCache, + cacheSize: layoutCache.size, + }; +}; + +export default useLayoutCache; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index f5fc1a5e..cb91769f 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useMemo } from "react"; import { LayoutList, ListTree, PanelRightOpen, X } from "lucide-react"; import ToolTip from "@/components/ToolTip"; import { Button } from "@/components/ui/button"; @@ -22,7 +22,7 @@ import { import { setPresentationData } from "@/store/slices/presentationGeneration"; import { SortableSlide } from "./SortableSlide"; import { SortableListItem } from "./SortableListItem"; -import { renderSlideContent } from "../../components/slide_config"; +import useLayoutCache from "../../hooks/useLayoutCache"; interface SidePanelProps { selectedSlide: number; @@ -49,6 +49,18 @@ const SidePanel = ({ (state: RootState) => state.theme ); const dispatch = useDispatch(); + const { getLayout } = useLayoutCache(); + + // Memoized slide renderer using layout cache + const renderSlideContent = useMemo(() => { + return (slide: any) => { + const Layout = getLayout(slide.layout); + if (!Layout) { + return
Layout not found
; + } + return ; + }; + }, [getLayout]); useEffect(() => { if (window.innerWidth < 768) { @@ -56,7 +68,6 @@ const SidePanel = ({ } }, [isMobilePanelOpen]); - const sensors = useSensors( useSensor(PointerSensor), useSensor(KeyboardSensor, { @@ -71,7 +82,6 @@ const SidePanel = ({ } }; - const handleDragEnd = (event: any) => { const { active, over } = event; @@ -114,104 +124,73 @@ const SidePanel = ({ presentationData?.slides.length === 0 ) { return null; - } return ( <> - {/* Desktop Toggle Button - Always visible when panel is closed */} - {!isOpen && ( -
- - - -
- )} - {/* Mobile Toggle Button */} - {!isMobilePanelOpen && ( -
- - - -
+
+ +
+ + {/* Backdrop for mobile */} + {isMobilePanelOpen && ( +
setIsMobilePanelOpen(false)} + /> )} - {/* Side Panel */}
-
-
-
-
- - - - - - -
- + {/* Header */} +
+

Slides

+
+ + + + + + +
+
- {renderSlideContent(slide, 'English')} + {renderSlideContent(slide)}
@@ -294,6 +273,7 @@ const SidePanel = ({ index={index} selectedSlide={selectedSlide} onSlideClick={onSlideClick} + renderSlideContent={renderSlideContent} /> ))} diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index aa268726..037a0c8b 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useCallback, useEffect, useState, useMemo } from "react"; import { Slide } from "../../types/slide"; import { Loader2, PlusIcon, Trash2, WandSparkles } from "lucide-react"; import { @@ -16,23 +16,18 @@ import { useDispatch, useSelector } from "react-redux"; import { addSlide, updateSlide } from "@/store/slices/presentationGeneration"; import NewSlide from "../../components/slide_layouts/NewSlide"; import { getEmptySlideContent } from "../../utils/NewSlideContent"; -import useLayoutSchema from "../../hooks/useLayoutSchema"; -import dynamic from "next/dynamic"; +import useLayoutCache from "../../hooks/useLayoutCache"; interface SlideContentProps { - slide: Slide; + slide: any; index: number; - presentationId: string; - onDeleteSlide: (index: number) => void; } - const SlideContent = ({ slide, index, - presentationId, onDeleteSlide, }: SlideContentProps) => { @@ -42,7 +37,16 @@ const SlideContent = ({ const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); - const { idMapFileNames, idMapSchema } = useLayoutSchema(); + const { getLayout } = useLayoutCache(); + + // Memoized layout component to prevent re-renders + const LayoutComponent = useMemo(() => { + const Layout = getLayout(slide.layout); + if (!Layout) { + return () =>
Layout not found
; + } + return Layout; + }, [slide.layout, getLayout]); const handleSubmit = async () => { const element = document.getElementById( @@ -96,6 +100,7 @@ const SlideContent = ({ dispatch(addSlide({ slide: newSlide, index: index + 1 })); setShowNewSlideSelection(false); }; + // Scroll to the new slide when the presentationData is updated // useEffect(() => { // if ( @@ -114,17 +119,10 @@ const SlideContent = ({ // } // }, [presentationData?.slides, isStreaming]); - const renderLayout = (slide: any) => { - console.log(slide) - console.log(idMapFileNames) - const layoutName = idMapFileNames[slide.layout]; - if (!layoutName) { - return
Layout not found
- } - console.log(layoutName) - const Layout = dynamic(() => import(`@/components/layouts/${layoutName}`)) as React.ComponentType<{ data: any }>; - return - }; + // Memoized slide content rendering to prevent unnecessary re-renders + const slideContent = useMemo(() => { + return ; + }, [LayoutComponent, slide.content]); return ( <> @@ -137,7 +135,7 @@ const SlideContent = ({ )}
{/* render slides */} - {renderLayout(slide)} + {slideContent} {!showNewSlideSelection && (
@@ -230,4 +228,13 @@ const SlideContent = ({ ); }; -export default SlideContent; +export default React.memo(SlideContent, (prevProps, nextProps) => { + // Only re-render if these specific props change + return ( + prevProps.slide.layout === nextProps.slide.layout && + JSON.stringify(prevProps.slide.content) === JSON.stringify(nextProps.slide.content) && + prevProps.slide.index === nextProps.slide.index && + prevProps.index === nextProps.index && + prevProps.presentationId === nextProps.presentationId + ); +}); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx index fc789137..77cbe88e 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx @@ -1,6 +1,5 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; -import { renderSlideContent } from '../../components/slide_config'; import { Slide } from '../../types/slide'; import { useState } from 'react'; @@ -9,9 +8,10 @@ interface SortableSlideProps { index: number; selectedSlide: number; onSlideClick: (index: number) => void; + renderSlideContent: (slide: any) => React.ReactElement; } -export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: SortableSlideProps) { +export function SortableSlide({ slide, index, selectedSlide, onSlideClick, renderSlideContent }: SortableSlideProps) { const [mouseDownTime, setMouseDownTime] = useState(0); const { @@ -57,7 +57,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
- {renderSlideContent(slide, 'English')} + {renderSlideContent(slide)}