From 049824836c6499ff4bfc0f48d0f9bf081ccea4ea Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Thu, 31 Jul 2025 22:36:58 +0545 Subject: [PATCH] feat(Nextjs): Save Layout implemented --- servers/nextjs/app/api/save-layout/route.ts | 5 +- .../custom-layout/components/EachSlide.tsx | 705 +++++++++-- .../custom-layout/components/SlideContent.tsx | 13 + servers/nextjs/app/custom-layout/page.tsx | 75 +- servers/nextjs/package-lock.json | 1119 ++++++++++++++++- servers/nextjs/package.json | 2 + servers/nextjs/tsconfig.tsbuildinfo | 1 + 7 files changed, 1745 insertions(+), 175 deletions(-) create mode 100644 servers/nextjs/app/custom-layout/components/SlideContent.tsx create mode 100644 servers/nextjs/tsconfig.tsbuildinfo diff --git a/servers/nextjs/app/api/save-layout/route.ts b/servers/nextjs/app/api/save-layout/route.ts index 89c26f19..9d57cf56 100644 --- a/servers/nextjs/app/api/save-layout/route.ts +++ b/servers/nextjs/app/api/save-layout/route.ts @@ -40,8 +40,11 @@ export async function POST(request: NextRequest) { const fileName = `${component_name}.tsx`; const filePath = join(layoutsDir, fileName); + const cleanComponentCode = component_code + .replace(/```tsx/g, "") + .replace(/```/g, ""); - await writeFile(filePath, component_code, "utf8"); + await writeFile(filePath, cleanComponentCode, "utf8"); savedFiles.push({ slide_number, component_name, diff --git a/servers/nextjs/app/custom-layout/components/EachSlide.tsx b/servers/nextjs/app/custom-layout/components/EachSlide.tsx index e244a4e4..af88f730 100644 --- a/servers/nextjs/app/custom-layout/components/EachSlide.tsx +++ b/servers/nextjs/app/custom-layout/components/EachSlide.tsx @@ -1,10 +1,21 @@ import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; - -import { AlertCircle, CheckCircle, Edit, Loader2 } from "lucide-react"; -import React, { useState, useEffect, useRef } from "react"; +import { Textarea } from "@/components/ui/textarea"; +import { + AlertCircle, + CheckCircle, + Edit, + Loader2, + Pencil, + Eraser, + RotateCcw, + SendHorizontal, + X, +} from "lucide-react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import ToolTip from "@/components/ToolTip"; -import DrawingCanvas from "./DrawingCanvas"; +import html2canvas from "html2canvas"; +import SlideContent from "./SlideContent"; const EachSlide = ({ slide, @@ -21,13 +32,26 @@ const EachSlide = ({ }) => { const [isUpdating, setIsUpdating] = useState(false); const [prompt, setPrompt] = useState(""); - const [showDrawingCanvas, setShowDrawingCanvas] = useState(false); + const [isEditMode, setIsEditMode] = useState(false); const slideContentRef = useRef(null); + // Drawing canvas states + const canvasRef = useRef(null); + const slideDisplayRef = useRef(null); + const [strokeWidth, setStrokeWidth] = useState(3); + const [strokeColor, setStrokeColor] = useState("#000000"); + const [eraserMode, setEraserMode] = useState(false); + const [isDrawing, setIsDrawing] = useState(false); + const [slideHtml, setSlideHtml] = useState(""); + const [canvasDimensions, setCanvasDimensions] = useState({ + width: 1280, + height: 720, + }); + const [didYourDraw, setDidYourDraw] = useState(false); + // Load Tailwind CSS dynamically for slide content useEffect(() => { if (slide.processed && slide.html) { - // Check if Tailwind CSS is already loaded const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' ); @@ -40,107 +64,578 @@ const EachSlide = ({ } }, [slide.processed, slide.html]); - const handleEditClick = () => { - setShowDrawingCanvas(true); - }; + // Set up canvas when entering edit mode + useEffect(() => { + if (isEditMode && slideContentRef.current && slide.html) { + const rect = slideContentRef.current.getBoundingClientRect(); - const handleCloseDrawingCanvas = () => { - setShowDrawingCanvas(false); - }; + setCanvasDimensions({ + width: Math.max(rect.width, 800), + height: Math.max(rect.height, 600), + }); - const handleSlideUpdate = (updatedSlideData: any) => { - if (onSlideUpdate) { - onSlideUpdate(updatedSlideData); - } else { - // Fallback to original behavior - setSlides((prevSlides) => - prevSlides.map((s, i) => - i === index ? { ...s, ...updatedSlideData } : s - ) + setSlideHtml(slide.html); + } + }, [isEditMode, slide.html]); + + // Apply optimizations once after slide content is rendered in edit mode + useEffect(() => { + if (isEditMode && slideDisplayRef.current && slideHtml) { + const slideContent = slideDisplayRef.current; + + slideContent.style.pointerEvents = "none"; + slideContent.style.userSelect = "none"; + slideContent.style.transform = "translateZ(0)"; + slideContent.style.willChange = "auto"; + slideContent.style.backfaceVisibility = "hidden"; + + const interactiveElements = slideContent.querySelectorAll( + "img, video, iframe, a, button, input, textarea, select" ); + + interactiveElements.forEach((element) => { + const el = element as HTMLElement; + el.style.pointerEvents = "none"; + el.style.userSelect = "none"; + (el.style as any).webkitUserSelect = "none"; + (el.style as any).webkitTouchCallout = "none"; + (el.style as any).webkitUserDrag = "none"; + el.style.transform = "translateZ(0)"; + el.style.backfaceVisibility = "hidden"; + + if (element.tagName === "IMG") { + (element as HTMLImageElement).draggable = false; + } + + el.onclick = null; + el.onmousedown = null; + el.onmouseup = null; + el.onmousemove = null; + }); + } + }, [isEditMode, slideHtml]); + + const getCanvasContext = () => { + const canvas = canvasRef.current; + if (!canvas) return null; + return canvas.getContext("2d"); + }; + + const getMousePos = (e: React.MouseEvent) => { + const canvas = canvasRef.current; + if (!canvas) return { x: 0, y: 0 }; + + const rect = canvas.getBoundingClientRect(); + return { + x: e.clientX - rect.left, + y: e.clientY - rect.top, + }; + }; + + const getTouchPos = (e: React.TouchEvent) => { + const canvas = canvasRef.current; + if (!canvas) return { x: 0, y: 0 }; + + const rect = canvas.getBoundingClientRect(); + const touch = e.touches[0]; + return { + x: touch.clientX - rect.left, + y: touch.clientY - rect.top, + }; + }; + + const startDrawing = useCallback( + (pos: { x: number; y: number }) => { + const ctx = getCanvasContext(); + if (!ctx) return; + + setIsDrawing(true); + ctx.beginPath(); + ctx.moveTo(pos.x, pos.y); + + if (eraserMode) { + ctx.globalCompositeOperation = "destination-out"; + ctx.lineWidth = strokeWidth * 2; + } else { + ctx.globalCompositeOperation = "source-over"; + ctx.strokeStyle = strokeColor; + ctx.lineWidth = strokeWidth; + } + + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + }, + [eraserMode, strokeColor, strokeWidth] + ); + + const draw = useCallback( + (pos: { x: number; y: number }) => { + if (!isDrawing) return; + setDidYourDraw(true); + + const ctx = getCanvasContext(); + if (!ctx) return; + + ctx.lineTo(pos.x, pos.y); + ctx.stroke(); + }, + [isDrawing] + ); + + const stopDrawing = useCallback(() => { + setIsDrawing(false); + }, []); + + // Mouse events + const handleMouseDown = (e: React.MouseEvent) => { + e.preventDefault(); + const pos = getMousePos(e); + startDrawing(pos); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + e.preventDefault(); + const pos = getMousePos(e); + draw(pos); + }; + + const handleMouseUp = (e: React.MouseEvent) => { + e.preventDefault(); + stopDrawing(); + }; + + // Touch events + const handleTouchStart = (e: React.TouchEvent) => { + e.preventDefault(); + const pos = getTouchPos(e); + startDrawing(pos); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + e.preventDefault(); + const pos = getTouchPos(e); + draw(pos); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + e.preventDefault(); + stopDrawing(); + }; + + const handleClearCanvas = () => { + const canvas = canvasRef.current; + setDidYourDraw(false); + const ctx = getCanvasContext(); + if (!canvas || !ctx) return; + + ctx.clearRect(0, 0, canvas.width, canvas.height); + }; + + // Convert data URL to blob for form data + 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 }); + }; + + const handleSave = async () => { + if ( + !slideContentRef.current || + !canvasRef.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 currentHtml = slide.html; + + const currentUiImageBlob = dataURLToBlob( + slideOnly.toDataURL("image/png") + ); + let sketchImageBlob; + if (didYourDraw && slideWithCanvas) { + sketchImageBlob = dataURLToBlob(slideWithCanvas.toDataURL("image/png")); + } + + const formData = new FormData(); + formData.append( + "current_ui_image", + currentUiImageBlob, + `slide-${slide.slide_number}-current.png` + ); + if (didYourDraw && slideWithCanvas && sketchImageBlob) { + formData.append( + "sketch_image", + sketchImageBlob, + `slide-${slide.slide_number}-sketch.png` + ); + } + formData.append("html", currentHtml); + 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(); + + const updatedSlideData = { + slide_number: slide.slide_number, + html: data.edited_html || currentHtml, + processed: true, + processing: false, + error: undefined, + }; + + if (onSlideUpdate) { + onSlideUpdate(updatedSlideData); + } else { + setSlides((prevSlides) => + prevSlides.map((s, i) => + i === index ? { ...s, ...updatedSlideData } : s + ) + ); + } + + // Exit edit mode + setIsEditMode(false); + setPrompt(""); + handleClearCanvas(); + } catch (error) { + console.error("Error updating slide:", error); + alert( + `Error updating slide: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } finally { + setIsUpdating(false); } }; + const handleEditClick = () => { + setIsEditMode(true); + }; + + const handleCancelEdit = () => { + setIsEditMode(false); + setPrompt(""); + handleClearCanvas(); + }; + + const handleEraserModeChange = (isEraser: boolean) => { + setEraserMode(isEraser); + }; + + const handleStrokeColorChange = (color: string) => { + setStrokeColor(color); + setEraserMode(false); + }; + + const handleStrokeWidthChange = (width: number) => { + setStrokeWidth(width); + }; + + const colors = [ + "#000000", + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + "#FFA500", + ]; + + const strokeWidths = [1, 3, 5, 8, 12]; + return ( - <> - - - - {slide.processing ? ( - - ) : slide.processed ? ( - - ) : slide.error ? ( - - ) : ( -
- )} - - - - {/* Slide Content */} + + + {slide.processing ? ( -
-

- 🔄 Converting to HTML... -

-
-
-
-
-
-
- ) : slide.processed && slide.html ? ( -
+ + ) : slide.processed ? ( + ) : slide.error ? ( -
-

- ✗ 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. -

+ + ) : ( +
+ )} + + + + + {/* Edit Mode Controls */} + {isEditMode && slide.processed && slide.html && ( +
+ {/* Drawing Tools */} +
+
+

+ Edit Mode +

+ + {/* Drawing Tools */} +
+ + + +
+ + {/* Color Picker */} + {!eraserMode && ( +
+ {colors.map((color) => ( +
- ) : ( - slide.error )} + + {/* Stroke Width */} +
+ {strokeWidths.map((width) => ( + + ))} +
+ +
+
- ) : ( -
-

- ⏳ Waiting in queue to process... -

-
-
-
-
+ + {/* Prompt Section */} +
+ +
+