diff --git a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx index f23f1a5c..bbaefa31 100644 --- a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx @@ -8,13 +8,13 @@ import { Trash2 } from 'lucide-react'; import { toast } from 'sonner'; interface NewSlideProps { setShowNewSlideSelection: (show: boolean) => void; - group: string; + templateID: string; index: number; presentationId: string; } const NewSlide = ({ setShowNewSlideSelection, - group, + templateID, index, presentationId, }: NewSlideProps) => { @@ -25,7 +25,7 @@ const NewSlide = ({ id: uuidv4(), index: index, content: sampleData, - layout_group: group, + layout_group: templateID, layout: id, presentation: presentationId, }; @@ -36,10 +36,8 @@ const NewSlide = ({ toast.error("Error adding new slide"); } }; - const { getFullDataByGroup, loading } = useLayout(); - - const fullData = getFullDataByGroup(group); - + const { getFullDataByTemplateID, loading } = useLayout(); + const fullData = getFullDataByTemplateID(templateID); if (loading) { return ( @@ -73,7 +71,7 @@ const NewSlide = ({ return (
handleNewSlide(sampleData, layoutId)} - key={`${group}-${index}`} + key={`${layoutId}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video" >
diff --git a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx index e466711a..4d04e500 100644 --- a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx @@ -9,7 +9,7 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Slide } from "../types/slide"; -import { useGroupLayouts } from "../hooks/useGroupLayouts"; +import { useTemplateLayouts } from "../hooks/useTemplateLayouts"; interface PresentationModeProps { @@ -33,7 +33,7 @@ const PresentationMode: React.FC = ({ onSlideChange, }) => { - const { renderSlideContent } = useGroupLayouts(); + const { renderSlideContent } = useTemplateLayouts(); // Modify the handleKeyPress to prevent default behavior const handleKeyPress = useCallback( (event: KeyboardEvent) => { diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index d8452d03..9e8e4ea5 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -11,14 +11,19 @@ import { toast } from "sonner"; import * as z from "zod"; import { useDispatch } from "react-redux"; import { setLayoutLoading } from "@/store/slices/presentationGeneration"; + import * as Babel from "@babel/standalone"; import * as Recharts from "recharts"; +import * as d3 from 'd3'; + +import { getHeader } from "../services/api/header"; export interface LayoutInfo { id: string; name?: string; description?: string; json_schema: any; - groupName: string; + templateID: string; + templateName?: string; } export interface FullDataInfo { name: string; @@ -26,43 +31,41 @@ export interface FullDataInfo { schema: any; sampleData: any; fileName: string; - groupName: string; + templateID: string; layoutId: string; } -export interface GroupSetting { +export interface TemplateSetting { description: string; ordered: boolean; default?: boolean; } -export interface GroupedLayoutsResponse { - groupName: string; +export interface TemplateResponse { + templateID: string; + templateName?: string; files: string[]; - settings: GroupSetting | null; + settings: TemplateSetting | null; } export interface LayoutData { layoutsById: Map; - layoutsByGroup: Map>; - groupSettings: Map; - fileMap: Map; - groupedLayouts: Map; + layoutsByTemplateID: Map>; + templateSettings: Map; + fileMap: Map; + templateLayouts: Map; layoutSchema: LayoutInfo[]; - fullDataByGroup: Map; + fullDataByTemplateID: Map; } export interface LayoutContextType { getLayoutById: (layoutId: string) => LayoutInfo | null; - getLayoutByIdAndGroup: ( - layoutId: string, - groupName: string - ) => LayoutInfo | null; - getLayoutsByGroup: (groupName: string) => LayoutInfo[]; - getGroupSetting: (groupName: string) => GroupSetting | null; - getAllGroups: () => string[]; + + getLayoutsByTemplateID: (templateID: string) => LayoutInfo[]; + getTemplateSetting: (templateID: string) => TemplateSetting | null; + getAllTemplateIDs: () => string[]; getAllLayouts: () => LayoutInfo[]; - getFullDataByGroup: (groupName: string) => FullDataInfo[]; + getFullDataByTemplateID: (templateID: string) => FullDataInfo[]; loading: boolean; error: string | null; getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; @@ -76,8 +79,8 @@ const LayoutContext = createContext(undefined); const layoutCache = new Map>(); -const createCacheKey = (groupName: string, fileName: string): string => - `${groupName}/${fileName}`; +const createCacheKey = (templateID: string, fileName: string): string => + `${templateID}/${fileName}`; // Extract Babel compilation logic into a utility function const compileCustomLayout = (layoutCode: string, React: any, z: any) => { @@ -102,11 +105,15 @@ const compileCustomLayout = (layoutCode: string, React: any, z: any) => { "React", "_z", "Recharts", + ` const z = _z; + + const useRef= React.useRef; + const useEffect= React.useEffect; // Expose commonly used Recharts components to compiled layouts - const { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, PieChart, Pie, Cell, AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ComposedChart, ScatterChart, Scatter, FunnelChart, Funnel, TreemapChart, Treemap, SankeyChart, Sankey, RadialBarChart, RadialBar, ReferenceLine, ReferenceDot, ReferenceArea, Brush, ErrorBar, LabelList, Label } = Recharts || {}; - + const { ResponsiveContainer, LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, PieChart, Pie, Cell, AreaChart, Area, RadarChart, Radar, PolarGrid, PolarAngleAxis, PolarRadiusAxis, ComposedChart, ScatterChart, Scatter, FunnelChart, Funnel, TreemapChart, Treemap, SankeyChart, Sankey, RadialBarChart, RadialBar, ReferenceLine, ReferenceDot, ReferenceArea, Brush, ErrorBar, LabelList, Label } = Recharts || {}; + ${compiled} /* everything declared in the string is in scope here */ @@ -134,45 +141,46 @@ export const LayoutProvider: React.FC<{ const [customTemplateFonts, setCustomTemplateFonts] = useState>(new Map()); const dispatch = useDispatch(); - const buildData = async (groupedLayoutsData: GroupedLayoutsResponse[]) => { + const buildData = async (templateData: TemplateResponse[]) => { const layouts: LayoutInfo[] = []; const layoutsById = new Map(); - const layoutsByGroup = new Map>(); - const groupSettingsMap = new Map(); - const fileMap = new Map(); - const groupedLayouts = new Map(); - const fullDataByGroup = new Map(); + const layoutsByTemplateID = new Map>(); + const templateSettingsMap = new Map(); + const fileMap = new Map(); + const templateLayoutsCache = new Map(); + const fullDataByTemplateID = new Map(); // Start preloading process setIsPreloading(true); try { - for (const groupData of groupedLayoutsData) { - // Initialize group - if (!layoutsByGroup.has(groupData.groupName)) { - layoutsByGroup.set(groupData.groupName, new Set()); + for (const template of templateData) { + // Initialize template + if (!layoutsByTemplateID.has(template.templateID)) { + layoutsByTemplateID.set(template.templateID, new Set()); } - fullDataByGroup.set(groupData.groupName, []); + fullDataByTemplateID.set(template.templateID, []); - // group settings or default settings - const settings = groupData.settings || { - description: `${groupData.groupName} presentation layouts`, + // template settings or default settings + const settings = template.settings || { + templateName: template.templateName, + description: `${template.templateID} presentation layouts`, ordered: false, default: false, }; - groupSettingsMap.set(groupData.groupName, settings); - const groupLayouts: LayoutInfo[] = []; - const groupFullData: FullDataInfo[] = []; + templateSettingsMap.set(template.templateID, settings); + const templateLayouts: LayoutInfo[] = []; + const templateFullData: FullDataInfo[] = []; - for (const fileName of groupData.files) { + for (const fileName of template.files) { try { const file = fileName.replace(".tsx", "").replace(".ts", ""); const module = await import( - `@/presentation-templates/${groupData.groupName}/${file}` + `@/presentation-templates/${template.templateID}/${file}` ); if (!module.default) { @@ -193,14 +201,14 @@ export const LayoutProvider: React.FC<{ } // Cache the layout component immediately after import - const cacheKey = createCacheKey(groupData.groupName, fileName); + const cacheKey = createCacheKey(template.templateID, fileName); if (!layoutCache.has(cacheKey)) { layoutCache.set(cacheKey, module.default); } const originalLayoutId = module.layoutId || file.toLowerCase().replace(/layout$/, ""); - const uniqueKey = `${groupData.groupName}:${originalLayoutId}`; + const uniqueKey = `${template.templateID}:${originalLayoutId}`; const layoutName = module.layoutName || file.replace(/([A-Z])/g, " $1").trim(); const layoutDescription = @@ -218,7 +226,8 @@ export const LayoutProvider: React.FC<{ name: layoutName, description: layoutDescription, json_schema: jsonSchema, - groupName: groupData.groupName, + templateID: template.templateID, + templateName: template.templateName, }; const sampleData = module.Schema.parse({}); @@ -228,30 +237,30 @@ export const LayoutProvider: React.FC<{ schema: jsonSchema, sampleData: sampleData, fileName, - groupName: groupData.groupName, + templateID: template.templateID, layoutId: uniqueKey, }; - groupFullData.push(fullData); + templateFullData.push(fullData); layoutsById.set(uniqueKey, layout); - layoutsByGroup.get(groupData.groupName)!.add(uniqueKey); + layoutsByTemplateID.get(template.templateID)!.add(uniqueKey); fileMap.set(uniqueKey, { fileName, - groupName: groupData.groupName, + templateID: template.templateID, }); - groupLayouts.push(layout); + templateLayouts.push(layout); layouts.push(layout); } catch (error) { console.error( - `💥 Error extracting schema for ${fileName} from ${groupData.groupName}:`, + `💥 Error extracting schema for ${fileName} from ${template.templateID}:`, error ); } } - fullDataByGroup.set(groupData.groupName, groupFullData); - // Cache grouped layouts - groupedLayouts.set(groupData.groupName, groupLayouts); + fullDataByTemplateID.set(template.templateID, templateFullData); + // Cache template layouts + templateLayoutsCache.set(template.templateID, templateLayouts); } } catch (err: any) { console.error("Compilation error:", err); @@ -259,12 +268,12 @@ export const LayoutProvider: React.FC<{ return { layoutsById, - layoutsByGroup, - groupSettings: groupSettingsMap, + layoutsByTemplateID, + templateSettings: templateSettingsMap, fileMap, - groupedLayouts, + templateLayoutsCache, layoutSchema: layouts, - fullDataByGroup, + fullDataByTemplateID, }; }; @@ -274,44 +283,44 @@ export const LayoutProvider: React.FC<{ setError(null); dispatch(setLayoutLoading(true)); - const layoutResponse = await fetch("/api/templates"); + const templateResponse = await fetch("/api/templates"); - if (!layoutResponse.ok) { + if (!templateResponse.ok) { throw new Error( - `Failed to fetch layouts: ${layoutResponse.statusText}` + `Failed to fetch layouts: ${templateResponse.statusText}` ); } - const groupedLayoutsData: GroupedLayoutsResponse[] = - await layoutResponse.json(); + const templateData: TemplateResponse[] = + await templateResponse.json(); - if (!groupedLayoutsData || groupedLayoutsData.length === 0) { - setError("No layout groups found"); + if (!templateData || templateData.length === 0) { + setError("No template found"); return; } - const data = await buildData(groupedLayoutsData); + const data = await buildData(templateData); const customLayouts = await LoadCustomLayouts(); setIsPreloading(false); const combinedData = { layoutsById: mergeMaps(data.layoutsById, customLayouts.layoutsById), - layoutsByGroup: mergeMaps( - data.layoutsByGroup, - customLayouts.layoutsByGroup + layoutsByTemplateID: mergeMaps( + data.layoutsByTemplateID, + customLayouts.layoutsByTemplateID ), - groupSettings: mergeMaps( - data.groupSettings, - customLayouts.groupSettings + templateSettings: mergeMaps( + data.templateSettings, + customLayouts.templateSettings ), fileMap: mergeMaps(data.fileMap, customLayouts.fileMap), - groupedLayouts: mergeMaps( - data.groupedLayouts, - customLayouts.groupedLayouts + templateLayouts: mergeMaps( + data.templateLayoutsCache, + customLayouts.templateLayoutsCache ), layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema], - fullDataByGroup: mergeMaps( - data.fullDataByGroup, - customLayouts.fullDataByGroup + fullDataByTemplateID: mergeMaps( + data.fullDataByTemplateID, + customLayouts.fullDataByTemplateID ), }; @@ -338,30 +347,52 @@ export const LayoutProvider: React.FC<{ } const LoadCustomLayouts = async () => { + const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null; + const layouts: LayoutInfo[] = []; const layoutsById = new Map(); - const layoutsByGroup = new Map>(); - const groupSettingsMap = new Map(); - const fileMap = new Map(); - const groupedLayouts = new Map(); - const fullDataByGroup = new Map(); + const layoutsByTemplateID = new Map>(); + const templateSettingsMap = new Map(); + const fileMap = new Map(); + const templateLayoutsCache = new Map(); + const fullDataByTemplateID = new Map(); try { - const customGroupResponse = await fetch( - "/api/v1/ppt/template-management/summary" + const customTemplateResponse = await fetch( + `/api/v1/ppt/template-management/summary`, + { + headers: { + ...getHeader(), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + } + } ); - const customGroupData = await customGroupResponse.json(); + const customTemplateData = await customTemplateResponse.json(); const customFonts = new Map(); - const customGroup = customGroupData.presentations; - for (const group of customGroup) { - const groupName = `custom-${group.presentation_id}`; - fullDataByGroup.set(groupName, []); - if (!layoutsByGroup.has(groupName)) { - layoutsByGroup.set(groupName, new Set()); + const customTemplates = customTemplateData.presentations || []; + for (const templateInfo of customTemplates) { + const pid = + (templateInfo && (templateInfo.presentation_id || templateInfo.presentation || templateInfo.id)) || + ""; + if (!pid) { + // skip invalid entries + continue; } - const presentationId = group.presentation_id; + const templateID = `custom-${pid}`; + const templateName = templateInfo.template?.name || templateID; + fullDataByTemplateID.set(templateID, []); + if (!layoutsByTemplateID.has(templateID)) { + layoutsByTemplateID.set(templateID, new Set()); + } + const presentationId = pid; const customLayoutResponse = await fetch( - `/api/v1/ppt/template-management/get-templates/${presentationId}` + `/api/v1/ppt/template-management/get-templates/${presentationId}`, + { + headers: { + ...getHeader(), + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, + } ); const customLayoutsData = await customLayoutResponse.json(); const allLayout = customLayoutsData.layouts; @@ -370,14 +401,15 @@ export const LayoutProvider: React.FC<{ const settings = { + templateName: templateName, description: `Custom presentation layouts`, ordered: false, default: false, }; - groupSettingsMap.set(`custom-${presentationId}`, settings); - const groupLayouts: LayoutInfo[] = []; - const groupFullData: FullDataInfo[] = []; + templateSettingsMap.set(`custom-${presentationId}`, settings); + const templateLayouts: LayoutInfo[] = []; + const templateFullData: FullDataInfo[] = []; // Helper to create an inline error component for this specific slide const createErrorComponent = (title: string, message: string): React.ComponentType<{ data: any }> => { @@ -480,7 +512,8 @@ export const LayoutProvider: React.FC<{ name: layoutName, description: layoutDescription, json_schema: jsonSchema, - groupName: groupName, + templateID: templateID, + templateName: templateName, }; fullData = { @@ -489,19 +522,19 @@ export const LayoutProvider: React.FC<{ schema: jsonSchema, sampleData: sampleData, fileName: i.layout_name, - groupName: groupName, + templateID: templateID, layoutId: uniqueKey, }; - groupFullData.push(fullData); + templateFullData.push(fullData); layoutsById.set(uniqueKey, layout); - layoutsByGroup.get(groupName)!.add(uniqueKey); + layoutsByTemplateID.get(templateID)!.add(uniqueKey); fileMap.set(uniqueKey, { fileName: i.layout_name, - groupName: groupName, + templateID: templateID, }); - groupLayouts.push(layout); + templateLayouts.push(layout); layouts.push(layout); } catch (e: any) { // Handle compilation/runtime errors during transformation @@ -517,7 +550,8 @@ export const LayoutProvider: React.FC<{ name: layoutName, description: `Failed to compile ${i.layout_name}`, json_schema: {}, - groupName: groupName, + templateID: templateID, + templateName: templateName, }; const fullData: FullDataInfo = { @@ -526,25 +560,25 @@ export const LayoutProvider: React.FC<{ schema: {}, sampleData: {}, fileName: i.layout_name, - groupName: groupName, + templateID: templateID, layoutId: uniqueKey, }; - groupFullData.push(fullData); + templateFullData.push(fullData); layoutsById.set(uniqueKey, layout); - layoutsByGroup.get(groupName)!.add(uniqueKey); + layoutsByTemplateID.get(templateID)!.add(uniqueKey); fileMap.set(uniqueKey, { fileName: i.layout_name, - groupName: groupName, + templateID: templateID, }); - groupLayouts.push(layout); + templateLayouts.push(layout); layouts.push(layout); } } setCustomTemplateFonts(customFonts); - // Cache grouped layouts - groupedLayouts.set(groupName, groupLayouts); - fullDataByGroup.set(groupName, groupFullData); + // Cache template layouts + templateLayoutsCache.set(templateID, templateLayouts); + fullDataByTemplateID.set(templateID, templateFullData); } } catch (err: any) { console.error("Compilation error:", err); @@ -553,12 +587,12 @@ export const LayoutProvider: React.FC<{ return { layoutsById, - layoutsByGroup, - groupSettings: groupSettingsMap, + layoutsByTemplateID, + templateSettings: templateSettingsMap, fileMap, - groupedLayouts, + templateLayoutsCache, layoutSchema: layouts, - fullDataByGroup, + fullDataByTemplateID, }; }; @@ -567,7 +601,7 @@ export const LayoutProvider: React.FC<{ ): React.ComponentType<{ data: any }> | null => { if (!layoutData) return null; - let fileInfo: { fileName: string; groupName: string } | undefined; + let fileInfo: { fileName: string; templateID: string } | undefined; // Search through all fileMap entries to find the layout for (const [key, info] of Array.from(layoutData.fileMap.entries())) { @@ -582,7 +616,7 @@ export const LayoutProvider: React.FC<{ return null; } - const cacheKey = createCacheKey(fileInfo.groupName, fileInfo.fileName); + const cacheKey = createCacheKey(fileInfo.templateID, fileInfo.fileName); // Return cached layout if available if (layoutCache.has(cacheKey)) { @@ -591,7 +625,7 @@ export const LayoutProvider: React.FC<{ // Create and cache layout if not available const file = fileInfo.fileName.replace(".tsx", "").replace(".ts", ""); const Layout = dynamic( - () => import(`@/presentation-templates/${fileInfo.groupName}/${file}`), + () => import(`@/presentation-templates/${fileInfo.templateID}/${file}`), { loading: () => (
@@ -604,11 +638,11 @@ export const LayoutProvider: React.FC<{ return Layout; }; - // Updated accessor methods to handle group-specific lookups + // Updated accessor methods to handle templateID-specific lookups const getLayoutById = (layoutId: string): LayoutInfo | null => { if (!layoutData) return null; - // Search through all entries to find the layout (since we don't know the group) + // Search through all entries to find the layout (since we don't know the templateID) for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) { if (key === layoutId) { return layout; @@ -617,32 +651,26 @@ export const LayoutProvider: React.FC<{ return null; }; - const getLayoutByIdAndGroup = ( - layoutId: string, - groupName: string - ): LayoutInfo | null => { - if (!layoutData) return null; - return layoutData.layoutsById.get(layoutId) || null; + + + const getLayoutsByTemplateID = (templateID: string): LayoutInfo[] => { + return layoutData?.templateLayouts.get(templateID) || []; }; - const getLayoutsByGroup = (groupName: string): LayoutInfo[] => { - return layoutData?.groupedLayouts.get(groupName) || []; + const getTemplateSetting = (templateID: string): TemplateSetting | null => { + return layoutData?.templateSettings.get(templateID) || null; }; - const getGroupSetting = (groupName: string): GroupSetting | null => { - return layoutData?.groupSettings.get(groupName) || null; - }; - - const getAllGroups = (): string[] => { - return layoutData ? Array.from(layoutData.groupSettings.keys()) : []; + const getAllTemplateIDs = (): string[] => { + return layoutData ? Array.from(layoutData.templateSettings.keys()) : []; }; const getAllLayouts = (): LayoutInfo[] => { return layoutData?.layoutSchema || []; }; - const getFullDataByGroup = (groupName: string): FullDataInfo[] => { - return layoutData?.fullDataByGroup.get(groupName) || []; + const getFullDataByTemplateID = (templateID: string): FullDataInfo[] => { + return layoutData?.fullDataByTemplateID.get(templateID) || []; }; const getCustomTemplateFonts = (presentationId: string): string[] | null => { return customTemplateFonts.get(presentationId) || null; @@ -655,12 +683,11 @@ export const LayoutProvider: React.FC<{ const contextValue: LayoutContextType = { getLayoutById, - getLayoutByIdAndGroup, - getLayoutsByGroup, - getGroupSetting, - getAllGroups, + getLayoutsByTemplateID, + getTemplateSetting, + getAllTemplateIDs, getAllLayouts, - getFullDataByGroup, + getFullDataByTemplateID, getCustomTemplateFonts, loading, error, diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx index c950a2e8..1fe9e4d4 100644 --- a/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx +++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx @@ -10,7 +10,7 @@ import { } from "@/components/ui/popover"; import { useRouter } from "next/navigation"; import { toast } from "sonner"; -import { useGroupLayouts } from "@/app/(presentation-generator)/hooks/useGroupLayouts"; +import { useTemplateLayouts } from "@/app/(presentation-generator)/hooks/useTemplateLayouts"; export const PresentationCard = ({ id, @@ -26,7 +26,7 @@ export const PresentationCard = ({ onDeleted?: (presentationId: string) => void; }) => { const router = useRouter(); - const { renderSlideContent } = useGroupLayouts(); + const { renderSlideContent } = useTemplateLayouts(); diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx similarity index 83% rename from servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx rename to servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx index bc848f1b..1d24cb54 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx @@ -8,32 +8,28 @@ import TiptapTextReplacer from "../components/TiptapTextReplacer"; import { updateSlideContent } from "../../../store/slices/presentationGeneration"; import { Loader2 } from "lucide-react"; -export const useGroupLayouts = () => { +export const useTemplateLayouts = () => { const dispatch = useDispatch(); - const { getLayoutByIdAndGroup, getLayoutsByGroup, getLayout, loading } = + const { getLayoutById, getLayout, loading } = useLayout(); - const getGroupLayout = useMemo(() => { + const getTemplateLayout = useMemo(() => { return (layoutId: string, groupName: string) => { - const layout = getLayoutByIdAndGroup(layoutId, groupName); + const layout = getLayoutById(layoutId); if (layout) { return getLayout(layoutId); } return null; }; - }, [getLayoutByIdAndGroup, getLayout]); + }, [getLayoutById, getLayout]); + - const getGroupLayouts = useMemo(() => { - return (groupName: string) => { - return getLayoutsByGroup(groupName); - }; - }, [getLayoutsByGroup]); // Render slide content with group validation, automatic Tiptap text editing, and editable images/icons const renderSlideContent = useMemo(() => { return (slide: any, isEditMode: boolean) => { - - const Layout = getGroupLayout(slide.layout, slide.layout_group); + + const Layout = getTemplateLayout(slide.layout, slide.layout_group); if (loading) { return (
@@ -92,11 +88,10 @@ export const useGroupLayouts = () => { ); }; - }, [getGroupLayout, dispatch]); + }, [getTemplateLayout, dispatch]); return { - getGroupLayout, - getGroupLayouts, + getTemplateLayout, renderSlideContent, loading, }; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx index fd6a8ae2..535f3d3b 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx @@ -2,19 +2,21 @@ import React from "react"; import { usePathname } from "next/navigation"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { Button } from "@/components/ui/button"; -import { LoadingState, LayoutGroup } from "../types/index"; +import { LoadingState, Template } from "../types/index"; interface GenerateButtonProps { loadingState: LoadingState; streamState: { isStreaming: boolean, isLoading: boolean }; - selectedLayoutGroup: LayoutGroup | null; + selectedTemplate: Template | null; onSubmit: () => void; + outlineCount: number; } const GenerateButton: React.FC = ({ loadingState, streamState, - selectedLayoutGroup, + selectedTemplate, + outlineCount, onSubmit }) => { const pathname = usePathname(); @@ -27,8 +29,8 @@ const GenerateButton: React.FC = ({ const getButtonText = () => { if (loadingState.isLoading) return loadingState.message; if (streamState.isLoading || streamState.isStreaming) return "Loading..."; - if (!selectedLayoutGroup) return "Select a Template"; - return "Generate Presentation"; + if (!selectedTemplate) return "Select a Template"; + return `Generate Presentation (${outlineCount * 1} credits)`; }; return ( @@ -36,7 +38,7 @@ const GenerateButton: React.FC = ({ disabled={isDisabled} onClick={() => { if (!streamState.isLoading && !streamState.isStreaming) { - if (!selectedLayoutGroup) { + if (!selectedTemplate) { trackEvent(MixpanelEvent.Outline_Select_Template_Button_Clicked, { pathname }); } else { trackEvent(MixpanelEvent.Outline_Generate_Presentation_Button_Clicked, { pathname }); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx deleted file mode 100644 index ed2452fc..00000000 --- a/servers/nextjs/app/(presentation-generator)/outline/components/LayoutSelection.tsx +++ /dev/null @@ -1,244 +0,0 @@ -"use client"; -import React, { useEffect } from "react"; -import { useLayout } from "../../context/LayoutContext"; -import GroupLayouts from "./GroupLayouts"; - -import { LayoutGroup } from "../types/index"; -interface LayoutSelectionProps { - selectedLayoutGroup: LayoutGroup | null; - onSelectLayoutGroup: (group: LayoutGroup) => void; -} - -const LayoutSelection: React.FC = ({ - selectedLayoutGroup, - onSelectLayoutGroup, -}) => { - const { - getLayoutsByGroup, - getGroupSetting, - getAllGroups, - getFullDataByGroup, - loading, - } = useLayout(); - - const [summaryMap, setSummaryMap] = React.useState< - Record< - string, - { lastUpdatedAt?: number; name?: string; description?: string } - > - >({}); - - useEffect(() => { - // Fetch custom templates summary to get last_updated_at and template meta for sorting and display - fetch("/api/v1/ppt/template-management/summary") - .then((res) => res.json()) - .then((data) => { - const map: Record< - string, - { lastUpdatedAt?: number; name?: string; description?: string } - > = {}; - if (data && Array.isArray(data.presentations)) { - for (const p of data.presentations) { - const slug = `custom-${p.presentation_id}`; - map[slug] = { - lastUpdatedAt: p.last_updated_at - ? new Date(p.last_updated_at).getTime() - : 0, - name: p.template?.name, - description: p.template?.description, - }; - } - } - setSummaryMap(map); - }) - .catch(() => setSummaryMap({})); - }, []); - - const layoutGroups: LayoutGroup[] = React.useMemo(() => { - const groups = getAllGroups(); - - - if (groups.length === 0) return []; - - 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-"); - return { - id: groupName, - name: isCustom && customMeta?.name ? customMeta.name : groupName, - description: - isCustom && customMeta?.description - ? customMeta.description - : settings?.description || `${groupName} presentation templates`, - ordered: settings?.ordered || false, - default: settings?.default || false, - }; - }); - - // Sort groups to put default first, then by name - return Groups.sort((a, b) => { - if (a.default && !b.default) return -1; - if (!a.default && b.default) return 1; - return a.name.localeCompare(b.name); - }); - }, [ - getAllGroups, - getLayoutsByGroup, - getGroupSetting, - getFullDataByGroup, - summaryMap, - ]); - - const inBuiltGroups = React.useMemo( - () => layoutGroups.filter((g) => !g.id.toLowerCase().startsWith("custom-")), - [layoutGroups] - ); - const customGroups = React.useMemo(() => { - const unsorted = layoutGroups.filter((g) => - g.id.toLowerCase().startsWith("custom-") - ); - // Sort by last_updated_at desc using summaryMap keyed by slug id - return unsorted.sort( - (a, b) => - (summaryMap[b.id]?.lastUpdatedAt || 0) - - (summaryMap[a.id]?.lastUpdatedAt || 0) - ); - }, [layoutGroups, summaryMap]); - - // Auto-select first group when groups are loaded - useEffect(() => { - if (layoutGroups.length > 0 && !selectedLayoutGroup) { - const defaultGroup = - layoutGroups.find((g) => g.default) || layoutGroups[0]; - const slides = getLayoutsByGroup(defaultGroup.id); - - onSelectLayoutGroup({ - ...defaultGroup, - slides: slides, - }); - } - }, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]); - useEffect(() => { - if (loading) { - return; - } - const existingScript = document.querySelector( - 'script[src*="tailwindcss.com"]' - ); - if (!existingScript) { - const script = document.createElement("script"); - script.src = "https://cdn.tailwindcss.com"; - script.async = true; - document.head.appendChild(script); - } - }, []); - - if (loading) { - return ( -
-
- {[1, 2, 3, 4].map((i) => ( -
-
-
-
- {[1, 2, 3].map((j) => ( -
- ))} -
-
- ))} -
-
- ); - } - - if (layoutGroups.length === 0) { - return ( -
-
-
- No Templates Available -
-

- No presentation templates could be loaded. Please try refreshing the - page. -

-
-
- ); - } - - const handleLayoutGroupSelection = (group: LayoutGroup) => { - const slides = getLayoutsByGroup(group.id); - onSelectLayoutGroup({ - ...group, - slides: slides, - }); - }; - - return ( -
- {/* In Built Templates */} -
-

- In Built Templates -

-
- {inBuiltGroups.map((group) => ( - - ))} -
-
- - {/* Custom AI Templates */} -
-
-

- Custom AI Templates -

-
- {customGroups.length === 0 ? ( -
- No custom templates. Create one from "Create Template" menu. -
- ) : ( -
- {customGroups.map((group) => ( - - ))} -
- )} -
-
- ); -}; - -export default LayoutSelection; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 235328d1..a5267fa1 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -7,14 +7,14 @@ import { useSelector } from "react-redux"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; import OutlineContent from "./OutlineContent"; -import LayoutSelection from "./LayoutSelection"; import EmptyStateView from "./EmptyStateView"; import GenerateButton from "./GenerateButton"; -import { TABS, LayoutGroup } from "../types/index"; +import { TABS, Template } from "../types/index"; import { useOutlineStreaming } from "../hooks/useOutlineStreaming"; import { useOutlineManagement } from "../hooks/useOutlineManagement"; import { usePresentationGeneration } from "../hooks/usePresentationGeneration"; +import TemplateSelection from "./TemplateSelection"; const OutlinePage: React.FC = () => { const { presentation_id, outlines } = useSelector( @@ -22,14 +22,14 @@ const OutlinePage: React.FC = () => { ); const [activeTab, setActiveTab] = useState(TABS.OUTLINE); - const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(null); + const [selectedTemplate, setSelectedTemplate] = useState