diff --git a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx index 4d4fdd1b..8ee17eec 100644 --- a/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx +++ b/servers/nextjs/app/(presentation-generator)/context/LayoutContext.tsx @@ -4,7 +4,7 @@ import dynamic from 'next/dynamic'; import { toast } from "@/hooks/use-toast"; import * as z from 'zod'; -interface LayoutInfo { +export interface LayoutInfo { id: string; name?: string; description?: string; @@ -12,19 +12,19 @@ interface LayoutInfo { groupName: string; } -interface GroupSetting { +export interface GroupSetting { description: string; ordered: boolean; isDefault?: boolean; } -interface GroupedLayoutsResponse { +export interface GroupedLayoutsResponse { groupName: string; files: string[]; settings: GroupSetting | null; } -interface LayoutData { +export interface LayoutData { layoutsById: Map; layoutsByGroup: Map>; groupSettings: Map; @@ -33,7 +33,7 @@ interface LayoutData { layoutSchema: LayoutInfo[]; } -interface LayoutContextType { +export interface LayoutContextType { getLayoutById: (layoutId: string) => LayoutInfo | null; getLayoutByIdAndGroup: (layoutId: string, groupName: string) => LayoutInfo | null; getLayoutsByGroup: (groupName: string) => LayoutInfo[]; diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/EmptyStateView.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/EmptyStateView.tsx new file mode 100644 index 00000000..583c3bf0 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/EmptyStateView.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { useRouter } from "next/navigation"; +import { Button } from "@/components/ui/button"; +import Wrapper from "@/components/Wrapper"; + +const EmptyStateView: React.FC = () => { + const router = useRouter(); + + return ( + +
+
+

+ No Presentation ID Found +

+

Please start a new presentation.

+ +
+
+
+ ); +}; + +export default EmptyStateView; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx new file mode 100644 index 00000000..22ae5cea --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/GenerateButton.tsx @@ -0,0 +1,74 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import { SlideOutline } from "@/store/slices/presentationGeneration"; +import { LoadingState, StreamState, LayoutGroup } from "../types/index"; + +interface GenerateButtonProps { + loadingState: LoadingState; + streamState: StreamState; + outlines: SlideOutline[] | null; + selectedLayoutGroup: LayoutGroup | null; + onSubmit: () => void; +} + +const GenerateButton: React.FC = ({ + loadingState, + streamState, + outlines, + selectedLayoutGroup, + onSubmit +}) => { + const isDisabled = + loadingState.isLoading || + streamState.isLoading || + streamState.isStreaming || + !outlines || + outlines.length === 0 || + !selectedLayoutGroup; + + const getButtonText = () => { + if (loadingState.isLoading) return loadingState.message; + if (streamState.isLoading || streamState.isStreaming) return "Loading..."; + if (!selectedLayoutGroup) return "Select a Layout Style"; + return "Generate Presentation"; + }; + + return ( +
+ +
+ ); +}; + +export default GenerateButton; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 713efa84..97f8fe3c 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -1,276 +1,41 @@ "use client"; -import React, { useEffect, useState } from "react"; -import { arrayMove } from "@dnd-kit/sortable"; -import { Button } from "@/components/ui/button"; + +import React, { useState } from "react"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { RootState } from "@/store/store"; -import { useSelector, useDispatch } from "react-redux"; -import { useRouter } from "next/navigation"; -import { PresentationGenerationApi } from "../../services/api/presentation-generation"; -import { toast } from "@/hooks/use-toast"; -import { - setPresentationData, - setOutlines, - SlideOutline, -} from "@/store/slices/presentationGeneration"; +import { useSelector } from "react-redux"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; -import { jsonrepair } from "jsonrepair"; import OutlineContent from "./OutlineContent"; import LayoutSelection from "./LayoutSelection"; -import { useLayout } from "../../context/LayoutContext"; +import EmptyStateView from "./EmptyStateView"; +import PageHeader from "./PageHeader"; +import GenerateButton from "./GenerateButton"; -interface LayoutGroup { - id: string; - name: string; - description: string; - ordered: boolean; - isDefault?: boolean; - slides: string[]; -} - -const OutlinePage = () => { - const dispatch = useDispatch(); - const router = useRouter(); - const { - getLayoutById, - loading: layoutLoading, - } = useLayout(); +import { TABS, LayoutGroup } from "../types/index"; +import { useOutlineStreaming } from "../hooks/useOutlineStreaming"; +import { useOutlineManagement } from "../hooks/useOutlineManagement"; +import { usePresentationGeneration } from "../hooks/usePresentationGeneration"; +const OutlinePage: React.FC = () => { const { presentation_id, outlines } = useSelector( (state: RootState) => state.presentationGeneration ); - const [activeTab, setActiveTab] = useState('outline'); + const [activeTab, setActiveTab] = useState(TABS.OUTLINE); const [selectedLayoutGroup, setSelectedLayoutGroup] = useState(null); - const [loadingState, setLoadingState] = useState({ - message: "", - isLoading: false, - showProgress: false, - duration: 0, - }); - const [isStreaming, setStreaming] = useState(false); - const [isLoading, setLoading] = useState(true); - - useEffect(() => { - let evtSource: EventSource; - let accumulatedChunks = ""; - - const fetchSlides = async () => { - setStreaming(true); - setLoading(true); - - try { - evtSource = new EventSource( - `/api/v1/ppt/outlines/stream?presentation_id=${presentation_id}` - ); - - evtSource.onopen = () => { - - }; - - evtSource.addEventListener("response", (event) => { - const data = JSON.parse(event.data); - - if (data.type === "chunk") { - accumulatedChunks += data.chunk; - - try { - const repairedJson = jsonrepair(accumulatedChunks); - const partialData = JSON.parse(repairedJson); - if (partialData.slides) { - dispatch(setOutlines(partialData.slides)); - setLoading(false); - } - } catch (error) { - // It's okay if this fails, it just means the JSON isn't complete yet - } - } else if (data.type === "complete") { - try { - setLoading(false); - setStreaming(false); - const outlinesData: SlideOutline[] = JSON.parse(data.presentation).outlines; - dispatch(setOutlines(outlinesData)); - evtSource.close(); - } catch (error) { - evtSource.close(); - console.error("Error parsing accumulated chunks:", error); - toast({ - title: "Error", - description: "Failed to parse presentation data", - variant: "destructive", - }); - } - accumulatedChunks = ""; - } else if (data.type === "closing") { - setLoading(false); - setStreaming(false); - evtSource.close(); - } - }); - - evtSource.onerror = (error) => { - - setLoading(false); - setStreaming(false); - evtSource.close(); - - toast({ - title: "Connection Error", - description: "Failed to connect to the server. Please try again.", - variant: "destructive", - }); - }; - } catch (error) { - - setLoading(false); - setStreaming(false); - - toast({ - title: "Error", - description: "Failed to initialize connection", - variant: "destructive", - }); - } - }; - - if (presentation_id) { - fetchSlides(); - } - - // Cleanup function - return () => { - if (evtSource) { - evtSource.close(); - } - }; - }, [presentation_id, dispatch]); - - const handleDragEnd = (event: any) => { - const { active, over } = event; - - if (!active || !over || !outlines) return; - - if (active.id !== over.id) { - // Find the indices of the dragged and target items - const oldIndex = outlines.findIndex((item) => item.title === active.id); - const newIndex = outlines.findIndex((item) => item.title === over.id); - - // Reorder the array - const reorderedArray = arrayMove(outlines, oldIndex, newIndex); - - // Update local state - setOutlines(reorderedArray); - // Update the store with new order - dispatch(setOutlines(reorderedArray)); - } - }; - - const handleSubmit = async () => { - if (!outlines || outlines.length === 0) { - toast({ - title: "No Outlines", - description: "Please wait for outlines to load before generating presentation", - variant: "destructive", - }); - return; - } - - if (!selectedLayoutGroup) { - toast({ - title: "Select Layout Group", - description: "Please select a layout group before generating presentation", - variant: "destructive", - }); - return; - } - - // Generate data - setLoadingState({ - message: "Generating presentation data...", - isLoading: true, - showProgress: true, - duration: 30, - }); - - try { - const groupLayoutSchemas = selectedLayoutGroup.slides - .map(slideId => { - const layout = getLayoutById(slideId); - return layout ? { - id: layout.id, - name: layout.name, - description: layout.description, - json_schema: layout.json_schema - } : null; - }) - .filter(schema => schema !== null); - - // Prepare layout data in the expected format with schemas - const layoutData = { - name: selectedLayoutGroup.name, - ordered: selectedLayoutGroup.ordered, - slides: groupLayoutSchemas - }; - - const response = await PresentationGenerationApi.presentationPrepare({ - presentation_id: presentation_id, - outlines: outlines, - layout: layoutData, - }); - - if (response) { - dispatch(setPresentationData(response)); - router.push(`/presentation?id=${presentation_id}&stream=true`); - } - } catch (error) { - console.error("error in data generation", error); - toast({ - title: "Generation Error", - description: "Failed to generate presentation. Please try again.", - variant: "destructive", - }); - } finally { - setLoadingState({ - isLoading: false, - message: "", - showProgress: false, - duration: 0, - }); - } - }; - - const handleAddSlide = () => { - if (!outlines) return; - - const newSlide: SlideOutline = { - title: "New Slide", - body: "", - // Add any other required properties based on your SlideOutline type - }; - - const updatedOutlines = [...outlines, newSlide]; - setOutlines(updatedOutlines); - dispatch(setOutlines(updatedOutlines)); - }; + // Custom hooks + const streamState = useOutlineStreaming(presentation_id); + const { handleDragEnd, handleAddSlide } = useOutlineManagement(outlines); + const { loadingState, handleSubmit } = usePresentationGeneration( + presentation_id, + outlines, + selectedLayoutGroup + ); if (!presentation_id) { - return ( - -
-
-

- No Presentation ID Found -

-

Please start a new presentation.

- -
-
-
- ); + return ; } return ( @@ -284,35 +49,25 @@ const OutlinePage = () => {
+ - {/* Header */} -
-

- Customize Your Presentation -

-

- Review your outline and select a layout style for your presentation. -

-
- - {/* Tabs */} - Outline & Content - Layout Style + Outline & Content + Layout Style - + - + { - {/* Generate button */} -
- -
+
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/PageHeader.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/PageHeader.tsx new file mode 100644 index 00000000..65cafcdd --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/components/PageHeader.tsx @@ -0,0 +1,14 @@ +import React from "react"; + +const PageHeader: React.FC = () => ( +
+

+ Customize Your Presentation +

+

+ Review your outline and select a layout style for your presentation. +

+
+); + +export default PageHeader; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineManagement.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineManagement.ts new file mode 100644 index 00000000..db15c4a6 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineManagement.ts @@ -0,0 +1,35 @@ +import { useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { arrayMove } from "@dnd-kit/sortable"; +import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"; + +export const useOutlineManagement = (outlines: SlideOutline[] | null) => { + const dispatch = useDispatch(); + + const handleDragEnd = useCallback((event: any) => { + const { active, over } = event; + + if (!active || !over || !outlines) return; + + if (active.id !== over.id) { + const oldIndex = outlines.findIndex((item) => item.title === active.id); + const newIndex = outlines.findIndex((item) => item.title === over.id); + const reorderedArray = arrayMove(outlines, oldIndex, newIndex); + dispatch(setOutlines(reorderedArray)); + } + }, [outlines, dispatch]); + + const handleAddSlide = useCallback(() => { + if (!outlines) return; + + const newSlide: SlideOutline = { + title: "Outline title", + body: "Outline body", + }; + + const updatedOutlines = [...outlines, newSlide]; + dispatch(setOutlines(updatedOutlines)); + }, [outlines, dispatch]); + + return { handleDragEnd, handleAddSlide }; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts new file mode 100644 index 00000000..5ac7219b --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/useOutlineStreaming.ts @@ -0,0 +1,103 @@ +import { useEffect, useState } from "react"; +import { useDispatch } from "react-redux"; +import { toast } from "@/hooks/use-toast"; +import { setOutlines, SlideOutline } from "@/store/slices/presentationGeneration"; +import { jsonrepair } from "jsonrepair"; +import { StreamState } from "../types/index"; + +const DEFAULT_STREAM_STATE: StreamState = { + isStreaming: false, + isLoading: true, +}; + +export const useOutlineStreaming = (presentationId: string | null) => { + const dispatch = useDispatch(); + const [streamState, setStreamState] = useState(DEFAULT_STREAM_STATE); + + useEffect(() => { + if (!presentationId) return; + + let eventSource: EventSource; + let accumulatedChunks = ""; + + const initializeStream = async () => { + setStreamState({ isStreaming: true, isLoading: true }); + + try { + eventSource = new EventSource( + `/api/v1/ppt/outlines/stream?presentation_id=${presentationId}` + ); + + eventSource.addEventListener("response", (event) => { + const data = JSON.parse(event.data); + + switch (data.type) { + case "chunk": + accumulatedChunks += data.chunk; + try { + const repairedJson = jsonrepair(accumulatedChunks); + const partialData = JSON.parse(repairedJson); + if (partialData.slides) { + dispatch(setOutlines(partialData.slides)); + setStreamState(prev => ({ ...prev, isLoading: false })); + } + } catch (error) { + // JSON isn't complete yet, continue accumulating + } + break; + + case "complete": + try { + const outlinesData: SlideOutline[] = JSON.parse(data.presentation).outlines; + dispatch(setOutlines(outlinesData)); + setStreamState({ isStreaming: false, isLoading: false }); + eventSource.close(); + } catch (error) { + console.error("Error parsing accumulated chunks:", error); + toast({ + title: "Error", + description: "Failed to parse presentation data", + variant: "destructive", + }); + eventSource.close(); + } + accumulatedChunks = ""; + break; + + case "closing": + setStreamState({ isStreaming: false, isLoading: false }); + eventSource.close(); + break; + } + }); + + eventSource.onerror = () => { + setStreamState({ isStreaming: false, isLoading: false }); + eventSource.close(); + toast({ + title: "Connection Error", + description: "Failed to connect to the server. Please try again.", + variant: "destructive", + }); + }; + } catch (error) { + setStreamState({ isStreaming: false, isLoading: false }); + toast({ + title: "Error", + description: "Failed to initialize connection", + variant: "destructive", + }); + } + }; + + initializeStream(); + + return () => { + if (eventSource) { + eventSource.close(); + } + }; + }, [presentationId, dispatch]); + + return streamState; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts new file mode 100644 index 00000000..82b454f3 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/hooks/usePresentationGeneration.ts @@ -0,0 +1,108 @@ +import { useState, useCallback } from "react"; +import { useDispatch } from "react-redux"; +import { useRouter } from "next/navigation"; +import { toast } from "@/hooks/use-toast"; +import { setPresentationData, SlideOutline } from "@/store/slices/presentationGeneration"; +import { PresentationGenerationApi } from "../../services/api/presentation-generation"; +import { useLayout } from "../../context/LayoutContext"; +import { LayoutGroup, LoadingState } from "../types/index"; + +const DEFAULT_LOADING_STATE: LoadingState = { + message: "", + isLoading: false, + showProgress: false, + duration: 0, +}; + +export const usePresentationGeneration = ( + presentationId: string | null, + outlines: SlideOutline[] | null, + selectedLayoutGroup: LayoutGroup | null +) => { + const dispatch = useDispatch(); + const router = useRouter(); + const { getLayoutById } = useLayout(); + const [loadingState, setLoadingState] = useState(DEFAULT_LOADING_STATE); + + const validateInputs = useCallback(() => { + if (!outlines || outlines.length === 0) { + toast({ + title: "No Outlines", + description: "Please wait for outlines to load before generating presentation", + variant: "destructive", + }); + return false; + } + + if (!selectedLayoutGroup) { + toast({ + title: "Select Layout Group", + description: "Please select a layout group before generating presentation", + variant: "destructive", + }); + return false; + } + + return true; + }, [outlines, selectedLayoutGroup]); + + const prepareLayoutData = useCallback(() => { + if (!selectedLayoutGroup) return null; + + const groupLayoutSchemas = selectedLayoutGroup.slides + .map(slideId => { + const layout = getLayoutById(slideId); + return layout ? { + id: layout.id, + name: layout.name, + description: layout.description, + json_schema: layout.json_schema + } : null; + }) + .filter(schema => schema !== null); + + return { + name: selectedLayoutGroup.name, + ordered: selectedLayoutGroup.ordered, + slides: groupLayoutSchemas + }; + }, [selectedLayoutGroup, getLayoutById]); + + const handleSubmit = useCallback(async () => { + if (!validateInputs()) return; + + setLoadingState({ + message: "Generating presentation data...", + isLoading: true, + showProgress: true, + duration: 30, + }); + + try { + const layoutData = prepareLayoutData(); + if (!layoutData) return; + + const response = await PresentationGenerationApi.presentationPrepare({ + presentation_id: presentationId, + outlines: outlines, + layout: layoutData, + }); + + if (response) { + dispatch(setPresentationData(response)); + router.push(`/presentation?id=${presentationId}&stream=true`); + } + } catch (error) { + console.error("Error in data generation", error); + toast({ + title: "Generation Error", + description: "Failed to generate presentation. Please try again.", + variant: "destructive", + }); + } finally { + setLoadingState(DEFAULT_LOADING_STATE); + } + }, [validateInputs, prepareLayoutData, presentationId, outlines, dispatch, router]); + + return { loadingState, handleSubmit }; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/outline/types/index.ts b/servers/nextjs/app/(presentation-generator)/outline/types/index.ts new file mode 100644 index 00000000..9965d2f4 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/outline/types/index.ts @@ -0,0 +1,27 @@ +export interface LayoutGroup { + id: string; + name: string; + description: string; + ordered: boolean; + isDefault?: boolean; + slides: string[]; +} + +export interface LoadingState { + message: string; + isLoading: boolean; + showProgress: boolean; + duration: number; +} + +export interface StreamState { + isStreaming: boolean; + isLoading: boolean; +} + +export const TABS = { + OUTLINE: 'outline', + LAYOUTS: 'layouts' +} as const; + +export type TabType = typeof TABS[keyof typeof TABS]; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx index bedce9c3..02a338c7 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/Header.tsx @@ -47,6 +47,7 @@ import Modal from "./Modal"; import Announcement from "@/components/Announcement"; import { getFontLink, getStaticFileUrl } from "../../utils/others"; +import JSPowerPointExtractor from "../../components/JSPowerPointExtractor"; const Header = ({ @@ -206,7 +207,7 @@ const Header = ({ method: 'POST', body: JSON.stringify({ id: presentation_id, - title: presentationData!.presentation!.title, + title: presentationData?.title, }) }); @@ -248,6 +249,9 @@ const Header = ({ pptx export Export as PPTX + {/*
+ +
*/}

Font Used: diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx index 7bb1a2e7..87f34916 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationPage.tsx @@ -1,290 +1,81 @@ "use client"; -import React, { useEffect, useState, useCallback, useRef } from "react"; -import { useDispatch, useSelector } from "react-redux"; -import { useRouter, useSearchParams } from "next/navigation"; +import React, { useState } from "react"; +import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { Skeleton } from "@/components/ui/skeleton"; import PresentationMode from "../../components/PresentationMode"; - -import { DashboardApi } from "@/app/dashboard/api/dashboard"; import SidePanel from "../components/SidePanel"; import SlideContent from "../components/SlideContent"; - -import { - deletePresentationSlide, - setPresentationData, - setStreaming, -} from "@/store/slices/presentationGeneration"; -import { toast } from "@/hooks/use-toast"; -import { PresentationGenerationApi } from "../../services/api/presentation-generation"; - import LoadingState from "../../components/LoadingState"; import Header from "../components/Header"; import { Loader2 } from "lucide-react"; - -import { jsonrepair } from "jsonrepair"; import { Button } from "@/components/ui/button"; import { AlertCircle } from "lucide-react"; import Help from "./Help"; +import { + usePresentationStreaming, + usePresentationData, + usePresentationNavigation +} from "../hooks"; +import { PresentationPageProps } from "../types"; - -// Custom debounce function -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] - ); -} - -const PresentationPage = ({ presentation_id }: { presentation_id: string }) => { - const dispatch = useDispatch(); +const PresentationPage: React.FC = ({ presentation_id }) => { + // State management const [loading, setLoading] = useState(true); const [selectedSlide, setSelectedSlide] = useState(0); 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( (state: RootState) => state.theme ); const { presentationData, isStreaming } = useSelector( (state: RootState) => state.presentationGeneration ); - const [error, setError] = useState(false); - const router = useRouter(); - const searchParams = useSearchParams(); - const isPresentMode = searchParams.get("mode") === "present"; - const stream = searchParams.get("stream"); - const currentSlide = parseInt( - searchParams.get("slide") || `${selectedSlide}` || "0" + + // Custom hooks + const { fetchUserSlides, handleDeleteSlide } = usePresentationData( + presentation_id, + setLoading, + setError ); - const [isMobilePanelOpen, setIsMobilePanelOpen] = useState(false); - const [autoSaveLoading, setAutoSaveLoading] = useState(false); - // Add ref for tracking initial load - const isInitialLoad = useRef(true); + const { + isPresentMode, + stream, + currentSlide, + handleSlideClick, + toggleFullscreen, + handlePresentExit, + handleSlideChange, + } = usePresentationNavigation( + presentation_id, + selectedSlide, + setSelectedSlide, + setIsFullscreen + ); - // Ref to track the previous length of slides - const previousSlidesLength = useRef(0); + // Initialize streaming + usePresentationStreaming( + presentation_id, + stream, + setLoading, + setError, + fetchUserSlides + ); - // Create auto-save function - // const autoSave = useCallback( - // (data: { presentation_id: string; slides: any[] }) => { - // setAutoSaveLoading(true); - // // Fire and forget - no await - // PresentationGenerationApi.updatePresentationContent(data) - // .then(() => { }) - // .catch((error) => { - // console.error("Error AAYO", error); - // }) - // .finally(() => { - - // setAutoSaveLoading(false); - // }); - // }, - // [presentation_id] - // ); - - // Create debounced version of autoSave - // const debouncedSave = useDebounce(autoSave, 2000); - - // Watch for changes in presentationData and trigger auto-save - // useEffect(() => { - // if ( - // presentationData && - // !isStreaming && - // !isInitialLoad.current && - // presentationData.slides && - // presentationData.slides.some( - // (slide: any) => slide.images && slide.images.length > 0 - // ) - // ) { - - // debouncedSave({ - // presentation_id: presentation_id, - // slides: presentationData.slides, - // }); - // } - // if (isInitialLoad.current) { - // isInitialLoad.current = false; - // } - // }, [presentationData, debouncedSave]); - - // Function to fetch the slides - useEffect(() => { - - let evtSource: EventSource; - let accumulatedChunks = ""; - - const fetchSlides = async () => { - dispatch(setStreaming(true)); - - evtSource = new EventSource( - `/api/v1/ppt/presentation/stream?presentation_id=${presentation_id}` - ); - - evtSource.onopen = () => { - }; - - evtSource.addEventListener("response", (event) => { - const data = JSON.parse(event.data); - - if (data.type === "chunk") { - accumulatedChunks += data.chunk; - try { - const repairedJson = jsonrepair(accumulatedChunks); - const partialData = JSON.parse(repairedJson); - - if (partialData.slides) { - // Check if the length of slides has changed - if ( - partialData.slides.length !== previousSlidesLength.current && - partialData.slides.length > 0 - ) { - // partialData.slides.splice(-1); - dispatch( - setPresentationData({ - ...partialData, - slides: partialData.slides, - }) - ); - previousSlidesLength.current = partialData.slides.length; // Update the previous length - setLoading(false); - } - } - } catch (error) { - // console.error('error while repairing json', error) - // It's okay if this fails, it just means the JSON isn't complete yet - } - } else if (data.type === "complete") { - try { - - dispatch(setPresentationData(data.presentation)); - dispatch(setStreaming(false)); - - setLoading(false); - - evtSource.close(); - // Remove session parameter from URL - const newUrl = new URL(window.location.href); - newUrl.searchParams.delete("stream"); - window.history.replaceState({}, "", newUrl.toString()); - } catch (error) { - evtSource.close(); - console.error("Error parsing accumulated chunks:", error); - } - accumulatedChunks = ""; - } else if (data.type === "closing") { - dispatch(setPresentationData(data.presentation)); - - setLoading(false); - dispatch(setStreaming(false)); - evtSource.close(); - // Remove session parameter from URL - const newUrl = new URL(window.location.href); - newUrl.searchParams.delete("stream"); - window.history.replaceState({}, "", newUrl.toString()); - } - }); - evtSource.onerror = (error) => { - console.error("EventSource failed:", error); - - setLoading(false); - dispatch(setStreaming(false)); - setError(true); - - evtSource.close(); - }; - }; - - if (stream) { - fetchSlides(); - } else { - fetchUserSlides(); - } - - return () => { - if (evtSource) { - evtSource.close(); - } - }; - }, []); - // Function to scroll to specific slide - const handleSlideClick = (index: any) => { - const slideElement = document.getElementById(`slide-${index}`); - if (slideElement) { - slideElement.scrollIntoView({ - behavior: "smooth", - block: "center", - }); - setSelectedSlide(index); - } - }; - // Function to fetch the user slides - const fetchUserSlides = async () => { - try { - const data = await DashboardApi.getPresentation(presentation_id); - if (data) { - dispatch(setPresentationData(data)); - setLoading(false); - } - } catch (error) { - setError(true); - toast({ - title: "Error", - description: "Failed to load presentation", - variant: "destructive", - }); - - console.error("Error fetching user slides:", error); - setLoading(false); - } + const onDeleteSlide = (index: number) => { + handleDeleteSlide(index, presentationData); }; - // Function to toggle fullscreen - const toggleFullscreen = () => { - if (!document.fullscreenElement) { - document.documentElement.requestFullscreen(); - setIsFullscreen(true); - } else { - document.exitFullscreen(); - setIsFullscreen(false); - } - }; - // Function to handle present exit - const handlePresentExit = () => { - setIsFullscreen(false); - router.push(`/presentation?id=${presentation_id}`); - }; - // Function to handle slide change for presentation mode - const handleSlideChange = (newSlide: number) => { - if (newSlide >= 0 && newSlide < presentationData?.slides.length!) { - setSelectedSlide(newSlide); - router.push( - `/presentation?id=${presentation_id}&mode=present&slide=${newSlide}`, - { scroll: false } - ); - } - }; - - const handleDeleteSlide = async (index: number) => { - dispatch(deletePresentationSlide(index)); - const response = PresentationGenerationApi.deleteSlide( - presentation_id, - presentationData?.slides[index].id! - ); + const onSlideChange = (newSlide: number) => { + handleSlideChange(newSlide, presentationData); }; + // Presentation Mode View if (isPresentMode) { return ( { isFullscreen={isFullscreen} onFullscreenToggle={toggleFullscreen} onExit={handlePresentExit} - onSlideChange={handleSlideChange} + onSlideChange={onSlideChange} /> ); } - // Regular view + if (error) { + return ( +

+ ); + } + + return (
- {/* Auto save loading state */} + {/* Auto save loading indicator */} {autoSaveLoading && (
)} +
- {error ? ( -
-
- - Oops! -

- We encountered an issue loading your presentation. -

-

- Please check your internet connection or try again later. -

- -
-
- ) : ( -
- -
-
- {!presentationData || - loading || - !presentationData?.slides || - presentationData?.slides.length === 0 ? ( -
-
- {Array.from({ length: 2 }).map((_, index) => ( - - ))} -
- {stream && } +
+ + +
+
+ {!presentationData || + loading || + !presentationData?.slides || + presentationData?.slides.length === 0 ? ( +
+
+ {Array.from({ length: 2 }).map((_, index) => ( + + ))}
- ) : ( - <> - {presentationData && - presentationData.slides && - presentationData.slides.length > 0 && - presentationData.slides.map((slide: any, index: number) => ( - - ))} - - )} -
+ {stream && } +
+ ) : ( + <> + {presentationData && + presentationData.slides && + presentationData.slides.length > 0 && + presentationData.slides.map((slide: any, index: number) => ( + + ))} + + )}
- )} +
); }; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx index 4d72eeb9..f9ddcb63 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/SlideContent.tsx @@ -154,13 +154,13 @@ const SlideContent = ({ {isStreaming && ( )} -
+
{/* render slides */} {loading ?
: slideContent} - {!showNewSlideSelection && ( + {/* {!showNewSlideSelection && (
{!isStreaming && ( @@ -178,9 +178,8 @@ const SlideContent = ({ handleNewSlide(type, slide.index)} setShowNewSlideSelection={setShowNewSlideSelection} - /> - )} + )} */} {!isStreaming && (
void, + setError: (error: boolean) => void +) => { + const dispatch = useDispatch(); + const router = useRouter(); + + const fetchUserSlides = useCallback(async () => { + try { + const data = await DashboardApi.getPresentation(presentationId); + if (data) { + dispatch(setPresentationData(data)); + setLoading(false); + } + } catch (error) { + setError(true); + toast({ + title: "Error", + description: "Failed to load presentation", + variant: "destructive", + }); + console.error("Error fetching user slides:", error); + setLoading(false); + } + }, [presentationId, dispatch, setLoading, setError]); + + const handleDeleteSlide = useCallback(async (index: number, presentationData: any) => { + dispatch(deletePresentationSlide(index)); + try { + await PresentationGenerationApi.deleteSlide( + presentationId, + presentationData?.slides[index].id! + ); + } catch (error) { + console.error("Error deleting slide:", error); + toast({ + title: "Error", + description: "Failed to delete slide", + variant: "destructive", + }); + } + }, [presentationId, dispatch]); + + return { + fetchUserSlides, + handleDeleteSlide, + }; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationNavigation.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationNavigation.ts new file mode 100644 index 00000000..a2292dfb --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationNavigation.ts @@ -0,0 +1,64 @@ +import { useCallback } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +export const usePresentationNavigation = ( + presentationId: string, + selectedSlide: number, + setSelectedSlide: (slide: number) => void, + setIsFullscreen: (fullscreen: boolean) => void +) => { + const router = useRouter(); + const searchParams = useSearchParams(); + + const isPresentMode = searchParams.get("mode") === "present"; + const stream = searchParams.get("stream"); + const currentSlide = parseInt( + searchParams.get("slide") || `${selectedSlide}` || "0" + ); + + const handleSlideClick = useCallback((index: number) => { + const slideElement = document.getElementById(`slide-${index}`); + if (slideElement) { + slideElement.scrollIntoView({ + behavior: "smooth", + block: "center", + }); + setSelectedSlide(index); + } + }, [setSelectedSlide]); + + const toggleFullscreen = useCallback(() => { + if (!document.fullscreenElement) { + document.documentElement.requestFullscreen(); + setIsFullscreen(true); + } else { + document.exitFullscreen(); + setIsFullscreen(false); + } + }, [setIsFullscreen]); + + const handlePresentExit = useCallback(() => { + setIsFullscreen(false); + router.push(`/presentation?id=${presentationId}`); + }, [router, presentationId, setIsFullscreen]); + + const handleSlideChange = useCallback((newSlide: number, presentationData: any) => { + if (newSlide >= 0 && newSlide < presentationData?.slides.length!) { + setSelectedSlide(newSlide); + router.push( + `/presentation?id=${presentationId}&mode=present&slide=${newSlide}`, + { scroll: false } + ); + } + }, [router, presentationId, setSelectedSlide]); + + return { + isPresentMode, + stream, + currentSlide, + handleSlideClick, + toggleFullscreen, + handlePresentExit, + handleSlideChange, + }; +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts new file mode 100644 index 00000000..ad03efdd --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/hooks/usePresentationStreaming.ts @@ -0,0 +1,111 @@ +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"; + +export const usePresentationStreaming = ( + presentationId: string, + stream: string | null, + setLoading: (loading: boolean) => void, + setError: (error: boolean) => void, + fetchUserSlides: () => void +) => { + const dispatch = useDispatch(); + const previousSlidesLength = useRef(0); + + useEffect(() => { + let eventSource: EventSource; + let accumulatedChunks = ""; + + const initializeStream = async () => { + dispatch(setStreaming(true)); + + eventSource = new EventSource( + `/api/v1/ppt/presentation/stream?presentation_id=${presentationId}` + ); + + eventSource.addEventListener("response", (event) => { + const data = JSON.parse(event.data); + + switch (data.type) { + case "chunk": + accumulatedChunks += data.chunk; + try { + const repairedJson = jsonrepair(accumulatedChunks); + const partialData = JSON.parse(repairedJson); + + if (partialData.slides) { + if ( + partialData.slides.length !== previousSlidesLength.current && + partialData.slides.length > 0 + ) { + dispatch( + setPresentationData({ + ...partialData, + slides: partialData.slides, + }) + ); + previousSlidesLength.current = partialData.slides.length; + setLoading(false); + } + } + } catch (error) { + // JSON isn't complete yet, continue accumulating + } + break; + + case "complete": + try { + dispatch(setPresentationData(data.presentation)); + dispatch(setStreaming(false)); + setLoading(false); + eventSource.close(); + + // Remove stream parameter from URL + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete("stream"); + window.history.replaceState({}, "", newUrl.toString()); + } catch (error) { + eventSource.close(); + console.error("Error parsing accumulated chunks:", error); + } + accumulatedChunks = ""; + break; + + case "closing": + dispatch(setPresentationData(data.presentation)); + setLoading(false); + dispatch(setStreaming(false)); + eventSource.close(); + + // Remove stream parameter from URL + const newUrl = new URL(window.location.href); + newUrl.searchParams.delete("stream"); + window.history.replaceState({}, "", newUrl.toString()); + break; + } + }); + + eventSource.onerror = (error) => { + console.error("EventSource failed:", error); + setLoading(false); + dispatch(setStreaming(false)); + setError(true); + eventSource.close(); + }; + }; + + if (stream) { + initializeStream(); + } else { + fetchUserSlides(); + } + + return () => { + if (eventSource) { + eventSource.close(); + } + }; + }, [presentationId, stream, dispatch, setLoading, setError, fetchUserSlides]); +}; \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/types/index.ts b/servers/nextjs/app/(presentation-generator)/presentation/types/index.ts new file mode 100644 index 00000000..ceabe23b --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/types/index.ts @@ -0,0 +1,16 @@ +export interface PresentationState { + loading: boolean; + selectedSlide: number; + isFullscreen: boolean; + error: boolean; + isMobilePanelOpen: boolean; + autoSaveLoading: boolean; +} + +export interface StreamState { + isStreaming: boolean; +} + +export interface PresentationPageProps { + presentation_id: string; +} \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts b/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts new file mode 100644 index 00000000..93c2a725 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/utils/debounce.ts @@ -0,0 +1,21 @@ +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)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index ed5a76a1..398b180f 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -185,20 +185,6 @@ const UploadPage = () => { }); }; - // Show loading state while layouts are being loaded - // if (layoutsLoading) { - // return ( - // - // - // - // ); - // } - return ( , + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/servers/nextjs/components/ui/checkbox.tsx b/servers/nextjs/components/ui/checkbox.tsx new file mode 100644 index 00000000..83692e24 --- /dev/null +++ b/servers/nextjs/components/ui/checkbox.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { cn } from "@/lib/utils" +import { CheckIcon } from "@radix-ui/react-icons" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/servers/nextjs/next.config.mjs b/servers/nextjs/next.config.mjs index e4cc1ab9..5338685f 100644 --- a/servers/nextjs/next.config.mjs +++ b/servers/nextjs/next.config.mjs @@ -42,6 +42,7 @@ const nextConfig = { }, ], }, + }; export default nextConfig; diff --git a/servers/nextjs/package-lock.json b/servers/nextjs/package-lock.json index c0ae3778..5ab95604 100644 --- a/servers/nextjs/package-lock.json +++ b/servers/nextjs/package-lock.json @@ -44,6 +44,7 @@ "marked": "^15.0.11", "next": "^14.2.14", "next-themes": "^0.4.6", + "pptxgenjs": "^4.0.1", "puppeteer": "^24.13.0", "react": "^18", "react-dom": "^18", @@ -3229,7 +3230,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -4350,6 +4350,12 @@ "node": ">=0.10" } }, + "node_modules/https": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https/-/https-1.0.0.tgz", + "integrity": "sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==", + "license": "ISC" + }, "node_modules/https-proxy-agent": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", @@ -4394,6 +4400,27 @@ ], "license": "BSD-3-Clause" }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/immer": { "version": "10.1.1", "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", @@ -4430,6 +4457,12 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, "node_modules/ini": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", @@ -4594,6 +4627,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4713,6 +4752,18 @@ "verror": "1.10.0" } }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, "node_modules/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -4723,6 +4774,15 @@ "node": "> 0.8" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", @@ -5310,6 +5370,12 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5547,6 +5613,27 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, + "node_modules/pptxgenjs": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pptxgenjs/-/pptxgenjs-4.0.1.tgz", + "integrity": "sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==", + "license": "MIT", + "dependencies": { + "@types/node": "^22.8.1", + "https": "^1.0.0", + "image-size": "^1.2.1", + "jszip": "^3.10.1" + } + }, + "node_modules/pptxgenjs/node_modules/@types/node": { + "version": "22.16.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.16.4.tgz", + "integrity": "sha512-PYRhNtZdm2wH/NT2k/oAJ6/f2VD2N2Dag0lGlx2vWgMSJXGNmlce5MiTQzoWAiIJtso30mjnfQCOKVH+kAQC/g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -5570,6 +5657,12 @@ "node": ">= 0.6.0" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/progress": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", @@ -5908,6 +6001,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -6091,6 +6193,27 @@ "pify": "^2.3.0" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -6323,6 +6446,12 @@ "node": ">=10" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -6569,6 +6698,21 @@ "bare-events": "^2.2.0" } }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -7120,7 +7264,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true, "license": "MIT" }, "node_modules/universalify": { diff --git a/servers/nextjs/package.json b/servers/nextjs/package.json index 972ab49a..8713f1a2 100644 --- a/servers/nextjs/package.json +++ b/servers/nextjs/package.json @@ -47,6 +47,7 @@ "marked": "^15.0.11", "next": "^14.2.14", "next-themes": "^0.4.6", + "pptxgenjs": "^4.0.1", "puppeteer": "^24.13.0", "react": "^18", "react-dom": "^18",