feat: add undo/redo buttons & re-generate in presentation page
This commit is contained in:
parent
cd6fbff1bd
commit
9765749a83
8 changed files with 112 additions and 12 deletions
|
|
@ -1,5 +1,5 @@
|
|||
from typing import List
|
||||
from fastapi import APIRouter, Depends, File, UploadFile
|
||||
from fastapi import APIRouter, Depends, File, UploadFile, HTTPException
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlmodel import select
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ async def get_generated_images(sql_session: AsyncSession = Depends(get_async_ses
|
|||
)
|
||||
return images
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to retrieve generated images: {str(e)}"}
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve generated images: {str(e)}")
|
||||
|
||||
@IMAGES_ROUTER.post("/upload-image")
|
||||
async def upload_image(file: UploadFile = File(...), sql_session: AsyncSession = Depends(get_async_session)):
|
||||
|
|
@ -57,7 +57,7 @@ async def upload_image(file: UploadFile = File(...), sql_session: AsyncSession =
|
|||
"id": str(image_asset.id),
|
||||
}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to upload image: {str(e)}"}
|
||||
raise HTTPException(status_code=500, detail=f"Failed to upload image: {str(e)}")
|
||||
|
||||
@IMAGES_ROUTER.get("/uploaded", response_model=List[ImageAsset])
|
||||
async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_session)):
|
||||
|
|
@ -67,7 +67,7 @@ async def get_uploaded_images(sql_session: AsyncSession = Depends(get_async_sess
|
|||
)
|
||||
return images
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to retrieve uploaded images: {str(e)}"}
|
||||
raise HTTPException(status_code=500, detail=f"Failed to retrieve uploaded images: {str(e)}")
|
||||
|
||||
|
||||
@IMAGES_ROUTER.delete("/uploaded-image/{image_id}")
|
||||
|
|
@ -76,13 +76,13 @@ async def delete_image(image_id: uuid.UUID, sql_session: AsyncSession = Depends(
|
|||
# Fetch the asset to get its actual file path
|
||||
image = await sql_session.get(ImageAsset, image_id)
|
||||
if not image:
|
||||
return {"error": "Image not found"}
|
||||
raise HTTPException(status_code=404, detail="Image not found")
|
||||
|
||||
service = ImageUploadService(get_uploads_directory())
|
||||
await service.delete_image(image.path)
|
||||
|
||||
await sql_session.delete(image)
|
||||
await sql_session.commit()
|
||||
return {"success": True, "message": "Image deleted successfully"}
|
||||
return {"message": "Image deleted successfully"}
|
||||
except Exception as e:
|
||||
return {"error": f"Failed to delete image: {str(e)}"}
|
||||
raise HTTPException(status_code=500, detail=f"Failed to delete image: {str(e)}")
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ async def delete_presentation(
|
|||
if not presentation:
|
||||
raise HTTPException(404, "Presentation not found")
|
||||
|
||||
await sql_session.execute(delete(SlideModel).where(SlideModel.presentation == id))
|
||||
await sql_session.delete(presentation)
|
||||
await sql_session.commit()
|
||||
|
||||
|
|
@ -205,6 +204,8 @@ async def stream_presentation(
|
|||
status_code=400,
|
||||
detail="Outlines can not be empty",
|
||||
)
|
||||
await sql_session.execute(delete(SlideModel).where(SlideModel.presentation == presentation_id))
|
||||
await sql_session.commit()
|
||||
|
||||
image_generation_service = ImageGenerationService(get_images_directory())
|
||||
|
||||
|
|
|
|||
|
|
@ -471,7 +471,7 @@ const ImageEditor = ({
|
|||
</div>
|
||||
) : (
|
||||
uploadedImages.map((image) => (
|
||||
<div>
|
||||
<div key={image.id}>
|
||||
<div
|
||||
onClick={() =>
|
||||
handleImageChange(image.path)
|
||||
|
|
|
|||
|
|
@ -89,9 +89,11 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
|
|||
tiptapContainer.className = Array.from(allClasses).join(" ");
|
||||
|
||||
// Replace the element
|
||||
htmlElement.parentNode?.replaceChild(tiptapContainer, htmlElement);
|
||||
if(htmlElement.parentNode) {
|
||||
htmlElement.parentNode.replaceChild(tiptapContainer, htmlElement);
|
||||
// Mark as processed
|
||||
htmlElement.innerHTML = "";
|
||||
}
|
||||
setProcessedElements((prev) => new Set(prev).add(htmlElement));
|
||||
// Render TiptapText
|
||||
const root = ReactDOM.createRoot(tiptapContainer);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ import {
|
|||
SquareArrowOutUpRight,
|
||||
Play,
|
||||
Loader2,
|
||||
Redo2 ,
|
||||
Undo2,
|
||||
RefreshCcw,
|
||||
} from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
|
|
@ -15,7 +18,7 @@ import {
|
|||
} from "@/components/ui/popover";
|
||||
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
|
||||
import { OverlayLoader } from "@/components/ui/overlay-loader";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
|
|
@ -30,6 +33,10 @@ import PDFIMAGE from "@/public/pdf.svg";
|
|||
import PPTXIMAGE from "@/public/pptx.svg";
|
||||
import Image from "next/image";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo";
|
||||
import ToolTip from "@/components/ToolTip";
|
||||
import { clearPresentationData } from "@/store/slices/presentationGeneration";
|
||||
import { clearHistory } from "@/store/slices/undoRedoSlice";
|
||||
|
||||
const Header = ({
|
||||
presentation_id,
|
||||
|
|
@ -42,12 +49,15 @@ const Header = ({
|
|||
const [showLoader, setShowLoader] = useState(false);
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
|
||||
const { presentationData, isStreaming } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
|
||||
const { onUndo, onRedo, canUndo, canRedo } = usePresentationUndoRedo();
|
||||
|
||||
const get_presentation_pptx_model = async (id: string): Promise<PptxPresentationModel> => {
|
||||
const response = await fetch(`/api/presentation_to_pptx_model?id=${id}`);
|
||||
const pptx_model = await response.json();
|
||||
|
|
@ -127,6 +137,12 @@ const Header = ({
|
|||
setShowLoader(false);
|
||||
}
|
||||
};
|
||||
const handleReGenerate = () => {
|
||||
dispatch(clearPresentationData());
|
||||
dispatch(clearHistory())
|
||||
trackEvent(MixpanelEvent.Header_ReGenerate_Button_Clicked, { pathname });
|
||||
router.push(`/presentation?id=${presentation_id}&stream=true`);
|
||||
};
|
||||
const downloadLink = (path: string) => {
|
||||
// if we have popup access give direct download if not redirect to the path
|
||||
if (window.opener) {
|
||||
|
|
@ -170,6 +186,33 @@ const Header = ({
|
|||
|
||||
const MenuItems = ({ mobile }: { mobile: boolean }) => (
|
||||
<div className="flex flex-col lg:flex-row items-center gap-4">
|
||||
{/* undo redo */}
|
||||
<button onClick={handleReGenerate} disabled={isStreaming || !presentationData} className="text-white disabled:opacity-50" >
|
||||
|
||||
Re-Generate
|
||||
</button>
|
||||
<div className="flex items-center gap-2 ">
|
||||
<ToolTip content="Undo">
|
||||
<button disabled={!canUndo} className="text-white disabled:opacity-50" onClick={() => {
|
||||
onUndo();
|
||||
}}>
|
||||
|
||||
<Undo2 className="w-6 h-6 " />
|
||||
|
||||
</button>
|
||||
</ToolTip>
|
||||
<ToolTip content="Redo">
|
||||
|
||||
<button disabled={!canRedo} className="text-white disabled:opacity-50" onClick={() => {
|
||||
onRedo();
|
||||
}}>
|
||||
<Redo2 className="w-6 h-6 " />
|
||||
|
||||
</button>
|
||||
</ToolTip>
|
||||
|
||||
</div>
|
||||
|
||||
{/* Present Button */}
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useCallback } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import { finishUndoRedo, redo, undo } from "@/store/slices/undoRedoSlice";
|
||||
|
|
@ -13,6 +14,56 @@ export const usePresentationUndoRedo = () => {
|
|||
const undoRedoState = useSelector((state: RootState) => state.undoRedo);
|
||||
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
|
||||
|
||||
const canUndo = undoRedoState.past.length > 0;
|
||||
const canRedo = undoRedoState.future.length > 0;
|
||||
console.log(canUndo, canRedo);
|
||||
|
||||
const onUndo = useCallback(() => {
|
||||
if (!canUndo) return;
|
||||
|
||||
const previousState = undoRedoState.past[undoRedoState.past.length - 1];
|
||||
|
||||
dispatch(undo());
|
||||
|
||||
if (previousState) {
|
||||
const newSlides = JSON.parse(JSON.stringify(previousState.slides));
|
||||
|
||||
dispatch(
|
||||
setPresentationData({
|
||||
...presentationData!,
|
||||
slides: newSlides,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(finishUndoRedo());
|
||||
}, 100);
|
||||
}, [canUndo, dispatch, presentationData, undoRedoState.past]);
|
||||
|
||||
const onRedo = useCallback(() => {
|
||||
if (!canRedo) return;
|
||||
|
||||
const nextState = undoRedoState.future[0];
|
||||
|
||||
dispatch(redo());
|
||||
|
||||
if (nextState) {
|
||||
const newSlides = JSON.parse(JSON.stringify(nextState.slides));
|
||||
|
||||
dispatch(
|
||||
setPresentationData({
|
||||
...presentationData!,
|
||||
slides: newSlides,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dispatch(finishUndoRedo());
|
||||
}, 100);
|
||||
}, [canRedo, dispatch, presentationData, undoRedoState.future]);
|
||||
|
||||
// Handle undo
|
||||
useKeyboardShortcut(
|
||||
["z"],
|
||||
|
|
@ -81,4 +132,6 @@ export const usePresentationUndoRedo = () => {
|
|||
},
|
||||
[undoRedoState.future, presentationData]
|
||||
);
|
||||
|
||||
return { onUndo, onRedo, canUndo, canRedo };
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useEffect, useRef } from "react";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
import { useDispatch } from "react-redux";
|
||||
import {
|
||||
clearPresentationData,
|
||||
setPresentationData,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ export enum MixpanelEvent {
|
|||
ImageEditor_GetPreviousGeneratedImages_API_Call = 'Image Editor Get Previous Generated Images API Call',
|
||||
ImageEditor_GenerateImage_API_Call = 'Image Editor Generate Image API Call',
|
||||
ImageEditor_UploadImage_API_Call = 'Image Editor Upload Image API Call',
|
||||
Header_ReGenerate_Button_Clicked = 'Header ReGenerate Button Clicked',
|
||||
}
|
||||
|
||||
export type MixpanelProps = Record<string, unknown>;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue