Spaces:
Running
Running
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; | |