Merge branch 'pdf-pptx-layout' of https://github.com/presenton/presenton into pdf-pptx-layout
merge
This commit is contained in:
commit
339a378e7f
6 changed files with 239 additions and 190 deletions
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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...",
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue