Merge pull request #94 from presenton/feat/slide_state_handling

feat/slide state handling
This commit is contained in:
Shiva Raj Badu 2025-07-19 01:09:12 +05:45 committed by GitHub
commit ffeded7045
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 247 additions and 482 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

@ -61,10 +61,10 @@ const TiptapText: React.FC<TiptapTextProps> = ({
}
return (
<div className="relative w-full">
<div className="relative z-50 w-full">
{!disabled && (
<BubbleMenu editor={editor} tippyOptions={{ duration: 100 }}>
<div className="flex bg-white rounded-lg shadow-lg p-2 gap-1 border border-gray-200 z-50">
<div className="flex bg-white rounded-lg shadow-lg p-2 gap-1 border border-gray-200 z-50">
<button
onClick={() => editor?.chain().focus().toggleBold().run()}
className={`p-1 rounded hover:bg-gray-100 transition-colors ${editor?.isActive("bold") ? "bg-blue-100 text-blue-600" : ""

View file

@ -1,5 +1,4 @@
"use client";
import { renderToStaticMarkup } from 'react-dom/server';
import React, { useRef, useEffect, useState, ReactNode } from 'react';
import ReactDOM from 'react-dom/client';
@ -11,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;
}
@ -19,14 +19,12 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
children,
slideData,
layout,
slideIndex,
onContentChange = () => { },
isEditMode = true
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const [processedElements, setProcessedElements] = useState(new Set<HTMLElement>());
useEffect(() => {
if (!isEditMode || !containerRef.current) return;
@ -46,6 +44,9 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
return;
}
// Skip if element is inside an ignored element tree
if (isInIgnoredElementTree(htmlElement)) return;
// Get direct text content (not from child elements)
const directTextContent = getDirectTextContent(htmlElement);
const trimmedText = directTextContent.trim();
@ -110,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..."
@ -120,6 +121,56 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
});
};
// Function to check if element is inside an ignored element tree
const isInIgnoredElementTree = (element: HTMLElement): boolean => {
// List of element types that should be ignored entirely with all their children
const ignoredElementTypes = [
'TABLE', 'TBODY', 'THEAD', 'TFOOT', 'TR', 'TD', 'TH', // Table elements
'SVG', 'G', 'PATH', 'CIRCLE', 'RECT', 'LINE', // SVG elements
'CANVAS', // Canvas element
'VIDEO', 'AUDIO', // Media elements
'IFRAME', 'EMBED', 'OBJECT', // Embedded content
'SELECT', 'OPTION', 'OPTGROUP', // Select dropdown elements
'SCRIPT', 'STYLE', 'NOSCRIPT', // Script/style elements
];
// List of class patterns that indicate ignored element trees
const ignoredClassPatterns = [
'chart', 'graph', 'visualization', // Chart/graph components
'menu', 'dropdown', 'tooltip', // UI components
'editor', 'wysiwyg', // Editor components
'calendar', 'datepicker', // Date picker components
'slider', 'carousel', // Interactive components
];
// Check if current element or any parent is in ignored list
let currentElement: HTMLElement | null = element;
while (currentElement) {
// Check element type
if (ignoredElementTypes.includes(currentElement.tagName)) {
return true;
}
// Check class patterns
const className = currentElement.className.length > 0 ? currentElement.className.toLowerCase() : '';
if (ignoredClassPatterns.some(pattern => className.includes(pattern))) {
return true;
}
// Check for specific attributes that indicate non-text content
if (currentElement.hasAttribute('contenteditable') ||
currentElement.hasAttribute('data-chart') ||
currentElement.hasAttribute('data-visualization') ||
currentElement.hasAttribute('data-interactive')) {
return true;
}
currentElement = currentElement.parentElement;
}
return false;
};
// Helper function to get only direct text content (not from children)
const getDirectTextContent = (element: HTMLElement): string => {
let text = '';
@ -155,7 +206,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
return true;
}
// Skip elements that contain interactive content
// Skip elements that contain interactive content (simplified since we now use isInIgnoredElementTree)
if (element.querySelector('img, svg, button, input, textarea, select, a[href]')) {
return true;
}
@ -163,7 +214,7 @@ const TiptapTextReplacer: React.FC<TiptapTextReplacerProps> = ({
// 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)
element.className.length > 0 ? element.className.includes(cls) : false
);
if (hasContainerClass) return true;
@ -208,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,
@ -43,7 +46,6 @@ export const useGroupLayouts = () => {
</div>
);
}
if (isEditMode) {
return (
<SmartEditableProvider
@ -54,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} />
@ -68,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;