refactor(Nextjs): outlines & presentation pages refactored
This commit is contained in:
parent
49412fccad
commit
a40984d4da
25 changed files with 1048 additions and 684 deletions
|
|
@ -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<string, LayoutInfo>;
|
||||
layoutsByGroup: Map<string, Set<string>>;
|
||||
groupSettings: Map<string, GroupSetting>;
|
||||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Wrapper>
|
||||
<div className="max-w-[1000px] min-h-screen flex justify-center items-center mx-auto px-4 sm:px-6 pb-6">
|
||||
<div className="mt-4 sm:mt-8 font-instrument_sans text-center">
|
||||
<h4 className="text-lg sm:text-xl font-medium mb-4">
|
||||
No Presentation ID Found
|
||||
</h4>
|
||||
<p className="text-gray-600 mb-4">Please start a new presentation.</p>
|
||||
<Button
|
||||
onClick={() => router.push("/upload")}
|
||||
className="bg-[#5146E5] w-full rounded-xl text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white mt-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Start New Presentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmptyStateView;
|
||||
|
|
@ -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<GenerateButtonProps> = ({
|
||||
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 (
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
onClick={onSubmit}
|
||||
className="bg-[#5146E5] w-full rounded-lg text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg
|
||||
className="mr-2"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_1960_939)">
|
||||
<path
|
||||
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1960_939">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.75 0.876953)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
{getButtonText()}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GenerateButton;
|
||||
|
|
@ -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<string>('outline');
|
||||
const [activeTab, setActiveTab] = useState<string>(TABS.OUTLINE);
|
||||
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(null);
|
||||
const [loadingState, setLoadingState] = useState({
|
||||
message: "",
|
||||
isLoading: false,
|
||||
showProgress: false,
|
||||
duration: 0,
|
||||
});
|
||||
|
||||
const [isStreaming, setStreaming] = useState<boolean>(false);
|
||||
const [isLoading, setLoading] = useState<boolean>(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 (
|
||||
<Wrapper>
|
||||
<div className="max-w-[1000px] min-h-screen flex justify-center items-center mx-auto px-4 sm:px-6 pb-6">
|
||||
<div className="mt-4 sm:mt-8 font-instrument_sans text-center">
|
||||
<h4 className="text-lg sm:text-xl font-medium mb-4">
|
||||
No Presentation ID Found
|
||||
</h4>
|
||||
<p className="text-gray-600 mb-4">Please start a new presentation.</p>
|
||||
<Button onClick={() => router.push("/upload")} className="bg-[#5146E5] w-full rounded-xl text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white mt-4 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Start New Presentation
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
return <EmptyStateView />;
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -284,35 +49,25 @@ const OutlinePage = () => {
|
|||
|
||||
<div className="max-w-[1200px] mx-auto px-4 sm:px-6 pb-6">
|
||||
<div className="mt-4 sm:mt-8">
|
||||
<PageHeader />
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-2xl font-bold mb-2 text-gray-900">
|
||||
Customize Your Presentation
|
||||
</h4>
|
||||
<p className="text-gray-600">
|
||||
Review your outline and select a layout style for your presentation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-[50%] mx-auto grid-cols-2">
|
||||
<TabsTrigger value="outline">Outline & Content</TabsTrigger>
|
||||
<TabsTrigger value="layouts">Layout Style</TabsTrigger>
|
||||
<TabsTrigger value={TABS.OUTLINE}>Outline & Content</TabsTrigger>
|
||||
<TabsTrigger value={TABS.LAYOUTS}>Layout Style</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="outline" className="mt-6">
|
||||
<TabsContent value={TABS.OUTLINE} className="mt-6">
|
||||
<OutlineContent
|
||||
outlines={outlines}
|
||||
isLoading={isLoading}
|
||||
isStreaming={isStreaming}
|
||||
isLoading={streamState.isLoading}
|
||||
isStreaming={streamState.isStreaming}
|
||||
onDragEnd={handleDragEnd}
|
||||
onAddSlide={handleAddSlide}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="layouts" className="mt-6">
|
||||
<TabsContent value={TABS.LAYOUTS} className="mt-6">
|
||||
<LayoutSelection
|
||||
selectedLayoutGroup={selectedLayoutGroup}
|
||||
onSelectLayoutGroup={setSelectedLayoutGroup}
|
||||
|
|
@ -320,54 +75,13 @@ const OutlinePage = () => {
|
|||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Generate button */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
disabled={
|
||||
loadingState.isLoading ||
|
||||
isLoading ||
|
||||
isStreaming ||
|
||||
!outlines ||
|
||||
outlines.length === 0 ||
|
||||
!selectedLayoutGroup
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#5146E5] w-full rounded-lg text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg
|
||||
className="mr-2"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_1960_939)">
|
||||
<path
|
||||
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1960_939">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.75 0.876953)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
{loadingState.isLoading
|
||||
? loadingState.message
|
||||
: isLoading || isStreaming
|
||||
? "Loading..."
|
||||
: !selectedLayoutGroup
|
||||
? "Select a Layout Style"
|
||||
: "Generate Presentation"}
|
||||
</Button>
|
||||
</div>
|
||||
<GenerateButton
|
||||
loadingState={loadingState}
|
||||
streamState={streamState}
|
||||
outlines={outlines}
|
||||
selectedLayoutGroup={selectedLayoutGroup}
|
||||
onSubmit={handleSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import React from "react";
|
||||
|
||||
const PageHeader: React.FC = () => (
|
||||
<div className="mb-8">
|
||||
<h4 className="text-2xl font-bold mb-2 text-gray-900">
|
||||
Customize Your Presentation
|
||||
</h4>
|
||||
<p className="text-gray-600">
|
||||
Review your outline and select a layout style for your presentation.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default PageHeader;
|
||||
|
|
@ -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 };
|
||||
};
|
||||
|
|
@ -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<StreamState>(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;
|
||||
};
|
||||
|
|
@ -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<LoadingState>(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 };
|
||||
};
|
||||
|
|
@ -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];
|
||||
|
|
@ -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 = ({
|
|||
<img src="/pptx.svg" alt="pptx export" width={30} height={30} />
|
||||
Export as PPTX
|
||||
</Button>
|
||||
{/* <div className={`w-full ${mobile ? "bg-white py-2 rounded-lg" : ""}`}>
|
||||
<JSPowerPointExtractor />
|
||||
</div> */}
|
||||
<p className={`text-sm pt-3 border-t border-gray-300 ${mobile ? "border-none text-white font-semibold" : ""}`}>
|
||||
Font Used:
|
||||
<a className={`text-blue-500 flex items-center gap-1 ${mobile ? "mt-2 py-2 px-4 bg-white rounded-lg w-fit" : ""}`} href={getFontLink(currentColors.fontFamily).link || ''} target="_blank" rel="noopener noreferrer">
|
||||
|
|
|
|||
|
|
@ -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<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]
|
||||
);
|
||||
}
|
||||
|
||||
const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
||||
const dispatch = useDispatch();
|
||||
const PresentationPage: React.FC<PresentationPageProps> = ({ 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 (
|
||||
<PresentationMode
|
||||
|
|
@ -294,98 +85,100 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
isFullscreen={isFullscreen}
|
||||
onFullscreenToggle={toggleFullscreen}
|
||||
onExit={handlePresentExit}
|
||||
onSlideChange={handleSlideChange}
|
||||
onSlideChange={onSlideChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Regular view
|
||||
if (error) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
|
||||
<div
|
||||
className="bg-white border border-red-300 text-red-700 px-6 py-8 rounded-lg shadow-lg flex flex-col items-center"
|
||||
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.
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="h-screen flex overflow-hidden flex-col">
|
||||
{/* Auto save loading state */}
|
||||
{/* 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>
|
||||
)}
|
||||
|
||||
<Header presentation_id={presentation_id} currentSlide={currentSlide} />
|
||||
<Help />
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
|
||||
<div
|
||||
className="bg-white border border-red-300 text-red-700 px-6 py-8 rounded-lg shadow-lg flex flex-col items-center"
|
||||
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.
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
background: currentColors.background,
|
||||
}}
|
||||
className="flex flex-1 relative pt-6"
|
||||
>
|
||||
<SidePanel
|
||||
selectedSlide={selectedSlide}
|
||||
onSlideClick={handleSlideClick}
|
||||
loading={loading}
|
||||
isMobilePanelOpen={isMobilePanelOpen}
|
||||
setIsMobilePanelOpen={setIsMobilePanelOpen}
|
||||
/>
|
||||
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
|
||||
<div
|
||||
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0 "
|
||||
|
||||
>
|
||||
{!presentationData ||
|
||||
loading ||
|
||||
!presentationData?.slides ||
|
||||
presentationData?.slides.length === 0 ? (
|
||||
<div className="relative w-full h-[calc(100vh-120px)] mx-auto ">
|
||||
<div className=" ">
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className="aspect-video bg-gray-400 my-4 w-full mx-auto max-w-[1280px]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{stream && <LoadingState />}
|
||||
<div
|
||||
style={{
|
||||
background: currentColors.background,
|
||||
}}
|
||||
className="flex flex-1 relative pt-6"
|
||||
>
|
||||
<SidePanel
|
||||
selectedSlide={selectedSlide}
|
||||
onSlideClick={handleSlideClick}
|
||||
loading={loading}
|
||||
isMobilePanelOpen={isMobilePanelOpen}
|
||||
setIsMobilePanelOpen={setIsMobilePanelOpen}
|
||||
/>
|
||||
|
||||
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
|
||||
<div className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0">
|
||||
{!presentationData ||
|
||||
loading ||
|
||||
!presentationData?.slides ||
|
||||
presentationData?.slides.length === 0 ? (
|
||||
<div className="relative w-full h-[calc(100vh-120px)] mx-auto">
|
||||
<div className="">
|
||||
{Array.from({ length: 2 }).map((_, index) => (
|
||||
<Skeleton
|
||||
key={index}
|
||||
className="aspect-video bg-gray-400 my-4 w-full mx-auto max-w-[1280px]"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{presentationData &&
|
||||
presentationData.slides &&
|
||||
presentationData.slides.length > 0 &&
|
||||
presentationData.slides.map((slide: any, index: number) => (
|
||||
<SlideContent
|
||||
key={`${slide.type}-${index}-${slide.index}}`}
|
||||
slide={slide}
|
||||
index={index}
|
||||
presentationId={presentation_id}
|
||||
onDeleteSlide={handleDeleteSlide}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{stream && <LoadingState />}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{presentationData &&
|
||||
presentationData.slides &&
|
||||
presentationData.slides.length > 0 &&
|
||||
presentationData.slides.map((slide: any, index: number) => (
|
||||
<SlideContent
|
||||
key={`${slide.type}-${index}-${slide.index}`}
|
||||
slide={slide}
|
||||
index={index}
|
||||
presentationId={presentation_id}
|
||||
onDeleteSlide={onDeleteSlide}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -154,13 +154,13 @@ const SlideContent = ({
|
|||
{isStreaming && (
|
||||
<Loader2 className="w-8 h-8 absolute right-2 top-2 z-30 text-blue-800 animate-spin" />
|
||||
)}
|
||||
<div className={` w-full group `}>
|
||||
<div className={` w-full group mb-6`}>
|
||||
{/* render slides */}
|
||||
{loading ? <div className="flex flex-col bg-white aspect-video items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div> : slideContent}
|
||||
|
||||
{!showNewSlideSelection && (
|
||||
{/* {!showNewSlideSelection && (
|
||||
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
|
||||
<ToolTip content="Add new slide below">
|
||||
{!isStreaming && (
|
||||
|
|
@ -178,9 +178,8 @@ const SlideContent = ({
|
|||
<NewSlide
|
||||
onSelectLayout={(type) => handleNewSlide(type, slide.index)}
|
||||
setShowNewSlideSelection={setShowNewSlideSelection}
|
||||
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
{!isStreaming && (
|
||||
<ToolTip content="Delete slide">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
export { usePresentationStreaming } from './usePresentationStreaming';
|
||||
export { usePresentationData } from './usePresentationData';
|
||||
export { usePresentationNavigation } from './usePresentationNavigation';
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { useCallback } from "react";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { DashboardApi } from "@/app/dashboard/api/dashboard";
|
||||
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
|
||||
import { setPresentationData, deletePresentationSlide } from "@/store/slices/presentationGeneration";
|
||||
|
||||
export const usePresentationData = (
|
||||
presentationId: string,
|
||||
setLoading: (loading: boolean) => 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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
|
@ -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]);
|
||||
};
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
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]
|
||||
);
|
||||
}
|
||||
|
|
@ -185,20 +185,6 @@ const UploadPage = () => {
|
|||
});
|
||||
};
|
||||
|
||||
// Show loading state while layouts are being loaded
|
||||
// if (layoutsLoading) {
|
||||
// return (
|
||||
// <Wrapper className="pb-10 lg:max-w-[70%] xl:max-w-[65%]">
|
||||
// <OverlayLoader
|
||||
// show={true}
|
||||
// text="Loading presentation layouts..."
|
||||
// showProgress={true}
|
||||
// duration={10}
|
||||
// />
|
||||
// </Wrapper>
|
||||
// );
|
||||
// }
|
||||
|
||||
return (
|
||||
<Wrapper className="pb-10 lg:max-w-[70%] xl:max-w-[65%]">
|
||||
<OverlayLoader
|
||||
|
|
|
|||
|
|
@ -1,12 +1,8 @@
|
|||
import { NextResponse } from 'next/server'
|
||||
import { promises as fs } from 'fs'
|
||||
import path from 'path'
|
||||
import { GroupSetting } from '@/app/layout-preview/types'
|
||||
|
||||
interface GroupSetting {
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
|
|
|
|||
36
servers/nextjs/components/ui/badge.tsx
Normal file
36
servers/nextjs/components/ui/badge.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
||||
29
servers/nextjs/components/ui/checkbox.tsx
Normal file
29
servers/nextjs/components/ui/checkbox.tsx
Normal file
|
|
@ -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<typeof CheckboxPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CheckboxPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
))
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
||||
|
||||
export { Checkbox }
|
||||
|
|
@ -42,6 +42,7 @@ const nextConfig = {
|
|||
},
|
||||
],
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
147
servers/nextjs/package-lock.json
generated
147
servers/nextjs/package-lock.json
generated
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue