Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout

This commit is contained in:
sauravniraula 2025-07-16 00:38:25 +05:45
commit 97104eef5f
No known key found for this signature in database
GPG key ID: 60FCC1B5A5E83326
27 changed files with 677 additions and 351 deletions

View file

@ -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

View file

@ -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<HTMLTextAreaElement>(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 (
<div className="h-full mr-4">
<div className={`overflow-y-auto custom_scrollbar ${hasTablesAndCharts ? "h-[calc(100vh-300px)]" : "h-full"
}`}>
<div className="overflow-y-auto custom_scrollbar h-full">
<div className="h-full w-full max-w-full flex flex-col mb-5">
<h1 className="text-2xl font-medium mb-5">Content:</h1>
{downloadingDocuments.includes(selectedDocument) ? (
@ -234,24 +221,6 @@ const DocumentsPreviewPage: React.FC = () => {
)}
</div>
</div>
{hasTablesAndCharts && (
<div className="py-4">
<h1 className="text-2xl font-medium mb-5">Tables And Charts</h1>
{documentTablesAndCharts().map((item, index) => (
<div
key={index}
className="w-full border rounded-lg p-4 my-4 bg-white shadow-sm"
>
{item.markdown && (
<MarkdownRenderer
key={selectedDocument}
content={item.markdown}
/>
)}
</div>
))}
</div>
)}
</div>
);
};
@ -268,13 +237,11 @@ const DocumentsPreviewPage: React.FC = () => {
size={20}
/>
{documentKeys.length > 0 && (
<div className="mt-8">
<p className="text-xs mt-2 text-[#2E2E2E] opacity-70">DOCUMENTS</p>
<div className="flex flex-col gap-2 mt-6">
{documentKeys.map((key) => (
{documentKeys.map((key: string) => (
<div
key={key}
onClick={() => updateSelectedDocument(key)}
@ -294,30 +261,6 @@ const DocumentsPreviewPage: React.FC = () => {
</div>
</div>
)}
{imageKeys.length > 0 && (
<div className="mt-8">
<p className="text-xs mt-2 text-[#2E2E2E] opacity-70">IMAGES</p>
<div className="flex flex-col gap-2 mt-6">
{imageKeys.map((key) => (
<div
key={key}
onClick={() => updateSelectedDocument(key)}
className="cursor-pointer"
>
<img
className={`${selectedDocument === key
? styles.selected_border
: styles.unselected_border
} ${styles.uploaded_images} rounded-lg h-24 w-full border border-gray-200`}
src={images[key]}
alt="Uploaded image"
/>
</div>
))}
</div>
</div>
)}
</div>
);
};

View file

@ -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<LayoutInfo[] | null>(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(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
};

View file

@ -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<boolean>(false);
const [isLoading, setLoading] = useState<boolean>(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 = () => {
<h4 className="text-lg sm:text-xl font-instrument_sans font-medium mb-4">
Outline
</h4>
<div className="border p-2 sm:p-4 md:p-6 rounded-lg">
{/* <div className="border p-2 sm:p-4 md:p-6 rounded-lg">
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
@ -193,15 +242,11 @@ const CreatePage = () => {
<Button
variant="outline"
onClick={handleAddSlide}
disabled={!outlines || outlines.length >= initialSlideCount}
className={`w-full mt-4 text-[#9034EA] border-[#9034EA] rounded-[32px] ${!outlines || outlines.length >= initialSlideCount
? "opacity-50 cursor-not-allowed"
: ""
}`}
className={`w-full mt-4 text-[#9034EA] border-[#9034EA] rounded-[32px] `}
>
+ Add Slide
</Button>
</div>
</div> */}
</div>
<Button
disabled={loadingState.isLoading}
@ -239,4 +284,4 @@ const CreatePage = () => {
);
};
export default CreatePage;
export default OutlinePage;

View file

@ -1,8 +1,7 @@
import React from 'react'
import CreatePage from './components/CreatePage'
import Header from '@/app/dashboard/components/Header'
import { Metadata } from 'next'
import OutlinePage from './components/OutlinePage'
export const metadata: Metadata = {
title: "Outline Presentation",
description: "Customize and organize your presentation outline. Drag and drop slides, add charts, and generate your presentation with ease.",
@ -26,7 +25,7 @@ const page = () => {
return (
<div className='relative min-h-screen'>
<Header />
<CreatePage />
<OutlinePage />
</div>
)
}

View file

@ -65,7 +65,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
const router = useRouter();
const searchParams = useSearchParams();
const isPresentMode = searchParams.get("mode") === "present";
const session = searchParams.get("session");
const stream = searchParams.get("stream");
const currentSlide = parseInt(
searchParams.get("slide") || `${selectedSlide}` || "0"
);
@ -131,7 +131,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
dispatch(setStreaming(true));
evtSource = new EventSource(
`/api/v1/ppt/generate/stream?presentation_id=${presentation_id}&session=${session}`
`/api/v1/ppt/generate/stream?presentation_id=${presentation_id}`
);
evtSource.onopen = () => {
@ -190,7 +190,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
evtSource.close();
// Remove session parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("session");
newUrl.searchParams.delete("stream");
window.history.replaceState({}, "", newUrl.toString());
} catch (error) {
evtSource.close();
@ -216,7 +216,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
evtSource.close();
// Remove session parameter from URL
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("session");
newUrl.searchParams.delete("stream");
window.history.replaceState({}, "", newUrl.toString());
}
});
@ -231,7 +231,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
};
};
if (session) {
if (stream) {
fetchSlides();
} else {
fetchUserSlides();
@ -408,7 +408,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
/>
))}
</div>
{session && <LoadingState />}
{stream && <LoadingState />}
</div>
) : (
<>

View file

@ -23,15 +23,11 @@ export class PresentationGenerationApi {
}
}
static async uploadDoc(documents: File[], images: File[]) {
static async uploadDoc(documents: File[]) {
const formData = new FormData();
documents.forEach((document) => {
formData.append("documents", document);
});
images.forEach((image) => {
formData.append("images", image);
formData.append("files", document);
});
try {
@ -40,7 +36,6 @@ export class PresentationGenerationApi {
{
method: "POST",
headers: getHeaderForFormData(),
// Remove Content-Type header as browser will set it automatically with boundary
body: formData,
cache: "no-cache",
}
@ -60,7 +55,7 @@ export class PresentationGenerationApi {
static async decomposeDocuments(documentKeys: string[], imageKeys: string[]) {
static async decomposeDocuments(documentKeys: string[]) {
try {
const response = await fetch(
`/api/v1/ppt/files/decompose`,
@ -68,8 +63,7 @@ export class PresentationGenerationApi {
method: "POST",
headers: getHeader(),
body: JSON.stringify({
documents: documentKeys,
images: imageKeys,
file_paths: documentKeys,
}),
cache: "no-cache",
}
@ -93,7 +87,7 @@ export class PresentationGenerationApi {
}) {
try {
const response = await fetch(
`/api/v1/ppt/outlines/generate`,
`/api/v1/ppt/presentation/outlines/generate`,
{
method: "POST",
headers: getHeader(),
@ -409,33 +403,32 @@ export class PresentationGenerationApi {
}
// QUESTIONS
static async getQuestions({
static async createPresentation({
prompt,
n_slides,
documents,
images,
language,
file_paths,
language,
layout
}: {
prompt: string;
n_slides: number | null;
documents?: string[];
images?: string[];
file_paths?: string[];
language: string | null;
layout: any;
}) {
try {
const response = await fetch(
`/api/v1/ppt/create`,
`/api/v1/ppt/presentation/create`,
{
method: "POST",
headers: getHeader(),
body: JSON.stringify({
prompt,
n_slides,
file_paths,
language,
documents,
images,
layout
}),
cache: "no-cache",

View file

@ -126,7 +126,7 @@ const ThemePage = () => {
}
dispatch(setTheme(selectedTheme as ThemeType));
router.push("/create");
router.push("/outline");
};
return (

View file

@ -20,8 +20,6 @@ export function PromptInput({
};
return (
<div className="space-y-2">
<div className="relative">
<Textarea
value={value}
@ -32,7 +30,6 @@ export function PromptInput({
className={`py-4 px-5 border-2 font-medium font-instrument_sans text-base min-h-[150px] max-h-[300px] border-[#5146E5] focus-visible:ring-offset-0 focus-visible:ring-[#5146E5] overflow-y-auto custom_scrollbar `}
/>
</div>
<p
className={`text-sm text-gray-500 font-inter font-medium ${showHint ? "opacity-100" : "opacity-0"
}`}

View file

@ -4,8 +4,7 @@
* This component handles the presentation generation upload process, allowing users to:
* - Configure presentation settings (slides, language)
* - Input prompts
* - Upload supporting documents and images
* - Upload supporting documents
*
* @component
*/
@ -14,11 +13,7 @@
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { useDispatch } from "react-redux";
import {
setError,
setPresentationId,
setOutlines,
} from "@/store/slices/presentationGeneration";
import { setPresentationId } from "@/store/slices/presentationGeneration";
import { ConfigurationSelects } from "./ConfigurationSelects";
import { PromptInput } from "./PromptInput";
import { LanguageType, PresentationConfig } from "../type";
@ -30,6 +25,7 @@ import { PresentationGenerationApi } from "../../services/api/presentation-gener
import { OverlayLoader } from "@/components/ui/overlay-loader";
import Wrapper from "@/components/Wrapper";
import { setPptGenUploadState } from "@/store/slices/presentationGenUpload";
import useLayoutSchema from "../../hooks/useLayoutSchema";
// Types for loading state
interface LoadingState {
@ -40,32 +36,14 @@ interface LoadingState {
extra_info?: string;
}
interface DecomposedResponse {
documents: Record<string, any>;
images: Record<string, any>;
charts: Record<string, any>;
tables: Record<string, any>;
}
interface ProcessedData {
config: PresentationConfig;
documents: Record<string, any>;
images: Record<string, any>;
charts: Record<string, any>;
tables: Record<string, any>;
}
const UploadPage = () => {
const router = useRouter();
const dispatch = useDispatch();
const { toast } = useToast();
const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayoutSchema();
// State management
const [documents, setDocuments] = useState<File[]>([]);
const [images, setImages] = useState<File[]>([]);
const [files, setFiles] = useState<File[]>([]);
const [config, setConfig] = useState<PresentationConfig>({
slides: "8",
language: LanguageType.English,
@ -89,24 +67,6 @@ const UploadPage = () => {
setConfig((prev) => ({ ...prev, [key]: value }));
};
/**
* Handles file uploads and separates them into documents and images
* @param newFiles - Array of files to process
*/
const handleFilesChange = (newFiles: File[]) => {
const { docs, imgs } = newFiles.reduce(
(acc, file) => {
const isImage = file.type?.startsWith("image/");
isImage ? acc.imgs.push(file) : acc.docs.push(file);
return acc;
},
{ docs: [] as File[], imgs: [] as File[] }
);
setDocuments(docs);
setImages(imgs);
};
/**
* Validates the current configuration and files
* @returns boolean indicating if the configuration is valid
@ -120,7 +80,7 @@ const UploadPage = () => {
return false;
}
if (!config.prompt.trim() && documents.length === 0 && images.length === 0) {
if (!config.prompt.trim() && files.length === 0) {
toast({
title: "No Prompt or Document Provided",
variant: "destructive",
@ -128,6 +88,24 @@ const UploadPage = () => {
return false;
}
if (layoutsError) {
toast({
title: "Layouts Error",
description: "Failed to load presentation layouts. Please try again.",
variant: "destructive",
});
return false;
}
if (!layoutSchema || layoutSchema.length === 0) {
toast({
title: "Layouts Not Available",
description: "Presentation layouts are still loading. Please wait.",
variant: "destructive",
});
return false;
}
return true;
};
@ -138,7 +116,7 @@ const UploadPage = () => {
if (!validateConfiguration()) return;
try {
const hasUploadedAssets = documents.length > 0 || images.length > 0;
const hasUploadedAssets = files.length > 0;
if (hasUploadedAssets) {
await handleDocumentProcessing();
@ -151,7 +129,7 @@ const UploadPage = () => {
};
/**
* Handles document processing
* Handles document processing
*/
const handleDocumentProcessing = async () => {
setLoadingState({
@ -159,58 +137,29 @@ const UploadPage = () => {
message: "Processing documents...",
showProgress: true,
duration: 90,
extra_info: documents.length > 0 ? "It might take a few minutes for large documents." : "",
extra_info: files.length > 0 ? "It might take a few minutes for large documents." : "",
});
let documentKeys = [];
let imageKeys = [];
let documents = [];
if (documents.length > 0 || images.length > 0) {
const uploadResponse = await PresentationGenerationApi.uploadDoc(documents, images);
documentKeys = uploadResponse["documents"];
imageKeys = uploadResponse["images"];
if (files.length > 0) {
const uploadResponse = await PresentationGenerationApi.uploadDoc(files);
documents = uploadResponse;
}
const promises: Promise<any>[] = [];
if (documents.length > 0 || images.length > 0) {
if (documents.length > 0) {
promises.push(
PresentationGenerationApi.decomposeDocuments(documentKeys, imageKeys)
PresentationGenerationApi.decomposeDocuments(documents)
);
}
const responses = await Promise.all(promises);
const processedData = processApiResponses(responses);
dispatch(setPptGenUploadState(processedData));
router.push("/documents-preview");
};
/**
* Processes API responses and formats data for state update
*/
const processApiResponses = (responses: (any | DecomposedResponse)[],): ProcessedData => {
const result: ProcessedData = {
dispatch(setPptGenUploadState({
config,
documents: {},
images: {},
charts: {},
tables: {},
};
if (responses.length > 0) {
const decomposedResponse = responses.shift() as DecomposedResponse;
Object.assign(result, {
documents: decomposedResponse.documents || {},
images: decomposedResponse.images || {},
charts: decomposedResponse.charts || {},
tables: decomposedResponse.tables || {},
});
}
return result;
files: responses,
}));
router.push("/documents-preview");
};
/**
@ -224,38 +173,23 @@ const UploadPage = () => {
duration: 30,
});
const createResponse = await PresentationGenerationApi.getQuestions({
// Use the first available layout group for direct generation
const createResponse = await PresentationGenerationApi.createPresentation({
prompt: config?.prompt ?? "",
n_slides: config?.slides ? parseInt(config.slides) : null,
documents: [],
images: [],
file_paths: [],
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));
router.push("/theme");
} catch (error) {
console.error("Error in title generation:", error);
toast({
title: "Error in title generation.",
description: "Please try again.",
variant: "destructive",
});
setLoadingState({
isLoading: false,
message: "",
showProgress: false,
duration: 0,
});
}
dispatch(setPresentationId(createResponse.id));
router.push("/outline");
};
/**
@ -263,7 +197,6 @@ const UploadPage = () => {
*/
const handleGenerationError = (error: any) => {
console.error("Error in presentation generation:", error);
dispatch(setError("Failed to generate presentation"));
setLoadingState({
isLoading: false,
message: "",
@ -277,6 +210,20 @@ 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
@ -297,13 +244,12 @@ const UploadPage = () => {
<PromptInput
value={config.prompt}
onChange={(value) => handleConfigChange("prompt", value)}
data-testid="prompt-input"
/>
</div>
<SupportingDoc
files={[...documents, ...images]}
onFilesChange={handleFilesChange}
files={[...files]}
onFilesChange={setFiles}
data-testid="file-upload-input"
/>
<Button

View file

@ -0,0 +1,247 @@
import { zodToJsonSchema } from 'zod-to-json-schema';
import fs from 'fs';
import * as path from 'path';
interface LayoutInfo {
id: string;
name: string;
description: string;
json_schema: Record<string, any>;
}
interface LayoutGroup {
id: string;
ordered: boolean;
slides: string[];
}
interface LayoutStructure {
name: string;
ordered: boolean;
slides: LayoutInfo[];
}
// Cache for layouts to avoid repeated file system operations
let layoutsCache: LayoutStructure[] | null = null;
/**
* Dynamically imports a layout file and extracts its schema and metadata
*/
async function extractLayoutFromFile(filePath: string, fileName: string): Promise<LayoutInfo | null> {
try {
// Import the layout module dynamically
const module = await import(filePath);
// Check if the module has a Schema export
if (!module.Schema) {
console.warn(`No Schema export found in ${fileName}`);
return null;
}
// Extract layout metadata (optional)
const layoutId = module.layoutId || fileName.replace(/\.tsx?$/, '').toLowerCase().replace(/layout$/, '');
const layoutName = module.layoutName || fileName.replace(/\.tsx?$/, '').replace(/([A-Z])/g, ' $1').trim();
const layoutDescription = module.layoutDescription || `${layoutName} layout for presentations`;
// Convert Zod schema to JSON schema
const jsonSchema = zodToJsonSchema(module.Schema, {
name: `${layoutId}Schema`,
$refStrategy: 'none'
});
return {
id: layoutId,
name: layoutName,
description: layoutDescription,
json_schema: jsonSchema
};
} catch (error: unknown) {
console.error(`Error extracting layout from ${fileName}:`, error);
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
throw new Error(`Failed to extract schema from ${fileName}: ${errorMessage}`);
}
}
/**
* Gets all layout files from the layouts directory
*/
async function getLayoutFiles(): Promise<string[]> {
const layoutsDirectory = path.join(process.cwd(), 'components', 'layouts')
if (! fs.existsSync(layoutsDirectory)) {
throw new Error(`Layouts directory not found at ${layoutsDirectory}`);
}
const files = fs.readdirSync(layoutsDirectory)
// Filter for TypeScript/TSX files, excluding layoutGroup.ts
return files.filter(file =>
(file.endsWith('.ts') || file.endsWith('.tsx')) &&
file !== 'layoutGroup.ts' &&
!file.startsWith('.')
);
}
/**
* Extracts layout groups from layoutGroup.ts file
*/
async function extractLayoutGroups(): Promise<LayoutGroup[]> {
try {
const layoutGroupPath = path.join(process.cwd(), 'components', 'layouts', 'layoutGroup.ts');
if (!fs.existsSync(layoutGroupPath)) {
throw new Error('layoutGroup.ts file not found in layouts directory');
}
const module = await import(layoutGroupPath);
// Extract all exported layout groups
const layoutGroups: LayoutGroup[] = [];
Object.keys(module).forEach(key => {
const exportedItem = module[key];
// Check if it's a layout group object
if (exportedItem &&
typeof exportedItem === 'object' &&
exportedItem.id &&
Array.isArray(exportedItem.slides)) {
layoutGroups.push({
id: exportedItem.id,
ordered: exportedItem.ordered || false,
slides: exportedItem.slides
});
}
});
if (layoutGroups.length === 0) {
throw new Error('No valid layout groups found in layoutGroup.ts');
}
return layoutGroups;
} catch (error) {
console.error('Error extracting layout groups:', error);
throw error;
}
}
/**
* Maps layout information to layout groups
*/
function mapLayoutsToGroups(
layoutInfos: LayoutInfo[],
layoutGroups: LayoutGroup[]
): LayoutStructure[] {
return layoutGroups.map(group => {
const groupSlides: LayoutInfo[] = [];
// Map slides in the group to their layout info
group.slides.forEach(slideId => {
const layoutInfo = layoutInfos.find(layout =>
layout.id === slideId ||
layout.id.replace('-', '') === slideId.replace('-', '') ||
layout.id.toLowerCase() === slideId.toLowerCase()
);
if (layoutInfo) {
groupSlides.push(layoutInfo);
} else {
console.warn(`Layout info not found for slide ID: ${slideId}`);
}
});
return {
name: group.id,
ordered: group.ordered,
slides: groupSlides
};
});
}
/**
* Main function to extract all layouts dynamically
*/
export async function extractLayouts(): Promise<LayoutStructure[]> {
// Return cached layouts if available
if (layoutsCache) {
return layoutsCache;
}
try {
// Get all layout files
const layoutFiles = await getLayoutFiles();
if (layoutFiles.length === 0) {
throw new Error('No layout files found in the layouts directory');
}
// Extract layout information from each file
const layoutPromises = layoutFiles.map(async (fileName) => {
const filePath = path.join(process.cwd(), 'components', 'layouts', fileName);
return extractLayoutFromFile(filePath, fileName);
});
const layoutResults = await Promise.all(layoutPromises);
// Filter out null results (files without valid schemas)
const validLayouts = layoutResults.filter((layout): layout is LayoutInfo => layout !== null);
if (validLayouts.length === 0) {
throw new Error('No valid schemas found in any layout files');
}
// Extract layout groups
const layoutGroups = await extractLayoutGroups();
// Map layouts to groups
const mappedLayouts = mapLayoutsToGroups(validLayouts, layoutGroups);
// Cache the results
layoutsCache = mappedLayouts;
return mappedLayouts;
} catch (error) {
console.error('Error extracting layouts:', error);
throw error;
}
}
/**
* Clears the layouts cache (useful for development)
*/
export function clearLayoutsCache(): void {
layoutsCache = null;
}
/**
* Gets a specific layout by ID
*/
export async function getLayoutById(layoutId: string): Promise<LayoutInfo | null> {
const layouts = await extractLayouts();
for (const group of layouts) {
const layout = group.slides.find(slide => slide.id === layoutId);
if (layout) {
return layout;
}
}
return null;
}
/**
* Gets all available layout IDs
*/
export async function getAllLayoutIds(): Promise<string[]> {
const layouts = await extractLayouts();
const ids: string[] = [];
layouts.forEach(group => {
group.slides.forEach(slide => {
ids.push(slide.id);
});
});
return ids;
}

View file

@ -2,6 +2,10 @@ import React from 'react'
import { zodToJsonSchema } from "zod-to-json-schema";
import * as z from "zod";
export const layoutId = 'bullet-point-slide'
export const layoutName = 'Bullet Point Slide'
export const layoutDescription = 'A slide with a title, subtitle, and a list of bullet points.'
const bulletPointSlideSchema = z.object({
title: z.string().min(3).max(100).default('Key Points').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),
@ -15,7 +19,6 @@ const bulletPointSlideSchema = z.object({
backgroundImage: z.string().optional().describe('URL to background image for the slide')
})
console.log(zodToJsonSchema(bulletPointSlideSchema))
export const Schema = bulletPointSlideSchema

View file

@ -1,6 +1,10 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'conclusion-slide'
export const layoutName = 'Conclusion Slide'
export const layoutDescription = 'A slide with a title, subtitle, key takeaways, call to action, and contact information'
const conclusionSlideSchema = z.object({
title: z.string().min(3).max(100).default('Conclusion').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'content-slide'
export const layoutName = 'Content Slide'
export const layoutDescription = 'A slide with a title, subtitle, and content'
const contentSlideSchema = z.object({
title: z.string().min(3).max(100).default('Slide Title').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'first-slide'
export const layoutName = 'First Slide'
export const layoutDescription = 'A slide with a title, subtitle, author, date, company, and background image'
const firstSlideSchema = z.object({
title: z.string().min(3).max(100).default('Welcome to Our Presentation').describe('Main title of the presentation'),
subtitle: z.string().min(10).max(200).default('Subtitle for the slide').optional().describe('Optional subtitle or tagline'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'image-slide'
export const layoutName = 'Image Slide'
export const layoutDescription = 'A slide with a title, subtitle, image, and content'
const imageSlideSchema = z.object({
title: z.string().min(3).max(100).default('Image Showcase').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).default('Subtitle for the slide').optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'process-slide'
export const layoutName = 'Process Slide'
export const layoutDescription = 'A slide with a title, subtitle, and process steps'
const processSlideSchema = z.object({
title: z.string().min(3).max(100).default('Our Process').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'quote-slide'
export const layoutName = 'Quote Slide'
export const layoutDescription = 'A slide with a title, subtitle, quote, author, author title, company, and author image'
const quoteSlideSchema = z.object({
title: z.string().min(3).max(100).default('Testimonials').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'statistics-slide'
export const layoutName = 'Statistics Slide'
export const layoutDescription = 'A slide with a title, subtitle, and statistics'
const statisticsSlideSchema = z.object({
title: z.string().min(3).max(100).default('Key Statistics').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,10 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'team-slide'
export const layoutName = 'Team Slide'
export const layoutDescription = 'A slide with a title, subtitle, and team members'
const teamSlideSchema = z.object({
title: z.string().min(3).max(100).default('Meet Our Team').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or team description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'timeline-slide'
export const layoutName = 'Timeline Slide'
export const layoutDescription = 'A slide with a title, subtitle, and timeline items'
const timelineSlideSchema = z.object({
title: z.string().min(3).max(100).default('Project Timeline').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -1,6 +1,11 @@
import React from 'react'
import * as z from "zod";
export const layoutId = 'two-column-slide'
export const layoutName = 'Two Column Slide'
export const layoutDescription = 'A slide with a title, subtitle, and two columns of content'
const twoColumnSlideSchema = z.object({
title: z.string().min(3).max(100).default('Two Column Layout').describe('Title of the slide'),
subtitle: z.string().min(3).max(150).optional().describe('Optional subtitle or description'),

View file

@ -0,0 +1,25 @@
export const ProfessionalLayoutGroup = {
id: 'professional',
ordered: true,
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
}
export const CasualLayoutGroup = {
id: 'casual',
ordered: false,
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
}
export const CreativeLayoutGroup = {
id: 'creative',
ordered: false,
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
}
export const ModernLayoutGroup = {
id: 'modern',
ordered: true,
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
}

View file

@ -4,28 +4,13 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface PresentationGenUploadState {
config: PresentationConfig | null;
documents: any;
images: any;
charts: any;
tables: any;
questions: any;
storyResponse: any;
files: any;
}
const initialState: PresentationGenUploadState = {
config: null,
documents: {},
images: {},
charts: {},
tables: {},
questions: [],
storyResponse: {
big_idea: null,
story_type: null,
story: null,
},
files: [],
};
export const presentationGenUploadSlice = createSlice({
@ -38,22 +23,12 @@ export const presentationGenUploadSlice = createSlice({
) => {
const payload = action.payload;
state.config = payload.config!;
state.documents = payload.documents;
state.images = payload.images;
state.charts = payload.charts;
state.tables = payload.tables;
state.questions = payload.questions;
},
setQuestions: (state, action: PayloadAction<any>) => {
state.questions = action.payload;
},
setStoryResponse: (state, action: PayloadAction<any>) => {
state.storyResponse = action.payload;
state.files = payload.files!;
},
},
});
export const { setPptGenUploadState, setQuestions, setStoryResponse } =
export const { setPptGenUploadState, } =
presentationGenUploadSlice.actions;
export default presentationGenUploadSlice.reducer;