'use client'; import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react'; import { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; import { Switch } from '@headlessui/react'; import ThemeSwitcher from '@/components/theme/Switcher'; import { ImagesIcon, VideoIcon } from 'lucide-react'; import Link from 'next/link'; interface SettingsType { chatModelProviders: { [key: string]: [Record]; }; embeddingModelProviders: { [key: string]: [Record]; }; openaiApiKey: string; groqApiKey: string; anthropicApiKey: string; geminiApiKey: string; ollamaApiUrl: string; customOpenaiApiKey: string; customOpenaiApiUrl: string; customOpenaiModelName: string; searchEngineBackends: { search: string; image: string; video: string; news: string; }; searxngEndpoint: string; googleApiKey: string; googleCseId: string; bingSubscriptionKey: string; braveApiKey: string; yacyEndpoint: string; } interface InputProps extends React.InputHTMLAttributes { isSaving?: boolean; onSave?: (value: string) => void; } const Input = ({ className, isSaving, onSave, ...restProps }: InputProps) => { return (
onSave?.(e.target.value)} /> {isSaving && (
)}
); }; const Select = ({ className, options, ...restProps }: React.SelectHTMLAttributes & { options: { value: string; label: string; disabled?: boolean }[]; }) => { return ( ); }; const SettingsSection = ({ title, children, }: { title: string; children: React.ReactNode; }) => (

{title}

{children}
); const Page = () => { const [config, setConfig] = useState(null); const [chatModels, setChatModels] = useState>({}); const [embeddingModels, setEmbeddingModels] = useState>( {}, ); const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< string | null >(null); const [selectedChatModel, setSelectedChatModel] = useState( null, ); const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = useState(null); const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< string | null >(null); const [isLoading, setIsLoading] = useState(false); const [automaticImageSearch, setAutomaticImageSearch] = useState(false); const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); const [savingStates, setSavingStates] = useState>({}); const [searchEngineBackends, setSearchEngineBackends] = useState({ search: '', image: '', video: '', news: '', }); useEffect(() => { const fetchConfig = async () => { setIsLoading(true); const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { headers: { 'Content-Type': 'application/json', }, }); const data = (await res.json()) as SettingsType; setConfig(data); // Set search engine backends if they exist in the response if (data.searchEngineBackends) { setSearchEngineBackends({ search: data.searchEngineBackends.search || '', image: data.searchEngineBackends.image || '', video: data.searchEngineBackends.video || '', news: data.searchEngineBackends.news || '', }); } const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {}); const embeddingModelProvidersKeys = Object.keys( data.embeddingModelProviders || {}, ); const defaultChatModelProvider = chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : ''; const defaultEmbeddingModelProvider = embeddingModelProvidersKeys.length > 0 ? embeddingModelProvidersKeys[0] : ''; const chatModelProvider = localStorage.getItem('chatModelProvider') || defaultChatModelProvider || ''; const chatModel = localStorage.getItem('chatModel') || (data.chatModelProviders && data.chatModelProviders[chatModelProvider]?.length > 0 ? data.chatModelProviders[chatModelProvider][0].name : undefined) || ''; const embeddingModelProvider = localStorage.getItem('embeddingModelProvider') || defaultEmbeddingModelProvider || ''; const embeddingModel = localStorage.getItem('embeddingModel') || (data.embeddingModelProviders && data.embeddingModelProviders[embeddingModelProvider]?.[0].name) || ''; setSelectedChatModelProvider(chatModelProvider); setSelectedChatModel(chatModel); setSelectedEmbeddingModelProvider(embeddingModelProvider); setSelectedEmbeddingModel(embeddingModel); setChatModels(data.chatModelProviders || {}); setEmbeddingModels(data.embeddingModelProviders || {}); setAutomaticImageSearch( localStorage.getItem('autoImageSearch') === 'true', ); setAutomaticVideoSearch( localStorage.getItem('autoVideoSearch') === 'true', ); setIsLoading(false); }; fetchConfig(); }, []); const saveConfig = async (key: string, value: any) => { setSavingStates((prev) => ({ ...prev, [key]: true })); try { const updatedConfig = { ...config, [key]: value, } as SettingsType; const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updatedConfig), }, ); if (!response.ok) { throw new Error('Failed to update config'); } setConfig(updatedConfig); if ( key.toLowerCase().includes('api') || key.toLowerCase().includes('url') ) { const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { headers: { 'Content-Type': 'application/json', }, }); if (!res.ok) { throw new Error('Failed to fetch updated config'); } const data = await res.json(); setChatModels(data.chatModelProviders || {}); setEmbeddingModels(data.embeddingModelProviders || {}); const currentChatProvider = selectedChatModelProvider; const newChatProviders = Object.keys(data.chatModelProviders || {}); if (!currentChatProvider && newChatProviders.length > 0) { const firstProvider = newChatProviders[0]; const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name; if (firstModel) { setSelectedChatModelProvider(firstProvider); setSelectedChatModel(firstModel); localStorage.setItem('chatModelProvider', firstProvider); localStorage.setItem('chatModel', firstModel); } } else if ( currentChatProvider && (!data.chatModelProviders || !data.chatModelProviders[currentChatProvider] || !Array.isArray(data.chatModelProviders[currentChatProvider]) || data.chatModelProviders[currentChatProvider].length === 0) ) { const firstValidProvider = Object.entries( data.chatModelProviders || {}, ).find( ([_, models]) => Array.isArray(models) && models.length > 0, )?.[0]; if (firstValidProvider) { setSelectedChatModelProvider(firstValidProvider); setSelectedChatModel( data.chatModelProviders[firstValidProvider][0].name, ); localStorage.setItem('chatModelProvider', firstValidProvider); localStorage.setItem( 'chatModel', data.chatModelProviders[firstValidProvider][0].name, ); } else { setSelectedChatModelProvider(null); setSelectedChatModel(null); localStorage.removeItem('chatModelProvider'); localStorage.removeItem('chatModel'); } } const currentEmbeddingProvider = selectedEmbeddingModelProvider; const newEmbeddingProviders = Object.keys( data.embeddingModelProviders || {}, ); if (!currentEmbeddingProvider && newEmbeddingProviders.length > 0) { const firstProvider = newEmbeddingProviders[0]; const firstModel = data.embeddingModelProviders[firstProvider]?.[0]?.name; if (firstModel) { setSelectedEmbeddingModelProvider(firstProvider); setSelectedEmbeddingModel(firstModel); localStorage.setItem('embeddingModelProvider', firstProvider); localStorage.setItem('embeddingModel', firstModel); } } else if ( currentEmbeddingProvider && (!data.embeddingModelProviders || !data.embeddingModelProviders[currentEmbeddingProvider] || !Array.isArray( data.embeddingModelProviders[currentEmbeddingProvider], ) || data.embeddingModelProviders[currentEmbeddingProvider].length === 0) ) { const firstValidProvider = Object.entries( data.embeddingModelProviders || {}, ).find( ([_, models]) => Array.isArray(models) && models.length > 0, )?.[0]; if (firstValidProvider) { setSelectedEmbeddingModelProvider(firstValidProvider); setSelectedEmbeddingModel( data.embeddingModelProviders[firstValidProvider][0].name, ); localStorage.setItem('embeddingModelProvider', firstValidProvider); localStorage.setItem( 'embeddingModel', data.embeddingModelProviders[firstValidProvider][0].name, ); } else { setSelectedEmbeddingModelProvider(null); setSelectedEmbeddingModel(null); localStorage.removeItem('embeddingModelProvider'); localStorage.removeItem('embeddingModel'); } } setConfig(data); } if (key === 'automaticImageSearch') { localStorage.setItem('autoImageSearch', value.toString()); } else if (key === 'automaticVideoSearch') { localStorage.setItem('autoVideoSearch', value.toString()); } else if (key === 'chatModelProvider') { localStorage.setItem('chatModelProvider', value); } else if (key === 'chatModel') { localStorage.setItem('chatModel', value); } else if (key === 'embeddingModelProvider') { localStorage.setItem('embeddingModelProvider', value); } else if (key === 'embeddingModel') { localStorage.setItem('embeddingModel', value); } else if (key === 'searchEngineBackends') { localStorage.setItem('searchEngineBackends', value); } } catch (err) { console.error('Failed to save:', err); setConfig((prev) => ({ ...prev! })); } finally { setTimeout(() => { setSavingStates((prev) => ({ ...prev, [key]: false })); }, 500); } }; return (

Settings


{isLoading ? (
) : ( config && (

Theme

Automatic Image Search

Automatically search for relevant images in chat responses

{ setAutomaticImageSearch(checked); saveConfig('automaticImageSearch', checked); }} className={cn( automaticImageSearch ? 'bg-[#24A0ED]' : 'bg-light-200 dark:bg-dark-200', 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', )} >

Automatic Video Search

Automatically search for relevant videos in chat responses

{ setAutomaticVideoSearch(checked); saveConfig('automaticVideoSearch', checked); }} className={cn( automaticVideoSearch ? 'bg-[#24A0ED]' : 'bg-light-200 dark:bg-dark-200', 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', )} >
{config.chatModelProviders && (

Chat Model Provider

{ const value = e.target.value; setSelectedChatModel(value); saveConfig('chatModel', value); }} options={(() => { const chatModelProvider = config.chatModelProviders[ selectedChatModelProvider ]; return chatModelProvider ? chatModelProvider.length > 0 ? chatModelProvider.map((model) => ({ value: model.name, label: model.displayName, })) : [ { value: '', label: 'No models available', disabled: true, }, ] : [ { value: '', label: 'Invalid provider, please check backend logs', disabled: true, }, ]; })()} />
)}
)} {selectedChatModelProvider && selectedChatModelProvider === 'custom_openai' && (

Model Name

) => { setConfig((prev) => ({ ...prev!, customOpenaiModelName: e.target.value, })); }} onSave={(value) => saveConfig('customOpenaiModelName', value) } />

Custom OpenAI API Key

) => { setConfig((prev) => ({ ...prev!, customOpenaiApiKey: e.target.value, })); }} onSave={(value) => saveConfig('customOpenaiApiKey', value) } />

Custom OpenAI Base URL

) => { setConfig((prev) => ({ ...prev!, customOpenaiApiUrl: e.target.value, })); }} onSave={(value) => saveConfig('customOpenaiApiUrl', value) } />
)} {config.embeddingModelProviders && (

Embedding Model Provider

{ const value = e.target.value; setSelectedEmbeddingModel(value); saveConfig('embeddingModel', value); }} options={(() => { const embeddingModelProvider = config.embeddingModelProviders[ selectedEmbeddingModelProvider ]; return embeddingModelProvider ? embeddingModelProvider.length > 0 ? embeddingModelProvider.map((model) => ({ value: model.name, label: model.displayName, })) : [ { value: '', label: 'No models available', disabled: true, }, ] : [ { value: '', label: 'Invalid provider, please check backend logs', disabled: true, }, ]; })()} />
)}
)}

OpenAI API Key

{ setConfig((prev) => ({ ...prev!, openaiApiKey: e.target.value, })); }} onSave={(value) => saveConfig('openaiApiKey', value)} />

Ollama API URL

{ setConfig((prev) => ({ ...prev!, ollamaApiUrl: e.target.value, })); }} onSave={(value) => saveConfig('ollamaApiUrl', value)} />

GROQ API Key

{ setConfig((prev) => ({ ...prev!, groqApiKey: e.target.value, })); }} onSave={(value) => saveConfig('groqApiKey', value)} />

Anthropic API Key

{ setConfig((prev) => ({ ...prev!, anthropicApiKey: e.target.value, })); }} onSave={(value) => saveConfig('anthropicApiKey', value)} />

Gemini API Key

{ setConfig((prev) => ({ ...prev!, geminiApiKey: e.target.value, })); }} onSave={(value) => saveConfig('geminiApiKey', value)} />

Default Search Engine

{ const value = e.target.value; setSearchEngineBackends((prev) => ({ ...prev, image: value, })); saveConfig('searchEngineBackends', { ...searchEngineBackends, image: value, }); }} options={[ { value: '', label: 'Use Default Search Engine' }, { value: 'searxng', label: 'SearXNG' }, { value: 'google', label: 'Google' }, { value: 'bing', label: 'Bing' }, { value: 'brave', label: 'Brave' }, ]} />

Video Search Engine

{ const value = e.target.value; setSearchEngineBackends((prev) => ({ ...prev, news: value, })); saveConfig('searchEngineBackends', { ...searchEngineBackends, news: value, }); }} options={[ { value: '', label: 'Use Default Search Engine' }, { value: 'searxng', label: 'SearXNG' }, { value: 'google', label: 'Google' }, { value: 'bing', label: 'Bing' }, { value: 'brave', label: 'Brave' }, ]} />

SearXNG Endpoint

{ setConfig((prev) => ({ ...prev!, searxngEndpoint: e.target.value, })); }} onSave={(value) => saveConfig('searxngEndpoint', value)} />

Google API Key

{ setConfig((prev) => ({ ...prev!, googleApiKey: e.target.value, })); }} onSave={(value) => saveConfig('googleApiKey', value)} />

Google CSE ID

{ setConfig((prev) => ({ ...prev!, googleCseId: e.target.value, })); }} onSave={(value) => saveConfig('googleCseId', value)} />

Bing Subscription Key

{ setConfig((prev) => ({ ...prev!, bingSubscriptionKey: e.target.value, })); }} onSave={(value) => saveConfig('bingSubscriptionKey', value)} />

Brave API Key

{ setConfig((prev) => ({ ...prev!, braveApiKey: e.target.value, })); }} onSave={(value) => saveConfig('braveApiKey', value)} />

YaCy Endpoint

{ setConfig((prev) => ({ ...prev!, yacyEndpoint: e.target.value, })); }} onSave={(value) => saveConfig('yacyEndpoint', value)} />
) )}
); }; export default Page;