Spaces:
Running
Running
import { Dialog, Transition } from '@headlessui/react'; | |
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react'; | |
import React, { Fragment, useEffect, useState } from 'react'; | |
interface SettingsType { | |
chatModelProviders: { | |
[key: string]: string[]; | |
}; | |
embeddingModelProviders: { | |
[key: string]: string[]; | |
}; | |
openaiApiKey: string; | |
groqApiKey: string; | |
ollamaApiUrl: string; | |
} | |
const SettingsDialog = ({ | |
isOpen, | |
setIsOpen, | |
}: { | |
isOpen: boolean; | |
setIsOpen: (isOpen: boolean) => void; | |
}) => { | |
const [config, setConfig] = useState<SettingsType | null>(null); | |
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 | null>( | |
null, | |
); | |
const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState<string | null>( | |
null, | |
); | |
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(); | |
setConfig(data); | |
setIsLoading(false); | |
}; | |
fetchConfig(); | |
} | |
// eslint-disable-next-line react-hooks/exhaustive-deps | |
}, [isOpen]); | |
useEffect(() => { | |
setSelectedChatModelProvider(localStorage.getItem('chatModelProvider')); | |
setSelectedChatModel(localStorage.getItem('chatModel')); | |
setSelectedEmbeddingModelProvider( | |
localStorage.getItem('embeddingModelProvider'), | |
); | |
setSelectedEmbeddingModel(localStorage.getItem('embeddingModel')); | |
setCustomOpenAIApiKey(localStorage.getItem('openAIApiKey')); | |
setCustomOpenAIBaseURL(localStorage.getItem('openAIBaseUrl')); | |
}, []); | |
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)} | |
> | |
<Transition.Child | |
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-black/50" /> | |
</Transition.Child> | |
<div className="fixed inset-0 overflow-y-auto"> | |
<div className="flex min-h-full items-center justify-center p-4 text-center"> | |
<Transition.Child | |
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" | |
> | |
<Dialog.Panel className="w-full max-w-md transform rounded-2xl bg-[#111111] border border-[#1c1c1c] p-6 text-left align-middle shadow-xl transition-all"> | |
<Dialog.Title className="text-xl font-medium leading-6 text-white"> | |
Settings | |
</Dialog.Title> | |
{config && !isLoading && ( | |
<div className="flex flex-col space-y-4 mt-6"> | |
{config.chatModelProviders && ( | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm"> | |
Chat model Provider | |
</p> | |
<select | |
onChange={(e) => { | |
setSelectedChatModelProvider(e.target.value); | |
setSelectedChatModel( | |
config.chatModelProviders[e.target.value][0], | |
); | |
}} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
> | |
{Object.keys(config.chatModelProviders).map( | |
(provider) => ( | |
<option | |
key={provider} | |
value={provider} | |
selected={ | |
provider === selectedChatModelProvider | |
} | |
> | |
{provider.charAt(0).toUpperCase() + | |
provider.slice(1)} | |
</option> | |
), | |
)} | |
</select> | |
</div> | |
)} | |
{selectedChatModelProvider && | |
selectedChatModelProvider != 'custom_openai' && ( | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm">Chat Model</p> | |
<select | |
onChange={(e) => | |
setSelectedChatModel(e.target.value) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
> | |
{config.chatModelProviders[ | |
selectedChatModelProvider | |
] ? ( | |
config.chatModelProviders[ | |
selectedChatModelProvider | |
].length > 0 ? ( | |
config.chatModelProviders[ | |
selectedChatModelProvider | |
].map((model) => ( | |
<option | |
key={model} | |
value={model} | |
selected={model === selectedChatModel} | |
> | |
{model} | |
</option> | |
)) | |
) : ( | |
<option value="" disabled selected> | |
No models available | |
</option> | |
) | |
) : ( | |
<option value="" disabled selected> | |
Invalid provider, please check backend logs | |
</option> | |
)} | |
</select> | |
</div> | |
)} | |
{selectedChatModelProvider && | |
selectedChatModelProvider === 'custom_openai' && ( | |
<> | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm">Model name</p> | |
<input | |
type="text" | |
placeholder="Model name" | |
defaultValue={selectedChatModel!} | |
onChange={(e) => | |
setSelectedChatModel(e.target.value) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm"> | |
Custom OpenAI API Key (optional) | |
</p> | |
<input | |
type="text" | |
placeholder="Custom OpenAI API Key" | |
defaultValue={customOpenAIApiKey!} | |
onChange={(e) => | |
setCustomOpenAIApiKey(e.target.value) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
<div className="flex flex-col space-y-1"> | |
<p className="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) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
</> | |
)} | |
{/* Embedding models */} | |
{config.embeddingModelProviders && ( | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm"> | |
Embedding model Provider | |
</p> | |
<select | |
onChange={(e) => { | |
setSelectedEmbeddingModelProvider(e.target.value); | |
setSelectedEmbeddingModel( | |
config.embeddingModelProviders[e.target.value][0], | |
); | |
}} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
> | |
{Object.keys(config.embeddingModelProviders).map( | |
(provider) => ( | |
<option | |
key={provider} | |
value={provider} | |
selected={ | |
provider === selectedEmbeddingModelProvider | |
} | |
> | |
{provider.charAt(0).toUpperCase() + | |
provider.slice(1)} | |
</option> | |
), | |
)} | |
</select> | |
</div> | |
)} | |
{selectedEmbeddingModelProvider && ( | |
<div className="flex flex-col space-y-1"> | |
<p className="text-white/70 text-sm">Embedding Model</p> | |
<select | |
onChange={(e) => | |
setSelectedEmbeddingModel(e.target.value) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
> | |
{config.embeddingModelProviders[ | |
selectedEmbeddingModelProvider | |
] ? ( | |
config.embeddingModelProviders[ | |
selectedEmbeddingModelProvider | |
].length > 0 ? ( | |
config.embeddingModelProviders[ | |
selectedEmbeddingModelProvider | |
].map((model) => ( | |
<option | |
key={model} | |
value={model} | |
selected={model === selectedEmbeddingModel} | |
> | |
{model} | |
</option> | |
)) | |
) : ( | |
<option value="" disabled selected> | |
No embedding models available | |
</option> | |
) | |
) : ( | |
<option value="" disabled selected> | |
Invalid provider, please check backend logs | |
</option> | |
)} | |
</select> | |
</div> | |
)} | |
<div className="flex flex-col space-y-1"> | |
<p className="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, | |
}) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
<div className="flex flex-col space-y-1"> | |
<p className="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, | |
}) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
<div className="flex flex-col space-y-1"> | |
<p className="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, | |
}) | |
} | |
className="bg-[#111111] px-3 py-2 flex items-center overflow-hidden border border-[#1C1C1C] text-white rounded-lg text-sm" | |
/> | |
</div> | |
</div> | |
)} | |
{isLoading && ( | |
<div className="w-full flex items-center justify-center mt-6 text-white/70 py-6"> | |
<RefreshCcw className="animate-spin" /> | |
</div> | |
)} | |
<div className="w-full mt-6 space-y-2"> | |
<p className="text-xs 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> | |
</Dialog.Panel> | |
</Transition.Child> | |
</div> | |
</div> | |
</Dialog> | |
</Transition> | |
); | |
}; | |
export default SettingsDialog; | |