Perplexica/ui/components/SettingsDialog.tsx

584 lines
24 KiB
TypeScript
Raw Normal View History

import { cn } from '@/lib/utils';
2024-04-23 16:52:41 +05:30
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';
2024-10-08 21:15:55 +03:00
import { Switch } from '@headlessui/react';
import Cookies from 'js-cookie';
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
2024-05-30 21:38:37 +05:30
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,
)}
/>
);
2024-05-30 21:38:37 +05:30
};
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,
)}
>
2024-10-08 21:15:55 +03:00
{options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
);
2024-05-30 21:38:37 +05:30
};
2024-04-23 16:52:41 +05:30
interface SettingsType {
chatModelProviders: {
[key: string]: [Record<string, any>];
};
embeddingModelProviders: {
[key: string]: [Record<string, any>];
2024-04-23 16:52:41 +05:30
};
2024-05-02 15:04:33 +05:30
openaiApiKey: string;
2024-05-01 19:43:06 +05:30
groqApiKey: string;
2024-07-15 21:20:16 +05:30
anthropicApiKey: string;
2024-04-23 16:52:41 +05:30
ollamaApiUrl: string;
}
2024-10-08 21:15:55 +03:00
interface AuthSettings {
isEnabled: boolean;
username: string;
password: string;
}
2024-04-23 16:52:41 +05:30
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>('');
2024-04-23 16:52:41 +05:30
const [isLoading, setIsLoading] = useState(false);
const [isUpdating, setIsUpdating] = useState(false);
2024-10-08 21:15:55 +03:00
const [authSettings, setAuthSettings] = useState<AuthSettings>({
isEnabled: false,
username: '',
password: '',
});
2024-04-23 16:52:41 +05:30
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: {
'Content-Type': 'application/json',
},
});
const data = (await res.json()) as SettingsType;
2024-04-23 16:52:41 +05:30
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]?.[0].name) ||
'';
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 || {});
2024-04-23 16:52:41 +05:30
setIsLoading(false);
};
fetchConfig();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isOpen]);
2024-10-08 21:15:55 +03:00
const handleAuthToggle = () => {
setAuthSettings((prev) => ({
...prev,
isEnabled: !prev.isEnabled,
username: '',
password: '',
}));
};
2024-04-23 16:52:41 +05:30
const handleSubmit = async () => {
setIsUpdating(true);
try {
2024-10-08 21:15:55 +03:00
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/auth-settings`, {
2024-04-23 16:52:41 +05:30
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
2024-10-08 21:15:55 +03:00
body: JSON.stringify(authSettings),
2024-04-23 16:52:41 +05:30
});
2024-10-08 21:15:55 +03:00
if (res.ok) {
if (authSettings.isEnabled) {
Cookies.set('authEnabled', 'true');
Cookies.set('authUsername', authSettings.username);
Cookies.set('authPassword', authSettings.password);
} else {
Cookies.remove('authEnabled');
Cookies.remove('authUsername');
Cookies.remove('authPassword');
}
window.location.reload();
} else {
const errorData = await res.json();
alert(errorData.message || 'Failed to update settings.');
}
} catch (error) {
console.error('Error updating auth settings:', error);
alert('An error occurred while updating settings.');
2024-04-23 16:52:41 +05:30
} finally {
setIsUpdating(false);
}
};
return (
<Transition appear show={isOpen} as={Fragment}>
2024-10-08 21:15:55 +03:00
<Dialog as="div" className="relative z-10" 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-10-08 21:15:55 +03:00
<div className="fixed inset-0 bg-black bg-opacity-25" />
2024-04-23 16:52:41 +05:30
</Transition.Child>
2024-10-08 21:15:55 +03:00
2024-04-23 16:52:41 +05:30
<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}
2024-10-08 21:15:55 +03:00
enter="ease-out duration-300"
2024-04-23 16:52:41 +05:30
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
2024-10-08 21:15:55 +03:00
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
2024-04-23 16:52:41 +05:30
leaveTo="opacity-0 scale-95"
>
2024-10-08 21:15:55 +03:00
<Dialog.Panel className="w-full max-w-md transform overflow-hidden rounded-2xl bg-white dark:bg-gray-800 p-6 text-left align-middle shadow-xl transition-all">
<Dialog.Title
as="h3"
className="text-lg font-medium leading-6 text-gray-900 dark:text-white"
>
2024-04-23 16:52:41 +05:30
Settings
</Dialog.Title>
{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 && (
2024-04-23 16:52:41 +05:30
<div className="flex flex-col space-y-1">
2024-05-24 20:29:49 +08:00
<p className="text-black/70 dark:text-white/70 text-sm">
2024-04-23 17:06:44 +05:30
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),
}),
)}
/>
2024-04-23 16:52:41 +05:30
</div>
)}
{selectedChatModelProvider &&
selectedChatModelProvider != 'custom_openai' && (
<div className="flex flex-col space-y-1">
2024-05-24 20:29:49 +08:00
<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">
2024-05-24 20:29:49 +08:00
<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">
2024-05-24 20:29:49 +08:00
<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">
2024-05-24 20:29:49 +08:00
<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-05-24 20:29:49 +08:00
<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">
2024-05-24 20:29:49 +08:00
<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,
},
];
})()}
/>
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-05-24 20:29:49 +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-04-23 17:06:44 +05:30
onChange={(e) =>
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-05-24 20:29:49 +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}
onChange={(e) =>
setConfig({
...config,
ollamaApiUrl: e.target.value,
})
}
/>
</div>
2024-05-01 19:43:06 +05:30
<div className="flex flex-col space-y-1">
2024-05-24 20:29:49 +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}
onChange={(e) =>
setConfig({
...config,
groqApiKey: e.target.value,
})
}
/>
</div>
2024-07-15 21:20:16 +05:30
<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>
2024-10-08 21:15:55 +03:00
{/* Authentication Toggle */}
<div className="mt-6 border-t border-gray-200 dark:border-gray-700 pt-6">
<h4 className="text-md font-semibold text-gray-700 dark:text-gray-200 mb-4">
HTTP Basic Authentication
</h4>
<div className="flex items-center justify-between">
<span className="text-gray-700 dark:text-gray-200">Enable Authentication</span>
<Switch
checked={authSettings.isEnabled}
onChange={handleAuthToggle}
className={`${
authSettings.isEnabled ? 'bg-blue-600' : 'bg-gray-200'
} relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2`}
>
<span
className={`${
authSettings.isEnabled ? 'translate-x-6' : 'translate-x-1'
} inline-block h-4 w-4 transform rounded-full bg-white transition-transform`}
/>
</Switch>
</div>
{authSettings.isEnabled && (
<div className="mt-4 space-y-4">
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Username</p>
<Input
type="text"
placeholder="Username"
value={authSettings.username}
onChange={(e) =>
setAuthSettings({
...authSettings,
username: e.target.value,
})
}
required={authSettings.isEnabled}
/>
</div>
<div className="flex flex-col space-y-1">
<p className="text-black/70 dark:text-white/70 text-sm">Password</p>
<Input
type="password"
placeholder="Password"
value={authSettings.password}
onChange={(e) =>
setAuthSettings({
...authSettings,
password: e.target.value,
})
}
required={authSettings.isEnabled}
/>
</div>
</div>
)}
</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}
>
{isUpdating ? (
2024-04-23 17:06:44 +05:30
<RefreshCw size={20} className="animate-spin" />
2024-04-23 16:52:41 +05:30
) : (
2024-04-23 17:06:44 +05:30
<CloudUpload size={20} />
2024-04-23 16:52:41 +05:30
)}
2024-10-08 21:15:55 +03:00
<span>Confirm</span>
2024-04-23 16:52:41 +05:30
</button>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
);
};
2024-10-08 21:15:55 +03:00
export default SettingsDialog;