diff --git a/crates/goose-server/src/routes/config_management.rs b/crates/goose-server/src/routes/config_management.rs index c1b1de5d2777..639875d54476 100644 --- a/crates/goose-server/src/routes/config_management.rs +++ b/crates/goose-server/src/routes/config_management.rs @@ -88,6 +88,19 @@ pub struct UpdateCustomProviderRequest { pub supports_streaming: Option, } +#[derive(Serialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct MaskedSecret { + pub masked_value: String, +} + +#[derive(Serialize, ToSchema)] +#[serde(untagged)] +pub enum ConfigValueResponse { + Value(Value), + MaskedValue(MaskedSecret), +} + #[utoipa::path( post, path = "/config/upsert", @@ -134,6 +147,22 @@ pub async fn remove_config(Json(query): Json) -> Result String { + let as_string = match secret { + Value::String(s) => s, + _ => serde_json::to_string(&secret).unwrap_or_else(|_| secret.to_string()), + }; + + let chars: Vec<_> = as_string.chars().collect(); + let show_len = std::cmp::min(chars.len() / 2, SECRET_MASK_SHOW_LEN); + let visible: String = chars.iter().take(show_len).collect(); + let mask = "*".repeat(chars.len() - show_len); + + format!("{}{}", visible, mask) +} + #[utoipa::path( post, path = "/config/read", @@ -143,12 +172,14 @@ pub async fn remove_config(Json(query): Json) -> Result) -> Result, StatusCode> { +pub async fn read_config( + Json(query): Json, +) -> Result, StatusCode> { if query.key == "model-limits" { let limits = ModelConfig::get_all_model_limits(); - return Ok(Json( + return Ok(Json(ConfigValueResponse::Value( serde_json::to_value(limits).map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?, - )); + ))); } let config = Config::global(); @@ -156,18 +187,14 @@ pub async fn read_config(Json(query): Json) -> Result { if query.is_secret { - Value::Bool(true) - } else { - value - } - } - Err(ConfigError::NotFound(_)) => { - if query.is_secret { - Value::Bool(false) + ConfigValueResponse::MaskedValue(MaskedSecret { + masked_value: mask_secret(value), + }) } else { - Value::Null + ConfigValueResponse::Value(value) } } + Err(ConfigError::NotFound(_)) => ConfigValueResponse::Value(Value::Null), Err(_) => { return Err(StatusCode::INTERNAL_SERVER_ERROR); } @@ -752,10 +779,12 @@ mod tests { .await; assert!(result.is_ok()); - let response = result.unwrap(); + let response = match result.unwrap().0 { + ConfigValueResponse::Value(value) => value, + ConfigValueResponse::MaskedValue(_) => panic!("unexpected secret"), + }; - let limits: Vec = - serde_json::from_value(response.0).unwrap(); + let limits: Vec = serde_json::from_value(response).unwrap(); assert!(!limits.is_empty()); let gpt4_limit = limits.iter().find(|l| l.pattern == "gpt-4o"); diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx index b4bb70fcf23e..f970e9335678 100644 --- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx @@ -1,7 +1,6 @@ import React, { memo, useMemo, useCallback, useState } from 'react'; import { ProviderCard } from './subcomponents/ProviderCard'; import CardContainer from './subcomponents/CardContainer'; -import { ProviderModalProvider, useProviderModal } from './modal/ProviderModalProvider'; import ProviderConfigurationModal from './modal/ProviderConfiguationModal'; import { DeclarativeProviderConfig, @@ -47,7 +46,7 @@ const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onCli ); }); -const ProviderCards = memo(function ProviderCards({ +function ProviderCards({ providers, isOnboarding, refreshProviders, @@ -58,7 +57,7 @@ const ProviderCards = memo(function ProviderCards({ refreshProviders?: () => void; onProviderLaunch: (provider: ProviderDetails) => void; }) { - const { openModal } = useProviderModal(); + const [configuringProvider, setConfiguringProvider] = useState(null); const [showCustomProviderModal, setShowCustomProviderModal] = useState(false); const [editingProvider, setEditingProvider] = useState<{ id: string; @@ -66,6 +65,11 @@ const ProviderCards = memo(function ProviderCards({ isEditable: boolean; } | null>(null); + const openModal = useCallback( + (provider: ProviderDetails) => setConfiguringProvider(provider), + [] + ); + const configureProviderViaModal = useCallback( async (provider: ProviderDetails) => { if (provider.provider_type === 'Custom' || provider.provider_type === 'Declarative') { @@ -81,22 +85,10 @@ const ProviderCards = memo(function ProviderCards({ setShowCustomProviderModal(true); } } else { - openModal(provider, { - onSubmit: () => { - if (refreshProviders) { - refreshProviders(); - } - }, - onDelete: (_values: unknown) => { - if (refreshProviders) { - refreshProviders(); - } - }, - formProps: {}, - }); + openModal(provider); } }, - [openModal, refreshProviders] + [openModal] ); const handleUpdateCustomProvider = useCallback( @@ -123,20 +115,12 @@ const ProviderCards = memo(function ProviderCards({ setEditingProvider(null); }, []); - const deleteProviderConfigViaModal = useCallback( - (provider: ProviderDetails) => { - openModal(provider, { - onDelete: (_values: unknown) => { - // Only refresh if the function is provided - if (refreshProviders) { - refreshProviders(); - } - }, - formProps: {}, - }); - }, - [openModal, refreshProviders] - ); + const onCloseProviderConfig = useCallback(() => { + setConfiguringProvider(null); + if (refreshProviders) { + refreshProviders(); + } + }, [refreshProviders]); const handleCreateCustomProvider = useCallback( async (data: UpdateCustomProviderRequest) => { @@ -160,7 +144,6 @@ const ProviderCards = memo(function ProviderCards({ key={provider.name} provider={provider} onConfigure={() => configureProviderViaModal(provider)} - onDelete={() => deleteProviderConfigViaModal(provider)} onLaunch={() => onProviderLaunch(provider)} isOnboarding={isOnboarding} /> @@ -171,13 +154,7 @@ const ProviderCards = memo(function ProviderCards({ ); return cards; - }, [ - providers, - isOnboarding, - configureProviderViaModal, - deleteProviderConfigViaModal, - onProviderLaunch, - ]); + }, [providers, isOnboarding, configureProviderViaModal, onProviderLaunch]); const initialData = editingProvider && { engine: editingProvider.config.engine.toLowerCase() + '_compatible', @@ -206,11 +183,17 @@ const ProviderCards = memo(function ProviderCards({ /> {' '} + {configuringProvider && ( + + )} ); -}); +} -export default memo(function ProviderGrid({ +export default function ProviderGrid({ providers, isOnboarding, refreshProviders, @@ -221,20 +204,14 @@ export default memo(function ProviderGrid({ refreshProviders?: () => void; onProviderLaunch?: (provider: ProviderDetails) => void; }) { - // Memoize the modal provider and its children to avoid recreating on every render - const modalProviderContent = useMemo( - () => ( - - {})} - /> - - - ), - [providers, isOnboarding, refreshProviders, onProviderLaunch] + return ( + + {})} + /> + ); - return {modalProviderContent}; -}); +} diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx index b8971a69b6de..e04b5ec49a13 100644 --- a/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings/providers/modal/ProviderConfiguationModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useState } from 'react'; import { Dialog, DialogContent, @@ -7,198 +7,123 @@ import { DialogHeader, DialogTitle, } from '../../../ui/dialog'; -import DefaultProviderSetupForm from './subcomponents/forms/DefaultProviderSetupForm'; +import DefaultProviderSetupForm, { + ConfigInput, +} from './subcomponents/forms/DefaultProviderSetupForm'; import ProviderSetupActions from './subcomponents/ProviderSetupActions'; import ProviderLogo from './subcomponents/ProviderLogo'; -import { useProviderModal } from './ProviderModalProvider'; import { SecureStorageNotice } from './subcomponents/SecureStorageNotice'; -import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; -import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler'; -import OllamaForm from './subcomponents/forms/OllamaForm'; +import { providerConfigSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; import { useConfig } from '../../../ConfigContext'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; import { AlertTriangle } from 'lucide-react'; -import { ConfigKey, removeCustomProvider } from '../../../../api'; +import { ProviderDetails, removeCustomProvider } from '../../../../api'; -interface FormValues { - [key: string]: string | number | boolean | null; +interface ProviderConfigurationModalProps { + provider: ProviderDetails; + onClose: () => void; } -const customSubmitHandlerMap: Record = { - provider_name: OllamaSubmitHandler, // example -}; - -const customFormsMap: Record = { - provider_name: OllamaForm, // example -}; - -export default function ProviderConfigurationModal() { +export default function ProviderConfigurationModal({ + provider, + onClose, +}: ProviderConfigurationModalProps) { const [validationErrors, setValidationErrors] = useState>({}); const { upsert, remove } = useConfig(); const { getCurrentModelAndProvider } = useModelAndProvider(); - const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal(); - const [configValues, setConfigValues] = useState>({}); + const [configValues, setConfigValues] = useState>({}); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); - const [isActiveProvider, setIsActiveProvider] = useState(false); // New state for tracking active provider - const [requiredParameters, setRequiredParameters] = useState([]); // New state for tracking active provider + const [isActiveProvider, setIsActiveProvider] = useState(false); - useEffect(() => { - if (isOpen && currentProvider) { - // Reset form state when the modal opens with a new provider - const requiredParameters = currentProvider.metadata.config_keys.filter( - (param) => param.required === true - ); - setRequiredParameters(requiredParameters); - setConfigValues({}); - setValidationErrors({}); - setShowDeleteConfirmation(false); - setIsActiveProvider(false); // Reset active provider state - } - }, [isOpen, currentProvider]); - - if (!isOpen || !currentProvider) return null; + const requiredParameters = provider.metadata.config_keys.filter( + (param) => param.required === true + ); - const isConfigured = currentProvider.is_configured; + const isConfigured = provider.is_configured; const headerText = showDeleteConfirmation - ? `Delete configuration for ${currentProvider.metadata.display_name}` - : `Configure ${currentProvider.metadata.display_name}`; + ? `Delete configuration for ${provider.metadata.display_name}` + : `Configure ${provider.metadata.display_name}`; - // Modify description text to show warning if it's the active provider const descriptionText = showDeleteConfirmation ? isActiveProvider ? `You cannot delete this provider while it's currently in use. Please switch to a different model first.` : 'This will permanently delete the current provider configuration.' : `Add your API key(s) for this provider to integrate into Goose`; - const SubmitHandler = - (customSubmitHandlerMap[currentProvider.name] as typeof DefaultSubmitHandler) || - DefaultSubmitHandler; - const FormComponent = - (customFormsMap[currentProvider.name] as typeof DefaultProviderSetupForm) || - DefaultProviderSetupForm; - const handleSubmitForm = async (e: React.FormEvent) => { e.preventDefault(); - console.log('Form submitted for:', currentProvider.name); - // Reset previous validation errors setValidationErrors({}); - // Validation logic - const parameters = currentProvider.metadata.config_keys || []; + const parameters = provider.metadata.config_keys || []; const errors: Record = {}; - // Check required fields parameters.forEach((parameter) => { if ( parameter.required && - (configValues[parameter.name] === undefined || - configValues[parameter.name] === null || - configValues[parameter.name] === '') + !configValues[parameter.name]?.value && + !configValues[parameter.name]?.serverValue ) { errors[parameter.name] = `${parameter.name} is required`; } }); - // If there are validation errors, stop the submission if (Object.keys(errors).length > 0) { setValidationErrors(errors); - return; // Stop the submission process + return; } - try { - // Wait for the submission to complete - await SubmitHandler(upsert, currentProvider, configValues); - - // Close the modal before triggering refreshes to avoid UI issues - closeModal(); + const toSubmit = Object.fromEntries( + Object.entries(configValues) + .filter(([_k, entry]) => !!entry.value) + .map(([k, entry]) => [k, entry.value || '']) + ); - // Call onSubmit callback if provided (from modal props) - if (modalProps.onSubmit) { - modalProps.onSubmit(configValues as FormValues); - } - } catch (error) { - console.error('Failed to save configuration:', error); - // Keep modal open if there's an error - } + await providerConfigSubmitHandler(upsert, provider, toSubmit); + onClose(); }; const handleCancel = () => { - // Reset delete confirmation state - setShowDeleteConfirmation(false); - setIsActiveProvider(false); - - // Use custom cancel handler if provided - if (modalProps.onCancel) { - modalProps.onCancel(); - } - - closeModal(); + onClose(); }; const handleDelete = async () => { - // Check if this is the currently active provider try { const providerModel = await getCurrentModelAndProvider(); - if (currentProvider.name === providerModel.provider) { - // It's the active provider - set state and show warning + if (provider.name === providerModel.provider) { setIsActiveProvider(true); setShowDeleteConfirmation(true); - return; // Exit early - don't allow actual deletion + return; } } catch (error) { console.error('Failed to check current provider:', error); } - // If we get here, it's not the active provider setIsActiveProvider(false); setShowDeleteConfirmation(true); }; const handleConfirmDelete = async () => { - // Don't proceed if this is the active provider if (isActiveProvider) { return; } - try { - const isCustomProvider = currentProvider.provider_type === 'Custom'; - - if (isCustomProvider) { - await removeCustomProvider({ - path: { id: currentProvider.name }, - }); - } else { - // Remove the provider configuration - // get the keys - const params = currentProvider.metadata.config_keys; - - // go through the keys are remove them - for (const param of params) { - await remove(param.name, param.secret); - } - } + const isCustomProvider = provider.provider_type === 'Custom'; - // Call onDelete callback if provided - // This should trigger the refreshProviders function - if (modalProps.onDelete) { - modalProps.onDelete(currentProvider.name as unknown as FormValues); + if (isCustomProvider) { + await removeCustomProvider({ + path: { id: provider.name }, + }); + } else { + const params = provider.metadata.config_keys; + for (const param of params) { + await remove(param.name, param.secret); } - - // Reset the delete confirmation state before closing - setShowDeleteConfirmation(false); - setIsActiveProvider(false); - - // Close the modal - // Close the modal after deletion and callback - closeModal(); - } catch (error) { - console.error('Failed to delete provider:', error); - // Keep modal open if there's an error } + + onClose(); }; - // Function to determine which icon to display const getModalIcon = () => { if (showDeleteConfirmation) { return ( @@ -208,11 +133,11 @@ export default function ProviderConfigurationModal() { /> ); } - return ; + return ; }; return ( - !open && closeModal()}> + !open && onClose()}> @@ -228,17 +153,16 @@ export default function ProviderConfigurationModal() { {!showDeleteConfirmation ? ( <> {/* Contains information used to set up each provider */} - {requiredParameters.length > 0 && - currentProvider.metadata.config_keys && - currentProvider.metadata.config_keys.length > 0 && } + provider.metadata.config_keys && + provider.metadata.config_keys.length > 0 && } ) : null} @@ -252,11 +176,11 @@ export default function ProviderConfigurationModal() { showDeleteConfirmation={showDeleteConfirmation} onConfirmDelete={handleConfirmDelete} onCancelDelete={() => { - setShowDeleteConfirmation(false); setIsActiveProvider(false); + setShowDeleteConfirmation(false); }} canDelete={isConfigured && !isActiveProvider} - providerName={currentProvider.metadata.display_name} + providerName={provider.metadata.display_name} isActiveProvider={isActiveProvider} /> diff --git a/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx b/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx deleted file mode 100644 index 5c1a48a9b29b..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/ProviderModalProvider.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { createContext, useContext, useState } from 'react'; -import { ProviderDetails } from '../../../../api/types.gen'; - -interface FormValues { - [key: string]: string | number | boolean | null; -} - -interface ModalProps { - onSubmit?: (values: FormValues) => void; - onCancel?: () => void; - onDelete?: (values: FormValues) => void; - formProps?: { - initialValues?: FormValues; - validationSchema?: object; - [key: string]: unknown; - }; -} - -interface ProviderModalContextType { - isOpen: boolean; - currentProvider: ProviderDetails | null; - modalProps: ModalProps; - openModal: (provider: ProviderDetails, additionalProps?: ModalProps) => void; - closeModal: () => void; -} - -const ProviderModalContext = createContext(undefined); - -export const ProviderModalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [isOpen, setIsOpen] = useState(false); - const [currentProvider, setCurrentProvider] = useState(null); - const [modalProps, setModalProps] = useState({}); - - const openModal = (provider: ProviderDetails, additionalProps: ModalProps = {}) => { - setCurrentProvider(provider); - setModalProps(additionalProps); - setIsOpen(true); - }; - - const closeModal = () => { - setIsOpen(false); - }; - - return ( - - {children} - - ); -}; - -export const useProviderModal = () => { - const context = useContext(ProviderModalContext); - if (context === undefined) { - throw new Error('useProviderModal must be used within a ProviderModalProvider'); - } - return context; -}; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx index 6f8e2dd69476..3d15a979838c 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx @@ -1,17 +1,40 @@ import React, { useEffect, useMemo, useState, useCallback } from 'react'; import { Input } from '../../../../../ui/input'; -import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed +import { useConfig } from '../../../../../ConfigContext'; import { ProviderDetails, ConfigKey } from '../../../../../../api'; +import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible'; type ValidationErrors = Record; +type ConfigValue = string | { maskedValue: string }; +export interface ConfigInput { + serverValue?: ConfigValue; + value?: string; +} + interface DefaultProviderSetupFormProps { - configValues: Record; - setConfigValues: React.Dispatch>>; + configValues: Record; + setConfigValues: React.Dispatch>>; provider: ProviderDetails; validationErrors: ValidationErrors; } +const envToPrettyName = (envVar: string) => { + const wordReplacements: { [w: string]: string } = { + Api: 'API', + Aws: 'AWS', + Gcp: 'GCP', + }; + + return envVar + .toLowerCase() + .split('_') + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) + .map((word) => wordReplacements[word] || word) + .join(' ') + .trim(); +}; + export default function DefaultProviderSetupForm({ configValues, setConfigValues, @@ -23,73 +46,49 @@ export default function DefaultProviderSetupForm({ [provider.metadata.config_keys] ); const [isLoading, setIsLoading] = useState(true); + const [optionalExpanded, setOptionalExpanded] = useState(false); const { read } = useConfig(); - console.log('configValues default form', configValues); - - // Initialize values when the component mounts or provider changes const loadConfigValues = useCallback(async () => { setIsLoading(true); - const newValues = { ...configValues }; + try { + const values: { [k: string]: ConfigInput } = {}; - // Try to load actual values from config for each parameter that is not secret - for (const parameter of parameters) { - try { - // Check if there's a stored value in the config system + for (const parameter of parameters) { const configKey = `${parameter.name}`; - const configResponse = await read(configKey, parameter.secret || false); - - if (configResponse) { - newValues[parameter.name] = parameter.secret ? 'true' : String(configResponse); - } else if ( - parameter.default !== undefined && - parameter.default !== null && - !configValues[parameter.name] - ) { - // Fall back to default value if no config value exists - newValues[parameter.name] = String(parameter.default); - } - } catch (error) { - console.error(`Failed to load config for ${parameter.name}:`, error); - // Fall back to default if read operation fails - if ( - parameter.default !== undefined && - parameter.default !== null && - !configValues[parameter.name] - ) { - newValues[parameter.name] = String(parameter.default); + const configValue = (await read(configKey, parameter.secret || false)) as ConfigValue; + + if (configValue) { + values[parameter.name] = { serverValue: configValue }; + } else if (parameter.default !== undefined && parameter.default !== null) { + values[parameter.name] = { value: parameter.default }; } } - } - // Update state with loaded values - setConfigValues((prev) => ({ - ...prev, - ...newValues, - })); - setIsLoading(false); - }, [configValues, parameters, read, setConfigValues]); + setConfigValues((prev) => ({ + ...prev, + ...values, + })); + } finally { + setIsLoading(false); + } + }, [parameters, read, setConfigValues]); useEffect(() => { loadConfigValues(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // Filter parameters to only show required ones - const requiredParameters = useMemo(() => { - return parameters.filter((param) => param.required === true); - }, [parameters]); - - // TODO: show all params, not just required ones - // const allParameters = useMemo(() => { - // return parameters; - // }, [parameters]); - - // Helper function to generate appropriate placeholder text const getPlaceholder = (parameter: ConfigKey): string => { - // If default is defined and not null, show it + if (parameter.secret) { + const serverValue = configValues[parameter.name]?.serverValue; + if (typeof serverValue === 'object' && 'maskedValue' in serverValue) { + return serverValue.maskedValue; + } + } + if (parameter.default !== undefined && parameter.default !== null) { - return `Default: ${parameter.default}`; + return parameter.default; } const name = parameter.name.toLowerCase(); @@ -99,66 +98,112 @@ export default function DefaultProviderSetupForm({ return parameter.name .replace(/_/g, ' ') - .replace(/([A-Z])/g, ' $1') .replace(/^./, (str) => str.toUpperCase()) .trim(); }; - // helper for custom labels - const getFieldLabel = (parameter: ConfigKey): string => { + const getFieldLabel = (parameter: ConfigKey) => { const name = parameter.name.toLowerCase(); if (name.includes('api_key')) return 'API Key'; if (name.includes('api_url') || name.includes('host')) return 'API Host'; if (name.includes('models')) return 'Models'; - return parameter.name - .replace(/_/g, ' ') - .replace(/([A-Z])/g, ' $1') - .replace(/^./, (str) => str.toUpperCase()) - .trim(); + let parameter_name = parameter.name.toUpperCase(); + if (parameter_name.startsWith(provider.name.toUpperCase().replace('-', '_'))) { + parameter_name = parameter_name.slice(provider.name.length + 1); + } + let pretty = envToPrettyName(parameter_name); + return ( + + {pretty} + ({parameter.name}) + + ); }; if (isLoading) { return
Loading configuration values...
; } - console.log('required params', requiredParameters); + function getRenderValue(parameter: ConfigKey): string | undefined { + if (parameter.secret) { + return undefined; + } + + const entry = configValues[parameter.name]; + return entry?.value || (entry?.serverValue as string) || ''; + } + + const renderParametersList = (parameters: ConfigKey[]) => { + return parameters.map((parameter) => ( +
+ + ) => { + setConfigValues((prev) => { + const newValue = { ...(prev[parameter.name] || {}), value: e.target.value }; + return { + ...prev, + [parameter.name]: newValue, + }; + }); + }} + placeholder={getPlaceholder(parameter)} + className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${ + validationErrors[parameter.name] + ? 'border-2 border-red-500' + : 'border border-borderSubtle hover:border-borderStandard' + } bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`} + required={parameter.required} + /> + {validationErrors[parameter.name] && ( +

{validationErrors[parameter.name]}

+ )} +
+ )); + }; + + let aboveFoldParameters = parameters.filter((p) => p.required); + let belowFoldParameters = parameters.filter((p) => !p.required); + if (aboveFoldParameters.length === 0) { + aboveFoldParameters = belowFoldParameters; + belowFoldParameters = []; + } + + const expandCtaText = `${optionalExpanded ? 'Hide' : 'Show'} ${belowFoldParameters.length} options `; + return (
- {requiredParameters.length === 0 ? ( + {aboveFoldParameters.length === 0 && belowFoldParameters.length === 0 ? (
- No required configuration for this provider. + No configuration parameters for this provider.
) : ( - requiredParameters.map((parameter) => ( -
- - ) => { - console.log(`Setting ${parameter.name} to:`, e.target.value); - setConfigValues((prev) => ({ - ...prev, - [parameter.name]: e.target.value, - })); - }} - placeholder={getPlaceholder(parameter)} - className={`w-full h-14 px-4 font-regular rounded-lg shadow-none ${ - validationErrors[parameter.name] - ? 'border-2 border-red-500' - : 'border border-borderSubtle hover:border-borderStandard' - } bg-background-default text-lg placeholder:text-textSubtle font-regular text-textStandard`} - required={parameter.required} - /> - {validationErrors[parameter.name] && ( -

{validationErrors[parameter.name]}

- )} -
- )) +
+
{renderParametersList(aboveFoldParameters)}
+ {belowFoldParameters.length > 0 && ( + + +
+ {expandCtaText} + {optionalExpanded ? '↑' : '↓'} +
+
+ + {renderParametersList(belowFoldParameters)} + +
+ )} +
)}
); diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx deleted file mode 100644 index fba9a68fa6f3..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/forms/OllamaForm.tsx +++ /dev/null @@ -1,149 +0,0 @@ -import { PROVIDER_REGISTRY } from '../../../ProviderRegistry'; -import { Input } from '../../../../../ui/input'; - -import React, { useState, useEffect, useCallback } from 'react'; -import { RefreshCw } from 'lucide-react'; -import CustomRadio from '../../../../../ui/CustomRadio'; - -export default function OllamaForm({ - configValues, - setConfigValues, - provider, -}: { - configValues: Record; - setConfigValues: React.Dispatch>>; - provider: { name: string; [key: string]: unknown }; -}) { - const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name); - const parameters = providerEntry?.details?.parameters || []; - const [isCheckingLocal, setIsCheckingLocal] = useState(false); - const [isLocalAvailable, setIsLocalAvailable] = useState(false); - - const handleConnectionTypeChange = useCallback( - (value: string) => { - setConfigValues((prev) => ({ - ...prev, - connection_type: value, - })); - }, - [setConfigValues] - ); - - // Function to handle input changes and auto-select/deselect the host radio - const handleInputChange = (paramName: string, value: string) => { - // Update the parameter value - setConfigValues((prev) => ({ - ...prev, - [paramName]: value, - })); - - // If the user is typing, auto-select the host radio button - if (value && configValues.connection_type !== 'host') { - handleConnectionTypeChange('host'); - } - // If the input becomes empty and the host radio is selected, switch to local if available - else if (!value && configValues.connection_type === 'host') { - if (isLocalAvailable) { - handleConnectionTypeChange('local'); - } - // If local is not available, we keep the host selected but leave the input empty - } - }; - - const checkLocalAvailability = useCallback(async () => { - setIsCheckingLocal(true); - - // Dummy implementation - simulates checking local availability - try { - console.log('Checking for local Ollama instance...'); - // Simulate a network request with a delay - await new Promise((resolve) => setTimeout(resolve, 800)); - - // Randomly determine if Ollama is available (for demo purposes) - const isAvailable = Math.random() > 0.3; - setIsLocalAvailable(isAvailable); - - if (isAvailable) { - console.log('Local Ollama instance found'); - // Enable local radio button - } else { - console.log('No local Ollama instance found'); - // If current selection is local, switch to host - if (configValues.connection_type === 'local') { - handleConnectionTypeChange('host'); - } - } - } catch (error) { - console.error('Error checking for local Ollama:', error); - setIsLocalAvailable(false); - } finally { - setIsCheckingLocal(false); - } - }, [configValues.connection_type, handleConnectionTypeChange]); - - // Check local availability on initial load - useEffect(() => { - checkLocalAvailability(); - }, [checkLocalAvailability]); - - return ( -
-
Connection
- - {/* Local Option */} -
-
- Background App - -
- - handleConnectionTypeChange('local')} - disabled={!isLocalAvailable} - /> -
- - {/* Other Parameters */} - {parameters - .filter((param) => param.name !== 'host_url') // Skip host_url as we handle it above - .map((parameter) => ( -
-
- handleInputChange(parameter.name, e.target.value)} - placeholder={ - parameter.default ? parameter.default : parameter.name.replace(/_/g, ' ') - } - className="w-full h-14 px-4 font-regular rounded-lg border shadow-none border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-900 text-lg placeholder:text-gray-400 dark:placeholder:text-gray-500 font-regular text-gray-900 dark:text-gray-100" - required={parameter.default == null} - /> -
-
- handleConnectionTypeChange('host')} - /> -
-
- ))} -
- ); -} diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx index 3356f4a8af88..2c9bb82c1500 100644 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx +++ b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/DefaultSubmitHandler.tsx @@ -2,7 +2,7 @@ * Standalone function to submit provider configuration * Useful for components that don't want to use the hook */ -export const DefaultSubmitHandler = async ( +export const providerConfigSubmitHandler = async ( upsertFn: (key: string, value: unknown, isSecret: boolean) => Promise, provider: { name: string; @@ -15,7 +15,7 @@ export const DefaultSubmitHandler = async ( }>; }; }, - configValues: Record + configValues: Record ) => { const parameters = provider.metadata.config_keys || []; diff --git a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx b/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx deleted file mode 100644 index 329babcc8656..000000000000 --- a/ui/desktop/src/components/settings/providers/modal/subcomponents/handlers/OllamaSubmitHandler.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function OllamaSubmitHandler(configValues: Record) { - // Log each field value individually for clarity - console.log('Ollama field values:'); - Object.entries(configValues).forEach(([key, value]) => { - console.log(`${key}: ${value}`); - }); -} diff --git a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx index 25d4d9c9f295..4be31bdf41bc 100644 --- a/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx +++ b/ui/desktop/src/components/settings/providers/subcomponents/ProviderCard.tsx @@ -1,4 +1,4 @@ -import { memo, useMemo } from 'react'; +import { useMemo } from 'react'; import CardContainer from './CardContainer'; import CardHeader from './CardHeader'; import CardBody from './CardBody'; @@ -9,11 +9,10 @@ type ProviderCardProps = { provider: ProviderDetails; onConfigure: () => void; onLaunch: () => void; - onDelete: () => void; isOnboarding: boolean; }; -export const ProviderCard = memo(function ProviderCard({ +export const ProviderCard = function ProviderCard({ provider, onConfigure, onLaunch, @@ -59,4 +58,4 @@ export const ProviderCard = memo(function ProviderCard({ } /> ); -}); +}; diff --git a/ui/desktop/src/components/ui/input.tsx b/ui/desktop/src/components/ui/input.tsx index 73fdb5155de8..39bab270b9d9 100644 --- a/ui/desktop/src/components/ui/input.tsx +++ b/ui/desktop/src/components/ui/input.tsx @@ -8,7 +8,7 @@ const Input = React.forwardRef>(