diff --git a/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx
index 1b8ecf12..1fe60fb0 100644
--- a/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx
@@ -28,6 +28,7 @@ interface IconsEditorProps {
isWhite?: boolean;
className?: string;
icon_prompt?: string[] | null;
+ onClose?: () => void;
}
const IconsEditor = ({
@@ -39,6 +40,7 @@ const IconsEditor = ({
slideIndex,
elementId,
icon_prompt,
+ onClose,
}: IconsEditorProps) => {
const dispatch = useDispatch();
@@ -97,124 +99,76 @@ const IconsEditor = ({
};
return (
- <>
-
onClose?.()}>
+
e.preventDefault()}
>
- {icon ? (
-
+ Choose Icon
+
+
+
+
);
};
diff --git a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
index de66031f..159abdac 100644
--- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
@@ -45,6 +45,7 @@ interface ImageEditorProps {
className?: string;
promptContent?: string;
properties?: null | any;
+ onClose?: () => void;
}
const ImageEditor = ({
@@ -56,6 +57,7 @@ const ImageEditor = ({
elementId,
promptContent,
properties,
+ onClose,
}: ImageEditorProps) => {
const dispatch = useDispatch();
@@ -289,376 +291,178 @@ const ImageEditor = ({
};
return (
- <>
-
{
- if (initialImage !== undefined) {
- if (isFocusPointMode) {
- handleFocusPointClick(e);
- } else {
- handleImageClick();
- }
- }
- }}
+
onClose?.()}>
+ e.preventDefault()}
>
- {image ? (
-
- ) : (
-
-
- {
-
- {initialImage !== undefined
- ? "Click to add image"
- : "Loading..."}
-
- }
-
- )}
+
+ Update Image
+
-
+
+
+
+
+ AI Generate
+
- {isFocusPointMode && (
-
-
-
- Click anywhere to set focus point
-
-
-
+
+ Upload
+
+
- {/* Focus point marker */}
-
-
- )}
+
+
+
+
+
Current Prompt
- {/* Image Toolbar */}
- {isToolbarOpen && !isFocusPointMode && (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- )}
-
-
-
- e.preventDefault()}
- >
-
- Update Image
-
-
-
-
-
-
- AI Generate
-
-
-
- Upload
-
-
-
-
-
-
-
-
Current Prompt
-
-
{promptContent}
-
-
-
- Image Description
-
-
-
-
- {error &&
{error}
}
-
-
- {isGenerating || previewImages.length === 0
- ? Array.from({ length: 4 }).map((_, index) => (
-
- ))
- : previewImages.map((image, index) => (
-
handleImageChange(image as string)}
- className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer"
- >
-

-
- ))}
-
+
{promptContent}
-
-
-
-
+
+ Image Description
+
+
+
+
+ {error &&
{error}
}
+
+
+ {isGenerating || previewImages.length === 0
+ ? Array.from({ length: 4 }).map((_, index) => (
+
+ ))
+ : previewImages.map((image, index) => (
+
handleImageChange(image as string)}
+ className="aspect-[4/3] w-full overflow-hidden rounded-lg border cursor-pointer"
+ >
+

+
+ ))}
+
+
+
+
+
+
- {uploadError && (
-
- {uploadError}
-
- )}
+ {isUploading ? (
+
+ ) : (
+
+ )}
+
+ {isUploading
+ ? "Uploading your image..."
+ : "Click to upload an image"}
+
+
+ Maximum file size: 5MB
+
+
+
+ {uploadError && (
+
+ {uploadError}
+
+ )}
- {(uploadedImageUrl || isUploading) && (
-
-
- Uploaded Image Preview
-
-
- {isUploading ? (
-
-
-
-
- Processing...
+ {(uploadedImageUrl || isUploading) && (
+
+
+ Uploaded Image Preview
+
+
+ {isUploading ? (
+
+ ) : (
+ uploadedImageUrl && (
+
+ handleImageChange(uploadedImageUrl)
+ }
+ className="cursor-pointer group w-full h-full"
+ >
+
})
+
+
+
+ Click to use this image
- ) : (
- uploadedImageUrl && (
-
- handleImageChange(uploadedImageUrl)
- }
- className="cursor-pointer group w-full h-full"
- >
-
})
-
-
-
- Click to use this image
-
-
-
- )
- )}
-
+ )
+ )}
- )}
-
-
-
-
-
-
- >
+
+ )}
+
+
+
+
+
+
);
};
diff --git a/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx b/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx
new file mode 100644
index 00000000..08ac5989
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx
@@ -0,0 +1,354 @@
+"use client";
+
+import React, { createContext, useContext, useRef, useEffect, ReactNode, useState } from 'react';
+import ReactDOM from 'react-dom';
+import ImageEditor from './ImageEditor';
+import IconsEditor from './IconsEditor';
+
+interface SmartEditableContextType {
+ slideIndex: number;
+ slideId: string;
+ isEditMode: boolean;
+ slideData: any;
+}
+
+const SmartEditableContext = createContext
(null);
+
+interface SmartEditableProviderProps {
+ children: ReactNode;
+ slideIndex: number;
+ slideId: string;
+ slideData: any;
+ isEditMode?: boolean;
+}
+
+interface EditableElement {
+ type: 'image' | 'icon';
+ element: HTMLImageElement;
+ dataPath: string;
+ props: any;
+}
+
+export const SmartEditableProvider: React.FC = ({
+ children,
+ slideIndex,
+ slideId,
+ slideData,
+ isEditMode = true,
+}) => {
+ const containerRef = useRef(null);
+ const [editableElements, setEditableElements] = useState([]);
+ const [activeEditor, setActiveEditor] = useState<{
+ type: 'image' | 'icon';
+ element: HTMLImageElement;
+ props: any;
+ rect: DOMRect;
+ } | null>(null);
+
+ useEffect(() => {
+ if (!isEditMode || !containerRef.current || !slideData) return;
+
+ const container = containerRef.current;
+
+ const findEditableElements = () => {
+ const elements: EditableElement[] = [];
+
+ console.log('🔍 Starting smart detection with slideData:', slideData);
+
+ // Scan data structure for __image_url__ and __icon_url__ patterns
+ const detectEditableElementsFromData = (data: any, path: string = '') => {
+ if (!data || typeof data !== 'object') return;
+
+ // Check for __image_url__ pattern
+ if (data.__image_url__) {
+ console.log(`📸 Found __image_url__ at ${path}:`, data.__image_url__);
+ const imgElement = findDOMElementByImageUrl(container, data.__image_url__);
+ if (imgElement) {
+ elements.push({
+ type: 'image',
+ element: imgElement,
+ dataPath: path,
+ props: {
+ slideIndex,
+ elementId: `image-${path.replace(/[^\w]/g, '-')}`,
+ initialImage: data.__image_url__,
+ title: imgElement.alt || 'Image',
+ promptContent: data.__image_prompt__ || '',
+ imageIdx: elements.filter(e => e.type === 'image').length
+ }
+ });
+ console.log(`✅ Matched image to DOM element:`, imgElement);
+ }
+ }
+
+ // Check for __icon_url__ pattern
+ if (data.__icon_url__) {
+ console.log(`🎯 Found __icon_url__ at ${path}:`, data.__icon_url__);
+ const imgElement = findDOMElementByImageUrl(container, data.__icon_url__);
+ if (imgElement) {
+ elements.push({
+ type: 'icon',
+ element: imgElement,
+ dataPath: path,
+ props: {
+ slideIndex,
+ elementId: `icon-${path.replace(/[^\w]/g, '-')}`,
+ icon: data.__icon_url__,
+ index: elements.filter(e => e.type === 'icon').length,
+ backgroundColor: '#3B82F6',
+ hasBg: false,
+ icon_prompt: data.__icon_query__ ? [data.__icon_query__] : []
+ }
+ });
+ console.log(`✅ Matched icon to DOM element:`, imgElement);
+ }
+ }
+
+ // Recursively scan nested objects and arrays
+ Object.keys(data).forEach(key => {
+ const value = data[key];
+ const newPath = path ? `${path}.${key}` : key;
+
+ if (Array.isArray(value)) {
+ value.forEach((item, index) => {
+ detectEditableElementsFromData(item, `${newPath}[${index}]`);
+ });
+ } else if (value && typeof value === 'object') {
+ detectEditableElementsFromData(value, newPath);
+ }
+ });
+ };
+
+ detectEditableElementsFromData(slideData);
+ console.log('🎉 Final detected elements:', elements);
+ setEditableElements(elements);
+ };
+
+ const findDOMElementByImageUrl = (container: HTMLElement, targetUrl: string): HTMLImageElement | null => {
+ const allImages = Array.from(container.getElementsByTagName('img'));
+
+ for (const img of allImages) {
+ if (isMatchingImageUrl(img.src, targetUrl)) {
+ return img;
+ }
+ }
+ return null;
+ };
+
+ const isMatchingImageUrl = (domSrc: string, dataSrc: string): boolean => {
+ // Direct match
+ if (domSrc === dataSrc) return true;
+
+ // Handle app_data paths
+ if (dataSrc.includes('/app_data/images/') || domSrc.includes('/app_data/images/')) {
+ const getFilename = (path: string) => path.split('/').pop() || '';
+ return getFilename(domSrc) === getFilename(dataSrc);
+ }
+
+ // Handle placeholder URLs
+ if (dataSrc.includes('placeholder') || domSrc.includes('placeholder')) {
+ return true;
+ }
+
+ // Extract and compare filenames
+ const getFilename = (path: string) => path.split('/').pop() || '';
+ return getFilename(domSrc) === getFilename(dataSrc) && getFilename(domSrc) !== '';
+ };
+
+ // Add event delegation for clicks
+ const handleClick = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const editableElement = editableElements.find(el => el.element === imgElement);
+
+ if (editableElement) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const rect = imgElement.getBoundingClientRect();
+ setActiveEditor({
+ type: editableElement.type,
+ element: imgElement,
+ props: editableElement.props,
+ rect
+ });
+ }
+ }
+ };
+
+ // Add hover effects
+ const handleMouseEnter = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const isEditable = editableElements.some(el => el.element === imgElement);
+
+ if (isEditable) {
+ imgElement.style.cursor = 'pointer';
+ imgElement.style.filter = 'brightness(0.9)';
+ imgElement.style.transition = 'filter 0.2s ease';
+ }
+ }
+ };
+
+ const handleMouseLeave = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const isEditable = editableElements.some(el => el.element === imgElement);
+
+ if (isEditable) {
+ imgElement.style.filter = '';
+ }
+ }
+ };
+
+ // Set up event listeners after elements are found
+ const timer = setTimeout(() => {
+ findEditableElements();
+ }, 500);
+
+ return () => {
+ clearTimeout(timer);
+ container.removeEventListener('click', handleClick);
+ container.removeEventListener('mouseenter', handleMouseEnter, true);
+ container.removeEventListener('mouseleave', handleMouseLeave, true);
+ };
+ }, [slideIndex, slideId, slideData, isEditMode, editableElements]);
+
+ // Set up event listeners when editableElements change
+ useEffect(() => {
+ if (!containerRef.current || editableElements.length === 0) return;
+
+ const container = containerRef.current;
+
+ const handleClick = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const editableElement = editableElements.find(el => el.element === imgElement);
+
+ if (editableElement) {
+ event.preventDefault();
+ event.stopPropagation();
+
+ const rect = imgElement.getBoundingClientRect();
+ setActiveEditor({
+ type: editableElement.type,
+ element: imgElement,
+ props: editableElement.props,
+ rect
+ });
+ }
+ }
+ };
+
+ const handleMouseEnter = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const isEditable = editableElements.some(el => el.element === imgElement);
+
+ if (isEditable) {
+ imgElement.style.cursor = 'pointer';
+ imgElement.style.filter = 'brightness(0.9)';
+ imgElement.style.transition = 'filter 0.2s ease';
+ }
+ }
+ };
+
+ const handleMouseLeave = (event: MouseEvent) => {
+ const target = event.target as HTMLElement;
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const isEditable = editableElements.some(el => el.element === imgElement);
+
+ if (isEditable) {
+ imgElement.style.filter = '';
+ }
+ }
+ };
+
+ container.addEventListener('click', handleClick);
+ container.addEventListener('mouseenter', handleMouseEnter, true);
+ container.addEventListener('mouseleave', handleMouseLeave, true);
+
+ return () => {
+ container.removeEventListener('click', handleClick);
+ container.removeEventListener('mouseenter', handleMouseEnter, true);
+ container.removeEventListener('mouseleave', handleMouseLeave, true);
+ };
+ }, [editableElements]);
+
+ return (
+
+
+ {children}
+
+
+ {/* Render active editor as a modal/overlay */}
+ {activeEditor && (
+ setActiveEditor(null)}
+ />
+ )}
+
+ );
+};
+
+// Simple overlay component for editors
+const EditorOverlay: React.FC<{
+ activeEditor: {
+ type: 'image' | 'icon';
+ element: HTMLImageElement;
+ props: any;
+ rect: DOMRect;
+ };
+ onClose: () => void;
+}> = ({ activeEditor, onClose }) => {
+ useEffect(() => {
+ const handleEscape = (e: KeyboardEvent) => {
+ if (e.key === 'Escape') onClose();
+ };
+
+ const handleClickOutside = (e: MouseEvent) => {
+ // Close if clicked outside the editor
+ const target = e.target as HTMLElement;
+ if (!target.closest('.editor-modal')) {
+ onClose();
+ }
+ };
+
+ document.addEventListener('keydown', handleEscape);
+ document.addEventListener('click', handleClickOutside);
+
+ return () => {
+ document.removeEventListener('keydown', handleEscape);
+ document.removeEventListener('click', handleClickOutside);
+ };
+ }, [onClose]);
+
+ const EditorComponent = activeEditor.type === 'image' ? ImageEditor : IconsEditor;
+
+ return ReactDOM.createPortal(
+ ,
+ document.body
+ );
+};
+
+export const useSmartEditable = () => {
+ const context = useContext(SmartEditableContext);
+ if (!context) {
+ throw new Error('useSmartEditable must be used within SmartEditableProvider');
+ }
+ return context;
+};
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
index 5738207e..aa379cf3 100644
--- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
+++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
@@ -1,6 +1,7 @@
'use client'
import React, { useMemo } from 'react';
import { useLayout } from '../context/LayoutContext';
+import { SmartEditableProvider } from '../components/SmartEditableWrapper';
export const useGroupLayouts = () => {
const {
@@ -28,9 +29,9 @@ export const useGroupLayouts = () => {
};
}, [getLayoutsByGroup]);
- // Render slide content with group validation
+ // Render slide content with group validation and smart editing capabilities
const renderSlideContent = useMemo(() => {
- return (slide: any) => {
+ return (slide: any, isEditMode: boolean = true) => {
const Layout = getGroupLayout(slide.layout, slide.layout_group);
if (!Layout) {
return (
@@ -41,6 +42,19 @@ export const useGroupLayouts = () => {
);
}
+
+ if (isEditMode) {
+ return (
+
+
+
+ );
+ }
return ;
};
}, [getGroupLayout]);
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx
index 235553ca..16c57120 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SidePanel.tsx
@@ -274,7 +274,7 @@ const SidePanel = ({
- {renderSlideContent(slide)}
+ {renderSlideContent(slide, false)}
@@ -294,7 +294,7 @@ const SidePanel = ({
index={index}
selectedSlide={selectedSlide}
onSlideClick={onSlideClick}
- renderSlideContent={renderSlideContent}
+ renderSlideContent={(slide) => renderSlideContent(slide, false)}
/>
))}
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
index f9ddcb63..82e613f5 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx
@@ -39,22 +39,7 @@ const SlideContent = ({
);
// Use the centralized group layouts hook
- const { getGroupLayout, loading } = useGroupLayouts();
-
- // Memoized layout component to prevent re-renders
- const LayoutComponent = useMemo(() => {
- const Layout = getGroupLayout(slide.layout, slide.layout_group);
- if (!Layout) {
- return () => (
-
-
- Layout "{slide.layout}" not found in current group
-
-
- );
- }
- return Layout;
- }, [slide.layout, getGroupLayout]);
+ const { renderSlideContent, loading } = useGroupLayouts();
const handleSubmit = async () => {
const element = document.getElementById(
@@ -142,8 +127,8 @@ const SlideContent = ({
// Memoized slide content rendering to prevent unnecessary re-renders
const slideContent = useMemo(() => {
- return ;
- }, [LayoutComponent, slide.content]);
+ return renderSlideContent(slide, true); // Enable edit mode for main content
+ }, [renderSlideContent, slide]);
return (
<>
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx
index fe4cf9e9..5fd793f4 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx
+++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SortableSlide.tsx
@@ -8,7 +8,7 @@ interface SortableSlideProps {
index: number;
selectedSlide: number;
onSlideClick: (index: any) => void;
- renderSlideContent: (slide: any) => React.ReactElement;
+ renderSlideContent: (slide: any, isEditMode?: boolean) => React.ReactElement;
}
export function SortableSlide({ slide, index, selectedSlide, onSlideClick, renderSlideContent }: SortableSlideProps) {
@@ -57,7 +57,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende
- {renderSlideContent(slide)}
+ {renderSlideContent(slide, false)}