+
+ Please add "GOOGLE_API_KEY" to enable template creation via AI.
+
Please add your OpenAI API Key to process the layout
This feature requires an OpenAI model GPT-5. Configure your key in settings or via environment variables.
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/EditControls.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/EditControls.tsx
index 2ad731b7..82f2f111 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/EditControls.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/EditControls.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import React from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx
index 6b04f1a1..284e7b4a 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/HtmlEditor.tsx
@@ -1,3 +1,4 @@
+'use client'
import React, { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/NewEachSlide.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/NewEachSlide.tsx
index 59f7653f..e766cf90 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/NewEachSlide.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/NewEachSlide.tsx
@@ -1,5 +1,6 @@
+'use client'
+
import React from "react";
-import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useDrawingCanvas } from "../../hooks/useDrawingCanvas";
@@ -155,36 +156,6 @@ const EachSlide: React.FC = ({
onTouchEnd={handleTouchEnd}
/>
-
- {/* Action Buttons */}
-
-
-
);
};
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideActions.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideActions.tsx
index 91d0e8f5..dbb282de 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideActions.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideActions.tsx
@@ -1,3 +1,4 @@
+'use client'
import React from "react";
import { AlertCircle, CheckCircle, Edit, Loader2, Repeat2, Trash, Code } from "lucide-react";
import ToolTip from "@/components/ToolTip";
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideContentDisplay.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideContentDisplay.tsx
index b934058c..baf501e6 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideContentDisplay.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/EachSlide/SlideContentDisplay.tsx
@@ -1,8 +1,11 @@
+'use client'
+
import React from "react";
import SlideContent from "../SlideContent";
import { SlideContentDisplayProps } from "../../types";
import { Repeat2 } from "lucide-react";
+import Timer from "../Timer";
export const SlideContentDisplay: React.FC = ({
slide,
@@ -33,16 +36,15 @@ export const SlideContentDisplay: React.FC = ({
if (slide.processing) {
return (
-
- 🔄 Converting to HTML...
-
-
-
-
-
-
-
+
🔄 Converting to HTML...
+
+
+
);
}
@@ -50,6 +52,12 @@ export const SlideContentDisplay: React.FC
= ({
if (slide.processed && slide.html) {
return (
+ {slide.convertingToReact && (
+
+
⚙️ Converting HTML to React...
+
+
+ )}
@@ -86,27 +94,21 @@ export const SlideContentDisplay: React.FC
= ({
if (slide.error) {
return (
-
- ✗ Conversion failed
-
+
✗ Conversion failed
{slide.error.includes("image exceeds 5 MB maximum") ? (
-
- Image too large for processing
-
-
- This slide's image exceeds the 5MB limit. Try using a
- smaller resolution PPTX file.
-
+
Image too large for processing
+
This slide's image exceeds the 5MB limit. Try using a smaller resolution PPTX file.
) : (
slide.error
)}
-
-
+
);
@@ -114,9 +116,7 @@ export const SlideContentDisplay: React.FC = ({
return (
-
- ⏳ Waiting in queue to process...
-
+
⏳ Waiting in queue to process...
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
index 3e9c405f..a371c9df 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/FileUploadSection.tsx
@@ -10,6 +10,7 @@ import {
import { Label } from "@/components/ui/label";
import { Upload, FileText, X, Loader2 } from "lucide-react";
import { ProcessedSlide } from "../types";
+import Timer from "./Timer";
interface FileUploadSectionProps {
selectedFile: File | null;
@@ -96,7 +97,7 @@ export const FileUploadSection: React.FC
= ({
)}
-
+
+ {isProcessingPptx && }
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/FontManager.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/FontManager.tsx
index 7ee5df52..5ce0f53b 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/FontManager.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/FontManager.tsx
@@ -224,7 +224,7 @@ const FontManager: React.FC
= ({
onClick={processSlideToHtml}
className="text-xs px-8 py-2 font-semibold bg-blue-600 text-white hover:text-white hover:bg-blue-700 border-blue-600"
>
- Extract layouts
+ Extract Template
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/SlideContent.tsx
index 40d63d91..f2c6881b 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/components/SlideContent.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/SlideContent.tsx
@@ -1,3 +1,5 @@
+'use client'
+
import React, { memo } from "react";
const SlideContent = memo(({ slide }: { slide: any }) => {
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/Timer.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/Timer.tsx
new file mode 100644
index 00000000..0a3922c1
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/Timer.tsx
@@ -0,0 +1,103 @@
+'use client'
+import React, { useEffect, useRef, useState } from 'react'
+
+interface TimerProps {
+ duration: number // seconds
+}
+
+const Timer = ({ duration }: TimerProps) => {
+ const [progress, setProgress] = useState
(0)
+ const rafIdRef = useRef(null)
+ const startTimeRef = useRef(null)
+
+ useEffect(() => {
+ // Guard against invalid durations
+ const totalMs = Math.max(0, duration * 1000)
+
+ const easeOutCubic = (x: number) => 1 - Math.pow(1 - x, 3)
+ const easeOutSine = (x: number) => Math.sin((x * Math.PI) / 2)
+
+ const tick = (now: number) => {
+ if (startTimeRef.current === null) startTimeRef.current = now
+ const elapsed = now - startTimeRef.current
+ const t = totalMs === 0 ? 1 : Math.min(elapsed / totalMs, 1)
+
+ // Piecewise progression:
+ // - Reach ~75% around 60% of the total duration (faster start)
+ // - Then ease slowly towards 99% for the remainder
+ let nextProgress: number
+ if (t <= 0.6) {
+ nextProgress = 75 * easeOutCubic(t / 0.6)
+ } else {
+ nextProgress = 75 + 24 * easeOutSine((t - 0.6) / 0.4)
+ }
+
+ // Clamp and ensure we never hit 100
+ nextProgress = Math.min(99, nextProgress)
+
+ setProgress(prev => (nextProgress < prev ? prev : nextProgress))
+
+ if (t < 1 && nextProgress < 99) {
+ rafIdRef.current = requestAnimationFrame(tick)
+ } else {
+ // End at 99 and stop
+ setProgress(99)
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)
+ rafIdRef.current = null
+ }
+ }
+
+ // Initialize animation
+ setProgress(0)
+ startTimeRef.current = null
+ rafIdRef.current = requestAnimationFrame(tick)
+
+ return () => {
+ if (rafIdRef.current) cancelAnimationFrame(rafIdRef.current)
+ rafIdRef.current = null
+ startTimeRef.current = null
+ }
+ }, [duration])
+
+ return (
+
+
+ {Math.round(progress)}%
+
+
+
+
+ )
+}
+
+export default Timer
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useAPIKeyCheck.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useAPIKeyCheck.ts
new file mode 100644
index 00000000..f16dd738
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useAPIKeyCheck.ts
@@ -0,0 +1,18 @@
+import { useState, useEffect } from "react";
+
+export const useAPIKeyCheck = () => {
+ const [hasRequiredKey, setHasRequiredKey] = useState(false);
+ const [isRequiredKeyLoading, setIsRequiredKeyLoading] = useState(true);
+
+ useEffect(() => {
+ fetch("/api/has-required-key")
+ .then((res) => res.json())
+ .then((data) => {
+ setHasRequiredKey(Boolean(data.hasKey));
+ setIsRequiredKeyLoading(false);
+ })
+ .catch(() => setIsRequiredKeyLoading(false));
+ }, []);
+
+ return { hasRequiredKey, isRequiredKeyLoading };
+};
\ No newline at end of file
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 2ca1ec8f..570e73f2 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useLayoutSaving.ts
@@ -7,7 +7,8 @@ import { ProcessedSlide, UploadedFont } from "../types";
export const useLayoutSaving = (
slides: ProcessedSlide[],
UploadedFonts: UploadedFont[],
- refetch: () => void
+ refetch: () => void,
+ setSlides: React.Dispatch>
) => {
const [isSavingLayout, setIsSavingLayout] = useState(false);
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -86,7 +87,7 @@ export const useLayoutSaving = (
try {
// Convert each slide HTML to React component
- const reactComponents = [];
+ const reactComponents: any[] = [];
const presentationId = uuidv4();
// Get all uploaded font URLs
@@ -101,6 +102,9 @@ export const useLayoutSaving = (
continue;
}
+ // Mark current slide as converting to React
+ setSlides(prev => prev.map((s, idx) => idx === i ? { ...s, convertingToReact: true } : s));
+
try {
const reactComponent = await convertSlideToReact(slide, presentationId, FontUrls);
reactComponents.push(reactComponent);
@@ -118,7 +122,9 @@ export const useLayoutSaving = (
: "An unexpected error occurred",
});
// Continue with other slides even if one fails
- continue;
+ } finally {
+ // Clear converting flag for this slide
+ setSlides(prev => prev.map((s, idx) => idx === i ? { ...s, convertingToReact: false } : s));
}
}
@@ -175,7 +181,7 @@ export const useLayoutSaving = (
} finally {
setIsSavingLayout(false);
}
- }, [slides, UploadedFonts, refetch, closeSaveModal]);
+ }, [slides, UploadedFonts, refetch, closeSaveModal, setSlides]);
return {
isSavingLayout,
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useOpenAIKeyCheck.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useOpenAIKeyCheck.ts
deleted file mode 100644
index 8263ddd9..00000000
--- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useOpenAIKeyCheck.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { useState, useEffect } from "react";
-
-export const useOpenAIKeyCheck = () => {
- const [hasOpenAIKey, setHasOpenAIKey] = useState(false);
- const [isOpenAIKeyLoading, setIsOpenAIKeyLoading] = useState(true);
-
- useEffect(() => {
- fetch("/api/has-openai-key")
- .then((res) => res.json())
- .then((data) => {
- setHasOpenAIKey(Boolean(data.hasKey));
- setIsOpenAIKeyLoading(false);
- })
- .catch(() => setIsOpenAIKeyLoading(false));
- }, []);
-
- return { hasOpenAIKey, isOpenAIKeyLoading };
-};
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
index 16a41d85..544c4804 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/hooks/useSlideProcessing.ts
@@ -158,7 +158,10 @@ export const useSlideProcessing = (
setSlides(initialSlides);
toast.success(
- `Successfully extracted ${pptxData.slides.length} slides! Converting to HTML...`
+ `Template Processing Finished`,
+ {
+ description: `Please Upload the not supported fonts, and click Extract Template`
+ }
);
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
index 81793fe3..640762b3 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/page.tsx
@@ -9,20 +9,20 @@ import { useFontManagement } from "./hooks/useFontManagement";
import { useFileUpload } from "./hooks/useFileUpload";
import { useSlideProcessing } from "./hooks/useSlideProcessing";
import { useLayoutSaving } from "./hooks/useLayoutSaving";
-import { useOpenAIKeyCheck } from "./hooks/useOpenAIKeyCheck";
import { LoadingSpinner } from "./components/LoadingSpinner";
-import { OpenAIKeyWarning } from "./components/OpenAIKeyWarning";
import { FileUploadSection } from "./components/FileUploadSection";
import { SaveLayoutButton } from "./components/SaveLayoutButton";
import { SaveLayoutModal } from "./components/SaveLayoutModal";
import EachSlide from "./components/EachSlide/NewEachSlide";
+import { APIKeyWarning } from "./components/APIKeyWarning";
+import { useAPIKeyCheck } from "./hooks/useAPIKeyCheck";
-const CustomLayoutPage = () => {
+const CustomTemplatePage = () => {
const { refetch } = useLayout();
// Custom hooks for different concerns
- const { hasOpenAIKey, isOpenAIKeyLoading } = useOpenAIKeyCheck();
+ const { hasRequiredKey, isRequiredKeyLoading } = useAPIKeyCheck();
const { selectedFile, handleFileSelect, removeFile } = useFileUpload();
const { slides, setSlides, completedSlides } = useCustomLayout();
const { fontsData, UploadedFonts, uploadFont, removeFont, getAllUnsupportedFonts, setFontsData } = useFontManagement();
@@ -35,7 +35,8 @@ const CustomLayoutPage = () => {
const { isSavingLayout, isModalOpen, openSaveModal, closeSaveModal, saveLayout } = useLayoutSaving(
slides,
UploadedFonts,
- refetch
+ refetch,
+ setSlides
);
const handleProcessSlideToHtml = (slide: any) => {
@@ -58,15 +59,16 @@ const CustomLayoutPage = () => {
};
// Loading state
- if (isOpenAIKeyLoading) {
- return ;
+ if (isRequiredKeyLoading) {
+ return ;
}
- // OpenAI key warning
- if (!hasOpenAIKey) {
- return ;
- }
+ // Anthropic key warning
+ if (!hasRequiredKey) {
+ return ;
+
+ }
return (
@@ -74,7 +76,7 @@ const CustomLayoutPage = () => {
{/* Header */}
- Custom Layout Processor
+ Custom Template Processor
Upload your PPTX file to extract slides and convert them to
@@ -151,4 +153,6 @@ const CustomLayoutPage = () => {
);
};
-export default CustomLayoutPage;
+export default CustomTemplatePage;
+
+
diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/types/index.ts b/servers/nextjs/app/(presentation-generator)/custom-template/types/index.ts
index 3866a322..8e57a035 100644
--- a/servers/nextjs/app/(presentation-generator)/custom-template/types/index.ts
+++ b/servers/nextjs/app/(presentation-generator)/custom-template/types/index.ts
@@ -19,6 +19,7 @@ export interface ProcessedSlide extends SlideData {
processed?: boolean;
error?: string;
modified?: boolean;
+ convertingToReact?: boolean; // indicates HTML-to-React conversion in progress
}
export interface FontData {
diff --git a/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx b/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx
index 41fabd4c..c7ad86dd 100644
--- a/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx
+++ b/servers/nextjs/app/(presentation-generator)/dashboard/components/DashboardPage.tsx
@@ -6,6 +6,7 @@ import Wrapper from "@/components/Wrapper";
import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard";
import { PresentationGrid } from "@/app/(presentation-generator)/dashboard/components/PresentationGrid";
+
import Header from "@/app/(presentation-generator)/dashboard/components/Header";
const DashboardPage: React.FC = () => {
diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts
index c7d2c35f..d53cab9e 100644
--- a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts
+++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts
@@ -31,13 +31,13 @@ export const useOutlineStreaming = (presentationId: string | null) => {
const data = JSON.parse(event.data);
switch (data.type) {
case "chunk":
- // console.log('data', data)
+ //
accumulatedChunks += data.chunk;
- // console.log('accumulatedChunks', accumulatedChunks)
+ //
try {
const repairedJson = jsonrepair(accumulatedChunks);
const partialData = JSON.parse(repairedJson);
- console.log('partialData', partialData)
+
if (partialData.slides) {
dispatch(setOutlines(partialData.slides));
setIsLoading(false)
@@ -48,7 +48,7 @@ export const useOutlineStreaming = (presentationId: string | null) => {
break;
case "complete":
- console.log('complete', data)
+
try {
const outlinesData: { content: string }[] = data.presentation.outlines.slides;
dispatch(setOutlines(outlinesData));
@@ -64,13 +64,13 @@ export const useOutlineStreaming = (presentationId: string | null) => {
break;
case "closing":
- console.log('closing', data)
+
setIsStreaming(false)
setIsLoading(false)
eventSource.close();
break;
case "error":
- console.log('error', data)
+
setIsStreaming(false)
setIsLoading(false)
eventSource.close();
@@ -84,14 +84,14 @@ export const useOutlineStreaming = (presentationId: string | null) => {
});
eventSource.onerror = () => {
- console.log('onerror')
+
setIsStreaming(false)
setIsLoading(false)
eventSource.close();
toast.error("Failed to connect to the server. Please try again.");
};
} catch (error) {
- console.log('error', error)
+
setIsStreaming(false)
setIsLoading(false)
toast.error("Failed to initialize connection");
diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx
new file mode 100644
index 00000000..4cc4055a
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/backup.tsx
@@ -0,0 +1,399 @@
+ "use client";
+
+ import React, { useEffect, useState, useRef } from "react";
+ import { useParams, useRouter } from "next/navigation";
+ // import { useGroupLayoutLoader } from '../hooks/useGroupLayoutLoader'
+ import LoadingStates from "../components/LoadingStates";
+ import { Card } from "@/components/ui/card";
+ import { Button } from "@/components/ui/button";
+ import { ArrowLeft, Edit, Home, Trash2 } from "lucide-react";
+ import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
+
+ import html2canvas from "html2canvas";
+import { EditControls } from "../../custom-template/components/EachSlide/EditControls";
+import { useDrawingCanvas } from "../../custom-template/hooks/useDrawingCanvas";
+ const GroupLayoutPreview = () => {
+ const params = useParams();
+ const router = useRouter();
+ const slug = params.slug as string;
+ // const isCustom = slug.includes("custom-");
+ const isCustom = true;
+ // Custom hooks
+ const {
+ canvasRef,
+ slideDisplayRef,
+ strokeWidth,
+ strokeColor,
+ eraserMode,
+ isDrawing,
+ canvasDimensions,
+ setCanvasDimensions,
+ didYourDraw,
+ handleMouseDown,
+ handleMouseMove,
+ handleMouseUp,
+ handleTouchStart,
+ handleTouchMove,
+ handleTouchEnd,
+ handleClearCanvas,
+ handleEraserModeChange,
+ handleStrokeColorChange,
+ handleStrokeWidthChange,
+ } = useDrawingCanvas();
+
+ const slideContentRef = useRef(null);
+
+ const { getFullDataByGroup, loading,refetch } = useLayout();
+ const layoutGroup = getFullDataByGroup(slug);
+ const [isEditMode, setIsEditMode] = useState(false);
+ const [selectedIndex, setSelectedIndex] = useState(null);
+ const [prompt, setPrompt] = useState("");
+ const [isUpdating, setIsUpdating] = useState(false);
+
+ useEffect(() => {
+ 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);
+ }
+ }, [slug]);
+
+ // Size canvas to content when entering edit mode
+ useEffect(() => {
+ if (isEditMode && slideContentRef.current) {
+ const rect = slideContentRef.current.getBoundingClientRect();
+ setCanvasDimensions({
+ width: Math.max(rect.width, 800),
+ height: Math.max(rect.height, 600),
+ });
+ }
+ }, [isEditMode, setCanvasDimensions]);
+
+ // Handle loading state
+ if (loading) {
+ return ;
+ }
+
+ // Handle empty state
+ if (!layoutGroup || layoutGroup.length === 0) {
+ return ;
+ }
+ const deleteLayouts = async () => {
+ const presentationId = slug.replace('custom-','');
+ refetch();
+ router.back();
+ const response = await fetch(`/api/v1/ppt/layout-management/delete-layouts/${presentationId}`, {
+ method: "DELETE",
+ });
+ if (response.ok) {
+ router.push("/template-preview");
+ }
+ }
+
+ const handleSave = async (
+ slideDisplayRef: React.RefObject,
+ didYourDraw: boolean
+ ) => {
+ if (
+ !slideContentRef.current ||
+ !slideDisplayRef.current
+ )
+ return;
+
+ if (!prompt.trim()) {
+ alert("Please enter a prompt before saving.");
+ return;
+ }
+
+ setIsUpdating(true);
+
+ try {
+ // Take screenshot of the slide display area (slide only)
+ const slideOnly = await html2canvas(slideDisplayRef.current, {
+ backgroundColor: "#ffffff",
+ scale: 1,
+ logging: false,
+ useCORS: true,
+ ignoreElements: (element) => {
+ return element.tagName === "CANVAS";
+ },
+ });
+ let slideWithCanvas;
+ if (didYourDraw) {
+ // Take screenshot of the entire slide display area including canvas
+ slideWithCanvas = await html2canvas(slideDisplayRef.current, {
+ backgroundColor: "#ffffff",
+ scale: 1,
+ logging: false,
+ useCORS: true,
+ });
+ }
+
+
+
+ const currentUiImageBlob = dataURLToBlob(
+ slideOnly.toDataURL("image/png")
+ );
+ let sketchImageBlob;
+ if (didYourDraw && slideWithCanvas) {
+ sketchImageBlob = dataURLToBlob(slideWithCanvas.toDataURL("image/png"));
+ }
+
+ // download the images
+
+ const currentUiImageUrl = URL.createObjectURL(currentUiImageBlob);
+ if (currentUiImageUrl) {
+ const a = document.createElement("a");
+ a.href = currentUiImageUrl;
+ a.download = `slide-current.png`;
+ a.click();
+ }
+ if (sketchImageBlob) {
+ const sketchImageUrl = URL.createObjectURL(sketchImageBlob);
+ if (sketchImageUrl) {
+ const b = document.createElement("a");
+ b.href = sketchImageUrl;
+ b.download = `slide-sketch.png`;
+ b.click();
+ }
+ }
+
+
+
+
+ // const formData = new FormData();
+ // formData.append(
+ // "current_ui_image",
+ // currentUiImageBlob,
+ // `slide--current.png`
+ // );
+ // if (didYourDraw && slideWithCanvas && sketchImageBlob) {
+ // formData.append(
+ // "sketch_image",
+ // sketchImageBlob,
+ // `slide-sketch.png`
+ // );
+ // }
+ // formData.append("html", '');
+ // formData.append("prompt", prompt);
+
+ // const response = await fetch("/api/v1/ppt/html-edit/", {
+ // method: "POST",
+ // body: formData,
+ // });
+
+ // if (!response.ok) {
+ // throw new Error(`API call failed: ${response.statusText}`);
+ // }
+
+ // const data = await response.json();
+
+
+ // Exit edit mode
+ setIsEditMode(false);
+ setPrompt("");
+ } catch (error) {
+ console.error("Error updating slide:", error);
+ alert(
+ `Error updating slide: ${
+ error instanceof Error ? error.message : "Unknown error"
+ }`
+ );
+ } finally {
+ setIsUpdating(false);
+ }
+ };
+ const dataURLToBlob = (dataURL: string): Blob => {
+ const parts = dataURL.split(",");
+ const contentType = parts[0].match(/:(.*?);/)?.[1] || "image/png";
+ const raw = window.atob(parts[1]);
+ const rawLength = raw.length;
+ const uInt8Array = new Uint8Array(rawLength);
+
+ for (let i = 0; i < rawLength; ++i) {
+ uInt8Array[i] = raw.charCodeAt(i);
+ }
+
+ return new Blob([uInt8Array], { type: contentType });
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Navigation */}
+
+
+
+ {isCustom &&
}
+
+
+
+
+ {layoutGroup[0].groupName} Layouts
+
+
+ {layoutGroup.length} layout{layoutGroup.length !== 1 ? "s" : ""} •{" "}
+ {layoutGroup[0].groupName}
+
+
+
+
+
+
+
+
+ {/* Edit Controls (no HTML editor) */}
+ {isCustom && (
+ {
+ setIsUpdating(true);
+ setTimeout(() => {
+ setIsUpdating(false);
+ setIsEditMode(false);
+ setSelectedIndex(null);
+ }, 300);
+ }}
+ onCancel={() => {
+ setIsEditMode(false);
+ setSelectedIndex(null);
+ handleClearCanvas();
+ }}
+ onStrokeWidthChange={handleStrokeWidthChange}
+ onStrokeColorChange={handleStrokeColorChange}
+ onEraserModeChange={handleEraserModeChange}
+ onClearCanvas={handleClearCanvas}
+ />
+ )}
+
+ {layoutGroup.map((layout: any, index: number) => {
+ const {
+ component: LayoutComponent,
+ sampleData,
+ name,
+ fileName,
+ } = layout;
+
+ const isSelected = isCustom && isEditMode && selectedIndex === index;
+
+ return (
+
+ {/* Layout Header */}
+
+
+
+
+ {name}
+
+
+
+ {fileName}
+
+
+ {layoutGroup[0].groupName}
+
+
+
+
+ {isCustom && (
+
+ )}
+
+
+
+
+ {/* Layout Content */}
+
+
+
+ {isSelected && (
+
+
+
+ );
+ })}
+
+
+
+ {/* Footer */}
+
+
+ );
+ };
+
+ export default GroupLayoutPreview;
diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx
index f54e7abd..6b634900 100644
--- a/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx
+++ b/servers/nextjs/app/(presentation-generator)/template-preview/[slug]/page.tsx
@@ -44,7 +44,7 @@ const GroupLayoutPreview = () => {
method: "DELETE",
});
if (response.ok) {
- router.push("/template-preview");
+ router.push("/layout-preview");
}
}
return (
@@ -66,7 +66,7 @@ const GroupLayoutPreview = () => {