feat(Nextjs): Text saving to store & Auto saving

This commit is contained in:
shiva raj badu 2025-07-19 01:07:44 +05:45
parent e57b21838a
commit 1e9d210954
No known key found for this signature in database
12 changed files with 190 additions and 473 deletions

View file

@ -61,7 +61,6 @@ export const SmartEditableProvider: React.FC<SmartEditableProviderProps> = ({
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<SmartEditableProviderProps> = ({
// 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<SmartEditableProviderProps> = ({
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<SmartEditableProviderProps> = ({
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<SmartEditableProviderProps> = ({
};
detectEditableElementsFromData(slideData);
console.log('🎉 Final detected elements:', elements);
setEditableElements(elements);
};

View file

@ -10,7 +10,8 @@ interface TiptapTextReplacerProps {
}>;
children: ReactNode;
slideData?: any;
onContentChange?: (content: string, path: string) => void;
slideIndex?: number;
onContentChange?: (content: string, path: string, slideIndex?: number) => void;
isEditMode?: boolean;
}
@ -18,6 +19,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
children,
slideData,
layout,
slideIndex,
onContentChange = () => { },
isEditMode = true
}) => {
@ -109,7 +111,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
content={trimmedText}
onContentChange={(content: string) => {
if (dataPath && onContentChange) {
onContentChange(content, dataPath);
onContentChange(content, dataPath, slideIndex);
}
}}
placeholder="Enter text..."
@ -257,7 +259,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
return () => {
clearTimeout(timer);
};
}, [slideData, isEditMode]);
}, [slideData, isEditMode, slideIndex]);
return (
<div ref={containerRef} className="tiptap-text-replacer">

View file

@ -1,10 +1,13 @@
'use client'
import React, { useMemo } from 'react';
import { useDispatch } from 'react-redux';
import { useLayout } from '../context/LayoutContext';
import { SmartEditableProvider } from '../components/SmartEditableWrapper';
import TiptapTextReplacer from '../components/TiptapTextReplacer';
import { updateSlideContent } from '../../../store/slices/presentationGeneration';
export const useGroupLayouts = () => {
const dispatch = useDispatch();
const {
getLayoutByIdAndGroup,
getLayoutsByGroup,
@ -53,11 +56,20 @@ export const useGroupLayouts = () => {
>
<TiptapTextReplacer
slideData={slide.content}
slideIndex={slide.index}
isEditMode={isEditMode}
layout={Layout}
onContentChange={(content: string, dataPath: string) => {
console.log(`Text content changed at ${dataPath}:`, content);
onContentChange={(content: string, dataPath: string, slideIndex?: number) => {
console.log(`Text content changed at slide ${slideIndex}, path ${dataPath}:`, content);
// Dispatch Redux action to update slide content
if (dataPath && slideIndex !== undefined) {
dispatch(updateSlideContent({
slideIndex: slideIndex,
dataPath: dataPath,
content: content
}));
}
}}
>
<Layout data={slide.content} />
@ -67,7 +79,7 @@ export const useGroupLayouts = () => {
}
return <Layout data={slide.content} />;
};
}, [getGroupLayout]);
}, [getGroupLayout, dispatch]);
return {
getGroupLayout,

View file

@ -47,7 +47,6 @@ import Modal from "./Modal";
import Announcement from "@/components/Announcement";
import { getFontLink, getStaticFileUrl } from "../../utils/others";
import JSPowerPointExtractor from "../../components/JSPowerPointExtractor";
const Header = ({
@ -108,13 +107,7 @@ const Header = ({
themeColors.slideBox
);
// Save in background
await PresentationGenerationApi.setThemeColors(presentation_id, {
name: themeType,
colors: {
...themeColors,
},
});
} catch (error) {
console.error("Failed to update theme:", error);
toast({

View file

@ -8,14 +8,14 @@ import SidePanel from "../components/SidePanel";
import SlideContent from "../components/SlideContent";
import LoadingState from "../../components/LoadingState";
import Header from "../components/Header";
import { Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { AlertCircle } from "lucide-react";
import { AlertCircle, Loader2 } from "lucide-react";
import Help from "./Help";
import {
usePresentationStreaming,
usePresentationData,
usePresentationNavigation
usePresentationNavigation,
useAutoSave
} from "../hooks";
import { PresentationPageProps } from "../types";
@ -26,7 +26,6 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
const [isFullscreen, setIsFullscreen] = useState(false);
const [error, setError] = useState(false);
const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false);
const [autoSaveLoading, setAutoSaveLoading] = useState(false);
// Redux state
const { currentTheme, currentColors } = useSelector(
@ -36,13 +35,19 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
(state: RootState) => state.presentationGeneration
);
// Auto-save functionality
const { isSaving } = useAutoSave({
debounceMs: 2000,
enabled: !!presentationData && !isStreaming,
});
// Custom hooks
const { fetchUserSlides, handleDeleteSlide } = usePresentationData(
presentation_id,
setLoading,
setError
);
const {
isPresentMode,
stream,
@ -98,33 +103,29 @@ const PresentationPage: React.FC<PresentationPageProps> = ({ presentation_id })
role="alert"
>
<AlertCircle className="w-16 h-16 mb-4 text-red-500" />
<strong className="font-bold text-4xl mb-2">Oops!</strong>
<p className="block text-2xl py-2">
We encountered an issue loading your presentation.
<h2 className="text-xl font-semibold mb-2">
Something went wrong
</h2>
<p className="text-center mb-4">
We couldn't load your presentation. Please try again.
</p>
<p className="text-lg py-2">
Please check your internet connection or try again later.
</p>
<Button
className="mt-4 bg-red-500 text-white hover:bg-red-600 focus:ring-4 focus:ring-red-300"
onClick={() => window.location.reload()}
>
Retry
<Button onClick={() => window.location.reload()}>
Refresh Page
</Button>
</div>
</div>
);
}
return (
<div className="h-screen flex overflow-hidden flex-col">
{/* Auto save loading indicator */}
{autoSaveLoading && (
<div className="fixed right-6 top-[5.2rem] z-50 bg-white bg-opacity-50 flex items-center justify-center">
<Loader2 className="animate-spin text-primary" />
</div>
)}
<div className="fixed right-6 top-[5.2rem] z-50">
{isSaving && (
<Loader2 className="w-6 h-6 animate-spin text-blue-500" />
)}
</div>
<Header presentation_id={presentation_id} currentSlide={currentSlide} />
<Help />

View file

@ -1,3 +1,4 @@
export { usePresentationStreaming } from './usePresentationStreaming';
export { usePresentationData } from './usePresentationData';
export { usePresentationNavigation } from './usePresentationNavigation';
export { usePresentationNavigation } from './usePresentationNavigation';
export { useAutoSave } from './useAutoSave';

View file

@ -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<NodeJS.Timeout | null>(null);
const lastSavedDataRef = useRef<string>('');
const [isSaving, setIsSaving] = useState<boolean>(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,
};
};

View file

@ -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);

View file

@ -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";

View file

@ -1,21 +0,0 @@
import { useCallback, useRef } from "react";
export function useDebounce<T extends (...args: any[]) => void>(
callback: T,
delay: number
) {
const timeoutRef = useRef<NodeJS.Timeout>();
return useCallback(
(...args: Parameters<T>) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
callback(...args);
}, delay);
},
[callback, delay]
);
}

View file

@ -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,

View file

@ -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<boolean>) => {
state.isSlidesRendered = action.payload;
},
// Error
setError: (state, action: PayloadAction<string>) => {
state.error = action.payload;
@ -97,14 +73,6 @@ const presentationGenerationSlice = createSlice({
state.error = null;
state.isLoading = false;
},
// Set documents
setDocs: (state, action: PayloadAction<string[]>) => {
state.documents = action.payload;
},
// Set images
setImgs: (state, action: PayloadAction<string[]>) => {
state.images = action.payload;
},
// Set outlines
setOutlines: (state, action: PayloadAction<SlideOutline[]>) => {
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;