mirror of
				https://github.com/ItzCrazyKns/Perplexica.git
				synced 2025-11-04 04:38:15 +00:00 
			
		
		
		
	feat(ui): add settings page
This commit is contained in:
		@@ -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
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										229
									
								
								ui/components/SettingsDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								ui/components/SettingsDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user