Merge pull request #94 from presenton/feat/slide_state_handling
feat/slide state handling
This commit is contained in:
commit
ffeded7045
13 changed files with 247 additions and 482 deletions
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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" : ""
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
export { usePresentationStreaming } from './usePresentationStreaming';
|
||||
export { usePresentationData } from './usePresentationData';
|
||||
export { usePresentationNavigation } from './usePresentationNavigation';
|
||||
export { usePresentationNavigation } from './usePresentationNavigation';
|
||||
export { useAutoSave } from './useAutoSave';
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue