diff --git a/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx b/servers/nextjs/app/(presentation-generator)/components/IconsEditor.tsx
index 1b8ecf12..ca8d6e17 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,83 @@ const IconsEditor = ({
};
return (
- <>
-
onClose?.()}>
+
e.preventDefault()}
+ onClick={(e) => e.stopPropagation()}
>
- {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..14349032 100644
--- a/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/ImageEditor.tsx
@@ -12,9 +12,8 @@ import { Textarea } from "@/components/ui/textarea";
import {
Wand2,
Upload,
- Edit,
Move,
- Maximize,
+
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useDispatch, useSelector } from "react-redux";
@@ -28,34 +27,27 @@ import {
} from "@/store/slices/presentationGeneration";
import { getStaticFileUrl, ThemeImagePrompt } from "../utils/others";
-import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import ToolTip from "@/components/ToolTip";
interface ImageEditorProps {
initialImage: string | null;
imageIdx?: number;
- title: string;
+
slideIndex: number;
- elementId: string;
+
className?: string;
promptContent?: string;
properties?: null | any;
+ onClose?: () => void;
}
const ImageEditor = ({
initialImage,
imageIdx = 0,
- className,
- title,
slideIndex,
- elementId,
promptContent,
properties,
+ onClose,
}: ImageEditorProps) => {
const dispatch = useDispatch();
@@ -64,9 +56,6 @@ const ImageEditor = ({
const searchParams = useSearchParams();
const [image, setImage] = useState(initialImage);
const [previewImages, setPreviewImages] = useState([initialImage]);
-
- const [isEditorOpen, setIsEditorOpen] = useState(false);
- const [isToolbarOpen, setIsToolbarOpen] = useState(false);
const [prompt, setPrompt] = useState
("");
const [isGenerating, setIsGenerating] = useState(false);
const [error, setError] = useState(null);
@@ -108,7 +97,7 @@ const ImageEditor = ({
!toolbarRef.current.contains(event.target as Node) &&
!popoverContentRef.current
) {
- setIsToolbarOpen(false);
+
if (isFocusPointMode) {
// saveFocusPoint(); // Save focus point before closing
saveImageProperties(objectFit, focusPoint);
@@ -123,16 +112,7 @@ const ImageEditor = ({
};
}, [isFocusPointMode, focusPoint]);
- const handleImageClick = () => {
- if (!isFocusPointMode) {
- setIsToolbarOpen(true);
- }
- };
- const handleOpenEditor = () => {
- setIsToolbarOpen(false);
- setIsEditorOpen(true);
- };
const handleImageChange = (newImage: string) => {
setImage(newImage);
@@ -143,7 +123,6 @@ const ImageEditor = ({
image: newImage,
})
);
- setIsEditorOpen(false);
};
const handleFocusPointClick = (e: React.MouseEvent) => {
@@ -282,383 +261,344 @@ const ImageEditor = ({
}
};
- // Helper function to determine image URL
- const getImageUrl = (src: string | null) => {
- if (!src) return "";
- return getStaticFileUrl(src) || "";
- };
+
return (
- <>
- {
- if (initialImage !== undefined) {
- if (isFocusPointMode) {
- handleFocusPointClick(e);
- } else {
- handleImageClick();
- }
- }
- }}
+
onClose?.()}>
+ e.preventDefault()}
+ onClick={(e) => e.stopPropagation()}
>
- {image ? (
-
- ) : (
-
-
- {
-
- {initialImage !== undefined
- ? "Click to add image"
- : "Loading..."}
-
- }
-
- )}
+
+ Update Image
+
-
+
+
+
+
+ Edit
+
+
+ AI Generate
+
+
+ Upload
+
+
- {isFocusPointMode && (
-
-
-
- Click anywhere to set focus point
-
-
-
-
- {/* Focus point marker */}
-
-
- )}
-
- {/* 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"
- >
-

-
- ))}
-
-
-
-
-
+
+
+ {/* Current Image Preview */}
+
+
Current Image
-
-
-
- {uploadError && (
-
- {uploadError}
-
- )}
-
- {(uploadedImageUrl || isUploading) && (
-
-
- Uploaded Image Preview
-
-
- {isUploading ? (
-
- ) : (
- uploadedImageUrl && (
-
- handleImageChange(uploadedImageUrl)
- }
- className="cursor-pointer group w-full h-full"
- >
-
})
-
-
-
- Click to use this image
-
-
-
- )
- )}
+ {image ? (
+

{
+ e.stopPropagation();
+ handleFocusPointClick(e);
+ }}
+ onError={(e) => {
+ console.error('Image failed to load:', image);
+ e.currentTarget.src = '/placeholder-image.png';
+ }}
+ />
+ ) : (
+
+ )}
+
+ {/* Focus Point Indicator */}
+ {isFocusPointMode && image && (
+
+ )}
+
+ {/* Debug info */}
+ {image && (
+
+
Image Path: {image}
+
Resolved URL: {image}
+
Focus Point: {focusPoint.x.toFixed(1)}%, {focusPoint.y.toFixed(1)}%
+
Object Fit: {objectFit}
)}
-
-
-
-
-
- >
+
+ {/* Editing Controls */}
+
+ {/* Focus Point Controls */}
+
+
+
Focus Point
+
+
+ {isFocusPointMode && (
+
+ Click on the image above to set the focus point
+
+ )}
+
+
+ {/* Object Fit Controls */}
+
+
Image Fit
+
+
+
+
+
+
+
Cover: Fill container, may crop image
+
Contain: Fit entire image, may show empty space
+
Fill: Stretch to fill container exactly
+
+
+
+ {/* Quick Actions */}
+
+
Quick Actions
+
+
+
+
+
+
+
+
+
+
+
+
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"
+ >
+

+
+ ))}
+
+
+
+
+
+
+
+
+
+ {uploadError && (
+
+ {uploadError}
+
+ )}
+
+ {(uploadedImageUrl || isUploading) && (
+
+
+ Uploaded Image Preview
+
+
+ {isUploading ? (
+
+ ) : (
+ 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..730c98d8
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx
@@ -0,0 +1,306 @@
+"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 EditableElement {
+ type: 'image' | 'icon';
+ element: HTMLElement;
+ dataPath?: string;
+ props: any;
+}
+
+interface SmartEditableContextType {
+ slideIndex: number;
+ slideId: string;
+ isEditMode: boolean;
+ slideData: any;
+}
+
+interface SmartEditableProviderProps {
+ children: ReactNode;
+ slideIndex: number;
+ slideId: string;
+ slideData: any;
+ isEditMode?: boolean;
+}
+
+const SmartEditableContext = createContext(null);
+
+export const useSmartEditable = () => {
+ const context = useContext(SmartEditableContext);
+ if (!context) {
+ throw new Error('useSmartEditable must be used within SmartEditableProvider');
+ }
+ return context;
+};
+
+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: HTMLElement;
+ 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);
+
+ // Detect Images and Icons only (text is now handled by SmartText components)
+ 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,
+ initialImage: data.__image_url__,
+ 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) !== '';
+ };
+
+ // Set up event listeners after elements are found
+ const timer = setTimeout(() => {
+ findEditableElements();
+ }, 500);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [slideIndex, slideId, slideData, isEditMode]); // Removed editableElements from dependency array
+
+ // 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;
+
+ // Handle image/icon clicks only
+ if (target.tagName === 'IMG') {
+ const imgElement = target as HTMLImageElement;
+ const editableElement = editableElements.find(el => el.element === imgElement);
+
+ if (editableElement && (editableElement.type === 'image' || editableElement.type === 'icon')) {
+ 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;
+
+ // Handle image/icon hover only
+ 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;
+
+ // Handle image/icon hover only
+ 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: HTMLElement;
+ 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]);
+
+ // Handle image/icon editing in modal
+ const EditorComponent = activeEditor.type === 'image' ? ImageEditor : IconsEditor;
+
+ return ReactDOM.createPortal(
+ ,
+ document.body
+ );
+};
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx b/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx
index 4ae37823..33e11dfb 100644
--- a/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx
+++ b/servers/nextjs/app/(presentation-generator)/components/Tiptap.tsx
@@ -4,16 +4,6 @@ import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import { Markdown } from "tiptap-markdown";
import Underline from "@tiptap/extension-underline";
-import { useDispatch, useSelector } from "react-redux";
-import {
- updateInfographicsDescription,
- updateInfographicsTitle,
- updateSlideBodyDescription,
- updateSlideBodyHeading,
- updateSlideBodyString,
- updateSlideDescription,
- updateSlideTitle,
-} from "@/store/slices/presentationGeneration";
import {
Bold,
Italic,
@@ -21,129 +11,24 @@ import {
Strikethrough,
Code,
} from "lucide-react";
-import { RootState } from "@/store/store";
-declare module "@tiptap/core" {
- interface Commands {
- fontSize: {
- /**
- * Set the font size
- */
- setFontSize: (size: string) => ReturnType;
- /**
- * Unset the font size
- */
- unsetFontSize: () => ReturnType;
- };
- }
-}
+
const TipTapEditor = ({
content,
- isAlingCenter,
- bodyIdx,
- slideIndex,
- elementId,
- type,
}: {
content: string;
- isAlingCenter: boolean;
- bodyIdx: number;
- slideIndex: number;
- elementId: string;
- type: string;
+
}) => {
- const dispatch = useDispatch();
- const { currentColors } = useSelector((state: RootState) => state.theme);
- const getTextStyle = () => {
- const baseStyle = "outline-none transition-all duration-200 ";
- switch (type) {
- case "title":
- return `${baseStyle} slide-title text-xl sm:text-2xl lg:text-[40px] leading-[36px] lg:leading-[48px] font-bold `;
- case "heading":
- case "info-heading":
- return `${baseStyle} slide-heading text-base sm:text-lg lg:text-[24px] leading-[26px] lg:leading-[32px] font-bold`;
- case "description":
- case "info-description":
- case "description-body":
- case "heading-description":
- return `${baseStyle} slide-description text-sm sm:text-base lg:text-[20px] leading-[20px] lg:leading-[30px] font-normal`;
- default:
- return `${baseStyle} slide-description text-sm sm:text-base lg:text-[20px] leading-[20px] lg:leading-[30px] font-normal`;
- }
- };
-
- const updateSlide = (type: string, value: string) => {
- switch (type) {
- case "title": {
- dispatch(updateSlideTitle({ index: slideIndex, title: value }));
- break;
- }
- case "heading": {
- dispatch(
- updateSlideBodyHeading({
- index: slideIndex,
- bodyIdx: bodyIdx,
- heading: value,
- })
- );
- break;
- }
- case "description": {
- dispatch(
- updateSlideDescription({ index: slideIndex, description: value })
- );
- break;
- }
- case "heading-description": {
- dispatch(
- updateSlideBodyDescription({
- index: slideIndex,
- bodyIdx: bodyIdx,
- description: value,
- })
- );
- break;
- }
- case "description-body": {
- dispatch(updateSlideBodyString({ index: slideIndex, body: value }));
- break;
- }
- case "info-heading": {
- dispatch(
- updateInfographicsTitle({
- slideIndex: slideIndex,
- itemIdx: bodyIdx,
- title: value,
- })
- );
- break;
- }
- case "info-description": {
- dispatch(
- updateInfographicsDescription({
- slideIndex: slideIndex,
- itemIdx: bodyIdx,
- description: value,
- })
- );
- break;
- }
- default:
- break;
- }
- };
const editor = useEditor({
extensions: [StarterKit, Markdown, Underline],
-
content: content,
editorProps: {
attributes: {
class: "outline-none transition-all duration-200",
},
},
-
immediatelyRender: false,
});
@@ -190,18 +75,13 @@ const TipTapEditor = ({
{
const markdown = editor?.storage.markdown.getMarkdown();
- updateSlide(type, markdown || "");
+ console.log("🔍 markdown", markdown);
}}
- data-slide-element
- data-text-content={editor?.storage.markdown.getMarkdown()}
- data-is-align={isAlingCenter}
- data-slide-index={slideIndex}
- data-element-type="text"
- data-element-id={elementId}
+
editor={editor}
/>
diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx
new file mode 100644
index 00000000..ee140619
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx
@@ -0,0 +1,129 @@
+"use client";
+
+import React, { useEffect } from 'react';
+import { useEditor, EditorContent, BubbleMenu } from "@tiptap/react";
+import StarterKit from "@tiptap/starter-kit";
+import { Markdown } from "tiptap-markdown";
+import Underline from "@tiptap/extension-underline";
+import {
+ Bold,
+ Italic,
+ Underline as UnderlinedIcon,
+ Strikethrough,
+ Code,
+} from "lucide-react";
+
+interface TiptapTextProps {
+ content: string;
+ onContentChange?: (content: string) => void;
+ className?: string;
+ placeholder?: string;
+ tag?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span' | 'div';
+ disabled?: boolean;
+}
+
+const TiptapText: React.FC = ({
+ content,
+ onContentChange,
+ className = "",
+ placeholder = "Enter text...",
+ tag = 'div',
+ disabled = false
+}) => {
+ const editor = useEditor({
+ extensions: [StarterKit, Markdown, Underline],
+ content: content || placeholder,
+ editorProps: {
+ attributes: {
+ class: `outline-none focus:outline-none transition-all duration-200 ${className}`,
+ 'data-placeholder': placeholder,
+ },
+ },
+ onBlur: ({ editor }) => {
+ const text = editor.getText();
+ if (onContentChange) {
+ onContentChange(text);
+ }
+ },
+ editable: !disabled,
+ immediatelyRender: false,
+ });
+
+ // Update editor content when content prop changes
+ useEffect(() => {
+ if (editor && content !== editor.getText()) {
+ editor.commands.setContent(content || placeholder);
+ }
+ }, [content, editor, placeholder]);
+
+ if (!editor) {
+ return {content || placeholder}
;
+ }
+
+ return (
+
+ {!disabled && (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default TiptapText;
\ No newline at end of file
diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx
new file mode 100644
index 00000000..12dd0c09
--- /dev/null
+++ b/servers/nextjs/app/(presentation-generator)/components/TiptapTextReplacer.tsx
@@ -0,0 +1,220 @@
+"use client";
+import { renderToStaticMarkup } from 'react-dom/server';
+
+import React, { useRef, useEffect, useState, ReactNode } from 'react';
+import ReactDOM from 'react-dom/client';
+import TiptapText from './TiptapText';
+
+interface TiptapTextReplacerProps {
+ layout: React.ComponentType<{
+ data: any;
+ }>;
+ children: ReactNode;
+ slideData?: any;
+ onContentChange?: (content: string, path: string) => void;
+ isEditMode?: boolean;
+}
+
+const TiptapTextReplacer: React.FC = ({
+ children,
+ slideData,
+ layout,
+ onContentChange = () => { },
+ isEditMode = true
+}) => {
+ const containerRef = useRef(null);
+ const [processedElements, setProcessedElements] = useState(new Set());
+
+
+
+ useEffect(() => {
+ if (!isEditMode || !containerRef.current) return;
+
+ const container = containerRef.current;
+
+ const replaceTextElements = () => {
+ // Get all elements in the container
+ const allElements = container.querySelectorAll('*');
+
+ allElements.forEach((element) => {
+ const htmlElement = element as HTMLElement;
+
+ // Skip if already processed
+ if (processedElements.has(htmlElement) ||
+ htmlElement.classList.contains('tiptap-text-editor') ||
+ htmlElement.closest('.tiptap-text-editor')) {
+ return;
+ }
+
+ // Get direct text content (not from child elements)
+ const directTextContent = getDirectTextContent(htmlElement);
+ const trimmedText = directTextContent.trim();
+
+ // Check if element has meaningful text content
+ if (!trimmedText || trimmedText.length < 2) return;
+
+ // Skip elements that contain other elements with text (to avoid double processing)
+ if (hasTextChildren(htmlElement)) return;
+
+ // Skip certain element types that shouldn't be editable
+ if (shouldSkipElement(htmlElement)) return;
+
+ console.log('Making element editable:', trimmedText, htmlElement);
+
+ // Get all computed styles to preserve them
+ const computedStyles = window.getComputedStyle(htmlElement);
+ const preservedStyles = {
+ fontSize: computedStyles.fontSize,
+ fontWeight: computedStyles.fontWeight,
+ fontFamily: computedStyles.fontFamily,
+ color: computedStyles.color,
+ lineHeight: computedStyles.lineHeight,
+ textAlign: computedStyles.textAlign,
+ marginTop: computedStyles.marginTop,
+ marginBottom: computedStyles.marginBottom,
+ marginLeft: computedStyles.marginLeft,
+ marginRight: computedStyles.marginRight,
+ paddingTop: computedStyles.paddingTop,
+ paddingBottom: computedStyles.paddingBottom,
+ paddingLeft: computedStyles.paddingLeft,
+ paddingRight: computedStyles.paddingRight,
+ };
+
+ // Try to find matching data path
+ const dataPath = findDataPath(slideData, trimmedText);
+
+ // Create a container for the TiptapText
+ const tiptapContainer = document.createElement('div');
+ tiptapContainer.className = htmlElement.className;
+
+ // Apply preserved styles
+ Object.entries(preservedStyles).forEach(([property, value]) => {
+ if (value && value !== 'auto') {
+ tiptapContainer.style.setProperty(
+ property.replace(/([A-Z])/g, '-$1').toLowerCase(),
+ value
+ );
+ }
+ });
+
+ // 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);
+ }
+ }}
+ placeholder="Enter text..."
+ disabled={!isEditMode}
+ />
+ );
+ });
+ };
+
+ // Helper function to get only direct text content (not from children)
+ const getDirectTextContent = (element: HTMLElement): string => {
+ let text = '';
+ const childNodes = Array.from(element.childNodes);
+ for (const node of childNodes) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ text += node.textContent || '';
+ }
+ }
+ return text;
+ };
+
+ // Helper function to check if element has child elements with text
+ const hasTextChildren = (element: HTMLElement): boolean => {
+ const children = Array.from(element.children) as HTMLElement[];
+ return children.some(child => {
+ const childText = getDirectTextContent(child).trim();
+ return childText.length > 1;
+ });
+ };
+
+ // Helper function to determine if element should be skipped
+ const shouldSkipElement = (element: HTMLElement): boolean => {
+ // Skip form elements
+ if (['INPUT', 'TEXTAREA', 'SELECT', 'BUTTON'].includes(element.tagName)) {
+ return true;
+ }
+
+ // Skip elements with certain roles or types
+ if (element.hasAttribute('role') ||
+ element.hasAttribute('aria-label') ||
+ element.hasAttribute('data-testid')) {
+ return true;
+ }
+
+ // Skip elements that contain interactive content
+ if (element.querySelector('img, svg, button, input, textarea, select, a[href]')) {
+ return true;
+ }
+
+ // Skip container elements (elements that primarily serve as layout containers)
+ const containerClasses = ['grid', 'flex', 'space-', 'gap-', 'container', 'wrapper'];
+ const hasContainerClass = containerClasses.some(cls =>
+ element.className.includes(cls)
+ );
+ if (hasContainerClass) return true;
+
+ // Skip very short text that might be UI elements
+ const text = getDirectTextContent(element).trim();
+ if (text.length < 2) return true;
+
+ // Skip elements that look like numbers or single characters (might be icons/UI)
+ if (/^[0-9]+$/.test(text) || text.length === 1) return true;
+
+ return false;
+ };
+
+ // Helper function to find data path for text content
+ const findDataPath = (data: any, targetText: string, path = ''): string => {
+ if (!data || typeof data !== 'object') return '';
+
+ for (const [key, value] of Object.entries(data)) {
+ const currentPath = path ? `${path}.${key}` : key;
+
+ if (typeof value === 'string' && value.trim() === targetText.trim()) {
+ return currentPath;
+ }
+
+ if (Array.isArray(value)) {
+ for (let i = 0; i < value.length; i++) {
+ const result = findDataPath(value[i], targetText, `${currentPath}[${i}]`);
+ if (result) return result;
+ }
+ } else if (typeof value === 'object' && value !== null) {
+ const result = findDataPath(value, targetText, currentPath);
+ if (result) return result;
+ }
+ }
+
+ return '';
+ };
+
+ // Replace text elements after a short delay to ensure DOM is ready
+ const timer = setTimeout(replaceTextElements, 500);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }, [slideData, isEditMode]);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default TiptapTextReplacer;
\ 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..2dd965a2 100644
--- a/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
+++ b/servers/nextjs/app/(presentation-generator)/hooks/useGroupLayouts.tsx
@@ -1,6 +1,8 @@
'use client'
import React, { useMemo } from 'react';
import { useLayout } from '../context/LayoutContext';
+import { SmartEditableProvider } from '../components/SmartEditableWrapper';
+import TiptapTextReplacer from '../components/TiptapTextReplacer';
export const useGroupLayouts = () => {
const {
@@ -28,9 +30,9 @@ export const useGroupLayouts = () => {
};
}, [getLayoutsByGroup]);
- // Render slide content with group validation
+ // Render slide content with group validation and automatic Tiptap text editing
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 +43,29 @@ export const useGroupLayouts = () => {
);
}
+
+ if (isEditMode) {
+ return (
+
+ {
+ console.log(`Text content changed at ${dataPath}:`, content);
+
+ }}
+ >
+
+
+
+ );
+ }
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..829aea72 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, isStreaming ? false : true); // Enable edit mode for main content
+ }, [renderSlideContent, slide, isStreaming]);
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)}
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts
index ce23fe7c..38710068 100644
--- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts
+++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts
@@ -1,6 +1,5 @@
import { useCallback } from "react";
import { useDispatch } from "react-redux";
-import { useRouter } from "next/navigation";
import { toast } from "@/hooks/use-toast";
import { DashboardApi } from "@/app/dashboard/api/dashboard";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
@@ -12,11 +11,12 @@ export const usePresentationData = (
setError: (error: boolean) => void
) => {
const dispatch = useDispatch();
- const router = useRouter();
+
const fetchUserSlides = useCallback(async () => {
try {
const data = await DashboardApi.getPresentation(presentationId);
+ console.log('Presentation Data',data);
if (data) {
dispatch(setPresentationData(data));
setLoading(false);
diff --git a/servers/nextjs/app/api/upload-image/route.ts b/servers/nextjs/app/api/upload-image/route.ts
index c354ec7c..af58449d 100644
--- a/servers/nextjs/app/api/upload-image/route.ts
+++ b/servers/nextjs/app/api/upload-image/route.ts
@@ -35,7 +35,7 @@ export async function POST(request: NextRequest) {
// Return the relative path that can be used in the frontend
return NextResponse.json({
success: true,
- filePath: `/app/user_data/uploads/${filename}`
+ filePath: `${userDataDir}/uploads/${filename}`
});
} catch (error) {
console.error("Error saving image:", error);
diff --git a/servers/nextjs/components/ui/chart.tsx b/servers/nextjs/components/ui/chart.tsx
new file mode 100644
index 00000000..39fba6d6
--- /dev/null
+++ b/servers/nextjs/components/ui/chart.tsx
@@ -0,0 +1,365 @@
+"use client"
+
+import * as React from "react"
+import * as RechartsPrimitive from "recharts"
+
+import { cn } from "@/lib/utils"
+
+// Format: { THEME_NAME: CSS_SELECTOR }
+const THEMES = { light: "", dark: ".dark" } as const
+
+export type ChartConfig = {
+ [k in string]: {
+ label?: React.ReactNode
+ icon?: React.ComponentType
+ } & (
+ | { color?: string; theme?: never }
+ | { color?: never; theme: Record }
+ )
+}
+
+type ChartContextProps = {
+ config: ChartConfig
+}
+
+const ChartContext = React.createContext(null)
+
+function useChart() {
+ const context = React.useContext(ChartContext)
+
+ if (!context) {
+ throw new Error("useChart must be used within a ")
+ }
+
+ return context
+}
+
+const ChartContainer = React.forwardRef<
+ HTMLDivElement,
+ React.ComponentProps<"div"> & {
+ config: ChartConfig
+ children: React.ComponentProps<
+ typeof RechartsPrimitive.ResponsiveContainer
+ >["children"]
+ }
+>(({ id, className, children, config, ...props }, ref) => {
+ const uniqueId = React.useId()
+ const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
+
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+})
+ChartContainer.displayName = "Chart"
+
+const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
+ const colorConfig = Object.entries(config).filter(
+ ([, config]) => config.theme || config.color
+ )
+
+ if (!colorConfig.length) {
+ return null
+ }
+
+ return (
+