From cab1aa705ca28fcea2ea3bc9bba0f28beb04247d Mon Sep 17 00:00:00 2001 From: ItzCrazyKns <95534749+ItzCrazyKns@users.noreply.github.com> Date: Sat, 15 Feb 2025 11:31:08 +0530 Subject: [PATCH] feat(settings): add new settings page --- ui/app/settings/page.tsx | 664 +++++++++++++++++++++++++++++++ ui/components/ChatWindow.tsx | 51 +-- ui/components/EmptyChat.tsx | 10 +- ui/components/SettingsDialog.tsx | 528 ------------------------ ui/components/Sidebar.tsx | 13 +- ui/components/theme/Switcher.tsx | 3 +- ui/components/ui/Select.tsx | 28 ++ 7 files changed, 715 insertions(+), 582 deletions(-) create mode 100644 ui/app/settings/page.tsx delete mode 100644 ui/components/SettingsDialog.tsx create mode 100644 ui/components/ui/Select.tsx diff --git a/ui/app/settings/page.tsx b/ui/app/settings/page.tsx new file mode 100644 index 0000000..4026f16 --- /dev/null +++ b/ui/app/settings/page.tsx @@ -0,0 +1,664 @@ +'use client'; + +import { Settings as SettingsIcon, ArrowLeft, Loader2 } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { cn } from '@/lib/utils'; +import { Switch } from '@headlessui/react'; +import ThemeSwitcher from '@/components/theme/Switcher'; +import { ImagesIcon, VideoIcon } from 'lucide-react'; +import Link from 'next/link'; + +interface SettingsType { + chatModelProviders: { + [key: string]: [Record]; + }; + embeddingModelProviders: { + [key: string]: [Record]; + }; + openaiApiKey: string; + groqApiKey: string; + anthropicApiKey: string; + geminiApiKey: string; + ollamaApiUrl: string; + customOpenaiApiKey: string; + customOpenaiApiUrl: string; + customOpenaiModelName: string; +} + +interface InputProps extends React.InputHTMLAttributes { + isSaving?: boolean; + onSave?: (value: string) => void; +} + +const Input = ({ className, isSaving, onSave, ...restProps }: InputProps) => { + return ( +
+ onSave?.(e.target.value)} + /> + {isSaving && ( +
+ +
+ )} +
+ ); +}; + +const Select = ({ + className, + options, + ...restProps +}: React.SelectHTMLAttributes & { + options: { value: string; label: string; disabled?: boolean }[]; +}) => { + return ( + + ); +}; + +const SettingsSection = ({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) => ( +
+

{title}

+ {children} +
+); + +const Page = () => { + const [config, setConfig] = useState(null); + const [chatModels, setChatModels] = useState>({}); + const [embeddingModels, setEmbeddingModels] = useState>( + {}, + ); + const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< + string | null + >(null); + const [selectedChatModel, setSelectedChatModel] = useState( + null, + ); + const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = + useState(null); + const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< + string | null + >(null); + const [isLoading, setIsLoading] = useState(false); + const [automaticImageSearch, setAutomaticImageSearch] = useState(false); + const [automaticVideoSearch, setAutomaticVideoSearch] = useState(false); + const [savingStates, setSavingStates] = useState>({}); + + useEffect(() => { + 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()) as SettingsType; + 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]?.length > 0 + ? data.chatModelProviders[chatModelProvider][0].name + : undefined) || + ''; + 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); + setChatModels(data.chatModelProviders || {}); + setEmbeddingModels(data.embeddingModelProviders || {}); + + setAutomaticImageSearch( + localStorage.getItem('autoImageSearch') === 'true', + ); + setAutomaticVideoSearch( + localStorage.getItem('autoVideoSearch') === 'true', + ); + + setIsLoading(false); + }; + + fetchConfig(); + }, []); + + const saveConfig = async (key: string, value: any) => { + setSavingStates((prev) => ({ ...prev, [key]: true })); + + try { + const updatedConfig = { + ...config, + [key]: value, + } as SettingsType; + + const response = await fetch( + `${process.env.NEXT_PUBLIC_API_URL}/config`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updatedConfig), + }, + ); + + if (!response.ok) { + throw new Error('Failed to update config'); + } + + setConfig(updatedConfig); + + if ( + key.toLowerCase().includes('api') || + key.toLowerCase().includes('url') + ) { + const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/config`, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error('Failed to fetch updated config'); + } + + const data = await res.json(); + + setChatModels(data.chatModelProviders || {}); + setEmbeddingModels(data.embeddingModelProviders || {}); + + const currentProvider = selectedChatModelProvider; + const newProviders = Object.keys(data.chatModelProviders || {}); + + if (!currentProvider && newProviders.length > 0) { + const firstProvider = newProviders[0]; + const firstModel = data.chatModelProviders[firstProvider]?.[0]?.name; + + if (firstModel) { + setSelectedChatModelProvider(firstProvider); + setSelectedChatModel(firstModel); + localStorage.setItem('chatModelProvider', firstProvider); + localStorage.setItem('chatModel', firstModel); + } + } else if ( + currentProvider && + (!data.chatModelProviders || + !data.chatModelProviders[currentProvider] || + !Array.isArray(data.chatModelProviders[currentProvider]) || + data.chatModelProviders[currentProvider].length === 0) + ) { + const firstValidProvider = Object.entries( + data.chatModelProviders || {}, + ).find( + ([_, models]) => Array.isArray(models) && models.length > 0, + )?.[0]; + + if (firstValidProvider) { + setSelectedChatModelProvider(firstValidProvider); + setSelectedChatModel( + data.chatModelProviders[firstValidProvider][0].name, + ); + localStorage.setItem('chatModelProvider', firstValidProvider); + localStorage.setItem( + 'chatModel', + data.chatModelProviders[firstValidProvider][0].name, + ); + } else { + setSelectedChatModelProvider(null); + setSelectedChatModel(null); + localStorage.removeItem('chatModelProvider'); + localStorage.removeItem('chatModel'); + } + } + + setConfig(data); + } + + if (key === 'automaticImageSearch') { + localStorage.setItem('autoImageSearch', value.toString()); + } else if (key === 'automaticVideoSearch') { + localStorage.setItem('autoVideoSearch', value.toString()); + } else if (key === 'chatModelProvider') { + localStorage.setItem('chatModelProvider', value); + } else if (key === 'chatModel') { + localStorage.setItem('chatModel', value); + } + } catch (err) { + console.error('Failed to save:', err); + setConfig((prev) => ({ ...prev! })); + } finally { + setTimeout(() => { + setSavingStates((prev) => ({ ...prev, [key]: false })); + }, 500); + } + }; + + return ( +
+
+
+ + + +
+ +

Settings

+
+
+
+
+ + {isLoading ? ( +
+ +
+ ) : ( + config && ( +
+ +
+

+ Theme +

+ +
+
+ + +
+
+
+
+ +
+
+

+ Automatic Image Search +

+

+ Automatically search for relevant images in chat + responses +

+
+
+ { + setAutomaticImageSearch(checked); + saveConfig('automaticImageSearch', checked); + }} + className={cn( + automaticImageSearch + ? 'bg-[#24A0ED]' + : 'bg-light-200 dark:bg-dark-200', + 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', + )} + > + + +
+ +
+
+
+ +
+
+

+ Automatic Video Search +

+

+ Automatically search for relevant videos in chat + responses +

+
+
+ { + setAutomaticVideoSearch(checked); + saveConfig('automaticVideoSearch', checked); + }} + className={cn( + automaticVideoSearch + ? 'bg-[#24A0ED]' + : 'bg-light-200 dark:bg-dark-200', + 'relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none', + )} + > + + +
+
+
+ + + {config.chatModelProviders && ( +
+
+

+ Chat Model Provider +

+ { + const value = e.target.value; + setSelectedChatModel(value); + saveConfig('chatModel', 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, + }, + ]; + })()} + /> +
+ )} +
+ )} + + {selectedChatModelProvider && + selectedChatModelProvider === 'custom_openai' && ( +
+
+

+ Model Name +

+ + setConfig({ + ...config, + customOpenaiModelName: e.target.value, + }) + } + /> +
+
+

+ Custom OpenAI API Key +

+ + setConfig({ + ...config, + customOpenaiApiKey: e.target.value, + }) + } + /> +
+
+

+ Custom OpenAI Base URL +

+ + setConfig({ + ...config, + customOpenaiApiUrl: e.target.value, + }) + } + /> +
+
+ )} +
+ + +
+
+

+ OpenAI API Key +

+ { + setConfig((prev) => ({ + ...prev!, + openaiApiKey: e.target.value, + })); + }} + onSave={(value) => saveConfig('openaiApiKey', value)} + /> +
+ +
+

+ Ollama API URL +

+ { + setConfig((prev) => ({ + ...prev!, + ollamaApiUrl: e.target.value, + })); + }} + onSave={(value) => saveConfig('ollamaApiUrl', value)} + /> +
+ +
+

+ GROQ API Key +

+ { + setConfig((prev) => ({ + ...prev!, + groqApiKey: e.target.value, + })); + }} + onSave={(value) => saveConfig('groqApiKey', value)} + /> +
+ +
+

+ Anthropic API Key +

+ { + setConfig((prev) => ({ + ...prev!, + anthropicApiKey: e.target.value, + })); + }} + onSave={(value) => saveConfig('anthropicApiKey', value)} + /> +
+ +
+

+ Gemini API Key +

+ { + setConfig((prev) => ({ + ...prev!, + geminiApiKey: e.target.value, + })); + }} + onSave={(value) => saveConfig('geminiApiKey', value)} + /> +
+
+
+
+ ) + )} +
+ ); +}; + +export default Page; diff --git a/ui/components/ChatWindow.tsx b/ui/components/ChatWindow.tsx index 33ef27b..1940f42 100644 --- a/ui/components/ChatWindow.tsx +++ b/ui/components/ChatWindow.tsx @@ -10,7 +10,7 @@ import { toast } from 'sonner'; import { useSearchParams } from 'next/navigation'; import { getSuggestions } from '@/lib/actions'; import { Settings } from 'lucide-react'; -import SettingsDialog from './SettingsDialog'; +import Link from 'next/link'; import NextError from 'next/error'; export type Message = { @@ -40,6 +40,7 @@ const useSocket = ( const isCleaningUpRef = useRef(false); const MAX_RETRIES = 3; const INITIAL_BACKOFF = 1000; // 1 second + const isConnectionErrorRef = useRef(false); const getBackoffDelay = (retryCount: number) => { return Math.min(INITIAL_BACKOFF * Math.pow(2, retryCount), 10000); // Cap at 10 seconds @@ -97,21 +98,13 @@ const useSocket = ( chatModelProvider = chatModelProvider || Object.keys(chatModelProviders)[0]; - if (chatModelProvider === 'custom_openai') { - toast.error( - 'Seems like you are using the custom OpenAI provider, please open the settings and enter a model name to use.', - ); - setError(true); - return; - } else { - chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; + chatModel = Object.keys(chatModelProviders[chatModelProvider])[0]; - if ( - !chatModelProviders || - Object.keys(chatModelProviders).length === 0 - ) - return toast.error('No chat models available'); - } + if ( + !chatModelProviders || + Object.keys(chatModelProviders).length === 0 + ) + return toast.error('No chat models available'); } if (!embeddingModel || !embeddingModelProvider) { @@ -142,9 +135,7 @@ const useSocket = ( if ( Object.keys(chatModelProviders).length > 0 && - (((!openAIBaseURL || !openAIPIKey) && - chatModelProvider === 'custom_openai') || - !chatModelProviders[chatModelProvider]) + !chatModelProviders[chatModelProvider] ) { const chatModelProvidersKeys = Object.keys(chatModelProviders); chatModelProvider = @@ -152,23 +143,11 @@ const useSocket = ( (key) => Object.keys(chatModelProviders[key]).length > 0, ) || chatModelProvidersKeys[0]; - if ( - chatModelProvider === 'custom_openai' && - (!openAIBaseURL || !openAIPIKey) - ) { - toast.error( - 'Seems like you are using the custom OpenAI provider, please open the settings and configure the API key and base URL', - ); - setError(true); - return; - } - localStorage.setItem('chatModelProvider', chatModelProvider); } if ( chatModelProvider && - (!openAIBaseURL || !openAIPIKey) && !chatModelProviders[chatModelProvider][chatModel] ) { chatModel = Object.keys( @@ -254,6 +233,8 @@ const useSocket = ( console.debug(new Date(), 'ws:connected'); } if (data.type === 'error') { + isConnectionErrorRef.current = true; + setError(true); toast.error(data.data); } }); @@ -268,7 +249,7 @@ const useSocket = ( clearTimeout(timeoutId); setIsWSReady(false); console.debug(new Date(), 'ws:disconnected'); - if (!isCleaningUpRef.current) { + if (!isCleaningUpRef.current && !isConnectionErrorRef.current) { toast.error('Connection lost. Attempting to reconnect...'); attemptReconnect(); } @@ -643,17 +624,15 @@ const ChatWindow = ({ id }: { id?: string }) => { return (
- setIsSettingsOpen(true)} - /> + + +

Failed to connect to the server. Please try again later.

-
); } diff --git a/ui/components/EmptyChat.tsx b/ui/components/EmptyChat.tsx index c47c301..838849f 100644 --- a/ui/components/EmptyChat.tsx +++ b/ui/components/EmptyChat.tsx @@ -1,8 +1,8 @@ import { Settings } from 'lucide-react'; import EmptyChatMessageInput from './EmptyChatMessageInput'; -import SettingsDialog from './SettingsDialog'; import { useState } from 'react'; import { File } from './ChatWindow'; +import Link from 'next/link'; const EmptyChat = ({ sendMessage, @@ -29,12 +29,10 @@ const EmptyChat = ({ return (
-
- setIsSettingsOpen(true)} - /> + + +

diff --git a/ui/components/SettingsDialog.tsx b/ui/components/SettingsDialog.tsx deleted file mode 100644 index 163857b..0000000 --- a/ui/components/SettingsDialog.tsx +++ /dev/null @@ -1,528 +0,0 @@ -import { cn } from '@/lib/utils'; -import { - Dialog, - DialogPanel, - DialogTitle, - Transition, - TransitionChild, -} 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 InputProps extends React.InputHTMLAttributes {} - -const Input = ({ className, ...restProps }: InputProps) => { - return ( - - ); -}; - -interface SelectProps extends SelectHTMLAttributes { - options: { value: string; label: string; disabled?: boolean }[]; -} - -export const Select = ({ className, options, ...restProps }: SelectProps) => { - return ( - - ); -}; - -interface SettingsType { - chatModelProviders: { - [key: string]: [Record]; - }; - embeddingModelProviders: { - [key: string]: [Record]; - }; - openaiApiKey: string; - groqApiKey: string; - anthropicApiKey: string; - geminiApiKey: string; - ollamaApiUrl: string; -} - -const SettingsDialog = ({ - isOpen, - setIsOpen, -}: { - isOpen: boolean; - setIsOpen: (isOpen: boolean) => void; -}) => { - const [config, setConfig] = useState(null); - const [chatModels, setChatModels] = useState>({}); - const [embeddingModels, setEmbeddingModels] = useState>( - {}, - ); - const [selectedChatModelProvider, setSelectedChatModelProvider] = useState< - string | null - >(null); - const [selectedChatModel, setSelectedChatModel] = useState( - null, - ); - const [selectedEmbeddingModelProvider, setSelectedEmbeddingModelProvider] = - useState(null); - const [selectedEmbeddingModel, setSelectedEmbeddingModel] = useState< - string | null - >(null); - const [customOpenAIApiKey, setCustomOpenAIApiKey] = useState(''); - const [customOpenAIBaseURL, setCustomOpenAIBaseURL] = useState(''); - 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()) as SettingsType; - 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]?.length > 0 - ? data.chatModelProviders[chatModelProvider][0].name - : undefined) || - ''; - 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 || {}); - 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), - }); - - 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 ( - - setIsOpen(false)} - > - -
- -
-
- - - - Settings - - {config && !isLoading && ( -
-
-

- Theme -

- -
- {config.chatModelProviders && ( -
-

- Chat model Provider -

- - 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, - }, - ]; - })()} - /> -
- )} - {selectedChatModelProvider && - selectedChatModelProvider === 'custom_openai' && ( - <> -
-

- Model name -

- - setSelectedChatModel(e.target.value) - } - /> -
-
-

- Custom OpenAI API Key -

- - setCustomOpenAIApiKey(e.target.value) - } - /> -
-
-

- Custom OpenAI Base URL -

- - setCustomOpenAIBaseURL(e.target.value) - } - /> -
- - )} - {/* Embedding models */} - {config.embeddingModelProviders && ( -
-

- Embedding model Provider -

- - 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, - }, - ]; - })()} - /> -
- )} -
-

- OpenAI API Key -

- - setConfig({ - ...config, - openaiApiKey: e.target.value, - }) - } - /> -
-
-

- Ollama API URL -

- - setConfig({ - ...config, - ollamaApiUrl: e.target.value, - }) - } - /> -
-
-

- GROQ API Key -

- - setConfig({ - ...config, - groqApiKey: e.target.value, - }) - } - /> -
-
-

- Anthropic API Key -

- - setConfig({ - ...config, - anthropicApiKey: e.target.value, - }) - } - /> -
-
-

- Gemini API Key -

- - setConfig({ - ...config, - geminiApiKey: e.target.value, - }) - } - /> -
-
- )} - {isLoading && ( -
- -
- )} -
-

- We'll refresh the page after updating the settings. -

- -
-
-
-
-
-
-
- ); -}; - -export default SettingsDialog; diff --git a/ui/components/Sidebar.tsx b/ui/components/Sidebar.tsx index cc2097d..81db8ba 100644 --- a/ui/components/Sidebar.tsx +++ b/ui/components/Sidebar.tsx @@ -6,7 +6,6 @@ import Link from 'next/link'; import { useSelectedLayoutSegments } from 'next/navigation'; import React, { useState, type ReactNode } from 'react'; import Layout from './Layout'; -import SettingsDialog from './SettingsDialog'; const VerticalIconContainer = ({ children }: { children: ReactNode }) => { return ( @@ -67,15 +66,9 @@ const Sidebar = ({ children }: { children: React.ReactNode }) => { ))} - setIsSettingsOpen(!isSettingsOpen)} - className="cursor-pointer" - /> - - + + +

diff --git a/ui/components/theme/Switcher.tsx b/ui/components/theme/Switcher.tsx index 43bbdc8..b1e7371 100644 --- a/ui/components/theme/Switcher.tsx +++ b/ui/components/theme/Switcher.tsx @@ -1,8 +1,7 @@ 'use client'; import { useTheme } from 'next-themes'; -import { SunIcon, MoonIcon, MonitorIcon } from 'lucide-react'; import { useCallback, useEffect, useState } from 'react'; -import { Select } from '../SettingsDialog'; +import Select from '../ui/Select'; type Theme = 'dark' | 'light' | 'system'; diff --git a/ui/components/ui/Select.tsx b/ui/components/ui/Select.tsx new file mode 100644 index 0000000..8402149 --- /dev/null +++ b/ui/components/ui/Select.tsx @@ -0,0 +1,28 @@ +import { cn } from '@/lib/utils'; +import { SelectHTMLAttributes } from 'react'; + +interface SelectProps extends SelectHTMLAttributes { + options: { value: string; label: string; disabled?: boolean }[]; +} + +export const Select = ({ className, options, ...restProps }: SelectProps) => { + return ( + + ); +}; + +export default Select;