Merge branch 'pdf-pptx-layout' of https://github.com/presenton/presenton into pdf-pptx-layout

merge
This commit is contained in:
Suraj Jha 2025-08-04 11:59:25 +05:45
commit 339a378e7f
6 changed files with 239 additions and 190 deletions

View file

@ -9,10 +9,7 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import {
Wand2,
Upload,
} from "lucide-react";
import { Wand2, Upload } from "lucide-react";
import { cn } from "@/lib/utils";
import { PresentationGenerationApi } from "../services/api/presentation-generation";
import { Skeleton } from "@/components/ui/skeleton";
@ -38,11 +35,12 @@ const ImageEditor = ({
onClose,
onFocusPointClick,
onImageChange,
}: ImageEditorProps) => {
// State management
const [previewImages, setPreviewImages] = useState(initialImage);
const [previousGeneratedImages, setPreviousGeneratedImages] = useState<PreviousGeneratedImagesResponse[]>([]);
const [previousGeneratedImages, setPreviousGeneratedImages] = useState<
PreviousGeneratedImagesResponse[]
>([]);
const [prompt, setPrompt] = useState<string>("");
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState<string | null>(null);
@ -65,7 +63,7 @@ const ImageEditor = ({
(properties &&
properties[imageIdx] &&
properties[imageIdx].initialObjectFit) ||
"cover"
"cover"
);
// Refs
@ -75,7 +73,6 @@ const ImageEditor = ({
setPreviewImages(initialImage);
}, [initialImage]);
useEffect(() => {
if (isOpen && !previousGeneratedImages.length) {
getPreviousGeneratedImage();
@ -91,26 +88,25 @@ const ImageEditor = ({
}, 300); // Match the Sheet animation duration
};
const getPreviousGeneratedImage = async () => {
try {
const response = await PresentationGenerationApi.getPreviousGeneratedImages();
const response =
await PresentationGenerationApi.getPreviousGeneratedImages();
setPreviousGeneratedImages(response);
} catch (error: any) {
toast.error("Failed to get previous generated images. Please try again.");
console.error("error in getting previous generated images", error);
setError(error.message || "Failed to get previous generated images. Please try again.");
setError(
error.message ||
"Failed to get previous generated images. Please try again."
);
}
}
};
/**
* Handles image selection and calls the parent callback
*/
const handleImageChange = (newImage: string) => {
if (onImageChange) {
onImageChange(newImage, promptContent);
setPreviewImages(newImage);
@ -230,17 +226,17 @@ const ImageEditor = ({
setUploadError(null);
const formData = new FormData();
formData.append('file', file);
formData.append("file", file);
const response = await fetch('/api/upload-image', {
method: 'POST',
const response = await fetch("/api/upload-image", {
method: "POST",
body: formData,
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || 'Upload failed');
throw new Error(result.error || "Upload failed");
}
setUploadedImageUrl(result.filePath);
@ -254,8 +250,6 @@ const ImageEditor = ({
return (
<div className="image-editor-container">
<Sheet open={isOpen} onOpenChange={() => handleClose()}>
<SheetContent
side="right"
@ -276,7 +270,9 @@ const ImageEditor = ({
<TabsTrigger className="font-medium" value="upload">
Upload
</TabsTrigger>
<TabsTrigger className="font-medium" value="edit">Edit</TabsTrigger>
<TabsTrigger className="font-medium" value="edit">
Edit
</TabsTrigger>
</TabsList>
{/* Generate Tab */}
<TabsContent value="generate" className="mt-4 space-y-4">
@ -287,7 +283,9 @@ const ImageEditor = ({
</div>
<div>
<h3 className="text-base font-medium mb-2">Image Description</h3>
<h3 className="text-base font-medium mb-2">
Image Description
</h3>
<Textarea
placeholder="Describe the image you want to generate..."
value={prompt}
@ -308,14 +306,15 @@ const ImageEditor = ({
{error && <p className="text-red-500 text-sm">{error}</p>}
<div className="grid grid-cols-2 gap-4">
{isGenerating || !previewImages
? Array.from({ length: 4 }).map((_, index) => (
{isGenerating || !previewImages ? (
Array.from({ length: 4 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-[4/3] w-full rounded-lg"
/>
))
: <div
) : (
<div
onClick={() => handleImageChange(previewImages)}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors"
>
@ -327,15 +326,25 @@ const ImageEditor = ({
/>
)}
</div>
}
)}
</div>
{previousGeneratedImages.length > 0 && (
<div className="mt-4">
<h3 className="text-sm font-medium mb-2">Previous Generated Images</h3>
<div className="grid grid-cols-2 gap-4">
<h3 className="text-sm font-medium mb-2">
Previous Generated Images
</h3>
<div className="grid grid-cols-2 gap-4 h-[400px] overflow-y-auto hide-scrollbar">
{previousGeneratedImages.map((image) => (
<div onClick={() => handleImageChange(image.path)} key={image.id} className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors" >
<img src={image.path} alt={image.extras.prompt} className="w-full h-full object-cover" />
<div
onClick={() => handleImageChange(image.path)}
key={image.id}
className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer hover:border-blue-500 transition-colors"
>
<img
src={image.path}
alt={image.extras.prompt}
className="w-full h-full object-cover"
/>
</div>
))}
</div>
@ -437,56 +446,66 @@ const ImageEditor = ({
<TabsContent value="edit" className="mt-4 space-y-4">
<div className="space-y-4">
<h3 className="text-sm font-medium mb-2">Current Image</h3>
<div onClick={(e) => {
if (isFocusPointMode) {
handleFocusPointClick(e);
} else {
}
}}
className="aspect-[4/3] group rounded-lg overflow-hidden relative border border-gray-200">
<p className="group-hover:opacity-100 opacity-0 transition-opacity absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm text-center font-medium bg-black/50 text-white px-2 py-1 rounded">Click to Change Focus Point</p>
{previewImages && <img ref={imageRef} onClick={
() => {
setIsFocusPointMode(true);
<div
onClick={(e) => {
if (isFocusPointMode) {
handleFocusPointClick(e);
} else {
}
} src={previewImages} style={{ objectFit: objectFit, objectPosition: `${focusPoint.x}% ${focusPoint.y}%`, }} alt={`Preview`} className="w-full h-full " />}
{isFocusPointMode && <div className="absolute inset-0 bg-black/20 flex items-center justify-center">
<div className="text-white text-center p-2 bg-black/50 rounded">
<p className="text-sm font-medium pointer-events-none">
Click anywhere to set focus point
</p>
<button
className="mt-2 px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
onClick={(e) => {
e.stopPropagation();
toggleFocusPointMode();
}}
className="aspect-[4/3] group rounded-lg overflow-hidden relative border border-gray-200"
>
<p className="group-hover:opacity-100 opacity-0 transition-opacity absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 text-sm text-center font-medium bg-black/50 text-white px-2 py-1 rounded">
Click to Change Focus Point
</p>
{previewImages && (
<img
ref={imageRef}
onClick={() => {
setIsFocusPointMode(true);
}}
src={previewImages}
style={{
objectFit: objectFit,
objectPosition: `${focusPoint.x}% ${focusPoint.y}%`,
}}
alt={`Preview`}
className="w-full h-full "
/>
)}
{isFocusPointMode && (
<div className="absolute inset-0 bg-black/20 flex items-center justify-center">
<div className="text-white text-center p-2 bg-black/50 rounded">
<p className="text-sm font-medium pointer-events-none">
Click anywhere to set focus point
</p>
<button
className="mt-2 px-3 py-1 bg-blue-500 text-white text-xs rounded hover:bg-blue-600"
onClick={(e) => {
e.stopPropagation();
toggleFocusPointMode();
}}
>
Done
</button>
</div>
<div
className="absolute w-8 h-8 border-2 border-white rounded-full transform -translate-x-1/2 -translate-y-1/2 pointer-events-none"
style={{
left: `${focusPoint.x}%`,
top: `${focusPoint.y}%`,
boxShadow: "0 0 0 2px rgba(0,0,0,0.5)",
}}
>
Done
</button>
</div>
<div
className="absolute w-8 h-8 border-2 border-white rounded-full transform -translate-x-1/2 -translate-y-1/2 pointer-events-none"
style={{
left: `${focusPoint.x}%`,
top: `${focusPoint.y}%`,
boxShadow: "0 0 0 2px rgba(0,0,0,0.5)",
}}
>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
<div className="absolute inset-0 flex items-center justify-center">
<div className="w-2 h-2 bg-white rounded-full"></div>
</div>
<div className="absolute w-16 h-0.5 bg-white/70 left-1/2 -translate-x-1/2"></div>
<div className="absolute w-0.5 h-16 bg-white/70 top-1/2 -translate-y-1/2"></div>
</div>
<div className="absolute w-16 h-0.5 bg-white/70 left-1/2 -translate-x-1/2"></div>
<div className="absolute w-0.5 h-16 bg-white/70 top-1/2 -translate-y-1/2"></div>
</div>
</div>}
)}
</div>
{/* Edit Image */}
{/* Object Fit */}
@ -494,17 +513,40 @@ const ImageEditor = ({
<div>
<h3 className="text-sm font-medium mb-2">Object Fit</h3>
<div className="flex gap-4">
<Button variant="outline" className={cn(objectFit === "cover" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("cover")}>Cover</Button>
<Button variant="outline" className={cn(objectFit === "contain" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("contain")}>Contain</Button>
<Button variant="outline" className={cn(objectFit === "fill" && "bg-blue-50 border-blue-500")} onClick={() => handleFitChange("fill")}>Fill</Button>
<Button
variant="outline"
className={cn(
objectFit === "cover" &&
"bg-blue-50 border-blue-500"
)}
onClick={() => handleFitChange("cover")}
>
Cover
</Button>
<Button
variant="outline"
className={cn(
objectFit === "contain" &&
"bg-blue-50 border-blue-500"
)}
onClick={() => handleFitChange("contain")}
>
Contain
</Button>
<Button
variant="outline"
className={cn(
objectFit === "fill" && "bg-blue-50 border-blue-500"
)}
onClick={() => handleFitChange("fill")}
>
Fill
</Button>
</div>
</div>
}
{/* Focus Point */}
{
}
{}
</div>
</TabsContent>
</Tabs>

View file

@ -18,11 +18,13 @@ interface TiptapTextProps {
onContentChange?: (content: string) => void;
className?: string;
placeholder?: string;
element?: HTMLElement;
tag?: "H1" | "H2" | "H3" | "H4" | "H5" | "H6" | "P" | "SPAN" | "DIV" | any;
}
const TiptapText: React.FC<TiptapTextProps> = ({
content,
element,
onContentChange,
className = "",
placeholder = "Enter text...",

View file

@ -76,12 +76,14 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
// Replace the element
htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement);
// Mark as processed
htmlElement.innerHTML = "";
setProcessedElements((prev) => new Set(prev).add(htmlElement));
// Render TiptapText
const root = ReactDOM.createRoot(tiptapContainer);
root.render(
<TiptapText
content={trimmedText}
element={htmlElement}
tag={htmlElement.tagName}
onContentChange={(content: string) => {
if (dataPath && onContentChange) {

View file

@ -4,115 +4,117 @@ import { useDispatch, useSelector } from "react-redux";
import { RootState } from "@/store/store";
import { Skeleton } from "@/components/ui/skeleton";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
import { useGroupLayouts } from "../hooks/useGroupLayouts";
import { setPresentationData } from "@/store/slices/presentationGeneration";
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const { renderSlideContent, loading } = useGroupLayouts();
const [contentLoading, setContentLoading] = useState(true);
const dispatch = useDispatch();
const { presentationData } = useSelector(
(state: RootState) => state.presentationGeneration
);
const [error, setError] = useState(false);
// Function to fetch the slides
useEffect(() => {
fetchUserSlides();
}, []);
const { renderSlideContent, loading } = useGroupLayouts();
const [contentLoading, setContentLoading] = useState(true);
const dispatch = useDispatch();
const { presentationData } = useSelector(
(state: RootState) => state.presentationGeneration
);
const [error, setError] = useState(false);
useEffect(() => {
if (presentationData?.slides[0].layout_group.includes("custom")) {
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);
}
}
}, [presentationData]);
// Function to fetch the slides
useEffect(() => {
fetchUserSlides();
}, []);
// Function to fetch the user slides
const fetchUserSlides = async () => {
try {
const data = await DashboardApi.getPresentation(presentation_id);
dispatch(setPresentationData(data));
setContentLoading(false);
} catch (error) {
setError(true);
toast.error("Failed to load presentation");
console.error("Error fetching user slides:", error);
setContentLoading(false);
}
};
// Regular view
return (
<div className="flex overflow-hidden flex-col">
{error ? (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<div
className="bg-white border border-red-300 text-red-700 px-6 py-8 rounded-lg shadow-lg flex flex-col items-center"
role="alert"
>
<AlertCircle className="w-16 h-16 mb-4 text-red-500" />
<strong className="font-bold text-4xl mb-2">Oops!</strong>
<p className="block text-2xl py-2">
We encountered an issue loading your presentation.
</p>
<p className="text-lg py-2">
Please check your internet connection or try again later.
</p>
<Button
className="mt-4 bg-red-500 text-white hover:bg-red-600 focus:ring-4 focus:ring-red-300"
onClick={() => window.location.reload()}
>
Retry
</Button>
</div>
</div>
) : (
<div className="">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
>
{!presentationData ||
loading ||
contentLoading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto ">
<div className=" ">
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-video bg-gray-400 my-4 w-full mx-auto max-w-[1280px]"
/>
))}
</div>
</div>
) : (
<>
{presentationData &&
presentationData.slides &&
presentationData.slides.length > 0 &&
presentationData.slides.map((slide: any, index: number) => (
<div key={index} className="w-full">
{renderSlideContent(slide, false)}
</div>
))}
</>
)}
</div>
</div>
)}
// Function to fetch the user slides
const fetchUserSlides = async () => {
try {
const data = await DashboardApi.getPresentation(presentation_id);
dispatch(setPresentationData(data));
setContentLoading(false);
} catch (error) {
setError(true);
toast.error("Failed to load presentation");
console.error("Error fetching user slides:", error);
setContentLoading(false);
}
};
// Regular view
return (
<div className="flex overflow-hidden flex-col">
{error ? (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<div
className="bg-white border border-red-300 text-red-700 px-6 py-8 rounded-lg shadow-lg flex flex-col items-center"
role="alert"
>
<AlertCircle className="w-16 h-16 mb-4 text-red-500" />
<strong className="font-bold text-4xl mb-2">Oops!</strong>
<p className="block text-2xl py-2">
We encountered an issue loading your presentation.
</p>
<p className="text-lg py-2">
Please check your internet connection or try again later.
</p>
<Button
className="mt-4 bg-red-500 text-white hover:bg-red-600 focus:ring-4 focus:ring-red-300"
onClick={() => window.location.reload()}
>
Retry
</Button>
</div>
</div>
);
) : (
<div className="">
<div
id="presentation-slides-wrapper"
className="mx-auto flex flex-col items-center overflow-hidden justify-center "
>
{!presentationData ||
loading ||
contentLoading ||
!presentationData?.slides ||
presentationData?.slides.length === 0 ? (
<div className="relative w-full h-[calc(100vh-120px)] mx-auto ">
<div className=" ">
{Array.from({ length: 2 }).map((_, index) => (
<Skeleton
key={index}
className="aspect-video bg-gray-400 my-4 w-full mx-auto max-w-[1280px]"
/>
))}
</div>
</div>
) : (
<>
{presentationData &&
presentationData.slides &&
presentationData.slides.length > 0 &&
presentationData.slides.map((slide: any, index: number) => (
<div key={index} className="w-full">
{renderSlideContent(slide, false)}
</div>
))}
</>
)}
</div>
</div>
)}
</div>
);
};
export default PresentationPage;

View file

@ -107,15 +107,16 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
if (isStreaming || loading) {
return;
}
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);
if (slide.layout_group.includes("custom")) {
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, isStreaming, loading]);