import { cn } from '@/lib/utils'; import { Dialog, DialogPanel, DialogTitle, Transition, TransitionChild, } from '@headlessui/react'; import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react'; import React, { Fragment, useEffect, useState, type SelectHTMLAttributes, } from 'react'; import ThemeSwitcher from './theme/Switcher'; interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {} const Input = ({ className, ...restProps }: InputProps) => { return ( <input {...restProps} className={cn( 'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm', className, )} /> ); }; interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> { options: { value: string; label: string; disabled?: boolean }[]; } export const Select = ({ className, options, ...restProps }: SelectProps) => { return ( <select {...restProps} className={cn( 'bg-light-secondary dark:bg-dark-secondary px-3 py-2 flex items-center overflow-hidden border border-light-200 dark:border-dark-200 dark:text-white rounded-lg text-sm', className, )} > {options.map(({ label, value, disabled }) => { return ( <option key={value} value={value} disabled={disabled}> {label} </option> ); })} </select> ); }; interface SettingsType { chatModelProviders: { [key: string]: [Record<string, any>]; }; embeddingModelProviders: { [key: string]: [Record<string, any>]; }; openaiApiKey: string; groqApiKey: string; anthropicApiKey: string; geminiApiKey: string; ollamaApiUrl: string; } const SettingsDialog = ({ isOpen, setIsOpen, }: { isOpen: boolean; setIsOpen: (isOpen: boolean) => void; }) => { const [config, setConfig] = useState<SettingsType | null>(null); const [chatModels, setChatModels] = useState<Record<string, any>>({}); const [embeddingModels, setEmbeddingModels] = useState<Record<string, any>>( {}, ); const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< string | null >(null); const [selectedChatModel, setSelectedChatModel] = useState<string | null>( null, ); const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = useState<string | null>(null); const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< string | null >(null); const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState<string>(''); const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string>(''); const [isLoading, setIsLoading] = useState(false); const [isUpdating, setIsUpdating] = useState(false); useEffect(() => { if (isOpen) { 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); 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); setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey') || ''); setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseURL') || ''); setChatModels(data.chatModelProviders || {}); setEmbeddingModels(data.embeddingModelProviders || {}); setIsLoading(false); }; fetchConfig(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); const handleSubmit = async () => { setIsUpdating(true); try { await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(config), }); localStorage.setItem('chatModelProvider', selectedChatModelProvider!); localStorage.setItem('chatModel', selectedChatModel!); localStorage.setItem( 'embeddingModelProvider', selectedEmbeddingModelProvider!, ); localStorage.setItem('embeddingModel', selectedEmbeddingModel!); localStorage.setItem('openAIApiKey', customOpenAIApiKey!); localStorage.setItem('openAIBaseURL', customOpenAIBaseURL!); } catch (err) { console.log(err); } finally { setIsUpdating(false); setIsOpen(false); window.location.reload(); } }; return ( <Transition appear show={isOpen} as={Fragment}> <Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)} > <TransitionChild as={Fragment} enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" leave="ease-in duration-200" leaveFrom="opacity-100" leaveTo="opacity-0" > <div className="fixed inset-0 bg-white/50 dark:bg-black/50" /> </TransitionChild> <div className="fixed inset-0 overflow-y-auto"> <div className="flex min-h-full items-center justify-center p-4 text-center"> <TransitionChild as={Fragment} enter="ease-out duration-200" enterFrom="opacity-0 scale-95" enterTo="opacity-100 scale-100" leave="ease-in duration-100" leaveFrom="opacity-100 scale-200" leaveTo="opacity-0 scale-95" > <DialogPanel className="w-full max-w-md transform rounded-2xl bg-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all"> <DialogTitle className="text-xl font-medium leading-6 dark:text-white"> Settings </DialogTitle> {config && !isLoading && ( <div className="flex flex-col space-y-4 mt-6"> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Theme </p> <ThemeSwitcher /> </div> {config.chatModelProviders && ( <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Chat model Provider </p> <Select value={selectedChatModelProvider ?? undefined} onChange={(e) => { setSelectedChatModelProvider(e.target.value); if (e.target.value === 'custom_openai') { setSelectedChatModel(''); } else { setSelectedChatModel( config.chatModelProviders[e.target.value][0] .name, ); } }} options={Object.keys(config.chatModelProviders).map( (provider) => ({ value: provider, label: provider.charAt(0).toUpperCase() + provider.slice(1), }), )} /> </div> )} {selectedChatModelProvider && selectedChatModelProvider != 'custom_openai' && ( <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Chat Model </p> <Select value={selectedChatModel ?? undefined} onChange={(e) => setSelectedChatModel(e.target.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, }, ]; })()} /> </div> )} {selectedChatModelProvider && selectedChatModelProvider === 'custom_openai' && ( <> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Model name </p> <Input type="text" placeholder="Model name" defaultValue={selectedChatModel!} onChange={(e) => setSelectedChatModel(e.target.value) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Custom OpenAI API Key </p> <Input type="text" placeholder="Custom OpenAI API Key" defaultValue={customOpenAIApiKey!} onChange={(e) => setCustomOpenAIApiKey(e.target.value) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Custom OpenAI Base URL </p> <Input type="text" placeholder="Custom OpenAI Base URL" defaultValue={customOpenAIBaseURL!} onChange={(e) => setCustomOpenAIBaseURL(e.target.value) } /> </div> </> )} {/* Embedding models */} {config.embeddingModelProviders && ( <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Embedding model Provider </p> <Select value={selectedEmbeddingModelProvider ?? undefined} onChange={(e) => { setSelectedEmbeddingModelProvider(e.target.value); setSelectedEmbeddingModel( config.embeddingModelProviders[e.target.value][0] .name, ); }} options={Object.keys( config.embeddingModelProviders, ).map((provider) => ({ label: provider.charAt(0).toUpperCase() + provider.slice(1), value: provider, }))} /> </div> )} {selectedEmbeddingModelProvider && ( <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Embedding Model </p> <Select value={selectedEmbeddingModel ?? undefined} onChange={(e) => setSelectedEmbeddingModel(e.target.value) } options={(() => { const embeddingModelProvider = config.embeddingModelProviders[ selectedEmbeddingModelProvider ]; return embeddingModelProvider ? embeddingModelProvider.length > 0 ? embeddingModelProvider.map((model) => ({ label: model.displayName, value: model.name, })) : [ { label: 'No embedding models available', value: '', disabled: true, }, ] : [ { label: 'Invalid provider, please check backend logs', value: '', disabled: true, }, ]; })()} /> </div> )} <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> OpenAI API Key </p> <Input type="text" placeholder="OpenAI API Key" defaultValue={config.openaiApiKey} onChange={(e) => setConfig({ ...config, openaiApiKey: e.target.value, }) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Ollama API URL </p> <Input type="text" placeholder="Ollama API URL" defaultValue={config.ollamaApiUrl} onChange={(e) => setConfig({ ...config, ollamaApiUrl: e.target.value, }) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> GROQ API Key </p> <Input type="text" placeholder="GROQ API Key" defaultValue={config.groqApiKey} onChange={(e) => setConfig({ ...config, groqApiKey: e.target.value, }) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Anthropic API Key </p> <Input type="text" placeholder="Anthropic API key" defaultValue={config.anthropicApiKey} onChange={(e) => setConfig({ ...config, anthropicApiKey: e.target.value, }) } /> </div> <div className="flex flex-col space-y-1"> <p className="text-black/70 dark:text-white/70 text-sm"> Gemini API Key </p> <Input type="text" placeholder="Gemini API key" defaultValue={config.geminiApiKey} onChange={(e) => setConfig({ ...config, geminiApiKey: e.target.value, }) } /> </div> </div> )} {isLoading && ( <div className="w-full flex items-center justify-center mt-6 text-black/70 dark:text-white/70 py-6"> <RefreshCcw className="animate-spin" /> </div> )} <div className="w-full mt-6 space-y-2"> <p className="text-xs text-black/50 dark:text-white/50"> We'll refresh the page after updating the settings. </p> <button onClick={handleSubmit} className="bg-[#24A0ED] flex flex-row items-center space-x-2 text-white disabled:text-white/50 hover:bg-opacity-85 transition duration-100 disabled:bg-[#ececec21] rounded-full px-4 py-2" disabled={isLoading || isUpdating} > {isUpdating ? ( <RefreshCw size={20} className="animate-spin" /> ) : ( <CloudUpload size={20} /> )} </button> </div> </DialogPanel> </TransitionChild> </div> </div> </Dialog> </Transition> ); }; export default SettingsDialog;