diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx new file mode 100644 index 00000000..d806faa7 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -0,0 +1,209 @@ +"use client"; +import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react'; +import dynamic from 'next/dynamic'; +import { toast } from "@/hooks/use-toast"; +import * as z from 'zod'; + +interface LayoutInfo { + id: string; + name?: string; + description?: string; + json_schema: any; +} + +interface LayoutContextType { + layoutSchema: LayoutInfo[] | null; + idMapFileNames: Record; + idMapSchema: Record; + loading: boolean; + error: string | null; + getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; + isPreloading: boolean; + cacheSize: number; + refetch: () => Promise; +} + +const LayoutContext = createContext(undefined); + +// Global layout cache +const layoutCache = new Map>(); + +export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [layoutSchema, setLayoutSchema] = useState(null); + const [idMapFileNames, setIdMapFileNames] = useState>({}); + const [idMapSchema, setIdMapSchema] = useState>({}); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [isPreloading, setIsPreloading] = useState(false); + + const extractSchema = async (layoutFiles: string[]) => { + const layouts: LayoutInfo[] = []; + const idMapFileNames: Record = {}; + const idMapSchema: Record = {}; + + for (const fileName of layoutFiles) { + try { + const file = fileName.replace('.tsx', '').replace('.ts', ''); + const module = await import(`@/components/layouts/${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 layoutId = module.layoutId; + if (!layoutId) { + toast({ + title: `${file} has no layoutId`, + description: 'Please ensure the layout file exports a layoutId', + }); + console.warn(`${file} has no layoutId`); + continue; + } + + const layoutName = module.layoutName; + const layoutDescription = module.layoutDescription; + const jsonSchema = z.toJSONSchema(module.Schema, { + override: (ctx) => { + delete ctx.jsonSchema.default; + }, + }); + + const layout = { + id: layoutId, + name: layoutName, + description: layoutDescription, + json_schema: jsonSchema, + }; + + idMapFileNames[layoutId] = fileName; + idMapSchema[layoutId] = module.Schema; + layouts.push(layout); + } catch (error) { + console.error(`Error extracting schema for ${fileName}:`, error); + } + } + + return { layouts, idMapFileNames, idMapSchema }; + }; + + const loadLayouts = async () => { + if (layoutSchema) return; // Already loaded + + try { + setLoading(true); + setError(null); + + const layoutResponse = await fetch('/api/layouts'); + const layoutFiles = await layoutResponse.json(); + const response = await extractSchema(layoutFiles); + + setLayoutSchema(response?.layouts || []); + setIdMapFileNames(response?.idMapFileNames || {}); + setIdMapSchema(response?.idMapSchema || {}); + + // Preload layouts after loading schema + await preloadLayouts(response?.idMapFileNames || {}); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts'; + setError(errorMessage); + console.error('Error loading layouts:', err); + } finally { + setLoading(false); + } + }; + + const preloadLayouts = async (fileNames: Record) => { + setIsPreloading(true); + + try { + const layoutPromises = Object.values(fileNames).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; + }; + + // Load layouts on mount + useEffect(() => { + loadLayouts(); + }, []); + + const contextValue: LayoutContextType = { + layoutSchema, + idMapFileNames, + idMapSchema, + loading, + error, + getLayout, + isPreloading, + cacheSize: layoutCache.size, + refetch: loadLayouts, + }; + + return ( + + {children} + + ); +}; + +export const useLayout = (): LayoutContextType => { + const context = useContext(LayoutContext); + if (context === undefined) { + throw new Error('useLayout must be used within a LayoutProvider'); + } + return context; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index 1e18f58e..14efe8ee 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -29,7 +29,7 @@ import { getIconFromFile } from "../../utils/others"; import { ChevronRight, PanelRightOpen, X } from "lucide-react"; import ToolTip from "@/components/ToolTip"; import Header from "@/app/dashboard/components/Header"; -import useLayoutSchema from "../../hooks/useLayoutSchema"; +import { useLayout } from "../../context/LayoutContext"; // Types interface LoadingState { @@ -70,7 +70,7 @@ const DocumentsPreviewPage: React.FC = () => { duration: 10, progress: false, }); - const { layoutSchema } = useLayoutSchema(); + const { layoutSchema } = useLayout(); // Memoized computed values const fileItems: FileItem[] = useMemo(() => { diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx deleted file mode 100644 index f6fcac0b..00000000 --- a/servers/nextjs/app/(presentation-generator)/hooks/useLayoutCache.tsx +++ /dev/null @@ -1,86 +0,0 @@ -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)/hooks/useLayoutSchema.ts b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutSchema.ts deleted file mode 100644 index e019d078..00000000 --- a/servers/nextjs/app/(presentation-generator)/hooks/useLayoutSchema.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { useState, useEffect } from "react"; -import { toast } from "@/hooks/use-toast"; -import * as z from 'zod'; -interface LayoutInfo { - id: string; - name?: string; - description?: string; - json_schema: any; -} - -// interface LayoutStructure { -// name: string; -// ordered: boolean; -// slides: LayoutInfo[]; -// } - -const useLayoutSchema = () => { - const [layoutSchema, setLayoutSchema] = useState(null); - const [idMapFileNames, setIdMapFileNames] = useState>({}); - const [idMapSchema, setIdMapSchema] = useState>({}); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const loadLayouts = async () => { - try { - setLoading(true); - setError(null); - const layoutResponse = await fetch('/api/layouts'); - const layoutFiles = await layoutResponse.json(); - const response = await extractSchema(layoutFiles); - - setLayoutSchema(response?.layouts || []); - setIdMapFileNames(response?.idMapFileNames || {}); - setIdMapSchema(response?.idMapSchema || {}); - } catch (err: unknown) { - const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts'; - setError(errorMessage); - console.error('Error loading layouts:', err); - } finally { - setLoading(false); - } - }; - - - - // Auto-load layouts on mount - useEffect(() => { - loadLayouts(); - }, []); - - return { - layoutSchema, - setLayoutSchema, - loading, - error, - refetch: loadLayouts, - idMapFileNames, - idMapSchema - }; -}; - -export default useLayoutSchema; - - -const extractSchema = async (layoutFiles: string[]) => { - const layouts: LayoutInfo[] = []; - const idMapFileNames: Record = {}; - const idMapSchema: Record = {}; - for (const fileName of layoutFiles) { - try { - const file = fileName.replace('.tsx', '').replace('.ts', '') - const module = await import(`@/components/layouts/${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`) - return - } - 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`) - return - } - const layoutId = module.layoutId - if(!layoutId) { - toast({ - title: `${file} has no layoutId`, - description: 'Please ensure the layout file exports a layoutId', - }) - console.warn(`${file} has no layoutId`) - return - } - const layoutName = module.layoutName - const layoutDescription = module.layoutDescription - const jsonSchema = z.toJSONSchema(module.Schema,{ - override :(ctx)=>{ - delete ctx.jsonSchema.default - }, - }) - const layout = { - id: layoutId, - name: layoutName, - description: layoutDescription, - json_schema: jsonSchema, - } - idMapFileNames[layoutId] = fileName - idMapSchema[layoutId] = module.Schema - layouts.push(layout) - } catch (error) { - console.error(`Error extracting schema for ${fileName}:`, error) - return null - } - } - return {layouts, idMapFileNames, idMapSchema} -}; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx new file mode 100644 index 00000000..b676e1f5 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx @@ -0,0 +1,105 @@ +"use client"; +import React from "react"; +import { LayoutGroups, LayoutGroup } from "@/components/layouts/layoutGroup"; +import { useLayout } from "../../context/LayoutContext"; +import { CheckCircle } from "lucide-react"; + +interface LayoutSelectionProps { + selectedLayoutGroup: LayoutGroup | null; + onSelectLayoutGroup: (group: LayoutGroup) => void; +} + +const LayoutSelection: React.FC = ({ + selectedLayoutGroup, + onSelectLayoutGroup +}) => { + const { getLayout } = useLayout(); + + const renderLayoutPreview = (layoutId: string) => { + const Layout = getLayout(layoutId); + if (!Layout) { + return ( +
+ Preview unavailable +
+ ); + } + + // Sample data for preview + const sampleData = { + title: "Sample Title", + description: "This is a preview of the layout", + subtitle: "Sample subtitle", + }; + + return ( +
+
+ +
+
+ ); + }; + + return ( +
+
+
+ Select Your Presentation Style +
+

+ Choose a layout group that best fits your presentation style and content. +

+
+ +
+ {LayoutGroups.map((group) => ( +
onSelectLayoutGroup(group)} + className={`relative p-4 rounded-lg border cursor-pointer ${selectedLayoutGroup?.id === group.id + ? 'border-blue-500 bg-blue-50' + : 'border-gray-200 bg-white' + }`} + > + {selectedLayoutGroup?.id === group.id && ( +
+ +
+ )} + +
+
+ {group.name} +
+

+ {group.description} +

+
+ + {/* Layout previews */} +
+ {group.slides.slice(0, 6).map((layoutId, index) => ( +
+ {renderLayoutPreview(layoutId)} +
+ ))} +
+ +
+ {group.slides.length} layouts + + {group.ordered ? 'Structured' : 'Flexible'} + +
+
+ ))} +
+
+ ); +}; + +export default LayoutSelection; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx new file mode 100644 index 00000000..12250105 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -0,0 +1,132 @@ +"use client"; +import React from "react"; +import { + DndContext, + closestCenter, + KeyboardSensor, + PointerSensor, + useSensor, + useSensors, +} from "@dnd-kit/core"; +import { + arrayMove, + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; +import { OutlineItem } from "./OutlineItem"; +import { Button } from "@/components/ui/button"; +import { SlideOutline } from "@/store/slices/presentationGeneration"; +import { FileText } from "lucide-react"; + +interface OutlineContentProps { + outlines: SlideOutline[] | null; + isLoading: boolean; + isStreaming: boolean; + onDragEnd: (event: any) => void; + onAddSlide: () => void; +} + +const OutlineContent: React.FC = ({ + outlines, + isLoading, + isStreaming, + onDragEnd, + onAddSlide +}) => { + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ); + + return ( +
+
+
+ Presentation Outline +
+ {isStreaming && ( +
+
+ Generating outlines... +
+ )} +
+ + {/* Skeleton loading state */} + {isLoading && ( +
+ {[...Array(5)].map((_, index) => ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ )} + + {/* Outlines content */} + {outlines && outlines.length > 0 && ( +
+ + ({ id: item.title || `slide-${index}` })) || []} + strategy={verticalListSortingStrategy} + > + {outlines?.map((item, index) => ( + + ))} + + + + +
+ )} + + {/* Empty state */} + {!isLoading && outlines && outlines.length === 0 && ( +
+ +

No outlines available

+ +
+ )} +
+ ); +}; + +export default OutlineContent; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx index f137ec7a..936835f0 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx @@ -103,8 +103,8 @@ export function OutlineItem({
handleSlideChange({ ...slideOutline, title: e.target.value })} + defaultValue={slideOutline.title || ''} + onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })} className="text-md sm:text-lg flex-1 font-semibold bg-transparent outline-none" placeholder="Title goes here" /> diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 08a55dfe..e2eaf9fd 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -1,21 +1,8 @@ "use client"; import React, { useEffect, useState } from "react"; -import { - DndContext, - closestCenter, - KeyboardSensor, - PointerSensor, - useSensor, - useSensors, -} from "@dnd-kit/core"; -import { - arrayMove, - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy, -} from "@dnd-kit/sortable"; -import { OutlineItem } from "./OutlineItem"; +import { arrayMove } from "@dnd-kit/sortable"; import { Button } from "@/components/ui/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { RootState } from "@/store/store"; import { useSelector, useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; @@ -29,26 +16,20 @@ import { import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { jsonrepair } from "jsonrepair"; +import { LayoutGroup, getDefaultLayoutGroup } from "@/components/layouts/layoutGroup"; +import OutlineContent from "./OutlineContent"; +import LayoutSelection from "./LayoutSelection"; const OutlinePage = () => { const dispatch = useDispatch(); const router = useRouter(); - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ); - const { presentation_id, outlines } = useSelector( (state: RootState) => state.presentationGeneration ); - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); - + const [activeTab, setActiveTab] = useState('outline'); + const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(getDefaultLayoutGroup()); const [loadingState, setLoadingState] = useState({ message: "", isLoading: false, @@ -182,6 +163,16 @@ const OutlinePage = () => { }); return; } + + if (!selectedLayoutGroup) { + toast({ + title: "Select Layout Group", + description: "Please select a layout group before generating presentation", + variant: "destructive", + }); + return; + } + // Generate data setLoadingState({ message: "Generating presentation data...", @@ -194,17 +185,11 @@ const OutlinePage = () => { const response = await PresentationGenerationApi.presentationPrepare({ presentation_id: presentation_id, outlines: outlines, + layoutGroup: selectedLayoutGroup, }); if (response) { dispatch(setPresentationData(response)); - - toast({ - title: "Success", - description: "Presentation generated successfully!", - variant: "default", - }); - router.push(`/presentation?id=${presentation_id}&stream=true`); } } catch (error) { @@ -238,7 +223,6 @@ const OutlinePage = () => { dispatch(setOutlines(updatedOutlines)); }; - if (!presentation_id) { return ( @@ -248,7 +232,7 @@ const OutlinePage = () => { No Presentation ID Found

Please start a new presentation.

-
@@ -266,129 +250,93 @@ const OutlinePage = () => { duration={loadingState.duration} /> -
-
-
-

- Outline +
+
+ + {/* Header */} +
+

+ Customize Your Presentation

- {isStreaming && ( -
-
- Generating outlines... -
- )} +

+ Review your outline and select a layout style for your presentation. +

- {/* Skeleton loading state */} - {isLoading && ( -
- {[...Array(5)].map((_, index) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
-
- )} + {/* Tabs */} + + + Outline & Content + Layout Style + - {/* Outlines content */} - {outlines && outlines.length > 0 && ( -
- + - ({ id: item.title || `slide-${index}` })) || []} - strategy={verticalListSortingStrategy} - > - {outlines?.map((item, index) => ( - - ))} - - - - -
- )} - - {/* Empty state */} - {!isLoading && outlines && outlines.length === 0 && ( -
-

No outlines available

- -
- )} -
- - {/* Generate button */} - {!isStreaming && } + + + + + + + + {/* Generate button */} +
+ +
+

); diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index d398fdd7..a3d50bf7 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -260,18 +260,6 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { try { const data = await DashboardApi.getPresentation(presentation_id); if (data) { - if (data.presentation.theme) { - dispatch( - setThemeColors({ - ...data.presentation.theme.colors, - theme: data.presentation.theme.name as ThemeType, - }) - ); - setColorsVariables( - data.presentation.theme.colors, - data.presentation.theme.name as ThemeType - ); - } dispatch(setPresentationData(data)); setLoading(false); } @@ -393,8 +381,8 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { />
{!presentationData || loading || diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx index 94b80189..93b8afb7 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx @@ -22,7 +22,7 @@ import { import { setPresentationData } from "@/store/slices/presentationGeneration"; import { SortableSlide } from "./SortableSlide"; import { SortableListItem } from "./SortableListItem"; -import useLayoutCache from "../../hooks/useLayoutCache"; +import { useLayout } from "../../context/LayoutContext"; interface SidePanelProps { selectedSlide: number; @@ -50,7 +50,7 @@ const SidePanel = ({ ); console.log('presentationData', presentationData) const dispatch = useDispatch(); - const { getLayout } = useLayoutCache(); + const { getLayout } = useLayout(); // Memoized slide renderer using layout cache const renderSlideContent = useMemo(() => { diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index e0029a9b..6063eb4b 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -16,7 +16,7 @@ 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 useLayoutCache from "../../hooks/useLayoutCache"; +import { useLayout } from "../../context/LayoutContext"; interface SlideContentProps { slide: any; @@ -37,13 +37,15 @@ const SlideContent = ({ const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); - const { getLayout } = useLayoutCache(); + const { getLayout, loading } = useLayout(); // Memoized layout component to prevent re-renders const LayoutComponent = useMemo(() => { const Layout = getLayout(slide.layout); if (!Layout) { - return () =>
Layout not found
; + return () =>
+ Layout not found +
; } return Layout; }, [slide.layout, getLayout]); @@ -122,7 +124,7 @@ const SlideContent = ({ ) { // Scroll to the last slide (newly generated during streaming) const lastSlideIndex = presentationData.slides.length - 1; - const slideElement = document.getElementById(`slide-${presentationData.slides[lastSlideIndex].id}`); + const slideElement = document.getElementById(`slide-${presentationData.slides[lastSlideIndex].index}`); if (slideElement) { slideElement.scrollIntoView({ behavior: "smooth", @@ -140,7 +142,7 @@ const SlideContent = ({ return ( <>
{isStreaming && ( @@ -148,7 +150,9 @@ const SlideContent = ({ )}
{/* render slides */} - {slideContent} + {loading ?
+ +
: slideContent} {!showNewSlideSelection && (
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableListItem.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableListItem.tsx index 30e88ac6..1aad2a56 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableListItem.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableListItem.tsx @@ -38,7 +38,7 @@ export function SortableListItem({ slide, index, selectedSlide, onSlideClick }: // If the mouse was down for less than 200ms, consider it a click if (timeDiff < 200 && !isDragging) { - onSlideClick(slide.id); + onSlideClick(slide.index); } }; @@ -50,10 +50,10 @@ export function SortableListItem({ slide, index, selectedSlide, onSlideClick }: {...listeners} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} - className={`p-3 cursor-pointer rounded-lg slide-box + className={`p-3 cursor-pointer ring-0 border-[3px] rounded-lg slide-box ${selectedSlide === index - ? 'ring-2 ring-[#5141e5] text-white' - : 'hover:slide-box/40' + ? ' border-[#5141e5] ' + : 'hover:slide-box/40 border-gray-300' }`} > Slide {index + 1} diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx index de527cc6..fe4cf9e9 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx @@ -39,7 +39,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende // If the mouse was down for less than 200ms, consider it a click if (timeDiff < 200 && !isDragging) { - onSlideClick(slide.id); + onSlideClick(slide.index); } }; @@ -51,7 +51,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende {...listeners} onMouseDown={handleMouseDown} onMouseUp={handleMouseUp} - className={` cursor-pointer border-[3px] p-1 shadow-lg rounded-md transition-all duration-200 ${selectedSlide === index ? ' border-[#5141e5]' : 'border-color' + className={` cursor-pointer border-[3px] p-1 shadow-lg rounded-md transition-all duration-200 ${selectedSlide === index ? ' border-[#5141e5]' : 'border-gray-300' }`} >
diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index 93ce4b4a..0f99f20d 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -25,7 +25,7 @@ import { PresentationGenerationApi } from "../../services/api/presentation-gener import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import { setPptGenUploadState } from "@/store/slices/presentationGenUpload"; -import useLayoutSchema from "../../hooks/useLayoutSchema"; +import { useLayout } from "../../context/LayoutContext"; // Types for loading state interface LoadingState { @@ -40,7 +40,7 @@ const UploadPage = () => { const router = useRouter(); const dispatch = useDispatch(); const { toast } = useToast(); - const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayoutSchema(); + const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayout(); // State management const [files, setFiles] = useState([]); diff --git a/servers/nextjs/app/(presentation-generator)/upload/page.tsx b/servers/nextjs/app/(presentation-generator)/upload/page.tsx index 5260ccbf..13d3c028 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/page.tsx @@ -39,18 +39,18 @@ export const metadata: Metadata = { } const page = () => { - return (
-

Create Presentation

{/*

We will generate a presentation for you

*/}
- -
) + + +
) } + export default page diff --git a/servers/nextjs/app/dashboard/api/dashboard.ts b/servers/nextjs/app/dashboard/api/dashboard.ts index cc3ab590..9de09536 100644 --- a/servers/nextjs/app/dashboard/api/dashboard.ts +++ b/servers/nextjs/app/dashboard/api/dashboard.ts @@ -1,6 +1,5 @@ import { getHeader, - getHeaderForFormData, } from "@/app/(presentation-generator)/services/api/header"; @@ -20,7 +19,7 @@ export interface PresentationResponse { vector_store: any; thumbnail: string; - slide: any; + slides: any[]; } export class DashboardApi { @@ -28,7 +27,7 @@ export class DashboardApi { static async getPresentations(): Promise { try { const response = await fetch( - `/api/v1/ppt/user_presentations`, + `/api/v1/ppt/presentation/all`, { method: "GET", } @@ -49,7 +48,7 @@ export class DashboardApi { static async getPresentation(id: string) { try { const response = await fetch( - `/api/v1/ppt/presentation?presentation_id=${id}`, + `/api/v1/ppt/presentation/?id=${id}`, { method: "GET", @@ -68,7 +67,7 @@ export class DashboardApi { static async deletePresentation(presentation_id: string) { try { const response = await fetch( - `/api/v1/ppt/delete?presentation_id=${presentation_id}`, + `/api/v1/ppt/delete?id=${presentation_id}`, { method: "DELETE", headers: getHeader(), diff --git a/servers/nextjs/app/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/dashboard/components/PresentationCard.tsx index a8f4dde2..8b32b7e7 100644 --- a/servers/nextjs/app/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/dashboard/components/PresentationCard.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useMemo } from "react"; import { Card } from "@/components/ui/card"; import { DashboardApi } from "../api/dashboard"; @@ -11,23 +11,23 @@ import { import { useRouter } from "next/navigation"; import { toast } from "@/hooks/use-toast"; import { renderSlideContent } from "@/app/(presentation-generator)/components/slide_config"; +import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext"; export const PresentationCard = ({ id, title, created_at, - thumbnail, - theme, slide }: { id: string; title: string; created_at: string; - thumbnail: string; - theme: any; slide: any }) => { + console.log('slide', slide) const router = useRouter(); + const { getLayout, loading } = useLayout(); + const handlePreview = (e: React.MouseEvent) => { e.preventDefault(); @@ -61,25 +61,27 @@ export const PresentationCard = ({ window.location.reload(); }; - const themeName = theme.name; - // Create CSS variables object - const cssVariables = { - '--slide-bg': theme.colors.slideBg, - '--slide-title': theme.colors.slideTitle, - '--slide-heading': theme.colors.slideHeading, - '--slide-description': theme.colors.slideDescription, - '--slide-box': theme.colors.slideBox, - '--icon-bg': theme.colors.iconBg, - '--background': theme.colors.background, - '--font-family': theme.colors.fontFamily, - } as React.CSSProperties; + const LayoutComponent = useMemo(() => { + const Layout = getLayout(slide.layout); + if (!Layout) { + return () =>
+ Layout not found +
; + } + return Layout; + }, [slide.layout, getLayout]); + const slideContent = useMemo(() => { + return ; + }, [LayoutComponent, slide.content]); + + return (
{/* Date */} @@ -103,25 +105,6 @@ export const PresentationCard = ({
- {/* Thumbnail */} - {/*
- {thumbnail ? ( - {title} - ) : ( -
-

- No thumbnail yet -

-

- Will be added shortly -

-
- )} -
*/}
- {renderSlideContent(slide, 'English')} + {slideContent}
diff --git a/servers/nextjs/app/dashboard/components/PresentationGrid.tsx b/servers/nextjs/app/dashboard/components/PresentationGrid.tsx index 9d700d29..9110da25 100644 --- a/servers/nextjs/app/dashboard/components/PresentationGrid.tsx +++ b/servers/nextjs/app/dashboard/components/PresentationGrid.tsx @@ -101,9 +101,10 @@ export const PresentationGrid = ({ presentations.map((presentation) => ( ))} diff --git a/servers/nextjs/app/layout.tsx b/servers/nextjs/app/layout.tsx index 783bf2b9..d8ea12de 100644 --- a/servers/nextjs/app/layout.tsx +++ b/servers/nextjs/app/layout.tsx @@ -5,6 +5,7 @@ import "./globals.css"; import { Providers } from "./providers"; import { Toaster } from "@/components/ui/toaster"; import { FooterProvider } from "./(presentation-generator)/context/footerContext"; +import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext"; const fraunces = Fraunces({ subsets: ["latin"], @@ -102,10 +103,13 @@ export default function RootLayout({ className={`$ ${inter.variable} ${fraunces.variable} ${montserrat.variable} ${inria_serif.variable} ${roboto.variable} ${instrument_sans.variable} antialiased`} > - + + - {children} - + + {children} + + diff --git a/servers/nextjs/components/layouts/Type1SlideLayout.tsx b/servers/nextjs/components/layouts/Type1SlideLayout.tsx index 30c73952..6b587fbb 100644 --- a/servers/nextjs/components/layouts/Type1SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type1SlideLayout.tsx @@ -33,7 +33,7 @@ const Type1SlideLayout: React.FC = ({ data: slideData }) return (
diff --git a/servers/nextjs/components/layouts/Type2NumberedSlideLayout.tsx b/servers/nextjs/components/layouts/Type2NumberedSlideLayout.tsx index 74d6b10f..0dc07e76 100644 --- a/servers/nextjs/components/layouts/Type2NumberedSlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type2NumberedSlideLayout.tsx @@ -49,7 +49,7 @@ const Type2NumberedSlideLayout: React.FC = ({ dat const renderGridContent = () => { return ( -
+
{items.map((item, index) => (
= ({ dat const renderHorizontalContent = () => { return ( -
+
{items.map((item, index) => (
= ({ dat return (
diff --git a/servers/nextjs/components/layouts/Type2SlideLayout.tsx b/servers/nextjs/components/layouts/Type2SlideLayout.tsx index 49f744ed..979117fa 100644 --- a/servers/nextjs/components/layouts/Type2SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type2SlideLayout.tsx @@ -94,7 +94,7 @@ const Type2SlideLayout: React.FC = ({ data: slideData }) return (
diff --git a/servers/nextjs/components/layouts/Type2TimelineSlideLayout.tsx b/servers/nextjs/components/layouts/Type2TimelineSlideLayout.tsx index 580627d1..2684fd06 100644 --- a/servers/nextjs/components/layouts/Type2TimelineSlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type2TimelineSlideLayout.tsx @@ -86,7 +86,7 @@ const Type2TimelineSlideLayout: React.FC = ({ dat return (

diff --git a/servers/nextjs/components/layouts/Type3SlideLayout.tsx b/servers/nextjs/components/layouts/Type3SlideLayout.tsx index 21385dba..2d5db420 100644 --- a/servers/nextjs/components/layouts/Type3SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type3SlideLayout.tsx @@ -73,7 +73,7 @@ const Type3SlideLayout: React.FC = ({ data: slideData }) return (
diff --git a/servers/nextjs/components/layouts/Type4SlideLayout.tsx b/servers/nextjs/components/layouts/Type4SlideLayout.tsx index 1fbb236f..f89f9ec5 100644 --- a/servers/nextjs/components/layouts/Type4SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type4SlideLayout.tsx @@ -44,14 +44,14 @@ const Type4SlideLayout: React.FC = ({ data: slideData }) return (

{slideData?.title || 'Chart Analysis'}

-
diff --git a/servers/nextjs/components/layouts/Type5SlideLayout.tsx b/servers/nextjs/components/layouts/Type5SlideLayout.tsx index 91765142..9a3a0a74 100644 --- a/servers/nextjs/components/layouts/Type5SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type5SlideLayout.tsx @@ -51,7 +51,7 @@ const Type5SlideLayout: React.FC = ({ data: slideData }) return (
diff --git a/servers/nextjs/components/layouts/Type6SlideLayout.tsx b/servers/nextjs/components/layouts/Type6SlideLayout.tsx index c199e288..34f2656c 100644 --- a/servers/nextjs/components/layouts/Type6SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type6SlideLayout.tsx @@ -133,7 +133,7 @@ const Type6SlideLayout: React.FC = ({ data: slideData }) return (
diff --git a/servers/nextjs/components/layouts/Type7SlideLayout.tsx b/servers/nextjs/components/layouts/Type7SlideLayout.tsx index b717e433..98700163 100644 --- a/servers/nextjs/components/layouts/Type7SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type7SlideLayout.tsx @@ -157,7 +157,7 @@ const Type7SlideLayout: React.FC = ({ data: slideData }) return (

diff --git a/servers/nextjs/components/layouts/Type8SlideLayout.tsx b/servers/nextjs/components/layouts/Type8SlideLayout.tsx index a87b18d5..b2b7494a 100644 --- a/servers/nextjs/components/layouts/Type8SlideLayout.tsx +++ b/servers/nextjs/components/layouts/Type8SlideLayout.tsx @@ -141,7 +141,7 @@ const Type8SlideLayout: React.FC = ({ data: slideData }) return (
{/* Left section - Title and Description */} diff --git a/servers/nextjs/components/layouts/defaultSchemes.ts b/servers/nextjs/components/layouts/defaultSchemes.ts index 90e668c5..6c78900d 100644 --- a/servers/nextjs/components/layouts/defaultSchemes.ts +++ b/servers/nextjs/components/layouts/defaultSchemes.ts @@ -7,8 +7,7 @@ export const ImageSchema = z.object({ prompt: z.string().meta({ description: "Prompt used to generate the image", }), -}).meta({ - imageType: 'image', + image_type:z.literal('image') }) export const IconSchema = z.object({ @@ -18,6 +17,5 @@ export const IconSchema = z.object({ prompt: z.string().meta({ description: "Prompt used to generate the icon", }), -}).meta({ - imageType: 'icon', + image_type:z.literal('icon') }) \ No newline at end of file diff --git a/servers/nextjs/components/layouts/layoutGroup.ts b/servers/nextjs/components/layouts/layoutGroup.ts index feff303a..67044221 100644 --- a/servers/nextjs/components/layouts/layoutGroup.ts +++ b/servers/nextjs/components/layouts/layoutGroup.ts @@ -1,25 +1,97 @@ -export const ProfessionalLayoutGroup = { +export interface LayoutGroup { + id: string; + name: string; + description: string; + ordered: boolean; + isDefault?: boolean; + slides: string[]; +} + +export const ProfessionalLayoutGroup: LayoutGroup = { id: 'professional', + name: 'Professional', + description: 'Clean, corporate designs perfect for business presentations', ordered: true, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + isDefault: true, + slides: [ + 'first-slide', + 'content-slide', + 'bullet-point-slide', + 'comparison-slide', + 'type4-slide', + 'statistics-slide', + 'team-slide', + 'quote-slide' + ] } -export const CasualLayoutGroup = { - id: 'casual', - ordered: false, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] -} - -export const CreativeLayoutGroup = { +export const CreativeLayoutGroup: LayoutGroup = { id: 'creative', + name: 'Creative', + description: 'Vibrant, artistic layouts for innovative and creative presentations', ordered: false, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + slides: [ + 'image-slide', + 'icon-slide', + 'card-slide', + 'type1-slide', + 'type2-slide', + 'type3-slide', + 'process-slide' + ] } -export const ModernLayoutGroup = { +export const ModernLayoutGroup: LayoutGroup = { id: 'modern', + name: 'Modern', + description: 'Contemporary designs with clean lines and sophisticated layouts', ordered: true, - slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide'] + slides: [ + 'type5-slide', + 'type6-slide', + 'type7-slide', + 'type8-slide', + 'timeline-slide', + 'type2-timeline-slide', + 'number-box-slide' + ] } +export const MinimalLayoutGroup: LayoutGroup = { + id: 'minimal', + name: 'Minimal', + description: 'Simple, focused layouts that emphasize content over decoration', + ordered: false, + slides: [ + 'content-slide', + 'bullet-point-slide', + 'type2-numbered-slide', + 'quote-slide', + 'statistics-slide' + ] +} + +export const LayoutGroups = [ + ProfessionalLayoutGroup, + CreativeLayoutGroup, + ModernLayoutGroup, + MinimalLayoutGroup +]; + +export const getDefaultLayoutGroup = (): LayoutGroup => { + return LayoutGroups.find(group => group.isDefault) || ProfessionalLayoutGroup; +}; + +export const getAllLayouts = (): string[] => { + const allLayouts = new Set(); + LayoutGroups.forEach(group => { + group.slides.forEach(slide => allLayouts.add(slide)); + }); + return Array.from(allLayouts); +}; + +export const getGroupByLayoutId = (layoutId: string): LayoutGroup | undefined => { + return LayoutGroups.find(group => group.slides.includes(layoutId)); +}; +