Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout
This commit is contained in:
commit
97104eef5f
27 changed files with 677 additions and 351 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -126,7 +126,7 @@ const ThemePage = () => {
|
|||
}
|
||||
dispatch(setTheme(selectedTheme as ThemeType));
|
||||
|
||||
router.push("/create");
|
||||
router.push("/outline");
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}`}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
|
|
|||
25
servers/nextjs/components/layouts/layoutGroup.ts
Normal file
25
servers/nextjs/components/layouts/layoutGroup.ts
Normal 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']
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue