Perplexica/ui/components/SettingsDialog.tsx

378 lines
17 KiB
TypeScript
Raw Normal View History

/* eslint-disable unicorn/no-nested-ternary */
2024-07-05 14:36:50 +08:00
import { cn } from "@/lib/utils";
import { Dialog, Transition } 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 InputProperties extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = ({ className, ...restProperties }: InputProperties) => {
return (
<input
{...restProperties}
className={cn(
2024-07-05 14:36:50 +08:00
"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,
)}
/>
);
2024-05-30 21:38:37 +05:30
};
interface SelectProperties extends SelectHTMLAttributes<HTMLSelectElement> {
options: { value: string; label: string; disabled?: boolean }[];
}
export const Select = ({ className, options, ...restProperties }: SelectProperties) => {
return (
<select
{...restProperties}
className={cn(
2024-07-05 14:36:50 +08:00
"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>
);
2024-05-30 21:38:37 +05:30
};
2024-04-23 16:52:41 +05:30
interface SettingsType {
chatModelProviders: {
[key: string]: string[];
};
embeddingModelProviders: {
2024-04-23 16:52:41 +05:30
[key: string]: string[];
};
2024-05-02 15:04:33 +05:30
openaiApiKey: string;
2024-05-01 19:43:06 +05:30
groqApiKey: string;
2024-04-23 16:52:41 +05:30
ollamaApiUrl: string;
}
2024-07-05 14:36:50 +08:00
const SettingsDialog = ({ isOpen, setIsOpen }: { isOpen: boolean; setIsOpen: (isOpen: boolean) => void }) => {
2024-04-23 16:52:41 +05:30
const [config, setConfig] = useState<SettingsType | null>(null);
2024-07-05 14:36:50 +08:00
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>("");
2024-04-23 16:52:41 +05:30
const [isLoading, setIsLoading] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
useEffect(() => {
if (isOpen) {
const fetchConfig = async () => {
setIsLoading(true);
2024-05-04 15:01:53 +05:30
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, {
headers: {
2024-07-05 14:36:50 +08:00
"Content-Type": "application/json",
2024-05-04 15:01:53 +05:30
},
});
const data = (await res.json()) as SettingsType;
2024-04-23 16:52:41 +05:30
setConfig(data);
2024-07-05 14:36:50 +08:00
const chatModelProvidersKeys = Object.keys(data.chatModelProviders || {});
const embeddingModelProvidersKeys = Object.keys(data.embeddingModelProviders || {});
2024-07-05 14:36:50 +08:00
const defaultChatModelProvider = chatModelProvidersKeys.length > 0 ? chatModelProvidersKeys[0] : "";
const defaultEmbeddingModelProvider =
2024-07-05 14:36:50 +08:00
embeddingModelProvidersKeys.length > 0 ? embeddingModelProvidersKeys[0] : "";
2024-07-05 14:36:50 +08:00
const chatModelProvider = localStorage.getItem("chatModelProvider") || defaultChatModelProvider || "";
const chatModel =
2024-07-05 14:36:50 +08:00
localStorage.getItem("chatModel") ||
(data.chatModelProviders && data.chatModelProviders[chatModelProvider]?.[0]) ||
"";
const embeddingModelProvider =
2024-07-05 14:36:50 +08:00
localStorage.getItem("embeddingModelProvider") || defaultEmbeddingModelProvider || "";
const embeddingModel =
2024-07-05 14:36:50 +08:00
localStorage.getItem("embeddingModel") ||
(data.embeddingModelProviders && data.embeddingModelProviders[embeddingModelProvider]?.[0]) ||
"";
setSelectedChatModelProvider(chatModelProvider);
setSelectedChatModel(chatModel);
setSelectedEmbeddingModelProvider(embeddingModelProvider);
setSelectedEmbeddingModel(embeddingModel);
2024-07-05 14:36:50 +08:00
setCustomOpenAIApiKey(localStorage.getItem("openAIApiKey") || "");
setCustomOpenAIBaseURL(localStorage.getItem("openAIBaseURL") || "");
2024-04-23 16:52:41 +05:30
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`, {
2024-07-05 14:36:50 +08:00
method: "POST",
2024-04-23 16:52:41 +05:30
headers: {
2024-07-05 14:36:50 +08:00
"Content-Type": "application/json",
2024-04-23 16:52:41 +05:30
},
body: JSON.stringify(config),
});
2024-07-05 14:36:50 +08:00
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 (error) {
console.log(error);
2024-04-23 16:52:41 +05:30
} finally {
setIsUpdating(false);
setIsOpen(false);
window.location.reload();
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
2024-07-05 14:36:50 +08:00
<Dialog as="div" className="relative z-50" onClose={() => setIsOpen(false)}>
2024-04-23 16:52:41 +05:30
<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"
>
2024-05-24 20:29:49 +08:00
<div className="fixed inset-0 bg-white/50 dark:bg-black/50" />
2024-04-23 16:52:41 +05:30
</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-light-secondary dark:bg-dark-secondary border border-light-200 dark:border-dark-200 p-6 text-left align-middle shadow-xl transition-all">
2024-07-05 14:36:50 +08:00
<Dialog.Title className="text-xl font-medium leading-6 dark:text-white">Settings</Dialog.Title>
2024-04-23 16:52:41 +05:30
{config && !isLoading && (
<div className="flex flex-col space-y-4 mt-6">
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">Theme</p>
<ThemeSwitcher />
</div>
{config.chatModelProviders && (
2024-04-23 16:52:41 +05:30
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">Chat model Provider</p>
<Select
value={selectedChatModelProvider ?? undefined}
2024-07-05 14:36:50 +08:00
onChange={e => {
setSelectedChatModelProvider(e.target.value);
2024-07-05 14:36:50 +08:00
setSelectedChatModel(config.chatModelProviders[e.target.value][0]);
}}
2024-07-05 14:36:50 +08:00
options={Object.keys(config.chatModelProviders).map(provider => ({
value: provider,
label: provider.charAt(0).toUpperCase() + provider.slice(1),
}))}
/>
2024-04-23 16:52:41 +05:30
</div>
)}
2024-07-05 14:36:50 +08:00
{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];
2024-07-05 14:36:50 +08:00
return chatModelProvider
? chatModelProvider.length > 0
? chatModelProvider.map(model => ({
value: model,
label: model,
}))
: [
{
2024-07-05 14:36:50 +08:00
value: "",
label: "No models available",
disabled: true,
},
2024-07-05 14:36:50 +08:00
]
: [
{
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>
2024-07-05 14:36:50 +08:00
<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 */}
2024-05-04 15:01:53 +05:30
{config.embeddingModelProviders && (
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">Embedding model Provider</p>
<Select
value={selectedEmbeddingModelProvider ?? undefined}
2024-07-05 14:36:50 +08:00
onChange={e => {
setSelectedEmbeddingModelProvider(e.target.value);
2024-07-05 14:36:50 +08:00
setSelectedEmbeddingModel(config.embeddingModelProviders[e.target.value][0]);
}}
2024-07-05 14:36:50 +08:00
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">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">Embedding Model</p>
<Select
value={selectedEmbeddingModel ?? undefined}
2024-07-05 14:36:50 +08:00
onChange={e => setSelectedEmbeddingModel(e.target.value)}
options={(() => {
const embeddingModelProvider =
2024-07-05 14:36:50 +08:00
config.embeddingModelProviders[selectedEmbeddingModelProvider];
return embeddingModelProvider
? embeddingModelProvider.length > 0
2024-07-05 14:36:50 +08:00
? embeddingModelProvider.map(model => ({
label: model,
value: model,
}))
: [
{
2024-07-05 14:36:50 +08:00
label: "No embedding models available",
value: "",
disabled: true,
},
]
: [
{
2024-07-05 14:36:50 +08:00
label: "Invalid provider, please check backend logs",
value: "",
disabled: true,
},
];
})()}
/>
2024-04-23 16:52:41 +05:30
</div>
)}
2024-04-23 17:06:44 +05:30
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">OpenAI API Key</p>
<Input
2024-04-23 17:06:44 +05:30
type="text"
placeholder="OpenAI API Key"
2024-05-02 15:04:33 +05:30
defaultValue={config.openaiApiKey}
2024-07-05 14:36:50 +08:00
onChange={e =>
2024-04-23 17:06:44 +05:30
setConfig({
...config,
2024-05-02 15:04:33 +05:30
openaiApiKey: e.target.value,
2024-04-23 17:06:44 +05:30
})
}
/>
</div>
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">Ollama API URL</p>
<Input
2024-04-23 17:06:44 +05:30
type="text"
placeholder="Ollama API URL"
2024-04-23 17:06:44 +05:30
defaultValue={config.ollamaApiUrl}
2024-07-05 14:36:50 +08:00
onChange={e =>
2024-04-23 17:06:44 +05:30
setConfig({
...config,
ollamaApiUrl: e.target.value,
})
}
/>
</div>
2024-05-01 19:43:06 +05:30
<div className="flex flex-col space-y-1">
2024-07-05 14:36:50 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">GROQ API Key</p>
<Input
2024-05-01 19:43:06 +05:30
type="text"
placeholder="GROQ API Key"
defaultValue={config.groqApiKey}
2024-07-05 14:36:50 +08:00
onChange={e =>
2024-05-01 19:43:06 +05:30
setConfig({
...config,
groqApiKey: e.target.value,
})
}
/>
</div>
2024-04-23 16:52:41 +05:30
</div>
)}
{isLoading && (
2024-05-24 20:29:49 +08:00
<div className="w-full flex items-center justify-center mt-6 text-black/70 dark:text-white/70 py-6">
2024-04-23 16:52:41 +05:30
<RefreshCcw className="animate-spin" />
</div>
)}
<div className="w-full mt-6 space-y-2">
2024-05-24 20:29:49 +08:00
<p className="text-xs text-black/50 dark:text-white/50">
2024-04-23 16:52:41 +05:30
We&apos;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}
>
2024-07-05 14:36:50 +08:00
{isUpdating ? <RefreshCw size={20} className="animate-spin" /> : <CloudUpload size={20} />}
2024-04-23 16:52:41 +05:30
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};
export default SettingsDialog;