diff --git a/Dockerfile.dev b/Dockerfile.dev index 1642a662..6de18da2 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -38,7 +38,7 @@ RUN pip install docling --extra-index-url https://download.pytorch.org/whl/cpu # Install dependencies for Next.js WORKDIR /node_dependencies COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./ -RUN npm install +RUN npm install --verbose # Install chrome for puppeteer RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps diff --git a/badu.js b/badu.js new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose.yml b/docker-compose.yml index 832b6ebe..85ca9210 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -129,7 +129,6 @@ services: - CUSTOM_LLM_API_KEY=${CUSTOM_LLM_API_KEY} - CUSTOM_MODEL=${CUSTOM_MODEL} - PEXELS_API_KEY=${PEXELS_API_KEY} - - DATABASE_URL=${DATABASE_URL} - EXTENDED_REASONING=${EXTENDED_REASONING} - TOOL_CALLS=${TOOL_CALLS} - DISABLE_THINKING=${DISABLE_THINKING} diff --git a/servers/fastapi/chroma/chroma.sqlite3 b/servers/fastapi/chroma/chroma.sqlite3 index caf17128..1b3a19a3 100644 Binary files a/servers/fastapi/chroma/chroma.sqlite3 and b/servers/fastapi/chroma/chroma.sqlite3 differ diff --git a/servers/nextjs/app/(presentation-generator)/custom-template/components/OpenAIKeyWarning.tsx b/servers/nextjs/app/(presentation-generator)/custom-template/components/APIKeyWarning.tsx similarity index 78% rename from servers/nextjs/app/(presentation-generator)/custom-template/components/OpenAIKeyWarning.tsx rename to servers/nextjs/app/(presentation-generator)/custom-template/components/APIKeyWarning.tsx index 0dd7aaa2..ad803232 100644 --- a/servers/nextjs/app/(presentation-generator)/custom-template/components/OpenAIKeyWarning.tsx +++ b/servers/nextjs/app/(presentation-generator)/custom-template/components/APIKeyWarning.tsx @@ -1,12 +1,15 @@ import React from "react"; import Header from "@/components/Header"; -export const OpenAIKeyWarning: React.FC = () => { +export const APIKeyWarning: React.FC = () => { return (
+

+ 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 && ( + e.preventDefault()} + /> + )} +
+
+
+ ); + })} +
+
+ + {/* Footer */} +
+
+
+

+ {layoutGroup[0].groupName} • {layoutGroup.length} components +

+
+
+
+
+ ); + }; + + 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 = () => {