import React from "react"; import { useTranslation } from "react-i18next"; import { AxiosError } from "axios"; import { ModelSelector } from "#/components/shared/modals/settings/model-selector"; import { organizeModelsAndProviders } from "#/utils/organize-models-and-providers"; import { useAIConfigOptions } from "#/hooks/query/use-ai-config-options"; import { useSettings } from "#/hooks/query/use-settings"; import { hasAdvancedSettingsSet } from "#/utils/has-advanced-settings-set"; import { useSaveSettings } from "#/hooks/mutation/use-save-settings"; import { SettingsSwitch } from "#/components/features/settings/settings-switch"; import { I18nKey } from "#/i18n/declaration"; import { SettingsInput } from "#/components/features/settings/settings-input"; import { HelpLink } from "#/components/features/settings/help-link"; import { BrandButton } from "#/components/features/settings/brand-button"; import { displayErrorToast, displaySuccessToast, } from "#/utils/custom-toast-handlers"; import { retrieveAxiosErrorMessage } from "#/utils/retrieve-axios-error-message"; import { SettingsDropdownInput } from "#/components/features/settings/settings-dropdown-input"; import { useConfig } from "#/hooks/query/use-config"; import { isCustomModel } from "#/utils/is-custom-model"; import { LlmSettingsInputsSkeleton } from "#/components/features/settings/llm-settings/llm-settings-inputs-skeleton"; import { KeyStatusIcon } from "#/components/features/settings/key-status-icon"; import { DEFAULT_SETTINGS } from "#/services/settings"; function LlmSettingsScreen() { const { t } = useTranslation(); const { mutate: saveSettings, isPending } = useSaveSettings(); const { data: resources } = useAIConfigOptions(); const { data: settings, isLoading, isFetching } = useSettings(); const { data: config } = useConfig(); const [view, setView] = React.useState<"basic" | "advanced">("basic"); const [securityAnalyzerInputIsVisible, setSecurityAnalyzerInputIsVisible] = React.useState(false); const [dirtyInputs, setDirtyInputs] = React.useState({ model: false, apiKey: false, searchApiKey: false, baseUrl: false, agent: false, confirmationMode: false, enableDefaultCondenser: false, securityAnalyzer: false, }); const modelsAndProviders = organizeModelsAndProviders( resources?.models || [], ); React.useEffect(() => { const determineWhetherToToggleAdvancedSettings = () => { if (resources && settings) { return ( isCustomModel(resources.models, settings.LLM_MODEL) || hasAdvancedSettingsSet({ ...settings, }) ); } return false; }; const userSettingsIsAdvanced = determineWhetherToToggleAdvancedSettings(); if (settings) setSecurityAnalyzerInputIsVisible(settings.CONFIRMATION_MODE); if (userSettingsIsAdvanced) setView("advanced"); else setView("basic"); }, [settings, resources]); const handleSuccessfulMutation = () => { displaySuccessToast(t(I18nKey.SETTINGS$SAVED)); setDirtyInputs({ model: false, apiKey: false, searchApiKey: false, baseUrl: false, agent: false, confirmationMode: false, enableDefaultCondenser: false, securityAnalyzer: false, }); }; const handleErrorMutation = (error: AxiosError) => { const errorMessage = retrieveAxiosErrorMessage(error); displayErrorToast(errorMessage || t(I18nKey.ERROR$GENERIC)); }; const basicFormAction = (formData: FormData) => { const provider = formData.get("llm-provider-input")?.toString(); const model = formData.get("llm-model-input")?.toString(); const apiKey = formData.get("llm-api-key-input")?.toString(); const searchApiKey = formData.get("search-api-key-input")?.toString(); const fullLlmModel = provider && model && `${provider}/${model}`.toLowerCase(); saveSettings( { LLM_MODEL: fullLlmModel, llm_api_key: apiKey || null, SEARCH_API_KEY: searchApiKey || "", // reset advanced settings LLM_BASE_URL: DEFAULT_SETTINGS.LLM_BASE_URL, AGENT: DEFAULT_SETTINGS.AGENT, CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE, SECURITY_ANALYZER: DEFAULT_SETTINGS.SECURITY_ANALYZER, ENABLE_DEFAULT_CONDENSER: DEFAULT_SETTINGS.ENABLE_DEFAULT_CONDENSER, }, { onSuccess: handleSuccessfulMutation, onError: handleErrorMutation, }, ); }; const advancedFormAction = (formData: FormData) => { const model = formData.get("llm-custom-model-input")?.toString(); const baseUrl = formData.get("base-url-input")?.toString(); const apiKey = formData.get("llm-api-key-input")?.toString(); const searchApiKey = formData.get("search-api-key-input")?.toString(); const agent = formData.get("agent-input")?.toString(); const confirmationMode = formData.get("enable-confirmation-mode-switch")?.toString() === "on"; const enableDefaultCondenser = formData.get("enable-memory-condenser-switch")?.toString() === "on"; const securityAnalyzer = formData .get("security-analyzer-input") ?.toString(); saveSettings( { LLM_MODEL: model, LLM_BASE_URL: baseUrl, llm_api_key: apiKey || null, SEARCH_API_KEY: searchApiKey || "", AGENT: agent, CONFIRMATION_MODE: confirmationMode, ENABLE_DEFAULT_CONDENSER: enableDefaultCondenser, SECURITY_ANALYZER: confirmationMode ? securityAnalyzer : undefined, }, { onSuccess: handleSuccessfulMutation, onError: handleErrorMutation, }, ); }; const formAction = (formData: FormData) => { if (view === "basic") basicFormAction(formData); else advancedFormAction(formData); }; const handleToggleAdvancedSettings = (isToggled: boolean) => { setSecurityAnalyzerInputIsVisible(!!settings?.CONFIRMATION_MODE); setView(isToggled ? "advanced" : "basic"); setDirtyInputs({ model: false, apiKey: false, searchApiKey: false, baseUrl: false, agent: false, confirmationMode: false, enableDefaultCondenser: false, securityAnalyzer: false, }); }; const handleModelIsDirty = (model: string | null) => { // openai providers are special case; see ModelSelector // component for details const modelIsDirty = model !== settings?.LLM_MODEL.replace("openai/", ""); setDirtyInputs((prev) => ({ ...prev, model: modelIsDirty, })); }; const handleApiKeyIsDirty = (apiKey: string) => { const apiKeyIsDirty = apiKey !== ""; setDirtyInputs((prev) => ({ ...prev, apiKey: apiKeyIsDirty, })); }; const handleSearchApiKeyIsDirty = (searchApiKey: string) => { const searchApiKeyIsDirty = searchApiKey !== settings?.SEARCH_API_KEY; setDirtyInputs((prev) => ({ ...prev, searchApiKey: searchApiKeyIsDirty, })); }; const handleCustomModelIsDirty = (model: string) => { const modelIsDirty = model !== settings?.LLM_MODEL && model !== ""; setDirtyInputs((prev) => ({ ...prev, model: modelIsDirty, })); }; const handleBaseUrlIsDirty = (baseUrl: string) => { const baseUrlIsDirty = baseUrl !== settings?.LLM_BASE_URL; setDirtyInputs((prev) => ({ ...prev, baseUrl: baseUrlIsDirty, })); }; const handleAgentIsDirty = (agent: string) => { const agentIsDirty = agent !== settings?.AGENT && agent !== ""; setDirtyInputs((prev) => ({ ...prev, agent: agentIsDirty, })); }; const handleConfirmationModeIsDirty = (isToggled: boolean) => { setSecurityAnalyzerInputIsVisible(isToggled); const confirmationModeIsDirty = isToggled !== settings?.CONFIRMATION_MODE; setDirtyInputs((prev) => ({ ...prev, confirmationMode: confirmationModeIsDirty, })); }; const handleEnableDefaultCondenserIsDirty = (isToggled: boolean) => { const enableDefaultCondenserIsDirty = isToggled !== settings?.ENABLE_DEFAULT_CONDENSER; setDirtyInputs((prev) => ({ ...prev, enableDefaultCondenser: enableDefaultCondenserIsDirty, })); }; const handleSecurityAnalyzerIsDirty = (securityAnalyzer: string) => { const securityAnalyzerIsDirty = securityAnalyzer !== settings?.SECURITY_ANALYZER; setDirtyInputs((prev) => ({ ...prev, securityAnalyzer: securityAnalyzerIsDirty, })); }; const formIsDirty = Object.values(dirtyInputs).some((isDirty) => isDirty); if (!settings || isFetching) return ; return (
{t(I18nKey.SETTINGS$ADVANCED)} {view === "basic" && (
{!isLoading && !isFetching && ( )} " : ""} onChange={handleApiKeyIsDirty} startContent={ settings.LLM_API_KEY_SET && ( ) } /> ) } />
)} {view === "advanced" && (
" : ""} onChange={handleApiKeyIsDirty} startContent={ settings.LLM_API_KEY_SET && ( ) } /> ) } /> ({ key: agent, label: agent, })) || [] } defaultSelectedKey={settings.AGENT} isClearable={false} onInputChange={handleAgentIsDirty} wrapperClassName="w-full max-w-[680px]" /> {config?.APP_MODE === "saas" && ( {t(I18nKey.SETTINGS$RUNTIME_SETTINGS)} {t(I18nKey.SETTINGS$GET_IN_TOUCH)} } items={[]} isDisabled wrapperClassName="w-full max-w-[680px]" /> )} {t(I18nKey.SETTINGS$ENABLE_MEMORY_CONDENSATION)} {t(I18nKey.SETTINGS$CONFIRMATION_MODE)} {securityAnalyzerInputIsVisible && ( ({ key: analyzer, label: analyzer, })) || [] } placeholder={t( I18nKey.SETTINGS$SECURITY_ANALYZER_PLACEHOLDER, )} defaultSelectedKey={settings.SECURITY_ANALYZER} isClearable showOptionalTag onInputChange={handleSecurityAnalyzerIsDirty} wrapperClassName="w-full max-w-[680px]" /> )}
)}
{!isPending && t("SETTINGS$SAVE_CHANGES")} {isPending && t("SETTINGS$SAVING")}
); } export default LlmSettingsScreen;