feat(Nextjs): Custom Layout Editing & convert to React Component
This commit is contained in:
parent
4ebd480d46
commit
0b8db37b73
5 changed files with 475 additions and 170 deletions
|
|
@ -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
|
||||
|
|
|
|||
67
servers/nextjs/app/api/save-layout/route.ts
Normal file
67
servers/nextjs/app/api/save-layout/route.ts
Normal file
|
|
@ -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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DrawingCanvasProps> = ({
|
||||
slideElement,
|
||||
onClose,
|
||||
slideNumber,
|
||||
onSlideUpdate,
|
||||
}) => {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const slideDisplayRef = useRef<HTMLDivElement>(null);
|
||||
const slideContentRef = useRef<HTMLDivElement>(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<DrawingCanvasProps> = ({
|
|||
|
||||
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<DrawingCanvasProps> = ({
|
|||
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<DrawingCanvasProps> = ({
|
|||
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<DrawingCanvasProps> = ({
|
|||
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<DrawingCanvasProps> = ({
|
|||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-1 bg-green-600 hover:bg-green-700"
|
||||
>
|
||||
<Download size={16} />
|
||||
Save & Download
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
|
|
@ -337,6 +447,42 @@ const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Prompt Section */}
|
||||
<div className="p-4 border-b bg-gray-50 flex-shrink-0">
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
htmlFor="edit-prompt"
|
||||
className="text-sm font-medium text-gray-700"
|
||||
>
|
||||
Describe the changes you want to make:
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Textarea
|
||||
id="edit-prompt"
|
||||
placeholder="Enter your prompt here... (e.g., 'Change the title color to blue', 'Add a border to the image', etc.)"
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="flex-1 min-h-[80px] max-h-[80px] resize-none"
|
||||
disabled={isUpdating}
|
||||
/>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isUpdating || !prompt.trim()}
|
||||
className="flex items-center gap-1 bg-green-600 hover:bg-green-700 px-6"
|
||||
>
|
||||
{isUpdating ? (
|
||||
"Updating..."
|
||||
) : (
|
||||
<>
|
||||
<SendHorizontal size={16} />
|
||||
Update Slide
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Canvas Area */}
|
||||
<div className="flex-1 overflow-auto bg-gray-100 p-4">
|
||||
<div
|
||||
|
|
@ -347,15 +493,19 @@ const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
|||
height: canvasDimensions.height,
|
||||
}}
|
||||
>
|
||||
{/* Slide Background */}
|
||||
{slideElement && (
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none z-10"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slideElement.innerHTML,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{/* Slide Background - Static HTML content */}
|
||||
<div
|
||||
ref={slideContentRef}
|
||||
className="absolute inset-0 z-10"
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
isolation: "isolate",
|
||||
contain: "layout style paint",
|
||||
}}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slideHtml,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Drawing Canvas */}
|
||||
<canvas
|
||||
|
|
@ -368,6 +518,8 @@ const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
|||
left: 0,
|
||||
zIndex: 20,
|
||||
cursor: eraserMode ? "grab" : "crosshair",
|
||||
pointerEvents: "auto",
|
||||
touchAction: "none",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
|
|
@ -376,6 +528,7 @@ const DrawingCanvas: React.FC<DrawingCanvasProps> = ({
|
|||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,8 @@
|
|||
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 { AlertCircle, CheckCircle, Edit, Loader2 } 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";
|
||||
|
||||
|
|
@ -23,11 +11,13 @@ const EachSlide = ({
|
|||
index,
|
||||
retrySlide,
|
||||
setSlides,
|
||||
onSlideUpdate,
|
||||
}: {
|
||||
slide: any;
|
||||
index: number;
|
||||
retrySlide: (index: number) => void;
|
||||
setSlides: (slides: any[]) => void;
|
||||
setSlides: React.Dispatch<React.SetStateAction<any[]>>;
|
||||
onSlideUpdate?: (updatedSlideData: any) => void;
|
||||
}) => {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [prompt, setPrompt] = useState("");
|
||||
|
|
@ -50,25 +40,6 @@ const EachSlide = ({
|
|||
}
|
||||
}, [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);
|
||||
};
|
||||
|
|
@ -77,6 +48,19 @@ const EachSlide = ({
|
|||
setShowDrawingCanvas(false);
|
||||
};
|
||||
|
||||
const handleSlideUpdate = (updatedSlideData: any) => {
|
||||
if (onSlideUpdate) {
|
||||
onSlideUpdate(updatedSlideData);
|
||||
} else {
|
||||
// Fallback to original behavior
|
||||
setSlides((prevSlides) =>
|
||||
prevSlides.map((s, i) =>
|
||||
i === index ? { ...s, ...updatedSlideData } : s
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card key={slide.slide_number} className="border-2 w-full relative">
|
||||
|
|
@ -184,60 +168,6 @@ const EachSlide = ({
|
|||
>
|
||||
Open in new tab
|
||||
</Button>
|
||||
<div className="absolute top-2 z-20 sm:top-4 hidden md:block left-2 sm:left-4 transition-transform">
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<ToolTip content="Update slide using prompt">
|
||||
<div
|
||||
className={`p-2 group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md `}
|
||||
>
|
||||
<WandSparkles className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
side="right"
|
||||
align="start"
|
||||
sideOffset={10}
|
||||
className="w-[280px] sm:w-[400px] z-20"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<form
|
||||
className="flex flex-col gap-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}}
|
||||
>
|
||||
<Textarea
|
||||
id={`slide-${slide.index}-prompt`}
|
||||
placeholder="Enter your prompt here..."
|
||||
className="w-full min-h-[100px] max-h-[100px] p-2 text-sm border rounded-lg focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
disabled={isUpdating}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
}}
|
||||
rows={4}
|
||||
wrap="soft"
|
||||
/>
|
||||
<button
|
||||
disabled={isUpdating}
|
||||
type="submit"
|
||||
className={`bg-gradient-to-r from-[#9034EA] to-[#5146E5] rounded-[32px] px-4 py-2 text-white flex items-center justify-end gap-2 ml-auto ${
|
||||
isUpdating ? "opacity-70 cursor-not-allowed" : ""
|
||||
}`}
|
||||
>
|
||||
{isUpdating ? "Updating..." : "Update"}
|
||||
<SendHorizontal className="w-4 sm:w-5 h-4 sm:h-5" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className="absolute top-2 z-20 sm:top-4 hidden md:block left-2 sm:left-16 transition-transform">
|
||||
<ToolTip content="Edit slide">
|
||||
<div
|
||||
|
|
@ -256,6 +186,7 @@ const EachSlide = ({
|
|||
slideElement={slideContentRef.current}
|
||||
onClose={handleCloseDrawingCanvas}
|
||||
slideNumber={slide.slide_number}
|
||||
onSlideUpdate={handleSlideUpdate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
|
|
@ -13,7 +13,7 @@ import { Input } from "@/components/ui/input";
|
|||
import { Label } from "@/components/ui/label";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { toast } from "sonner";
|
||||
import { Upload, FileText, X } from "lucide-react";
|
||||
import { Upload, FileText, X, Loader2 } from "lucide-react";
|
||||
import { ApiResponseHandler } from "@/app/(presentation-generator)/services/api/api-error-handler";
|
||||
|
||||
// Types
|
||||
|
|
@ -30,6 +30,7 @@ interface ProcessedSlide extends SlideData {
|
|||
processing?: boolean;
|
||||
processed?: boolean;
|
||||
error?: string;
|
||||
modified?: boolean; // Added for unsaved changes
|
||||
}
|
||||
|
||||
const CustomLayoutPage = () => {
|
||||
|
|
@ -37,6 +38,121 @@ const CustomLayoutPage = () => {
|
|||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
||||
const [isProcessingPptx, setIsProcessingPptx] = useState(false);
|
||||
const [slides, setSlides] = useState<ProcessedSlide[]>([]);
|
||||
const [isSavingLayout, setIsSavingLayout] = useState(false);
|
||||
|
||||
// Warning before page unload
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
e.preventDefault();
|
||||
return "You have unsaved changes. Are you sure you want to leave?";
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}, []);
|
||||
|
||||
// Save layout functionality
|
||||
const saveLayout = useCallback(async () => {
|
||||
if (!slides.length) {
|
||||
toast.error("No slides to save");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSavingLayout(true);
|
||||
const layoutName = `layout-${Date.now()}`;
|
||||
|
||||
try {
|
||||
// Convert each slide HTML to React component
|
||||
const reactComponents = [];
|
||||
|
||||
for (let i = 0; i < slides.length; i++) {
|
||||
const slide = slides[i];
|
||||
|
||||
if (!slide.html) {
|
||||
toast.error(`Slide ${slide.slide_number} has no HTML content`);
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/v1/ppt/html-to-react/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
html: slide.html,
|
||||
component_name: `Slide${slide.slide_number}`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to convert slide ${slide.slide_number} to React`
|
||||
);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
reactComponents.push({
|
||||
slide_number: slide.slide_number,
|
||||
component_code: data.react_component || data.component_code,
|
||||
component_name: `Slide${slide.slide_number}`,
|
||||
});
|
||||
|
||||
// Update progress
|
||||
toast.info(
|
||||
`Converted slide ${slide.slide_number} to React component`
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(`Error converting slide ${slide.slide_number}:`, error);
|
||||
toast.error(`Failed to convert slide ${slide.slide_number}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (reactComponents.length === 0) {
|
||||
toast.error("No slides were successfully converted");
|
||||
return;
|
||||
}
|
||||
|
||||
// Save the layout components to the app_data/layouts folder
|
||||
const saveResponse = await fetch("/api/save-layout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
layout_name: layoutName,
|
||||
components: reactComponents,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!saveResponse.ok) {
|
||||
throw new Error("Failed to save layout components");
|
||||
}
|
||||
|
||||
const saveData = await saveResponse.json();
|
||||
|
||||
// Mark all slides as saved (remove modified flag)
|
||||
setSlides((prevSlides) =>
|
||||
prevSlides.map((slide) => ({ ...slide, modified: false }))
|
||||
);
|
||||
|
||||
toast.success(`Layout saved successfully as ${layoutName}`, {
|
||||
description: `${reactComponents.length} React components saved to ${
|
||||
saveData.path || "/app_data/layouts/"
|
||||
}`,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error saving layout:", error);
|
||||
toast.error("Failed to save layout", {
|
||||
description:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: "An unexpected error occurred",
|
||||
});
|
||||
} finally {
|
||||
setIsSavingLayout(false);
|
||||
}
|
||||
}, [slides]);
|
||||
|
||||
// File upload handler
|
||||
const handleFileSelect = useCallback(
|
||||
|
|
@ -84,34 +200,21 @@ const CustomLayoutPage = () => {
|
|||
);
|
||||
|
||||
try {
|
||||
// const htmlResponse = await fetch("/api/v1/ppt/slide-to-html/", {
|
||||
// method: "POST",
|
||||
// headers: {
|
||||
// "Content-Type": "application/json",
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// image: slide.screenshot_url,
|
||||
// xml: slide.xml_content,
|
||||
// }),
|
||||
// });
|
||||
let htmlResponse: any;
|
||||
if (index === 0) {
|
||||
htmlResponse = firstSlide;
|
||||
} else if (index === 1) {
|
||||
htmlResponse = slide2;
|
||||
} else if (index === 2) {
|
||||
htmlResponse = slide3;
|
||||
} else {
|
||||
htmlResponse = slide4;
|
||||
}
|
||||
const htmlResponse = await fetch("/api/v1/ppt/slide-to-html/", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image: slide.screenshot_url,
|
||||
xml: slide.xml_content,
|
||||
}),
|
||||
});
|
||||
|
||||
// const htmlData: SlideToHtmlResponse =
|
||||
// await ApiResponseHandler.handleResponse(
|
||||
// htmlResponse,
|
||||
// `Failed to convert slide ${slide.slide_number} to HTML`
|
||||
// );
|
||||
|
||||
const htmlData = htmlResponse;
|
||||
const htmlData = await ApiResponseHandler.handleResponse(
|
||||
htmlResponse,
|
||||
`Failed to convert slide ${slide.slide_number} to HTML`
|
||||
);
|
||||
|
||||
console.log(`Successfully processed slide ${slide.slide_number}`);
|
||||
|
||||
|
|
@ -198,29 +301,27 @@ const CustomLayoutPage = () => {
|
|||
const formData = new FormData();
|
||||
formData.append("pptx_file", selectedFile);
|
||||
|
||||
// const pptxResponse = await fetch("/api/v1/ppt/pptx-slides/process", {
|
||||
// method: "POST",
|
||||
// body: formData,
|
||||
// });
|
||||
const pptxResponse = processData;
|
||||
const pptxData = pptxResponse;
|
||||
const pptxResponse = await fetch("/api/v1/ppt/pptx-slides/process", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
const pptxData = await ApiResponseHandler.handleResponse(
|
||||
pptxResponse,
|
||||
"Failed to process PPTX file"
|
||||
);
|
||||
|
||||
// const pptxData: PptxProcessResponse =
|
||||
// await ApiResponseHandler.handleResponse(
|
||||
// pptxResponse,
|
||||
// "Failed to process PPTX file"
|
||||
// );
|
||||
|
||||
// if (!pptxData.success || !pptxData.slides?.length) {
|
||||
// throw new Error("No slides found in the PPTX file");
|
||||
// }
|
||||
if (!pptxData.success || !pptxData.slides?.length) {
|
||||
throw new Error("No slides found in the PPTX file");
|
||||
}
|
||||
|
||||
// Initialize slides with skeleton state
|
||||
const initialSlides: ProcessedSlide[] = pptxData.slides.map((slide) => ({
|
||||
...slide,
|
||||
processing: false,
|
||||
processed: false,
|
||||
}));
|
||||
const initialSlides: ProcessedSlide[] = pptxData.slides.map(
|
||||
(slide: any) => ({
|
||||
...slide,
|
||||
processing: false,
|
||||
processed: false,
|
||||
})
|
||||
);
|
||||
|
||||
setSlides(initialSlides);
|
||||
|
||||
|
|
@ -256,6 +357,24 @@ const CustomLayoutPage = () => {
|
|||
[slides, processSlideToHtml]
|
||||
);
|
||||
|
||||
// Mark slide as modified when it's updated
|
||||
const handleSlideUpdate = useCallback(
|
||||
(index: number, updatedSlideData: any) => {
|
||||
setSlides((prevSlides) =>
|
||||
prevSlides.map((s, i) =>
|
||||
i === index
|
||||
? {
|
||||
...s,
|
||||
...updatedSlideData,
|
||||
modified: true,
|
||||
}
|
||||
: s
|
||||
)
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// Calculate progress
|
||||
const completedSlides = slides.filter(
|
||||
(slide) => slide.processed || slide.error
|
||||
|
|
@ -386,10 +505,37 @@ const CustomLayoutPage = () => {
|
|||
index={index}
|
||||
retrySlide={retrySlide}
|
||||
setSlides={setSlides}
|
||||
onSlideUpdate={(updatedSlideData) =>
|
||||
handleSlideUpdate(index, updatedSlideData)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Floating Save Layout Button */}
|
||||
{slides.length > 0 && slides.some((s) => s.processed) && (
|
||||
<div className="fixed bottom-6 right-6 z-50">
|
||||
<Button
|
||||
onClick={saveLayout}
|
||||
disabled={isSavingLayout}
|
||||
className="bg-green-600 hover:bg-green-700 text-white shadow-lg hover:shadow-xl transition-all duration-200 px-6 py-3 text-lg"
|
||||
size="lg"
|
||||
>
|
||||
{isSavingLayout ? (
|
||||
<>
|
||||
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
||||
Saving Layout...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<FileText className="w-5 h-5 mr-2" />
|
||||
Save Layout
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue