diff --git a/Dockerfile.dev b/Dockerfile.dev index b6b66633..ad11f17f 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -30,10 +30,10 @@ RUN pip install -r requirements.txt # Install dependencies for Next.js WORKDIR /node_dependencies COPY servers/nextjs/package.json servers/nextjs/package-lock.json ./ -RUN npm install +RUN npm install # Install chrome for puppeteer -RUN npx puppeteer browsers install chrome@136.0.7103.92 --install-deps +RUN npx puppeteer browsers install chrome@138.0.7204.94 --install-deps RUN chmod -R 777 /node_dependencies diff --git a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx index ec52888f..1e18f58e 100644 --- a/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/documents-preview/components/DocumentPreviewPage.tsx @@ -4,9 +4,8 @@ * A component that displays and manages document previews for presentation generation. * Features: * - Document content preview with markdown support - * - Sidebar navigation for documents, reports, and images + * - Sidebar navigation for documents * - Document content editing and saving - * - Tables and charts display * - Presentation generation workflow * * @component @@ -19,17 +18,18 @@ import { useEffect, useState, useRef, useMemo } from "react"; import { Skeleton } from "@/components/ui/skeleton"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; -import { setOutlines, setPresentationId } from "@/store/slices/presentationGeneration"; +import { setPresentationId } from "@/store/slices/presentationGeneration"; import { useDispatch, useSelector } from "react-redux"; import { useRouter } from "next/navigation"; import { RootState } from "@/store/store"; import { Button } from "@/components/ui/button"; import { toast } from "@/hooks/use-toast"; import MarkdownRenderer from "./MarkdownRenderer"; -import { getIconFromFile, removeUUID } from "../../utils/others"; +import { getIconFromFile } from "../../utils/others"; import { ChevronRight, PanelRightOpen, X } from "lucide-react"; import ToolTip from "@/components/ToolTip"; import Header from "@/app/dashboard/components/Header"; +import useLayoutSchema from "../../hooks/useLayoutSchema"; // Types interface LoadingState { @@ -43,6 +43,10 @@ interface TextContents { [key: string]: string; } +interface FileItem { + name: string; + file_path: string; +} const DocumentsPreviewPage: React.FC = () => { // Hooks @@ -51,7 +55,7 @@ const DocumentsPreviewPage: React.FC = () => { const textareaRef = useRef(null); // Redux state - const { config, documents, images, charts, tables } = useSelector( + const { config, files } = useSelector( (state: RootState) => state.pptGenUpload ); @@ -66,13 +70,17 @@ const DocumentsPreviewPage: React.FC = () => { duration: 10, progress: false, }); + const { layoutSchema } = useLayoutSchema(); - // Memoized values - const documentKeys = useMemo(() => Object.keys(documents), [documents]); - const imageKeys = useMemo(() => Object.keys(images), [images]); - const allSources = useMemo(() => [...documentKeys, ...imageKeys], [documentKeys, imageKeys]); - + // Memoized computed values + const fileItems: FileItem[] = useMemo(() => { + if (!files || !Array.isArray(files) || files.length === 0) return []; + return files.flat().filter((item: any) => item && item.name && item.file_path); + }, [files]); + const documentKeys = useMemo(() => { + return fileItems.map(file => file.name); + }, [fileItems]); const updateSelectedDocument = (value: string) => { setSelectedDocument(value); @@ -81,7 +89,6 @@ const DocumentsPreviewPage: React.FC = () => { } }; - const readFile = async (filePath: string) => { const res = await fetch(`/api/read-file`, { method: "POST", @@ -95,16 +102,16 @@ const DocumentsPreviewPage: React.FC = () => { const promises: Promise<{ content: string }>[] = []; // Process documents - documentKeys.forEach(key => { + documentKeys.forEach((key: string) => { if (!(key in textContents)) { newDocuments.push(key); - // @ts-ignore - promises.push(readFile(documents[key])); + const fileItem = fileItems.find(item => item.name === key); + if (fileItem) { + promises.push(readFile(fileItem.file_path)); + } } }); - - if (promises.length > 0) { setDownloadingDocuments(newDocuments); try { @@ -128,18 +135,19 @@ const DocumentsPreviewPage: React.FC = () => { } }; - - - const documentTablesAndCharts = () => { - if (!selectedDocument) return []; - - const tablesList = tables[selectedDocument] || []; - const chartsList = charts[selectedDocument] || []; - return [...tablesList, ...chartsList]; - }; - const handleCreatePresentation = async () => { try { + if (!layoutSchema) { + toast({ + title: "Error", + description: "No layout schema found", + variant: "destructive", + }); + return; + } + + + setShowLoading({ message: "Generating presentation outline...", show: true, @@ -147,40 +155,21 @@ const DocumentsPreviewPage: React.FC = () => { progress: true, }); - const documentPaths = documentKeys.map(key => documents[key]); - const createResponse = await PresentationGenerationApi.getQuestions({ + const documentPaths = fileItems.map((fileItem: FileItem) => fileItem.file_path); + const createResponse = await PresentationGenerationApi.createPresentation({ prompt: config?.prompt ?? "", n_slides: config?.slides ? parseInt(config.slides) : null, - documents: documentPaths, - images: imageKeys, + file_paths: documentPaths, language: config?.language ?? "", - + layout: { + name: 'Professional', + ordered: false, + slides: layoutSchema + } }); - try { - const presentationWithOutlines = await PresentationGenerationApi.titleGeneration({ - presentation_id: createResponse.id, - }); - - dispatch(setPresentationId(presentationWithOutlines.id)); - dispatch(setOutlines(presentationWithOutlines.outlines)); - - setShowLoading({ - message: "", - show: false, - duration: 0, - progress: false, - }); - - router.push("/theme"); - } catch (error) { - console.error("Error in title generation:", error); - toast({ - title: "Error in title generation.", - description: "Please try again.", - variant: "destructive", - }); - } + dispatch(setPresentationId(createResponse.id)); + router.push("/outline"); } catch (error) { console.error("Error in presentation creation:", error); toast({ @@ -206,25 +195,23 @@ const DocumentsPreviewPage: React.FC = () => { // Effects useEffect(() => { - if (allSources.length > 0) { - setSelectedDocument(allSources[0]); + if (documentKeys.length > 0) { + setSelectedDocument(documentKeys[0]); maintainDocumentTexts(); } - }, [allSources]); + }, [documentKeys]); // Render helpers const renderDocumentContent = () => { if (!selectedDocument) return null; const isDocument = documentKeys.includes(selectedDocument); - const hasTablesAndCharts = documentTablesAndCharts().length > 0; if (!isDocument) return null; return (
-
+

Content:

{downloadingDocuments.includes(selectedDocument) ? ( @@ -234,24 +221,6 @@ const DocumentsPreviewPage: React.FC = () => { )}
- {hasTablesAndCharts && ( -
-

Tables And Charts

- {documentTablesAndCharts().map((item, index) => ( -
- {item.markdown && ( - - )} -
- ))} -
- )}
); }; @@ -268,13 +237,11 @@ const DocumentsPreviewPage: React.FC = () => { size={20} /> - - {documentKeys.length > 0 && (

DOCUMENTS

- {documentKeys.map((key) => ( + {documentKeys.map((key: string) => (
updateSelectedDocument(key)} @@ -294,30 +261,6 @@ const DocumentsPreviewPage: React.FC = () => {
)} - - {imageKeys.length > 0 && ( -
-

IMAGES

-
- {imageKeys.map((key) => ( -
updateSelectedDocument(key)} - className="cursor-pointer" - > - Uploaded image -
- ))} -
-
- )}
); }; diff --git a/servers/nextjs/app/(presentation-generator)/hooks/useLayoutSchema.ts b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutSchema.ts new file mode 100644 index 00000000..abc7f494 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/hooks/useLayoutSchema.ts @@ -0,0 +1,105 @@ +import { useState, useEffect } from "react"; +import { toast } from "@/hooks/use-toast"; +import { zodToJsonSchema } from "zod-to-json-schema"; + +interface LayoutInfo { + id: string; + name?: string; + description?: string; + json_schema: any; +} + +// interface LayoutStructure { +// name: string; +// ordered: boolean; +// slides: LayoutInfo[]; +// } + +const useLayoutSchema = () => { + const [layoutSchema, setLayoutSchema] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadLayouts = async () => { + try { + setLoading(true); + setError(null); + const response = await fetch('/api/layouts'); + const layoutFiles = await response.json(); + const layouts = await extractSchema(layoutFiles); + // console.log(layouts); + setLayoutSchema(layouts || []); + } catch (err: unknown) { + const errorMessage = err instanceof Error ? err.message : 'Failed to load layouts'; + setError(errorMessage); + console.error('Error loading layouts:', err); + } finally { + setLoading(false); + } + }; + + // Auto-load layouts on mount + useEffect(() => { + loadLayouts(); + }, []); + + return { + layoutSchema, + setLayoutSchema, + loading, + error, + refetch: loadLayouts + }; +}; + +export default useLayoutSchema; + + +const extractSchema = async (layoutFiles: string[]) => { + const layouts: LayoutInfo[] = []; + for (const fileName of layoutFiles) { + try { + const file = fileName.replace('.tsx', '').replace('.ts', '') + const module = await import(`@/components/layouts/${file}`) + if (!module.default) { + toast({ + title: `${file} has no default export`, + description: 'Please ensure the layout file exports a default component', + }) + console.warn(`${file} has no default export`) + return + } + if (!module.Schema) { + toast({ + title: `${file} has no Schema export`, + description: 'Please ensure the layout file exports a Schema', + }) + console.warn(`${file} has no Schema export`) + return + } + const layoutId = module.layoutId + if(!layoutId) { + toast({ + title: `${file} has no layoutId`, + description: 'Please ensure the layout file exports a layoutId', + }) + console.warn(`${file} has no layoutId`) + return + } + const layoutName = module.layoutName + const layoutDescription = module.layoutDescription + const jsonSchema = zodToJsonSchema(module.Schema) + const layout = { + id: layoutId, + name: layoutName, + description: layoutDescription, + json_schema: jsonSchema + } + layouts.push(layout) + } catch (error) { + console.error(`Error extracting schema for ${fileName}:`, error) + return null + } + } + return layouts +}; diff --git a/servers/nextjs/app/(presentation-generator)/create/components/OutlineItem.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx similarity index 100% rename from servers/nextjs/app/(presentation-generator)/create/components/OutlineItem.tsx rename to servers/nextjs/app/(presentation-generator)/outline/components/OutlineItem.tsx diff --git a/servers/nextjs/app/(presentation-generator)/create/components/CreatePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx similarity index 71% rename from servers/nextjs/app/(presentation-generator)/create/components/CreatePage.tsx rename to servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 147f4c81..b343f858 100644 --- a/servers/nextjs/app/(presentation-generator)/create/components/CreatePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -20,43 +20,18 @@ import { RootState } from "@/store/store"; import { useSelector, useDispatch } from "react-redux"; import { useRouter } from "next/navigation"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; -import { useToast } from "@/hooks/use-toast"; +import { toast } from "@/hooks/use-toast"; import { setPresentationData, setOutlines, } from "@/store/slices/presentationGeneration"; import { OverlayLoader } from "@/components/ui/overlay-loader"; import Wrapper from "@/components/Wrapper"; +import { jsonrepair } from "jsonrepair"; -const CreatePage = () => { +const OutlinePage = () => { const dispatch = useDispatch(); const router = useRouter(); - const { presentation_id, images, outlines } = useSelector( - (state: RootState) => state.presentationGeneration - ); - const { - - documents, - images: imagesUploaded, - } = useSelector((state: RootState) => state.pptGenUpload); - const { currentTheme, currentColors } = useSelector( - (state: RootState) => state.theme - ); - const { toast } = useToast(); - - const [loadingState, setLoadingState] = useState({ - message: "", - isLoading: false, - showProgress: false, - duration: 0, - }); - const [initialSlideCount, setInitialSlideCount] = useState(0); - - useEffect(() => { - if (outlines && initialSlideCount === 0) { - setInitialSlideCount(outlines.length); - } - }, [outlines]); const sensors = useSensors( useSensor(PointerSensor), @@ -64,6 +39,96 @@ const CreatePage = () => { coordinateGetter: sortableKeyboardCoordinates, }) ); + const { presentation_id, outlines } = useSelector( + (state: RootState) => state.presentationGeneration + ); + + const { currentTheme, currentColors } = useSelector( + (state: RootState) => state.theme + ); + + + 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); + evtSource = new EventSource( + `/api/v1/ppt/outlines/stream?presentation_id=${presentation_id}` + ); + evtSource.onopen = () => { + console.log('connection open'); + }; + + evtSource.addEventListener("response", (event) => { + const data = JSON.parse(event.data); + console.log(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 > 1 + // ) { + // partialData.slides.splice(-1); + + // previousSlidesLength.current = partialData.slides.length + 1; // 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 { + setLoading(false); + setStreaming(false); + + evtSource.close(); + + } catch (error) { + evtSource.close(); + console.error("Error parsing accumulated chunks:", error); + } + accumulatedChunks = ""; + } else if (data.type === "closing") { + setLoading(false); + setStreaming(false); + evtSource.close(); + } + }); + evtSource.onerror = (error) => { + console.error("EventSource failed:", error); + + setLoading(false); + setStreaming(false); + evtSource.close(); + }; + }; + fetchSlides(); + + }, []) + + + const handleDragEnd = (event: any) => { const { active, over } = event; @@ -102,8 +167,7 @@ const CreatePage = () => { name: currentTheme.toLocaleLowerCase(), colors: currentColors, }, - watermark: false, - images: images, + outlines: outlines, }); @@ -112,7 +176,7 @@ const CreatePage = () => { dispatch(setPresentationData(response)); router.push( - `/presentation?id=${presentation_id}&session=${response.session}` + `/presentation?id=${presentation_id}&stream=true` ); } } catch (error) { @@ -133,28 +197,13 @@ const CreatePage = () => { }; const handleAddSlide = () => { - if (!outlines) { - toast({ - title: "Error", - description: "Cannot add slide at this time", - variant: "destructive", - }); - return; - } - if (outlines.length >= initialSlideCount) { - toast({ - title: "Cannot add more slides", - description: - "You can only add back slides that were previously deleted", - variant: "destructive", - }); - return; - } - const newTitleWithCharts = [...outlines, { title: "New Slide", body: "" }]; - dispatch(setOutlines(newTitleWithCharts)); + + // const newTitleWithCharts = [...outlines, { title: "New Slide", body: "" }]; + + // dispatch(setOutlines(newTitleWithCharts)); }; if (!presentation_id) { @@ -175,7 +224,7 @@ const CreatePage = () => {

Outline

-
+ {/*
{ -
+
*/}