From 45e27bf96f31f3bca88f7cb6aee05e63d5f7c588 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Wed, 11 Feb 2026 14:39:30 +0545 Subject: [PATCH] feat: New Templates and refactor template loading approach --- Dockerfile.dev | 2 +- .../components/NewSlide.tsx | 99 ++- .../components/PresentationMode.tsx | 76 +- .../components/PresentationRender.tsx | 80 ++ .../components/V1ContentRender.tsx | 139 ++++ .../context/LayoutContext.tsx | 713 ------------------ .../custom-template/hooks/useLayoutSaving.ts | 12 +- .../custom-template/page.tsx | 28 +- .../dashboard/components/PresentationCard.tsx | 6 +- .../hooks/useTemplateLayouts.tsx | 98 --- .../outline/components/CustomTemplateCard.tsx | 98 +++ .../outline/components/GenerateButton.tsx | 3 +- .../outline/components/OutlinePage.tsx | 3 +- .../outline/components/TemplateLayouts.tsx | 87 --- .../outline/components/TemplateSelection.tsx | 229 ++---- .../hooks/usePresentationGeneration.ts | 109 ++- .../pdf-maker/PdfMakerPage.tsx | 21 +- .../components/PresentationPage.tsx | 32 +- .../presentation/components/SidePanel.tsx | 9 +- .../presentation/components/SlideContent.tsx | 31 +- .../presentation/components/SortableSlide.tsx | 41 +- .../services/api/template.ts | 36 + .../hooks/useTemplateLayoutsAutoSave.ts | 179 +++++ .../template-preview/[slug]/page.tsx | 556 ++++++-------- .../components/LoadingStates.tsx | 4 +- .../template-preview/page.tsx | 421 +++++------ servers/nextjs/app/hooks/compileLayout.ts | 132 ++++ .../nextjs/app/hooks/useCustomTemplates.ts | 469 ++++++++++++ servers/nextjs/app/layout.tsx | 7 +- .../ExampleSlideLayout.tsx | 2 +- .../ExampleSlideLayoutTemplate.tsx | 2 +- .../presentation-templates/defaultSchemes.ts | 0 .../general/BasicInfoSlideLayout.tsx | 27 +- .../general/BulletIconsOnlySlideLayout.tsx | 38 +- .../general/BulletWithIconsSlideLayout.tsx | 36 +- .../general/ChartWithBulletsSlideLayout.tsx | 191 +++-- .../general/IntroSlideLayout.tsx | 41 +- .../general/MetricsSlideLayout.tsx | 38 +- .../general/MetricsWithImageSlideLayout.tsx | 32 +- .../general/NumberedBulletsSlideLayout.tsx | 36 +- .../general/QuoteSlideLayout.tsx | 38 +- .../general/TableInfoSlideLayout.tsx | 54 +- .../general/TableOfContentsSlideLayout.tsx | 36 +- .../general/TeamSlideLayout.tsx | 46 +- .../general/settings.json | 0 .../app/presentation-templates/index.tsx | 453 +++++++++++ .../modern/BulletWithIconsSlideLayout.tsx} | 97 +-- .../BulletsWithIconsDescriptionGrid.tsx | 196 +++++ .../modern/ChartOrTableWithDescription.tsx | 286 +++++++ .../ChartOrTableWithMetricsDescription.tsx} | 142 ++-- .../modern/ImageAndDescriptionLayout.tsx} | 65 +- .../ImageListWithDescriptionSlideLayout.tsx | 193 +++++ .../modern/ImagesWithDescriptionLayout.tsx} | 108 +-- .../modern/IntroSlideLayout.tsx | 141 ++++ .../modern/MetricsWithDescription.tsx} | 69 +- .../modern/TableOfContentsLayout.tsx | 112 +++ .../modern/settings.json | 0 .../BulletIconsOnlySlideLayout.tsx | 212 ++++++ .../BulletWithIconsSlideLayout.tsx | 182 +++++ .../ChallengeAndOutcomeWithOneStat.tsx | 202 +++++ .../ChartWithBulletsSlideLayout.tsx | 582 ++++++++++++++ .../GridBasedEightMetricsSnapshots.tsx | 176 +++++ .../HeadlineDescriptionWithDoubleImage.tsx | 128 ++++ .../HeadlineDescriptionWithImage.tsx | 104 +++ .../HeadlineTextWithBulletsAndStats.tsx | 144 ++++ .../neo-general/IndexedThreeColumnList.tsx | 163 ++++ .../LayoutTextBlockWithMetricCards.tsx | 324 ++++++++ .../neo-general/LeftAlignQuote.tsx | 118 +++ .../MetricsWithImageSlideLayout.tsx | 144 ++++ .../neo-general/MultiChartGridSlideLayout.tsx | 671 ++++++++++++++++ .../NumberedBulletsSlideLayout.tsx | 163 ++++ .../neo-general/QuoteSlideLayout.tsx | 139 ++++ .../TableOfContentWithoutPageNumber.tsx | 163 ++++ .../neo-general/TeamSlideLayout.tsx | 184 +++++ .../TextSplitWithEmphasisBlock.tsx | 119 +++ .../ThankYouContactInfoFooterImageSlide.tsx | 120 +++ .../neo-general/Timeline.tsx | 179 +++++ ...leDescriptionMultiChartGridWithBullets.tsx | 671 ++++++++++++++++ ...leDescriptionMultiChartGridWithMetrics.tsx | 698 +++++++++++++++++ .../neo-general/TitleDescriptionWithTable.tsx | 150 ++++ ...itleMetricValueMetricLabelFunnelStages.tsx | 133 ++++ .../neo-general/TitleMetricsWithChart.tsx | 599 +++++++++++++++ .../TitleThreeColumnRiskConstraints.tsx | 124 +++ ...TitleTopDescriptionFourTeamMembersGrid.tsx | 156 ++++ .../neo-general/TitleWithFullWidthChart.tsx | 588 +++++++++++++++ ...itleWithGridBasedHeadingAndDescription.tsx | 129 ++++ .../neo-general/settings.json | 5 + .../neo-modern/TitleDescriptionBulletList.tsx | 92 +++ .../TitleDescriptionContactList.tsx | 153 ++++ .../TitleDescriptionDualMetricsGrid.tsx | 129 ++++ .../TitleDescriptionIconTimeline.tsx | 134 ++++ .../neo-modern/TitleDescriptionImageRight.tsx | 84 +++ .../TitleDescriptionMetricsChart.tsx | 478 ++++++++++++ .../TitleDescriptionMetricsImage.tsx | 131 ++++ .../TitleDescriptionMultiChartGrid.tsx | 533 +++++++++++++ ...leDescriptionMultiChartGridWithBullets.tsx | 557 ++++++++++++++ ...leDescriptionMultiChartGridWithMetrics.tsx | 561 ++++++++++++++ .../neo-modern/TitleDescriptionTable.tsx | 127 ++++ .../neo-modern/TitleDualComparisonCards.tsx | 109 +++ .../neo-modern/TitleDualComparisonCharts.tsx | 485 ++++++++++++ .../TitleHorizontalAlternatingTimeline.tsx | 151 ++++ .../neo-modern/TitleKpiSnapshotGrid.tsx | 109 +++ .../neo-modern/TitleSubtitlesChart.tsx | 510 +++++++++++++ .../neo-modern/TitleTwoColumnNumberedList.tsx | 135 ++++ .../neo-modern/settings.json | 5 + .../neo-standard/TitleBadgeChart.tsx | 421 +++++++++++ .../TitleDescriptionBulletList.tsx | 90 +++ .../TitleDescriptionContactCards.tsx | 111 +++ .../neo-standard/TitleDescriptionIconList.tsx | 137 ++++ .../TitleDescriptionImageRight.tsx | 112 +++ .../TitleDescriptionMultiChartGrid.tsx | 534 +++++++++++++ ...leDescriptionMultiChartGridWithBullets.tsx | 558 ++++++++++++++ ...leDescriptionMultiChartGridWithMetrics.tsx | 562 ++++++++++++++ .../TitleDescriptionRadialCards.tsx | 162 ++++ .../neo-standard/TitleDescriptionTable.tsx | 139 ++++ .../neo-standard/TitleDescriptionTimeline.tsx | 141 ++++ .../TitleDualChartsComparison.tsx | 475 ++++++++++++ .../neo-standard/TitleDualComparisonCards.tsx | 139 ++++ .../neo-standard/TitleKpiGrid.tsx | 111 +++ .../neo-standard/TitleMetricsChart.tsx | 493 ++++++++++++ .../neo-standard/TitleMetricsImage.tsx | 122 +++ .../neo-standard/TitlePointsDonutGrid.tsx | 163 ++++ .../neo-standard/settings.json | 5 + .../neo-swift/TitleCenteredChart.tsx | 463 ++++++++++++ .../neo-swift/TitleChartMetricsSidebar.tsx | 553 ++++++++++++++ .../neo-swift/TitleDescriptionBulletList.tsx | 106 +++ .../neo-swift/TitleDescriptionDataTable.tsx | 150 ++++ .../TitleDescriptionFourChartsSixBullets.tsx | 542 +++++++++++++ .../neo-swift/TitleDescriptionImageRight.tsx | 101 +++ .../neo-swift/TitleDescriptionMetricsGrid.tsx | 109 +++ .../TitleDescriptionMetricsGridImage.tsx | 177 +++++ .../TitleDescriptionSixChartsFourMetrics.tsx | 538 +++++++++++++ .../TitleDescriptionSixChartsGrid.tsx | 510 +++++++++++++ .../neo-swift/TitleDualComparisonBlocks.tsx | 137 ++++ .../TitleLabelDescriptionStatCards.tsx | 154 ++++ .../TitleSubtitleTeamMemberCards.tsx | 165 ++++ .../TitleTaglineDescriptionNumberedSteps.tsx | 144 ++++ .../TitleThreeByThreeMetricsGrid.tsx | 162 ++++ .../neo-swift/settings.json | 5 + .../standard/ChartLeftTextRightLayout.tsx | 75 +- .../standard/ContactLayout.tsx | 25 +- .../HeadingBulletImageDescriptionLayout.tsx | 19 +- .../standard/IconBulletDescriptionLayout.tsx | 36 +- .../standard/IconImageDescriptionLayout.tsx | 26 +- .../ImageListWithDescriptionLayout.tsx | 24 +- .../standard/IntroSlideLayout.tsx | 31 +- .../standard/MetricsDescriptionLayout.tsx | 27 +- .../NumberedBulletSingleImageLayout.tsx | 27 +- .../standard/TableOfContentsLayout.tsx | 30 +- .../standard/VisualMetricsSlideLayout.tsx | 38 +- .../standard/settings.json | 0 .../BulletsWithIconsTitleDescription.tsx | 37 +- .../swift/IconBulletListDescription.tsx | 35 +- .../swift/ImageListDescription.tsx | 29 +- .../swift/IntroSlideLayout.tsx | 43 +- .../swift/MetricsNumbers.tsx | 36 +- .../swift/SimpleBulletPointsLayout.tsx | 26 +- .../swift/TableOfContents.tsx | 28 +- .../swift/TableorChart.tsx | 117 +-- .../presentation-templates/swift/Timeline.tsx | 46 +- .../swift/settings.json | 0 .../app/presentation-templates/utils.ts | 91 +++ servers/nextjs/app/schema/page.tsx | 73 +- .../modern/1IntroSlideLayout.tsx | 136 ---- .../modern/4SolutionSlideLayout.tsx | 170 ----- .../modern/5ProductOverviewSlideLayout.tsx | 227 ------ .../modern/7MarketValidationSlideLayout.tsx | 194 ----- .../modern/9BusinessModelSlideLayout.tsx | 155 ---- .../modern/z11ThankYouSlideLayout.tsx | 136 ---- 169 files changed, 25109 insertions(+), 3471 deletions(-) create mode 100644 servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx create mode 100644 servers/nextjs/app/(presentation-generator)/components/V1ContentRender.tsx delete mode 100644 servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx delete mode 100644 servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx create mode 100644 servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx delete mode 100644 servers/nextjs/app/(presentation-generator)/outline/components/TemplateLayouts.tsx create mode 100644 servers/nextjs/app/(presentation-generator)/services/api/template.ts create mode 100644 servers/nextjs/app/(presentation-generator)/template-preview/[slug]/hooks/useTemplateLayoutsAutoSave.ts create mode 100644 servers/nextjs/app/hooks/compileLayout.ts create mode 100644 servers/nextjs/app/hooks/useCustomTemplates.ts rename servers/nextjs/{ => app}/presentation-templates/ExampleSlideLayout.tsx (97%) rename servers/nextjs/{ => app}/presentation-templates/ExampleSlideLayoutTemplate.tsx (93%) rename servers/nextjs/{ => app}/presentation-templates/defaultSchemes.ts (100%) rename servers/nextjs/{ => app}/presentation-templates/general/BasicInfoSlideLayout.tsx (69%) rename servers/nextjs/{ => app}/presentation-templates/general/BulletIconsOnlySlideLayout.tsx (80%) rename servers/nextjs/{ => app}/presentation-templates/general/BulletWithIconsSlideLayout.tsx (79%) rename servers/nextjs/{ => app}/presentation-templates/general/ChartWithBulletsSlideLayout.tsx (57%) rename servers/nextjs/{ => app}/presentation-templates/general/IntroSlideLayout.tsx (69%) rename servers/nextjs/{ => app}/presentation-templates/general/MetricsSlideLayout.tsx (76%) rename servers/nextjs/{ => app}/presentation-templates/general/MetricsWithImageSlideLayout.tsx (74%) rename servers/nextjs/{ => app}/presentation-templates/general/NumberedBulletsSlideLayout.tsx (76%) rename servers/nextjs/{ => app}/presentation-templates/general/QuoteSlideLayout.tsx (72%) rename servers/nextjs/{ => app}/presentation-templates/general/TableInfoSlideLayout.tsx (73%) rename servers/nextjs/{ => app}/presentation-templates/general/TableOfContentsSlideLayout.tsx (74%) rename servers/nextjs/{ => app}/presentation-templates/general/TeamSlideLayout.tsx (78%) rename servers/nextjs/{ => app}/presentation-templates/general/settings.json (100%) create mode 100644 servers/nextjs/app/presentation-templates/index.tsx rename servers/nextjs/{presentation-templates/modern/3ProblemSlideLayout.tsx => app/presentation-templates/modern/BulletWithIconsSlideLayout.tsx} (53%) create mode 100644 servers/nextjs/app/presentation-templates/modern/BulletsWithIconsDescriptionGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/modern/ChartOrTableWithDescription.tsx rename servers/nextjs/{presentation-templates/modern/8CompanyTractionSlideLayout.tsx => app/presentation-templates/modern/ChartOrTableWithMetricsDescription.tsx} (58%) rename servers/nextjs/{presentation-templates/modern/2AboutCompanySlideLayout.tsx => app/presentation-templates/modern/ImageAndDescriptionLayout.tsx} (64%) create mode 100644 servers/nextjs/app/presentation-templates/modern/ImageListWithDescriptionSlideLayout.tsx rename servers/nextjs/{presentation-templates/modern/z10TeamSlideLayout.tsx => app/presentation-templates/modern/ImagesWithDescriptionLayout.tsx} (62%) create mode 100644 servers/nextjs/app/presentation-templates/modern/IntroSlideLayout.tsx rename servers/nextjs/{presentation-templates/modern/6MarketSizeSlideLayout.tsx => app/presentation-templates/modern/MetricsWithDescription.tsx} (63%) create mode 100644 servers/nextjs/app/presentation-templates/modern/TableOfContentsLayout.tsx rename servers/nextjs/{ => app}/presentation-templates/modern/settings.json (100%) create mode 100644 servers/nextjs/app/presentation-templates/neo-general/BulletIconsOnlySlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/BulletWithIconsSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/ChallengeAndOutcomeWithOneStat.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/ChartWithBulletsSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/GridBasedEightMetricsSnapshots.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/HeadlineDescriptionWithDoubleImage.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/HeadlineDescriptionWithImage.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/HeadlineTextWithBulletsAndStats.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/IndexedThreeColumnList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/LayoutTextBlockWithMetricCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/LeftAlignQuote.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/MetricsWithImageSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/MultiChartGridSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/NumberedBulletsSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/QuoteSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TableOfContentWithoutPageNumber.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TeamSlideLayout.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TextSplitWithEmphasisBlock.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/ThankYouContactInfoFooterImageSlide.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/Timeline.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleDescriptionMultiChartGridWithBullets.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleDescriptionMultiChartGridWithMetrics.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleDescriptionWithTable.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleMetricValueMetricLabelFunnelStages.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleMetricsWithChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleThreeColumnRiskConstraints.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleTopDescriptionFourTeamMembersGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleWithFullWidthChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/TitleWithGridBasedHeadingAndDescription.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-general/settings.json create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionBulletList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionContactList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionDualMetricsGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionIconTimeline.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionImageRight.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionMetricsChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionMetricsImage.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionMultiChartGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionMultiChartGridWithBullets.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionMultiChartGridWithMetrics.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDescriptionTable.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDualComparisonCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleDualComparisonCharts.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleHorizontalAlternatingTimeline.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleKpiSnapshotGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleSubtitlesChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/TitleTwoColumnNumberedList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-modern/settings.json create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleBadgeChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionBulletList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionContactCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionIconList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionImageRight.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionMultiChartGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionMultiChartGridWithBullets.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionMultiChartGridWithMetrics.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionRadialCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionTable.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDescriptionTimeline.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDualChartsComparison.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleDualComparisonCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleKpiGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleMetricsChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitleMetricsImage.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/TitlePointsDonutGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-standard/settings.json create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleCenteredChart.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleChartMetricsSidebar.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionBulletList.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionDataTable.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionFourChartsSixBullets.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionImageRight.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionMetricsGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionMetricsGridImage.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionSixChartsFourMetrics.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDescriptionSixChartsGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleDualComparisonBlocks.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleLabelDescriptionStatCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleSubtitleTeamMemberCards.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleTaglineDescriptionNumberedSteps.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/TitleThreeByThreeMetricsGrid.tsx create mode 100644 servers/nextjs/app/presentation-templates/neo-swift/settings.json rename servers/nextjs/{ => app}/presentation-templates/standard/ChartLeftTextRightLayout.tsx (57%) rename servers/nextjs/{ => app}/presentation-templates/standard/ContactLayout.tsx (82%) rename servers/nextjs/{ => app}/presentation-templates/standard/HeadingBulletImageDescriptionLayout.tsx (84%) rename servers/nextjs/{ => app}/presentation-templates/standard/IconBulletDescriptionLayout.tsx (80%) rename servers/nextjs/{ => app}/presentation-templates/standard/IconImageDescriptionLayout.tsx (88%) rename servers/nextjs/{ => app}/presentation-templates/standard/ImageListWithDescriptionLayout.tsx (82%) rename servers/nextjs/{ => app}/presentation-templates/standard/IntroSlideLayout.tsx (81%) rename servers/nextjs/{ => app}/presentation-templates/standard/MetricsDescriptionLayout.tsx (84%) rename servers/nextjs/{ => app}/presentation-templates/standard/NumberedBulletSingleImageLayout.tsx (83%) rename servers/nextjs/{ => app}/presentation-templates/standard/TableOfContentsLayout.tsx (76%) rename servers/nextjs/{ => app}/presentation-templates/standard/VisualMetricsSlideLayout.tsx (80%) rename servers/nextjs/{ => app}/presentation-templates/standard/settings.json (100%) rename servers/nextjs/{ => app}/presentation-templates/swift/BulletsWithIconsTitleDescription.tsx (83%) rename servers/nextjs/{ => app}/presentation-templates/swift/IconBulletListDescription.tsx (79%) rename servers/nextjs/{ => app}/presentation-templates/swift/ImageListDescription.tsx (78%) rename servers/nextjs/{ => app}/presentation-templates/swift/IntroSlideLayout.tsx (77%) rename servers/nextjs/{ => app}/presentation-templates/swift/MetricsNumbers.tsx (76%) rename servers/nextjs/{ => app}/presentation-templates/swift/SimpleBulletPointsLayout.tsx (77%) rename servers/nextjs/{ => app}/presentation-templates/swift/TableOfContents.tsx (79%) rename servers/nextjs/{ => app}/presentation-templates/swift/TableorChart.tsx (61%) rename servers/nextjs/{ => app}/presentation-templates/swift/Timeline.tsx (75%) rename servers/nextjs/{ => app}/presentation-templates/swift/settings.json (100%) create mode 100644 servers/nextjs/app/presentation-templates/utils.ts delete mode 100644 servers/nextjs/presentation-templates/modern/1IntroSlideLayout.tsx delete mode 100644 servers/nextjs/presentation-templates/modern/4SolutionSlideLayout.tsx delete mode 100644 servers/nextjs/presentation-templates/modern/5ProductOverviewSlideLayout.tsx delete mode 100644 servers/nextjs/presentation-templates/modern/7MarketValidationSlideLayout.tsx delete mode 100644 servers/nextjs/presentation-templates/modern/9BusinessModelSlideLayout.tsx delete mode 100644 servers/nextjs/presentation-templates/modern/z11ThankYouSlideLayout.tsx diff --git a/Dockerfile.dev b/Dockerfile.dev index 89c2b6a8..7f168f4a 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -24,7 +24,7 @@ ENV TEMP_DIRECTORY=/tmp/presenton ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium # Install ollama -RUN curl -fsSL http://ollama.com/install.sh | sh +# RUN curl -fsSL http://ollama.com/install.sh | sh # Install dependencies for FastAPI RUN pip install aiohttp aiomysql aiosqlite asyncpg fastapi[standard] \ diff --git a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx index bbaefa31..65d5c17d 100644 --- a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx @@ -1,32 +1,60 @@ -import React from "react"; +'use client' +import React, { useEffect, useState, memo, useCallback } from "react"; import { useDispatch } from "react-redux"; import { addNewSlide } from "@/store/slices/presentationGeneration"; import { Loader2 } from "lucide-react"; -import { useLayout, FullDataInfo } from "../context/LayoutContext"; import { v4 as uuidv4 } from "uuid"; import { Trash2 } from 'lucide-react'; import { toast } from 'sonner'; -interface NewSlideProps { +import { getCustomTemplateDetails } from "@/app/hooks/useCustomTemplates"; +import { getTemplatesByTemplateName } from "@/app/presentation-templates"; + +interface LayoutItemProps { + layout: any; + onSelect: (sampleData: any, layoutId: string) => void; +} + +const LayoutItem = memo(({ layout, onSelect }: LayoutItemProps) => { + const { component: LayoutComponent, sampleData, layoutId } = layout; + return ( +
onSelect(sampleData, layoutId)} + className="relative cursor-pointer overflow-hidden aspect-video" + > +
+
+ +
+
+ ); +}); + +LayoutItem.displayName = 'LayoutItem'; +interface NewSlideV1Props { setShowNewSlideSelection: (show: boolean) => void; templateID: string; index: number; presentationId: string; } -const NewSlide = ({ +const NewSlideV1 = ({ setShowNewSlideSelection, templateID, index, presentationId, -}: NewSlideProps) => { +}: NewSlideV1Props) => { const dispatch = useDispatch(); - const handleNewSlide = (sampleData: any, id: string) => { + const [layouts, setLayouts] = useState([]); + const [loading, setLoading] = useState(false); + + const isCustomTemplate = templateID.startsWith("custom-"); + const handleNewSlide = useCallback((sampleData: any, id: string) => { try { const newSlide = { id: uuidv4(), index: index, content: sampleData, layout_group: templateID, - layout: id, + layout: isCustomTemplate ? `${templateID}:${id}` : id, presentation: presentationId, }; dispatch(addNewSlide({ slideData: newSlide, index })); @@ -35,9 +63,32 @@ const NewSlide = ({ console.error(error); toast.error("Error adding new slide"); } - }; - const { getFullDataByTemplateID, loading } = useLayout(); - const fullData = getFullDataByTemplateID(templateID); + }, [index, templateID, presentationId, dispatch, setShowNewSlideSelection]); + + + + useEffect(() => { + if (layouts.length > 0 || loading) return; + + const fetchLayouts = async () => { + + if (isCustomTemplate) { + setLoading(true); + const customTemplateId = templateID.split("custom-")[1]; + const templateDetails = await getCustomTemplateDetails(customTemplateId, "Custom Template", "User-created template"); + setLayouts(templateDetails?.layouts || []); + setLoading(false); + } else { + setLoading(true); + const templateDetails = getTemplatesByTemplateName(templateID); + setLayouts(templateDetails || []); + setLoading(false); + } + } + fetchLayouts(); + + }, []); + if (loading) { return ( @@ -66,24 +117,20 @@ const NewSlide = ({ />
- {fullData.map((layout: FullDataInfo, index: number) => { - const { component: LayoutComponent, sampleData, layoutId } = layout; - return ( -
handleNewSlide(sampleData, layoutId)} - key={`${layoutId}-${index}`} - className=" relative cursor-pointer overflow-hidden aspect-video" - > -
-
- -
-
- ); - })} + {layouts.map((layout: any) => ( + + ))}
); }; -export default NewSlide; +export default NewSlideV1; + + + + diff --git a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx index 4d04e500..a75663e0 100644 --- a/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/PresentationMode.tsx @@ -1,5 +1,5 @@ "use client"; -import React, { useCallback, useEffect } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; import { ChevronLeft, ChevronRight, @@ -9,7 +9,7 @@ import { } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Slide } from "../types/slide"; -import { useTemplateLayouts } from "../hooks/useTemplateLayouts"; +import { V1ContentRender } from "./V1ContentRender"; interface PresentationModeProps { @@ -32,8 +32,33 @@ const PresentationMode: React.FC = ({ onExit, onSlideChange, + }) => { - const { renderSlideContent } = useTemplateLayouts(); + if (slides === undefined || slides === null || slides.length === 0) { + return null; + } + + + + const recomputeScale = useCallback(() => { + if (typeof window === "undefined") return; + const padding = isFullscreen ? 0 : 64; // match p-8 when not fullscreen + const fullscreenMargin = isFullscreen ? 16 : 0; // small safety margin to prevent clipping + const availableWidth = Math.max(window.innerWidth - padding - fullscreenMargin, 0); + const availableHeight = Math.max(window.innerHeight - padding - fullscreenMargin, 0); + const baseW = 1280; + const baseH = 720; + const s = Math.min(availableWidth / baseW, availableHeight / baseH); + + }, [isFullscreen]); + + useEffect(() => { + recomputeScale(); + window.addEventListener("resize", recomputeScale); + return () => window.removeEventListener("resize", recomputeScale); + }, [recomputeScale]); + + // Modify the handleKeyPress to prevent default behavior const handleKeyPress = useCallback( (event: KeyboardEvent) => { @@ -54,6 +79,11 @@ const PresentationMode: React.FC = ({ } break; case "Escape": + // If fullscreen is active, only exit fullscreen on first ESC. Second ESC exits present mode. + if (document.fullscreenElement) { + try { document.exitFullscreen(); } catch (_) { } + return; + } onExit(); break; case "f": @@ -62,7 +92,7 @@ const PresentationMode: React.FC = ({ break; } }, - [currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle] + [currentSlide, slides.length, onSlideChange, onExit, onFullscreenToggle, isFullscreen] ); // Add both keydown and keyup listeners @@ -118,7 +148,8 @@ const PresentationMode: React.FC = ({ return (
@@ -128,6 +159,7 @@ const PresentationMode: React.FC = ({
- + {currentSlide + 1} / {slides.length}
)} - {/* Current Slide */} -
-
- {slides[currentSlide] && - renderSlideContent(slides[currentSlide], false)} + {/* Slides (all mounted, only current visible) */} +
+
+
+ {slides.length > 0 && slides.map((slide, index) => ( +
+ +
+ ))} +
diff --git a/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx b/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx new file mode 100644 index 00000000..4361845a --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/components/PresentationRender.tsx @@ -0,0 +1,80 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react' + +import { V1ContentRender } from '../../(presentation-generator)/components/V1ContentRender'; + + +const BASE_WIDTH = 1280; +const BASE_HEIGHT = 720; + +const SlideScale = ({ slide }: { slide: any }) => { + + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + const scale = useMemo(() => { + // Slight padding to avoid overflow due to borders/scrollbars + const safeWidth = Math.max(0, containerWidth + 20); + if (!safeWidth) return 1; + return Math.min((safeWidth / BASE_WIDTH) * 0.98, 1); + }, [containerWidth]); + + useEffect(() => { + if (!containerRef.current) return; + + const el = containerRef.current; + const ro = new ResizeObserver(() => { + // Use clientWidth so we match the actual available column width + setContainerWidth(el.clientWidth); + }); + + ro.observe(el); + // Initial measure + setContainerWidth(el.clientWidth); + + return () => ro.disconnect(); + }, []); + return (
+
+
+ +
+ + + + +
+
+
+ ) +} + +export default SlideScale \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/components/V1ContentRender.tsx b/servers/nextjs/app/(presentation-generator)/components/V1ContentRender.tsx new file mode 100644 index 00000000..1cc867cf --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/components/V1ContentRender.tsx @@ -0,0 +1,139 @@ +"use client"; + +import React, { useMemo, useRef } from "react"; +import EditableLayoutWrapper from "../components/EditableLayoutWrapper"; +import SlideErrorBoundary from "../components/SlideErrorBoundary"; +import TiptapTextReplacer from "../components/TiptapTextReplacer"; +import { validate as uuidValidate } from 'uuid'; +import { getLayoutByLayoutId } from "@/app/presentation-templates"; +import { useCustomTemplateDetails } from "@/app/hooks/useCustomTemplates"; +import { updateSlideContent } from "@/store/slices/presentationGeneration"; +import { useDispatch } from "react-redux"; +import { Loader2 } from "lucide-react"; + + + + +export const V1ContentRender = ({ slide, isEditMode, theme }: { slide: any, isEditMode: boolean, theme?: any, enableEditMode?: boolean }) => { + const dispatch = useDispatch(); + const containerRef = useRef(null); + + + const customTemplateId = slide.layout_group.startsWith("custom-") ? slide.layout_group.split("custom-")[1] : slide.layout_group; + const isCustomTemplate = uuidValidate(customTemplateId) || slide.layout_group.startsWith("custom-"); + + // Always call the hook (React hooks rule), but with empty id when not a custom template + const { template: customTemplate, loading: customLoading, fonts } = useCustomTemplateDetails({ + id: isCustomTemplate ? customTemplateId : "", + name: isCustomTemplate ? slide.layout_group : "", + description: "" + }); + if (fonts && typeof fonts === 'object') { + // useFontLoader(fonts as unknown as Record); + } + + // Memoize layout resolution to prevent unnecessary recalculations + const Layout = useMemo(() => { + if (isCustomTemplate) { + if (customTemplate) { + const layoutId = slide.layout.startsWith("custom-") ? slide.layout.split(":")[1] : slide.layout; + + + const compiledLayout = customTemplate.layouts.find( + (layout) => layout.layoutId === layoutId + ); + + + return compiledLayout?.component ?? null; + } + return null; + } else { + const template = getLayoutByLayoutId(slide.layout); + return template?.component ?? null; + } + }, [isCustomTemplate, customTemplate, slide.layout]); + + // Show loading state for custom templates + if (isCustomTemplate && customLoading) { + return ( +
+ +
+ ); + } + + + if (!Layout) { + if (Object.keys(slide.content).length === 0) { + return ( +
+

Blank Slide

+

This slide is empty. Please add content to it using the edit button.

+
+ ) + } + return ( +
+

+ Layout "{slide.layout}" not found in " + {slide.layout_group}" Template +

+
+ ); + } + const LayoutComp = Layout as React.ComponentType<{ data: any }>; + + if (isEditMode) { + return ( + +
+ + + { + if (dataPath && slideIndex !== undefined) { + dispatch( + updateSlideContent({ + slideIndex: slideIndex, + dataPath: dataPath, + content: content, + }) + ); + } + }} + > + + + + + + +
+
+ + ); + } + return ( + + ) +}; + diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx deleted file mode 100644 index 9e8e4ea5..00000000 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ /dev/null @@ -1,713 +0,0 @@ -"use client"; -import React, { - createContext, - useContext, - useEffect, - useState, - ReactNode, -} from "react"; -import dynamic from "next/dynamic"; -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; - templateID: string; - templateName?: string; -} -export interface FullDataInfo { - name: string; - component: React.ComponentType; - schema: any; - sampleData: any; - fileName: string; - templateID: string; - layoutId: string; -} - -export interface TemplateSetting { - description: string; - ordered: boolean; - default?: boolean; -} - -export interface TemplateResponse { - templateID: string; - templateName?: string; - files: string[]; - settings: TemplateSetting | null; -} - -export interface LayoutData { - layoutsById: Map; - layoutsByTemplateID: Map>; - templateSettings: Map; - fileMap: Map; - templateLayouts: Map; - layoutSchema: LayoutInfo[]; - fullDataByTemplateID: Map; -} - -export interface LayoutContextType { - getLayoutById: (layoutId: string) => LayoutInfo | null; - - getLayoutsByTemplateID: (templateID: string) => LayoutInfo[]; - getTemplateSetting: (templateID: string) => TemplateSetting | null; - getAllTemplateIDs: () => string[]; - getAllLayouts: () => LayoutInfo[]; - getFullDataByTemplateID: (templateID: string) => FullDataInfo[]; - loading: boolean; - error: string | null; - getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; - isPreloading: boolean; - cacheSize: number; - refetch: () => Promise; - getCustomTemplateFonts: (presentationId: string) => string[] | null; -} - -const LayoutContext = createContext(undefined); - -const layoutCache = new Map>(); - -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) => { - - const cleanCode = layoutCode - .replace(/import\s+React\s+from\s+'react';?/g, "") - .replace(/import\s*{\s*z\s*}\s*from\s+'zod';?/g, "") - .replace(/import\s+.*\s+from\s+['"]zod['"];?/g, "") - // remove every zod import (any style) - .replace(/import\s+.*\s+from\s+['"]zod['"];?/g, "") - .replace(/const\s+[^=]*=\s*require\(['"]zod['"]\);?/g, "") - .replace(/typescript/g, "") - const compiled = Babel.transform(cleanCode, { - presets: [ - ["react", { runtime: "classic" }], - ["typescript", { isTSX: true, allExtensions: true }], - ], - sourceType: "script", - }).code; - - const factory = new Function( - "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 || {}; - - ${compiled} - - /* everything declared in the string is in scope here */ - return { - __esModule: true, - default: typeof dynamicSlideLayout !== 'undefined' ? dynamicSlideLayout : (typeof DefaultLayout !== 'undefined' ? DefaultLayout : undefined), - layoutName, - layoutId, - layoutDescription, - Schema - }; - ` - ); - - return factory(React, z, Recharts); -}; - -export const LayoutProvider: React.FC<{ - children: ReactNode; -}> = ({ children }) => { - const [layoutData, setLayoutData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [isPreloading, setIsPreloading] = useState(false); - const [customTemplateFonts, setCustomTemplateFonts] = useState>(new Map()); - const dispatch = useDispatch(); - - const buildData = async (templateData: TemplateResponse[]) => { - const layouts: LayoutInfo[] = []; - - const layoutsById = 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 template of templateData) { - // Initialize template - if (!layoutsByTemplateID.has(template.templateID)) { - layoutsByTemplateID.set(template.templateID, new Set()); - } - - fullDataByTemplateID.set(template.templateID, []); - - // template settings or default settings - const settings = template.settings || { - templateName: template.templateName, - description: `${template.templateID} presentation layouts`, - ordered: false, - default: false, - }; - - templateSettingsMap.set(template.templateID, settings); - const templateLayouts: LayoutInfo[] = []; - const templateFullData: FullDataInfo[] = []; - - for (const fileName of template.files) { - try { - const file = fileName.replace(".tsx", "").replace(".ts", ""); - - const module = await import( - `@/presentation-templates/${template.templateID}/${file}` - ); - - if (!module.default) { - toast.error(`${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.error(`${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(template.templateID, fileName); - if (!layoutCache.has(cacheKey)) { - layoutCache.set(cacheKey, module.default); - } - - const originalLayoutId = - module.layoutId || file.toLowerCase().replace(/layout$/, ""); - const uniqueKey = `${template.templateID}:${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, - templateID: template.templateID, - templateName: template.templateName, - }; - - const sampleData = module.Schema.parse({}); - const fullData: FullDataInfo = { - name: layoutName, - component: module.default, - schema: jsonSchema, - sampleData: sampleData, - fileName, - templateID: template.templateID, - layoutId: uniqueKey, - }; - templateFullData.push(fullData); - - layoutsById.set(uniqueKey, layout); - layoutsByTemplateID.get(template.templateID)!.add(uniqueKey); - fileMap.set(uniqueKey, { - fileName, - templateID: template.templateID, - }); - templateLayouts.push(layout); - layouts.push(layout); - } catch (error) { - console.error( - `πŸ’₯ Error extracting schema for ${fileName} from ${template.templateID}:`, - error - ); - } - } - - fullDataByTemplateID.set(template.templateID, templateFullData); - // Cache template layouts - templateLayoutsCache.set(template.templateID, templateLayouts); - } - } catch (err: any) { - console.error("Compilation error:", err); - } - - return { - layoutsById, - layoutsByTemplateID, - templateSettings: templateSettingsMap, - fileMap, - templateLayoutsCache, - layoutSchema: layouts, - fullDataByTemplateID, - }; - }; - - const loadLayouts = async () => { - try { - setLoading(true); - setError(null); - dispatch(setLayoutLoading(true)); - - const templateResponse = await fetch("/api/templates"); - - if (!templateResponse.ok) { - throw new Error( - `Failed to fetch layouts: ${templateResponse.statusText}` - ); - } - - const templateData: TemplateResponse[] = - await templateResponse.json(); - - if (!templateData || templateData.length === 0) { - setError("No template found"); - return; - } - - const data = await buildData(templateData); - const customLayouts = await LoadCustomLayouts(); - setIsPreloading(false); - const combinedData = { - layoutsById: mergeMaps(data.layoutsById, customLayouts.layoutsById), - layoutsByTemplateID: mergeMaps( - data.layoutsByTemplateID, - customLayouts.layoutsByTemplateID - ), - templateSettings: mergeMaps( - data.templateSettings, - customLayouts.templateSettings - ), - fileMap: mergeMaps(data.fileMap, customLayouts.fileMap), - templateLayouts: mergeMaps( - data.templateLayoutsCache, - customLayouts.templateLayoutsCache - ), - layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema], - fullDataByTemplateID: mergeMaps( - data.fullDataByTemplateID, - customLayouts.fullDataByTemplateID - ), - }; - - setLayoutData(combinedData); - - // The preloading is now handled within buildData - } catch (err: unknown) { - const errorMessage = - err instanceof Error ? err.message : "Failed to load layouts"; - setError(errorMessage); - console.error("πŸ’₯ Error loading layouts:", err); - } finally { - dispatch(setLayoutLoading(false)); - setLoading(false); - } - }; - - function mergeMaps(map1: Map, map2: Map): Map { - const merged = new Map(map1); - map2.forEach((value, key) => { - merged.set(key, value); - }); - return merged; - } - - const LoadCustomLayouts = async () => { - const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null; - - const layouts: LayoutInfo[] = []; - const layoutsById = new Map(); - const layoutsByTemplateID = new Map>(); - const templateSettingsMap = new Map(); - const fileMap = new Map(); - const templateLayoutsCache = new Map(); - const fullDataByTemplateID = new Map(); - try { - const customTemplateResponse = await fetch( - `/api/v1/ppt/template-management/summary`, - { - headers: { - ...getHeader(), - ...(token ? { Authorization: `Bearer ${token}` } : {}), - } - } - ); - const customTemplateData = await customTemplateResponse.json(); - - const customFonts = new Map(); - 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 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}`, - { - headers: { - ...getHeader(), - ...(token ? { Authorization: `Bearer ${token}` } : {}), - }, - } - ); - const customLayoutsData = await customLayoutResponse.json(); - const allLayout = customLayoutsData.layouts; - - - - - const settings = { - templateName: templateName, - description: `Custom presentation layouts`, - ordered: false, - default: false, - }; - - 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 }> => { - const ErrorSlide: React.FC<{ data: any }> = () => ( -
-
{title}
-
{message}
-
- ); - ErrorSlide.displayName = "CustomTemplateErrorSlide"; - return ErrorSlide; - }; - - for (const i of allLayout) { - try { - /* ---------- 1. compile JSX to plain script ------------------ */ - const module = compileCustomLayout(i.layout_code, React, z); - - // Determine identifiers even if subsequent steps fail - const originalLayoutId = - (module && (module as any).layoutId) || - i.layout_name.toLowerCase().replace(/layout$/, ""); - const uniqueKey = `${`custom-${presentationId}`}:${originalLayoutId}`; - const layoutName = - (module && (module as any).layoutName) || - i.layout_name.replace(/([A-Z])/g, " $1").trim(); - const layoutDescription = - (module && (module as any).layoutDescription) || - `${layoutName} layout for presentations`; - - let fullData: FullDataInfo | null = null; - let jsonSchema: any = null; - let componentToUse: React.ComponentType<{ data: any } | any> | null = null; - let sampleData: any = {}; - - // Validate exports - if (!module || !(module as any).default) { - const errorComp = createErrorComponent( - `Invalid export in ${i.layout_name}`, - "Default export not found. Please export a default React component." - ); - componentToUse = errorComp; - jsonSchema = {}; - } else if (!(module as any).Schema) { - const errorComp = createErrorComponent( - `Schema missing in ${i.layout_name}`, - "Schema export not found. Please export a Zod Schema as 'Schema'." - ); - componentToUse = errorComp; - jsonSchema = {}; - } else { - // Cache valid component - const cacheKey = createCacheKey( - `custom-${presentationId}`, - i.layout_name - ); - if (!layoutCache.has(cacheKey)) { - layoutCache.set(cacheKey, (module as any).default); - } - componentToUse = (module as any).default; - - // Build schema and sample data with guards - try { - jsonSchema = z.toJSONSchema((module as any).Schema, { - override: (ctx) => { - delete ctx.jsonSchema.default; - }, - }); - } catch (schemaErr: any) { - const errorComp = createErrorComponent( - `Schema generation failed for ${i.layout_name}`, - schemaErr?.message || String(schemaErr) - ); - componentToUse = errorComp; - jsonSchema = {}; - } - - if (componentToUse !== null && componentToUse !== (module as any).default) { - // componentToUse already replaced with error component - sampleData = {}; - } else { - try { - sampleData = (module as any).Schema.parse({}); - } catch (parseErr: any) { - const errorComp = createErrorComponent( - `Schema.parse failed for ${i.layout_name}`, - parseErr?.message || String(parseErr) - ); - componentToUse = errorComp; - sampleData = {}; - jsonSchema = jsonSchema || {}; - } - } - } - - customFonts.set(presentationId, i.fonts); - - const layout: LayoutInfo = { - id: uniqueKey, - name: layoutName, - description: layoutDescription, - json_schema: jsonSchema, - templateID: templateID, - templateName: templateName, - }; - - fullData = { - name: layoutName, - component: componentToUse as React.ComponentType, - schema: jsonSchema, - sampleData: sampleData, - fileName: i.layout_name, - templateID: templateID, - layoutId: uniqueKey, - }; - - templateFullData.push(fullData); - - layoutsById.set(uniqueKey, layout); - layoutsByTemplateID.get(templateID)!.add(uniqueKey); - fileMap.set(uniqueKey, { - fileName: i.layout_name, - templateID: templateID, - }); - templateLayouts.push(layout); - layouts.push(layout); - } catch (e: any) { - // Handle compilation/runtime errors during transformation - const uniqueKey = `${`custom-${presentationId}`}:${i.layout_name.toLowerCase().replace(/layout$/, "")}`; - const layoutName = i.layout_name.replace(/([A-Z])/g, " $1").trim(); - const errorComp = createErrorComponent( - `Compilation error in ${i.layout_name}`, - e?.message || String(e) - ); - - const layout: LayoutInfo = { - id: uniqueKey, - name: layoutName, - description: `Failed to compile ${i.layout_name}`, - json_schema: {}, - templateID: templateID, - templateName: templateName, - }; - - const fullData: FullDataInfo = { - name: layoutName, - component: errorComp, - schema: {}, - sampleData: {}, - fileName: i.layout_name, - templateID: templateID, - layoutId: uniqueKey, - }; - - templateFullData.push(fullData); - layoutsById.set(uniqueKey, layout); - layoutsByTemplateID.get(templateID)!.add(uniqueKey); - fileMap.set(uniqueKey, { - fileName: i.layout_name, - templateID: templateID, - }); - templateLayouts.push(layout); - layouts.push(layout); - } - } - setCustomTemplateFonts(customFonts); - // Cache template layouts - templateLayoutsCache.set(templateID, templateLayouts); - fullDataByTemplateID.set(templateID, templateFullData); - } - } catch (err: any) { - console.error("Compilation error:", err); - } - - - return { - layoutsById, - layoutsByTemplateID, - templateSettings: templateSettingsMap, - fileMap, - templateLayoutsCache, - layoutSchema: layouts, - fullDataByTemplateID, - }; - }; - - const getLayout = ( - layoutId: string - ): React.ComponentType<{ data: any }> | null => { - if (!layoutData) return null; - - 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())) { - if (key === layoutId) { - fileInfo = info; - break; - } - } - - if (!fileInfo) { - console.warn(`No file info found for layout: ${layoutId}`); - return null; - } - - const cacheKey = createCacheKey(fileInfo.templateID, fileInfo.fileName); - - // Return cached layout if available - if (layoutCache.has(cacheKey)) { - return layoutCache.get(cacheKey)!; - } - // Create and cache layout if not available - const file = fileInfo.fileName.replace(".tsx", "").replace(".ts", ""); - const Layout = dynamic( - () => import(`@/presentation-templates/${fileInfo.templateID}/${file}`), - { - loading: () => ( -
- ), - ssr: false, - } - ) as React.ComponentType<{ data: any }>; - - layoutCache.set(cacheKey, Layout); - return Layout; - }; - - // 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 templateID) - for (const [key, layout] of Array.from(layoutData.layoutsById.entries())) { - if (key === layoutId) { - return layout; - } - } - return null; - }; - - - - const getLayoutsByTemplateID = (templateID: string): LayoutInfo[] => { - return layoutData?.templateLayouts.get(templateID) || []; - }; - - const getTemplateSetting = (templateID: string): TemplateSetting | null => { - return layoutData?.templateSettings.get(templateID) || null; - }; - - const getAllTemplateIDs = (): string[] => { - return layoutData ? Array.from(layoutData.templateSettings.keys()) : []; - }; - - const getAllLayouts = (): LayoutInfo[] => { - return layoutData?.layoutSchema || []; - }; - - const getFullDataByTemplateID = (templateID: string): FullDataInfo[] => { - return layoutData?.fullDataByTemplateID.get(templateID) || []; - }; - const getCustomTemplateFonts = (presentationId: string): string[] | null => { - return customTemplateFonts.get(presentationId) || null; - }; - - // Load layouts on mount - useEffect(() => { - loadLayouts(); - }, []); // Add presentationId to dependency array - - const contextValue: LayoutContextType = { - getLayoutById, - getLayoutsByTemplateID, - getTemplateSetting, - getAllTemplateIDs, - getAllLayouts, - getFullDataByTemplateID, - getCustomTemplateFonts, - 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; -}; diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts index ad06c260..0dd617b0 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts +++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts @@ -8,7 +8,7 @@ export const useLayoutSaving = ( slides: ProcessedSlide[], UploadedFonts: UploadedFont[], fontsData: FontData | null, - refetch: () => void, + // refetch: () => void, setSlides: React.Dispatch> ) => { const [isSavingLayout, setIsSavingLayout] = useState(false); @@ -61,15 +61,15 @@ export const useLayoutSaving = ( } catch (error) { retryCount++; console.error(`Error converting slide ${slide.slide_number} (attempt ${retryCount}):`, error); - + if (retryCount < maxRetries) { toast.error(`Failed to convert slide ${slide.slide_number}. Retrying in 2 minutes...`, { description: `Attempt ${retryCount}/${maxRetries}. Error: ${error instanceof Error ? error.message : "An unexpected error occurred"}`, }); - + // Wait for 2 minutes before retrying await delay(2 * 60 * 1000); - + toast.info(`Retrying conversion for slide ${slide.slide_number}...`); } else { throw new Error(`Failed to convert slide ${slide.slide_number} after ${maxRetries} attempts: ${error instanceof Error ? error.message : "An unexpected error occurred"}`); @@ -173,7 +173,7 @@ export const useLayoutSaving = ( }); toast.success(`Layout "${layoutName}" saved successfully`); - refetch(); + // refetch(); closeSaveModal(); return presentationId; } catch (error) { @@ -188,7 +188,7 @@ export const useLayoutSaving = ( } finally { setIsSavingLayout(false); } - }, [slides, UploadedFonts, fontsData, refetch, closeSaveModal, setSlides]); + }, [slides, UploadedFonts, fontsData, closeSaveModal, setSlides]); return { isSavingLayout, diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx index 7a582fb5..cd355765 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx @@ -3,7 +3,7 @@ import React, { useEffect } from "react"; import FontManager from "./components/FontManager"; import Header from "../dashboard/components/Header"; -import { useLayout } from "../context/LayoutContext"; + import { useCustomLayout } from "./hooks/useCustomLayout"; import { useFontManagement } from "./hooks/useFontManagement"; import { useFileUpload } from "./hooks/useFileUpload"; @@ -22,14 +22,14 @@ import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; const CustomTemplatePage = () => { const router = useRouter(); const pathname = usePathname(); - const { refetch } = useLayout(); - + + // Custom hooks for different concerns const { hasRequiredKey, isRequiredKeyLoading } = useAPIKeyCheck(); const { selectedFile, handleFileSelect, removeFile } = useFileUpload(); const { slides, setSlides, completedSlides } = useCustomLayout(); const { fontsData, UploadedFonts, uploadFont, removeFont, getAllUnsupportedFonts, setFontsData } = useFontManagement(); - const { isProcessingPptx, processFile, retrySlide,processSlideToHtml } = useSlideProcessing( + const { isProcessingPptx, processFile, retrySlide, processSlideToHtml } = useSlideProcessing( selectedFile, slides, setSlides, @@ -39,7 +39,7 @@ const CustomTemplatePage = () => { slides, UploadedFonts, fontsData, - refetch, + setSlides ); @@ -53,7 +53,7 @@ const CustomTemplatePage = () => { }; const handleProcessSlideToHtml = (slide: any) => { - processSlideToHtml(slide,0) + processSlideToHtml(slide, 0) } // Handle slide updates @@ -62,15 +62,15 @@ const CustomTemplatePage = () => { prevSlides.map((s, i) => i === index ? { - ...s, - ...updatedSlideData, - modified: true, - } + ...s, + ...updatedSlideData, + modified: true, + } : s ) ); }; - useEffect(() => { + useEffect(() => { const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' ); @@ -90,7 +90,7 @@ const CustomTemplatePage = () => { // Anthropic key warning if (!hasRequiredKey) { return ; - + } return ( @@ -112,7 +112,7 @@ const CustomTemplatePage = () => {
- + {/* File Upload Section */} { uploadFont={uploadFont} removeFont={removeFont} getAllUnsupportedFonts={getAllUnsupportedFonts} - processSlideToHtml={()=>handleProcessSlideToHtml(slides[0])} + processSlideToHtml={() => handleProcessSlideToHtml(slides[0])} /> )} diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/PresentationCard.tsx index 1fe9e4d4..588f61dd 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 { useTemplateLayouts } from "@/app/(presentation-generator)/hooks/useTemplateLayouts"; +import SlideScale from "../../components/PresentationRender"; export const PresentationCard = ({ id, @@ -26,7 +26,7 @@ export const PresentationCard = ({ onDeleted?: (presentationId: string) => void; }) => { const router = useRouter(); - const { renderSlideContent } = useTemplateLayouts(); + @@ -92,7 +92,7 @@ export const PresentationCard = ({ >
- {renderSlideContent(slide, false)} +
diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx deleted file mode 100644 index 1d24cb54..00000000 --- a/servers/nextjs/app/(presentation-generator)/hooks/useTemplateLayouts.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client"; -import React, { useMemo } from "react"; -import { useDispatch } from "react-redux"; -import { useLayout } from "../context/LayoutContext"; -import EditableLayoutWrapper from "../components/EditableLayoutWrapper"; -import SlideErrorBoundary from "../components/SlideErrorBoundary"; -import TiptapTextReplacer from "../components/TiptapTextReplacer"; -import { updateSlideContent } from "../../../store/slices/presentationGeneration"; -import { Loader2 } from "lucide-react"; - -export const useTemplateLayouts = () => { - const dispatch = useDispatch(); - const { getLayoutById, getLayout, loading } = - useLayout(); - - const getTemplateLayout = useMemo(() => { - return (layoutId: string, groupName: string) => { - const layout = getLayoutById(layoutId); - if (layout) { - return getLayout(layoutId); - } - return null; - }; - }, [getLayoutById, getLayout]); - - - - // Render slide content with group validation, automatic Tiptap text editing, and editable images/icons - const renderSlideContent = useMemo(() => { - return (slide: any, isEditMode: boolean) => { - - const Layout = getTemplateLayout(slide.layout, slide.layout_group); - if (loading) { - return ( -
- -
- ); - } - if (!Layout) { - return ( -
-

- Layout "{slide.layout}" not found in " - {slide.layout_group}" group -

-
- ); - } - - if (isEditMode) { - return ( - - { - if (dataPath && slideIndex !== undefined) { - dispatch( - updateSlideContent({ - slideIndex: slideIndex, - dataPath: dataPath, - content: content, - }) - ); - } - }} - > - - - - - - ); - } - return ( - - - - ); - }; - }, [getTemplateLayout, dispatch]); - - return { - getTemplateLayout, - renderSlideContent, - loading, - }; -}; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx new file mode 100644 index 00000000..dbd1c94a --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx @@ -0,0 +1,98 @@ +"use client"; +import React, { memo } from "react"; +import { Card } from "@/components/ui/card"; +import { CustomTemplates, useCustomTemplatePreview } from "@/app/hooks/useCustomTemplates"; +import { Loader2 } from "lucide-react"; +import { CompiledLayout } from "@/app/hooks/compileLayout"; + +// Memoized preview component to prevent re-renders during scroll +export const LayoutPreview = memo(({ layout, templateId, index }: { layout: CompiledLayout, templateId: string, index: number }) => { + const LayoutComponent = layout.component; + return ( +
+
+
+ +
+
+ ); +}); +LayoutPreview.displayName = 'LayoutPreview'; + +export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => { + + const { previewLayouts, loading: customLoading } = useCustomTemplatePreview(template.id); + const isSelected = selectedTemplate === template.id; + + return ( + { + onSelectTemplate(template.id); + }} + > +
+
+

+ {template.name} +

+ +
+ + + + {/* Layout previews */} +
+ {customLoading ? ( + // Loading placeholders + [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( +
+ +
+ )) + ) : previewLayouts && previewLayouts?.length > 0 ? ( + // Actual layout previews - using memoized component + previewLayouts?.slice(0, 4).map((layout: CompiledLayout, index: number) => ( + + )) + ) : ( + // Empty state placeholders + [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( +
+ No preview +
+ )) + )} +
+ + +
+ {isSelected && ( +
+ Selected +
+ )} +
+ ); +}); +CustomTemplateCard.displayName = 'CustomTemplateCard'; + diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx index 98b84504..1cd950b9 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx @@ -3,11 +3,12 @@ import { usePathname } from "next/navigation"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { Button } from "@/components/ui/button"; import { LoadingState, Template } from "../types/index"; +import { TemplateLayoutsWithSettings } from "@/app/presentation-templates"; interface GenerateButtonProps { loadingState: LoadingState; streamState: { isStreaming: boolean; isLoading: boolean }; - selectedTemplate: Template | null; + selectedTemplate: TemplateLayoutsWithSettings | string | null; onSubmit: () => void; outlineCount: number; } diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index a5267fa1..de5d6f46 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -15,6 +15,7 @@ import { useOutlineStreaming } from "../hooks/useOutlineStreaming"; import { useOutlineManagement } from "../hooks/useOutlineManagement"; import { usePresentationGeneration } from "../hooks/usePresentationGeneration"; import TemplateSelection from "./TemplateSelection"; +import { TemplateLayoutsWithSettings } from "@/app/presentation-templates"; const OutlinePage: React.FC = () => { const { presentation_id, outlines } = useSelector( @@ -22,7 +23,7 @@ const OutlinePage: React.FC = () => { ); const [activeTab, setActiveTab] = useState(TABS.OUTLINE); - const [selectedTemplate, setSelectedTemplate] = useState