From 201e2ee11d4ac06da511b3b1bd8503bc14e6ee0f Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Thu, 31 Jul 2025 01:56:29 +0545 Subject: [PATCH] feat(Nextjs): Custom Layout Extractor & Canvas Draw implemented --- docker-compose.yml | 18 +- .../components/DrawingCanvas.tsx | 387 +++++++++++++++++ .../custom-layout/components/EachSlide.tsx | 265 ++++++++++++ servers/nextjs/app/custom-layout/data.ts | 122 ++++++ servers/nextjs/app/custom-layout/page.tsx | 398 ++++++++++++++++++ servers/nextjs/package.json | 2 + 6 files changed, 1184 insertions(+), 8 deletions(-) create mode 100644 servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx create mode 100644 servers/nextjs/app/custom-layout/components/EachSlide.tsx create mode 100644 servers/nextjs/app/custom-layout/data.ts create mode 100644 servers/nextjs/app/custom-layout/page.tsx diff --git a/docker-compose.yml b/docker-compose.yml index 64798520..ed92019b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,13 @@ -services: +services: production: # image: ghcr.io/presenton/presenton:latest build: context: . dockerfile: Dockerfile - ports: + ports: # You can replace 5000 with any other port number of your choice to run Presenton on a different port number. - "5000:80" - volumes: + volumes: - ./app_data:/app_data environment: - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} @@ -34,10 +34,10 @@ services: - driver: nvidia count: 1 capabilities: [gpu] - ports: + ports: # You can replace 5000 with any other port number of your choice to run Presenton on a different port number. - "5000:80" - volumes: + volumes: - ./app_data:/app_data environment: - CAN_CHANGE_KEYS=${CAN_CHANGE_KEYS} @@ -57,7 +57,7 @@ services: context: . dockerfile: Dockerfile.dev ports: - - "5000:80" + - "5000:80" volumes: - .:/app - ./app_data:/app_data @@ -74,6 +74,7 @@ services: - CUSTOM_MODEL=${CUSTOM_MODEL} - PEXELS_API_KEY=${PEXELS_API_KEY} - DATABASE_URL=${DATABASE_URL} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} development-gpu: build: @@ -87,7 +88,7 @@ services: count: 1 capabilities: [gpu] ports: - - "5000:80" + - "5000:80" volumes: - .:/app - ./app_data:/app_data @@ -103,4 +104,5 @@ services: - CUSTOM_LLM_API_KEY=${CUSTOM_LLM_API_KEY} - CUSTOM_MODEL=${CUSTOM_MODEL} - PEXELS_API_KEY=${PEXELS_API_KEY} - - DATABASE_URL=${DATABASE_URL} \ No newline at end of file + - DATABASE_URL=${DATABASE_URL} + - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY} diff --git a/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx b/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx new file mode 100644 index 00000000..69c84e60 --- /dev/null +++ b/servers/nextjs/app/custom-layout/components/DrawingCanvas.tsx @@ -0,0 +1,387 @@ +"use client"; + +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 html2canvas from "html2canvas"; + +interface DrawingCanvasProps { + slideElement: HTMLElement | null; + onClose: () => void; + slideNumber: number; +} + +const DrawingCanvas: React.FC = ({ + slideElement, + onClose, + slideNumber, +}) => { + const canvasRef = useRef(null); + const containerRef = 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 [canvasDimensions, setCanvasDimensions] = useState({ + width: 800, + height: 600, + }); + + useEffect(() => { + if (slideElement && containerRef.current) { + const rect = slideElement.getBoundingClientRect(); + + // Set canvas dimensions to match the slide element + setCanvasDimensions({ + width: Math.max(rect.width, 800), + height: Math.max(rect.height, 600), + }); + } + }, [slideElement]); + + 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; + + 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; + const ctx = getCanvasContext(); + if (!canvas || !ctx) return; + + 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); + }; + + const handleSave = async () => { + if (!slideElement || !canvasRef.current || !slideDisplayRef.current) return; + + 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) => { + // Ignore the canvas element when taking screenshot of slide only + return element.tagName === "CANVAS"; + }, + }); + + // Take screenshot of the entire slide display area including canvas + const slideWithCanvas = await html2canvas(slideDisplayRef.current, { + backgroundColor: "#ffffff", + scale: 1, + logging: false, + useCORS: true, + }); + + // Download both images + const slideOnlyDataURL = slideOnly.toDataURL("image/png"); + const slideWithCanvasDataURL = slideWithCanvas.toDataURL("image/png"); + + downloadImage(slideOnlyDataURL, `slide-${slideNumber}-original.png`); + downloadImage( + slideWithCanvasDataURL, + `slide-${slideNumber}-with-annotations.png` + ); + } catch (error) { + console.error("Error capturing slide:", error); + } + }; + + const handleEraserModeChange = (isEraser: boolean) => { + setEraserMode(isEraser); + }; + + const handleStrokeColorChange = (color: string) => { + setStrokeColor(color); + setEraserMode(false); // Switch back to draw mode when selecting color + }; + + const handleStrokeWidthChange = (width: number) => { + setStrokeWidth(width); + }; + + const colors = [ + "#000000", + "#FF0000", + "#00FF00", + "#0000FF", + "#FFFF00", + "#FF00FF", + "#00FFFF", + "#FFA500", + ]; + + const strokeWidths = [1, 3, 5, 8, 12]; + + return ( +
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} + > +
+ {/* Controls */} +
+
+

Edit Slide {slideNumber}

+ + {/* Drawing Tools */} +
+ + + +
+ + {/* Color Picker */} + {!eraserMode && ( +
+ {colors.map((color) => ( +
+ )} + + {/* Stroke Width */} +
+ {strokeWidths.map((width) => ( + + ))} +
+ + +
+ +
+ + +
+
+ + {/* Canvas Area */} +
+
+ {/* Slide Background */} + {slideElement && ( +
+ )} + + {/* Drawing Canvas */} + +
+
+
+
+ ); +}; + +export default DrawingCanvas; diff --git a/servers/nextjs/app/custom-layout/components/EachSlide.tsx b/servers/nextjs/app/custom-layout/components/EachSlide.tsx new file mode 100644 index 00000000..d5c0b7eb --- /dev/null +++ b/servers/nextjs/app/custom-layout/components/EachSlide.tsx @@ -0,0 +1,265 @@ +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + AlertCircle, + CheckCircle, + Edit, + Loader2, + SendHorizontal, + WandSparkles, +} from "lucide-react"; +import React, { useState, useEffect, useRef } from "react"; +import { Textarea } from "@/components/ui/textarea"; +import ToolTip from "@/components/ToolTip"; +import DrawingCanvas from "./DrawingCanvas"; + +const EachSlide = ({ + slide, + index, + retrySlide, + setSlides, +}: { + slide: any; + index: number; + retrySlide: (index: number) => void; + setSlides: (slides: any[]) => void; +}) => { + const [isUpdating, setIsUpdating] = useState(false); + const [prompt, setPrompt] = useState(""); + const [showDrawingCanvas, setShowDrawingCanvas] = useState(false); + const slideContentRef = useRef(null); + + // 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"]' + ); + if (!existingScript) { + const script = document.createElement("script"); + script.src = "https://cdn.tailwindcss.com"; + script.async = true; + document.head.appendChild(script); + } + } + }, [slide.processed, slide.html]); + + const handleSubmit = async () => { + setIsUpdating(true); + try { + const response = await fetch("/api/update-slide", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ slide_number: slide.slide_number, prompt }), + }); + const data = await response.json(); + console.log(data); + } catch (error) { + console.error("Error updating slide:", error); + } finally { + setIsUpdating(false); + } + }; + + const handleEditClick = () => { + setShowDrawingCanvas(true); + }; + + const handleCloseDrawingCanvas = () => { + setShowDrawingCanvas(false); + }; + + return ( + <> + + + + {slide.processing ? ( + + ) : slide.processed ? ( + + ) : slide.error ? ( + + ) : ( +
+ )} + + + + {/* Slide Content */} + {slide.processing ? ( +
+

+ 🔄 Converting to HTML... +

+
+
+
+
+
+
+ ) : slide.processed && slide.html ? ( +
+ ) : 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. +

+
+ ) : ( + slide.error + )} +
+ +
+ ) : ( +
+

+ ⏳ Waiting in queue to process... +

+
+
+
+
+
+
+ )} +
+ +
+ + + +
+ +
+
+
+ +
+
{ + e.preventDefault(); + handleSubmit(); + }} + > +