diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/DashboardPage.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/DashboardPage.tsx index 210fe07f..1e5de6ec 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/DashboardPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/components/DashboardPage.tsx @@ -4,6 +4,8 @@ import React, { useState, useEffect } from "react"; import { DashboardApi } from "@/app/(presentation-generator)/services/api/dashboard"; import { PresentationGrid } from "@/app/(presentation-generator)/(dashboard)/dashboard/components/PresentationGrid"; +import Link from "next/link"; +import { ChevronRight } from "lucide-react"; @@ -44,7 +46,49 @@ const DashboardPage: React.FC = () => { }; return ( -
+
+
+
+

+ + Slide Presentations +

+
+ + + + { + + New presentation + New + + } + {/* { + + New Themes + New + + + } */} +
+
+
{
- {(pathname !== "/upload" && pathname !== "/dashboard") && } + {/* {(pathname !== "/upload" && pathname !== "/dashboard") && } */} trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/dashboard" })}> +
diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/loading.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/loading.tsx index 41cd811c..919a55ad 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/loading.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/dashboard/loading.tsx @@ -1,23 +1,26 @@ -import { Skeleton } from '@/components/ui/skeleton' import React from 'react' const loading = () => { return ( -
- - -
- - -
- { - Array.from({ length: 8 }).map((_, index) => ( - - )) - } +
+
+
+
+
+
+
+
- + {[...Array(15)].map((_, i) => ( +
+
+
+
+
+
+
+ ))}
) } diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/layout.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/layout.tsx index 3a67a294..a1b9172e 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/layout.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/layout.tsx @@ -1,13 +1,12 @@ import React from 'react' import DashboardSidebar from './Components/DashboardSidebar' -import DashboardNav from './Components/DashboardNav' const layout = ({ children }: { children: React.ReactNode }) => { return ( -
+
- + {children}
diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx index 33e2a808..c9cc3f37 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingPage.tsx @@ -14,6 +14,7 @@ import LLMProviderSelection from "@/components/LLMSelection"; import Header from "../dashboard/components/Header"; import { LLMConfig } from "@/types/llm_config"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; +import SettingSideBar from "./SettingSideBar"; // Button state interface interface ButtonState { @@ -28,6 +29,8 @@ interface ButtonState { const SettingsPage = () => { const router = useRouter(); const pathname = usePathname(); + const [mode, setMode] = useState<'nanobanana' | 'presenton'>('presenton') + const [selectedProvider, setSelectedProvider] = useState<'text-provider' | 'image-provider'>('text-provider') const userConfigState = useSelector((state: RootState) => state.userConfig); const [llmConfig, setLlmConfig] = useState( userConfigState.llm_config @@ -155,15 +158,30 @@ const SettingsPage = () => { return (
-
+
+ +
+
+
+

+ Settings +

+
+
+
+
- + {mode === 'nanobanana' &&
+

Nano Banana

+
} + {mode === 'presenton' && } +
{/* Fixed Bottom Button */} diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx new file mode 100644 index 00000000..b2eb2d75 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/SettingSideBar.tsx @@ -0,0 +1,59 @@ +import React from 'react' +const SettingSideBar = ({ mode, setMode, selectedProvider, setSelectedProvider }: { mode: 'nanobanana' | 'presenton', setMode: (mode: 'nanobanana' | 'presenton') => void, selectedProvider: 'text-provider' | 'image-provider', setSelectedProvider: (provider: 'text-provider' | 'image-provider') => void }) => { + return ( +
+

FILTER BY:

+
+

Select Mode

+
+ + + + + +
+

Select Provider

+ {mode === 'presenton' &&
+ + +
} + { + mode === 'nanobanana' &&
+ +
+ } +
+
+ ) +} + +export default SettingSideBar diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx index 7a972392..92e26ac3 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/templates/components/TemplatePanel.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useRouter } from "next/navigation"; import { Card } from "@/components/ui/card"; -import { ExternalLink, Loader2, Plus } from "lucide-react"; +import { ChevronRight, ExternalLink, Loader2, Plus } from "lucide-react"; import { templates } from "@/app/presentation-templates"; import { TemplateWithData, TemplateLayoutsWithSettings } from "@/app/presentation-templates/utils"; import { @@ -12,11 +12,12 @@ import { } from "@/app/hooks/useCustomTemplates"; import { CompiledLayout } from "@/app/hooks/compileLayout"; import CreateCustomTemplate from "./CreateCustomTemplate"; +import Link from "next/link"; // Component for rendering custom template card with lazy-loaded previews export const CustomTemplateCard = React.memo(function CustomTemplateCard({ template }: { template: CustomTemplates }) { const router = useRouter(); - const { previewLayouts, loading } = useCustomTemplatePreview(`${template.id}`); + const { previewLayouts, loading, totalLayouts } = useCustomTemplatePreview(`${template.id}`); const handleOpen = useCallback(() => { if (template.id.startsWith('custom-')) { router.push(`/template-preview/${template.id}`) @@ -34,7 +35,7 @@ export const CustomTemplateCard = React.memo(function CustomTemplateCard({ templ - Layouts- {template.layoutCount} + Layouts- {totalLayouts}
@@ -157,7 +158,7 @@ const InbuiltTemplateCard = React.memo(function InbuiltTemplateCard({ }); const LayoutPreview = () => { - const [tab, setTab] = useState<'custom' | 'default'>('custom'); + const [tab, setTab] = useState<'custom' | 'default'>('default'); const router = useRouter(); const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries(); @@ -191,6 +192,33 @@ const LayoutPreview = () => { return (
+
+
+

+ Templates +

+
+ + + + + + New Template + New + + + +
+
+
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx index 4a5ac6fa..3f795157 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/CustomTemplateCard.tsx @@ -28,70 +28,11 @@ LayoutPreview.displayName = 'LayoutPreview'; export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTemplate }: { template: CustomTemplates, onSelectTemplate: (template: string) => void, selectedTemplate: string | null }) => { - const { previewLayouts, loading: customLoading } = useCustomTemplatePreview(template.id); + const { previewLayouts, loading: customLoading, totalLayouts } = useCustomTemplatePreview(template.id); const isSelected = selectedTemplate === template.id; return ( - // { - // onSelectTemplate(template.id); - // }} - // > - //
- //
- //

- // {template.name} - //

- //
- - - - // {/* Layout previews */} - //
- // {customLoading ? ( - // // Loading placeholders - // [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( - //
- // - //
- // )) - // ) : previewLayouts && previewLayouts?.length > 0 ? ( - // // Actual layout previews - using memoized component - // previewLayouts?.slice(0, 4).map((layout: CompiledLayout, index: number) => ( - // - // )) - // ) : ( - // // Empty state placeholders - // [...Array(Math.min(4, template.layoutCount))].map((_, index) => ( - //
- // No preview - //
- // )) - // )} - //
- - - //
- // {isSelected && ( - //
- // Selected - //
- // )} - //
onSelectTemplate(template.id)} @@ -99,7 +40,7 @@ export const CustomTemplateCard = memo(({ template, onSelectTemplate, selectedTe - Layouts- {template.layoutCount} + Layouts- {totalLayouts}
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx index 78d6afdb..aa82ad1f 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlinePage.tsx @@ -48,10 +48,10 @@ const OutlinePage: React.FC = () => { duration={loadingState.duration} /> - -
+ +
- + { + const LayoutComponent = layout.component; + return ( +
+
+
+ +
+
+ ); +}); +BuiltInLayoutPreview.displayName = 'BuiltInLayoutPreview'; + +// Memoized built-in template card +const BuiltInTemplateCard = memo(({ template, isSelected, onSelect }: { + template: TemplateLayoutsWithSettings; + isSelected: boolean; + onSelect: (template: TemplateLayoutsWithSettings) => void; +}) => { + const previewLayouts = useMemo(() => template.layouts.slice(0, 4), [template.layouts]); + const handleClick = useCallback(() => onSelect(template), [onSelect, template]); + + return ( + + + Layouts- {template.layouts.length} + + +
+
+ {previewLayouts.map((layout: TemplateWithData, index: number) => ( + + ))} +
+
+
+
+

+ {template.name} +

+

+ {template.description} +

+
+
+
+ ); +}); +BuiltInTemplateCard.displayName = 'BuiltInTemplateCard'; + interface TemplateSelectionProps { selectedTemplate: (TemplateLayoutsWithSettings | string) | null; onSelectTemplate: (template: TemplateLayoutsWithSettings | string) => void; } -const TemplateSelection: React.FC = ({ +const TemplateSelection: React.FC = memo(({ selectedTemplate, onSelectTemplate }) => { - - useEffect(() => { - const existingScript = document.querySelector( 'script[src*="tailwindcss.com"]' ); @@ -30,146 +97,101 @@ const TemplateSelection: React.FC = ({ script.async = true; document.head.appendChild(script); } - }, []); const { templates: customTemplates, loading: customLoading } = useCustomTemplateSummaries(); + // Stable callback for custom template selection + const handleCustomSelect = useCallback( + (template: TemplateLayoutsWithSettings | string) => onSelectTemplate(template), + [onSelectTemplate] + ); + + // Stable callback for built-in template selection + const handleBuiltInSelect = useCallback( + (template: TemplateLayoutsWithSettings) => onSelectTemplate(template), + [onSelectTemplate] + ); + + // Derive the selected custom template id only when selectedTemplate changes + const selectedCustomId = useMemo( + () => (typeof selectedTemplate === 'string' ? selectedTemplate : null), + [selectedTemplate] + ); + + // Derive the selected built-in template id only when selectedTemplate changes + const selectedBuiltInId = useMemo( + () => (typeof selectedTemplate !== 'string' ? selectedTemplate?.id ?? null : null), + [selectedTemplate] + ); + + // Memoize the custom templates section + const customTemplateCards = useMemo(() => { + if (customLoading) { + return ( +
+ + Loading custom templates... +
+ ); + } + if (customTemplates.length === 0) { + return ( + +

No custom templates yet.

+

+ Custom templates you create will appear here. +

+
+ ); + } + return ( +
+ {customTemplates.map((template: CustomTemplates) => ( + + ))} +
+ ); + }, [customLoading, customTemplates, handleCustomSelect, selectedCustomId]); + + // Memoize the built-in templates list + const builtInTemplateCards = useMemo( + () => + templates.map((template: TemplateLayoutsWithSettings) => ( + + )), + [selectedBuiltInId, handleBuiltInSelect] + ); + return (
- {/* In Built Templates */} -
-

In Built Templates

-
- {templates.map((template: TemplateLayoutsWithSettings) => { - const previewLayouts = template.layouts.slice(0, 4); - - return ( - onSelectTemplate(template)} - > - - Layouts- {template.layouts.length} - - -
-
- {previewLayouts.map((layout: TemplateWithData, index: number) => { - const LayoutComponent = layout.component; - return ( -
-
-
- -
-
- ); - })} -
-
-
-
- -

- {template.name} -

-

- {template.description} -

-
- -
- - // onSelectTemplate(template)} - // > - //
- //
- //

- // {template.name} - //

- - //
- - //

- // {template.description} - //

- - //
- // {previewLayouts.map((layout: TemplateWithData, index: number) => { - // const LayoutComponent = layout.component; - // return ( - //
- //
- //
- // - //
- //
- // ); - // })} - //
- //
- // {typeof selectedTemplate !== 'string' && selectedTemplate?.id === template.id && ( - //
- // Selected - //
- // )} - // - ); - })} -
-
- {/* Custom AI Templates */}
-

Custom AI Templates

+

Custom

+
+ {customTemplateCards} +
+ {/* In Built Templates */} +
+

In Built

+
+ {builtInTemplateCards}
- {customLoading ? ( -
- - Loading custom templates... -
- ) : customTemplates.length === 0 ? ( - -

No custom templates yet.

-

- Custom templates you create will appear here. -

-
- ) : ( -
- {customTemplates.map((template: CustomTemplates) => ( - - - ))} -
- )}
); -}; +}); +TemplateSelection.displayName = 'TemplateSelection'; export default TemplateSelection; diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx index d7d387e9..691cb222 100644 --- a/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/PresentationHeader.tsx @@ -1,17 +1,18 @@ "use client"; import { Button } from "@/components/ui/button"; import { - SquareArrowOutUpRight, Play, Loader2, Redo2, Undo2, RotateCcw, ArrowRightFromLine, + ExternalLink, + MoveUpRight, + ArrowUpRight, } from "lucide-react"; import React, { useState } from "react"; -import Wrapper from "@/components/Wrapper"; import { useRouter, usePathname } from "next/navigation"; import { Popover, @@ -19,27 +20,21 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { PresentationGenerationApi } from "../../services/api/presentation-generation"; -import { OverlayLoader } from "@/components/ui/overlay-loader"; import { useDispatch, useSelector } from "react-redux"; -import Link from "next/link"; import { RootState } from "@/store/store"; import { toast } from "sonner"; -import Announcement from "@/components/Announcement"; import { PptxPresentationModel } from "@/types/pptx_models"; -import HeaderNav from "../../components/HeaderNab"; -import PDFIMAGE from "@/public/pdf.svg"; -import PPTXIMAGE from "@/public/pptx.svg"; -import Image from "next/image"; import { trackEvent, MixpanelEvent } from "@/utils/mixpanel"; import { usePresentationUndoRedo } from "../hooks/PresentationUndoRedo"; import ToolTip from "@/components/ToolTip"; import { clearPresentationData } from "@/store/slices/presentationGeneration"; import { clearHistory } from "@/store/slices/undoRedoSlice"; import { Separator } from "@/components/ui/separator"; +import ThemeSelector from "./ThemeSelector"; const PresentationHeader = ({ presentation_id, @@ -157,28 +152,35 @@ const PresentationHeader = ({ }; const ExportOptions = ({ mobile }: { mobile: boolean }) => ( -
- - +
+

Export as

+
+
+ + + +
@@ -189,12 +191,13 @@ const PresentationHeader = ({ return ( <>
-

{presentationData?.title || "Presentation"}

+

{presentationData?.title || "Presentation"}

{isPresentationSaving &&
} +
@@ -246,10 +249,10 @@ const PresentationHeader = ({ }} disabled={isExporting} > - {isExporting ? : "Export"} + {isExporting ? : "Export"} - + diff --git a/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx b/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx new file mode 100644 index 00000000..65ecec72 --- /dev/null +++ b/servers/nextjs/app/(presentation-generator)/presentation/components/ThemeSelector.tsx @@ -0,0 +1,120 @@ +"use client"; +import React, { useState } from 'react' +// import { Theme } from '@/app/(presentation-generator)/services/api/types' +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { Palette } from 'lucide-react'; + +import { useDispatch } from 'react-redux'; +// import { updateTheme } from '@/store/slices/presentationGeneration'; +import { useRouter } from 'next/navigation'; +import { useFontLoader } from '../../hooks/useFontLoader'; +const ThemeSelector = ({ presentation_id, current_theme, themes: allThemes }: { presentation_id: string, current_theme: any, themes: any[] }) => { + const [currentTheme, setCurrentTheme] = useState(current_theme) + const dispatch = useDispatch() + const router = useRouter() + const applyTheme = async (theme: any) => { + const element = document.getElementById('presentation-slides-wrapper') + if (!element) return; + if (allThemes.length === 0) return; + setCurrentTheme(theme) + clearTheme() + if (!theme.data.colors['graph_0']) { return; } + const cssVariables = { + '--primary-color': theme.data.colors['primary'], + '--background-color': theme.data.colors['background'], + '--card-color': theme.data.colors['card'], + '--stroke': theme.data.colors['stroke'], + '--primary-text': theme.data.colors['primary_text'], + '--background-text': theme.data.colors['background_text'], + '--graph-0': theme.data.colors['graph_0'], + '--graph-1': theme.data.colors['graph_1'], + '--graph-2': theme.data.colors['graph_2'], + '--graph-3': theme.data.colors['graph_3'], + '--graph-4': theme.data.colors['graph_4'], + '--graph-5': theme.data.colors['graph_5'], + '--graph-6': theme.data.colors['graph_6'], + '--graph-7': theme.data.colors['graph_7'], + '--graph-8': theme.data.colors['graph_8'], + '--graph-9': theme.data.colors['graph_9'], + } + Object.entries(cssVariables).forEach(([key, value]) => { + element.style.setProperty(key, value) + }) + // useFontLoader({ [theme.data.fonts.textFont.name]: theme.data.fonts.textFont.url }) + + // Apply fonts to preview container + element.style.setProperty('font-family', `"${theme.data.fonts.textFont.name}"`) + element.style.setProperty('--heading-font-family', `"${theme.data.fonts.textFont.name}"`) + + // dispatch(updateTheme(theme)) + } + const clearTheme = () => { + const element = document.getElementById('presentation-slides-wrapper') + if (!element) return; + element.style.removeProperty('--primary-color'); + element.style.removeProperty('--background-color'); + element.style.removeProperty('--card-color'); + element.style.removeProperty('--stroke'); + element.style.removeProperty('--primary-text'); + element.style.removeProperty('--background-text'); + element.style.removeProperty('--graph-0'); + element.style.removeProperty('--graph-1'); + element.style.removeProperty('--graph-2'); + element.style.removeProperty('--graph-3'); + element.style.removeProperty('--graph-4'); + element.style.removeProperty('--graph-5'); + element.style.removeProperty('--graph-6'); + element.style.removeProperty('--graph-7'); + element.style.removeProperty('--graph-8'); + element.style.removeProperty('--graph-9'); + } + const resetTheme = async () => { + clearTheme(); + + // dispatch(updateTheme({} as any)) + } + + + return ( + + + + + +
+ + +
+
+ + {allThemes && allThemes.length > 0 && allThemes.map((t) => ( +
applyTheme(t)} + className={`text-left group relative`} + > + +
+
+
+
+
+
+
+
+
+
+

+ {t.name} +

+
+ ))} +
+ + + ) +} + +export default ThemeSelector \ No newline at end of file diff --git a/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx b/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx index a7977a07..e067a288 100644 --- a/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx +++ b/servers/nextjs/app/(presentation-generator)/template-preview/page.tsx @@ -5,8 +5,7 @@ import { Card } from "@/components/ui/card"; import { ExternalLink, Loader2, Plus } from "lucide-react"; import { templates } from "@/app/presentation-templates"; -import type { TemplateLayoutsWithSettings } from "@/app/presentation-templates"; -import { TemplateWithData } from "@/app/presentation-templates/utils"; +import { TemplateLayoutsWithSettings, TemplateWithData } from "@/app/presentation-templates/utils"; import { useCustomTemplateSummaries, useCustomTemplatePreview, diff --git a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx index 55248085..e97e810d 100644 --- a/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx +++ b/servers/nextjs/app/(presentation-generator)/upload/components/UploadPage.tsx @@ -46,7 +46,7 @@ const UploadPage = () => { // State management const [files, setFiles] = useState([]); const [config, setConfig] = useState({ - slides: "8", + slides: "5", language: LanguageType.English, prompt: "", tone: ToneType.Default, diff --git a/servers/nextjs/components/LLMSelection.tsx b/servers/nextjs/components/LLMSelection.tsx index 92c583ac..062b547a 100644 --- a/servers/nextjs/components/LLMSelection.tsx +++ b/servers/nextjs/components/LLMSelection.tsx @@ -34,28 +34,7 @@ interface LLMProviderSelectionProps { ) => void; } -const LLM_TABS = [ - { - label: 'OpenAI', - value: 'openai', - }, - { - label: 'Google', - value: 'google', - }, - { - label: 'Anthropic', - value: 'anthropic', - }, - { - label: 'Ollama', - value: 'ollama', - }, - { - label: 'Custom', - value: 'custom', - }, -]; + export default function LLMProviderSelection({ initialLLMConfig, onConfigChange, @@ -236,23 +215,9 @@ export default function LLMProviderSelection({ return ( -
- {/* Provider Selection - Fixed Header */} -
-
- {LLM_TABS.map((tab) => ( - - ))} +
-
-
{/* Scrollable Content */}
diff --git a/servers/nextjs/public/providers/image-provider.png b/servers/nextjs/public/providers/image-provider.png new file mode 100644 index 00000000..c27e0591 Binary files /dev/null and b/servers/nextjs/public/providers/image-provider.png differ diff --git a/servers/nextjs/public/providers/openai.png b/servers/nextjs/public/providers/openai.png new file mode 100644 index 00000000..4146ce93 Binary files /dev/null and b/servers/nextjs/public/providers/openai.png differ