import { useCallback, useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { AmpcodeSection, ClaudeSection, CodexSection, GeminiSection, OpenAISection, VertexSection, ProviderNav, useProviderStats, } from '@/components/providers'; import { withDisableAllModelsRule, withoutDisableAllModelsRule, } from '@/components/providers/utils'; import { ampcodeApi, providersApi } from '@/services/api'; import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores'; import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types'; import styles from './AiProvidersPage.module.scss'; export function AiProvidersPage() { const { t } = useTranslation(); const navigate = useNavigate(); const { showNotification, showConfirmation } = useNotificationStore(); const resolvedTheme = useThemeStore((state) => state.resolvedTheme); const connectionStatus = useAuthStore((state) => state.connectionStatus); const config = useConfigStore((state) => state.config); const fetchConfig = useConfigStore((state) => state.fetchConfig); const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const clearCache = useConfigStore((state) => state.clearCache); const isCacheValid = useConfigStore((state) => state.isCacheValid); const hasMounted = useRef(false); const [loading, setLoading] = useState(() => !isCacheValid()); const [error, setError] = useState(''); const [geminiKeys, setGeminiKeys] = useState( () => config?.geminiApiKeys || [] ); const [codexConfigs, setCodexConfigs] = useState( () => config?.codexApiKeys || [] ); const [claudeConfigs, setClaudeConfigs] = useState( () => config?.claudeApiKeys || [] ); const [vertexConfigs, setVertexConfigs] = useState( () => config?.vertexApiKeys || [] ); const [openaiProviders, setOpenaiProviders] = useState( () => config?.openaiCompatibility || [] ); const [configSwitchingKey, setConfigSwitchingKey] = useState(null); const disableControls = connectionStatus !== 'connected'; const isSwitching = Boolean(configSwitchingKey); const { keyStats, usageDetails, loadKeyStats } = useProviderStats(); const getErrorMessage = (err: unknown) => { if (err instanceof Error) return err.message; if (typeof err === 'string') return err; return ''; }; const loadConfigs = useCallback(async () => { const hasValidCache = isCacheValid(); if (!hasValidCache) { setLoading(true); } setError(''); try { const [configResult, vertexResult, ampcodeResult] = await Promise.allSettled([ fetchConfig(), providersApi.getVertexConfigs(), ampcodeApi.getAmpcode(), ]); if (configResult.status !== 'fulfilled') { throw configResult.reason; } const data = configResult.value; setGeminiKeys(data?.geminiApiKeys || []); setCodexConfigs(data?.codexApiKeys || []); setClaudeConfigs(data?.claudeApiKeys || []); setVertexConfigs(data?.vertexApiKeys || []); setOpenaiProviders(data?.openaiCompatibility || []); if (vertexResult.status === 'fulfilled') { setVertexConfigs(vertexResult.value || []); updateConfigValue('vertex-api-key', vertexResult.value || []); clearCache('vertex-api-key'); } if (ampcodeResult.status === 'fulfilled') { updateConfigValue('ampcode', ampcodeResult.value); clearCache('ampcode'); } } catch (err: unknown) { const message = getErrorMessage(err) || t('notification.refresh_failed'); setError(message); } finally { setLoading(false); } }, [clearCache, fetchConfig, isCacheValid, t, updateConfigValue]); useEffect(() => { if (hasMounted.current) return; hasMounted.current = true; loadConfigs(); loadKeyStats(); }, [loadConfigs, loadKeyStats]); useEffect(() => { if (config?.geminiApiKeys) setGeminiKeys(config.geminiApiKeys); if (config?.codexApiKeys) setCodexConfigs(config.codexApiKeys); if (config?.claudeApiKeys) setClaudeConfigs(config.claudeApiKeys); if (config?.vertexApiKeys) setVertexConfigs(config.vertexApiKeys); if (config?.openaiCompatibility) setOpenaiProviders(config.openaiCompatibility); }, [ config?.geminiApiKeys, config?.codexApiKeys, config?.claudeApiKeys, config?.vertexApiKeys, config?.openaiCompatibility, ]); const openEditor = useCallback( (path: string) => { navigate(path, { state: { fromAiProviders: true } }); }, [navigate] ); const deleteGemini = async (index: number) => { const entry = geminiKeys[index]; if (!entry) return; showConfirmation({ title: t('ai_providers.gemini_delete_title', { defaultValue: 'Delete Gemini Key' }), message: t('ai_providers.gemini_delete_confirm'), variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { try { await providersApi.deleteGeminiKey(entry.apiKey); const next = geminiKeys.filter((_, idx) => idx !== index); setGeminiKeys(next); updateConfigValue('gemini-api-key', next); clearCache('gemini-api-key'); showNotification(t('notification.gemini_key_deleted'), 'success'); } catch (err: unknown) { const message = getErrorMessage(err); showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); } }, }); }; const setConfigEnabled = async ( provider: 'gemini' | 'codex' | 'claude', index: number, enabled: boolean ) => { if (provider === 'gemini') { const current = geminiKeys[index]; if (!current) return; const switchingKey = `${provider}:${current.apiKey}`; setConfigSwitchingKey(switchingKey); const previousList = geminiKeys; const nextExcluded = enabled ? withoutDisableAllModelsRule(current.excludedModels) : withDisableAllModelsRule(current.excludedModels); const nextItem: GeminiKeyConfig = { ...current, excludedModels: nextExcluded }; const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item)); setGeminiKeys(nextList); updateConfigValue('gemini-api-key', nextList); clearCache('gemini-api-key'); try { await providersApi.saveGeminiKeys(nextList); showNotification( enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success' ); } catch (err: unknown) { const message = getErrorMessage(err); setGeminiKeys(previousList); updateConfigValue('gemini-api-key', previousList); clearCache('gemini-api-key'); showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); } finally { setConfigSwitchingKey(null); } return; } const source = provider === 'codex' ? codexConfigs : claudeConfigs; const current = source[index]; if (!current) return; const switchingKey = `${provider}:${current.apiKey}`; setConfigSwitchingKey(switchingKey); const previousList = source; const nextExcluded = enabled ? withoutDisableAllModelsRule(current.excludedModels) : withDisableAllModelsRule(current.excludedModels); const nextItem: ProviderKeyConfig = { ...current, excludedModels: nextExcluded }; const nextList = previousList.map((item, idx) => (idx === index ? nextItem : item)); if (provider === 'codex') { setCodexConfigs(nextList); updateConfigValue('codex-api-key', nextList); clearCache('codex-api-key'); } else { setClaudeConfigs(nextList); updateConfigValue('claude-api-key', nextList); clearCache('claude-api-key'); } try { if (provider === 'codex') { await providersApi.saveCodexConfigs(nextList); } else { await providersApi.saveClaudeConfigs(nextList); } showNotification( enabled ? t('notification.config_enabled') : t('notification.config_disabled'), 'success' ); } catch (err: unknown) { const message = getErrorMessage(err); if (provider === 'codex') { setCodexConfigs(previousList); updateConfigValue('codex-api-key', previousList); clearCache('codex-api-key'); } else { setClaudeConfigs(previousList); updateConfigValue('claude-api-key', previousList); clearCache('claude-api-key'); } showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); } finally { setConfigSwitchingKey(null); } }; const deleteProviderEntry = async (type: 'codex' | 'claude', index: number) => { const source = type === 'codex' ? codexConfigs : claudeConfigs; const entry = source[index]; if (!entry) return; showConfirmation({ title: t(`ai_providers.${type}_delete_title`, { defaultValue: `Delete ${type === 'codex' ? 'Codex' : 'Claude'} Config` }), message: t(`ai_providers.${type}_delete_confirm`), variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { try { if (type === 'codex') { await providersApi.deleteCodexConfig(entry.apiKey); const next = codexConfigs.filter((_, idx) => idx !== index); setCodexConfigs(next); updateConfigValue('codex-api-key', next); clearCache('codex-api-key'); showNotification(t('notification.codex_config_deleted'), 'success'); } else { await providersApi.deleteClaudeConfig(entry.apiKey); const next = claudeConfigs.filter((_, idx) => idx !== index); setClaudeConfigs(next); updateConfigValue('claude-api-key', next); clearCache('claude-api-key'); showNotification(t('notification.claude_config_deleted'), 'success'); } } catch (err: unknown) { const message = getErrorMessage(err); showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); } }, }); }; const deleteVertex = async (index: number) => { const entry = vertexConfigs[index]; if (!entry) return; showConfirmation({ title: t('ai_providers.vertex_delete_title', { defaultValue: 'Delete Vertex Config' }), message: t('ai_providers.vertex_delete_confirm'), variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { try { await providersApi.deleteVertexConfig(entry.apiKey); const next = vertexConfigs.filter((_, idx) => idx !== index); setVertexConfigs(next); updateConfigValue('vertex-api-key', next); clearCache('vertex-api-key'); showNotification(t('notification.vertex_config_deleted'), 'success'); } catch (err: unknown) { const message = getErrorMessage(err); showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); } }, }); }; const deleteOpenai = async (index: number) => { const entry = openaiProviders[index]; if (!entry) return; showConfirmation({ title: t('ai_providers.openai_delete_title', { defaultValue: 'Delete OpenAI Provider' }), message: t('ai_providers.openai_delete_confirm'), variant: 'danger', confirmText: t('common.confirm'), onConfirm: async () => { try { await providersApi.deleteOpenAIProvider(entry.name); const next = openaiProviders.filter((_, idx) => idx !== index); setOpenaiProviders(next); updateConfigValue('openai-compatibility', next); clearCache('openai-compatibility'); showNotification(t('notification.openai_provider_deleted'), 'success'); } catch (err: unknown) { const message = getErrorMessage(err); showNotification(`${t('notification.delete_failed')}: ${message}`, 'error'); } }, }); }; return (

{t('ai_providers.title')}

{error &&
{error}
}
openEditor('/ai-providers/gemini/new')} onEdit={(index) => openEditor(`/ai-providers/gemini/${index}`)} onDelete={deleteGemini} onToggle={(index, enabled) => void setConfigEnabled('gemini', index, enabled)} />
openEditor('/ai-providers/codex/new')} onEdit={(index) => openEditor(`/ai-providers/codex/${index}`)} onDelete={(index) => void deleteProviderEntry('codex', index)} onToggle={(index, enabled) => void setConfigEnabled('codex', index, enabled)} />
openEditor('/ai-providers/claude/new')} onEdit={(index) => openEditor(`/ai-providers/claude/${index}`)} onDelete={(index) => void deleteProviderEntry('claude', index)} onToggle={(index, enabled) => void setConfigEnabled('claude', index, enabled)} />
openEditor('/ai-providers/vertex/new')} onEdit={(index) => openEditor(`/ai-providers/vertex/${index}`)} onDelete={deleteVertex} />
openEditor('/ai-providers/ampcode')} />
openEditor('/ai-providers/openai/new')} onEdit={(index) => openEditor(`/ai-providers/openai/${index}`)} onDelete={deleteOpenai} />
); }