feat(Nextjs): Save Layout implemented
This commit is contained in:
parent
0b8db37b73
commit
049824836c
7 changed files with 1745 additions and 175 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<HTMLDivElement>(null);
|
||||
|
||||
// Drawing canvas states
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const slideDisplayRef = useRef<HTMLDivElement>(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<HTMLCanvasElement>) => {
|
||||
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<HTMLCanvasElement>) => {
|
||||
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<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(e);
|
||||
startDrawing(pos);
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const pos = getMousePos(e);
|
||||
draw(pos);
|
||||
};
|
||||
|
||||
const handleMouseUp = (e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
stopDrawing();
|
||||
};
|
||||
|
||||
// Touch events
|
||||
const handleTouchStart = (e: React.TouchEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const pos = getTouchPos(e);
|
||||
startDrawing(pos);
|
||||
};
|
||||
|
||||
const handleTouchMove = (e: React.TouchEvent<HTMLCanvasElement>) => {
|
||||
e.preventDefault();
|
||||
const pos = getTouchPos(e);
|
||||
draw(pos);
|
||||
};
|
||||
|
||||
const handleTouchEnd = (e: React.TouchEvent<HTMLCanvasElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<Card key={slide.slide_number} className="border-2 w-full relative">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl flex items-center justify-between">
|
||||
{slide.processing ? (
|
||||
<Loader2 className="w-6 h-6 text-blue-600 animate-spin" />
|
||||
) : slide.processed ? (
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
) : slide.error ? (
|
||||
<AlertCircle className="w-6 h-6 text-red-600" />
|
||||
) : (
|
||||
<div className="w-6 h-6 border-2 border-gray-300 rounded-full" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Slide Content */}
|
||||
<Card key={slide.slide_number} className="border-2 w-full relative">
|
||||
<CardHeader className="pb-4">
|
||||
<CardTitle className="text-xl flex items-center justify-between">
|
||||
{slide.processing ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-blue-600 font-medium">
|
||||
🔄 Converting to HTML...
|
||||
</p>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : slide.processed && slide.html ? (
|
||||
<div
|
||||
ref={slideContentRef}
|
||||
className="relative "
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slide.html,
|
||||
}}
|
||||
></div>
|
||||
<Loader2 className="w-6 h-6 text-blue-600 animate-spin" />
|
||||
) : slide.processed ? (
|
||||
<CheckCircle className="w-6 h-6 text-green-600" />
|
||||
) : slide.error ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-red-600 font-medium">
|
||||
✗ Conversion failed
|
||||
</p>
|
||||
<div className="text-sm text-gray-700 p-4 bg-red-50 rounded border border-red-200">
|
||||
{slide.error.includes("image exceeds 5 MB maximum") ? (
|
||||
<div>
|
||||
<p className="font-medium text-red-700 mb-2">
|
||||
Image too large for processing
|
||||
</p>
|
||||
<p>
|
||||
This slide's image exceeds the 5MB limit. Try using a
|
||||
smaller resolution PPTX file.
|
||||
</p>
|
||||
<AlertCircle className="w-6 h-6 text-red-600" />
|
||||
) : (
|
||||
<div className="w-6 h-6 border-2 border-gray-300 rounded-full" />
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="space-y-4">
|
||||
{/* Edit Mode Controls */}
|
||||
{isEditMode && slide.processed && slide.html && (
|
||||
<div className="border-2 border-blue-200 rounded-lg p-4 bg-blue-50 space-y-4">
|
||||
{/* Drawing Tools */}
|
||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
||||
<div className="flex items-center gap-4 flex-wrap">
|
||||
<h4 className="text-sm font-semibold text-blue-800">
|
||||
Edit Mode
|
||||
</h4>
|
||||
|
||||
{/* Drawing Tools */}
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={!eraserMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleEraserModeChange(false)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
Draw
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={eraserMode ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => handleEraserModeChange(true)}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Eraser size={14} />
|
||||
Erase
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Color Picker */}
|
||||
{!eraserMode && (
|
||||
<div className="flex items-center gap-1">
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
className={`w-5 h-5 rounded-full border-2 ${
|
||||
strokeColor === color
|
||||
? "border-gray-800"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => handleStrokeColorChange(color)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
slide.error
|
||||
)}
|
||||
|
||||
{/* Stroke Width */}
|
||||
<div className="flex items-center gap-1">
|
||||
{strokeWidths.map((width) => (
|
||||
<button
|
||||
key={width}
|
||||
className={`w-7 h-7 rounded border flex items-center justify-center ${
|
||||
strokeWidth === width
|
||||
? "bg-blue-100 border-blue-500"
|
||||
: "border-gray-300"
|
||||
}`}
|
||||
onClick={() => handleStrokeWidthChange(width)}
|
||||
>
|
||||
<div
|
||||
className="rounded-full bg-gray-800"
|
||||
style={{
|
||||
width: `${width + 1}px`,
|
||||
height: `${width + 1}px`,
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleClearCanvas}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<RotateCcw size={14} />
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => retrySlide(index)}
|
||||
disabled={slide.processing}
|
||||
className="w-full text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
onClick={handleCancelEdit}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
🔄 Retry Conversion
|
||||
<X size={14} />
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-gray-500">
|
||||
⏳ Waiting in queue to process...
|
||||
</p>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
|
||||
{/* Prompt Section */}
|
||||
<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-[60px] max-h-[60px] 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-4"
|
||||
>
|
||||
{isUpdating ? (
|
||||
"Updating..."
|
||||
) : (
|
||||
<>
|
||||
<SendHorizontal size={14} />
|
||||
Update
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Slide Content */}
|
||||
{slide.processing ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-blue-600 font-medium">
|
||||
🔄 Converting to HTML...
|
||||
</p>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
) : slide.processed && slide.html ? (
|
||||
<div className="relative">
|
||||
<div ref={slideDisplayRef} className="relative mx-auto w-full ">
|
||||
{/* <div
|
||||
ref={slideContentRef}
|
||||
className="relative"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slide.html,
|
||||
}}
|
||||
/> */}
|
||||
<div ref={slideContentRef}>
|
||||
<SlideContent slide={slide} />
|
||||
</div>
|
||||
{isEditMode && (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={canvasDimensions.width}
|
||||
height={canvasDimensions.height}
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 30,
|
||||
cursor: eraserMode ? "grab" : "crosshair",
|
||||
pointerEvents: "auto",
|
||||
touchAction: "none",
|
||||
}}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseUp}
|
||||
onTouchStart={handleTouchStart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
onContextMenu={(e) => e.preventDefault()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : slide.error ? (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-red-600 font-medium">
|
||||
✗ Conversion failed
|
||||
</p>
|
||||
<div className="text-sm text-gray-700 p-4 bg-red-50 rounded border border-red-200">
|
||||
{slide.error.includes("image exceeds 5 MB maximum") ? (
|
||||
<div>
|
||||
<p className="font-medium text-red-700 mb-2">
|
||||
Image too large for processing
|
||||
</p>
|
||||
<p>
|
||||
This slide's image exceeds the 5MB limit. Try using a
|
||||
smaller resolution PPTX file.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
slide.error
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => retrySlide(index)}
|
||||
disabled={slide.processing}
|
||||
className="w-full text-red-600 hover:text-red-700 hover:bg-red-50"
|
||||
>
|
||||
🔄 Retry Conversion
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<p className="text-base text-gray-500">
|
||||
⏳ Waiting in queue to process...
|
||||
</p>
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-gray-200 rounded w-2/3"></div>
|
||||
<div className="h-6 bg-gray-200 rounded w-1/2"></div>
|
||||
<div className="h-64 bg-gray-200 rounded"></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="p-4 pt-0 flex gap-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
const newWindow = window.open("", "_blank");
|
||||
|
|
@ -153,7 +648,6 @@ const EachSlide = ({
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Slide ${slide.slide_number} - HTML Preview</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="slide-container">
|
||||
|
|
@ -168,28 +662,21 @@ const EachSlide = ({
|
|||
>
|
||||
Open in new tab
|
||||
</Button>
|
||||
<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
|
||||
onClick={handleEditClick}
|
||||
className={`p-2 group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md `}
|
||||
>
|
||||
<Edit className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Drawing Canvas Modal */}
|
||||
{showDrawingCanvas && slideContentRef.current && (
|
||||
<DrawingCanvas
|
||||
slideElement={slideContentRef.current}
|
||||
onClose={handleCloseDrawingCanvas}
|
||||
slideNumber={slide.slide_number}
|
||||
onSlideUpdate={handleSlideUpdate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{slide.processed && slide.html && !isEditMode && (
|
||||
<div className="absolute top-2 z-20 sm:top-4 hidden md:block right-2 transition-transform">
|
||||
<ToolTip content="Edit slide">
|
||||
<div
|
||||
onClick={handleEditClick}
|
||||
className={`px-4 py-2 group-hover:scale-105 rounded-lg bg-[#5141e5] hover:shadow-md transition-all duration-300 cursor-pointer shadow-md `}
|
||||
>
|
||||
<Edit className="w-4 sm:w-5 h-4 sm:h-5 text-white" />
|
||||
</div>
|
||||
</ToolTip>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
13
servers/nextjs/app/custom-layout/components/SlideContent.tsx
Normal file
13
servers/nextjs/app/custom-layout/components/SlideContent.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React, { memo } from "react";
|
||||
|
||||
const SlideContent = memo(({ slide }: { slide: any }) => {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: slide.html,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default SlideContent;
|
||||
|
|
@ -128,19 +128,12 @@ const CustomLayoutPage = () => {
|
|||
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/"
|
||||
}`,
|
||||
});
|
||||
toast.success(`Layout saved successfully`);
|
||||
} catch (error) {
|
||||
console.error("Error saving layout:", error);
|
||||
toast.error("Failed to save layout", {
|
||||
|
|
@ -200,23 +193,34 @@ 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,
|
||||
}),
|
||||
});
|
||||
// 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 = await ApiResponseHandler.handleResponse(
|
||||
htmlResponse,
|
||||
`Failed to convert slide ${slide.slide_number} to HTML`
|
||||
);
|
||||
// const htmlData = await ApiResponseHandler.handleResponse(
|
||||
// htmlResponse,
|
||||
// `Failed to convert slide ${slide.slide_number} to HTML`
|
||||
// );
|
||||
|
||||
console.log(`Successfully processed slide ${slide.slide_number}`);
|
||||
// console.log(`Successfully processed slide ${slide.slide_number}`);
|
||||
|
||||
let data: any;
|
||||
if (slide.slide_number === 1) {
|
||||
data = firstSlide;
|
||||
} else if (slide.slide_number === 2) {
|
||||
data = slide2;
|
||||
} else if (slide.slide_number === 3) {
|
||||
data = slide3;
|
||||
} else if (slide.slide_number === 4) {
|
||||
data = slide4;
|
||||
}
|
||||
|
||||
// Update slide with success
|
||||
setSlides((prev) => {
|
||||
|
|
@ -226,7 +230,7 @@ const CustomLayoutPage = () => {
|
|||
...s,
|
||||
processing: false,
|
||||
processed: true,
|
||||
html: htmlData.html,
|
||||
html: data.html,
|
||||
}
|
||||
: s
|
||||
);
|
||||
|
|
@ -301,19 +305,20 @@ 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 pptxData = await ApiResponseHandler.handleResponse(
|
||||
pptxResponse,
|
||||
"Failed to process PPTX file"
|
||||
);
|
||||
// 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"
|
||||
// );
|
||||
|
||||
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");
|
||||
// }
|
||||
|
||||
const pptxData = processData;
|
||||
// Initialize slides with skeleton state
|
||||
const initialSlides: ProcessedSlide[] = pptxData.slides.map(
|
||||
(slide: any) => ({
|
||||
|
|
|
|||
1119
servers/nextjs/package-lock.json
generated
1119
servers/nextjs/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -37,9 +37,11 @@
|
|||
"@tiptap/extension-underline": "^2.0.0",
|
||||
"@tiptap/react": "^2.11.5",
|
||||
"@tiptap/starter-kit": "^2.11.5",
|
||||
"@types/fabric": "^5.3.10",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.0.0",
|
||||
"fabric": "^6.7.1",
|
||||
"html2canvas": "^1.4.1",
|
||||
"jsonrepair": "^3.12.0",
|
||||
"lucide-react": "^0.447.0",
|
||||
|
|
|
|||
1
servers/nextjs/tsconfig.tsbuildinfo
Normal file
1
servers/nextjs/tsconfig.tsbuildinfo
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue