feat(desktop): add i18n infrastructure with react-intl (#8105)
This commit is contained in:
@@ -13,6 +13,34 @@ import CustomProviderForm from './modal/subcomponents/forms/CustomProviderForm';
|
||||
import { SwitchModelModal } from '../models/subcomponents/SwitchModelModal';
|
||||
import { useModelAndProvider } from '../../ModelAndProviderContext';
|
||||
import type { View } from '../../../utils/navigationUtils';
|
||||
import { defineMessages, useIntl } from '../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
addProvider: {
|
||||
id: 'providerGrid.addProvider',
|
||||
defaultMessage: 'Add Provider',
|
||||
},
|
||||
fromTemplateOrManual: {
|
||||
id: 'providerGrid.fromTemplateOrManual',
|
||||
defaultMessage: 'From template or manual setup',
|
||||
},
|
||||
editProvider: {
|
||||
id: 'providerGrid.editProvider',
|
||||
defaultMessage: 'Edit Provider',
|
||||
},
|
||||
configureProvider: {
|
||||
id: 'providerGrid.configureProvider',
|
||||
defaultMessage: 'Configure Provider',
|
||||
},
|
||||
addProviderTitle: {
|
||||
id: 'providerGrid.addProviderTitle',
|
||||
defaultMessage: 'Add Provider',
|
||||
},
|
||||
chooseModel: {
|
||||
id: 'providerGrid.chooseModel',
|
||||
defaultMessage: 'Choose Model',
|
||||
},
|
||||
});
|
||||
|
||||
const GridLayout = memo(function GridLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
@@ -29,6 +57,7 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac
|
||||
});
|
||||
|
||||
const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onClick: () => void }) {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<CardContainer
|
||||
testId="add-custom-provider-card"
|
||||
@@ -38,8 +67,8 @@ const CustomProviderCard = memo(function CustomProviderCard({ onClick }: { onCli
|
||||
<div className="flex flex-col items-center justify-center min-h-[200px]">
|
||||
<Plus className="w-8 h-8 text-gray-400 mb-2" />
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400 text-center">
|
||||
<div className="font-medium">Add Provider</div>
|
||||
<div className="text-xs text-gray-500 mt-1">From template or manual setup</div>
|
||||
<div className="font-medium">{intl.formatMessage(i18n.addProvider)}</div>
|
||||
<div className="text-xs text-gray-500 mt-1">{intl.formatMessage(i18n.fromTemplateOrManual)}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -62,6 +91,7 @@ function ProviderCards({
|
||||
setView?: (view: View) => void;
|
||||
onModelSelected?: (model?: string) => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const [configuringProvider, setConfiguringProvider] = useState<ProviderDetails | null>(null);
|
||||
const [showCustomProviderModal, setShowCustomProviderModal] = useState(false);
|
||||
const [showSwitchModelModal, setShowSwitchModelModal] = useState(false);
|
||||
@@ -246,7 +276,9 @@ function ProviderCards({
|
||||
};
|
||||
|
||||
const editable = editingProvider ? editingProvider.isEditable : true;
|
||||
const title = (editingProvider ? (editable ? 'Edit' : 'Configure') : 'Add') + ' Provider';
|
||||
const title = editingProvider
|
||||
? (editable ? intl.formatMessage(i18n.editProvider) : intl.formatMessage(i18n.configureProvider))
|
||||
: intl.formatMessage(i18n.addProviderTitle);
|
||||
return (
|
||||
<>
|
||||
{providerCards}
|
||||
@@ -281,7 +313,7 @@ function ProviderCards({
|
||||
setView={handleSetView}
|
||||
onModelSelected={onModelSelected}
|
||||
initialProvider={switchModelProvider}
|
||||
titleOverride="Choose Model"
|
||||
titleOverride={intl.formatMessage(i18n.chooseModel)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,27 @@ import ProviderGrid from './ProviderGrid';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ProviderDetails } from '../../../api';
|
||||
import { createNavigationHandler } from '../../../utils/navigationUtils';
|
||||
import { defineMessages, useIntl } from '../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
otherProviders: {
|
||||
id: 'providerSettings.otherProviders',
|
||||
defaultMessage: 'Other providers',
|
||||
},
|
||||
configurationSettings: {
|
||||
id: 'providerSettings.configurationSettings',
|
||||
defaultMessage: 'Provider Configuration Settings',
|
||||
},
|
||||
onboardingDescription: {
|
||||
id: 'providerSettings.onboardingDescription',
|
||||
defaultMessage:
|
||||
"Select an AI model provider to get started with goose. You'll need to use API keys generated by each provider which will be encrypted and stored locally. You can change your provider at any time in settings.",
|
||||
},
|
||||
loadingProviders: {
|
||||
id: 'providerSettings.loadingProviders',
|
||||
defaultMessage: 'Loading providers...',
|
||||
},
|
||||
});
|
||||
|
||||
interface ProviderSettingsProps {
|
||||
onClose: () => void;
|
||||
@@ -18,6 +39,7 @@ export default function ProviderSettings({
|
||||
isOnboarding,
|
||||
onProviderLaunched,
|
||||
}: ProviderSettingsProps) {
|
||||
const intl = useIntl();
|
||||
const { getProviders } = useConfig();
|
||||
const navigate = useNavigate();
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -67,13 +89,13 @@ export default function ProviderSettings({
|
||||
<BackButton onClick={onClose} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-light mb-4 pt-6" data-testid="provider-selection-heading">
|
||||
{isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}
|
||||
{isOnboarding
|
||||
? intl.formatMessage(i18n.otherProviders)
|
||||
: intl.formatMessage(i18n.configurationSettings)}
|
||||
</h1>
|
||||
{isOnboarding && (
|
||||
<p className="text-sm sm:text-base text-text-secondary max-w-full sm:max-w-2xl">
|
||||
Select an AI model provider to get started with goose. You'll need to use API keys
|
||||
generated by each provider which will be encrypted and stored locally. You can
|
||||
change your provider at any time in settings.
|
||||
{intl.formatMessage(i18n.onboardingDescription)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -84,7 +106,7 @@ export default function ProviderSettings({
|
||||
<div className="w-full max-w-6xl mx-auto pt-4 px-4 sm:px-6 md:px-8">
|
||||
<div className="relative z-10">
|
||||
{loading ? (
|
||||
<div>Loading providers...</div>
|
||||
<div>{intl.formatMessage(i18n.loadingProviders)}</div>
|
||||
) : (
|
||||
<ProviderGrid
|
||||
providers={providers}
|
||||
|
||||
@@ -25,6 +25,96 @@ import {
|
||||
} from '../../../../api';
|
||||
import { Button } from '../../../../components/ui/button';
|
||||
import { errorMessage } from '../../../../utils/conversionUtils';
|
||||
import { defineMessages, useIntl } from '../../../../i18n';
|
||||
|
||||
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 <link>documentation</link> for more details.',
|
||||
},
|
||||
cancel: {
|
||||
id: 'providerConfigurationModal.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
removeConfiguration: {
|
||||
id: 'providerConfigurationModal.removeConfiguration',
|
||||
defaultMessage: 'Remove Configuration',
|
||||
},
|
||||
close: {
|
||||
id: 'providerConfigurationModal.close',
|
||||
defaultMessage: 'Close',
|
||||
},
|
||||
});
|
||||
|
||||
/** Render a setup step string, turning `backtick` spans into <code> and newlines into <br/>. */
|
||||
function renderSetupStep(text: string) {
|
||||
@@ -67,6 +157,7 @@ export default function ProviderConfigurationModal({
|
||||
onClose,
|
||||
onConfigured,
|
||||
}: ProviderConfigurationModalProps) {
|
||||
const intl = useIntl();
|
||||
const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
|
||||
const { upsert, remove } = useConfig();
|
||||
const { getCurrentModelAndProvider } = useModelAndProvider();
|
||||
@@ -86,8 +177,8 @@ export default function ProviderConfigurationModal({
|
||||
|
||||
const isConfigured = provider.is_configured;
|
||||
const headerText = showDeleteConfirmation
|
||||
? `Delete configuration for ${provider.metadata.display_name}`
|
||||
: `Configure ${provider.metadata.display_name}`;
|
||||
? 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 &&
|
||||
@@ -96,13 +187,13 @@ export default function ProviderConfigurationModal({
|
||||
|
||||
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.'
|
||||
? intl.formatMessage(i18n.cannotDeleteActive)
|
||||
: intl.formatMessage(i18n.deleteConfirmation)
|
||||
: isOAuthProvider
|
||||
? `Sign in with your ${provider.metadata.display_name} account to use this provider`
|
||||
? intl.formatMessage(i18n.oauthSignInDescription, { providerName: provider.metadata.display_name })
|
||||
: isExternalSetup
|
||||
? provider.metadata.description
|
||||
: `Add your API key(s) for this provider to integrate into goose`;
|
||||
: intl.formatMessage(i18n.addApiKeyDescription);
|
||||
|
||||
const handleOAuthLogin = async () => {
|
||||
setIsOAuthLoading(true);
|
||||
@@ -117,7 +208,7 @@ export default function ProviderConfigurationModal({
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(`OAuth login failed: ${errorMessage(err)}`);
|
||||
setError(intl.formatMessage(i18n.oauthLoginFailed, { error: errorMessage(err) }));
|
||||
} finally {
|
||||
setIsOAuthLoading(false);
|
||||
}
|
||||
@@ -137,7 +228,7 @@ export default function ProviderConfigurationModal({
|
||||
!configValues[parameter.name]?.value &&
|
||||
!configValues[parameter.name]?.serverValue
|
||||
) {
|
||||
errors[parameter.name] = `${parameter.name} is required`;
|
||||
errors[parameter.name] = intl.formatMessage(i18n.parameterRequired, { paramName: parameter.name });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -240,15 +331,15 @@ export default function ProviderConfigurationModal({
|
||||
<>
|
||||
<Dialog open={!!error} onOpenChange={(open) => !open && setError(null)}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogTitle className="flex items-center gap-2">Error</DialogTitle>
|
||||
<DialogTitle className="flex items-center gap-2">{intl.formatMessage(i18n.errorTitle)}</DialogTitle>
|
||||
<DialogDescription className="text-inherit text-base">
|
||||
There was an error checking this provider configuration.
|
||||
{intl.formatMessage(i18n.errorCheckingConfig)}
|
||||
</DialogDescription>
|
||||
<pre className="ml-2">{error}</pre>
|
||||
<div>Check your configuration again to use this provider.</div>
|
||||
<div>{intl.formatMessage(i18n.checkConfigAgain)}</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setError(null)}>
|
||||
Go Back
|
||||
{intl.formatMessage(i18n.goBack)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -277,13 +368,13 @@ export default function ProviderConfigurationModal({
|
||||
>
|
||||
<LogIn size={20} />
|
||||
{isOAuthLoading
|
||||
? 'Signing in...'
|
||||
: `Sign in with ${provider.metadata.display_name}`}
|
||||
? intl.formatMessage(i18n.signingIn)
|
||||
: intl.formatMessage(i18n.signInWith, { providerName: provider.metadata.display_name })}
|
||||
</Button>
|
||||
<p className="text-sm text-text-secondary text-center">
|
||||
{provider.metadata.config_keys.some((key) => key.device_code_flow)
|
||||
? 'A browser window will open and the verification code will be copied to your clipboard. Paste it in the browser to complete sign-in.'
|
||||
: 'A browser window will open for you to complete the login.'}
|
||||
? intl.formatMessage(i18n.deviceCodeFlowHint)
|
||||
: intl.formatMessage(i18n.browserWindowHint)}
|
||||
</p>
|
||||
</div>
|
||||
) : provider.metadata.config_keys.length === 0 &&
|
||||
@@ -291,7 +382,7 @@ export default function ProviderConfigurationModal({
|
||||
provider.metadata.setup_steps.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
<p className="text-sm text-text-secondary">
|
||||
This provider is configured outside of goose. Follow these steps:
|
||||
{intl.formatMessage(i18n.externalSetupIntro)}
|
||||
</p>
|
||||
<ol className="ml-5 list-decimal text-sm text-text-primary space-y-2">
|
||||
{provider.metadata.setup_steps.map((step, i) => (
|
||||
@@ -300,18 +391,20 @@ export default function ProviderConfigurationModal({
|
||||
</ol>
|
||||
{provider.metadata.model_doc_link && (
|
||||
<p className="text-sm text-text-secondary mt-4">
|
||||
See the{' '}
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.electron.openExternal(provider.metadata.model_doc_link);
|
||||
}}
|
||||
className="underline hover:text-text-primary"
|
||||
>
|
||||
documentation
|
||||
</a>{' '}
|
||||
for more details.
|
||||
{intl.formatMessage(i18n.seeDocumentation, {
|
||||
link: (chunks: React.ReactNode) => (
|
||||
<a
|
||||
href="#"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.electron.openExternal(provider.metadata.model_doc_link);
|
||||
}}
|
||||
className="underline hover:text-text-primary"
|
||||
>
|
||||
{chunks}
|
||||
</a>
|
||||
),
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -337,11 +430,11 @@ export default function ProviderConfigurationModal({
|
||||
{isOAuthProvider && !showDeleteConfirmation ? (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={handleCancel}>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
{isConfigured && (
|
||||
<Button variant="destructive" onClick={handleDelete}>
|
||||
Remove Configuration
|
||||
{intl.formatMessage(i18n.removeConfiguration)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -356,7 +449,7 @@ export default function ProviderConfigurationModal({
|
||||
onClick={handleCancel}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
|
||||
>
|
||||
Close
|
||||
{intl.formatMessage(i18n.close)}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
|
||||
+71
-12
@@ -9,6 +9,62 @@ import {
|
||||
type ProviderCatalogEntry,
|
||||
type ProviderTemplate,
|
||||
} from '../../../../../api';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
chooseProvider: {
|
||||
id: 'providerCatalogPicker.chooseProvider',
|
||||
defaultMessage: 'Choose Provider',
|
||||
},
|
||||
selectFormatDescription: {
|
||||
id: 'providerCatalogPicker.selectFormatDescription',
|
||||
defaultMessage: "Select an API format and provider. We'll auto-fill the configuration for you.",
|
||||
},
|
||||
apiFormat: {
|
||||
id: 'providerCatalogPicker.apiFormat',
|
||||
defaultMessage: 'API Format',
|
||||
},
|
||||
openaiCompatible: {
|
||||
id: 'providerCatalogPicker.openaiCompatible',
|
||||
defaultMessage: 'OpenAI Compatible',
|
||||
},
|
||||
anthropicCompatible: {
|
||||
id: 'providerCatalogPicker.anthropicCompatible',
|
||||
defaultMessage: 'Anthropic Compatible',
|
||||
},
|
||||
searchProviders: {
|
||||
id: 'providerCatalogPicker.searchProviders',
|
||||
defaultMessage: 'Search providers...',
|
||||
},
|
||||
loadingProviders: {
|
||||
id: 'providerCatalogPicker.loadingProviders',
|
||||
defaultMessage: 'Loading providers...',
|
||||
},
|
||||
errorPrefix: {
|
||||
id: 'providerCatalogPicker.errorPrefix',
|
||||
defaultMessage: 'Error: {error}',
|
||||
},
|
||||
noProvidersFound: {
|
||||
id: 'providerCatalogPicker.noProvidersFound',
|
||||
defaultMessage: 'No providers found for "{query}"',
|
||||
},
|
||||
noProvidersAvailable: {
|
||||
id: 'providerCatalogPicker.noProvidersAvailable',
|
||||
defaultMessage: 'No providers available',
|
||||
},
|
||||
modelsAvailable: {
|
||||
id: 'providerCatalogPicker.modelsAvailable',
|
||||
defaultMessage: '{count} models available',
|
||||
},
|
||||
requiresEnvVar: {
|
||||
id: 'providerCatalogPicker.requiresEnvVar',
|
||||
defaultMessage: ' • Requires {envVar}',
|
||||
},
|
||||
cancel: {
|
||||
id: 'providerCatalogPicker.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
});
|
||||
|
||||
interface ProviderCatalogPickerProps {
|
||||
onSelect: (template: ProviderTemplate) => void;
|
||||
@@ -21,6 +77,7 @@ export default function ProviderCatalogPicker({
|
||||
onCancel,
|
||||
embedded,
|
||||
}: ProviderCatalogPickerProps) {
|
||||
const intl = useIntl();
|
||||
const [selectedFormat, setSelectedFormat] = useState<string>('openai');
|
||||
const [providers, setProviders] = useState<ProviderCatalogEntry[]>([]);
|
||||
const [filteredProviders, setFilteredProviders] = useState<ProviderCatalogEntry[]>([]);
|
||||
@@ -29,8 +86,8 @@ export default function ProviderCatalogPicker({
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const formatOptions = [
|
||||
{ value: 'openai', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic', label: 'Anthropic Compatible' },
|
||||
{ value: 'openai', label: intl.formatMessage(i18n.openaiCompatible) },
|
||||
{ value: 'anthropic', label: intl.formatMessage(i18n.anthropicCompatible) },
|
||||
];
|
||||
|
||||
// Fetch providers when format changes
|
||||
@@ -91,15 +148,15 @@ export default function ProviderCatalogPicker({
|
||||
<div className="space-y-4">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-textStandard mb-2">Choose Provider</h3>
|
||||
<h3 className="text-lg font-semibold text-textStandard mb-2">{intl.formatMessage(i18n.chooseProvider)}</h3>
|
||||
<p className="text-sm text-textSubtle">
|
||||
Select an API format and provider. We'll auto-fill the configuration for you.
|
||||
{intl.formatMessage(i18n.selectFormatDescription)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Format Selection */}
|
||||
<div>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">API Format</label>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">{intl.formatMessage(i18n.apiFormat)}</label>
|
||||
<Select
|
||||
options={formatOptions}
|
||||
value={formatOptions.find((opt) => opt.value === selectedFormat)}
|
||||
@@ -118,7 +175,7 @@ export default function ProviderCatalogPicker({
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-textSubtle w-4 h-4" />
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search providers..."
|
||||
placeholder={intl.formatMessage(i18n.searchProviders)}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -126,15 +183,17 @@ export default function ProviderCatalogPicker({
|
||||
</div>
|
||||
|
||||
{/* Loading/Error */}
|
||||
{loading && <div className="text-center py-8 text-textSubtle">Loading providers...</div>}
|
||||
{error && <div className="text-center py-8 text-red-500">Error: {error}</div>}
|
||||
{loading && <div className="text-center py-8 text-textSubtle">{intl.formatMessage(i18n.loadingProviders)}</div>}
|
||||
{error && <div className="text-center py-8 text-red-500">{intl.formatMessage(i18n.errorPrefix, { error })}</div>}
|
||||
|
||||
{/* Provider List */}
|
||||
{!loading && !error && (
|
||||
<div className="space-y-2 max-h-96 overflow-y-auto">
|
||||
{filteredProviders.length === 0 ? (
|
||||
<div className="text-center py-8 text-textSubtle">
|
||||
{searchQuery ? `No providers found for "${searchQuery}"` : 'No providers available'}
|
||||
{searchQuery
|
||||
? intl.formatMessage(i18n.noProvidersFound, { query: searchQuery })
|
||||
: intl.formatMessage(i18n.noProvidersAvailable)}
|
||||
</div>
|
||||
) : (
|
||||
filteredProviders.map((provider) => (
|
||||
@@ -161,8 +220,8 @@ export default function ProviderCatalogPicker({
|
||||
</div>
|
||||
<div className="text-sm text-textSubtle mt-1 break-all">{provider.api_url}</div>
|
||||
<div className="text-xs text-textSubtle mt-2">
|
||||
{provider.model_count} models available
|
||||
{provider.env_var && ` • Requires ${provider.env_var}`}
|
||||
{intl.formatMessage(i18n.modelsAvailable, { count: provider.model_count })}
|
||||
{provider.env_var && intl.formatMessage(i18n.requiresEnvVar, { envVar: provider.env_var })}
|
||||
</div>
|
||||
</div>
|
||||
<Check className="w-5 h-5 text-primary opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0" />
|
||||
@@ -177,7 +236,7 @@ export default function ProviderCatalogPicker({
|
||||
{!embedded && (
|
||||
<div className="flex justify-end space-x-2 pt-4">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -10,6 +10,14 @@ import XaiLogo from './icons/xai@3x.png';
|
||||
import MiniMaxLogo from './icons/minimax@3x.png';
|
||||
import TanzuLogo from './icons/tanzu@3x.png';
|
||||
import DefaultLogo from './icons/default@3x.png';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
logoAlt: {
|
||||
id: 'providerLogo.alt',
|
||||
defaultMessage: '{providerName} logo',
|
||||
},
|
||||
});
|
||||
|
||||
// Map provider names to their logos
|
||||
const providerLogos: Record<string, string> = {
|
||||
@@ -32,6 +40,7 @@ interface ProviderLogoProps {
|
||||
}
|
||||
|
||||
export default function ProviderLogo({ providerName }: ProviderLogoProps) {
|
||||
const intl = useIntl();
|
||||
// Convert provider name to lowercase and fetch the logo
|
||||
const logoKey = providerName.toLowerCase();
|
||||
const logo = providerLogos[logoKey] || DefaultLogo;
|
||||
@@ -50,7 +59,7 @@ export default function ProviderLogo({ providerName }: ProviderLogoProps) {
|
||||
<div className="w-12 h-12 bg-black rounded-full overflow-hidden flex items-center justify-center">
|
||||
<img
|
||||
src={logo}
|
||||
alt={`${providerName} logo`}
|
||||
alt={intl.formatMessage(i18n.logoAlt, { providerName })}
|
||||
className={imageClassName}
|
||||
style={imageStyle}
|
||||
/>
|
||||
|
||||
+50
-12
@@ -2,6 +2,44 @@ import { SyntheticEvent } from 'react';
|
||||
import { Button } from '../../../../ui/button';
|
||||
import { Trash2, AlertTriangle } from 'lucide-react';
|
||||
import { ConfigKey } from '../../../../../api';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
cannotDeleteActive: {
|
||||
id: 'providerSetupActions.cannotDeleteActive',
|
||||
defaultMessage:
|
||||
'You cannot delete {providerName} while it\'s currently in use. Please switch to a different model before deleting this provider.',
|
||||
},
|
||||
ok: {
|
||||
id: 'providerSetupActions.ok',
|
||||
defaultMessage: 'Ok',
|
||||
},
|
||||
confirmDeleteMessage: {
|
||||
id: 'providerSetupActions.confirmDeleteMessage',
|
||||
defaultMessage:
|
||||
'Are you sure you want to delete the configuration parameters for {providerName}? This action cannot be undone.',
|
||||
},
|
||||
confirmDelete: {
|
||||
id: 'providerSetupActions.confirmDelete',
|
||||
defaultMessage: 'Confirm Delete',
|
||||
},
|
||||
cancel: {
|
||||
id: 'providerSetupActions.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
deleteProvider: {
|
||||
id: 'providerSetupActions.deleteProvider',
|
||||
defaultMessage: 'Delete Provider',
|
||||
},
|
||||
submit: {
|
||||
id: 'providerSetupActions.submit',
|
||||
defaultMessage: 'Submit',
|
||||
},
|
||||
enableProvider: {
|
||||
id: 'providerSetupActions.enableProvider',
|
||||
defaultMessage: 'Enable Provider',
|
||||
},
|
||||
});
|
||||
|
||||
interface ProviderSetupActionsProps {
|
||||
onCancel: () => void;
|
||||
@@ -32,6 +70,8 @@ export default function ProviderSetupActions({
|
||||
primaryParameters,
|
||||
isActiveProvider = false, // Default value provided
|
||||
}: ProviderSetupActionsProps) {
|
||||
const intl = useIntl();
|
||||
|
||||
// If we're showing delete confirmation, render the delete confirmation buttons
|
||||
if (showDeleteConfirmation) {
|
||||
// Check if this is the active provider
|
||||
@@ -42,8 +82,7 @@ export default function ProviderSetupActions({
|
||||
<p className="text-yellow-500 text-sm mb-2 flex items-start">
|
||||
<AlertTriangle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
You cannot delete {providerName} while it's currently in use. Please switch to a
|
||||
different model before deleting this provider.
|
||||
{intl.formatMessage(i18n.cannotDeleteActive, { providerName })}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
@@ -52,7 +91,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onCancelDelete}
|
||||
className="w-full h-[60px] rounded-none hover:bg-background-secondary text-text-secondary hover:text-text-primary text-md font-regular"
|
||||
>
|
||||
Ok
|
||||
{intl.formatMessage(i18n.ok)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -63,22 +102,21 @@ export default function ProviderSetupActions({
|
||||
<div className="w-full">
|
||||
<div className="w-full px-6 py-4 bg-red-900/20 border-t border-red-500/30">
|
||||
<p className="text-red-400 text-sm mb-2">
|
||||
Are you sure you want to delete the configuration parameters for {providerName}? This
|
||||
action cannot be undone.
|
||||
{intl.formatMessage(i18n.confirmDeleteMessage, { providerName })}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={onConfirmDelete}
|
||||
className="w-full h-[60px] rounded-none border-b border-border-primary bg-transparent hover:bg-red-900/20 text-red-500 font-medium text-md"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" /> Confirm Delete
|
||||
<Trash2 className="h-4 w-4 mr-2" /> {intl.formatMessage(i18n.confirmDelete)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onCancelDelete}
|
||||
className="w-full h-[60px] rounded-none hover:bg-background-secondary text-text-secondary hover:text-text-primary text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@@ -93,7 +131,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onDelete}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary bg-transparent hover:bg-background-secondary text-red-500 font-medium text-md"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" /> Delete Provider
|
||||
<Trash2 className="h-4 w-4 mr-2" /> {intl.formatMessage(i18n.deleteProvider)}
|
||||
</Button>
|
||||
)}
|
||||
{primaryParameters && primaryParameters.length > 0 ? (
|
||||
@@ -104,7 +142,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onSubmit}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
|
||||
>
|
||||
Submit
|
||||
{intl.formatMessage(i18n.submit)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -112,7 +150,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onCancel}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary hover:text-text-primary text-text-secondary hover:bg-background-secondary text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -123,7 +161,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onSubmit}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary text-md hover:bg-background-secondary text-text-primary font-medium"
|
||||
>
|
||||
Enable Provider
|
||||
{intl.formatMessage(i18n.enableProvider)}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
@@ -131,7 +169,7 @@ export default function ProviderSetupActions({
|
||||
onClick={onCancel}
|
||||
className="w-full h-[60px] rounded-none border-t border-border-primary hover:text-text-primary text-text-secondary hover:bg-background-secondary text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
+15
-2
@@ -1,4 +1,12 @@
|
||||
import { Lock } from 'lucide-react';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
defaultMessage: {
|
||||
id: 'secureStorageNotice.defaultMessage',
|
||||
defaultMessage: 'Keys are stored securely in the keychain',
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* SecureStorageNotice - A reusable component that displays a message about secure storage
|
||||
@@ -10,12 +18,17 @@ import { Lock } from 'lucide-react';
|
||||
*/
|
||||
export function SecureStorageNotice({
|
||||
className = '',
|
||||
message = 'Keys are stored securely in the keychain',
|
||||
message,
|
||||
}: {
|
||||
className?: string;
|
||||
message?: string;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
const displayMessage = message ?? intl.formatMessage(i18n.defaultMessage);
|
||||
return (
|
||||
<div className={`flex items-center mt-2 text-gray-600 dark:text-gray-300 ${className}`}>
|
||||
<Lock className="w-5 h-5" />
|
||||
<span className="text-sm font-light ml-2">{message}</span>
|
||||
<span className="text-sm font-light ml-2">{displayMessage}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+274
-61
@@ -7,6 +7,221 @@ import { UpdateCustomProviderRequest, type ProviderTemplate } from '../../../../
|
||||
import { Plus, X, Trash2, AlertTriangle, ExternalLink, Search, Settings } from 'lucide-react';
|
||||
import { cn } from '../../../../../../utils';
|
||||
import ProviderCatalogPicker from '../ProviderCatalogPicker';
|
||||
import { defineMessages, useIntl } from '../../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
chooseSetup: {
|
||||
id: 'customProviderForm.chooseSetup',
|
||||
defaultMessage: "Choose how you'd like to set up your provider.",
|
||||
},
|
||||
startFromTemplate: {
|
||||
id: 'customProviderForm.startFromTemplate',
|
||||
defaultMessage: 'Start from a provider template',
|
||||
},
|
||||
startFromTemplateDesc: {
|
||||
id: 'customProviderForm.startFromTemplateDesc',
|
||||
defaultMessage: "Pick a known provider and we'll auto-fill the configuration",
|
||||
},
|
||||
configureManually: {
|
||||
id: 'customProviderForm.configureManually',
|
||||
defaultMessage: 'Configure manually',
|
||||
},
|
||||
configureManuallyDesc: {
|
||||
id: 'customProviderForm.configureManuallyDesc',
|
||||
defaultMessage: 'Enter all provider details yourself',
|
||||
},
|
||||
cancel: {
|
||||
id: 'customProviderForm.cancel',
|
||||
defaultMessage: 'Cancel',
|
||||
},
|
||||
back: {
|
||||
id: 'customProviderForm.back',
|
||||
defaultMessage: '← Back',
|
||||
},
|
||||
usingTemplate: {
|
||||
id: 'customProviderForm.usingTemplate',
|
||||
defaultMessage: 'Using template: {name}',
|
||||
},
|
||||
docs: {
|
||||
id: 'customProviderForm.docs',
|
||||
defaultMessage: 'Docs',
|
||||
},
|
||||
clear: {
|
||||
id: 'customProviderForm.clear',
|
||||
defaultMessage: 'Clear',
|
||||
},
|
||||
providerType: {
|
||||
id: 'customProviderForm.providerType',
|
||||
defaultMessage: 'Provider Type',
|
||||
},
|
||||
openaiCompatible: {
|
||||
id: 'customProviderForm.openaiCompatible',
|
||||
defaultMessage: 'OpenAI Compatible',
|
||||
},
|
||||
anthropicCompatible: {
|
||||
id: 'customProviderForm.anthropicCompatible',
|
||||
defaultMessage: 'Anthropic Compatible',
|
||||
},
|
||||
ollamaCompatible: {
|
||||
id: 'customProviderForm.ollamaCompatible',
|
||||
defaultMessage: 'Ollama Compatible',
|
||||
},
|
||||
displayName: {
|
||||
id: 'customProviderForm.displayName',
|
||||
defaultMessage: 'Display Name',
|
||||
},
|
||||
displayNamePlaceholder: {
|
||||
id: 'customProviderForm.displayNamePlaceholder',
|
||||
defaultMessage: 'Your Provider Name',
|
||||
},
|
||||
apiUrl: {
|
||||
id: 'customProviderForm.apiUrl',
|
||||
defaultMessage: 'API URL',
|
||||
},
|
||||
apiUrlPlaceholder: {
|
||||
id: 'customProviderForm.apiUrlPlaceholder',
|
||||
defaultMessage: 'https://api.example.com',
|
||||
},
|
||||
apiBasePath: {
|
||||
id: 'customProviderForm.apiBasePath',
|
||||
defaultMessage: 'API Base Path (optional)',
|
||||
},
|
||||
apiBasePathPlaceholder: {
|
||||
id: 'customProviderForm.apiBasePathPlaceholder',
|
||||
defaultMessage: 'e.g., v1/chat/completions or project_id/v1',
|
||||
},
|
||||
apiBasePathHint: {
|
||||
id: 'customProviderForm.apiBasePathHint',
|
||||
defaultMessage: "Override the default API path. Leave blank to use the provider's default path.",
|
||||
},
|
||||
authentication: {
|
||||
id: 'customProviderForm.authentication',
|
||||
defaultMessage: 'Authentication',
|
||||
},
|
||||
authHint: {
|
||||
id: 'customProviderForm.authHint',
|
||||
defaultMessage: "Local LLMs like Ollama typically don't require an API key.",
|
||||
},
|
||||
requiresApiKey: {
|
||||
id: 'customProviderForm.requiresApiKey',
|
||||
defaultMessage: 'This provider requires an API key',
|
||||
},
|
||||
apiKey: {
|
||||
id: 'customProviderForm.apiKey',
|
||||
defaultMessage: 'API Key',
|
||||
},
|
||||
apiKeyPlaceholderExisting: {
|
||||
id: 'customProviderForm.apiKeyPlaceholderExisting',
|
||||
defaultMessage: 'Leave blank to keep existing key',
|
||||
},
|
||||
apiKeyPlaceholderNew: {
|
||||
id: 'customProviderForm.apiKeyPlaceholderNew',
|
||||
defaultMessage: 'Your API key',
|
||||
},
|
||||
availableModels: {
|
||||
id: 'customProviderForm.availableModels',
|
||||
defaultMessage: 'Available Models (comma-separated)',
|
||||
},
|
||||
modelsPlaceholder: {
|
||||
id: 'customProviderForm.modelsPlaceholder',
|
||||
defaultMessage: 'model-a, model-b, model-c',
|
||||
},
|
||||
toolCalling: {
|
||||
id: 'customProviderForm.toolCalling',
|
||||
defaultMessage: 'Tool calling',
|
||||
},
|
||||
reasoning: {
|
||||
id: 'customProviderForm.reasoning',
|
||||
defaultMessage: 'Reasoning',
|
||||
},
|
||||
attachments: {
|
||||
id: 'customProviderForm.attachments',
|
||||
defaultMessage: 'Attachments',
|
||||
},
|
||||
supportsStreaming: {
|
||||
id: 'customProviderForm.supportsStreaming',
|
||||
defaultMessage: 'Provider supports streaming responses',
|
||||
},
|
||||
customHeaders: {
|
||||
id: 'customProviderForm.customHeaders',
|
||||
defaultMessage: 'Custom Headers',
|
||||
},
|
||||
customHeadersHint: {
|
||||
id: 'customProviderForm.customHeadersHint',
|
||||
defaultMessage:
|
||||
'Add custom HTTP headers to include in requests to the provider. Click the "+" button to add after filling both fields.',
|
||||
},
|
||||
headerNamePlaceholder: {
|
||||
id: 'customProviderForm.headerNamePlaceholder',
|
||||
defaultMessage: 'Header name',
|
||||
},
|
||||
valuePlaceholder: {
|
||||
id: 'customProviderForm.valuePlaceholder',
|
||||
defaultMessage: 'Value',
|
||||
},
|
||||
add: {
|
||||
id: 'customProviderForm.add',
|
||||
defaultMessage: 'Add',
|
||||
},
|
||||
headerBothRequired: {
|
||||
id: 'customProviderForm.headerBothRequired',
|
||||
defaultMessage: 'Both header name and value must be entered',
|
||||
},
|
||||
headerNoSpaces: {
|
||||
id: 'customProviderForm.headerNoSpaces',
|
||||
defaultMessage: 'Header name cannot contain spaces',
|
||||
},
|
||||
headerDuplicate: {
|
||||
id: 'customProviderForm.headerDuplicate',
|
||||
defaultMessage: 'A header with this name already exists',
|
||||
},
|
||||
displayNameRequired: {
|
||||
id: 'customProviderForm.displayNameRequired',
|
||||
defaultMessage: 'Display name is required',
|
||||
},
|
||||
apiUrlRequired: {
|
||||
id: 'customProviderForm.apiUrlRequired',
|
||||
defaultMessage: 'API URL is required',
|
||||
},
|
||||
apiKeyRequired: {
|
||||
id: 'customProviderForm.apiKeyRequired',
|
||||
defaultMessage: 'API key is required',
|
||||
},
|
||||
modelsRequired: {
|
||||
id: 'customProviderForm.modelsRequired',
|
||||
defaultMessage: 'At least one model is required',
|
||||
},
|
||||
submitError: {
|
||||
id: 'customProviderForm.submitError',
|
||||
defaultMessage: 'Failed to save provider. Please check your configuration and try again.',
|
||||
},
|
||||
cannotDeleteActive: {
|
||||
id: 'customProviderForm.cannotDeleteActive',
|
||||
defaultMessage:
|
||||
"You cannot delete this provider while it's currently in use. Please switch to a different model first.",
|
||||
},
|
||||
deleteConfirmation: {
|
||||
id: 'customProviderForm.deleteConfirmation',
|
||||
defaultMessage:
|
||||
'Are you sure you want to delete this custom provider? This will permanently remove the provider and its stored API key. This action cannot be undone.',
|
||||
},
|
||||
confirmDelete: {
|
||||
id: 'customProviderForm.confirmDelete',
|
||||
defaultMessage: 'Confirm Delete',
|
||||
},
|
||||
deleteProvider: {
|
||||
id: 'customProviderForm.deleteProvider',
|
||||
defaultMessage: 'Delete Provider',
|
||||
},
|
||||
updateProvider: {
|
||||
id: 'customProviderForm.updateProvider',
|
||||
defaultMessage: 'Update Provider',
|
||||
},
|
||||
createProvider: {
|
||||
id: 'customProviderForm.createProvider',
|
||||
defaultMessage: 'Create Provider',
|
||||
},
|
||||
});
|
||||
|
||||
type Step = 'choice' | 'catalog' | 'form';
|
||||
|
||||
@@ -27,6 +242,7 @@ export default function CustomProviderForm({
|
||||
initialData,
|
||||
isEditable,
|
||||
}: CustomProviderFormProps) {
|
||||
const intl = useIntl();
|
||||
const [engine, setEngine] = useState('openai_compatible');
|
||||
const [displayName, setDisplayName] = useState('');
|
||||
const [apiUrl, setApiUrl] = useState('');
|
||||
@@ -129,19 +345,19 @@ export default function CustomProviderForm({
|
||||
|
||||
if (keyEmpty || valueEmpty) {
|
||||
setInvalidHeaderFields({ key: keyEmpty, value: valueEmpty });
|
||||
setHeaderValidationError('Both header name and value must be entered');
|
||||
setHeaderValidationError(intl.formatMessage(i18n.headerBothRequired));
|
||||
return;
|
||||
}
|
||||
|
||||
if (keyHasSpaces) {
|
||||
setInvalidHeaderFields({ key: true, value: false });
|
||||
setHeaderValidationError('Header name cannot contain spaces');
|
||||
setHeaderValidationError(intl.formatMessage(i18n.headerNoSpaces));
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDuplicate) {
|
||||
setInvalidHeaderFields({ key: true, value: false });
|
||||
setHeaderValidationError('A header with this name already exists');
|
||||
setHeaderValidationError(intl.formatMessage(i18n.headerDuplicate));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,11 +408,11 @@ export default function CustomProviderForm({
|
||||
setValidationErrors({});
|
||||
|
||||
const errors: Record<string, string> = {};
|
||||
if (!displayName) errors.displayName = 'Display name is required';
|
||||
if (!apiUrl) errors.apiUrl = 'API URL is required';
|
||||
if (!displayName) errors.displayName = intl.formatMessage(i18n.displayNameRequired);
|
||||
if (!apiUrl) errors.apiUrl = intl.formatMessage(i18n.apiUrlRequired);
|
||||
const existingHadAuth = initialData && (initialData.requires_auth ?? true);
|
||||
if (requiresAuth && !apiKey && !existingHadAuth) errors.apiKey = 'API key is required';
|
||||
if (!models) errors.models = 'At least one model is required';
|
||||
if (requiresAuth && !apiKey && !existingHadAuth) errors.apiKey = intl.formatMessage(i18n.apiKeyRequired);
|
||||
if (!models) errors.models = intl.formatMessage(i18n.modelsRequired);
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
setValidationErrors(errors);
|
||||
@@ -245,7 +461,7 @@ export default function CustomProviderForm({
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to save custom provider:', error);
|
||||
setSubmitError('Failed to save provider. Please check your configuration and try again.');
|
||||
setSubmitError(intl.formatMessage(i18n.submitError));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -266,7 +482,7 @@ export default function CustomProviderForm({
|
||||
if (step === 'choice') {
|
||||
return (
|
||||
<div className="mt-4 space-y-3">
|
||||
<p className="text-sm text-textSubtle">Choose how you'd like to set up your provider.</p>
|
||||
<p className="text-sm text-textSubtle">{intl.formatMessage(i18n.chooseSetup)}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep('catalog')}
|
||||
@@ -275,9 +491,9 @@ export default function CustomProviderForm({
|
||||
<div className="flex items-center gap-3">
|
||||
<Search className="w-5 h-5 text-primary flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-textStandard">Start from a provider template</div>
|
||||
<div className="font-medium text-textStandard">{intl.formatMessage(i18n.startFromTemplate)}</div>
|
||||
<div className="text-sm text-textSubtle mt-0.5">
|
||||
Pick a known provider and we'll auto-fill the configuration
|
||||
{intl.formatMessage(i18n.startFromTemplateDesc)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -290,16 +506,16 @@ export default function CustomProviderForm({
|
||||
<div className="flex items-center gap-3">
|
||||
<Settings className="w-5 h-5 text-textSubtle flex-shrink-0" />
|
||||
<div>
|
||||
<div className="font-medium text-textStandard">Configure manually</div>
|
||||
<div className="font-medium text-textStandard">{intl.formatMessage(i18n.configureManually)}</div>
|
||||
<div className="text-sm text-textSubtle mt-0.5">
|
||||
Enter all provider details yourself
|
||||
{intl.formatMessage(i18n.configureManuallyDesc)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<div className="flex justify-end pt-2">
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -313,10 +529,10 @@ export default function CustomProviderForm({
|
||||
<ProviderCatalogPicker onSelect={handleTemplateSelect} onCancel={onCancel} embedded />
|
||||
<div className="flex justify-between pt-4">
|
||||
<Button type="button" variant="ghost" onClick={() => setStep('choice')}>
|
||||
← Back
|
||||
{intl.formatMessage(i18n.back)}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -332,7 +548,7 @@ export default function CustomProviderForm({
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm">
|
||||
<div className="font-medium text-textStandard">
|
||||
Using template: {selectedTemplate.name}
|
||||
{intl.formatMessage(i18n.usingTemplate, { name: selectedTemplate.name })}
|
||||
</div>
|
||||
<div className="text-textSubtle mt-1">{selectedTemplate.api_url}</div>
|
||||
</div>
|
||||
@@ -344,7 +560,7 @@ export default function CustomProviderForm({
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline text-sm flex items-center gap-1"
|
||||
>
|
||||
Docs <ExternalLink className="w-3 h-3" />
|
||||
{intl.formatMessage(i18n.docs)} <ExternalLink className="w-3 h-3" />
|
||||
</a>
|
||||
)}
|
||||
<Button
|
||||
@@ -354,7 +570,7 @@ export default function CustomProviderForm({
|
||||
onClick={handleClearTemplate}
|
||||
className="text-textSubtle hover:text-textStandard"
|
||||
>
|
||||
Clear
|
||||
{intl.formatMessage(i18n.clear)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -364,7 +580,7 @@ export default function CustomProviderForm({
|
||||
{/* Back to choice (create without template only) */}
|
||||
{!initialData && !selectedTemplate && (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={() => setStep('choice')}>
|
||||
← Back
|
||||
{intl.formatMessage(i18n.back)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -375,7 +591,7 @@ export default function CustomProviderForm({
|
||||
htmlFor="provider-select"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Provider Type
|
||||
{intl.formatMessage(i18n.providerType)}
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Select
|
||||
@@ -383,18 +599,18 @@ export default function CustomProviderForm({
|
||||
aria-invalid={!!validationErrors.providerType}
|
||||
aria-describedby={validationErrors.providerType ? 'provider-select-error' : undefined}
|
||||
options={[
|
||||
{ value: 'openai_compatible', label: 'OpenAI Compatible' },
|
||||
{ value: 'anthropic_compatible', label: 'Anthropic Compatible' },
|
||||
{ value: 'ollama_compatible', label: 'Ollama Compatible' },
|
||||
{ value: 'openai_compatible', label: intl.formatMessage(i18n.openaiCompatible) },
|
||||
{ value: 'anthropic_compatible', label: intl.formatMessage(i18n.anthropicCompatible) },
|
||||
{ value: 'ollama_compatible', label: intl.formatMessage(i18n.ollamaCompatible) },
|
||||
]}
|
||||
value={{
|
||||
value: engine,
|
||||
label:
|
||||
engine === 'openai_compatible'
|
||||
? 'OpenAI Compatible'
|
||||
? intl.formatMessage(i18n.openaiCompatible)
|
||||
: engine === 'anthropic_compatible'
|
||||
? 'Anthropic Compatible'
|
||||
: 'Ollama Compatible',
|
||||
? intl.formatMessage(i18n.anthropicCompatible)
|
||||
: intl.formatMessage(i18n.ollamaCompatible),
|
||||
}}
|
||||
onChange={(option: unknown) => {
|
||||
const selectedOption = option as { value: string; label: string } | null;
|
||||
@@ -417,14 +633,14 @@ export default function CustomProviderForm({
|
||||
htmlFor="display-name"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Display Name
|
||||
{intl.formatMessage(i18n.displayName)}
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="display-name"
|
||||
value={displayName}
|
||||
onChange={(e) => setDisplayName(e.target.value)}
|
||||
placeholder="Your Provider Name"
|
||||
placeholder={intl.formatMessage(i18n.displayNamePlaceholder)}
|
||||
aria-invalid={!!validationErrors.displayName}
|
||||
aria-describedby={validationErrors.displayName ? 'display-name-error' : undefined}
|
||||
className={validationErrors.displayName ? 'border-red-500' : ''}
|
||||
@@ -444,14 +660,14 @@ export default function CustomProviderForm({
|
||||
htmlFor="api-url"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API URL
|
||||
{intl.formatMessage(i18n.apiUrl)}
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="api-url"
|
||||
value={apiUrl}
|
||||
onChange={(e) => setApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
placeholder={intl.formatMessage(i18n.apiUrlPlaceholder)}
|
||||
aria-invalid={!!validationErrors.apiUrl}
|
||||
aria-describedby={validationErrors.apiUrl ? 'api-url-error' : undefined}
|
||||
className={validationErrors.apiUrl ? 'border-red-500' : ''}
|
||||
@@ -471,25 +687,25 @@ export default function CustomProviderForm({
|
||||
htmlFor="base-path"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API Base Path (optional)
|
||||
{intl.formatMessage(i18n.apiBasePath)}
|
||||
</label>
|
||||
<Input
|
||||
id="base-path"
|
||||
value={basePath}
|
||||
onChange={(e) => setBasePath(e.target.value)}
|
||||
placeholder="e.g., v1/chat/completions or project_id/v1"
|
||||
placeholder={intl.formatMessage(i18n.apiBasePathPlaceholder)}
|
||||
/>
|
||||
<p className="text-xs text-textSubtle mt-1">
|
||||
Override the default API path. Leave blank to use the provider's default path.
|
||||
{intl.formatMessage(i18n.apiBasePathHint)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Authentication */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">Authentication</label>
|
||||
<label className="block text-sm font-medium text-text-primary mb-2">{intl.formatMessage(i18n.authentication)}</label>
|
||||
<p className="text-sm text-text-secondary mb-3">
|
||||
Local LLMs like Ollama typically don't require an API key.
|
||||
{intl.formatMessage(i18n.authHint)}
|
||||
</p>
|
||||
<div className="flex items-center space-x-2">
|
||||
<input
|
||||
@@ -500,7 +716,7 @@ export default function CustomProviderForm({
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="requires-auth" className="text-sm text-text-secondary">
|
||||
This provider requires an API key
|
||||
{intl.formatMessage(i18n.requiresApiKey)}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -510,7 +726,7 @@ export default function CustomProviderForm({
|
||||
htmlFor="api-key"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
API Key
|
||||
{intl.formatMessage(i18n.apiKey)}
|
||||
{selectedTemplate?.env_var && (
|
||||
<span className="text-textSubtle ml-1 font-normal">
|
||||
({selectedTemplate.env_var})
|
||||
@@ -523,7 +739,7 @@ export default function CustomProviderForm({
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder={initialData ? 'Leave blank to keep existing key' : 'Your API key'}
|
||||
placeholder={initialData ? intl.formatMessage(i18n.apiKeyPlaceholderExisting) : intl.formatMessage(i18n.apiKeyPlaceholderNew)}
|
||||
aria-invalid={!!validationErrors.apiKey}
|
||||
aria-describedby={validationErrors.apiKey ? 'api-key-error' : undefined}
|
||||
className={validationErrors.apiKey ? 'border-red-500' : ''}
|
||||
@@ -544,14 +760,14 @@ export default function CustomProviderForm({
|
||||
htmlFor="available-models"
|
||||
className="flex items-center text-sm font-medium text-text-primary mb-2"
|
||||
>
|
||||
Available Models (comma-separated)
|
||||
{intl.formatMessage(i18n.availableModels)}
|
||||
<span className="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="available-models"
|
||||
value={models}
|
||||
onChange={(e) => setModels(e.target.value)}
|
||||
placeholder="model-a, model-b, model-c"
|
||||
placeholder={intl.formatMessage(i18n.modelsPlaceholder)}
|
||||
aria-invalid={!!validationErrors.models}
|
||||
aria-describedby={validationErrors.models ? 'available-models-error' : undefined}
|
||||
className={validationErrors.models ? 'border-red-500' : ''}
|
||||
@@ -566,17 +782,17 @@ export default function CustomProviderForm({
|
||||
<div className="flex gap-2 mt-2">
|
||||
{templateModelCapabilities.tool_call && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300">
|
||||
Tool calling
|
||||
{intl.formatMessage(i18n.toolCalling)}
|
||||
</span>
|
||||
)}
|
||||
{templateModelCapabilities.reasoning && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300">
|
||||
Reasoning
|
||||
{intl.formatMessage(i18n.reasoning)}
|
||||
</span>
|
||||
)}
|
||||
{templateModelCapabilities.attachment && (
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300">
|
||||
Attachments
|
||||
{intl.formatMessage(i18n.attachments)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -595,7 +811,7 @@ export default function CustomProviderForm({
|
||||
className="rounded border-border-primary"
|
||||
/>
|
||||
<label htmlFor="supports-streaming" className="text-sm text-text-secondary">
|
||||
Provider supports streaming responses
|
||||
{intl.formatMessage(i18n.supportsStreaming)}
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
@@ -603,10 +819,9 @@ export default function CustomProviderForm({
|
||||
{/* Custom headers */}
|
||||
{isEditable && (
|
||||
<div>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">Custom Headers</label>
|
||||
<label className="text-sm font-medium text-textStandard mb-2 block">{intl.formatMessage(i18n.customHeaders)}</label>
|
||||
<p className="text-xs text-textSubtle mb-4">
|
||||
Add custom HTTP headers to include in requests to the provider. Click the "+" button to
|
||||
add after filling both fields.
|
||||
{intl.formatMessage(i18n.customHeadersHint)}
|
||||
</p>
|
||||
<div className="grid grid-cols-[1fr_1fr_auto] gap-2 items-center">
|
||||
{headers.map((header, index) => (
|
||||
@@ -614,13 +829,13 @@ export default function CustomProviderForm({
|
||||
<Input
|
||||
value={header.key}
|
||||
onChange={(e) => handleHeaderChange(index, 'key', e.target.value)}
|
||||
placeholder="Header name"
|
||||
placeholder={intl.formatMessage(i18n.headerNamePlaceholder)}
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Input
|
||||
value={header.value}
|
||||
onChange={(e) => handleHeaderChange(index, 'value', e.target.value)}
|
||||
placeholder="Value"
|
||||
placeholder={intl.formatMessage(i18n.valuePlaceholder)}
|
||||
className="w-full text-textStandard border-borderSubtle hover:border-borderStandard"
|
||||
/>
|
||||
<Button
|
||||
@@ -654,7 +869,7 @@ export default function CustomProviderForm({
|
||||
clearHeaderValidation();
|
||||
}}
|
||||
onKeyDown={handleHeaderKeyDown}
|
||||
placeholder="Value"
|
||||
placeholder={intl.formatMessage(i18n.valuePlaceholder)}
|
||||
className={cn(
|
||||
'w-full text-textStandard border-borderSubtle hover:border-borderStandard',
|
||||
invalidHeaderFields.value && 'border-red-500 focus:border-red-500'
|
||||
@@ -666,7 +881,7 @@ export default function CustomProviderForm({
|
||||
type="button"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-primary border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
>
|
||||
<Plus /> Add
|
||||
<Plus /> {intl.formatMessage(i18n.add)}
|
||||
</Button>
|
||||
</div>
|
||||
{headerValidationError && (
|
||||
@@ -686,16 +901,14 @@ export default function CustomProviderForm({
|
||||
<p className="text-yellow-500 text-sm flex items-start">
|
||||
<AlertTriangle className="h-4 w-4 mr-2 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
You cannot delete this provider while it's currently in use. Please switch to a
|
||||
different model first.
|
||||
{intl.formatMessage(i18n.cannotDeleteActive)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-4 py-3 bg-red-900/20 border border-red-500/30 rounded">
|
||||
<p className="text-red-400 text-sm">
|
||||
Are you sure you want to delete this custom provider? This will permanently remove
|
||||
the provider and its stored API key. This action cannot be undone.
|
||||
{intl.formatMessage(i18n.deleteConfirmation)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -705,12 +918,12 @@ export default function CustomProviderForm({
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteConfirmation(false)}
|
||||
>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
{!isActiveProvider && (
|
||||
<Button type="button" variant="destructive" onClick={onDelete}>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Confirm Delete
|
||||
{intl.formatMessage(i18n.confirmDelete)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -725,13 +938,13 @@ export default function CustomProviderForm({
|
||||
onClick={() => setShowDeleteConfirmation(true)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Delete Provider
|
||||
{intl.formatMessage(i18n.deleteProvider)}
|
||||
</Button>
|
||||
)}
|
||||
<Button type="button" variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
{intl.formatMessage(i18n.cancel)}
|
||||
</Button>
|
||||
<Button type="submit">{initialData ? 'Update Provider' : 'Create Provider'}</Button>
|
||||
<Button type="submit">{initialData ? intl.formatMessage(i18n.updateProvider) : intl.formatMessage(i18n.createProvider)}</Button>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
+56
-9
@@ -3,6 +3,50 @@ import { Input } from '../../../../../ui/input';
|
||||
import { useConfig } from '../../../../../ConfigContext';
|
||||
import { ProviderDetails, ConfigKey } from '../../../../../../api';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../../../../ui/collapsible';
|
||||
import { defineMessages, useIntl } from '../../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
loadingConfig: {
|
||||
id: 'defaultProviderSetupForm.loadingConfig',
|
||||
defaultMessage: 'Loading configuration values...',
|
||||
},
|
||||
noConfigParameters: {
|
||||
id: 'defaultProviderSetupForm.noConfigParameters',
|
||||
defaultMessage: 'No configuration parameters for this provider.',
|
||||
},
|
||||
apiKeyPlaceholder: {
|
||||
id: 'defaultProviderSetupForm.apiKeyPlaceholder',
|
||||
defaultMessage: 'Your API key',
|
||||
},
|
||||
apiHostPlaceholder: {
|
||||
id: 'defaultProviderSetupForm.apiHostPlaceholder',
|
||||
defaultMessage: 'https://api.example.com',
|
||||
},
|
||||
modelsPlaceholder: {
|
||||
id: 'defaultProviderSetupForm.modelsPlaceholder',
|
||||
defaultMessage: 'model-a, model-b',
|
||||
},
|
||||
apiKeyLabel: {
|
||||
id: 'defaultProviderSetupForm.apiKeyLabel',
|
||||
defaultMessage: 'API Key',
|
||||
},
|
||||
apiHostLabel: {
|
||||
id: 'defaultProviderSetupForm.apiHostLabel',
|
||||
defaultMessage: 'API Host',
|
||||
},
|
||||
modelsLabel: {
|
||||
id: 'defaultProviderSetupForm.modelsLabel',
|
||||
defaultMessage: 'Models',
|
||||
},
|
||||
showOptions: {
|
||||
id: 'defaultProviderSetupForm.showOptions',
|
||||
defaultMessage: 'Show {count} options',
|
||||
},
|
||||
hideOptions: {
|
||||
id: 'defaultProviderSetupForm.hideOptions',
|
||||
defaultMessage: 'Hide {count} options',
|
||||
},
|
||||
});
|
||||
|
||||
type ValidationErrors = Record<string, string>;
|
||||
|
||||
@@ -47,6 +91,7 @@ export default function DefaultProviderSetupForm({
|
||||
() => provider.metadata.config_keys || [],
|
||||
[provider.metadata.config_keys]
|
||||
);
|
||||
const intl = useIntl();
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [optionalExpanded, setOptionalExpanded] = useState(false);
|
||||
const { read } = useConfig();
|
||||
@@ -94,9 +139,9 @@ export default function DefaultProviderSetupForm({
|
||||
}
|
||||
|
||||
const name = parameter.name.toLowerCase();
|
||||
if (name.includes('api_key')) return 'Your API key';
|
||||
if (name.includes('api_url') || name.includes('host')) return 'https://api.example.com';
|
||||
if (name.includes('models')) return 'model-a, model-b';
|
||||
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyPlaceholder);
|
||||
if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostPlaceholder);
|
||||
if (name.includes('models')) return intl.formatMessage(i18n.modelsPlaceholder);
|
||||
|
||||
return parameter.name
|
||||
.replace(/_/g, ' ')
|
||||
@@ -106,9 +151,9 @@ export default function DefaultProviderSetupForm({
|
||||
|
||||
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';
|
||||
if (name.includes('api_key')) return intl.formatMessage(i18n.apiKeyLabel);
|
||||
if (name.includes('api_url') || name.includes('host')) return intl.formatMessage(i18n.apiHostLabel);
|
||||
if (name.includes('models')) return intl.formatMessage(i18n.modelsLabel);
|
||||
|
||||
let parameter_name = parameter.name.toUpperCase();
|
||||
if (parameter_name.startsWith(provider.name.toUpperCase().replace('-', '_'))) {
|
||||
@@ -124,7 +169,7 @@ export default function DefaultProviderSetupForm({
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-center py-4">Loading configuration values...</div>;
|
||||
return <div className="text-center py-4">{intl.formatMessage(i18n.loadingConfig)}</div>;
|
||||
}
|
||||
|
||||
function getRenderValue(parameter: ConfigKey): string {
|
||||
@@ -244,13 +289,15 @@ export default function DefaultProviderSetupForm({
|
||||
belowFoldParameters = [];
|
||||
}
|
||||
|
||||
const expandCtaText = `${optionalExpanded ? 'Hide' : 'Show'} ${belowFoldParameters.length} options `;
|
||||
const expandCtaText = optionalExpanded
|
||||
? intl.formatMessage(i18n.hideOptions, { count: belowFoldParameters.length })
|
||||
: intl.formatMessage(i18n.showOptions, { count: belowFoldParameters.length });
|
||||
|
||||
return (
|
||||
<div className="mt-4 space-y-4">
|
||||
{aboveFoldParameters.length === 0 && belowFoldParameters.length === 0 ? (
|
||||
<div className="text-center text-gray-500">
|
||||
No configuration parameters for this provider.
|
||||
{intl.formatMessage(i18n.noConfigParameters)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo } from 'react';
|
||||
import { GreenCheckButton } from './buttons/CardButtons';
|
||||
import { ConfiguredProviderTooltipMessage, ProviderDescription } from './utils/StringUtils';
|
||||
import { useIntl } from '../../../../i18n';
|
||||
|
||||
interface CardHeaderProps {
|
||||
name: string;
|
||||
@@ -21,12 +22,13 @@ interface ProviderNameAndStatusProps {
|
||||
}
|
||||
|
||||
const ProviderNameAndStatus = memo(({ name, isConfigured }: ProviderNameAndStatusProps) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<CardTitle name={name} />
|
||||
|
||||
{/* Configured state: Green check */}
|
||||
{isConfigured && <GreenCheckButton tooltip={ConfiguredProviderTooltipMessage(name)} />}
|
||||
{isConfigured && <GreenCheckButton tooltip={ConfiguredProviderTooltipMessage(intl, name)} />}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,18 @@ import CardHeader from './CardHeader';
|
||||
import CardBody from './CardBody';
|
||||
import DefaultCardButtons from './buttons/DefaultCardButtons';
|
||||
import { ProviderDetails, ProviderMetadata } from '../../../../api';
|
||||
import { defineMessages, useIntl } from '../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
noMetadata: {
|
||||
id: 'providerCard.noMetadata',
|
||||
defaultMessage: 'ProviderCard error: No metadata provided',
|
||||
},
|
||||
unknownProvider: {
|
||||
id: 'providerCard.unknownProvider',
|
||||
defaultMessage: 'Unknown Provider',
|
||||
},
|
||||
});
|
||||
|
||||
type ProviderCardProps = {
|
||||
provider: ProviderDetails;
|
||||
@@ -18,6 +30,7 @@ export const ProviderCard = function ProviderCard({
|
||||
onLaunch,
|
||||
isOnboarding,
|
||||
}: ProviderCardProps) {
|
||||
const intl = useIntl();
|
||||
// Safely access metadata with null checks
|
||||
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
|
||||
|
||||
@@ -25,7 +38,7 @@ export const ProviderCard = function ProviderCard({
|
||||
const metadata = useMemo(() => providerMetadata, [providerMetadata]);
|
||||
|
||||
if (!metadata) {
|
||||
return <div>ProviderCard error: No metadata provided</div>;
|
||||
return <div>{intl.formatMessage(i18n.noMetadata)}</div>;
|
||||
}
|
||||
|
||||
const handleCardClick = () => {
|
||||
@@ -41,7 +54,7 @@ export const ProviderCard = function ProviderCard({
|
||||
onClick={handleCardClick}
|
||||
header={
|
||||
<CardHeader
|
||||
name={metadata.display_name || provider?.name || 'Unknown Provider'}
|
||||
name={metadata.display_name || provider?.name || intl.formatMessage(i18n.unknownProvider)}
|
||||
description={metadata.description || ''}
|
||||
isConfigured={provider?.is_configured || false}
|
||||
/>
|
||||
|
||||
@@ -3,6 +3,18 @@ import { Button } from '../../../../ui/button';
|
||||
import clsx from 'clsx';
|
||||
import { TooltipWrapper } from './TooltipWrapper';
|
||||
import { Check, Rocket, Sliders } from 'lucide-react';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
configure: {
|
||||
id: 'cardButtons.configure',
|
||||
defaultMessage: 'Configure',
|
||||
},
|
||||
launch: {
|
||||
id: 'cardButtons.launch',
|
||||
defaultMessage: 'Launch',
|
||||
},
|
||||
});
|
||||
|
||||
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
|
||||
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
|
||||
@@ -65,12 +77,13 @@ export function GreenCheckButton({ tooltip, className = '', ...props }: ActionBu
|
||||
}
|
||||
|
||||
export function ConfigureSettingsButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ActionButton
|
||||
icon={Sliders}
|
||||
tooltip={tooltip}
|
||||
variant="outline"
|
||||
text={'Configure'}
|
||||
text={intl.formatMessage(i18n.configure)}
|
||||
iconClassName="rotate-90"
|
||||
className={clsx('cursor-pointer', className)}
|
||||
{...props}
|
||||
@@ -79,13 +92,14 @@ export function ConfigureSettingsButton({ tooltip, className, ...props }: Action
|
||||
}
|
||||
|
||||
export function RocketButton({ tooltip, className, ...props }: ActionButtonProps) {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ActionButton
|
||||
data-testid="provider-launch-button"
|
||||
icon={Rocket}
|
||||
tooltip={tooltip}
|
||||
variant="outline"
|
||||
text={'Launch'}
|
||||
text={intl.formatMessage(i18n.launch)}
|
||||
className={clsx('cursor-pointer', className)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
+26
-16
@@ -1,5 +1,25 @@
|
||||
import { ConfigureSettingsButton, RocketButton } from './CardButtons';
|
||||
import { ProviderDetails } from '../../../../../api';
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
|
||||
const i18n = defineMessages({
|
||||
configureSettings: {
|
||||
id: 'defaultCardButtons.configureSettings',
|
||||
defaultMessage: 'Configure {name} settings',
|
||||
},
|
||||
editSettings: {
|
||||
id: 'defaultCardButtons.editSettings',
|
||||
defaultMessage: 'Edit {name} settings',
|
||||
},
|
||||
deleteSettings: {
|
||||
id: 'defaultCardButtons.deleteSettings',
|
||||
defaultMessage: 'Delete {name} settings',
|
||||
},
|
||||
getStarted: {
|
||||
id: 'defaultCardButtons.getStarted',
|
||||
defaultMessage: 'Get started with goose!',
|
||||
},
|
||||
});
|
||||
|
||||
// can define other optional callbacks as needed
|
||||
interface CardButtonsProps {
|
||||
@@ -9,31 +29,21 @@ interface CardButtonsProps {
|
||||
onLaunch: (provider: ProviderDetails) => void;
|
||||
}
|
||||
|
||||
function getDefaultTooltipMessages(name: string, actionType: string) {
|
||||
switch (actionType) {
|
||||
case 'add':
|
||||
return `Configure ${name} settings`;
|
||||
case 'edit':
|
||||
return `Edit ${name} settings`;
|
||||
case 'delete':
|
||||
return `Delete ${name} settings`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function DefaultCardButtons({
|
||||
provider,
|
||||
isOnboardingPage,
|
||||
onLaunch,
|
||||
onConfigure,
|
||||
}: CardButtonsProps) {
|
||||
const intl = useIntl();
|
||||
const name = provider.metadata.display_name;
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*Set up an unconfigured provider */}
|
||||
{!provider.is_configured && (
|
||||
<ConfigureSettingsButton
|
||||
tooltip={getDefaultTooltipMessages(provider.metadata.display_name, 'add')}
|
||||
tooltip={intl.formatMessage(i18n.configureSettings, { name })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfigure(provider);
|
||||
@@ -43,7 +53,7 @@ export default function DefaultCardButtons({
|
||||
{/*show edit tooltip instead when hovering over button for configured providers*/}
|
||||
{provider.is_configured && !isOnboardingPage && (
|
||||
<ConfigureSettingsButton
|
||||
tooltip={getDefaultTooltipMessages(provider.metadata.display_name, 'edit')}
|
||||
tooltip={intl.formatMessage(i18n.editSettings, { name })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfigure(provider);
|
||||
@@ -53,7 +63,7 @@ export default function DefaultCardButtons({
|
||||
{/*show Launch button for configured providers on onboarding page*/}
|
||||
{provider.is_configured && isOnboardingPage && (
|
||||
<RocketButton
|
||||
tooltip={'Get started with goose!'}
|
||||
tooltip={intl.formatMessage(i18n.getStarted)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onLaunch(provider);
|
||||
|
||||
@@ -1,23 +1,46 @@
|
||||
import { defineMessages, useIntl } from '../../../../../i18n';
|
||||
import type { IntlShape } from 'react-intl';
|
||||
|
||||
const i18n = defineMessages({
|
||||
ollamaNotConfiguredPrefix: {
|
||||
id: 'stringUtils.ollamaNotConfiguredPrefix',
|
||||
defaultMessage: 'To use, either the',
|
||||
},
|
||||
ollamaApp: {
|
||||
id: 'stringUtils.ollamaApp',
|
||||
defaultMessage: 'Ollama app',
|
||||
},
|
||||
ollamaNotConfiguredSuffix: {
|
||||
id: 'stringUtils.ollamaNotConfiguredSuffix',
|
||||
defaultMessage: 'must be installed on your machine and open, or you must enter a value for OLLAMA_HOST.',
|
||||
},
|
||||
configuredProvider: {
|
||||
id: 'stringUtils.configuredProvider',
|
||||
defaultMessage: '{name} provider is configured',
|
||||
},
|
||||
});
|
||||
|
||||
// Functions for string / string-based element creation (e.g. tooltips for each provider, descriptions, etc)
|
||||
export function OllamaNotConfiguredTooltipMessage() {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<p>
|
||||
To use, either the{' '}
|
||||
{intl.formatMessage(i18n.ollamaNotConfiguredPrefix)}{' '}
|
||||
<a
|
||||
href="https://ollama.com/download"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-600 underline hover:text-blue-800"
|
||||
>
|
||||
Ollama app
|
||||
{intl.formatMessage(i18n.ollamaApp)}
|
||||
</a>{' '}
|
||||
must be installed on your machine and open, or you must enter a value for OLLAMA_HOST.
|
||||
{intl.formatMessage(i18n.ollamaNotConfiguredSuffix)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
export function ConfiguredProviderTooltipMessage(name: string) {
|
||||
return `${name} provider is configured`;
|
||||
export function ConfiguredProviderTooltipMessage(intl: IntlShape, name: string) {
|
||||
return intl.formatMessage(i18n.configuredProvider, { name });
|
||||
}
|
||||
|
||||
interface ProviderDescriptionProps {
|
||||
|
||||
Reference in New Issue
Block a user