diff --git a/src/components/Preferences/ModelsSettings.tsx b/src/components/Preferences/ModelsSettings.tsx index 4f11c5d0..92299908 100644 --- a/src/components/Preferences/ModelsSettings.tsx +++ b/src/components/Preferences/ModelsSettings.tsx @@ -30,7 +30,6 @@ import { VStack, } from "@chakra-ui/react"; import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useDebounce } from "react-use"; import { capitalize } from "lodash-es"; import { FaCheck } from "react-icons/fa"; @@ -65,6 +64,10 @@ async function isStoragePersisted() { return false; } +interface ModelsSettingsProps { + isOpen: boolean; +} + function ModelsSettings(isOpen: ModelsSettingsProps) { const { settings, setSettings } = useSettings(); const { models } = useModels(); @@ -87,39 +90,6 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { settings.currentProvider ); - // Get the list of providers to display in providers table - // Combination of default list of supported providers and settings.providers from localStorage - useEffect(() => { - setTableProviders({ ...supportedProviders, ...settings.providers }); - }, [settings.providers]); - - // Check the API Key, but debounce requests if user is typing - useDebounce( - () => { - setIsApiKeyInvalid(false); - if (focusedProvider) { - setIsValidating(true); - if (!focusedProvider.apiKey) { - setIsApiKeyInvalid(true); - setIsValidating(false); - } else { - focusedProvider - .validateApiKey(focusedProvider.apiKey) - .then((result: boolean) => { - setIsApiKeyInvalid(!result); - }) - .catch((err: any) => { - console.warn("Error validating API key", err.message); - setIsApiKeyInvalid(true); - }) - .finally(() => setIsValidating(false)); - } - } - }, - 500, - [focusedProvider] - ); - useEffect(() => { isStoragePersisted() .then((value) => setIsPersisted(value)) @@ -215,7 +185,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { } }, [addToAudioQueue, clearAudioQueue, error, settings.textToSpeech]); - const handleApiKeyChange = (provider: ChatCraftProvider, apiKey: string) => { + const handleApiKeyChange = async (provider: ChatCraftProvider, apiKey: string) => { const newProvider = providerFromUrl( provider.apiUrl, apiKey, @@ -223,31 +193,45 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { provider.defaultModel ); - // Set as focused provider to trigger key validation setFocusedProvider(newProvider); - if (newProvider.name === settings.currentProvider.name) { - // Save key to settings.currentProvider and settings.providers - setSettings({ - ...settings, - currentProvider: newProvider, - providers: { - ...settings.providers, - [newProvider.name]: newProvider, - }, - }); - } else { - // Save key to settings.providers - setSettings({ - ...settings, - providers: { - ...settings.providers, - [newProvider.name]: newProvider, - }, - }); + const newProviders = { ...settings.providers }; + + // Update api key in table + tableProviders[newProvider.name] = newProvider; + + // Api key validation + try { + setIsApiKeyInvalid(false); + setIsValidating(true); + + const result = await newProvider.validateApiKey(newProvider.apiKey!); + + setIsApiKeyInvalid(!result); + setIsValidating(false); + + // Valid key, update in settings.providers + if (result) { + // Valid key, update in settings.providers + newProviders[newProvider.name] = newProvider; + } else { + // Invalid key, remove from settings.providers + delete newProviders[newProvider.name]; + } + } catch (err: any) { + setIsApiKeyInvalid(true); + setIsValidating(false); + + // Invalid key, remove from settings.providers + delete newProviders[newProvider.name]; } - // If the key that was changed is the selected provider, update the selected provider + setSettings({ + ...settings, + ...(newProvider.name === settings.currentProvider.name && { currentProvider: newProvider }), + providers: newProviders, + }); + if (newProvider.name === selectedProvider?.name) { setSelectedProvider(newProvider); } @@ -273,41 +257,53 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { setFocusedProvider(selectedProvider); - // Validate api key - setIsApiKeyInvalid(false); - setIsValidating(true); - if (!selectedProvider.apiKey) { - setIsApiKeyInvalid(true); + // Api key validation + try { + setIsApiKeyInvalid(false); + setIsValidating(true); + + const result = await selectedProvider.validateApiKey(selectedProvider.apiKey!); + + setIsApiKeyInvalid(!result); setIsValidating(false); - } else { - selectedProvider - .validateApiKey(selectedProvider.apiKey) - .then((result: boolean) => { - setIsApiKeyInvalid(!result); - - // Set as current provider - setSettings({ ...settings, currentProvider: selectedProvider }); - setApiKeySaved(true); - setSelectedProvider(null); - - success({ - title: "Current provider changed", - message: `${selectedProvider.name} set as current provider`, - }); - - // Uncheck checkbox - setSelectedProvider(null); - }) - .catch((err: any) => { - console.warn("Error validating API key", err); - setIsApiKeyInvalid(true); - - warning({ - title: "Provider not set", - message: err.message, - }); - }) - .finally(() => setIsValidating(false)); + + // Valid key + if (result) { + // Set as current provider + setSettings({ ...settings, currentProvider: selectedProvider }); + setApiKeySaved(true); + setSelectedProvider(null); + + success({ + title: "Current provider changed", + message: `${selectedProvider.name} set as current provider`, + }); + + setSettings({ ...settings, currentProvider: selectedProvider }); + setApiKeySaved(true); + + // Sync table + tableProviders[selectedProvider.name] = selectedProvider; + + // Uncheck checkbox + setSelectedProvider(null); + } else { + console.warn("Error validating API key"); + error({ + title: "Provider not set", + message: "Invalid API key", + }); + } + } catch (err: any) { + setIsValidating(false); + + console.warn("Error validating API key", err); + setIsApiKeyInvalid(true); + + error({ + title: "Provider not set", + message: err.message, + }); } }; @@ -352,12 +348,15 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { return; } - const newSettingsProviders = { ...settings.providers }; - delete newSettingsProviders[selectedProvider.name]; - setSettings({ ...settings, providers: newSettingsProviders }); + const newProviders = { ...settings.providers }; + delete newProviders[selectedProvider.name]; + setSettings({ ...settings, providers: newProviders }); // Uncheck checkbox setSelectedProvider(null); + + // Sync table + setTableProviders({ ...supportedProviders, ...newProviders }); }; const handleSaveNewCustomProvider = async () => { @@ -400,12 +399,16 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { return; } - // Validate api key + // Api key validation try { setIsApiKeyInvalid(false); setIsValidating(true); + const result = await newProvider.validateApiKey(newProvider.apiKey!); + setIsApiKeyInvalid(!result); + setIsValidating(false); + if (!result) { return; } @@ -417,7 +420,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { } catch (err: any) { console.warn("Error querying models for custom provider:", err); setFocusedProvider(null); - warning({ + error({ title: "Provider not added", message: err.message, }); @@ -427,7 +430,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { if (models.length === 0) { console.warn("No models available for custom provider"); setFocusedProvider(null); - warning({ + error({ title: "Provider not added", message: "Provider is not Open AI compatible.", }); @@ -442,16 +445,18 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { models[0] ); - // Save the new custom provider + const newProviders = { ...settings.providers }; + newProviders[newProviderWithModel.name] = newProviderWithModel; + setSettings({ ...settings, - providers: { - ...settings.providers, - [newProviderWithModel.name]: newProviderWithModel, - }, + providers: newProviders, }); setApiKeySaved(true); + // Sync table + setTableProviders({ ...supportedProviders, ...newProviders }); + success({ title: `New provider added`, message: `${newProviderWithModel.name}`, @@ -463,7 +468,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { } catch (err: any) { console.warn("Error validating API key", err.message); setFocusedProvider(null); - warning({ + error({ title: "Provider not added", message: err.message, }); @@ -486,10 +491,14 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { // Clean up actions when modal closes useEffect(() => { + // Sync table + setTableProviders({ ...supportedProviders, ...settings.providers }); + if (!isOpen) { setNewCustomProvider(null); setIsApiKeyInvalid(false); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [isOpen]); const isTtsSupported = useMemo(() => { @@ -501,7 +510,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { {!tableProviders ? ( Loading providers... ) : ( - + Providers @@ -653,7 +662,6 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { paddingRight={"2.5rem"} paddingLeft={"0.5rem"} fontSize="xs" - placeholder="API Key" value={newCustomProvider.apiKey || ""} onChange={(e) => { setNewCustomProvider( @@ -750,7 +758,7 @@ function ModelsSettings(isOpen: ModelsSettingsProps) { ) : ( - {focusedProvider?.apiKey + {provider.apiKey ? "Unable to verify key." : "API Key is required."} diff --git a/src/components/PreferencesModal.tsx b/src/components/PreferencesModal.tsx deleted file mode 100644 index 57e0e657..00000000 --- a/src/components/PreferencesModal.tsx +++ /dev/null @@ -1,1085 +0,0 @@ -import { - Box, - Button, - ButtonGroup, - Checkbox, - Flex, - FormControl, - FormErrorMessage, - FormHelperText, - FormLabel, - IconButton, - Input, - InputGroup, - Kbd, - Link, - Modal, - ModalBody, - ModalCloseButton, - ModalContent, - ModalFooter, - ModalHeader, - ModalOverlay, - Radio, - RadioGroup, - Select, - Slider, - SliderFilledTrack, - SliderThumb, - SliderTrack, - Spinner, - Stack, - Table, - Tbody, - Td, - Text, - Th, - Thead, - Tooltip, - Tr, - VStack, -} from "@chakra-ui/react"; -import { ChangeEvent, RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; - -import { capitalize } from "lodash-es"; -import { FaCheck } from "react-icons/fa"; -import { MdCancel, MdVolumeUp } from "react-icons/md"; -import { useAlert } from "../hooks/use-alert"; -import useAudioPlayer from "../hooks/use-audio-player"; -import { useModels } from "../hooks/use-models"; -import { useSettings } from "../hooks/use-settings"; -import { ChatCraftModel } from "../lib/ChatCraftModel"; -import { ChatCraftProvider, ProviderData } from "../lib/ChatCraftProvider"; -import { textToSpeech } from "../lib/ai"; -import db from "../lib/db"; -import { providerFromUrl, supportedProviders } from "../lib/providers"; -import { CustomProvider } from "../lib/providers/CustomProvider"; -import { FreeModelProvider } from "../lib/providers/DefaultProvider/FreeModelProvider"; -import { OpenAiProvider } from "../lib/providers/OpenAiProvider"; -import { OpenRouterProvider } from "../lib/providers/OpenRouterProvider"; -import { TextToSpeechVoices } from "../lib/settings"; -import { download, isMac } from "../lib/utils"; -import PasswordInput from "./PasswordInput"; - -// https://dexie.org/docs/StorageManager -async function isStoragePersisted() { - if (navigator.storage?.persisted) { - return await navigator.storage.persisted(); - } - - return false; -} - -type PreferencesModalProps = { - isOpen: boolean; - onClose: () => void; - finalFocusRef?: RefObject; -}; - -function PreferencesModal({ isOpen, onClose, finalFocusRef }: PreferencesModalProps) { - const { settings, setSettings } = useSettings(); - const { models } = useModels(); - - // Whether our db is being persisted - const [isPersisted, setIsPersisted] = useState(false); - const { info, error, success, warning } = useAlert(); - const inputRef = useRef(null); - const [isApiKeyInvalid, setIsApiKeyInvalid] = useState(false); - const [selectedProvider, setSelectedProvider] = useState(null); - const [newCustomProvider, setNewCustomProvider] = useState(null); - const [apiKeySaved, setApiKeySaved] = useState(false); - const [isValidating, setIsValidating] = useState(false); - - // Stores the list of providers we are displaying in providers table - const [tableProviders, setTableProviders] = useState({}); - - // Stores the provider that has its api key field currently actively selected - const [focusedProvider, setFocusedProvider] = useState( - settings.currentProvider - ); - - useEffect(() => { - isStoragePersisted() - .then((value) => setIsPersisted(value)) - .catch(console.error); - }, []); - - async function handlePersistClick() { - if (navigator.storage?.persist) { - await navigator.storage.persist(); - const persisted = await isStoragePersisted(); - setIsPersisted(persisted); - } - } - - const handleExportClick = useCallback( - async function () { - // Don't load this unless it's needed (150K) - const { exportDB } = await import("dexie-export-import"); - const blob = await exportDB(db); - download(blob, "chatcraft-db.json", "application/json"); - info({ - title: "Downloaded", - message: "Message was downloaded as a file", - }); - }, - [info] - ); - - const handleFileChange = useCallback( - (event: ChangeEvent) => { - const file = event.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onloadend = () => { - const blob = new Blob([new Uint8Array(reader.result as ArrayBuffer)]); - // Don't load this unless it's needed (150K) - import("dexie-export-import") - .then(({ importDB }) => importDB(blob)) - .then(() => { - info({ - title: "Database Import", - message: "Database imported successfully. You may need to refresh.", - }); - }) - .catch((err) => { - console.warn("Error importing db", err); - error({ - title: "Database Import", - message: "Unable to import database. See Console for more details.", - }); - }); - }; - reader.readAsArrayBuffer(file); - } - }, - [error, info] - ); - - const handleImportClick = useCallback( - function () { - if (inputRef.current) { - inputRef.current.click(); - } - }, - [inputRef] - ); - - const handleVoiceSelection = useCallback( - (voice: TextToSpeechVoices) => { - setSettings({ - ...settings, - textToSpeech: { - ...settings.textToSpeech, - voice, - }, - }); - }, - [setSettings, settings] - ); - - const { clearAudioQueue, addToAudioQueue } = useAudioPlayer(); - - const handlePlayAudioPreview = useCallback(async () => { - try { - const { voice } = settings.textToSpeech; - const previewText = `Hi there, this is ${voice}!`; - - clearAudioQueue(); - addToAudioQueue(textToSpeech(previewText, voice)); - } catch (err: any) { - console.error(err); - error({ title: "Error while generating Audio", message: err.message }); - } - }, [addToAudioQueue, clearAudioQueue, error, settings.textToSpeech]); - - const handleApiKeyChange = async (provider: ChatCraftProvider, apiKey: string) => { - const newProvider = providerFromUrl( - provider.apiUrl, - apiKey, - provider.name, - provider.defaultModel - ); - - setFocusedProvider(newProvider); - - const newProviders = { ...settings.providers }; - - // Update api key in table - tableProviders[newProvider.name] = newProvider; - - // Api key validation - try { - setIsApiKeyInvalid(false); - setIsValidating(true); - - const result = await newProvider.validateApiKey(newProvider.apiKey!); - - setIsApiKeyInvalid(!result); - setIsValidating(false); - - // Valid key, update in settings.providers - if (result) { - // Valid key, update in settings.providers - newProviders[newProvider.name] = newProvider; - } else { - // Invalid key, remove from settings.providers - delete newProviders[newProvider.name]; - } - } catch (err: any) { - setIsApiKeyInvalid(true); - setIsValidating(false); - - // Invalid key, remove from settings.providers - delete newProviders[newProvider.name]; - } - - setSettings({ - ...settings, - ...(newProvider.name === settings.currentProvider.name && { currentProvider: newProvider }), - providers: newProviders, - }); - - if (newProvider.name === selectedProvider?.name) { - setSelectedProvider(newProvider); - } - - setApiKeySaved(true); - }; - - const handleSetCurrentProvider = async () => { - if (!selectedProvider) { - console.error("Error trying to set current provider, missing selected provider"); - setFocusedProvider(null); - return; - } - - if (selectedProvider.name === settings.currentProvider.name) { - setFocusedProvider(null); - warning({ - title: "No change needed", - message: "This is already your current provider", - }); - return; - } - - setFocusedProvider(selectedProvider); - - // Api key validation - try { - setIsApiKeyInvalid(false); - setIsValidating(true); - - const result = await selectedProvider.validateApiKey(selectedProvider.apiKey!); - - setIsApiKeyInvalid(!result); - setIsValidating(false); - - // Valid key - if (result) { - // Set as current provider - setSettings({ ...settings, currentProvider: selectedProvider }); - setApiKeySaved(true); - setSelectedProvider(null); - - success({ - title: "Current provider changed", - message: `${selectedProvider.name} set as current provider`, - }); - - setSettings({ ...settings, currentProvider: selectedProvider }); - setApiKeySaved(true); - - // Sync table - tableProviders[selectedProvider.name] = selectedProvider; - - // Uncheck checkbox - setSelectedProvider(null); - } else { - console.warn("Error validating API key"); - error({ - title: "Provider not set", - message: "Invalid API key", - }); - } - } catch (err: any) { - setIsValidating(false); - - console.warn("Error validating API key", err); - setIsApiKeyInvalid(true); - - error({ - title: "Provider not set", - message: err.message, - }); - } - }; - - // Handle checkbox change - const handleSelectedProviderChange = (provider: ChatCraftProvider) => { - if (selectedProvider?.name === provider.name) { - // Selecting same one means deselect - setSelectedProvider(null); - } else { - setSelectedProvider(provider); - } - }; - - const handleAddProvider = () => { - setIsApiKeyInvalid(false); - setNewCustomProvider(new CustomProvider("", "", "", "")); - }; - - const handleDeleteCustomProvider = () => { - setFocusedProvider(null); - - if (!selectedProvider) { - console.error("Error trying to delete provider, missing selected provider"); - return; - } - - // Do not allow default provider to be deleted - if (selectedProvider.name === settings.currentProvider.name) { - warning({ - title: "Action not allowed", - message: "You may not delete the current provider.", - }); - return; - } - - // Do not allow ChatCraft initial providers to be deleted - if (supportedProviders[selectedProvider.name]) { - warning({ - title: "Action not allowed", - message: "You may not delete default ChatCraft providers.", - }); - return; - } - - const newProviders = { ...settings.providers }; - delete newProviders[selectedProvider.name]; - setSettings({ ...settings, providers: newProviders }); - - // Uncheck checkbox - setSelectedProvider(null); - - // Sync table - setTableProviders({ ...supportedProviders, ...newProviders }); - }; - - const handleSaveNewCustomProvider = async () => { - setFocusedProvider(newCustomProvider); - - if (!newCustomProvider) { - console.error("Error trying to save new custom provider, missing custom provider"); - return; - } - - const trimmedData = new CustomProvider( - newCustomProvider.name?.trim(), - newCustomProvider.apiUrl?.trim(), - "", - newCustomProvider.apiKey?.trim() - ); - - if (!trimmedData.name || !trimmedData.apiUrl || !trimmedData.apiKey) { - setNewCustomProvider(trimmedData); - return; - } - - // Check if provider name already exists - if (tableProviders[trimmedData.name]) { - warning({ - title: "Provider not added", - message: "A provider with this name already exists.", - }); - return; - } - - // Create new ChatCraftProvider object from CustomProviderProvider parsing provider type from url - const newProvider = providerFromUrl(trimmedData.apiUrl, trimmedData.apiKey, trimmedData.name); - - if (newProvider instanceof FreeModelProvider) { - warning({ - title: "Provider not added", - message: "Free AI Models provider already exists", - }); - return; - } - - // Api key validation - try { - setIsApiKeyInvalid(false); - setIsValidating(true); - - const result = await newProvider.validateApiKey(newProvider.apiKey!); - - setIsApiKeyInvalid(!result); - setIsValidating(false); - - if (!result) { - return; - } - - // Query list of models - let models = []; - try { - models = await newProvider.queryModels(newProvider.apiKey!); - } catch (err: any) { - console.warn("Error querying models for custom provider:", err); - setFocusedProvider(null); - error({ - title: "Provider not added", - message: err.message, - }); - return; - } - - if (models.length === 0) { - console.warn("No models available for custom provider"); - setFocusedProvider(null); - error({ - title: "Provider not added", - message: "Provider is not Open AI compatible.", - }); - return; - } - - // Set the first model in list as defaultModel - const newProviderWithModel = providerFromUrl( - newCustomProvider.apiUrl, - newCustomProvider.apiKey, - newCustomProvider.name, - models[0] - ); - - const newProviders = { ...settings.providers }; - newProviders[newProviderWithModel.name] = newProviderWithModel; - - setSettings({ - ...settings, - providers: newProviders, - }); - setApiKeySaved(true); - - // Sync table - setTableProviders({ ...supportedProviders, ...newProviders }); - - success({ - title: `New provider added`, - message: `${newProviderWithModel.name}`, - }); - - // Clear the form and hide the new provider row - setNewCustomProvider(null); - setIsValidating(false); - } catch (err: any) { - console.warn("Error validating API key", err.message); - setFocusedProvider(null); - error({ - title: "Provider not added", - message: err.message, - }); - setIsValidating(false); - return; - } - }; - - const extractDomain = (url: string | URL): string => { - try { - // ensure the input is always treated as a string - const urlString = url instanceof URL ? url.href : url; - const parsedUrl = new URL(urlString); - return parsedUrl.hostname; - } catch (err: any) { - console.error("Error extracting domain from URL:", err); - return typeof url === "string" ? url : "Invalid URL"; - } - }; - - // Clean up actions when modal closes - useEffect(() => { - // Sync table - setTableProviders({ ...supportedProviders, ...settings.providers }); - - if (!isOpen) { - setNewCustomProvider(null); - setIsApiKeyInvalid(false); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen]); - - const isTtsSupported = useMemo(() => { - return !!models.filter((model) => model.id.includes("tts"))?.length; - }, [models]); - - return ( - - - - User Settings - - - {!tableProviders ? ( - Loading providers... - ) : ( - - - - Providers - - - - - - - Advanced option for use with other OpenAI-compatible APIs - - - - - - - - - - - - - - {newCustomProvider && ( - - - - - - - - )} - {[...Object.values(tableProviders)] // copy of the array - .reverse() // reverse the array so new provider is at top - .map((provider) => { - return ( - - - - - - - - ); - })} - -
NameAPI URLAPI KeyIn Use
- } - size={"xs"} - onClick={() => setNewCustomProvider(null)} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - setNewCustomProvider(null); - } - }} - variant="outline" - tabIndex={0} - color={"grey"} - border={"none"} - p={0} - fontSize={16} - borderRadius={"50%"} - _hover={{ - borderColor: "gray.400", - color: "gray.400", - }} - _focus={{ - _focus: { - outline: "none", - boxShadow: "0 0 0 3px rgba(66, 153, 225, 0.6)", - borderColor: "blue.300", - }, - }} - _active={{ - backgroundColor: "none", - }} - /> - - - - { - setNewCustomProvider( - new CustomProvider( - e.target.value, - newCustomProvider.apiUrl, - "", - newCustomProvider.apiKey - ) - ); - }} - /> - - Name is required. - - - - - { - setNewCustomProvider( - new CustomProvider( - newCustomProvider.name, - e.target.value, - "", - newCustomProvider.apiKey - ) - ); - }} - /> - - API URL is required. - - - - { - setNewCustomProvider( - new CustomProvider( - newCustomProvider.name, - newCustomProvider.apiUrl, - "", - e.target.value - ) - ); - }} - /> - API Key is required. - - - - - -
- - handleSelectedProviderChange(provider)} - isChecked={selectedProvider?.name === provider.name} - /> - - {provider.name} - - {extractDomain(provider.apiUrl)} - - - {provider.name === "Free AI Models" ? ( - - - - ) : ( - - handleApiKeyChange(provider, e.target.value)} - onFocus={() => setFocusedProvider(provider)} - isDisabled={provider instanceof FreeModelProvider} - isInvalid={ - !!( - !isValidating && - focusedProvider?.name === provider.name && - isApiKeyInvalid - ) - } - /> - {focusedProvider?.name === provider.name && isValidating ? ( - - - - Validating... - {" "} - - ) : ( - - {provider.apiKey - ? "Unable to verify key." - : "API Key is required."} - - )} - {provider instanceof OpenRouterProvider && - provider.name === focusedProvider?.name && - !provider.apiKey && ( - - )} - - )} - - - {settings.currentProvider.name === provider.name && ( - - )} - -
- {apiKeySaved && ( - - Your API Key(s) are stored in browser storage - - )} -
- - - Offline database is {isPersisted ? "persisted" : "not persisted"} - - - - - - - - - Persisted databases use the{" "} - - Storage API - {" "} - and are retained by the browser as long as possible. See{" "} - - docs - {" "} - for database export details. - - - - - GPT Model - - - See{" "} - - docs - {" "} - and{" "} - - pricing - - . NOTE: not all accounts have access to GPT - 4 - - - - - Temperature: {settings.temperature} - - setSettings({ ...settings, temperature: value })} - min={0} - max={2} - step={0.05} - > - - - - - - - Temperature must be a number between 0 and 2. - - The temperature increases the randomness of the response (0.0 - 2.0). - - - - {isTtsSupported && ( - - Select Voice - - - - - } - onClick={handlePlayAudioPreview} - /> - - - - - Used when announcing messages in real-time or with the ‘Speak’ - option - - - )} - - - Image Compression - - - - - Maximum file size after compression: {settings.maxCompressedFileSizeMB} (MB) - - - setSettings({ ...settings, maxCompressedFileSizeMB: value }) - } - min={1} - max={20} - step={1} - > - - - - - - - Maximum file size must be between 1 and 20 MB. - - - After compression, each attached image will be under your chosen maximum - file size (1-20 MB). - - - - - - - Maximum image dimension: {settings.maxImageDimension} (px) - - setSettings({ ...settings, maxImageDimension: value })} - min={16} - max={2048} - step={16} - > - - - - - - - Maximum Image dimension must be between 16 and 2048 - - - Your compressed image's maximum width or height will be within the - dimension you choose (16-2048 pixels). - - - - - - Compression factor: {settings.compressionFactor} - setSettings({ ...settings, compressionFactor: value })} - min={0.1} - max={1} - step={0.1} - > - - - - - - - Compression factor must be between 0.1 and 1.0 - - - Set the maximum file size based on the original size multiplied by the - factor you choose (0.1-1.0). - - - - - - - - - When writing a prompt, press Enter to... - - - setSettings({ ...settings, enterBehaviour: nextValue as EnterBehaviour }) - } - > - - Send the message - - Start a new line (use {isMac() ? Command ⌘ : Ctrl} + - Enter to send) - - - - - - - setSettings({ ...settings, countTokens: e.target.checked })} - > - Track and Display Token Count and Cost - - -
- )} -
- - -
-
- ); -} - -export default PreferencesModal;