diff --git a/servers/fastapi/api/v1/ppt/endpoints/slide.py b/servers/fastapi/api/v1/ppt/endpoints/slide.py index 3a5b4230..a6f9ee9a 100644 --- a/servers/fastapi/api/v1/ppt/endpoints/slide.py +++ b/servers/fastapi/api/v1/ppt/endpoints/slide.py @@ -7,13 +7,17 @@ from services.database import get_sql_session from utils.llm_calls.edit_slide import get_edited_slide_content from utils.llm_calls.select_slide_type_on_edit import get_slide_layout_from_prompt from utils.process_slides import process_old_and_new_slides_and_fetch_assets +from utils.randomizers import get_random_uuid SLIDE_ROUTER = APIRouter(prefix="/slide", tags=["Slide"]) @SLIDE_ROUTER.post("/edit") -async def edit_slide(id: Annotated[str, Body()], prompt: Annotated[str, Body()]): +async def edit_slide( + id: Annotated[str, Body()], + prompt: Annotated[str, Body()] +): with get_sql_session() as sql_session: slide = sql_session.get(SlideModel, id) @@ -37,6 +41,9 @@ async def edit_slide(id: Annotated[str, Body()], prompt: Annotated[str, Body()]) slide.content, edited_slide_content ) + # Always assign a new unique id to the slide + slide.id = get_random_uuid() + with get_sql_session() as sql_session: sql_session.add(slide) slide.content = edited_slide_content diff --git a/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx b/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx index 9485386d..1c5ddc74 100644 --- a/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/EditableLayoutWrapper.tsx @@ -26,7 +26,6 @@ const EditableLayoutWrapper: React.FC = ({ children, slideIndex, slideData, - isEditMode = true, }) => { const dispatch = useDispatch(); const containerRef = useRef(null); @@ -109,8 +108,6 @@ const EditableLayoutWrapper: React.FC = ({ * Checks if two URLs match using various comparison strategies */ const isMatchingUrl = (url1: string, url2: string): boolean => { - console.log('url1', url1); - console.log('url2', url2); if (!url1 || !url2) return false; // Direct match @@ -154,7 +151,7 @@ const EditableLayoutWrapper: React.FC = ({ * Finds and processes images in the DOM, making them editable */ const findAndProcessImages = () => { - if (!containerRef.current || !isEditMode) return; + if (!containerRef.current) return; const imgElements = containerRef.current.querySelectorAll('img:not([data-editable-processed])'); const newEditableElements: EditableElement[] = []; diff --git a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx index 9eceeb34..ba3093b7 100644 --- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx @@ -16,7 +16,8 @@ import { import { cn } from "@/lib/utils"; import { PresentationGenerationApi } from "../services/api/presentation-generation"; import { Skeleton } from "@/components/ui/skeleton"; - +import { toast } from "sonner"; +import { PreviousGeneratedImagesResponse } from "../services/api/params"; interface ImageEditorProps { initialImage: string | null; imageIdx?: number; @@ -32,7 +33,6 @@ interface ImageEditorProps { const ImageEditor = ({ initialImage, imageIdx = 0, - slideIndex, promptContent, properties, onClose, @@ -41,6 +41,7 @@ const ImageEditor = ({ }: ImageEditorProps) => { // State management const [previewImages, setPreviewImages] = useState(initialImage); + const [previousGeneratedImages, setPreviousGeneratedImages] = useState([]); const [prompt, setPrompt] = useState(""); const [isGenerating, setIsGenerating] = useState(false); const [error, setError] = useState(null); @@ -65,7 +66,6 @@ const ImageEditor = ({ properties[imageIdx].initialObjectFit) || "cover" ); - console.log("previewImages", previewImages); // Refs const imageRef = useRef(null); @@ -77,6 +77,13 @@ const ImageEditor = ({ setPreviewImages(initialImage); }, [initialImage]); + + useEffect(() => { + if (isOpen && !previousGeneratedImages.length) { + getPreviousGeneratedImage(); + } + }, [isOpen, previousGeneratedImages]); + // Handle close with animation const handleClose = () => { setIsOpen(false); @@ -86,6 +93,18 @@ const ImageEditor = ({ }, 300); // Match the Sheet animation duration }; + + const getPreviousGeneratedImage = async () => { + try { + const response = await PresentationGenerationApi.getPreviousGeneratedImages(); + setPreviousGeneratedImages(response); + } catch (error) { + toast.error("Failed to get previous generated images. Please try again."); + console.error("error in getting previous generated images", error); + setError("Failed to get previous generated images. Please try again."); + } + } + // Close toolbar when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { @@ -331,6 +350,18 @@ const ImageEditor = ({ } + {previousGeneratedImages.length > 0 && ( +
+

Previous Generated Images

+
+ {previousGeneratedImages.map((image) => ( +
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" > + {image.extras.prompt} +
+ ))} +
+
+ )} diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx index 8b901739..d7958481 100644 --- a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx @@ -19,7 +19,6 @@ interface TiptapTextProps { className?: string; placeholder?: string; tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div'; - disabled?: boolean; } const TiptapText: React.FC = ({ @@ -28,7 +27,6 @@ const TiptapText: React.FC = ({ className = "", placeholder = "Enter text...", tag = 'div', - disabled = false }) => { const editor = useEditor({ extensions: [StarterKit, Markdown, Underline], @@ -45,7 +43,7 @@ const TiptapText: React.FC = ({ onContentChange(markdown); } }, - editable: !disabled, + editable: true, immediatelyRender: false, }); @@ -62,56 +60,54 @@ const TiptapText: React.FC = ({ return (
- {!disabled && ( - -
- - - - - -
-
- )} + +
+ + + + + +
+
void; - isEditMode?: boolean; } const TiptapTextReplacer: React.FC = ({ @@ -17,13 +16,12 @@ const TiptapTextReplacer: React.FC = ({ slideData, slideIndex, onContentChange = () => { }, - isEditMode = true }) => { const containerRef = useRef(null); const [processedElements, setProcessedElements] = useState(new Set()); useEffect(() => { - if (!isEditMode || !containerRef.current) return; + if (!containerRef.current) return; const container = containerRef.current; @@ -49,7 +47,7 @@ const TiptapTextReplacer: React.FC = ({ const trimmedText = directTextContent.trim(); // Check if element has meaningful text content - if (!trimmedText || trimmedText.length < 2) return; + if (!trimmedText || trimmedText.length <= 2) return; // Skip elements that contain other elements with text (to avoid double processing) if (hasTextChildren(htmlElement)) return; @@ -93,30 +91,29 @@ const TiptapTextReplacer: React.FC = ({ ); } }); - // Replace the element htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement); - // Mark as processed setProcessedElements(prev => new Set(prev).add(htmlElement)); - // Render TiptapText const root = ReactDOM.createRoot(tiptapContainer); root.render( { if (dataPath && onContentChange) { - onContentChange(content, dataPath, slideIndex); + onContentChange(content, dataPath.path, slideIndex); } }} placeholder="Enter text..." - disabled={!isEditMode} /> ); }); }; + + // Function to check if element is inside an ignored element tree const isInIgnoredElementTree = (element: HTMLElement): boolean => { // List of element types that should be ignored entirely with all their children @@ -224,28 +221,30 @@ const TiptapTextReplacer: React.FC = ({ }; // Helper function to find data path for text content - const findDataPath = (data: any, targetText: string, path = ''): string => { - if (!data || typeof data !== 'object') return ''; + const findDataPath = (data: any, targetText: string, path = ''): { + path: string; + originalText: string; + } => { + if (!data || typeof data !== 'object') return { path: '', originalText: '' }; for (const [key, value] of Object.entries(data)) { const currentPath = path ? `${path}.${key}` : key; if (typeof value === 'string' && value.trim() === targetText.trim()) { - return currentPath; + return { path: currentPath, originalText: value }; } if (Array.isArray(value)) { for (let i = 0; i < value.length; i++) { const result = findDataPath(value[i], targetText, `${currentPath}[${i}]`); - if (result) return result; + if (result.path) return result; } } else if (typeof value === 'object' && value !== null) { const result = findDataPath(value, targetText, currentPath); - if (result) return result; + if (result.path) return result; } } - - return ''; + return { path: '', originalText: '' }; }; // Replace text elements after a short delay to ensure DOM is ready @@ -254,10 +253,10 @@ const TiptapTextReplacer: React.FC = ({ return () => { clearTimeout(timer); }; - }, [slideData, isEditMode, slideIndex]); + }, [slideData, slideIndex]); return ( -
+
{children}
); diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx index dbcd5af8..e7450a14 100644 --- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx @@ -52,13 +52,13 @@ export const useGroupLayouts = () => { { // Dispatch Redux action to update slide content if (dataPath && slideIndex !== undefined) { @@ -70,12 +70,12 @@ export const useGroupLayouts = () => { } }} > - + ); } - return ; + return ; }; }, [getGroupLayout, dispatch]); diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx index 02a48c6f..0d7ed721 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -41,7 +41,7 @@ const OutlineContent: React.FC = ({ ); return ( -
+
Presentation Outline @@ -53,11 +53,10 @@ const OutlineContent: React.FC = ({
)}
- {/* Skeleton loading state */} {isLoading && (
- {[...Array(5)].map((_, index) => ( + {[...Array(6)].map((_, index) => (
diff --git a/servers/nextjs/app/(presentation-generator)/services/api/params.ts b/servers/nextjs/app/(presentation-generator)/services/api/params.ts index 8e7003de..bc50ef8b 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/params.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/params.ts @@ -17,3 +17,14 @@ export interface IconSearch { limit: number; } + +export interface PreviousGeneratedImagesResponse { + + extras: { + prompt: string; + theme_prompt: string | null; + }, + created_at: string; + id: string; + path: string; +} \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts index f3e3bcf4..9d567e08 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts @@ -1,6 +1,5 @@ import { getHeader, getHeaderForFormData } from "./header"; -import { IconSearch, ImageGenerate, ImageSearch } from "./params"; - +import { IconSearch, ImageGenerate, ImageSearch, PreviousGeneratedImagesResponse } from "./params"; export class PresentationGenerationApi { @@ -70,6 +69,7 @@ export class PresentationGenerationApi { prompt: string ) { try { + const response = await fetch( `/api/v1/ppt/slide/edit`, { @@ -78,6 +78,7 @@ export class PresentationGenerationApi { body: JSON.stringify({ id: slide_id, prompt, + }), cache: "no-cache", } @@ -189,6 +190,27 @@ export class PresentationGenerationApi { throw error; } } + + static getPreviousGeneratedImages = async():Promise=>{ + try { + const response = await fetch( + `/api/v1/ppt/images/generated`, + { + method: "GET", + headers: getHeader(), + } + ); + if (response.ok) { + const data = await response.json(); + return data; + } else { + throw new Error(`Failed to get previous generated images: ${response.statusText}`); + } + } catch (error) { + console.error("error in getting previous generated images", error); + throw error; + } + } static async searchIcons(iconSearch: IconSearch) { try { const response = await fetch( diff --git a/servers/nextjs/app/dashboard/api/dashboard.ts b/servers/nextjs/app/dashboard/api/dashboard.ts index 498d136d..9e04d77f 100644 --- a/servers/nextjs/app/dashboard/api/dashboard.ts +++ b/servers/nextjs/app/dashboard/api/dashboard.ts @@ -8,6 +8,7 @@ export interface PresentationResponse { id: string; title: string; created_at: string; + updated_at: string; data: any | null; file: string; n_slides: number; diff --git a/servers/nextjs/app/dashboard/components/DashboardPage.tsx b/servers/nextjs/app/dashboard/components/DashboardPage.tsx index 0546522f..645f3e8f 100644 --- a/servers/nextjs/app/dashboard/components/DashboardPage.tsx +++ b/servers/nextjs/app/dashboard/components/DashboardPage.tsx @@ -26,6 +26,7 @@ const DashboardPage: React.FC = () => { setIsLoading(true); setError(null); const data = await DashboardApi.getPresentations(); + data.sort((a: any, b: any) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()); setPresentations(data); } catch (err) { setError(null);