diff --git a/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx b/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx index 730c98d8..30781d00 100644 --- a/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/SmartEditableWrapper.tsx @@ -61,7 +61,6 @@ export const SmartEditableProvider: React.FC = ({ 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 = '') => { @@ -69,7 +68,6 @@ export const SmartEditableProvider: React.FC = ({ // 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({ @@ -83,13 +81,11 @@ export const SmartEditableProvider: React.FC = ({ 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({ @@ -106,7 +102,6 @@ export const SmartEditableProvider: React.FC = ({ icon_prompt: data.__icon_query__ ? [data.__icon_query__] : [] } }); - console.log(`✅ Matched icon to DOM element:`, imgElement); } } // Recursively scan nested objects and arrays @@ -125,7 +120,6 @@ export const SmartEditableProvider: React.FC = ({ }; detectEditableElementsFromData(slideData); - console.log('🎉 Final detected elements:', elements); setEditableElements(elements); }; diff --git a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx index ee140619..aebfdfbf 100644 --- a/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/TiptapText.tsx @@ -61,10 +61,10 @@ const TiptapText: React.FC = ({ } return ( -
+
{!disabled && ( -
+
); } - return (
- {/* Auto save loading indicator */} - {autoSaveLoading && ( -
- -
- )} + +
+ {isSaving && ( + + )} + +
diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/index.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/index.ts index c416e2b9..9191e5d5 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/index.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/index.ts @@ -1,3 +1,4 @@ export { usePresentationStreaming } from './usePresentationStreaming'; export { usePresentationData } from './usePresentationData'; -export { usePresentationNavigation } from './usePresentationNavigation'; \ No newline at end of file +export { usePresentationNavigation } from './usePresentationNavigation'; +export { useAutoSave } from './useAutoSave'; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx new file mode 100644 index 00000000..596e2405 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/useAutoSave.tsx @@ -0,0 +1,80 @@ +'use client' +import { useEffect, useRef, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { RootState } from '@/store/store'; +import { PresentationGenerationApi } from '../../services/api/presentation-generation'; + +interface UseAutoSaveOptions { + debounceMs?: number; + enabled?: boolean; +} + +export const useAutoSave = ({ + debounceMs = 2000, + enabled = true, +}: UseAutoSaveOptions = {}) => { + const { presentationData } = useSelector( + (state: RootState) => state.presentationGeneration + ); + + const saveTimeoutRef = useRef(null); + const lastSavedDataRef = useRef(''); + const [isSaving, setIsSaving] = useState(false); + + // Debounced save function + const debouncedSave = useCallback(async (data: any) => { + // Clear existing timeout + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + + // Set new timeout + saveTimeoutRef.current = setTimeout(async () => { + if (!data || isSaving) return; + + const currentDataString = JSON.stringify(data); + + // Skip if data hasn't changed since last save + if (currentDataString === lastSavedDataRef.current) { + return; + } + + try { + setIsSaving(true); + console.log('🔄 Auto-saving presentation data...'); + + // Call the API to update presentation content + await PresentationGenerationApi.updatePresentationContent(data); + + // Update last saved data reference + lastSavedDataRef.current = currentDataString; + + console.log('✅ Auto-save successful'); + + } catch (error) { + console.error('❌ Auto-save failed:', error); + } finally { + setIsSaving(false); + } + }, debounceMs); + }, [debounceMs, isSaving]); + + // Effect to trigger auto-save when presentation data changes + useEffect(() => { + if (!enabled || !presentationData) return; + + // Trigger debounced save + debouncedSave(presentationData); + + // Cleanup timeout on unmount + return () => { + if (saveTimeoutRef.current) { + clearTimeout(saveTimeoutRef.current); + } + }; + }, [presentationData, enabled, debouncedSave]); + + return { + isSaving, + }; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts index 38710068..4c7681f9 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationData.ts @@ -16,7 +16,6 @@ export const usePresentationData = ( 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/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts index ad03efdd..66b7c1b1 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts @@ -1,6 +1,5 @@ import { useEffect, useRef } from "react"; import { useDispatch } from "react-redux"; -import { toast } from "@/hooks/use-toast"; import { setPresentationData, setStreaming } from "@/store/slices/presentationGeneration"; import { jsonrepair } from "jsonrepair"; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts b/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts deleted file mode 100644 index 93c2a725..00000000 --- a/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useCallback, useRef } from "react"; - -export function useDebounce void>( - callback: T, - delay: number -) { - const timeoutRef = useRef(); - - return useCallback( - (...args: Parameters) => { - if (timeoutRef.current) { - clearTimeout(timeoutRef.current); - } - - timeoutRef.current = setTimeout(() => { - callback(...args); - }, delay); - }, - [callback, delay] - ); -} \ 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 187808ea..6a53e489 100644 --- a/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts +++ b/servers/nextjs/app/(presentation-generator)/services/api/presentation-generation.ts @@ -3,25 +3,6 @@ import { IconSearch, ImageGenerate, ImageSearch } from "./params"; export class PresentationGenerationApi { - static async getChapterDetails() { - try { - const response = await fetch( - `/api/v1/ppt/chapter-details`, - { - method: "GET", - headers: getHeader(), - cache: "no-cache", - } - ); - if (response.status === 200) { - const data = await response.json(); - return data; - } - } catch (error) { - console.error("Error getting chapter details:", error); - throw error; - } - } static async uploadDoc(documents: File[]) { const formData = new FormData(); @@ -80,62 +61,9 @@ export class PresentationGenerationApi { throw error; } } - static async titleGeneration({ - presentation_id, - }: { - presentation_id: string; - }) { - try { - const response = await fetch( - `/api/v1/ppt/presentation/outlines/generate`, - { - method: "POST", - headers: getHeader(), - body: JSON.stringify({ - prompt: prompt, - presentation_id: presentation_id, - }), - cache: "no-cache", - } - ); - if (response.status === 200) { - const data = await response.json(); + - return data; - } else { - throw new Error(`Failed to generate titles: ${response.statusText}`); - } - } catch (error) { - console.error("error in title generation", error); - throw error; - } - } - - static async generatePresentation(presentationData: any) { - try { - const response = await fetch( - `/api/v1/ppt/generate`, - { - method: "POST", - headers: getHeader(), - body: JSON.stringify(presentationData), - cache: "no-cache", - } - ); - if (response.status === 200) { - const data = await response.json(); - - return data; - } else { - throw new Error( - `Failed to generate presentation: ${response.statusText}` - ); - } - } catch (error) { - console.error("error in presentation generation", error); - throw error; - } - } + static async editSlide( presentation_id: string, index: number, @@ -172,9 +100,9 @@ export class PresentationGenerationApi { static async updatePresentationContent(body: any) { try { const response = await fetch( - `/api/v1/ppt/slides/update`, + `/api/v1/ppt/presentation/update`, { - method: "POST", + method: "PUT", headers: getHeader(), body: JSON.stringify(body), cache: "no-cache", @@ -375,33 +303,7 @@ export class PresentationGenerationApi { throw error; } } - // SET THEME COLORS - static async setThemeColors(presentation_id: string, theme: any) { - try { - const response = await fetch( - `/api/v1/ppt/presentation/theme`, - { - method: "POST", - headers: getHeader(), - body: JSON.stringify({ - presentation_id, - theme, - }), - - } - ); - if (response.ok) { - const data = await response.json(); - return data; - } else { - throw new Error(`Failed to set theme colors: ${response.statusText}`); - } - } catch (error) { - console.error("error in theme colors set", error); - throw error; - } - } - // QUESTIONS + static async createPresentation({ prompt, diff --git a/servers/nextjs/store/slices/presentationGeneration.ts b/servers/nextjs/store/slices/presentationGeneration.ts index b65202c1..8223182d 100644 --- a/servers/nextjs/store/slices/presentationGeneration.ts +++ b/servers/nextjs/store/slices/presentationGeneration.ts @@ -1,40 +1,14 @@ import { Slide } from "@/app/(presentation-generator)/types/slide"; import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -interface Series { - data: number[]; - name?: string; -} -interface DataLabel { - dataLabelPosition: "Outside" | "Inside"; - dataLabelAlignment: "Base" | "Center" | "End"; -} -export interface ChartSettings { - showLegend: boolean; - showGrid: boolean; - showAxisLabel: boolean; - showDataLabel: boolean; - dataLabel: DataLabel; -} + export interface SlideOutline { title?: string; body?: string; } -export interface Chart { - id: string; - name: string; - type: string; - style: ChartSettings | {} | null; - unit?: string | null; - presentation: string; - postfix: string; - data: { - categories: string[]; - series: Series[]; - }; -} + export interface PresentationData { id: string; language: string; @@ -50,20 +24,18 @@ export interface PresentationData { interface PresentationGenerationState { presentation_id: string | null; - documents: string[]; - images: string[]; isLoading: boolean; isStreaming: boolean | null; outlines: SlideOutline[]; error: string | null; presentationData: PresentationData | null; + isSlidesRendered: boolean; } const initialState: PresentationGenerationState = { presentation_id: null, - documents: [], - images: [], outlines: [], + isSlidesRendered: false, isLoading: false, isStreaming: null, error: null, @@ -86,6 +58,10 @@ const presentationGenerationSlice = createSlice({ state.presentation_id = action.payload; state.error = null; }, + // Slides rendered + setSlidesRendered: (state, action: PayloadAction) => { + state.isSlidesRendered = action.payload; + }, // Error setError: (state, action: PayloadAction) => { state.error = action.payload; @@ -97,14 +73,6 @@ const presentationGenerationSlice = createSlice({ state.error = null; state.isLoading = false; }, - // Set documents - setDocs: (state, action: PayloadAction) => { - state.documents = action.payload; - }, - // Set images - setImgs: (state, action: PayloadAction) => { - state.images = action.payload; - }, // Set outlines setOutlines: (state, action: PayloadAction) => { state.outlines = action.payload; @@ -166,252 +134,61 @@ const presentationGenerationSlice = createSlice({ action.payload.slide; } }, - updateSlideVariant: ( + + // Update slide content at specific data path (for Tiptap text editing) + updateSlideContent: ( state, - action: PayloadAction<{ index: number; variant: number }> + action: PayloadAction<{ + slideIndex: number; + dataPath: string; + content: string; + }> ) => { if ( state.presentationData && - state.presentationData.slides[action.payload.index] + state.presentationData.slides && + state.presentationData.slides[action.payload.slideIndex] ) { - state.presentationData.slides[action.payload.index].design_index = - action.payload.variant; - } - }, - updateSlideTitle: ( - state, - action: PayloadAction<{ index: number; title: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[action.payload.index].content.title = - action.payload.title; - } - }, - updateSlideDescription: ( - state, - action: PayloadAction<{ index: number; description: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[ - action.payload.index - ].content.description = action.payload.description; - } - }, - updateSlideBodyString: ( - state, - action: PayloadAction<{ index: number; body: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[action.payload.index].content.body = - action.payload.body; - } - }, - updateSlideBodyHeading: ( - state, - action: PayloadAction<{ index: number; bodyIdx: number; heading: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[action.payload.index].content.body[ - action.payload.bodyIdx - // @ts-ignore - ].heading = action.payload.heading; - } - }, - updateSlideBodyDescription: ( - state, - action: PayloadAction<{ - index: number; - bodyIdx: number; - description: string; - }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[action.payload.index].content.body[ - action.payload.bodyIdx - // @ts-ignore - ].description = action.payload.description; - } - }, - updateSlideImage: ( - state, - action: PayloadAction<{ index: number; imageIdx: number; image: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.images) { - state.presentationData.slides[action.payload.index].images![ - action.payload.imageIdx - ] = action.payload.image; - } - }, - updateSlideIcon: ( - state, - action: PayloadAction<{ index: number; iconIdx: number; icon: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.icons) { - state.presentationData.slides[action.payload.index].icons![ - action.payload.iconIdx - ] = action.payload.icon; - } - }, - updateSlideChart: ( - state, - action: PayloadAction<{ index: number; chart: Chart }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - state.presentationData.slides[action.payload.index].content.graph = - action.payload.chart; - } - }, - updateSlideChartSettings: ( - state, - action: PayloadAction<{ index: number; chartSettings: ChartSettings }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - const defaultSettings: ChartSettings = { - showLegend: false, - showGrid: false, - showAxisLabel: true, - showDataLabel: true, - dataLabel: { - dataLabelPosition: "Outside", - dataLabelAlignment: "Center", - }, + const slide = state.presentationData.slides[action.payload.slideIndex]; + const { dataPath, content } = action.payload; + + // Helper function to set nested property value + const setNestedValue = (obj: any, path: string, value: string) => { + const keys = path.split(/[.\[\]]+/).filter(Boolean); + let current = obj; + + // Navigate to the parent object + for (let i = 0; i < keys.length - 1; i++) { + const key = keys[i]; + if (isNaN(Number(key))) { + // String key + if (!current[key]) { + current[key] = {}; + } + current = current[key]; + } else { + // Array index + const index = Number(key); + if (!current[index]) { + current[index] = {}; + } + current = current[index]; + } + } + + // Set the final value + const finalKey = keys[keys.length - 1]; + if (isNaN(Number(finalKey))) { + current[finalKey] = value; + } else { + current[Number(finalKey)] = value; + } }; - state.presentationData.slides[ - action.payload.index - ].content.graph.style = { - ...defaultSettings, - ...action.payload.chartSettings, - }; - } - }, - - addSlideBodyItem: ( - state, - action: PayloadAction<{ - index: number; - item: { heading: string; description: string }; - }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.content.body) { - // @ts-ignore - state.presentationData.slides[action.payload.index].content.body.push( - action.payload.item - ); - } - }, - addSlideImage: ( - state, - action: PayloadAction<{ index: number; image: string }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.images) { - state.presentationData.slides[action.payload.index].images!.push( - action.payload.image - ); - } - }, - deleteSlideImage: ( - state, - action: PayloadAction<{ index: number; imageIdx: number }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.images) { - state.presentationData.slides[action.payload.index].images!.splice( - action.payload.imageIdx, - 1 - ); - } - }, - updateSlideProperties: ( - state, - action: PayloadAction<{ index: number; itemIdx: number; properties: any }> - ) => { - if (state.presentationData?.slides[action.payload.index]) { - // Initialize properties object if it doesn't exist - if (!state.presentationData.slides[action.payload.index].properties) { - state.presentationData.slides[action.payload.index].properties = {}; + + // Update the slide content + if (dataPath && slide.content) { + setNestedValue(slide.content, dataPath, content); } - // Assign the properties to the specific item index - state.presentationData.slides[action.payload.index].properties[ - action.payload.itemIdx - ] = action.payload.properties; - } - }, - // Infographics - addInfographics: ( - state, - action: PayloadAction<{ slideIndex: number; item: any }> - ) => { - if (state.presentationData?.slides[action.payload.slideIndex]?.content) { - // @ts-ignore - state.presentationData.slides[ - action.payload.slideIndex - ].content.infographics.push(action.payload.item); - } - }, - deleteInfographics: ( - state, - action: PayloadAction<{ slideIndex: number; itemIdx: number }> - ) => { - if (state.presentationData?.slides[action.payload.slideIndex]?.content) { - // @ts-ignore - state.presentationData.slides[ - action.payload.slideIndex - ].content.infographics.splice(action.payload.itemIdx, 1); - } - }, - updateInfographicsTitle: ( - state, - action: PayloadAction<{ - slideIndex: number; - itemIdx: number; - title: string; - }> - ) => { - if (state.presentationData?.slides[action.payload.slideIndex]?.content) { - // @ts-ignore - state.presentationData.slides[ - action.payload.slideIndex - ].content.infographics[action.payload.itemIdx].title = - action.payload.title; - } - }, - updateInfographicsDescription: ( - state, - action: PayloadAction<{ - slideIndex: number; - itemIdx: number; - description: string; - }> - ) => { - if (state.presentationData?.slides[action.payload.slideIndex]?.content) { - // @ts-ignore - state.presentationData.slides[ - action.payload.slideIndex - ].content.infographics[action.payload.itemIdx].description = - action.payload.description; - } - }, - updateInfographicsChart: ( - state, - action: PayloadAction<{ slideIndex: number; itemIdx: number; chart: any }> - ) => { - if (state.presentationData?.slides[action.payload.slideIndex]?.content) { - // @ts-ignore - state.presentationData.slides[ - action.payload.slideIndex - ].content.infographics[action.payload.itemIdx].chart = - action.payload.chart; - } - }, - deleteSlideBodyItem: ( - state, - action: PayloadAction<{ index: number; itemIdx: number }> - ) => { - if (state.presentationData?.slides[action.payload.index]?.content.body) { - // @ts-ignore - state.presentationData.slides[action.payload.index].content.body.splice( - action.payload.itemIdx, - 1 - ); } }, }, @@ -421,39 +198,17 @@ export const { setStreaming, setLoading, setPresentationId, + setSlidesRendered, setError, clearPresentationData, - setDocs, - setImgs, - deleteSlideOutline, setPresentationData, setOutlines, // slides operations addSlide, updateSlide, - updateSlideVariant, - updateSlideChart, - updateSlideChartSettings, - updateSlideTitle, - updateSlideDescription, - updateSlideBodyString, - updateSlideBodyHeading, - updateSlideBodyDescription, - updateSlideImage, - updateSlideIcon, deletePresentationSlide, - addSlideBodyItem, - addSlideImage, - deleteSlideImage, - deleteSlideBodyItem, - updateSlideProperties, - // infographics - addInfographics, - deleteInfographics, - updateInfographicsTitle, - updateInfographicsDescription, - updateInfographicsChart, + updateSlideContent, } = presentationGenerationSlice.actions; export default presentationGenerationSlice.reducer;