"use client"; import { useState, useEffect } from "react"; import { useRouter } from "next/navigation"; import { toast } from "@/hooks/use-toast"; import { Info, ExternalLink, PlayCircle, Loader2, Check, ChevronsUpDown } from "lucide-react"; import Link from "next/link"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@/components/ui/accordion"; import { useSelector } from "react-redux"; import { RootState } from "@/store/store"; import { handleSaveLLMConfig } from "@/utils/storeHelpers"; import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select"; import { Button } from "./ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "./ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "./ui/popover"; import { cn } from "@/lib/utils"; import { Switch } from "./ui/switch"; import { setLLMConfig } from "@/store/slices/userConfig"; interface ModelOption { value: string; label: string; description?: string; icon?: string; size: string; } interface ProviderConfig { textModels: ModelOption[]; imageModels: ModelOption[]; apiGuide: { title: string; steps: string[]; videoUrl?: string; docsUrl: string; }; } const PROVIDER_CONFIGS: Record = { openai: { textModels: [ { value: "gpt-4", label: "GPT-4", description: "Most capable model, best for complex tasks", icon: "/icons/openai.png", size: "8GB", }, ], imageModels: [ { value: "dall-e-3", label: "DALL-E 3", description: "Latest version with highest quality", icon: "/icons/dall-e.png", size: "8GB", }, ], apiGuide: { title: "How to get your OpenAI API Key", steps: [ "Go to platform.openai.com and sign in or create an account", 'Click on your profile icon and select "View API keys"', 'Click "Create new secret key" and give it a name', "Copy your API key immediately (you won't be able to see it again)", "Make sure you have sufficient credits in your account", ], videoUrl: "https://www.youtube.com/watch?v=OB99E7Y1cMA", docsUrl: "https://platform.openai.com/docs/api-reference/authentication", }, }, google: { textModels: [ { value: "gemini-pro", label: "Gemini Pro", description: "Balanced model for most tasks", icon: "/icons/google.png", size: "8GB", }, ], imageModels: [ { value: "imagen", label: "Imagen", description: "Google's primary image generation model", icon: "/icons/google.png", size: "8GB", }, ], apiGuide: { title: "How to get your Google AI Studio API Key", steps: [ "Visit aistudio.google.com", 'Click on "Get API key" in the top navigation', 'Click "Create API key" on the next page', 'Choose either "Create API Key in new Project" or select an existing project', "Copy your API key - you're ready to go!", ], videoUrl: "https://www.youtube.com/watch?v=o8iyrtQyrZM&t=66s", docsUrl: "https://aistudio.google.com/app/apikey", }, }, ollama: { textModels: [], imageModels: [ { value: "pexels", label: "Pexels", description: "Pexels is a free stock photo and video platform that allows you to download high-quality images and videos for free.", icon: "/icons/pexels.png", size: "8GB", }, ], apiGuide: { title: "How to get your Pexels API Key", steps: [ "Visit pexels.com", 'Click on "Get API key" in the top navigation', "Copy your API key - you're ready to go!", ], videoUrl: "https://www.youtube.com/watch?v=o8iyrtQyrZM&t=66s", docsUrl: "https://www.pexels.com/api/documentation/", }, }, custom: { textModels: [], imageModels: [ { value: "pexels", label: "Pexels", description: "Pexels is a free stock photo and video platform that allows you to download high-quality images and videos for free.", icon: "/icons/pexels.png", size: "8GB", }, ], apiGuide: { title: "How to get your Pexels API Key", steps: [ "Visit pexels.com", 'Click on "Get API key" in the top navigation', "Copy your API key - you're ready to go!", ], videoUrl: "https://www.youtube.com/watch?v=o8iyrtQyrZM&t=66s", docsUrl: "https://www.pexels.com/api/documentation/", }, }, }; export default function Home() { const router = useRouter(); const config = useSelector((state: RootState) => state.userConfig); const [llmConfig, setLlmConfig] = useState(config.llm_config); const [ollamaModels, setOllamaModels] = useState<{ label: string; value: string; description: string; size: string; icon: string; }[]>([]); const [customModels, setCustomModels] = useState([]); const [downloadingModel, setDownloadingModel] = useState({ name: '', size: null, downloaded: null, status: '', done: false, }); const [isLoading, setIsLoading] = useState(false); const [openModelSelect, setOpenModelSelect] = useState(false); const [useCustomOllamaUrl, setUseCustomOllamaUrl] = useState(llmConfig.USE_CUSTOM_URL || false); const [customModelsLoading, setCustomModelsLoading] = useState(false); const [customModelsChecked, setCustomModelsChecked] = useState(false); const canChangeKeys = config.can_change_keys; const input_field_changed = (new_value: string, field: string) => { if (field === 'openai_api_key') { setLlmConfig({ ...llmConfig, OPENAI_API_KEY: new_value }); } else if (field === 'google_api_key') { setLlmConfig({ ...llmConfig, GOOGLE_API_KEY: new_value }); } else if (field === 'ollama_url') { setLlmConfig({ ...llmConfig, OLLAMA_URL: new_value }); } else if (field === 'ollama_model') { setLlmConfig({ ...llmConfig, OLLAMA_MODEL: new_value }); } else if (field === 'custom_llm_url') { setLlmConfig({ ...llmConfig, CUSTOM_LLM_URL: new_value }); } else if (field === 'custom_llm_api_key') { setLlmConfig({ ...llmConfig, CUSTOM_LLM_API_KEY: new_value }); } else if (field === 'custom_model') { setLlmConfig({ ...llmConfig, CUSTOM_MODEL: new_value }); } else if (field === 'pexels_api_key') { setLlmConfig({ ...llmConfig, PEXELS_API_KEY: new_value }); } } const handleSaveConfig = async () => { try { await handleSaveLLMConfig(llmConfig); if (llmConfig.LLM === 'ollama') { setIsLoading(true); await pullOllamaModels(); } toast({ title: 'Success', description: 'Configuration saved successfully', }); setIsLoading(false); router.push("/upload"); } catch (error) { console.error('Error:', error); toast({ title: 'Error', description: error instanceof Error ? error.message : 'Failed to save configuration', variant: 'destructive', }); setIsLoading(false); } }; const fetchOllamaModelsWithConfig = async (config: any) => { try { const response = await fetch('/api/v1/ppt/ollama/list-supported-models'); const data = await response.json(); setOllamaModels(data.models); // Check if currently selected model is still available if (config.OLLAMA_MODEL && data.models.length > 0) { const isModelAvailable = data.models.some((model: any) => model.value === config.OLLAMA_MODEL); if (!isModelAvailable) { setLlmConfig({ ...config, OLLAMA_MODEL: '' }); } } } catch (error) { console.error('Error fetching ollama models:', error); } } const changeProvider = (provider: string) => { const newConfig = { ...llmConfig, LLM: provider }; setLlmConfig(newConfig); if (provider === 'ollama') { // Use the new config to avoid stale state issues fetchOllamaModelsWithConfig(newConfig); } } const resetDownloadingModel = () => { setDownloadingModel({ name: '', size: null, downloaded: null, status: '', done: false, }); } const pullOllamaModels = async (): Promise => { return new Promise((resolve, reject) => { const interval = setInterval(async () => { try { const response = await fetch(`/api/v1/ppt/ollama/pull-model?name=${llmConfig.OLLAMA_MODEL}`); if (response.status === 200) { const data = await response.json(); if (data.done && data.status !== 'error') { clearInterval(interval); setDownloadingModel(data); resolve(); } else if (data.status === 'error') { clearInterval(interval); resetDownloadingModel(); reject(new Error('Error occurred while pulling model')); } else { setDownloadingModel(data); } } else { clearInterval(interval); resetDownloadingModel(); if (response.status === 403) { reject(new Error('Request to Ollama Not Authorized')); } reject(new Error('Error occurred while pulling model')); } } catch (error) { clearInterval(interval); resetDownloadingModel(); reject(error); } }, 1000); }); } const fetchOllamaModels = async () => { await fetchOllamaModelsWithConfig(llmConfig); } const fetchCustomModels = async () => { try { setCustomModelsLoading(true); const response = await fetch('/api/v1/ppt/models/list/custom', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ url: llmConfig.CUSTOM_LLM_URL || '', api_key: llmConfig.CUSTOM_LLM_API_KEY || '' }) }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setCustomModels(data); // Only set customModelsChecked to true if the API call succeeds setCustomModelsChecked(true); } catch (error) { console.error('Error fetching custom models:', error); // Don't set customModelsChecked to true on error, so the button remains visible setCustomModels([]); toast({ title: 'Error', description: 'Failed to fetch available models. Please check your URL and API key.', variant: 'destructive', }); } finally { setCustomModelsLoading(false); } } const setOllamaConfig = () => { if (!useCustomOllamaUrl) { setLlmConfig({ ...llmConfig, OLLAMA_URL: 'http://localhost:11434', USE_CUSTOM_URL: false }); } else { setLlmConfig({ ...llmConfig, USE_CUSTOM_URL: true }); } } useEffect(() => { if (!canChangeKeys) { router.push("/upload"); } if (llmConfig.LLM === 'ollama') { fetchOllamaModels(); } }, []); useEffect(() => { setOllamaConfig(); }, [useCustomOllamaUrl]); // Reset custom models when URL or API key changes useEffect(() => { if (llmConfig.LLM === 'custom') { setCustomModels([]); setCustomModelsChecked(false); setLlmConfig({ ...llmConfig, CUSTOM_MODEL: '' }); } }, [llmConfig.CUSTOM_LLM_URL, llmConfig.CUSTOM_LLM_API_KEY]); if (!canChangeKeys) { return null; } return (
{/* Branding Header */}
Presenton Logo

Open-source AI presentation generator

{/* Main Configuration Card */}
{/* Provider Selection */}
{Object.keys(PROVIDER_CONFIGS).map((provider) => ( ))}
{/* API Key Input */} {llmConfig.LLM !== 'ollama' && llmConfig.LLM !== 'custom' &&
input_field_changed(e.target.value, llmConfig.LLM === 'openai' ? 'openai_api_key' : 'google_api_key')} className="w-full px-4 py-2.5 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 API key" />

Your API key will be stored locally and never shared

} { llmConfig.LLM === 'ollama' && (
{ollamaModels.length > 0 ? ( No model found. {ollamaModels.map((model, index) => ( { input_field_changed(value, 'ollama_model'); setOpenModelSelect(false); }} >
{`${model.label}
{model.label} {model.size}
{model.description}
))}
) : (
)}
{ollamaModels.length === 0 && (

Loading available models...

)}
{useCustomOllamaUrl && ( <>
input_field_changed(e.target.value, 'ollama_url')} />

Change this if you are using a custom Ollama instance

)}
input_field_changed(e.target.value, 'pexels_api_key')} />

Provide a Pexels API key to generate presentation images

) } { llmConfig.LLM === 'custom' && ( <>
input_field_changed(e.target.value, 'custom_llm_url')} />
input_field_changed(e.target.value, 'custom_llm_api_key')} />
{/* Model selection dropdown - only show if models are available */} {customModelsChecked && customModels.length > 0 && (

Important: Only models with function calling capabilities (tool calls) or JSON schema support will work.

No model found. {customModels.map((model, index) => ( { input_field_changed(value, 'custom_model'); setOpenModelSelect(false); }} > {model} ))}
)} {/* Check for available models button - show when no models checked or no models found */} {(!customModelsChecked || (customModelsChecked && customModels.length === 0)) && (
)} {/* Show message if no models found */} {customModelsChecked && customModels.length === 0 && (

No models found. Please make sure models are available.

)}
input_field_changed(e.target.value, 'pexels_api_key')} />

Provide a Pexels API key to generate presentation images

) } {/* Model Information */}

Selected Models

Using {llmConfig.LLM === 'ollama' ? llmConfig.OLLAMA_MODEL ?? '_____' : llmConfig.LLM === 'custom' ? llmConfig.CUSTOM_MODEL ?? '_____' : PROVIDER_CONFIGS[llmConfig.LLM!].textModels[0].label} for text generation and {PROVIDER_CONFIGS[llmConfig.LLM!].imageModels[0].label} for images

We've pre-selected the best models for optimal presentation generation

{/* API Guide Section */}

{PROVIDER_CONFIGS[llmConfig.LLM!].apiGuide.title}

    {PROVIDER_CONFIGS[llmConfig.LLM!].apiGuide.steps.map((step, index) => (
  1. {step}
  2. ))}
{PROVIDER_CONFIGS[llmConfig.LLM!].apiGuide.videoUrl && ( Watch Video Tutorial )} Official Documentation
{/* Save Button */} { llmConfig.LLM === 'ollama' && downloadingModel.status && downloadingModel.status !== 'pulled' && (
{downloadingModel.status}
) }
); }