feat(ui): add settings page
This commit is contained in:
parent
3ffbddd237
commit
b2b1d724ee
|
@ -90,6 +90,7 @@ For setups without Docker:
|
||||||
## Upcoming Features
|
## Upcoming Features
|
||||||
|
|
||||||
- [ ] Finalizing Copilot Mode
|
- [ ] Finalizing Copilot Mode
|
||||||
|
- [x] Add settings page
|
||||||
- [x] Adding support for local LLMs
|
- [x] Adding support for local LLMs
|
||||||
- [ ] Adding Discover and History Saving features
|
- [ ] Adding Discover and History Saving features
|
||||||
- [x] Introducing various Focus Modes
|
- [x] Introducing various Focus Modes
|
||||||
|
|
|
@ -0,0 +1,229 @@
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
import { CloudUpload, RefreshCcw, RefreshCw } from 'lucide-react';
|
||||||
|
import React, { Fragment, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
interface SettingsType {
|
||||||
|
providers: {
|
||||||
|
[key: string]: string[];
|
||||||
|
};
|
||||||
|
selectedProvider: string;
|
||||||
|
selectedChatModel: string;
|
||||||
|
openeaiApiKey: string;
|
||||||
|
ollamaApiUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SettingsDialog = ({
|
||||||
|
isOpen,
|
||||||
|
setIsOpen,
|
||||||
|
}: {
|
||||||
|
isOpen: boolean;
|
||||||
|
setIsOpen: (isOpen: boolean) => void;
|
||||||
|
}) => {
|
||||||
|
const [config, setConfig] = useState<SettingsType | 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`);
|
||||||
|
const data = await res.json();
|
||||||
|
setConfig(data);
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
} 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.providers && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-white/70 text-sm">LLM Provider</p>
|
||||||
|
<select
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
selectedProvider: e.target.value,
|
||||||
|
selectedChatModel:
|
||||||
|
config.providers[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.providers).map((provider) => (
|
||||||
|
<option
|
||||||
|
key={provider}
|
||||||
|
value={provider}
|
||||||
|
selected={provider === config.selectedProvider}
|
||||||
|
>
|
||||||
|
{provider.charAt(0).toUpperCase() +
|
||||||
|
provider.slice(1)}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{config.selectedProvider && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-white/70 text-sm">Chat Model</p>
|
||||||
|
<select
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
selectedChatModel: 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.providers[config.selectedProvider] ? (
|
||||||
|
config.providers[config.selectedProvider].length >
|
||||||
|
0 ? (
|
||||||
|
config.providers[config.selectedProvider].map(
|
||||||
|
(model) => (
|
||||||
|
<option
|
||||||
|
key={model}
|
||||||
|
value={model}
|
||||||
|
selected={
|
||||||
|
model === config.selectedChatModel
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{model}
|
||||||
|
</option>
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<option value="" disabled selected>
|
||||||
|
No models available
|
||||||
|
</option>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<option value="" disabled selected>
|
||||||
|
Invalid provider
|
||||||
|
</option>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{config.selectedProvider === 'openai' && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-white/70 text-sm">OpenAI API Key</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
defaultValue={config.openeaiApiKey}
|
||||||
|
onChange={(e) =>
|
||||||
|
setConfig({
|
||||||
|
...config,
|
||||||
|
openeaiApiKey: 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>
|
||||||
|
)}
|
||||||
|
{config.selectedProvider === 'ollama' && (
|
||||||
|
<div className="flex flex-col space-y-1">
|
||||||
|
<p className="text-white/70 text-sm">Ollama API URL</p>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
{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 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<CloudUpload />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsDialog;
|
|
@ -1,16 +1,19 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { BookOpenText, Home, Search, SquarePen } from 'lucide-react';
|
import { BookOpenText, Home, Search, SquarePen, Settings } from 'lucide-react';
|
||||||
import { SiGithub } from '@icons-pack/react-simple-icons';
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useSelectedLayoutSegments } from 'next/navigation';
|
import { useSelectedLayoutSegments } from 'next/navigation';
|
||||||
import React from 'react';
|
import React, { Fragment, useState } from 'react';
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
import SettingsDialog from './SettingsDialog';
|
||||||
|
|
||||||
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
const segments = useSelectedLayoutSegments();
|
const segments = useSelectedLayoutSegments();
|
||||||
|
|
||||||
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{
|
{
|
||||||
icon: Home,
|
icon: Home,
|
||||||
|
@ -56,16 +59,14 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => {
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Settings
|
||||||
href="https://github.com/ItzCrazyKns/Perplexica"
|
onClick={() => setIsSettingsOpen(!isSettingsOpen)}
|
||||||
className="flex flex-col items-center text-center justify-center"
|
className="text-white cursor-pointer"
|
||||||
>
|
/>
|
||||||
<SiGithub
|
<SettingsDialog
|
||||||
className="text-white"
|
isOpen={isSettingsOpen}
|
||||||
onPointerEnterCapture={undefined}
|
setIsOpen={setIsSettingsOpen}
|
||||||
onPointerLeaveCapture={undefined}
|
/>
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue