From 48415db9906aa5c96f9077c5f8e29ad3055d0bed Mon Sep 17 00:00:00 2001 From: paisley <8197966+su8su@users.noreply.github.com> Date: Thu, 16 Apr 2026 19:09:15 +0800 Subject: [PATCH] feat(setup): remove AI provider configuration step from onboarding wizard (#863) --- src/pages/Setup/index.tsx | 1020 +------------------------------------ 1 file changed, 6 insertions(+), 1014 deletions(-) diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx index 33c8e29f6..9c7ea516d 100644 --- a/src/pages/Setup/index.tsx +++ b/src/pages/Setup/index.tsx @@ -7,24 +7,18 @@ import { useNavigate } from 'react-router-dom'; import { motion, AnimatePresence } from 'framer-motion'; import { Check, - ChevronDown, ChevronLeft, ChevronRight, Loader2, AlertCircle, - Eye, - EyeOff, RefreshCw, CheckCircle2, XCircle, ExternalLink, - Copy, } from 'lucide-react'; import { TitleBar } from '@/components/layout/TitleBar'; import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; import { Tooltip, TooltipTrigger, TooltipContent } from '@/components/ui/tooltip'; -import { Label } from '@/components/ui/label'; import { cn } from '@/lib/utils'; import { useGatewayStore } from '@/stores/gateway'; import { useSettingsStore } from '@/stores/settings'; @@ -34,7 +28,7 @@ import { SUPPORTED_LANGUAGES } from '@/i18n'; import { toast } from 'sonner'; import { invokeIpc } from '@/lib/api-client'; import { hostApiFetch } from '@/lib/host-api'; -import { subscribeHostEvent } from '@/lib/host-events'; + interface SetupStep { id: string; title: string; @@ -44,9 +38,8 @@ interface SetupStep { const STEP = { WELCOME: 0, RUNTIME: 1, - PROVIDER: 2, - INSTALLING: 3, - COMPLETE: 4, + INSTALLING: 2, + COMPLETE: 3, } as const; const getSteps = (t: TFunction): SetupStep[] => [ @@ -60,11 +53,6 @@ const getSteps = (t: TFunction): SetupStep[] => [ title: t('steps.runtime.title'), description: t('steps.runtime.description'), }, - { - id: 'provider', - title: t('steps.provider.title'), - description: t('steps.provider.description'), - }, { id: 'installing', title: t('steps.installing.title'), @@ -92,39 +80,8 @@ const getDefaultSkills = (t: TFunction): DefaultSkill[] => [ { id: 'terminal', name: t('defaultSkills.terminal.name'), description: t('defaultSkills.terminal.description') }, ]; -import { - SETUP_PROVIDERS, - type ProviderAccount, - type ProviderType, - type ProviderTypeInfo, - getProviderDocsUrl, - getProviderIconUrl, - normalizeProviderApiKeyInput, - resolveProviderApiKeyForSave, - resolveProviderModelForSave, - shouldInvertInDark, - shouldShowProviderModelId, -} from '@/lib/providers'; -import { - buildProviderAccountId, - fetchProviderSnapshot, - hasConfiguredCredentials, - pickPreferredAccount, -} from '@/lib/provider-accounts'; import clawxIcon from '@/assets/logo.svg'; -// Use the shared provider registry for setup providers -const providers = SETUP_PROVIDERS; - -function getProtocolBaseUrlPlaceholder( - apiProtocol: ProviderAccount['apiProtocol'], -): string { - if (apiProtocol === 'anthropic-messages') { - return 'https://api.example.com/anthropic'; - } - return 'https://api.example.com/v1'; -} - // NOTE: Channel types moved to Settings > Channels page // NOTE: Skill bundles moved to Settings > Skills page - auto-install essential skills during setup @@ -134,9 +91,6 @@ export function Setup() { const [currentStep, setCurrentStep] = useState(STEP.WELCOME); // Setup state - const [selectedProvider, setSelectedProvider] = useState(null); - const [providerConfigured, setProviderConfigured] = useState(false); - const [apiKey, setApiKey] = useState(''); // Installation state for the Installing step const [installedSkills, setInstalledSkills] = useState([]); // Runtime check status @@ -159,8 +113,6 @@ export function Setup() { return true; case STEP.RUNTIME: return runtimeChecksPassed; - case STEP.PROVIDER: - return providerConfigured; case STEP.INSTALLING: return false; // Cannot manually proceed, auto-proceeds when done case STEP.COMPLETE: @@ -168,7 +120,7 @@ export function Setup() { default: return true; } - }, [safeStepIndex, providerConfigured, runtimeChecksPassed]); + }, [safeStepIndex, runtimeChecksPassed]); const handleNext = async () => { if (isLastStep) { @@ -256,16 +208,6 @@ export function Setup() {
{safeStepIndex === STEP.WELCOME && } {safeStepIndex === STEP.RUNTIME && } - {safeStepIndex === STEP.PROVIDER && ( - - )} {safeStepIndex === STEP.INSTALLING && ( )} @@ -698,949 +639,8 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) { ); } -interface ProviderContentProps { - providers: ProviderTypeInfo[]; - selectedProvider: string | null; - onSelectProvider: (id: string | null) => void; - apiKey: string; - onApiKeyChange: (key: string) => void; - onConfiguredChange: (configured: boolean) => void; -} +// NOTE: ProviderContent component removed - configure providers via Settings > AI Providers -function ProviderContent({ - providers, - selectedProvider, - onSelectProvider, - apiKey, - onApiKeyChange, - onConfiguredChange, -}: ProviderContentProps) { - const { t, i18n } = useTranslation(['setup', 'settings']); - const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked); - const [showKey, setShowKey] = useState(false); - const [validating, setValidating] = useState(false); - const [keyValid, setKeyValid] = useState(null); - const [selectedAccountId, setSelectedAccountId] = useState(null); - const [baseUrl, setBaseUrl] = useState(''); - const [modelId, setModelId] = useState(''); - const [apiProtocol, setApiProtocol] = useState('openai-completions'); - const [providerMenuOpen, setProviderMenuOpen] = useState(false); - const providerMenuRef = useRef(null); - - const [authMode, setAuthMode] = useState<'oauth' | 'apikey'>('oauth'); - const [arkMode, setArkMode] = useState<'apikey' | 'codeplan'>('apikey'); - - // OAuth Flow State - const [oauthFlowing, setOauthFlowing] = useState(false); - const [oauthData, setOauthData] = useState<{ - mode: 'device'; - verificationUri: string; - userCode: string; - expiresIn: number; - } | { - mode: 'manual'; - authorizationUrl: string; - message?: string; - } | null>(null); - const [manualCodeInput, setManualCodeInput] = useState(''); - const [oauthError, setOauthError] = useState(null); - const pendingOAuthRef = useRef<{ accountId: string; label: string } | null>(null); - - // Manage OAuth events - useEffect(() => { - const handleCode = (data: unknown) => { - const payload = data as Record; - if (payload?.mode === 'manual') { - setOauthData({ - mode: 'manual', - authorizationUrl: String(payload.authorizationUrl || ''), - message: typeof payload.message === 'string' ? payload.message : undefined, - }); - } else { - setOauthData({ - mode: 'device', - verificationUri: String(payload.verificationUri || ''), - userCode: String(payload.userCode || ''), - expiresIn: Number(payload.expiresIn || 300), - }); - } - setOauthError(null); - }; - - const handleSuccess = async (data: unknown) => { - setOauthFlowing(false); - setOauthData(null); - setManualCodeInput(''); - setKeyValid(true); - - const payload = (data as { accountId?: string } | undefined) || undefined; - const accountId = payload?.accountId || pendingOAuthRef.current?.accountId; - - if (accountId) { - try { - await hostApiFetch('/api/provider-accounts/default', { - method: 'PUT', - body: JSON.stringify({ accountId }), - }); - setSelectedAccountId(accountId); - } catch (error) { - console.error('Failed to set default provider account:', error); - } - } - - pendingOAuthRef.current = null; - onConfiguredChange(true); - toast.success(t('provider.valid')); - }; - - const handleError = (data: unknown) => { - setOauthError((data as { message: string }).message); - setOauthData(null); - pendingOAuthRef.current = null; - }; - - const offCode = subscribeHostEvent('oauth:code', handleCode); - const offSuccess = subscribeHostEvent('oauth:success', handleSuccess); - const offError = subscribeHostEvent('oauth:error', handleError); - - return () => { - offCode(); - offSuccess(); - offError(); - }; - }, [onConfiguredChange, t]); - - const handleStartOAuth = async () => { - if (!selectedProvider) return; - - try { - const snapshot = await fetchProviderSnapshot(); - const existingVendorIds = new Set(snapshot.accounts.map((account) => account.vendorId)); - if (selectedProvider === 'minimax-portal' && existingVendorIds.has('minimax-portal-cn')) { - toast.error(t('settings:aiProviders.toast.minimaxConflict')); - return; - } - if (selectedProvider === 'minimax-portal-cn' && existingVendorIds.has('minimax-portal')) { - toast.error(t('settings:aiProviders.toast.minimaxConflict')); - return; - } - } catch { - // ignore check failure - } - - setOauthFlowing(true); - setOauthData(null); - setManualCodeInput(''); - setOauthError(null); - - try { - const snapshot = await fetchProviderSnapshot(); - const accountId = buildProviderAccountId( - selectedProvider as ProviderType, - selectedAccountId, - snapshot.vendors, - ); - const label = selectedProviderData?.name || selectedProvider; - pendingOAuthRef.current = { accountId, label }; - await hostApiFetch('/api/providers/oauth/start', { - method: 'POST', - body: JSON.stringify({ provider: selectedProvider, accountId, label }), - }); - } catch (e) { - setOauthError(String(e)); - setOauthFlowing(false); - pendingOAuthRef.current = null; - } - }; - - const handleCancelOAuth = async () => { - setOauthFlowing(false); - setOauthData(null); - setManualCodeInput(''); - setOauthError(null); - pendingOAuthRef.current = null; - await hostApiFetch('/api/providers/oauth/cancel', { method: 'POST' }); - }; - - const handleSubmitManualOAuthCode = async () => { - const value = manualCodeInput.trim(); - if (!value) return; - try { - await hostApiFetch('/api/providers/oauth/submit', { - method: 'POST', - body: JSON.stringify({ code: value }), - }); - setOauthError(null); - } catch (error) { - setOauthError(String(error)); - } - }; - - // On mount, try to restore previously configured provider - useEffect(() => { - let cancelled = false; - (async () => { - try { - const snapshot = await fetchProviderSnapshot(); - const statusMap = new Map(snapshot.statuses.map((status) => [status.id, status])); - const setupProviderTypes = new Set(providers.map((p) => p.id)); - const setupCandidates = snapshot.accounts.filter((account) => setupProviderTypes.has(account.vendorId)); - const preferred = - (snapshot.defaultAccountId - && setupCandidates.find((account) => account.id === snapshot.defaultAccountId)) - || setupCandidates.find((account) => hasConfiguredCredentials(account, statusMap.get(account.id))) - || setupCandidates[0]; - if (preferred && !cancelled) { - onSelectProvider(preferred.vendorId); - setSelectedAccountId(preferred.id); - const typeInfo = providers.find((p) => p.id === preferred.vendorId); - const requiresKey = typeInfo?.requiresApiKey ?? false; - onConfiguredChange(!requiresKey || hasConfiguredCredentials(preferred, statusMap.get(preferred.id))); - const storedKey = (await hostApiFetch<{ apiKey: string | null }>( - `/api/providers/${encodeURIComponent(preferred.id)}/api-key`, - )).apiKey; - onApiKeyChange(storedKey || ''); - } else if (!cancelled) { - onConfiguredChange(false); - onApiKeyChange(''); - } - } catch (error) { - if (!cancelled) { - console.error('Failed to load provider list:', error); - } - } - })(); - return () => { cancelled = true; }; - }, [onApiKeyChange, onConfiguredChange, onSelectProvider, providers]); - - // When provider changes, load stored key + reset base URL - useEffect(() => { - let cancelled = false; - (async () => { - if (!selectedProvider) return; - setApiProtocol('openai-completions'); - try { - const snapshot = await fetchProviderSnapshot(); - const statusMap = new Map(snapshot.statuses.map((status) => [status.id, status])); - const preferredAccount = pickPreferredAccount( - snapshot.accounts, - snapshot.defaultAccountId, - selectedProvider, - statusMap, - ); - const accountIdForLoad = preferredAccount?.id || selectedProvider; - setSelectedAccountId(preferredAccount?.id || null); - - const savedProvider = await hostApiFetch<{ baseUrl?: string; model?: string; apiProtocol?: ProviderAccount['apiProtocol'] } | null>( - `/api/providers/${encodeURIComponent(accountIdForLoad)}`, - ); - const storedKey = (await hostApiFetch<{ apiKey: string | null }>( - `/api/providers/${encodeURIComponent(accountIdForLoad)}/api-key`, - )).apiKey; - if (!cancelled) { - onApiKeyChange(storedKey || ''); - - const info = providers.find((p) => p.id === selectedProvider); - const nextBaseUrl = savedProvider?.baseUrl || info?.defaultBaseUrl || ''; - const nextModelId = savedProvider?.model || info?.defaultModelId || ''; - setBaseUrl(nextBaseUrl); - setModelId(nextModelId); - setApiProtocol(savedProvider?.apiProtocol || 'openai-completions'); - if ( - selectedProvider === 'ark' - && info?.codePlanPresetBaseUrl - && info?.codePlanPresetModelId - && nextBaseUrl.trim() === info.codePlanPresetBaseUrl - && nextModelId.trim() === info.codePlanPresetModelId - ) { - setArkMode('codeplan'); - } else { - setArkMode('apikey'); - } - } - } catch (error) { - if (!cancelled) { - console.error('Failed to load provider key:', error); - } - } - })(); - return () => { cancelled = true; }; - }, [onApiKeyChange, selectedProvider, providers]); - - useEffect(() => { - if (!providerMenuOpen) return; - - const handlePointerDown = (event: MouseEvent) => { - if (providerMenuRef.current && !providerMenuRef.current.contains(event.target as Node)) { - setProviderMenuOpen(false); - } - }; - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - setProviderMenuOpen(false); - } - }; - - document.addEventListener('mousedown', handlePointerDown); - document.addEventListener('keydown', handleEscape); - return () => { - document.removeEventListener('mousedown', handlePointerDown); - document.removeEventListener('keydown', handleEscape); - }; - }, [providerMenuOpen]); - - const selectedProviderData = providers.find((p) => p.id === selectedProvider); - const providerDocsUrl = getProviderDocsUrl(selectedProviderData, i18n.language); - const effectiveProviderDocsUrl = selectedProvider === 'ark' && arkMode === 'codeplan' - ? (selectedProviderData?.codePlanDocsUrl || providerDocsUrl) - : providerDocsUrl; - const selectedProviderIconUrl = selectedProviderData - ? getProviderIconUrl(selectedProviderData.id) - : undefined; - const showBaseUrlField = selectedProviderData?.showBaseUrl ?? false; - const showModelIdField = shouldShowProviderModelId(selectedProviderData, devModeUnlocked); - const codePlanPreset = selectedProviderData?.codePlanPresetBaseUrl && selectedProviderData?.codePlanPresetModelId - ? { - baseUrl: selectedProviderData.codePlanPresetBaseUrl, - modelId: selectedProviderData.codePlanPresetModelId, - } - : null; - const requiresKey = selectedProviderData?.requiresApiKey ?? false; - const isOAuth = selectedProviderData?.isOAuth ?? false; - const supportsApiKey = selectedProviderData?.supportsApiKey ?? false; - const useOAuthFlow = isOAuth && (!supportsApiKey || authMode === 'oauth'); - const normalizedApiKey = normalizeProviderApiKeyInput(apiKey); - - const handleValidateAndSave = async () => { - if (!selectedProvider) return; - - try { - const snapshot = await fetchProviderSnapshot(); - const existingVendorIds = new Set(snapshot.accounts.map((account) => account.vendorId)); - if (selectedProvider === 'minimax-portal' && existingVendorIds.has('minimax-portal-cn')) { - toast.error(t('settings:aiProviders.toast.minimaxConflict')); - return; - } - if (selectedProvider === 'minimax-portal-cn' && existingVendorIds.has('minimax-portal')) { - toast.error(t('settings:aiProviders.toast.minimaxConflict')); - return; - } - } catch { - // ignore check failure - } - - setValidating(true); - setKeyValid(null); - - try { - // Validate key if the provider requires one and a key was entered - const isApiKeyRequired = requiresKey || (supportsApiKey && authMode === 'apikey'); - if (isApiKeyRequired && !normalizedApiKey) { - setKeyValid(false); - onConfiguredChange(false); - toast.error(t('provider.invalid')); - setValidating(false); - return; - } - - if (isApiKeyRequired) { - const result = await invokeIpc( - 'provider:validateKey', - selectedAccountId || selectedProvider, - normalizedApiKey, - { - baseUrl: baseUrl.trim() || undefined, - apiProtocol: (selectedProvider === 'custom' || selectedProvider === 'ollama') - ? apiProtocol - : undefined, - } - ) as { valid: boolean; error?: string }; - - setKeyValid(result.valid); - - if (!result.valid) { - toast.error(result.error || t('provider.invalid')); - setValidating(false); - return; - } - } else { - setKeyValid(true); - } - - const effectiveModelId = resolveProviderModelForSave( - selectedProviderData, - modelId, - devModeUnlocked - ); - const snapshot = await fetchProviderSnapshot(); - const accountIdForSave = buildProviderAccountId( - selectedProvider as ProviderType, - selectedAccountId, - snapshot.vendors, - ); - - const effectiveApiKey = resolveProviderApiKeyForSave(selectedProvider, apiKey); - const accountPayload: ProviderAccount = { - id: accountIdForSave, - vendorId: selectedProvider as ProviderType, - label: selectedProvider === 'custom' - ? t('settings:aiProviders.custom') - : (selectedProviderData?.name || selectedProvider), - authMode: selectedProvider === 'ollama' - ? 'local' - : 'api_key', - baseUrl: baseUrl.trim() || undefined, - apiProtocol: (selectedProvider === 'custom' || selectedProvider === 'ollama') - ? apiProtocol - : undefined, - model: effectiveModelId, - enabled: true, - isDefault: false, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }; - - const saveResult = selectedAccountId - ? await hostApiFetch<{ success: boolean; error?: string }>( - `/api/provider-accounts/${encodeURIComponent(accountIdForSave)}`, - { - method: 'PUT', - body: JSON.stringify({ - updates: { - label: accountPayload.label, - authMode: accountPayload.authMode, - baseUrl: accountPayload.baseUrl, - apiProtocol: accountPayload.apiProtocol, - model: accountPayload.model, - enabled: accountPayload.enabled, - }, - apiKey: effectiveApiKey, - }), - }, - ) - : await hostApiFetch<{ success: boolean; error?: string }>('/api/provider-accounts', { - method: 'POST', - body: JSON.stringify({ account: accountPayload, apiKey: effectiveApiKey }), - }); - - if (!saveResult.success) { - throw new Error(saveResult.error || 'Failed to save provider config'); - } - - const defaultResult = await hostApiFetch<{ success: boolean; error?: string }>( - '/api/provider-accounts/default', - { - method: 'PUT', - body: JSON.stringify({ accountId: accountIdForSave }), - }, - ); - - if (!defaultResult.success) { - throw new Error(defaultResult.error || 'Failed to set default provider'); - } - - setSelectedAccountId(accountIdForSave); - onConfiguredChange(true); - toast.success(t('provider.valid')); - } catch (error) { - setKeyValid(false); - onConfiguredChange(false); - toast.error('Configuration failed: ' + String(error)); - } finally { - setValidating(false); - } - }; - - // Can the user submit? - const isApiKeyRequired = requiresKey || (supportsApiKey && authMode === 'apikey'); - const canSubmit = - selectedProvider - && (isApiKeyRequired ? normalizedApiKey.length > 0 : true) - && (showModelIdField ? modelId.trim().length > 0 : true) - && !useOAuthFlow; - - const handleSelectProvider = (providerId: string) => { - onSelectProvider(providerId); - setSelectedAccountId(null); - onConfiguredChange(false); - onApiKeyChange(''); - setKeyValid(null); - setProviderMenuOpen(false); - setAuthMode('oauth'); - setArkMode('apikey'); - }; - - return ( -
- {/* Provider selector — dropdown */} -
-
- - {selectedProvider && effectiveProviderDocsUrl && ( - - {t('settings:aiProviders.dialog.customDoc')} - - - )} -
-
- - - {providerMenuOpen && ( -
- {providers.map((p) => { - const iconUrl = getProviderIconUrl(p.id); - const isSelected = selectedProvider === p.id; - - return ( - - ); - })} -
- )} -
-
- - {/* Dynamic config fields based on selected provider */} - {selectedProvider && ( - - {codePlanPreset && ( -
-
- - {selectedProviderData?.codePlanDocsUrl && ( - - {t('provider.codePlanDoc')} - - - )} -
-
- - -
- {arkMode === 'codeplan' && ( -

- {t('provider.codePlanPresetDesc')} -

- )} -
- )} - - {/* Base URL field (for siliconflow, ollama, custom) */} - {showBaseUrlField && ( -
- - { - setBaseUrl(e.target.value); - onConfiguredChange(false); - }} - autoComplete="off" - className="bg-background border-input" - /> -
- )} - - {/* Model ID field (for siliconflow etc.) */} - {showModelIdField && ( -
- - { - setModelId(e.target.value); - onConfiguredChange(false); - }} - autoComplete="off" - className="bg-background border-input" - /> -

- {t('provider.modelIdDesc')} -

-
- )} - - {selectedProvider === 'custom' && ( -
- -
- - - -
-
- )} - - {/* Auth mode toggle for providers supporting both */} - {isOAuth && supportsApiKey && ( -
- - -
- )} - - {/* API Key field (hidden for ollama) */} - {(!isOAuth || (supportsApiKey && authMode === 'apikey')) && ( -
- -
- { - onApiKeyChange(e.target.value); - onConfiguredChange(false); - setKeyValid(null); - }} - autoComplete="off" - className="pr-10 bg-background border-input" - /> - -
-
- )} - - {/* Device OAuth Trigger */} - {useOAuthFlow && ( -
-
-

- This provider requires signing in via your browser. -

- -
- - {/* OAuth Active State Modal / Inline View */} - {oauthFlowing && ( -
- {/* Background pulse effect */} -
- -
- {oauthError ? ( -
- -

Authentication Failed

-

{oauthError}

- -
- ) : !oauthData ? ( -
- -

Requesting secure login code...

-
- ) : oauthData.mode === 'manual' ? ( -
-
-

Complete OpenAI Login

-

- {oauthData.message || 'Open the authorization page, complete login, then paste the callback URL or code below.'} -

-
- - - - setManualCodeInput(e.target.value)} - /> - - - - -
- ) : ( -
-
-

Approve Login

-
-

1. Copy the authorization code below.

-

2. Open the login page in your browser.

-

3. Paste the code to approve access.

-
-
- -
- - {oauthData.userCode} - - -
- - - -
- - Waiting for approval in browser... -
- - -
- )} -
-
- )} -
- )} - - {/* Validate & Save */} - - - {keyValid !== null && ( -

- {keyValid ? `✓ ${t('provider.valid')}` : `✗ ${t('provider.invalid')}`} -

- )} - -

- {t('provider.storedLocally')} -

- - )} -
- ); -} - -// NOTE: SkillsContent component removed - auto-install essential skills // Installation status for each skill type InstallStatus = 'pending' | 'installing' | 'completed' | 'failed'; @@ -1825,15 +825,13 @@ function InstallingContent({ skills, onComplete, onSkip }: InstallingContentProp ); } interface CompleteContentProps { - selectedProvider: string | null; installedSkills: string[]; } -function CompleteContent({ selectedProvider, installedSkills }: CompleteContentProps) { +function CompleteContent({ installedSkills }: CompleteContentProps) { const { t } = useTranslation(['setup', 'settings']); const gatewayStatus = useGatewayStore((state) => state.status); - const providerData = providers.find((p) => p.id === selectedProvider); const installedSkillNames = getDefaultSkills(t) .filter((s: DefaultSkill) => installedSkills.includes(s.id)) .map((s: DefaultSkill) => s.name) @@ -1848,12 +846,6 @@ function CompleteContent({ selectedProvider, installedSkills }: CompleteContentP

-
- {t('complete.provider')} - - {providerData ? {getProviderIconUrl(providerData.id) ? {providerData.name} : providerData.icon} {providerData.id === 'custom' ? t('settings:aiProviders.custom') : providerData.name} : '—'} - -
{t('complete.components')}