From 0b8db37b73600816d2fc18de77e86bc8ebb977cb Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Thu, 31 Jul 2025 14:25:58 +0545 Subject: [PATCH] feat(Nextjs): Custom Layout Editing & convert to React Component --- Dockerfile.dev | 10 +- servers/nextjs/app/api/save-layout/route.ts | 67 +++++ .../components/DrawingCanvas.tsx | 217 +++++++++++++--- .../custom-layout/components/EachSlide.tsx | 107 ++------ servers/nextjs/app/custom-layout/page.tsx | 244 ++++++++++++++---- 5 files changed, 475 insertions(+), 170 deletions(-) create mode 100644 servers/nextjs/app/api/save-layout/route.ts diff --git a/Dockerfile.dev b/Dockerfile.dev index 3c84ac4b..a4dd8964 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -4,7 +4,15 @@ FROM python:3.11-slim-bookworm RUN apt-get update && apt-get install -y \ nginx \ curl \ - redis-server + redis-server \ + default-libmysqlclient-dev \ + build-essential \ + pkg-config \ + libreoffice \ + fontconfig \ + imagemagick + +RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml # Install Node.js 20 using NodeSource repository diff --git a/servers/nextjs/app/api/save-layout/route.ts b/servers/nextjs/app/api/save-layout/route.ts new file mode 100644 index 00000000..89c26f19 --- /dev/null +++ b/servers/nextjs/app/api/save-layout/route.ts @@ -0,0 +1,67 @@ +import { NextRequest, NextResponse } from "next/server"; +import { writeFile, mkdir } from "fs/promises"; +import { join } from "path"; +import { existsSync } from "fs"; + +export async function POST(request: NextRequest) { + try { + const { layout_name, components } = await request.json(); + + if (!layout_name || !components || !Array.isArray(components)) { + return NextResponse.json( + { + error: + "Invalid request body. Expected layout_name and components array.", + }, + { status: 400 } + ); + } + + // Define the layouts directory path + const layoutsDir = join(process.cwd(), "app_data", "layouts", layout_name); + + // Create the directory if it doesn't exist + if (!existsSync(layoutsDir)) { + await mkdir(layoutsDir, { recursive: true }); + } + + // Save each component as a separate file + const savedFiles = []; + + for (const component of components) { + const { slide_number, component_code, component_name } = component; + + if (!component_code || !component_name) { + console.warn( + `Skipping component for slide ${slide_number}: missing code or name` + ); + continue; + } + + const fileName = `${component_name}.tsx`; + const filePath = join(layoutsDir, fileName); + + await writeFile(filePath, component_code, "utf8"); + savedFiles.push({ + slide_number, + component_name, + file_path: filePath, + file_name: fileName, + }); + } + + return NextResponse.json({ + success: true, + layout_name, + path: layoutsDir, + saved_files: savedFiles.length, + components: savedFiles, + }); + } catch (error) { + console.error("Error saving layout:", error); + return NextResponse.json( + { error: "Failed to save layout components" }, + { status: 500 } + ); + } +} diff --git a/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx b/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx index 69c84e60..5b140d7d 100644 --- a/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx +++ b/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx @@ -2,27 +2,41 @@ import React, { useRef, useState, useEffect, useCallback } from "react"; import { Button } from "@/components/ui/button"; -import { Save, X, Pencil, Eraser, RotateCcw, Download } from "lucide-react"; +import { Textarea } from "@/components/ui/textarea"; +import { + X, + Pencil, + Eraser, + RotateCcw, + Download, + SendHorizontal, +} from "lucide-react"; import html2canvas from "html2canvas"; interface DrawingCanvasProps { slideElement: HTMLElement | null; onClose: () => void; slideNumber: number; + onSlideUpdate: (updatedSlide: any) => void; } const DrawingCanvas: React.FC = ({ slideElement, onClose, slideNumber, + onSlideUpdate, }) => { const canvasRef = useRef(null); const containerRef = useRef(null); const slideDisplayRef = useRef(null); + const slideContentRef = useRef(null); const [strokeWidth, setStrokeWidth] = useState(3); const [strokeColor, setStrokeColor] = useState("#000000"); const [eraserMode, setEraserMode] = useState(false); const [isDrawing, setIsDrawing] = useState(false); + const [prompt, setPrompt] = useState(""); + const [isUpdating, setIsUpdating] = useState(false); + const [slideHtml, setSlideHtml] = useState(""); const [canvasDimensions, setCanvasDimensions] = useState({ width: 800, height: 600, @@ -30,6 +44,7 @@ const DrawingCanvas: React.FC = ({ useEffect(() => { if (slideElement && containerRef.current) { + console.log("slideElement", slideElement); const rect = slideElement.getBoundingClientRect(); // Set canvas dimensions to match the slide element @@ -37,9 +52,52 @@ const DrawingCanvas: React.FC = ({ width: Math.max(rect.width, 800), height: Math.max(rect.height, 600), }); + + // Store the HTML once to prevent re-renders + setSlideHtml(slideElement.innerHTML); } }, [slideElement]); + // Apply optimizations once after slide content is rendered + useEffect(() => { + if (slideContentRef.current && slideHtml) { + const slideContent = slideContentRef.current; + + // Apply styles to prevent interactions and flickering + slideContent.style.pointerEvents = "none"; + slideContent.style.userSelect = "none"; + slideContent.style.transform = "translateZ(0)"; + slideContent.style.willChange = "auto"; + slideContent.style.backfaceVisibility = "hidden"; + + // Target all interactive elements + 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; + } + + // Remove any event listeners + el.onclick = null; + el.onmousedown = null; + el.onmouseup = null; + el.onmousemove = null; + }); + } + }, [slideHtml]); + const getCanvasContext = () => { const canvas = canvasRef.current; if (!canvas) return null; @@ -154,18 +212,31 @@ const DrawingCanvas: React.FC = ({ ctx.clearRect(0, 0, canvas.width, canvas.height); }; - const downloadImage = (dataURL: string, filename: string) => { - const link = document.createElement("a"); - link.download = filename; - link.href = dataURL; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + // 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 (!slideElement || !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, { @@ -187,17 +258,63 @@ const DrawingCanvas: React.FC = ({ useCORS: true, }); - // Download both images - const slideOnlyDataURL = slideOnly.toDataURL("image/png"); - const slideWithCanvasDataURL = slideWithCanvas.toDataURL("image/png"); + // Get the current HTML content from the original slide element + const currentHtml = slideElement.innerHTML; - downloadImage(slideOnlyDataURL, `slide-${slideNumber}-original.png`); - downloadImage( - slideWithCanvasDataURL, - `slide-${slideNumber}-with-annotations.png` + // Convert canvas images to blobs + const currentUiImageBlob = dataURLToBlob( + slideOnly.toDataURL("image/png") ); + const sketchImageBlob = dataURLToBlob( + slideWithCanvas.toDataURL("image/png") + ); + + // Prepare form data + const formData = new FormData(); + formData.append( + "current_ui_image", + currentUiImageBlob, + `slide-${slideNumber}-current.png` + ); + formData.append( + "sketch_image", + sketchImageBlob, + `slide-${slideNumber}-sketch.png` + ); + formData.append("html", currentHtml); + formData.append("prompt", prompt); + + // Call the API + 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(); + + // Update the slide with new data + onSlideUpdate({ + slide_number: slideNumber, + html: data.edited_html || currentHtml, + processed: true, + processing: false, + error: undefined, + }); + // Close the drawing canvas + onClose(); } catch (error) { - console.error("Error capturing slide:", error); + console.error("Error updating slide:", error); + alert( + `Error updating slide: ${ + error instanceof Error ? error.message : "Unknown error" + }` + ); + } finally { + setIsUpdating(false); } }; @@ -319,13 +436,6 @@ const DrawingCanvas: React.FC = ({
-
+ {/* Prompt Section */} +
+
+ +
+