import { useState, useMemo, useCallback } from 'react' import { Button } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Input } from '@/components/ui/input' import { DeleteDialog } from '@/components/ui/delete-dialog' import { Loader2, Check, X, Shield, ChevronDown, ChevronRight, Key, Search, Pencil, Trash2 } from 'lucide-react' import { providerCredentialsApi } from '@/api/providers' import type { Provider } from '@/api/providers' import { type IntegrationInfo, type IntegrationMethod, type OAuthAuthorizeResponse } from '@/api/oauth' import { useMutation, useQueryClient } from '@tanstack/react-query' import { OAuthAuthorizeDialog } from './OAuthAuthorizeDialog' import { OAuthCallbackDialog } from './OAuthCallbackDialog' import { ApiKeyDialog } from '@/components/model/ApiKeyDialog' import { invalidateProviderCaches } from '@/lib/queryInvalidation' import { useProviders } from '@/hooks/useProviders' import { useProviderIntegrations } from '@/hooks/useProviderIntegrations' interface ProviderEntry { id: string name: string models: Provider['models'] methods: IntegrationMethod[] connected: boolean keyConnected: boolean } function hasCredentialConnection(integration: IntegrationInfo): boolean { return integration.connections.some((connection) => connection.type === 'credential') } function hasKeyConnection(integration: IntegrationInfo): boolean { return integration.connections.some( (connection) => connection.type === 'credential' && connection.method === 'key', ) } function supportsOAuthMethods(methods: IntegrationMethod[]): boolean { return methods.some((method) => method.type === 'oauth') } function supportsSettingsAuthentication(methods: IntegrationMethod[]): boolean { return methods.some((method) => method.type === 'oauth' || method.type === 'key') } function buildProviderEntries( providers: Provider[] | undefined, integrations: IntegrationInfo[] | undefined, ): ProviderEntry[] { const integrationsByID = new Map((integrations ?? []).map((integration) => [integration.id, integration])) const seen = new Set() const entriesByID = new Map() const entries: ProviderEntry[] = [] for (const provider of providers ?? []) { const integrationID = provider.integrationID ?? provider.id const integration = integrationsByID.get(integrationID) seen.add(integrationID) const existingEntry = entriesByID.get(integrationID) if (existingEntry) { existingEntry.models.push(...provider.models) continue } const entry: ProviderEntry = { id: integrationID, name: integration?.name || provider.name || provider.id, models: [...provider.models], methods: integration?.methods ?? [], connected: integration ? hasCredentialConnection(integration) : false, keyConnected: integration ? hasKeyConnection(integration) : false, } entriesByID.set(integrationID, entry) entries.push(entry) } const integrationOnly = (integrations ?? []) .filter((integration) => !seen.has(integration.id) && supportsSettingsAuthentication(integration.methods)) .sort((a, b) => a.name.localeCompare(b.name)) .map((integration): ProviderEntry => ({ id: integration.id, name: integration.name || integration.id, models: [], methods: integration.methods, connected: hasCredentialConnection(integration), keyConnected: hasKeyConnection(integration), })) return [...entries, ...integrationOnly] } export function ProviderSettings() { const [selectedProvider, setSelectedProvider] = useState(null) const [oauthDialogOpen, setOauthDialogOpen] = useState(false) const [oauthCallbackDialogOpen, setOauthCallbackDialogOpen] = useState(false) const [oauthResponse, setOauthResponse] = useState(null) const [connectedExpanded, setConnectedExpanded] = useState(false) const [availableExpanded, setAvailableExpanded] = useState(true) const [availableSearch, setAvailableSearch] = useState('') const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false) const [apiKeyProvider, setApiKeyProvider] = useState(null) const [apiKeyMode, setApiKeyMode] = useState<'add' | 'edit'>('add') const [deleteTarget, setDeleteTarget] = useState(null) const queryClient = useQueryClient() const { data: providersData, isLoading: providersLoading } = useProviders() const providers = providersData?.providers const { data: integrations, isLoading: integrationsLoading, isError: integrationsError, } = useProviderIntegrations() const providerEntries = useMemo( () => integrationsError ? [] : buildProviderEntries(providers, integrations), [providers, integrations, integrationsError], ) const deleteCredentialMutation = useMutation({ mutationFn: (providerId: string) => providerCredentialsApi.delete(providerId), onSuccess: () => { invalidateProviderCaches(queryClient) }, }) const handleDeleteCredential = (providerId: string) => { setDeleteTarget(providerId) } const handleDeleteConfirm = () => { if (deleteTarget) { deleteCredentialMutation.mutate(deleteTarget) setDeleteTarget(null) } } const handleDeleteCancel = () => { setDeleteTarget(null) } const handleOAuthAuthorize = (response: OAuthAuthorizeResponse) => { setOauthResponse(response) setOauthDialogOpen(false) setOauthCallbackDialogOpen(true) } const handleOAuthDialogClose = () => { setOauthDialogOpen(false) setSelectedProvider(null) } const handleOAuthSuccess = () => { invalidateProviderCaches(queryClient) setOauthCallbackDialogOpen(false) setOauthResponse(null) setSelectedProvider(null) } const oauthProviders = useMemo(() => { return providerEntries .filter((provider) => supportsOAuthMethods(provider.methods)) .sort((a, b) => Number(b.connected) - Number(a.connected)) }, [providerEntries]) const apiKeyProviders = useMemo(() => { const nonOAuthProviders = providerEntries.filter( (provider) => !supportsOAuthMethods(provider.methods) && (provider.connected || provider.methods.some((method) => method.type === 'key')), ) return { connected: nonOAuthProviders.filter((provider) => provider.connected), available: nonOAuthProviders.filter((provider) => !provider.connected), } }, [providerEntries]) const filteredAvailableProviders = useMemo(() => { if (!availableSearch.trim()) return apiKeyProviders.available const search = availableSearch.toLowerCase() return apiKeyProviders.available.filter(provider => provider.name.toLowerCase().includes(search) || provider.id.toLowerCase().includes(search) ) }, [apiKeyProviders.available, availableSearch]) const selectedProviderName = useMemo(() => { if (!selectedProvider) return '' return providerEntries.find((provider) => provider.id === selectedProvider)?.name || selectedProvider }, [selectedProvider, providerEntries]) const selectedProviderMethods = useMemo(() => { if (!selectedProvider) return [] return providerEntries.find((provider) => provider.id === selectedProvider)?.methods ?? [] }, [selectedProvider, providerEntries]) const handleAddApiKey = useCallback((provider: Provider) => { setApiKeyProvider(provider) setApiKeyMode('add') setApiKeyDialogOpen(true) }, []) const handleEditApiKey = useCallback((provider: Provider) => { setApiKeyProvider(provider) setApiKeyMode('edit') setApiKeyDialogOpen(true) }, []) const handleApiKeySuccess = useCallback(() => { setApiKeyDialogOpen(false) setApiKeyProvider(null) invalidateProviderCaches(queryClient) }, [queryClient]) const handleApiKeyDialogClose = useCallback((open: boolean) => { setApiKeyDialogOpen(open) if (!open) { setApiKeyProvider(null) } }, []) if (integrationsError) { return (

Unable to load provider integrations.

Provider connection status and available authentication methods could not be verified. Reload the settings page to try again.

) } if (providersLoading || integrationsLoading) { return (
) } return (

OAuth Providers

Connect to AI providers using OAuth authentication.

{oauthProviders.length === 0 ? (

No OAuth-capable providers available.

) : (
{oauthProviders.map((provider) => { const isConnected = provider.connected const keyConnected = provider.keyConnected const modelCount = provider.models.length return (

{provider.name || provider.id}

{modelCount > 0 && (

{modelCount} model{modelCount !== 1 ? 's' : ''}

)}
{isConnected ? ( Connected ) : ( Not Connected )} {provider.methods.some((method) => method.type === 'key') && ( )} {isConnected && ( )}
) })}
)} {selectedProvider && ( )} {selectedProvider && oauthResponse && ( )}

API Keys

Manage API keys for AI providers.

{connectedExpanded && (
{apiKeyProviders.connected.length === 0 ? (

No providers configured. Add an API key below to get started.

) : ( apiKeyProviders.connected.map((provider) => { const modelCount = provider.models.length return (
{provider.name || provider.id} {modelCount > 0 && ( {modelCount} model{modelCount !== 1 ? 's' : ''} )}
Connected {provider.methods.some((method) => method.type === 'key') && ( )}
) }) )}
)}
{availableExpanded && (
setAvailableSearch(e.target.value)} className="pl-9 md:text-sm" autoComplete="off" />
{filteredAvailableProviders.length === 0 ? (

{availableSearch ? 'No providers match your search.' : 'No available providers.'}

) : ( filteredAvailableProviders.map((provider, index) => { const modelCount = provider.models.length return (
{provider.name || provider.id} {modelCount > 0 && ( {modelCount} model{modelCount !== 1 ? 's' : ''} )}
) }) )}
)}
{apiKeyProvider && ( )} !open && setDeleteTarget(null)} onConfirm={handleDeleteConfirm} onCancel={handleDeleteCancel} title="Remove Credentials" description={`Are you sure you want to remove credentials for ${deleteTarget || 'this provider'}?`} isDeleting={deleteCredentialMutation.isPending} />
) }