diff --git a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx index f57a39e4..e3f87b7f 100644 --- a/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/NewSlide.tsx @@ -1,74 +1,90 @@ -import { Trash2 } from 'lucide-react'; -import React from 'react' -import { useDispatch } from 'react-redux'; -import { addNewSlide } from '@/store/slices/presentationGeneration'; -import { Loader2 } from 'lucide-react'; -import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader'; -import { v4 as uuidv4 } from 'uuid'; -import { toast } from 'sonner'; +import { Trash2 } from "lucide-react"; +import React from "react"; +import { useDispatch } from "react-redux"; +import { addNewSlide } from "@/store/slices/presentationGeneration"; +import { Loader2 } from "lucide-react"; +// import { useGroupLayoutLoader } from '@/app/layout-preview/hooks/useGroupLayoutLoader'; +import { useLayout, FullDataInfo } from "../context/LayoutContext"; +import { v4 as uuidv4 } from "uuid"; +import { toast } from "sonner"; interface NewSlideProps { - setShowNewSlideSelection: (show: boolean) => void; - group: string; - index: number; - presentationId: string; + setShowNewSlideSelection: (show: boolean) => void; + group: string; + index: number; + presentationId: string; } -const NewSlide = ({ setShowNewSlideSelection, group, index, presentationId }: NewSlideProps) => { - const dispatch = useDispatch(); - const handleNewSlide = (sampleData: any, id: string) => { - try { - const newSlide = { - id: uuidv4(), - index: index, - content: sampleData, - layout_group: group, - layout: `${group}:${id}`, - presentation: presentationId - } - dispatch(addNewSlide({ slideData: newSlide, index })); - setShowNewSlideSelection(false); - } catch (error: any) { - console.error(error) - toast.error('Error adding new slide') - } - } - const { layoutGroup, loading } = useGroupLayoutLoader(group) - - if (loading) { - return ( -
-
-

Select a Slide Layout

- setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' /> -
-
- -
-
- ) +const NewSlide = ({ + setShowNewSlideSelection, + group, + index, + presentationId, +}: NewSlideProps) => { + const dispatch = useDispatch(); + const handleNewSlide = (sampleData: any, id: string) => { + try { + const newSlide = { + id: uuidv4(), + index: index, + content: sampleData, + layout_group: group, + layout: id, + presentation: presentationId, + }; + dispatch(addNewSlide({ slideData: newSlide, index })); + setShowNewSlideSelection(false); + } catch (error: any) { + console.error(error); + toast.error("Error adding new slide"); } + }; + const { getFullDataByGroup, loading } = useLayout(); + const fullData = getFullDataByGroup(group); + if (loading) { return ( -
-
- -

Select a Slide Layout

- setShowNewSlideSelection(false)} className='text-gray-500 text-2xl cursor-pointer' /> -
-
- {layoutGroup && layoutGroup?.layouts.map((layout: any, index: number) => { - const { component: LayoutComponent, sampleData, layoutId } = layout - return ( -
handleNewSlide(sampleData, layoutId)} key={`${layoutGroup?.groupName}-${index}`} className=" relative cursor-pointer overflow-hidden aspect-video"> -
-
- -
-
- ) - })} -
+
+
+

Select a Slide Layout

+ setShowNewSlideSelection(false)} + className="text-gray-500 text-2xl cursor-pointer" + />
- ) -} +
+ +
+
+ ); + } -export default NewSlide + return ( +
+
+

Select a Slide Layout

+ setShowNewSlideSelection(false)} + className="text-gray-500 text-2xl cursor-pointer" + /> +
+
+ {fullData.map((layout: FullDataInfo, index: number) => { + const { component: LayoutComponent, sampleData, layoutId } = layout; + return ( +
handleNewSlide(sampleData, layoutId)} + key={`${group}-${index}`} + className=" relative cursor-pointer overflow-hidden aspect-video" + > +
+
+ +
+
+ ); + })} +
+
+ ); +}; + +export default NewSlide; diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index d08147b4..16bd6724 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -19,6 +19,15 @@ export interface LayoutInfo { json_schema: any; groupName: string; } +export interface FullDataInfo { + name: string; + component: React.ComponentType; + schema: any; + sampleData: any; + fileName: string; + groupName: string; + layoutId: string; +} export interface GroupSetting { description: string; @@ -39,6 +48,7 @@ export interface LayoutData { fileMap: Map; groupedLayouts: Map; layoutSchema: LayoutInfo[]; + fullDataByGroup: Map; } export interface LayoutContextType { @@ -51,7 +61,7 @@ export interface LayoutContextType { getGroupSetting: (groupName: string) => GroupSetting | null; getAllGroups: () => string[]; getAllLayouts: () => LayoutInfo[]; - + getFullDataByGroup: (groupName: string) => FullDataInfo[]; loading: boolean; error: string | null; getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null; @@ -119,6 +129,7 @@ export const LayoutProvider: React.FC<{ const groupSettingsMap = new Map(); const fileMap = new Map(); const groupedLayouts = new Map(); + const fullDataByGroup = new Map(); // Start preloading process setIsPreloading(true); @@ -130,6 +141,8 @@ export const LayoutProvider: React.FC<{ layoutsByGroup.set(groupData.groupName, new Set()); } + fullDataByGroup.set(groupData.groupName, []); + // group settings or default settings const settings = groupData.settings || { description: `${groupData.groupName} presentation layouts`, @@ -139,6 +152,7 @@ export const LayoutProvider: React.FC<{ groupSettingsMap.set(groupData.groupName, settings); const groupLayouts: LayoutInfo[] = []; + const groupFullData: FullDataInfo[] = []; for (const fileName of groupData.files) { try { @@ -194,6 +208,18 @@ export const LayoutProvider: React.FC<{ groupName: groupData.groupName, }; + const sampleData = module.Schema.parse({}); + const fullData: FullDataInfo = { + name: layoutName, + component: module.default, + schema: jsonSchema, + sampleData: sampleData, + fileName, + groupName: groupData.groupName, + layoutId: uniqueKey, + }; + groupFullData.push(fullData); + layoutsById.set(uniqueKey, layout); layoutsByGroup.get(groupData.groupName)!.add(uniqueKey); fileMap.set(uniqueKey, { @@ -210,6 +236,7 @@ export const LayoutProvider: React.FC<{ } } + fullDataByGroup.set(groupData.groupName, groupFullData); // Cache grouped layouts groupedLayouts.set(groupData.groupName, groupLayouts); } @@ -224,6 +251,7 @@ export const LayoutProvider: React.FC<{ fileMap, groupedLayouts, layoutSchema: layouts, + fullDataByGroup, }; }; @@ -269,6 +297,10 @@ export const LayoutProvider: React.FC<{ customLayouts.groupedLayouts ), layoutSchema: [...data.layoutSchema, ...customLayouts.layoutSchema], + fullDataByGroup: mergeMaps( + data.fullDataByGroup, + customLayouts.fullDataByGroup + ), }; setLayoutData(combinedData); @@ -301,7 +333,7 @@ export const LayoutProvider: React.FC<{ const groupSettingsMap = new Map(); const fileMap = new Map(); const groupedLayouts = new Map(); - + const fullDataByGroup = new Map(); try { const customGroupResponse = await fetch( "/api/v1/ppt/layout-management/summary" @@ -312,6 +344,7 @@ export const LayoutProvider: React.FC<{ console.log("🔍 customGroup", customGroup); for (const group of customGroup) { const groupName = `custom-${group.presentation_id}`; + fullDataByGroup.set(groupName, []); if (!layoutsByGroup.has(groupName)) { layoutsByGroup.set(groupName, new Set()); } @@ -330,6 +363,7 @@ export const LayoutProvider: React.FC<{ groupSettingsMap.set(`custom-${presentationId}`, settings); const groupLayouts: LayoutInfo[] = []; + const groupFullData: FullDataInfo[] = []; for (const i of allLayout) { /* ---------- 1. compile JSX to plain script ------------------ */ @@ -383,6 +417,17 @@ export const LayoutProvider: React.FC<{ json_schema: jsonSchema, groupName: groupName, }; + const sampleData = module.Schema.parse({}); + const fullData: FullDataInfo = { + name: layoutName, + component: module.default, + schema: jsonSchema, + sampleData: sampleData, + fileName: i.layout_name, + groupName: groupName, + layoutId: uniqueKey, + }; + groupFullData.push(fullData); layoutsById.set(uniqueKey, layout); layoutsByGroup.get(groupName)!.add(uniqueKey); @@ -395,6 +440,7 @@ export const LayoutProvider: React.FC<{ } // Cache grouped layouts groupedLayouts.set(groupName, groupLayouts); + fullDataByGroup.set(groupName, groupFullData); } } catch (err: any) { console.error("Compilation error:", err); @@ -407,6 +453,7 @@ export const LayoutProvider: React.FC<{ fileMap, groupedLayouts, layoutSchema: layouts, + fullDataByGroup, }; }; @@ -489,6 +536,10 @@ export const LayoutProvider: React.FC<{ return layoutData?.layoutSchema || []; }; + const getFullDataByGroup = (groupName: string): FullDataInfo[] => { + return layoutData?.fullDataByGroup.get(groupName) || []; + }; + // Load layouts on mount useEffect(() => { loadLayouts(); @@ -501,7 +552,7 @@ export const LayoutProvider: React.FC<{ getGroupSetting, getAllGroups, getAllLayouts, - + getFullDataByGroup, loading, error, getLayout, diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx index fcb35750..e0370ebe 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GroupLayouts.tsx @@ -1,7 +1,8 @@ import { CheckCircle } from "lucide-react"; import React from "react"; import { LayoutGroup } from "../types/index"; -import { useGroupLayoutLoader } from "@/app/layout-preview/hooks/useGroupLayoutLoader"; +import { useLayout } from "../../context/LayoutContext"; +// import { useGroupLayoutLoader } from "@/app/layout-preview/hooks/useGroupLayoutLoader"; interface GroupLayoutsProps { group: LayoutGroup; onSelectLayoutGroup: (group: LayoutGroup) => void; @@ -13,7 +14,8 @@ const GroupLayouts: React.FC = ({ onSelectLayoutGroup, selectedLayoutGroup, }) => { - const { layoutGroup } = useGroupLayoutLoader(group.id); + const { getFullDataByGroup } = useLayout(); + const layoutGroup = getFullDataByGroup(group.id); return (
onSelectLayoutGroup(group)} @@ -39,11 +41,16 @@ const GroupLayouts: React.FC = ({ {/* Layout previews */}
{layoutGroup && - layoutGroup?.layouts.slice(0, 4).map((layout: any, index: number) => { - const { component: LayoutComponent, sampleData, layoutId } = layout; + layoutGroup?.slice(0, 4).map((layout: any, index: number) => { + const { + component: LayoutComponent, + sampleData, + layoutId, + groupName, + } = layout; return (
@@ -56,7 +63,7 @@ const GroupLayouts: React.FC = ({
- {layoutGroup?.layouts.length} layouts + {layoutGroup?.length} layouts void; - slideNumber: number; - onSlideUpdate: (updatedSlide: any) => void; -} - -const DrawingCanvas: React.FC = ({ - slideElement, - onClose, - slideNumber, - onSlideUpdate, -}) => { - const canvasRef = useRef(null); - const containerRef = useRef(null); - const slideDisplayRef = useRef(null); - const slideContentRef = useRef(null); - const [strokeWidth, setStrokeWidth] = useState(3); - const [strokeColor, setStrokeColor] = useState("#000000"); - const [eraserMode, setEraserMode] = useState(false); - const [isDrawing, setIsDrawing] = useState(false); - const [prompt, setPrompt] = useState(""); - const [isUpdating, setIsUpdating] = useState(false); - const [slideHtml, setSlideHtml] = useState(""); - const [canvasDimensions, setCanvasDimensions] = useState({ - width: 800, - height: 600, - }); - - useEffect(() => { - if (slideElement && containerRef.current) { - console.log("slideElement", slideElement); - const rect = slideElement.getBoundingClientRect(); - - // Set canvas dimensions to match the slide element - setCanvasDimensions({ - width: Math.max(rect.width, 800), - height: Math.max(rect.height, 600), - }); - - // Store the HTML once to prevent re-renders - setSlideHtml(slideElement.innerHTML); - } - }, [slideElement]); - - // Apply optimizations once after slide content is rendered - useEffect(() => { - if (slideContentRef.current && slideHtml) { - const slideContent = slideContentRef.current; - - // Apply styles to prevent interactions and flickering - slideContent.style.pointerEvents = "none"; - slideContent.style.userSelect = "none"; - slideContent.style.transform = "translateZ(0)"; - slideContent.style.willChange = "auto"; - slideContent.style.backfaceVisibility = "hidden"; - - // Target all interactive elements - 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; - } - - // Remove any event listeners - el.onclick = null; - el.onmousedown = null; - el.onmouseup = null; - el.onmousemove = null; - }); - } - }, [slideHtml]); - - const getCanvasContext = () => { - const canvas = canvasRef.current; - if (!canvas) return null; - return canvas.getContext("2d"); - }; - - const getMousePos = (e: React.MouseEvent) => { - 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) => { - 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; - - 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) => { - e.preventDefault(); - const pos = getMousePos(e); - startDrawing(pos); - }; - - const handleMouseMove = (e: React.MouseEvent) => { - e.preventDefault(); - const pos = getMousePos(e); - draw(pos); - }; - - const handleMouseUp = (e: React.MouseEvent) => { - e.preventDefault(); - stopDrawing(); - }; - - // Touch events - const handleTouchStart = (e: React.TouchEvent) => { - e.preventDefault(); - const pos = getTouchPos(e); - startDrawing(pos); - }; - - const handleTouchMove = (e: React.TouchEvent) => { - e.preventDefault(); - const pos = getTouchPos(e); - draw(pos); - }; - - const handleTouchEnd = (e: React.TouchEvent) => { - e.preventDefault(); - stopDrawing(); - }; - - const handleClearCanvas = () => { - const canvas = canvasRef.current; - 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 (!slideElement || !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) => { - // Ignore the canvas element when taking screenshot of slide only - return element.tagName === "CANVAS"; - }, - }); - - // Take screenshot of the entire slide display area including canvas - const slideWithCanvas = await html2canvas(slideDisplayRef.current, { - backgroundColor: "#ffffff", - scale: 1, - logging: false, - useCORS: true, - }); - - // Get the current HTML content from the original slide element - const currentHtml = slideElement.innerHTML; - - // Convert canvas images to blobs - const currentUiImageBlob = dataURLToBlob( - slideOnly.toDataURL("image/png") - ); - const sketchImageBlob = dataURLToBlob( - slideWithCanvas.toDataURL("image/png") - ); - - // Prepare form data - const formData = new FormData(); - formData.append( - "current_ui_image", - currentUiImageBlob, - `slide-${slideNumber}-current.png` - ); - formData.append( - "sketch_image", - sketchImageBlob, - `slide-${slideNumber}-sketch.png` - ); - formData.append("html", currentHtml); - formData.append("prompt", prompt); - - // Call the API - 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(); - - // Update the slide with new data - onSlideUpdate({ - slide_number: slideNumber, - html: data.edited_html || currentHtml, - processed: true, - processing: false, - error: undefined, - }); - // Close the drawing canvas - onClose(); - } catch (error) { - console.error("Error updating slide:", error); - alert( - `Error updating slide: ${ - error instanceof Error ? error.message : "Unknown error" - }` - ); - } finally { - setIsUpdating(false); - } - }; - - const handleEraserModeChange = (isEraser: boolean) => { - setEraserMode(isEraser); - }; - - const handleStrokeColorChange = (color: string) => { - setStrokeColor(color); - setEraserMode(false); // Switch back to draw mode when selecting color - }; - - const handleStrokeWidthChange = (width: number) => { - setStrokeWidth(width); - }; - - const colors = [ - "#000000", - "#FF0000", - "#00FF00", - "#0000FF", - "#FFFF00", - "#FF00FF", - "#00FFFF", - "#FFA500", - ]; - - const strokeWidths = [1, 3, 5, 8, 12]; - - return ( -
{ - if (e.target === e.currentTarget) { - onClose(); - } - }} - > -
- {/* Controls */} -
-
-

Edit Slide {slideNumber}

- - {/* Drawing Tools */} -
- - - -
- - {/* Color Picker */} - {!eraserMode && ( -
- {colors.map((color) => ( -
- )} - - {/* Stroke Width */} -
- {strokeWidths.map((width) => ( - - ))} -
- - -
- -
- -
-
- - {/* Prompt Section */} -
-
- -
-