'use client'; import React, { useState, useEffect } from "react"; import Header from "../dashboard/components/Header"; import Wrapper from "@/components/Wrapper"; import { Settings, Key, Loader2, Check, ChevronsUpDown } from 'lucide-react'; import { toast } from '@/hooks/use-toast'; import { RootState } from "@/store/store"; import { useSelector } from "react-redux"; import { handleSaveLLMConfig } from "@/utils/storeHelpers"; import { useRouter } from "next/navigation"; import { Select, SelectContent, SelectItem, SelectTrigger } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@/components/ui/command"; import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; import { Switch } from "@/components/ui/switch"; const PROVIDER_CONFIGS: Record = { openai: { title: "OpenAI API Key", description: "Required for using OpenAI services", placeholder: "Enter your OpenAI API key", }, google: { title: "Google API Key", description: "Required for using Google services", placeholder: "Enter your Google API key", }, ollama: { title: "Ollama API Key", description: "Required for using Ollama services", placeholder: "Choose a model", }, custom: { title: "Custom Model Configuration", description: "Configure your own OpenAI-compatible model", placeholder: "Enter your custom model details", } }; interface ProviderConfig { title: string; description: string; placeholder: string; } const SettingsPage = () => { const router = useRouter(); const userConfigState = useSelector((state: RootState) => state.userConfig); const [llmConfig, setLlmConfig] = useState(userConfigState.llm_config); const canChangeKeys = userConfigState.can_change_keys; 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(userConfigState.llm_config.USE_CUSTOM_URL || false); const [customModelsLoading, setCustomModelsLoading] = useState(false); const [customModelsChecked, setCustomModelsChecked] = useState(false); 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.back(); } 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); // Check if currently selected model is still available if (llmConfig.CUSTOM_MODEL && data.length > 0) { const isModelAvailable = data.includes(llmConfig.CUSTOM_MODEL); if (!isModelAvailable) { setLlmConfig({ ...llmConfig, CUSTOM_MODEL: '' }); toast({ title: 'Model Unavailable', description: `The selected model "${llmConfig.CUSTOM_MODEL}" is no longer available. Please select a different model.`, variant: 'destructive', }); } } } 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 }); } } const onCustomModelInfoChange = (value: string, field: string) => { setCustomModels([]); setCustomModelsChecked(false); setLlmConfig({ ...llmConfig, CUSTOM_MODEL: '', CUSTOM_LLM_URL: field === 'custom_llm_url' ? value : llmConfig.CUSTOM_LLM_URL, CUSTOM_LLM_API_KEY: field === 'custom_llm_api_key' ? value : llmConfig.CUSTOM_LLM_API_KEY }); } useEffect(() => { if (!canChangeKeys) { router.push("/dashboard"); } if (userConfigState.llm_config.LLM === 'ollama') { fetchOllamaModels(); } else if (userConfigState.llm_config.LLM === 'custom' && userConfigState.llm_config.CUSTOM_MODEL && userConfigState.llm_config.CUSTOM_LLM_URL) { fetchCustomModels(); } }, [userConfigState.llm_config.LLM]); useEffect(() => { setOllamaConfig(); }, [useCustomOllamaUrl]); if (!canChangeKeys) { return null; } return (
{/* Settings Header */}

Settings

{/* API Configuration Section */}

API Configuration

{/* 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 border border-gray-300 outline-none rounded-lg focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-colors" placeholder={PROVIDER_CONFIGS[llmConfig.LLM!].placeholder} />

{PROVIDER_CONFIGS[llmConfig.LLM!].description}

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

Loading available models...

)}
{/* Custom Ollama URL Configuration */}
{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

)} {/* Custom Model Configuration */} {llmConfig.LLM === 'custom' && (
onCustomModelInfoChange(e.target.value, 'custom_llm_url')} />
onCustomModelInfoChange(e.target.value, 'custom_llm_api_key')} />
{/* Model selection dropdown - show if models are available or if there's a selected model */} {((customModelsChecked && customModels.length > 0) || llmConfig.CUSTOM_MODEL) && (

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

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

No models found. Please make sure models are available.

)} {/* Refresh models button - show when there's a selected model but we want to refresh */} {llmConfig.CUSTOM_MODEL && customModelsChecked && (
)}
input_field_changed(e.target.value, 'pexels_api_key')} />

Provide a Pexels API key to generate presentation images

)} {/* Save Button */} { llmConfig.LLM === 'ollama' && downloadingModel.status && downloadingModel.status !== 'pulled' && (
{downloadingModel.status}
) }
); }; export default SettingsPage;