chore(nextjs)

This commit is contained in:
shiva raj badu 2025-07-17 15:27:37 +05:45
parent 03afc2ee87
commit 3847336af6
17 changed files with 247 additions and 151 deletions

View file

@ -12,13 +12,23 @@ interface LayoutInfo {
group: string;
}
interface GroupSetting {
id: string;
name: string;
description: string;
ordered: boolean;
isDefault?: boolean;
}
interface GroupedLayoutsResponse {
group: string;
files: string[];
settings: GroupSetting | null;
}
interface LayoutContextType {
layoutSchema: LayoutInfo[] | null;
groupSettings: Record<string, GroupSetting>;
idMapFileNames: Record<string, string>;
idMapSchema: Record<string, z.ZodSchema>;
idMapGroups: Record<string, string>;
@ -37,6 +47,7 @@ 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 [groupSettings, setGroupSettings] = useState<Record<string, GroupSetting>>({});
const [idMapFileNames, setIdMapFileNames] = useState<Record<string, string>>({});
const [idMapSchema, setIdMapSchema] = useState<Record<string, z.ZodSchema>>({});
const [idMapGroups, setIdMapGroups] = useState<Record<string, string>>({});
@ -49,8 +60,23 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const idMapFileNames: Record<string, string> = {};
const idMapSchema: Record<string, z.ZodSchema> = {};
const idMapGroups: Record<string, string> = {};
const groupSettings: Record<string, GroupSetting> = {};
for (const groupData of groupedLayoutsData) {
// Store group settings
if (groupData.settings) {
groupSettings[groupData.group] = groupData.settings;
} else {
// Provide default settings if not available
groupSettings[groupData.group] = {
id: groupData.group,
name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1),
description: `${groupData.group} presentation layouts`,
ordered: false,
isDefault: false
};
}
for (const fileName of groupData.files) {
try {
const file = fileName.replace('.tsx', '').replace('.ts', '');
@ -102,7 +128,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
}
}
return { layouts, idMapFileNames, idMapSchema, idMapGroups };
return { layouts, idMapFileNames, idMapSchema, idMapGroups, groupSettings };
};
const loadLayouts = async () => {
@ -119,6 +145,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const response = await extractSchema(groupedLayoutsData);
setLayoutSchema(response?.layouts || []);
setGroupSettings(response?.groupSettings || {});
setIdMapFileNames(response?.idMapFileNames || {});
setIdMapSchema(response?.idMapSchema || {});
setIdMapGroups(response?.idMapGroups || {});
@ -200,6 +227,7 @@ export const LayoutProvider: React.FC<{ children: ReactNode }> = ({ children })
const contextValue: LayoutContextType = {
layoutSchema,
groupSettings,
idMapFileNames,
idMapSchema,
idMapGroups,

View file

@ -0,0 +1,68 @@
'use client'
import React, { useMemo } from 'react';
import { useLayout } from '../context/LayoutContext';
import { useSelector } from 'react-redux';
import { RootState } from '@/store/store';
interface LayoutInfo {
id: string;
name?: string;
description?: string;
json_schema: any;
group: string;
}
export const useGroupLayouts = () => {
const { layoutSchema, getLayout, loading } = useLayout();
const { presentationData } = useSelector((state: RootState) => state.presentationGeneration);
// Get the selected group name from presentation data
const selectedGroupName = presentationData?.layouts?.name?.toLowerCase() ?? null;
// Filter layouts to only include those from the selected group
const groupLayouts = useMemo(() => {
if (!layoutSchema || !selectedGroupName) return [];
return layoutSchema.filter(layout => layout.group === selectedGroupName);
}, [layoutSchema, selectedGroupName]);
// Get group-specific layout component with validation
const getGroupLayout = useMemo(() => {
return (layoutId: string) => {
// First check if the layout exists in the current group
const groupLayout = groupLayouts.find(layout => layout.id === layoutId);
if (groupLayout) {
return getLayout(layoutId);
}
// If layout not found in group, return null
console.warn(`Layout ${layoutId} not found in group ${selectedGroupName}`);
return null;
};
}, [groupLayouts, selectedGroupName, getLayout]);
// Render slide content with group validation
const renderSlideContent = useMemo(() => {
return (slide: any) => {
const Layout = getGroupLayout(slide.layout);
if (!Layout) {
return (
<div className="flex flex-col items-center justify-center h-full bg-gray-100 rounded-lg">
<p className="text-gray-600 text-center text-sm">
Layout &quot;{slide.layout}&quot; not found in &quot;{selectedGroupName}&quot; group
</p>
</div>
);
}
return <Layout data={slide.content} />;
};
}, [getGroupLayout, selectedGroupName]);
return {
groupLayouts,
selectedGroupName,
getGroupLayout,
renderSlideContent,
loading
};
};

View file

@ -21,9 +21,9 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
selectedLayoutGroup,
onSelectLayoutGroup
}) => {
const { layoutSchema, getLayout, loading } = useLayout();
const { layoutSchema, groupSettings, getLayout, loading } = useLayout();
// Create layout groups from the loaded layout schema
// Convert layoutSchema to grouped format using actual group settings
const layoutGroups: LayoutGroup[] = React.useMemo(() => {
if (!layoutSchema || layoutSchema.length === 0) return [];
@ -37,27 +37,29 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
groupMap.get(groupName)?.push(layout);
});
// Convert to LayoutGroup format
// Convert to LayoutGroup format using actual group settings
const groups: LayoutGroup[] = [];
groupMap.forEach((layouts, groupName) => {
const settings = groupSettings[groupName];
const group: LayoutGroup = {
id: groupName,
name: groupName.charAt(0).toUpperCase() + groupName.slice(1),
description: getGroupDescription(groupName),
ordered: getGroupOrdered(groupName),
isDefault: groupName === 'professional',
slides: layouts.map(layout => layout.id)
id: settings?.id || groupName,
name: settings?.name || groupName.charAt(0).toUpperCase() + groupName.slice(1),
description: settings?.description || `${groupName} presentation layouts`,
ordered: settings?.ordered || false,
isDefault: settings?.isDefault || false,
slides: layouts.map((layout: any) => layout.id)
};
groups.push(group);
});
// Sort groups to put default first
// Sort groups to put default first, then by name
return groups.sort((a, b) => {
if (a.isDefault) return -1;
if (b.isDefault) return 1;
if (a.isDefault && !b.isDefault) return -1;
if (!a.isDefault && b.isDefault) return 1;
return a.name.localeCompare(b.name);
});
}, [layoutSchema]);
}, [layoutSchema, groupSettings]);
// Auto-select first group when groups are loaded
useEffect(() => {
@ -67,22 +69,6 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
}
}, [layoutGroups, selectedLayoutGroup, onSelectLayoutGroup]);
const getGroupDescription = (groupName: string): string => {
const descriptions: Record<string, string> = {
professional: 'Clean, corporate designs perfect for business presentations',
modern: 'Contemporary designs with clean lines and sophisticated layouts',
default: 'Standard layouts suitable for general presentations',
creative: 'Vibrant, artistic layouts for innovative presentations',
minimal: 'Simple, focused layouts that emphasize content'
};
return descriptions[groupName] || `${groupName} presentation layouts`;
};
const getGroupOrdered = (groupName: string): boolean => {
const orderedGroups = ['professional', 'modern'];
return orderedGroups.includes(groupName);
};
const renderLayoutPreview = (layoutId: string) => {
const Layout = getLayout(layoutId);
if (!Layout) {
@ -112,14 +98,6 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
if (loading) {
return (
<div className="space-y-6">
<div className="mb-6">
<h5 className="text-lg font-medium mb-2">
Loading Layout Styles...
</h5>
<p className="text-gray-600 text-sm">
Please wait while we load the available presentation styles.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="p-4 rounded-lg border border-gray-200 bg-gray-50 animate-pulse">
@ -140,8 +118,8 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
if (layoutGroups.length === 0) {
return (
<div className="space-y-6">
<div className="mb-6">
<h5 className="text-lg font-medium mb-2">
<div className="text-center py-8">
<h5 className="text-lg font-medium mb-2 text-gray-700">
No Layout Styles Available
</h5>
<p className="text-gray-600 text-sm">
@ -154,15 +132,6 @@ const LayoutSelection: React.FC<LayoutSelectionProps> = ({
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

View file

@ -18,6 +18,7 @@ import Wrapper from "@/components/Wrapper";
import { jsonrepair } from "jsonrepair";
import OutlineContent from "./OutlineContent";
import LayoutSelection from "./LayoutSelection";
import { useLayout } from "../../context/LayoutContext";
interface LayoutGroup {
id: string;
@ -31,6 +32,7 @@ interface LayoutGroup {
const OutlinePage = () => {
const dispatch = useDispatch();
const router = useRouter();
const { layoutSchema } = useLayout();
const { presentation_id, outlines } = useSelector(
(state: RootState) => state.presentationGeneration
@ -190,13 +192,27 @@ const OutlinePage = () => {
});
try {
// Prepare layout data in the expected format
// Collect the actual schemas for layouts in the selected group
const groupLayoutSchemas = selectedLayoutGroup.slides
.map(slideId => {
const layout = layoutSchema?.find(l => l.id === slideId);
return layout ? {
id: layout.id,
name: layout.name,
description: layout.description,
json_schema: layout.json_schema
} : null;
})
.filter(schema => schema !== null);
// Prepare layout data in the expected format with schemas
const layoutData = {
name: selectedLayoutGroup.name,
ordered: selectedLayoutGroup.ordered,
slides: selectedLayoutGroup.slides
slides: groupLayoutSchemas
};
console.log("layoutData", layoutData);
const response = await PresentationGenerationApi.presentationPrepare({
presentation_id: presentation_id,
outlines: outlines,

View file

@ -17,8 +17,7 @@ import {
} from "@/store/slices/presentationGeneration";
import { toast } from "@/hooks/use-toast";
import { PresentationGenerationApi } from "../../services/api/presentation-generation";
import { setThemeColors, ThemeColors } from "../../store/themeSlice";
import { ThemeType } from "../../upload/type";
import LoadingState from "../../components/LoadingState";
import Header from "../components/Header";
import { Loader2 } from "lucide-react";
@ -135,7 +134,6 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
);
evtSource.onopen = () => {
setColorsVariables(currentColors, currentTheme);
};
evtSource.addEventListener("response", (event) => {
@ -157,7 +155,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
partialData.slides.splice(-1);
dispatch(
setPresentationData({
presentation: null,
...partialData,
slides: partialData.slides,
})
);
@ -174,18 +172,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
dispatch(setPresentationData(data.presentation));
dispatch(setStreaming(false));
if (data.presentation.theme) {
dispatch(
setThemeColors({
...data.presentation.presentation.theme.colors,
theme: data.presentation.presentation.theme.name as ThemeType,
})
);
setColorsVariables(
data.presentation.presentation.theme.colors,
data.presentation.presentation.theme.name as ThemeType
);
}
setLoading(false);
evtSource.close();
@ -200,18 +187,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
accumulatedChunks = "";
} else if (data.type === "closing") {
dispatch(setPresentationData(data.presentation));
if (data.presentation.theme) {
dispatch(
setThemeColors({
...data.presentation.presentation.theme.colors,
theme: data.presentation.presentation.theme.name as ThemeType,
})
);
setColorsVariables(
data.presentation.presentation.theme.colors,
data.presentation.presentation.theme.name as ThemeType
);
}
setLoading(false);
dispatch(setStreaming(false));
evtSource.close();
@ -275,13 +251,7 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
setLoading(false);
}
};
const setColorsVariables = (colors: ThemeColors, theme: ThemeType) => {
const root = document.documentElement;
Object.entries(colors).forEach(([key, value]) => {
const cssKey = key.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
root.style.setProperty(`--${theme}-${cssKey}`, value);
});
};
// Function to toggle fullscreen
const toggleFullscreen = () => {
if (!document.fullscreenElement) {
@ -316,21 +286,21 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
);
};
if (isPresentMode) {
return (
<PresentationMode
// if (isPresentMode) {
// return (
// <PresentationMode
slides={presentationData?.slides!}
currentSlide={currentSlide}
currentTheme={currentTheme}
isFullscreen={isFullscreen}
onFullscreenToggle={toggleFullscreen}
onExit={handlePresentExit}
onSlideChange={handleSlideChange}
language={presentationData?.presentation?.language || "English"}
/>
);
}
// slides={presentationData?.slides!}
// currentSlide={currentSlide}
// currentTheme={currentTheme}
// isFullscreen={isFullscreen}
// onFullscreenToggle={toggleFullscreen}
// onExit={handlePresentExit}
// onSlideChange={handleSlideChange}
// language={presentationData?.presentation?.language || "English"}
// />
// );
// }
// Regular view
return (

View file

@ -1,5 +1,5 @@
"use client";
import React, { useState, useEffect, useMemo } from "react";
import React, { useState, useEffect } from "react";
import { LayoutList, ListTree, PanelRightOpen, X } from "lucide-react";
import ToolTip from "@/components/ToolTip";
import { Button } from "@/components/ui/button";
@ -22,7 +22,7 @@ import {
import { setPresentationData } from "@/store/slices/presentationGeneration";
import { SortableSlide } from "./SortableSlide";
import { SortableListItem } from "./SortableListItem";
import { useLayout } from "../../context/LayoutContext";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
interface SidePanelProps {
selectedSlide: number;
@ -50,18 +50,9 @@ const SidePanel = ({
);
console.log('presentationData', presentationData)
const dispatch = useDispatch();
const { getLayout } = useLayout();
// Memoized slide renderer using layout cache
const renderSlideContent = useMemo(() => {
return (slide: any) => {
const Layout = getLayout(slide.layout);
if (!Layout) {
return <div>Layout not found</div>;
}
return <Layout data={slide.content} />;
};
}, [getLayout]);
// Use the centralized group layouts hook
const { renderSlideContent } = useGroupLayouts();
useEffect(() => {
if (window.innerWidth < 768) {

View file

@ -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 { useLayout } from "../../context/LayoutContext";
import { useGroupLayouts } from "../../hooks/useGroupLayouts";
interface SlideContentProps {
slide: any;
@ -37,18 +37,24 @@ const SlideContent = ({
const { presentationData, isStreaming } = useSelector(
(state: RootState) => state.presentationGeneration
);
const { getLayout, loading } = useLayout();
// Use the centralized group layouts hook
const { groupLayouts, getGroupLayout, loading } = useGroupLayouts();
// Memoized layout component to prevent re-renders
const LayoutComponent = useMemo(() => {
const Layout = getLayout(slide.layout);
const Layout = getGroupLayout(slide.layout);
if (!Layout) {
return () => <div className="flex flex-col items-center justify-center h-full">
Layout not found
</div>;
return () => (
<div className="flex flex-col items-center justify-center h-full bg-gray-100 rounded-lg">
<p className="text-gray-600 text-center">
Layout "{slide.layout}" not found in current group
</p>
</div>
);
}
return Layout;
}, [slide.layout, getLayout]);
}, [slide.layout, getGroupLayout]);
const handleSubmit = async () => {
const element = document.getElementById(
@ -96,7 +102,7 @@ const SlideContent = ({
const newSlide: Slide = getEmptySlideContent(
type,
index + 1,
presentationData?.presentation!.id!
presentationData?.id!
);
dispatch(addSlide({ slide: newSlide, index: index + 1 }));
@ -172,6 +178,7 @@ const SlideContent = ({
<NewSlide
onSelectLayout={(type) => handleNewSlide(type, slide.index)}
setShowNewSlideSelection={setShowNewSlideSelection}
/>
)}
{!isStreaming && (

View file

@ -408,6 +408,7 @@ export class PresentationGenerationApi {
n_slides,
file_paths,
language,
}: {
prompt: string;
@ -427,6 +428,8 @@ export class PresentationGenerationApi {
n_slides,
file_paths,
language,
}),
cache: "no-cache",
}

View file

@ -41,7 +41,6 @@ const UploadPage = () => {
const dispatch = useDispatch();
const { toast } = useToast();
// State management
const [files, setFiles] = useState<File[]>([]);
const [config, setConfig] = useState<PresentationConfig>({
@ -87,9 +86,6 @@ const UploadPage = () => {
});
return false;
}
return true;
};
@ -165,7 +161,6 @@ const UploadPage = () => {
n_slides: config?.slides ? parseInt(config.slides) : null,
file_paths: [],
language: config?.language ?? "",
});
dispatch(setPresentationId(createResponse.id));

View file

@ -2,6 +2,14 @@ import { NextResponse } from 'next/server'
import { promises as fs } from 'fs'
import path from 'path'
interface GroupSetting {
id: string;
name: string;
description: string;
ordered: boolean;
isDefault?: boolean;
}
export async function GET() {
try {
// Get the path to the presentation-layouts directory
@ -15,9 +23,9 @@ export async function GET() {
.filter(item => item.isDirectory())
.map(dir => dir.name)
const allLayouts: { group: string; files: string[] }[] = []
const allLayouts: { group: string; files: string[]; settings: GroupSetting | null }[] = []
// Scan each group directory for layout files
// Scan each group directory for layout files and settings
for (const groupName of groupDirectories) {
try {
const groupPath = path.join(layoutsDirectory, groupName)
@ -32,10 +40,29 @@ export async function GET() {
file !== 'setting.json'
)
// Read settings.json if it exists
let settings: GroupSetting | null = null
const settingsPath = path.join(groupPath, 'setting.json')
try {
const settingsContent = await fs.readFile(settingsPath, 'utf-8')
settings = JSON.parse(settingsContent) as GroupSetting
} catch (settingsError) {
console.warn(`No settings.json found for group ${groupName} or invalid JSON`)
// Provide default settings if setting.json is missing or invalid
settings = {
id: groupName,
name: groupName.charAt(0).toUpperCase() + groupName.slice(1),
description: `${groupName} presentation layouts`,
ordered: false,
isDefault: false
}
}
if (layoutFiles.length > 0) {
allLayouts.push({
group: groupName,
files: layoutFiles
files: layoutFiles,
settings: settings
})
}
} catch (error) {

View file

@ -1,7 +1,7 @@
'use client'
import { useState, useEffect } from 'react'
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse } from '../types'
import { LayoutInfo, LayoutGroup, GroupedLayoutsResponse, GroupSetting } from '../types'
import { toast } from '@/hooks/use-toast'
interface UseLayoutLoaderReturn {
@ -40,6 +40,15 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
for (const groupData of groupedLayoutsData) {
const groupLayouts: LayoutInfo[] = []
// Use settings from setting.json or provide defaults
const groupSettings: GroupSetting = groupData.settings || {
id: groupData.group,
name: groupData.group.charAt(0).toUpperCase() + groupData.group.slice(1),
description: `${groupData.group} presentation layouts`,
ordered: false,
isDefault: false
}
for (const fileName of groupData.files) {
try {
const layoutName = fileName.replace('.tsx', '').replace('.ts', '')
@ -115,11 +124,19 @@ export const useLayoutLoader = (): UseLayoutLoaderReturn => {
if (groupLayouts.length > 0) {
loadedGroups.push({
group: groupData.group,
layouts: groupLayouts
layouts: groupLayouts,
settings: groupSettings
})
}
}
// Sort groups to put default first, then by name
loadedGroups.sort((a, b) => {
if (a.settings.isDefault && !b.settings.isDefault) return -1
if (!a.settings.isDefault && b.settings.isDefault) return 1
return a.settings.name.localeCompare(b.settings.name)
})
if (allLayouts.length === 0) {
toast({
title: 'No valid layouts found',

View file

@ -11,14 +11,24 @@ export interface LayoutInfo {
group: string
}
export interface GroupSetting {
id: string;
name: string;
description: string;
ordered: boolean;
isDefault?: boolean;
}
export interface LayoutGroup {
group: string
layouts: LayoutInfo[]
settings: GroupSetting
}
export interface GroupedLayoutsResponse {
group: string
files: string[]
settings: GroupSetting | null
}
export interface LoadingState {

View file

@ -2,5 +2,6 @@
"id": "default",
"name": "Default",
"description": "Default layout for presentations",
"ordered": true
"ordered": true,
"isDefault": false
}

View file

@ -71,7 +71,7 @@ const CardSlideLayout: React.FC<CardSlideLayoutProps> = ({ data: slideData }) =>
return (
<div
className="relative w-full aspect-[16/9] flex flex-col bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
className="relative w-full aspect-[16/9] flex flex-col bg-white overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
style={slideData?.backgroundImage ? {
backgroundImage: `linear-gradient(135deg, rgba(0,0,0,0.5), rgba(0,0,0,0.7)), url(${slideData.backgroundImage.__image_url__})`,
backgroundSize: 'cover',

View file

@ -2,5 +2,6 @@
"id": "modern",
"name": "Modern",
"description": "Contemporary designs with clean lines and sophisticated layouts",
"ordered": true
"ordered": true,
"isDefault": true
}

View file

@ -92,7 +92,7 @@ const StatisticsSlideLayout: React.FC<StatisticsSlideLayoutProps> = ({ data: sli
return (
<div
className="relative w-full aspect-[16/9] bg-gradient-to-br from-slate-50 via-white to-slate-100 overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
className="relative w-full aspect-[16/9] bg-white overflow-hidden shadow-2xl border border-slate-200 print:shadow-none print:border-gray-300"
style={slideData?.backgroundImage ? {
backgroundImage: `url("${slideData.backgroundImage.__image_url__}")`,
backgroundSize: 'cover',

View file

@ -36,22 +36,15 @@ export interface Chart {
};
}
export interface PresentationData {
presentation: {
created_at: string;
data: string | null;
file: string;
id: string;
user_id: string;
n_slides: number;
prompt: string;
summary: string | null;
theme: string | null;
title: string;
titles: string[];
thumbnail: string | null;
language: string;
} | null;
id: string;
language: string;
layouts: {
name: string;
ordered: boolean;
slides: any[];
};
n_slides: number;
title: string;
slides: any;
}