import { useState, Fragment } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '../../../ui/dialog'; import DefaultProviderSetupForm, { ConfigInput, } from './subcomponents/forms/DefaultProviderSetupForm'; import ProviderSetupActions from './subcomponents/ProviderSetupActions'; import ProviderLogo from './subcomponents/ProviderLogo'; import { SecureStorageNotice } from './subcomponents/SecureStorageNotice'; import { providerConfigSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler'; import { acpAuthenticateProvider, acpDeleteCustomProvider, acpDeleteProviderConfig, acpSaveProviderConfig, } from '../../../../acp/providers'; import { useModelAndProvider } from '../../../ModelAndProviderContext'; import { AlertTriangle, LogIn } from 'lucide-react'; import { ProviderDetails } from '../../../../api'; import { Button } from '../../../../components/ui/button'; import { errorMessage } from '../../../../utils/conversionUtils'; import { defineMessages, useIntl } from '../../../../i18n'; import HuggingFaceSignInPrompt from '../../auth/HuggingFaceSignInPrompt'; const i18n = defineMessages({ deleteConfigHeader: { id: 'providerConfigurationModal.deleteConfigHeader', defaultMessage: 'Delete configuration for {providerName}', }, configureHeader: { id: 'providerConfigurationModal.configureHeader', defaultMessage: 'Configure {providerName}', }, cannotDeleteActive: { id: 'providerConfigurationModal.cannotDeleteActive', defaultMessage: "You cannot delete this provider while it's currently in use. Please switch to a different model first.", }, deleteConfirmation: { id: 'providerConfigurationModal.deleteConfirmation', defaultMessage: 'This will permanently delete the current provider configuration.', }, oauthSignInDescription: { id: 'providerConfigurationModal.oauthSignInDescription', defaultMessage: 'Sign in with your {providerName} account to use this provider', }, addApiKeyDescription: { id: 'providerConfigurationModal.addApiKeyDescription', defaultMessage: 'Add your API key(s) for this provider to integrate into goose', }, oauthLoginFailed: { id: 'providerConfigurationModal.oauthLoginFailed', defaultMessage: 'OAuth login failed: {error}', }, parameterRequired: { id: 'providerConfigurationModal.parameterRequired', defaultMessage: '{paramName} is required', }, errorTitle: { id: 'providerConfigurationModal.errorTitle', defaultMessage: 'Error', }, errorCheckingConfig: { id: 'providerConfigurationModal.errorCheckingConfig', defaultMessage: 'There was an error checking this provider configuration.', }, checkConfigAgain: { id: 'providerConfigurationModal.checkConfigAgain', defaultMessage: 'Check your configuration again to use this provider.', }, goBack: { id: 'providerConfigurationModal.goBack', defaultMessage: 'Go Back', }, signingIn: { id: 'providerConfigurationModal.signingIn', defaultMessage: 'Signing in...', }, signInWith: { id: 'providerConfigurationModal.signInWith', defaultMessage: 'Sign in with {providerName}', }, browserWindowHint: { id: 'providerConfigurationModal.browserWindowHint', defaultMessage: 'A browser window will open for you to complete the login.', }, deviceCodeFlowHint: { id: 'providerConfigurationModal.deviceCodeFlowHint', defaultMessage: 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.', }, externalSetupIntro: { id: 'providerConfigurationModal.externalSetupIntro', defaultMessage: 'This provider is configured outside of goose. Follow these steps:', }, seeDocumentation: { id: 'providerConfigurationModal.seeDocumentation', defaultMessage: 'See the documentation for more details.', }, cancel: { id: 'providerConfigurationModal.cancel', defaultMessage: 'Cancel', }, removeConfiguration: { id: 'providerConfigurationModal.removeConfiguration', defaultMessage: 'Remove Configuration', }, close: { id: 'providerConfigurationModal.close', defaultMessage: 'Close', }, huggingFaceOAuthDescription: { id: 'providerConfigurationModal.huggingFaceOAuthDescription', defaultMessage: 'Sign in to use Hugging Face Inference Providers without manually entering an API token.', }, }); /** Render a setup step string, turning `backtick` spans into and newlines into
. */ function renderSetupStep(text: string) { // Split on backtick-wrapped segments const parts = text.split(/(`[^`]+`)/g); return parts.map((part, i) => { if (part.startsWith('`') && part.endsWith('`')) { return ( {part.slice(1, -1)} ); } // Handle newlines within text const lines = part.split('\n'); return ( {lines.map((line, j) => ( {j > 0 &&
} {line}
))}
); }); } interface ProviderConfigurationModalProps { provider: ProviderDetails; onClose: () => void; onConfigured?: (provider: ProviderDetails) => void; } export default function ProviderConfigurationModal({ provider, onClose, onConfigured, }: ProviderConfigurationModalProps) { const intl = useIntl(); const [validationErrors, setValidationErrors] = useState>({}); const { getCurrentModelAndProvider } = useModelAndProvider(); const [configValues, setConfigValues] = useState>({}); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); const [isActiveProvider, setIsActiveProvider] = useState(false); const [error, setError] = useState(null); const [isOAuthLoading, setIsOAuthLoading] = useState(false); let primaryParameters = provider.metadata.config_keys.filter((param) => param.primary); if (primaryParameters.length === 0) { primaryParameters = provider.metadata.config_keys; } const configKeys = provider.metadata.config_keys.filter((key) => !key.oauth_flow); const hasOAuth = provider.metadata.config_keys.some((key) => key.oauth_flow); const hasConfig = configKeys.length > 0; const hasDeviceCodeFlow = provider.metadata.config_keys.some((key) => key.device_code_flow); const isHuggingFaceProvider = provider.name === 'huggingface'; const isConfigured = provider.is_configured; const headerText = showDeleteConfirmation ? intl.formatMessage(i18n.deleteConfigHeader, { providerName: provider.metadata.display_name }) : intl.formatMessage(i18n.configureHeader, { providerName: provider.metadata.display_name }); const isExternalSetup = provider.metadata.config_keys.length === 0 && provider.metadata.setup_steps && provider.metadata.setup_steps.length > 0; const descriptionText = showDeleteConfirmation ? isActiveProvider ? intl.formatMessage(i18n.cannotDeleteActive) : intl.formatMessage(i18n.deleteConfirmation) : hasOAuth ? intl.formatMessage(i18n.oauthSignInDescription, { providerName: provider.metadata.display_name, }) : isExternalSetup ? provider.metadata.description : intl.formatMessage(i18n.addApiKeyDescription); const handleOAuthLogin = async () => { setIsOAuthLoading(true); setError(null); try { if (hasConfig) { const fields: { key: string; value: string }[] = []; for (const key of configKeys) { const entry = configValues[key.name]; const value = entry?.value ?? (typeof entry?.serverValue === 'string' ? entry.serverValue : null); if (value) { fields.push({ key: key.name, value }); } } if (fields.length > 0) { await acpSaveProviderConfig(provider.name, fields); } } await acpAuthenticateProvider(provider.name); if (onConfigured) { onConfigured(provider); } else { onClose(); } } catch (err) { setError(intl.formatMessage(i18n.oauthLoginFailed, { error: errorMessage(err) })); } finally { setIsOAuthLoading(false); } }; const handleSubmitForm = async (e: React.FormEvent) => { e.preventDefault(); setValidationErrors({}); const parameters = provider.metadata.config_keys || []; const errors: Record = {}; parameters.forEach((parameter) => { if ( parameter.required && !configValues[parameter.name]?.value && !configValues[parameter.name]?.serverValue ) { errors[parameter.name] = intl.formatMessage(i18n.parameterRequired, { paramName: parameter.name, }); } }); if (Object.keys(errors).length > 0) { setValidationErrors(errors); return; } const toSubmit = Object.fromEntries( Object.entries(configValues) .filter( ([_k, entry]) => !!entry.value || (entry.serverValue != null && typeof entry.serverValue === 'string') ) .map(([k, entry]) => [ k, entry.value ?? (typeof entry.serverValue === 'string' ? entry.serverValue : ''), ]) ); try { await providerConfigSubmitHandler(provider, toSubmit); if (onConfigured) { onConfigured(provider); } else { onClose(); } } catch (error) { setError(errorMessage(error)); } }; const handleCancel = () => { onClose(); }; const handleDelete = async () => { try { const providerModel = await getCurrentModelAndProvider(); if (provider.name === providerModel.provider) { setIsActiveProvider(true); setShowDeleteConfirmation(true); return; } } catch (error) { console.error('Failed to check current provider:', error); } setIsActiveProvider(false); setShowDeleteConfirmation(true); }; const handleConfirmDelete = async () => { if (isActiveProvider) { return; } const isCustomProvider = provider.provider_type === 'Custom'; if (isCustomProvider) { await acpDeleteCustomProvider(provider.name); } else { // Deletes all config/secret fields and cleans up provider-specific cache // (e.g. OAuth tokens) server-side. await acpDeleteProviderConfig(provider.name); } onClose(); }; const getModalIcon = () => { if (showDeleteConfirmation) { return ( ); } return ; }; return ( <> !open && setError(null)}> {intl.formatMessage(i18n.errorTitle)} {intl.formatMessage(i18n.errorCheckingConfig)}
{error}
{intl.formatMessage(i18n.checkConfigAgain)}
!open && onClose()}> {getModalIcon()} {headerText} {descriptionText}
{/* Contains information used to set up each provider */} {/* Only show the form when NOT in delete confirmation mode */} {!showDeleteConfirmation && ( <> {hasOAuth && (

{hasDeviceCodeFlow ? intl.formatMessage(i18n.deviceCodeFlowHint) : intl.formatMessage(i18n.browserWindowHint)}

)} {hasOAuth && hasConfig && ( )} {isHuggingFaceProvider && !hasOAuth && ( { if (onConfigured) { onConfigured(provider); } else { onClose(); } }} /> )} {isExternalSetup && (

{intl.formatMessage(i18n.externalSetupIntro)}

    {provider.metadata.setup_steps?.map((step, i) => (
  1. {renderSetupStep(step)}
  2. ))}
{provider.metadata.model_doc_link && (

{intl.formatMessage(i18n.seeDocumentation, { link: (chunks: React.ReactNode) => ( { e.preventDefault(); window.electron.openExternal(provider.metadata.model_doc_link); }} className="underline hover:text-text-primary" > {chunks} ), })}

)}
)} {!hasOAuth && !isExternalSetup && ( <> {primaryParameters.length > 0 && provider.metadata.config_keys.length > 0 && ( )} )} )}
{hasOAuth && !showDeleteConfirmation ? (
{isConfigured && ( )}
) : isExternalSetup && !showDeleteConfirmation ? (
) : ( { setIsActiveProvider(false); setShowDeleteConfirmation(false); }} canDelete={isConfigured && !isActiveProvider} providerName={provider.metadata.display_name} isActiveProvider={isActiveProvider} /> )}
); }