From ed76eff535646155d43377f6af7feed52f525bd0 Mon Sep 17 00:00:00 2001 From: shiva raj badu Date: Wed, 4 Mar 2026 00:32:14 +0545 Subject: [PATCH] feat: OnBoarding two pages design --- .../nextjs/app/ConfigurationInitializer.tsx | 10 +- .../(dashboard)/settings/ImageProvider.tsx | 4 +- .../(dashboard)/settings/TextProvider.tsx | 5 +- .../outline/components/OutlineContent.tsx | 2 +- .../outline/components/OutlineItem.tsx | 6 +- .../outline/components/OutlinePage.tsx | 8 +- .../outline/components/TemplateSelection.tsx | 13 +- .../presentation/components/ThemeSelector.tsx | 1 - .../nextjs/app/ConfigurationInitializer.tsx | 3 + servers/nextjs/components/Home.tsx | 250 ++--- .../OnBoarding/GenerationWithImage.tsx | 11 + .../components/OnBoarding/ModeSelectStep.tsx | 56 ++ .../OnBoarding/OnBoardingHeader.tsx | 36 + .../OnBoarding/OnBoardingSlidebar.tsx | 19 + .../components/OnBoarding/PresentonMode.tsx | 883 ++++++++++++++++++ servers/nextjs/public/image_mode.png | Bin 0 -> 5966 bytes servers/nextjs/store/slices/userConfig.ts | 6 +- 17 files changed, 1170 insertions(+), 143 deletions(-) create mode 100644 servers/nextjs/components/OnBoarding/GenerationWithImage.tsx create mode 100644 servers/nextjs/components/OnBoarding/ModeSelectStep.tsx create mode 100644 servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx create mode 100644 servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx create mode 100644 servers/nextjs/components/OnBoarding/PresentonMode.tsx create mode 100644 servers/nextjs/public/image_mode.png diff --git a/electron/servers/nextjs/app/ConfigurationInitializer.tsx b/electron/servers/nextjs/app/ConfigurationInitializer.tsx index fd407ceb..8a70a740 100644 --- a/electron/servers/nextjs/app/ConfigurationInitializer.tsx +++ b/electron/servers/nextjs/app/ConfigurationInitializer.tsx @@ -32,7 +32,7 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo const fetchUserConfigState = async () => { setIsLoading(true); - + let canChangeKeys = false; if (typeof window !== 'undefined' && (window as any).electron) { // Electron mode: use IPC @@ -74,13 +74,13 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo } dispatch(setLLMConfig(llmConfig)); const isValid = hasValidLLMConfig(llmConfig); - + // Allow access to pdf-maker without LLM configuration (needed for PPTX export) if (route.startsWith('/pdf-maker')) { setIsLoading(false); return; } - + if (isValid) { // Check if the selected Ollama model is pulled if (llmConfig.LLM === 'ollama' && llmConfig.OLLAMA_MODEL) { @@ -134,12 +134,12 @@ export function ConfigurationInitializer({ children }: { children: React.ReactNo api_key: llmConfig.CUSTOM_LLM_API_KEY, }), }); - + if (!response.ok) { console.error('Custom model check failed with status:', response.status); return false; } - + const data = await response.json(); return data.includes(llmConfig.CUSTOM_MODEL); } catch (error) { diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx index 883cb91d..08c4125d 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/ImageProvider.tsx @@ -122,9 +122,9 @@ const ImageProvider = ({ llmConfig, setLlmConfig }: { llmConfig: LLMConfig, setL
handleChangeImageGenerationDisabled(checked)} + onCheckedChange={(checked) => handleChangeImageGenerationDisabled(!checked)} />
diff --git a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/TextProvider.tsx b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/TextProvider.tsx index 5ab0d15e..98cd1e27 100644 --- a/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/TextProvider.tsx +++ b/servers/nextjs/app/(presentation-generator)/(dashboard)/settings/TextProvider.tsx @@ -468,7 +468,7 @@ const TextProvider = ({
-
+
-
+ {/*
*/}
+
diff --git a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx index cbc7c40f..ca3e6719 100644 --- a/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx +++ b/servers/nextjs/app/(presentation-generator)/outline/components/OutlineContent.tsx @@ -93,7 +93,7 @@ const OutlineContent: React.FC = ({ {/* Outlines content */} {outlines && outlines.length > 0 && ( -
+
+
-
+
+ //
+ //
+ //
+
+ +
+ + {step === 1 && } + {step === 2 && selectedMode === "presenton" && } + {step === 2 && selectedMode === "image" && }
- - {/* Download Progress Modal */} - {showDownloadModal && downloadingModel && ( -
-
- {/* Modal Content */} -
- {/* Icon */} -
- {downloadingModel.done ? ( - - ) : ( - - )} -
- - {/* Title */} -

- {downloadingModel.done ? "Download Complete!" : "Downloading Model"} -

- - {/* Model Name */} -

- {llmConfig.OLLAMA_MODEL} -

- - {/* Progress Bar */} - {downloadProgress > 0 && ( -
-
-
-
-

- {downloadProgress}% Complete -

-
- )} - - {/* Status */} - {downloadingModel.status && ( -
- - - {downloadingModel.status} - -
- )} - - {/* Status Message */} - {downloadingModel.status && downloadingModel.status !== "pulled" && ( -
- {downloadingModel.status === "downloading" && "Downloading model files..."} - {downloadingModel.status === "verifying" && "Verifying model integrity..."} - {downloadingModel.status === "pulling" && "Pulling model from registry..."} -
- )} - - {/* Download Info */} - {downloadingModel.downloaded && downloadingModel.size && ( -
-
- Downloaded: {(downloadingModel.downloaded / 1024 / 1024).toFixed(1)} MB - Total: {(downloadingModel.size / 1024 / 1024).toFixed(1)} MB -
-
- )} -
-
-
- )} - - {/* Fixed Bottom Button */} -
-
- -
-
); } diff --git a/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx b/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx new file mode 100644 index 00000000..2b65250a --- /dev/null +++ b/servers/nextjs/components/OnBoarding/GenerationWithImage.tsx @@ -0,0 +1,11 @@ +import React from 'react' + +const GenerationWithImage = () => { + return ( +
+ +
+ ) +} + +export default GenerationWithImage \ No newline at end of file diff --git a/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx b/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx new file mode 100644 index 00000000..331df849 --- /dev/null +++ b/servers/nextjs/components/OnBoarding/ModeSelectStep.tsx @@ -0,0 +1,56 @@ +import { ChevronRight } from 'lucide-react' +import React from 'react' + +const ModeSelectStep = ({ setStep, setSelectedMode }: { setStep: (step: number) => void, setSelectedMode: (mode: string) => void }) => { + return ( +
+
+ +

Let’s set up your AI workspace

+

First, choose the intelligence behind your presentation generation.

+
+
+
{ + setSelectedMode("presenton") + setStep(2) + }} className='border border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'> +
+
+ presenton +
+
+
+ +

Presenton

+

PPTX

+
+

Optimized for fast, structured slide generation.

+
+
+ +
+
{ + setSelectedMode("image") + setStep(2) + }} className='border border-[#EDEEEF] rounded-[11px] p-3 flex items-center justify-between gap-6 cursor-pointer'> +
+
+ presenton +
+
+
+ +

Generate with Image Model

+ +
+

Generate presentations with visual layouts and elements.

+
+
+ +
+
+
+ ) +} + +export default ModeSelectStep diff --git a/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx b/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx new file mode 100644 index 00000000..78a8e6c9 --- /dev/null +++ b/servers/nextjs/components/OnBoarding/OnBoardingHeader.tsx @@ -0,0 +1,36 @@ +import React from 'react' + + +const OnBoardingHeader = ({ currentStep }: { currentStep: number }) => { + return ( +
+ +
+
= 1 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}> + 1 +
+

Select Mode

+
+ + + +
+
= 2 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}> + 2 +
+

Choose Providers

+
+ + + +
+
= 3 ? 'bg-[#010100] text-white' : 'border border-[#ECECEF] text-[#494A4D]'} px-2.5 h-7 w-7 text-xs font-medium rounded-full flex items-center justify-center`}> + 3 +
+

Finish Setup

+
+
+ ) +} + +export default OnBoardingHeader diff --git a/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx b/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx new file mode 100644 index 00000000..7e39b524 --- /dev/null +++ b/servers/nextjs/components/OnBoarding/OnBoardingSlidebar.tsx @@ -0,0 +1,19 @@ +import React from 'react' + +const OnBoardingSlidebar = () => { + return ( +
+ Presenton logo + + + + + + + + +
+ ) +} + +export default OnBoardingSlidebar diff --git a/servers/nextjs/components/OnBoarding/PresentonMode.tsx b/servers/nextjs/components/OnBoarding/PresentonMode.tsx new file mode 100644 index 00000000..09907783 --- /dev/null +++ b/servers/nextjs/components/OnBoarding/PresentonMode.tsx @@ -0,0 +1,883 @@ +import React, { useEffect, useMemo, useState } from 'react' +import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover'; +import { Button } from '../ui/button'; +import { Check, CheckCircle, ChevronLeft, ChevronRight, ChevronUp, Download, Eye, EyeOff, Loader2 } from 'lucide-react'; +import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../ui/command'; +import { DALLE_3_QUALITY_OPTIONS, GPT_IMAGE_1_5_QUALITY_OPTIONS, IMAGE_PROVIDERS, LLM_PROVIDERS } from '@/utils/providerConstants'; +import { cn } from '@/lib/utils'; +import { LLMConfig } from '@/types/llm_config'; +import { RootState } from '@/store/store'; +import { useSelector } from 'react-redux'; +import { toast } from 'sonner'; +import ToolTip from '../ToolTip'; +import { Switch } from '../ui/switch'; +import { Select, SelectItem, SelectContent, SelectValue, SelectTrigger } from '../ui/select'; +import { MixpanelEvent, trackEvent } from '@/utils/mixpanel'; +import { usePathname, useRouter } from 'next/navigation'; +import { handleSaveLLMConfig } from '@/utils/storeHelpers'; +import { checkIfSelectedOllamaModelIsPulled, pullOllamaModel } from '@/utils/providerUtils'; + +const PresentonMode = ({ currentStep, setStep }: { currentStep: number, setStep: (step: number) => void }) => { + const pathname = usePathname(); + const router = useRouter(); + const [openProviderSelect, setOpenProviderSelect] = useState(false); + const [openImageProviderSelect, setOpenImageProviderSelect] = useState(false); + const userConfigState = useSelector((state: RootState) => state.userConfig); + + const [showApiKey, setShowApiKey] = useState(false); + const [availableModels, setAvailableModels] = useState([]); + const [openModelSelect, setOpenModelSelect] = useState(false); + const [modelsLoading, setModelsLoading] = useState(false); + const [modelsChecked, setModelsChecked] = useState(false); + const [showDownloadModal, setShowDownloadModal] = useState(false); + const [savingConfig, setSavingConfig] = useState(false); + const [llmConfig, setLlmConfig] = useState( + userConfigState.llm_config + ); + const [downloadingModel, setDownloadingModel] = useState<{ + name: string; + size: number | null; + downloaded: number | null; + status: string; + done: boolean; + } | null>(null); + + const handleProviderChange = (provider: string) => { + + setLlmConfig(prev => ({ + ...prev, + LLM: provider + })); + setOpenProviderSelect(false); + setAvailableModels([]); + setModelsChecked(false); + if (currentModelField) { + setLlmConfig(prev => ({ + ...prev, + [currentModelField]: '' + })); + } + }; + + const currentModelField = useMemo(() => { + switch (llmConfig.LLM) { + case 'openai': + return 'OPENAI_MODEL'; + case 'google': + return 'GOOGLE_MODEL'; + case 'anthropic': + return 'ANTHROPIC_MODEL'; + case 'ollama': + return 'OLLAMA_MODEL'; + case 'custom': + return 'CUSTOM_MODEL'; + default: + return ''; + } + }, [llmConfig.LLM]); + const currentApiKeyField = useMemo(() => { + switch (llmConfig.LLM) { + case 'openai': + return 'OPENAI_API_KEY'; + case 'google': + return 'GOOGLE_API_KEY'; + case 'anthropic': + return 'ANTHROPIC_API_KEY'; + case 'custom': + return 'CUSTOM_LLM_API_KEY'; + default: + return ''; + } + }, [llmConfig.LLM]); + + + + const getFieldValue = (field?: string) => { + if (!field) return ""; + return (llmConfig as Record)[field] || ""; + }; + + const currentApiKey = currentApiKeyField ? ((llmConfig as Record)[currentApiKeyField] as string || '') : ''; + const currentModel = currentModelField ? ((llmConfig as Record)[currentModelField] as string || '') : ''; + + const fetchAvailableModels = async () => { + if (llmConfig.LLM === 'openai' && !currentApiKey) return; + if (llmConfig.LLM === 'google' && !currentApiKey) return; + if (llmConfig.LLM === 'anthropic' && !currentApiKey) return; + if (llmConfig.LLM === 'custom' && !llmConfig.CUSTOM_LLM_URL) return; + + setModelsLoading(true); + try { + let response: Response; + if (llmConfig.LLM === 'google') { + response = await fetch('/api/v1/ppt/google/models/available', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: currentApiKey + }), + }); + } else if (llmConfig.LLM === 'anthropic') { + response = await fetch('/api/v1/ppt/anthropic/models/available', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + api_key: currentApiKey + }), + }); + } else if (llmConfig.LLM === 'ollama') { + response = await fetch('/api/v1/ppt/ollama/models/supported'); + } else { + response = await fetch('/api/v1/ppt/openai/models/available', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + url: llmConfig.LLM === 'custom' ? llmConfig.CUSTOM_LLM_URL : LLM_PROVIDERS[llmConfig.LLM!]?.url || '', + api_key: currentApiKey + }), + }); + } + + if (response.ok) { + const data = await response.json(); + const normalizedModels: string[] = llmConfig.LLM === 'ollama' + ? Array.isArray(data) + ? data.map((model: { value?: string; label?: string }) => model.value || model.label || '').filter(Boolean) + : [] + : Array.isArray(data) + ? data + : []; + + setAvailableModels(normalizedModels); + setModelsChecked(true); + + if (normalizedModels.length > 0 && currentModelField) { + if (llmConfig[currentModelField] && normalizedModels.includes(llmConfig[currentModelField])) { + setLlmConfig(prev => ({ + ...prev, + [currentModelField]: llmConfig[currentModelField] + })); + return; + } + + const preferredDefault = + llmConfig.LLM === 'openai' + ? 'gpt-4.1' + : llmConfig.LLM === 'google' + ? 'models/gemini-2.5-flash' + : llmConfig.LLM === 'anthropic' + ? 'claude-sonnet-4-20250514' + : normalizedModels[0]; + + const nextModel = normalizedModels.includes(preferredDefault) ? preferredDefault : normalizedModels[0]; + setLlmConfig(prev => ({ + ...prev, + [currentModelField]: nextModel + })); + } + } else { + console.error('Failed to fetch models'); + setAvailableModels([]); + setModelsChecked(true); + toast.error(`Failed to fetch ${LLM_PROVIDERS[llmConfig.LLM!]?.label} models`); + } + } catch (error) { + console.error('Error fetching models:', error); + toast.error('Error fetching models'); + setAvailableModels([]); + setModelsChecked(true); + } finally { + setModelsLoading(false); + } + }; + + const renderQualitySelector = (llmConfig: LLMConfig) => { + if (llmConfig.IMAGE_PROVIDER === "dall-e-3") { + return ( +
+ +
+ + +
+
+ ); + } + + if (llmConfig.IMAGE_PROVIDER === "gpt-image-1.5") { + return ( +
+ +
+ + +
+
+ ); + } + + return null; + }; + const handleModelDownload = async () => { + try { + await pullOllamaModel(llmConfig.OLLAMA_MODEL!, setDownloadingModel); + } + finally { + setDownloadingModel(null); + setShowDownloadModal(false); + } + }; + + + const handleSaveConfig = async () => { + trackEvent(MixpanelEvent.Home_SaveConfiguration_Button_Clicked, { pathname }); + try { + setSavingConfig(true); + // API: save config + trackEvent(MixpanelEvent.Home_SaveConfiguration_API_Call); + // API CALL: save config + await handleSaveLLMConfig(llmConfig); + + if (llmConfig.LLM === "ollama" && llmConfig.OLLAMA_MODEL) { + // API: check model pulled + trackEvent(MixpanelEvent.Home_CheckOllamaModelPulled_API_Call); + const isPulled = await checkIfSelectedOllamaModelIsPulled(llmConfig.OLLAMA_MODEL); + if (!isPulled) { + setShowDownloadModal(true); + // API: download model + trackEvent(MixpanelEvent.Home_DownloadOllamaModel_API_Call); + await handleModelDownload(); + } + } + toast.info("Configuration saved successfully"); + setSavingConfig(false); + // Track navigation from -> to + trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/upload" }); + router.push("/upload"); + } catch (error) { + toast.info(error instanceof Error ? error.message : "Failed to save configuration"); + + } + }; + + const downloadProgress = useMemo(() => { + if (downloadingModel && downloadingModel.downloaded !== null && downloadingModel.size !== null) { + return Math.round((downloadingModel.downloaded / downloadingModel.size) * 100); + } + return 0; + }, [downloadingModel?.downloaded, downloadingModel?.size]); + + + return ( +
+

PRESENTON

+
+ +

Choose your content providers

+

Select the AI engines that will generate your slide text and visuals.

+
+ {/* Text Provider */} +
+
+
+ + + + + +
+
+ +

Text Generation Settings

+

+ Choosing where text contets come from +

+
+
+
+
+ + + + + + + + + + + No provider found. + + {Object.values(LLM_PROVIDERS).map( + (provider, index) => ( + handleProviderChange(provider.value)} + > + +
+
+
+ + {provider.label} + +
+ + {provider.description} + +
+
+
+ ) + )} +
+
+
+
+
+
+
+
+ +
+ setLlmConfig(prev => ({ + ...prev, + [currentApiKeyField]: e.target.value + }))} + className="w-full px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors" + placeholder={llmConfig.LLM === 'ollama' ? 'http://localhost:11434' : `Enter your ${llmConfig.LLM} API key`} + /> + {llmConfig.LLM !== 'ollama' && ( + + )} +
+ {llmConfig.LLM === 'custom' && ( + setLlmConfig(prev => ({ + ...prev, + CUSTOM_LLM_URL: e.target.value + }))} + className="w-full mt-2 px-2 py-3 outline-none border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors" + placeholder="OpenAI-compatible URL" + /> + )} + + +
+ + + {llmConfig.LLM !== 'ollama' && (!modelsChecked || (modelsChecked && availableModels.length === 0)) && ( + + + )} +
+ +
+
+

+ + {/* Model Selection - only show if models are available */} + {modelsChecked && availableModels.length > 0 && ( +
+
+ +
+ + + + + + + + + No model found. + + {availableModels.map((model, index) => ( + { + if (currentModelField) { + setLlmConfig(prev => ({ + ...prev, + [currentModelField]: value + })); + } + setOpenModelSelect(false); + }} + > + +
+
+
+ + {model} + +
+
+
+
+ ))} +
+
+
+
+
+
+
+
+ )} +
+
+ {/* Image Provider */} +
+ +
+ setLlmConfig(prev => ({ + ...prev, + DISABLE_IMAGE_GENERATION: !checked + }))} + /> +
+ +
+
+
+ image-markup +
+
+ +

Image Generation Settings

+

+ Choosing where images come from +

+
+
+ {!llmConfig.DISABLE_IMAGE_GENERATION && ( +
+ {/* Image Provider Selection */} +
+ +
+ + + + + + + + + No provider found. + + {Object.values(IMAGE_PROVIDERS).map( + (provider, index) => ( + { + setLlmConfig(prev => ({ + ...prev, + IMAGE_PROVIDER: value + })); + setOpenImageProviderSelect(false); + }} + > + +
+
+
+ + {provider.label} + +
+ + {provider.description} + +
+
+
+ ) + )} +
+
+
+
+
+
+
+ + + + {/* Dynamic API Key Input for Image Provider */} + {llmConfig.IMAGE_PROVIDER && + IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER] && + (() => { + const provider = IMAGE_PROVIDERS[llmConfig.IMAGE_PROVIDER]; + + + + // Show ComfyUI configuration + if (provider.value === "comfyui") { + return ( +
+
+ +
+ { + setLlmConfig(prev => ({ + ...prev, + COMFYUI_URL: e.target.value + })); + }} + /> +
+ +
+ +
+ ); + } + + // Show API key input for other providers + return ( +
+ +
+ { + setLlmConfig((prev) => ({ + ...prev, + [provider.apiKeyField as keyof LLMConfig]: e.target.value + })) + } + + } + /> + +
+ +
+ ); + })()} + +
+ )} + {!llmConfig.DISABLE_IMAGE_GENERATION &&
+
+

+ {renderQualitySelector(llmConfig)} +
+ {llmConfig.IMAGE_PROVIDER === "comfyui" &&
+ +
+