Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout
This commit is contained in:
commit
98cc85a931
31 changed files with 712 additions and 475 deletions
|
|
@ -0,0 +1,209 @@
|
|||
"use client";
|
||||
import React, { createContext, useContext, useEffect, useState, ReactNode } from 'react';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import * as z from 'zod';
|
||||
|
||||
interface LayoutInfo {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
json_schema: any;
|
||||
}
|
||||
|
||||
interface LayoutContextType {
|
||||
layoutSchema: LayoutInfo[] | null;
|
||||
idMapFileNames: Record<string, string>;
|
||||
idMapSchema: Record<string, z.ZodSchema>;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
getLayout: (layoutId: string) => React.ComponentType<{ data: any }> | null;
|
||||
isPreloading: boolean;
|
||||
cacheSize: number;
|
||||
refetch: () => Promise<void>;
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<LayoutContextType | undefined>(undefined);
|
||||
|
||||
// Global layout cache
|
||||
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
|
||||
|
||||
export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||||
const [layoutSchema, setLayoutSchema] = useState<LayoutInfo[] | null>(null);
|
||||
const [idMapFileNames, setIdMapFileNames] = useState<Record<string, string>>({});
|
||||
const [idMapSchema, setIdMapSchema] = useState<Record<string, z.ZodSchema>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPreloading, setIsPreloading] = useState(false);
|
||||
|
||||
const extractSchema = async (layoutFiles: string[]) => {
|
||||
const layouts: LayoutInfo[] = [];
|
||||
const idMapFileNames: Record<string, string> = {};
|
||||
const idMapSchema: Record<string, z.ZodSchema> = {};
|
||||
|
||||
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`);
|
||||
continue;
|
||||
}
|
||||
|
||||
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`);
|
||||
continue;
|
||||
}
|
||||
|
||||
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`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const layoutName = module.layoutName;
|
||||
const layoutDescription = module.layoutDescription;
|
||||
const jsonSchema = z.toJSONSchema(module.Schema, {
|
||||
override: (ctx) => {
|
||||
delete ctx.jsonSchema.default;
|
||||
},
|
||||
});
|
||||
|
||||
const layout = {
|
||||
id: layoutId,
|
||||
name: layoutName,
|
||||
description: layoutDescription,
|
||||
json_schema: jsonSchema,
|
||||
};
|
||||
|
||||
idMapFileNames[layoutId] = fileName;
|
||||
idMapSchema[layoutId] = module.Schema;
|
||||
layouts.push(layout);
|
||||
} catch (error) {
|
||||
console.error(`Error extracting schema for ${fileName}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return { layouts, idMapFileNames, idMapSchema };
|
||||
};
|
||||
|
||||
const loadLayouts = async () => {
|
||||
if (layoutSchema) return; // Already loaded
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const layoutResponse = await fetch('/api/layouts');
|
||||
const layoutFiles = await layoutResponse.json();
|
||||
const response = await extractSchema(layoutFiles);
|
||||
|
||||
setLayoutSchema(response?.layouts || []);
|
||||
setIdMapFileNames(response?.idMapFileNames || {});
|
||||
setIdMapSchema(response?.idMapSchema || {});
|
||||
|
||||
// Preload layouts after loading schema
|
||||
await preloadLayouts(response?.idMapFileNames || {});
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
|
||||
const preloadLayouts = async (fileNames: Record<string, string>) => {
|
||||
setIsPreloading(true);
|
||||
|
||||
try {
|
||||
const layoutPromises = Object.values(fileNames).map(async (layoutName) => {
|
||||
if (!layoutCache.has(layoutName)) {
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(layoutPromises);
|
||||
} catch (error) {
|
||||
console.error('Error preloading layouts:', error);
|
||||
} finally {
|
||||
setIsPreloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => {
|
||||
const layoutName = idMapFileNames[layoutId];
|
||||
if (!layoutName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return cached layout if available
|
||||
if (layoutCache.has(layoutName)) {
|
||||
return layoutCache.get(layoutName)!;
|
||||
}
|
||||
|
||||
// Create and cache layout if not available
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
return Layout;
|
||||
};
|
||||
|
||||
// Load layouts on mount
|
||||
useEffect(() => {
|
||||
loadLayouts();
|
||||
}, []);
|
||||
|
||||
const contextValue: LayoutContextType = {
|
||||
layoutSchema,
|
||||
idMapFileNames,
|
||||
idMapSchema,
|
||||
loading,
|
||||
error,
|
||||
getLayout,
|
||||
isPreloading,
|
||||
cacheSize: layoutCache.size,
|
||||
refetch: loadLayouts,
|
||||
};
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useLayout = (): LayoutContextType => {
|
||||
const context = useContext(LayoutContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useLayout must be used within a LayoutProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
|
@ -29,7 +29,7 @@ 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";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
|
||||
// Types
|
||||
interface LoadingState {
|
||||
|
|
@ -70,7 +70,7 @@ const DocumentsPreviewPage: React.FC = () => {
|
|||
duration: 10,
|
||||
progress: false,
|
||||
});
|
||||
const { layoutSchema } = useLayoutSchema();
|
||||
const { layoutSchema } = useLayout();
|
||||
|
||||
// Memoized computed values
|
||||
const fileItems: FileItem[] = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -1,86 +0,0 @@
|
|||
import React, { useState, useEffect, useRef } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import useLayoutSchema from "./useLayoutSchema";
|
||||
|
||||
// Global layout cache to persist across component unmounts
|
||||
const layoutCache = new Map<string, React.ComponentType<{ data: any }>>();
|
||||
|
||||
const useLayoutCache = () => {
|
||||
const { idMapFileNames, loading } = useLayoutSchema();
|
||||
const [isPreloading, setIsPreloading] = useState(false);
|
||||
const preloadedRef = useRef(false);
|
||||
|
||||
// Pre-load all layouts when schema is available
|
||||
useEffect(() => {
|
||||
if (!loading && idMapFileNames && Object.keys(idMapFileNames).length > 0 && !preloadedRef.current) {
|
||||
preloadLayouts();
|
||||
preloadedRef.current = true;
|
||||
}
|
||||
}, [idMapFileNames, loading]);
|
||||
|
||||
const preloadLayouts = async () => {
|
||||
if (isPreloading) return;
|
||||
|
||||
setIsPreloading(true);
|
||||
|
||||
try {
|
||||
const layoutPromises = Object.values(idMapFileNames).map(async (layoutName) => {
|
||||
if (!layoutCache.has(layoutName)) {
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(layoutPromises);
|
||||
} catch (error) {
|
||||
console.error('Error preloading layouts:', error);
|
||||
} finally {
|
||||
setIsPreloading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getLayout = (layoutId: string): React.ComponentType<{ data: any }> | null => {
|
||||
const layoutName = idMapFileNames[layoutId];
|
||||
if (!layoutName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return cached layout if available
|
||||
if (layoutCache.has(layoutName)) {
|
||||
return layoutCache.get(layoutName)!;
|
||||
}
|
||||
|
||||
// Create and cache layout if not available
|
||||
const Layout = dynamic(
|
||||
() => import(`@/components/layouts/${layoutName}`),
|
||||
{
|
||||
loading: () => <div className="w-full aspect-[16/9] bg-gray-100 animate-pulse rounded-lg" />,
|
||||
ssr: false,
|
||||
}
|
||||
) as React.ComponentType<{ data: any }>;
|
||||
|
||||
layoutCache.set(layoutName, Layout);
|
||||
return Layout;
|
||||
};
|
||||
|
||||
const clearCache = () => {
|
||||
layoutCache.clear();
|
||||
preloadedRef.current = false;
|
||||
};
|
||||
|
||||
return {
|
||||
getLayout,
|
||||
isPreloading,
|
||||
clearCache,
|
||||
cacheSize: layoutCache.size,
|
||||
};
|
||||
};
|
||||
|
||||
export default useLayoutCache;
|
||||
|
|
@ -1,120 +0,0 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import * as z from 'zod';
|
||||
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 [idMapFileNames, setIdMapFileNames] = useState<Record<string, string>>({});
|
||||
const [idMapSchema, setIdMapSchema] = useState<Record<string, z.ZodSchema>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const loadLayouts = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const layoutResponse = await fetch('/api/layouts');
|
||||
const layoutFiles = await layoutResponse.json();
|
||||
const response = await extractSchema(layoutFiles);
|
||||
|
||||
setLayoutSchema(response?.layouts || []);
|
||||
setIdMapFileNames(response?.idMapFileNames || {});
|
||||
setIdMapSchema(response?.idMapSchema || {});
|
||||
} 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,
|
||||
idMapFileNames,
|
||||
idMapSchema
|
||||
};
|
||||
};
|
||||
|
||||
export default useLayoutSchema;
|
||||
|
||||
|
||||
const extractSchema = async (layoutFiles: string[]) => {
|
||||
const layouts: LayoutInfo[] = [];
|
||||
const idMapFileNames: Record<string, string> = {};
|
||||
const idMapSchema: Record<string, z.ZodSchema> = {};
|
||||
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 = z.toJSONSchema(module.Schema,{
|
||||
override :(ctx)=>{
|
||||
delete ctx.jsonSchema.default
|
||||
},
|
||||
})
|
||||
const layout = {
|
||||
id: layoutId,
|
||||
name: layoutName,
|
||||
description: layoutDescription,
|
||||
json_schema: jsonSchema,
|
||||
}
|
||||
idMapFileNames[layoutId] = fileName
|
||||
idMapSchema[layoutId] = module.Schema
|
||||
layouts.push(layout)
|
||||
} catch (error) {
|
||||
console.error(`Error extracting schema for ${fileName}:`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
return {layouts, idMapFileNames, idMapSchema}
|
||||
};
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { LayoutGroups, LayoutGroup } from "@/components/layouts/layoutGroup";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
import { CheckCircle } from "lucide-react";
|
||||
|
||||
interface LayoutSelectionProps {
|
||||
selectedLayoutGroup: LayoutGroup | null;
|
||||
onSelectLayoutGroup: (group: LayoutGroup) => void;
|
||||
}
|
||||
|
||||
const LayoutSelection: React.FC<LayoutSelectionProps> = ({
|
||||
selectedLayoutGroup,
|
||||
onSelectLayoutGroup
|
||||
}) => {
|
||||
const { getLayout } = useLayout();
|
||||
|
||||
const renderLayoutPreview = (layoutId: string) => {
|
||||
const Layout = getLayout(layoutId);
|
||||
if (!Layout) {
|
||||
return (
|
||||
<div className="w-full h-16 bg-gray-100 rounded flex items-center justify-center">
|
||||
<span className="text-gray-400 text-xs">Preview unavailable</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Sample data for preview
|
||||
const sampleData = {
|
||||
title: "Sample Title",
|
||||
description: "This is a preview of the layout",
|
||||
subtitle: "Sample subtitle",
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-16 overflow-hidden rounded bg-white border">
|
||||
<div className="transform scale-[0.12] origin-top-left w-[833%] h-[833%]">
|
||||
<Layout data={sampleData} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="mb-6">
|
||||
<h5 className="text-lg font-medium mb-2">
|
||||
Select Your Presentation Style
|
||||
</h5>
|
||||
<p className="text-gray-600 text-sm">
|
||||
Choose a layout group that best fits your presentation style and content.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{LayoutGroups.map((group) => (
|
||||
<div
|
||||
key={group.id}
|
||||
onClick={() => onSelectLayoutGroup(group)}
|
||||
className={`relative p-4 rounded-lg border cursor-pointer ${selectedLayoutGroup?.id === group.id
|
||||
? 'border-blue-500 bg-blue-50'
|
||||
: 'border-gray-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
{selectedLayoutGroup?.id === group.id && (
|
||||
<div className="absolute top-3 right-3">
|
||||
<CheckCircle className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-3">
|
||||
<h6 className="text-base font-medium text-gray-900 mb-1">
|
||||
{group.name}
|
||||
</h6>
|
||||
<p className="text-sm text-gray-600">
|
||||
{group.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Layout previews */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
{group.slides.slice(0, 6).map((layoutId, index) => (
|
||||
<div key={index} className="aspect-video">
|
||||
{renderLayoutPreview(layoutId)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between text-sm text-gray-500">
|
||||
<span>{group.slides.length} layouts</span>
|
||||
<span className={`px-2 py-1 rounded text-xs ${group.ordered
|
||||
? 'bg-gray-100 text-gray-700'
|
||||
: 'bg-blue-100 text-blue-700'
|
||||
}`}>
|
||||
{group.ordered ? 'Structured' : 'Flexible'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LayoutSelection;
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { OutlineItem } from "./OutlineItem";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SlideOutline } from "@/store/slices/presentationGeneration";
|
||||
import { FileText } from "lucide-react";
|
||||
|
||||
interface OutlineContentProps {
|
||||
outlines: SlideOutline[] | null;
|
||||
isLoading: boolean;
|
||||
isStreaming: boolean;
|
||||
onDragEnd: (event: any) => void;
|
||||
onAddSlide: () => void;
|
||||
}
|
||||
|
||||
const OutlineContent: React.FC<OutlineContentProps> = ({
|
||||
outlines,
|
||||
isLoading,
|
||||
isStreaming,
|
||||
onDragEnd,
|
||||
onAddSlide
|
||||
}) => {
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h5 className="text-lg font-medium">
|
||||
Presentation Outline
|
||||
</h5>
|
||||
{isStreaming && (
|
||||
<div className="flex items-center text-sm text-blue-600">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
||||
Generating outlines...
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Skeleton loading state */}
|
||||
{isLoading && (
|
||||
<div className="space-y-4">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="flex items-start space-x-3 p-4 border rounded-lg bg-white">
|
||||
<div className="w-6 h-6 bg-gray-200 rounded-full flex-shrink-0"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-5 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="space-y-1">
|
||||
<div className="h-4 bg-gray-100 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-100 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-100 rounded w-4/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-5 h-5 bg-gray-200 rounded flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Outlines content */}
|
||||
{outlines && outlines.length > 0 && (
|
||||
<div className="border rounded-lg p-4 bg-white">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={onDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={outlines?.map((item, index) => ({ id: item.title || `slide-${index}` })) || []}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{outlines?.map((item, index) => (
|
||||
<OutlineItem
|
||||
key={item.title || `slide-${index}`}
|
||||
index={index + 1}
|
||||
slideOutline={item}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onAddSlide}
|
||||
disabled={isLoading || isStreaming}
|
||||
className="w-full mt-4 text-blue-600 border-blue-200"
|
||||
>
|
||||
+ Add Slide
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoading && outlines && outlines.length === 0 && (
|
||||
<div className="text-center py-12 bg-white rounded-lg border-2 border-dashed border-gray-200">
|
||||
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||
<p className="text-gray-600 mb-4">No outlines available</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onAddSlide}
|
||||
className="text-blue-600 border-blue-200"
|
||||
>
|
||||
+ Add First Slide
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OutlineContent;
|
||||
|
|
@ -103,8 +103,8 @@ export function OutlineItem({
|
|||
<div id={`outline-item-${index}`} className="flex flex-col basis-full gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={slideOutline.title || ''}
|
||||
onChange={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })}
|
||||
defaultValue={slideOutline.title || ''}
|
||||
onBlur={(e) => handleSlideChange({ ...slideOutline, title: e.target.value })}
|
||||
className="text-md sm:text-lg flex-1 font-semibold bg-transparent outline-none"
|
||||
placeholder="Title goes here"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,21 +1,8 @@
|
|||
"use client";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { OutlineItem } from "./OutlineItem";
|
||||
import { arrayMove } from "@dnd-kit/sortable";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { RootState } from "@/store/store";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
|
@ -29,26 +16,20 @@ import {
|
|||
import { OverlayLoader } from "@/components/ui/overlay-loader";
|
||||
import Wrapper from "@/components/Wrapper";
|
||||
import { jsonrepair } from "jsonrepair";
|
||||
import { LayoutGroup, getDefaultLayoutGroup } from "@/components/layouts/layoutGroup";
|
||||
import OutlineContent from "./OutlineContent";
|
||||
import LayoutSelection from "./LayoutSelection";
|
||||
|
||||
const OutlinePage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const router = useRouter();
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
})
|
||||
);
|
||||
|
||||
const { presentation_id, outlines } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
|
||||
const { currentTheme, currentColors } = useSelector(
|
||||
(state: RootState) => state.theme
|
||||
);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>('outline');
|
||||
const [selectedLayoutGroup, setSelectedLayoutGroup] = useState<LayoutGroup | null>(getDefaultLayoutGroup());
|
||||
const [loadingState, setLoadingState] = useState({
|
||||
message: "",
|
||||
isLoading: false,
|
||||
|
|
@ -182,6 +163,16 @@ const OutlinePage = () => {
|
|||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedLayoutGroup) {
|
||||
toast({
|
||||
title: "Select Layout Group",
|
||||
description: "Please select a layout group before generating presentation",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generate data
|
||||
setLoadingState({
|
||||
message: "Generating presentation data...",
|
||||
|
|
@ -194,17 +185,11 @@ const OutlinePage = () => {
|
|||
const response = await PresentationGenerationApi.presentationPrepare({
|
||||
presentation_id: presentation_id,
|
||||
outlines: outlines,
|
||||
layoutGroup: selectedLayoutGroup,
|
||||
});
|
||||
|
||||
if (response) {
|
||||
dispatch(setPresentationData(response));
|
||||
|
||||
toast({
|
||||
title: "Success",
|
||||
description: "Presentation generated successfully!",
|
||||
variant: "default",
|
||||
});
|
||||
|
||||
router.push(`/presentation?id=${presentation_id}&stream=true`);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -238,7 +223,6 @@ const OutlinePage = () => {
|
|||
dispatch(setOutlines(updatedOutlines));
|
||||
};
|
||||
|
||||
|
||||
if (!presentation_id) {
|
||||
return (
|
||||
<Wrapper>
|
||||
|
|
@ -248,7 +232,7 @@ const OutlinePage = () => {
|
|||
No Presentation ID Found
|
||||
</h4>
|
||||
<p className="text-gray-600 mb-4">Please start a new presentation.</p>
|
||||
<Button onClick={() => router.push("/upload")} className="bg-[#5146E5] w-full rounded-xl text-base sm:text-lg py-4 sm:py-6 transition-all duration-300 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white mt-4 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<Button onClick={() => router.push("/upload")} className="bg-[#5146E5] w-full rounded-xl text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white mt-4 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
Start New Presentation
|
||||
</Button>
|
||||
</div>
|
||||
|
|
@ -266,129 +250,93 @@ const OutlinePage = () => {
|
|||
duration={loadingState.duration}
|
||||
/>
|
||||
|
||||
<div className="max-w-[1000px] mx-auto px-4 sm:px-6 pb-6">
|
||||
<div className="mt-4 sm:mt-8 font-instrument_sans relative">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-lg sm:text-xl font-instrument_sans font-medium">
|
||||
Outline
|
||||
<div className="max-w-[1200px] mx-auto px-4 sm:px-6 pb-6">
|
||||
<div className="mt-4 sm:mt-8">
|
||||
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<h4 className="text-2xl font-bold mb-2 text-gray-900">
|
||||
Customize Your Presentation
|
||||
</h4>
|
||||
{isStreaming && (
|
||||
<div className="flex items-center text-sm text-blue-600">
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
|
||||
Generating outlines...
|
||||
</div>
|
||||
)}
|
||||
<p className="text-gray-600">
|
||||
Review your outline and select a layout style for your presentation.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Skeleton loading state */}
|
||||
{isLoading && (
|
||||
<div className="space-y-4">
|
||||
{[...Array(5)].map((_, index) => (
|
||||
<div key={index} className="animate-pulse">
|
||||
<div className="flex items-start space-x-3 p-3 sm:p-4 border rounded-lg">
|
||||
<div className="w-6 h-6 bg-gray-200 rounded-full flex-shrink-0"></div>
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="h-5 bg-gray-200 rounded w-3/4"></div>
|
||||
<div className="space-y-1">
|
||||
<div className="h-4 bg-gray-100 rounded w-full"></div>
|
||||
<div className="h-4 bg-gray-100 rounded w-5/6"></div>
|
||||
<div className="h-4 bg-gray-100 rounded w-4/6"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-5 h-5 bg-gray-200 rounded flex-shrink-0"></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="w-full mt-4 h-10 bg-gray-200 rounded-[32px] animate-pulse"></div>
|
||||
</div>
|
||||
)}
|
||||
{/* Tabs */}
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
|
||||
<TabsList className="grid w-[50%] mx-auto grid-cols-2">
|
||||
<TabsTrigger value="outline">Outline & Content</TabsTrigger>
|
||||
<TabsTrigger value="layouts">Layout Style</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Outlines content */}
|
||||
{outlines && outlines.length > 0 && (
|
||||
<div className="border p-2 sm:p-4 md:p-6 rounded-lg">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
<TabsContent value="outline" className="mt-6">
|
||||
<OutlineContent
|
||||
outlines={outlines}
|
||||
isLoading={isLoading}
|
||||
isStreaming={isStreaming}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<SortableContext
|
||||
items={outlines?.map((item, index) => ({ id: item.title || `slide-${index}` })) || []}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{outlines?.map((item, index) => (
|
||||
<OutlineItem
|
||||
key={item.title || `slide-${index}`}
|
||||
index={index + 1}
|
||||
slideOutline={item}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</DndContext>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAddSlide}
|
||||
disabled={isLoading || isStreaming}
|
||||
className="w-full mt-4 text-[#9034EA] border-[#9034EA] rounded-[32px] hover:bg-[#9034EA]/10"
|
||||
>
|
||||
+ Add Slide
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state */}
|
||||
{!isLoading && outlines && outlines.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-600 mb-4">No outlines available</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleAddSlide}
|
||||
className="text-[#9034EA] border-[#9034EA] rounded-[32px]"
|
||||
>
|
||||
+ Add First Slide
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Generate button */}
|
||||
{!isStreaming && <Button
|
||||
disabled={loadingState.isLoading || isLoading || isStreaming || !outlines || outlines.length === 0}
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#5146E5] w-full rounded-[32px] text-base sm:text-lg py-4 sm:py-6 transition-all duration-300 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white mt-4 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg
|
||||
className="mr-2"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_1960_939)">
|
||||
<path
|
||||
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
|
||||
fill="white"
|
||||
onAddSlide={handleAddSlide}
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1960_939">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.75 0.876953)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
{loadingState.isLoading
|
||||
? loadingState.message
|
||||
: isLoading || isStreaming
|
||||
? "Loading..."
|
||||
: "Generate Presentation"}
|
||||
</Button>}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="layouts" className="mt-6">
|
||||
<LayoutSelection
|
||||
selectedLayoutGroup={selectedLayoutGroup}
|
||||
onSelectLayoutGroup={setSelectedLayoutGroup}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Generate button */}
|
||||
<div className="mt-8 pt-6 border-t border-gray-200">
|
||||
<Button
|
||||
disabled={
|
||||
loadingState.isLoading ||
|
||||
isLoading ||
|
||||
isStreaming ||
|
||||
!outlines ||
|
||||
outlines.length === 0 ||
|
||||
!selectedLayoutGroup
|
||||
}
|
||||
onClick={handleSubmit}
|
||||
className="bg-[#5146E5] w-full rounded-lg text-base sm:text-lg py-4 sm:py-6 font-roboto font-semibold hover:bg-[#5146E5]/80 text-white disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
<svg
|
||||
className="mr-2"
|
||||
width="24"
|
||||
height="24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 25 25"
|
||||
fill="none"
|
||||
>
|
||||
<g clipPath="url(#clip0_1960_939)">
|
||||
<path
|
||||
d="M21.217 9.57008L21.463 9.00408C21.8955 8.0028 22.6876 7.2 23.683 6.75408L24.442 6.41508C24.5341 6.37272 24.6121 6.30485 24.6668 6.21951C24.7214 6.13417 24.7505 6.03494 24.7505 5.93358C24.7505 5.83222 24.7214 5.73299 24.6668 5.64765C24.6121 5.56231 24.5341 5.49444 24.442 5.45208L23.725 5.13308C22.7046 4.67446 21.8989 3.84196 21.474 2.80708L21.221 2.19608C21.1838 2.10144 21.119 2.02018 21.035 1.96291C20.951 1.90563 20.8517 1.875 20.75 1.875C20.6483 1.875 20.549 1.90563 20.465 1.96291C20.381 2.02018 20.3162 2.10144 20.279 2.19608L20.026 2.80608C19.6015 3.84116 18.7962 4.67401 17.776 5.13308L17.058 5.45308C16.9662 5.49556 16.8885 5.56342 16.834 5.64865C16.7795 5.73389 16.7506 5.83293 16.7506 5.93408C16.7506 6.03523 16.7795 6.13428 16.834 6.21951C16.8885 6.30474 16.9662 6.3726 17.058 6.41508L17.818 6.75308C18.8132 7.19945 19.6049 8.00261 20.037 9.00408L20.283 9.57008C20.463 9.98408 21.036 9.98408 21.217 9.57008ZM6.55 16.8761H8.704L9.304 15.3761H12.196L12.796 16.8761H14.95L11.75 8.87608H9.75L6.55 16.8761ZM10.75 11.7611L11.396 13.3761H10.104L10.75 11.7611ZM15.75 16.8761V8.87608H17.75V16.8761H15.75ZM3.75 3.87608C3.48478 3.87608 3.23043 3.98144 3.04289 4.16897C2.85536 4.35651 2.75 4.61086 2.75 4.87608V20.8761C2.75 21.1413 2.85536 21.3957 3.04289 21.5832C3.23043 21.7707 3.48478 21.8761 3.75 21.8761H21.75C22.0152 21.8761 22.2696 21.7707 22.4571 21.5832C22.6446 21.3957 22.75 21.1413 22.75 20.8761V11.8761H20.75V19.8761H4.75V5.87608H14.75V3.87608H3.75Z"
|
||||
fill="white"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_1960_939">
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="white"
|
||||
transform="translate(0.75 0.876953)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
{loadingState.isLoading
|
||||
? loadingState.message
|
||||
: isLoading || isStreaming
|
||||
? "Loading..."
|
||||
: !selectedLayoutGroup
|
||||
? "Select a Layout Style"
|
||||
: "Generate Presentation"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -260,18 +260,6 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
try {
|
||||
const data = await DashboardApi.getPresentation(presentation_id);
|
||||
if (data) {
|
||||
if (data.presentation.theme) {
|
||||
dispatch(
|
||||
setThemeColors({
|
||||
...data.presentation.theme.colors,
|
||||
theme: data.presentation.theme.name as ThemeType,
|
||||
})
|
||||
);
|
||||
setColorsVariables(
|
||||
data.presentation.theme.colors,
|
||||
data.presentation.theme.name as ThemeType
|
||||
);
|
||||
}
|
||||
dispatch(setPresentationData(data));
|
||||
setLoading(false);
|
||||
}
|
||||
|
|
@ -393,8 +381,8 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
|
|||
/>
|
||||
<div className="flex-1 h-[calc(100vh-100px)] overflow-y-auto">
|
||||
<div
|
||||
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0 slide-theme"
|
||||
data-theme={currentTheme}
|
||||
className="mx-auto flex flex-col items-center overflow-hidden justify-center p-2 sm:p-6 pt-0 "
|
||||
|
||||
>
|
||||
{!presentationData ||
|
||||
loading ||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import {
|
|||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
import { SortableSlide } from "./SortableSlide";
|
||||
import { SortableListItem } from "./SortableListItem";
|
||||
import useLayoutCache from "../../hooks/useLayoutCache";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
|
||||
interface SidePanelProps {
|
||||
selectedSlide: number;
|
||||
|
|
@ -50,7 +50,7 @@ const SidePanel = ({
|
|||
);
|
||||
console.log('presentationData', presentationData)
|
||||
const dispatch = useDispatch();
|
||||
const { getLayout } = useLayoutCache();
|
||||
const { getLayout } = useLayout();
|
||||
|
||||
// Memoized slide renderer using layout cache
|
||||
const renderSlideContent = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import { useDispatch, useSelector } from "react-redux";
|
|||
import { addSlide, updateSlide } from "@/store/slices/presentationGeneration";
|
||||
import NewSlide from "../../components/slide_layouts/NewSlide";
|
||||
import { getEmptySlideContent } from "../../utils/NewSlideContent";
|
||||
import useLayoutCache from "../../hooks/useLayoutCache";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
|
||||
interface SlideContentProps {
|
||||
slide: any;
|
||||
|
|
@ -37,13 +37,15 @@ const SlideContent = ({
|
|||
const { presentationData, isStreaming } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
const { getLayout } = useLayoutCache();
|
||||
const { getLayout, loading } = useLayout();
|
||||
|
||||
// Memoized layout component to prevent re-renders
|
||||
const LayoutComponent = useMemo(() => {
|
||||
const Layout = getLayout(slide.layout);
|
||||
if (!Layout) {
|
||||
return () => <div>Layout not found</div>;
|
||||
return () => <div className="flex flex-col items-center justify-center h-full">
|
||||
Layout not found
|
||||
</div>;
|
||||
}
|
||||
return Layout;
|
||||
}, [slide.layout, getLayout]);
|
||||
|
|
@ -122,7 +124,7 @@ const SlideContent = ({
|
|||
) {
|
||||
// Scroll to the last slide (newly generated during streaming)
|
||||
const lastSlideIndex = presentationData.slides.length - 1;
|
||||
const slideElement = document.getElementById(`slide-${presentationData.slides[lastSlideIndex].id}`);
|
||||
const slideElement = document.getElementById(`slide-${presentationData.slides[lastSlideIndex].index}`);
|
||||
if (slideElement) {
|
||||
slideElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
|
|
@ -140,7 +142,7 @@ const SlideContent = ({
|
|||
return (
|
||||
<>
|
||||
<div
|
||||
id={`slide-${slide.id}`}
|
||||
id={`slide-${slide.index}`}
|
||||
className=" w-full max-w-[1280px] main-slide flex items-center max-md:mb-4 justify-center relative"
|
||||
>
|
||||
{isStreaming && (
|
||||
|
|
@ -148,7 +150,9 @@ const SlideContent = ({
|
|||
)}
|
||||
<div className={` w-full group `}>
|
||||
{/* render slides */}
|
||||
{slideContent}
|
||||
{loading ? <div className="flex flex-col bg-white aspect-video items-center justify-center h-full">
|
||||
<Loader2 className="w-8 h-8 animate-spin" />
|
||||
</div> : slideContent}
|
||||
|
||||
{!showNewSlideSelection && (
|
||||
<div className="group-hover:opacity-100 hidden md:block opacity-0 transition-opacity my-4 duration-300">
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export function SortableListItem({ slide, index, selectedSlide, onSlideClick }:
|
|||
|
||||
// If the mouse was down for less than 200ms, consider it a click
|
||||
if (timeDiff < 200 && !isDragging) {
|
||||
onSlideClick(slide.id);
|
||||
onSlideClick(slide.index);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -50,10 +50,10 @@ export function SortableListItem({ slide, index, selectedSlide, onSlideClick }:
|
|||
{...listeners}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className={`p-3 cursor-pointer rounded-lg slide-box
|
||||
className={`p-3 cursor-pointer ring-0 border-[3px] rounded-lg slide-box
|
||||
${selectedSlide === index
|
||||
? 'ring-2 ring-[#5141e5] text-white'
|
||||
: 'hover:slide-box/40'
|
||||
? ' border-[#5141e5] '
|
||||
: 'hover:slide-box/40 border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium slide-title">Slide {index + 1}</span>
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende
|
|||
|
||||
// If the mouse was down for less than 200ms, consider it a click
|
||||
if (timeDiff < 200 && !isDragging) {
|
||||
onSlideClick(slide.id);
|
||||
onSlideClick(slide.index);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -51,7 +51,7 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick, rende
|
|||
{...listeners}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
className={` cursor-pointer border-[3px] p-1 shadow-lg rounded-md transition-all duration-200 ${selectedSlide === index ? ' border-[#5141e5]' : 'border-color'
|
||||
className={` cursor-pointer border-[3px] p-1 shadow-lg rounded-md transition-all duration-200 ${selectedSlide === index ? ' border-[#5141e5]' : 'border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<div className=" slide-box relative overflow-hidden aspect-video">
|
||||
|
|
|
|||
|
|
@ -25,7 +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";
|
||||
import { useLayout } from "../../context/LayoutContext";
|
||||
|
||||
// Types for loading state
|
||||
interface LoadingState {
|
||||
|
|
@ -40,7 +40,7 @@ const UploadPage = () => {
|
|||
const router = useRouter();
|
||||
const dispatch = useDispatch();
|
||||
const { toast } = useToast();
|
||||
const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayoutSchema();
|
||||
const { layoutSchema, loading: layoutsLoading, error: layoutsError } = useLayout();
|
||||
|
||||
// State management
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
|
|
|
|||
|
|
@ -39,18 +39,18 @@ export const metadata: Metadata = {
|
|||
}
|
||||
|
||||
const page = () => {
|
||||
|
||||
return (
|
||||
<div className='relative'>
|
||||
|
||||
<Header />
|
||||
<div className='flex flex-col items-center justify-center py-8'>
|
||||
<h1 className='text-3xl font-semibold font-instrument_sans'>Create Presentation </h1>
|
||||
{/* <p className='text-sm text-gray-500'>We will generate a presentation for you</p> */}
|
||||
</div>
|
||||
<UploadPage />
|
||||
</div>)
|
||||
|
||||
<UploadPage />
|
||||
|
||||
</div>)
|
||||
}
|
||||
|
||||
export default page
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import {
|
||||
getHeader,
|
||||
getHeaderForFormData,
|
||||
} from "@/app/(presentation-generator)/services/api/header";
|
||||
|
||||
|
||||
|
|
@ -20,7 +19,7 @@ export interface PresentationResponse {
|
|||
vector_store: any;
|
||||
|
||||
thumbnail: string;
|
||||
slide: any;
|
||||
slides: any[];
|
||||
}
|
||||
|
||||
export class DashboardApi {
|
||||
|
|
@ -28,7 +27,7 @@ export class DashboardApi {
|
|||
static async getPresentations(): Promise<PresentationResponse[]> {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/user_presentations`,
|
||||
`/api/v1/ppt/presentation/all`,
|
||||
{
|
||||
method: "GET",
|
||||
}
|
||||
|
|
@ -49,7 +48,7 @@ export class DashboardApi {
|
|||
static async getPresentation(id: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/presentation?presentation_id=${id}`,
|
||||
`/api/v1/ppt/presentation/?id=${id}`,
|
||||
{
|
||||
method: "GET",
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ export class DashboardApi {
|
|||
static async deletePresentation(presentation_id: string) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/v1/ppt/delete?presentation_id=${presentation_id}`,
|
||||
`/api/v1/ppt/delete?id=${presentation_id}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: getHeader(),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { DashboardApi } from "../api/dashboard";
|
||||
|
|
@ -11,23 +11,23 @@ import {
|
|||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { renderSlideContent } from "@/app/(presentation-generator)/components/slide_config";
|
||||
import { useLayout } from "@/app/(presentation-generator)/context/LayoutContext";
|
||||
|
||||
export const PresentationCard = ({
|
||||
id,
|
||||
title,
|
||||
created_at,
|
||||
thumbnail,
|
||||
theme,
|
||||
slide
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
created_at: string;
|
||||
thumbnail: string;
|
||||
theme: any;
|
||||
slide: any
|
||||
}) => {
|
||||
console.log('slide', slide)
|
||||
const router = useRouter();
|
||||
const { getLayout, loading } = useLayout();
|
||||
|
||||
|
||||
const handlePreview = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
|
@ -61,25 +61,27 @@ export const PresentationCard = ({
|
|||
window.location.reload();
|
||||
};
|
||||
|
||||
const themeName = theme.name;
|
||||
// Create CSS variables object
|
||||
const cssVariables = {
|
||||
'--slide-bg': theme.colors.slideBg,
|
||||
'--slide-title': theme.colors.slideTitle,
|
||||
'--slide-heading': theme.colors.slideHeading,
|
||||
'--slide-description': theme.colors.slideDescription,
|
||||
'--slide-box': theme.colors.slideBox,
|
||||
'--icon-bg': theme.colors.iconBg,
|
||||
'--background': theme.colors.background,
|
||||
'--font-family': theme.colors.fontFamily,
|
||||
} as React.CSSProperties;
|
||||
const LayoutComponent = useMemo(() => {
|
||||
const Layout = getLayout(slide.layout);
|
||||
if (!Layout) {
|
||||
return () => <div className="flex flex-col items-center justify-center h-full">
|
||||
Layout not found
|
||||
</div>;
|
||||
}
|
||||
return Layout;
|
||||
}, [slide.layout, getLayout]);
|
||||
const slideContent = useMemo(() => {
|
||||
return <LayoutComponent data={slide.content} />;
|
||||
}, [LayoutComponent, slide.content]);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Card
|
||||
onClick={handlePreview}
|
||||
data-theme={themeName}
|
||||
|
||||
className="bg-white rounded-[8px] slide-theme cursor-pointer overflow-hidden p-4"
|
||||
style={cssVariables}
|
||||
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Date */}
|
||||
|
|
@ -103,25 +105,6 @@ export const PresentationCard = ({
|
|||
</Popover>
|
||||
</div>
|
||||
|
||||
{/* Thumbnail */}
|
||||
{/* <div className="relative border-2 border-gray-200 aspect-[16/9] rounded-[8px] overflow-hidden">
|
||||
{thumbnail ? (
|
||||
<img
|
||||
src={getStaticFileUrl(thumbnail)}
|
||||
alt={title}
|
||||
className="object-cover h-full w-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center h-full w-full">
|
||||
<p className="text-gray-500 text-sm font-roboto">
|
||||
No thumbnail yet
|
||||
</p>
|
||||
<p className="text-gray-500 text-sm font-roboto">
|
||||
Will be added shortly
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div> */}
|
||||
<div className=" slide-box relative overflow-hidden border aspect-video"
|
||||
style={{
|
||||
|
||||
|
|
@ -129,7 +112,7 @@ export const PresentationCard = ({
|
|||
>
|
||||
<div className="absolute bg-transparent z-40 top-0 left-0 w-full h-full" />
|
||||
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
|
||||
{renderSlideContent(slide, 'English')}
|
||||
{slideContent}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -101,9 +101,10 @@ export const PresentationGrid = ({
|
|||
presentations.map((presentation) => (
|
||||
<PresentationCard
|
||||
key={presentation.id}
|
||||
{...presentation}
|
||||
theme={presentation.theme}
|
||||
slide={presentation.slide}
|
||||
id={presentation.id}
|
||||
title={presentation.title}
|
||||
created_at={presentation.created_at}
|
||||
slide={presentation.slides[0]}
|
||||
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import "./globals.css";
|
|||
import { Providers } from "./providers";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { FooterProvider } from "./(presentation-generator)/context/footerContext";
|
||||
import { LayoutProvider } from "./(presentation-generator)/context/LayoutContext";
|
||||
|
||||
const fraunces = Fraunces({
|
||||
subsets: ["latin"],
|
||||
|
|
@ -102,10 +103,13 @@ export default function RootLayout({
|
|||
className={`$ ${inter.variable} ${fraunces.variable} ${montserrat.variable} ${inria_serif.variable} ${roboto.variable} ${instrument_sans.variable} antialiased`}
|
||||
>
|
||||
<Providers>
|
||||
<FooterProvider>
|
||||
<LayoutProvider>
|
||||
<FooterProvider>
|
||||
|
||||
{children}
|
||||
</FooterProvider>
|
||||
|
||||
{children}
|
||||
</FooterProvider>
|
||||
</LayoutProvider>
|
||||
</Providers>
|
||||
<Toaster />
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ const Type1SlideLayout: React.FC<Type1SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" w-full rounded-sm max-w-[1280px] shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] max-h-[720px] flex items-center aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3 sm:gap-8 md:gap-12 lg:gap-16 w-full">
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const Type2NumberedSlideLayout: React.FC<Type2NumberedSlideLayoutProps> = ({ dat
|
|||
|
||||
const renderGridContent = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 relative gap-4 lg:gap-8 mt-4 lg:mt-12">
|
||||
<div className="grid grid-cols-1 bg-white lg:grid-cols-2 relative gap-4 lg:gap-8 mt-4 lg:mt-12">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
@ -79,7 +79,7 @@ const Type2NumberedSlideLayout: React.FC<Type2NumberedSlideLayoutProps> = ({ dat
|
|||
|
||||
const renderHorizontalContent = () => {
|
||||
return (
|
||||
<div className="flex flex-col lg:flex-row mt-4 lg:mt-12 w-full relative gap-4 lg:gap-8">
|
||||
<div className="flex flex-col lg:flex-row bg-white mt-4 lg:mt-12 w-full relative gap-4 lg:gap-8">
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
|
|
@ -107,7 +107,7 @@ const Type2NumberedSlideLayout: React.FC<Type2NumberedSlideLayoutProps> = ({ dat
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="text-center lg:pb-8 w-full">
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ const Type2SlideLayout: React.FC<Type2SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="text-center lg:pb-8 w-full">
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ const Type2TimelineSlideLayout: React.FC<Type2TimelineSlideLayoutProps> = ({ dat
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm max-w-[1280px] w-full shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
>
|
||||
<div className="text-center lg:pb-8 w-full">
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const Type3SlideLayout: React.FC<Type3SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container shadow-lg rounded-sm w-full max-w-[1280px] px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" shadow-lg rounded-sm w-full max-w-[1280px] px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] font-inter flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="text-center mb-4 lg:mb-16 w-full">
|
||||
|
|
|
|||
|
|
@ -44,14 +44,14 @@ const Type4SlideLayout: React.FC<Type4SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container font-inter rounded-sm w-full max-w-[1280px] px-3 py-[10px] sm:px-12 lg:px-20 sm:py-[40px] lg:py-[86px] shadow-lg max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm w-full max-w-[1280px] px-3 py-[10px] sm:px-12 lg:px-20 sm:py-[40px] lg:py-[86px] shadow-lg max-h-[720px] flex flex-col items-center justify-center aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight mb-4 lg:mb-8">
|
||||
{slideData?.title || 'Chart Analysis'}
|
||||
</h1>
|
||||
|
||||
<div className={`flex w-full items-center ${isFullSizeGraph
|
||||
<div className={`flex w-full items-center ${isFullSizeGraph
|
||||
? "flex-col mt-4 lg:mt-10 gap-2 sm:gap-4 md:gap-6 lg:gap-10"
|
||||
: "mt-4 lg:mt-16 gap-4 sm:gap-8 md:gap-12 lg:gap-16"
|
||||
}`}>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const Type5SlideLayout: React.FC<Type5SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className="rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="flex flex-col lg:flex-row gap-4 sm:gap-18 md:gap-16 items-center w-full">
|
||||
|
|
|
|||
|
|
@ -133,7 +133,7 @@ const Type6SlideLayout: React.FC<Type6SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
|
||||
>
|
||||
<div className="text-center sm:pb-2 lg:pb-8 w-full">
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const Type7SlideLayout: React.FC<Type7SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" rounded-sm w-full max-w-[1280px] font-inter shadow-lg px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex flex-col items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
>
|
||||
<div className="text-center sm:pb-2 lg:pb-8 w-full">
|
||||
<h1 className="text-2xl sm:text-3xl lg:text-4xl xl:text-5xl font-bold text-gray-900 leading-tight">
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ const Type8SlideLayout: React.FC<Type8SlideLayoutProps> = ({ data: slideData })
|
|||
|
||||
return (
|
||||
<div
|
||||
className="slide-container shadow-lg w-full max-w-[1280px] rounded-sm font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
className=" shadow-lg w-full max-w-[1280px] rounded-sm font-inter px-3 sm:px-12 lg:px-20 py-[10px] sm:py-[40px] lg:py-[86px] flex items-center justify-center max-h-[720px] aspect-video bg-white relative z-20 mx-auto"
|
||||
>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-8 lg:gap-16 items-center w-full">
|
||||
{/* Left section - Title and Description */}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ export const ImageSchema = z.object({
|
|||
prompt: z.string().meta({
|
||||
description: "Prompt used to generate the image",
|
||||
}),
|
||||
}).meta({
|
||||
imageType: 'image',
|
||||
image_type:z.literal('image')
|
||||
})
|
||||
|
||||
export const IconSchema = z.object({
|
||||
|
|
@ -18,6 +17,5 @@ export const IconSchema = z.object({
|
|||
prompt: z.string().meta({
|
||||
description: "Prompt used to generate the icon",
|
||||
}),
|
||||
}).meta({
|
||||
imageType: 'icon',
|
||||
image_type:z.literal('icon')
|
||||
})
|
||||
|
|
@ -1,25 +1,97 @@
|
|||
export const ProfessionalLayoutGroup = {
|
||||
export interface LayoutGroup {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
ordered: boolean;
|
||||
isDefault?: boolean;
|
||||
slides: string[];
|
||||
}
|
||||
|
||||
export const ProfessionalLayoutGroup: LayoutGroup = {
|
||||
id: 'professional',
|
||||
name: 'Professional',
|
||||
description: 'Clean, corporate designs perfect for business presentations',
|
||||
ordered: true,
|
||||
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
|
||||
isDefault: true,
|
||||
slides: [
|
||||
'first-slide',
|
||||
'content-slide',
|
||||
'bullet-point-slide',
|
||||
'comparison-slide',
|
||||
'type4-slide',
|
||||
'statistics-slide',
|
||||
'team-slide',
|
||||
'quote-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 = {
|
||||
export const CreativeLayoutGroup: LayoutGroup = {
|
||||
id: 'creative',
|
||||
name: 'Creative',
|
||||
description: 'Vibrant, artistic layouts for innovative and creative presentations',
|
||||
ordered: false,
|
||||
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
|
||||
slides: [
|
||||
'image-slide',
|
||||
'icon-slide',
|
||||
'card-slide',
|
||||
'type1-slide',
|
||||
'type2-slide',
|
||||
'type3-slide',
|
||||
'process-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const ModernLayoutGroup = {
|
||||
export const ModernLayoutGroup: LayoutGroup = {
|
||||
id: 'modern',
|
||||
name: 'Modern',
|
||||
description: 'Contemporary designs with clean lines and sophisticated layouts',
|
||||
ordered: true,
|
||||
slides: ['bullet-point-slide', 'image-slide', 'chart-slide', 'table-slide', 'text-slide', 'title-slide', 'subtitle-slide', 'footer-slide']
|
||||
slides: [
|
||||
'type5-slide',
|
||||
'type6-slide',
|
||||
'type7-slide',
|
||||
'type8-slide',
|
||||
'timeline-slide',
|
||||
'type2-timeline-slide',
|
||||
'number-box-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const MinimalLayoutGroup: LayoutGroup = {
|
||||
id: 'minimal',
|
||||
name: 'Minimal',
|
||||
description: 'Simple, focused layouts that emphasize content over decoration',
|
||||
ordered: false,
|
||||
slides: [
|
||||
'content-slide',
|
||||
'bullet-point-slide',
|
||||
'type2-numbered-slide',
|
||||
'quote-slide',
|
||||
'statistics-slide'
|
||||
]
|
||||
}
|
||||
|
||||
export const LayoutGroups = [
|
||||
ProfessionalLayoutGroup,
|
||||
CreativeLayoutGroup,
|
||||
ModernLayoutGroup,
|
||||
MinimalLayoutGroup
|
||||
];
|
||||
|
||||
export const getDefaultLayoutGroup = (): LayoutGroup => {
|
||||
return LayoutGroups.find(group => group.isDefault) || ProfessionalLayoutGroup;
|
||||
};
|
||||
|
||||
export const getAllLayouts = (): string[] => {
|
||||
const allLayouts = new Set<string>();
|
||||
LayoutGroups.forEach(group => {
|
||||
group.slides.forEach(slide => allLayouts.add(slide));
|
||||
});
|
||||
return Array.from(allLayouts);
|
||||
};
|
||||
|
||||
export const getGroupByLayoutId = (layoutId: string): LayoutGroup | undefined => {
|
||||
return LayoutGroups.find(group => group.slides.includes(layoutId));
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue