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 currentOllamaUrl = llmConfig.OLLAMA_URL || ''; const useCustomOllamaUrl = !!llmConfig.USE_CUSTOM_URL; 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"); // Track navigation from -> to trackEvent(MixpanelEvent.Navigation, { from: pathname, to: "/final onboarding step" }); setStep(3) // router.push("/upload"); } catch (error) { toast.info(error instanceof Error ? error.message : "Failed to save configuration"); } finally { setSavingConfig(false); } }; 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]); useEffect(() => { if (llmConfig.LLM === 'ollama' && !modelsChecked && !modelsLoading) { fetchAvailableModels(); } }, [llmConfig.LLM, modelsChecked, modelsLoading]); 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}
) )}
{llmConfig.LLM === 'ollama' ? ( <> {!useCustomOllamaUrl ? ( ) : ( <>
setLlmConfig(prev => ({ ...prev, OLLAMA_URL: 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="http://localhost:11434" />
)} ) : ( <>
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={`Enter your ${llmConfig.LLM} API key`} />
)} {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" &&