import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { RefreshCw, Save } from 'lucide-react'; import { useAppContext } from '../context/AppContext'; import { useUI } from '../context/UIContext'; import { Button, FixedButton } from '../components/Button'; import PageHeader from '../components/PageHeader'; import { ConfigDiffModal } from '../components/config/ConfigPageChrome'; import { ProviderProxyCard, ProviderRuntimeDrawer, ProviderRuntimeSummary, ProviderRuntimeToolbar } from '../components/config/ProviderConfigSection'; import { buildDiffRows, RuntimeWindow } from '../components/config/configUtils'; import { useConfigProviderActions } from '../components/config/useConfigProviderActions'; import { useConfigRuntimeView } from '../components/config/useConfigRuntimeView'; import { useConfigSaveAction } from '../components/config/useConfigSaveAction'; import { cloneJSON } from '../utils/object'; const Providers: React.FC = () => { const { t } = useTranslation(); const ui = useUI(); const { cfg, setCfg, cfgRaw, loadConfig, q, setConfigEditing, providerRuntimeItems, setToken } = useAppContext(); const [newProxyName, setNewProxyName] = useState(''); const [runtimeAutoRefresh, setRuntimeAutoRefresh] = useState(true); const [runtimeRefreshSec, setRuntimeRefreshSec] = useState(10); const [runtimeWindow, setRuntimeWindow] = useState('24h'); const [runtimeDrawerProvider, setRuntimeDrawerProvider] = useState(''); const [selectedProviderTab, setSelectedProviderTab] = useState(''); const [showDiff, setShowDiff] = useState(false); const [baseline, setBaseline] = useState(null); const oauthImportInputRef = useRef(null); const latestProviderRuntimeRef = useRef([]); const [displayedProviderRuntimeItems, setDisplayedProviderRuntimeItems] = useState([]); const [oauthAccounts, setOAuthAccounts] = useState>>({}); const [oauthAccountsLoading, setOAuthAccountsLoading] = useState>({}); const [oauthAccountsLoaded, setOAuthAccountsLoaded] = useState>({}); const providerEntries = useMemo(() => { const providers = (((cfg as any)?.models || {}) as any)?.providers || {}; const entries: Array<[string, any]> = []; if (providers && typeof providers === 'object' && !Array.isArray(providers)) { Object.entries(providers).forEach(([name, value]) => entries.push([name, value])); } return entries; }, [cfg]); const providerRuntimeMap = useMemo(() => { const entries = Array.isArray(displayedProviderRuntimeItems) ? displayedProviderRuntimeItems : []; return Object.fromEntries(entries.map((item: any) => [String(item?.name || ''), item])); }, [displayedProviderRuntimeItems]); const activeProviderName = useMemo(() => { if (providerEntries.length === 0) return ''; if (providerEntries.some(([name]) => name === selectedProviderTab)) return selectedProviderTab; return providerEntries[0]?.[0] || ''; }, [providerEntries, selectedProviderTab]); const activeProviderEntry = useMemo( () => providerEntries.find(([name]) => name === activeProviderName) || null, [providerEntries, activeProviderName], ); useEffect(() => { latestProviderRuntimeRef.current = Array.isArray(providerRuntimeItems) ? providerRuntimeItems : []; if (runtimeAutoRefresh && runtimeRefreshSec <= 1) { setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current); } }, [providerRuntimeItems, runtimeAutoRefresh, runtimeRefreshSec]); useEffect(() => { if (!runtimeAutoRefresh) return; setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current); const timer = window.setInterval(() => { setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current); }, Math.max(1, runtimeRefreshSec) * 1000); return () => window.clearInterval(timer); }, [runtimeAutoRefresh, runtimeRefreshSec]); useEffect(() => { if (!activeProviderName && providerEntries.length > 0) { setSelectedProviderTab(providerEntries[0][0]); } }, [activeProviderName, providerEntries]); useEffect(() => { if (baseline == null && cfg && Object.keys(cfg).length > 0) { setBaseline(cloneJSON(cfg)); } }, [cfg, baseline]); const diffRows = useMemo(() => buildDiffRows(baseline, cfg, t('configRoot')), [baseline, cfg, t]); const isDirty = useMemo(() => { if (baseline == null) return false; return JSON.stringify(baseline) !== JSON.stringify(cfg || {}); }, [baseline, cfg]); useEffect(() => { setConfigEditing(isDirty); return () => setConfigEditing(false); }, [isDirty, setConfigEditing]); const { filterRuntimeEvents, renderRuntimeEventList, runtimeSectionOpen, toggleRuntimeSection } = useConfigRuntimeView(runtimeWindow); const { addProxy, clearAPIKeyCooldown, clearOAuthCooldown, clearProviderHistory, deleteOAuthAccount, exportProviderHistory, loadOAuthAccounts, onOAuthImportChange, refreshOAuthAccount, refreshProviderRuntimeNow, removeProxy, startOAuthLogin, triggerOAuthImport, updateProxyField, } = useConfigProviderActions({ inputRef: oauthImportInputRef, loadConfig, onProviderRuntimeRefreshed: () => setDisplayedProviderRuntimeItems(latestProviderRuntimeRef.current), providerRuntimeMap, q, setCfg, setNewProxyName, setOAuthAccounts, t, ui, }); const loadOAuthAccountsNow = useCallback(async (name: string) => { if (!name) return; setOAuthAccountsLoading((prev) => ({ ...prev, [name]: true })); try { await loadOAuthAccounts(name); setOAuthAccountsLoaded((prev) => ({ ...prev, [name]: true })); } finally { setOAuthAccountsLoading((prev) => ({ ...prev, [name]: false })); } }, [loadOAuthAccounts]); useEffect(() => { providerEntries.forEach(([name, p]) => { if (!['oauth', 'hybrid'].includes(String(p?.auth || ''))) return; if (oauthAccountsLoaded[name] || oauthAccountsLoading[name]) return; void loadOAuthAccountsNow(name); }); }, [loadOAuthAccountsNow, oauthAccountsLoaded, oauthAccountsLoading, providerEntries]); useEffect(() => { if (!activeProviderEntry) return; const [name, provider] = activeProviderEntry; if (!['oauth', 'hybrid'].includes(String(provider?.auth || ''))) return; if (oauthAccountsLoaded[name] || oauthAccountsLoading[name]) return; void loadOAuthAccountsNow(name); }, [activeProviderEntry, loadOAuthAccountsNow, oauthAccountsLoaded, oauthAccountsLoading]); useEffect(() => { const oauthProviderNames = new Set( providerEntries .filter(([, provider]) => ['oauth', 'hybrid'].includes(String(provider?.auth || ''))) .map(([name]) => name), ); setOAuthAccountsLoaded((prev) => { const next = Object.fromEntries(Object.entries(prev).filter(([name]) => oauthProviderNames.has(name))); return Object.keys(next).length === Object.keys(prev).length ? prev : next; }); setOAuthAccountsLoading((prev) => { const next = Object.fromEntries(Object.entries(prev).filter(([name]) => oauthProviderNames.has(name))); return Object.keys(next).length === Object.keys(prev).length ? prev : next; }); }, [providerEntries]); const { saveConfig } = useConfigSaveAction({ cfg, cfgRaw, loadConfig, q, setBaseline, setConfigEditing, setToken, setShowDiff, showRaw: false, t, ui, }); return (
{ const reloaded = await loadConfig(true); setBaseline(cloneJSON(reloaded ?? cfg)); }} label={t('reload')}>
} />
addProxy(newProxyName)} onNewProxyNameChange={setNewProxyName} onRefreshRuntime={refreshProviderRuntimeNow} onRuntimeAutoRefreshChange={setRuntimeAutoRefresh} onRuntimeRefreshSecChange={setRuntimeRefreshSec} onRuntimeWindowChange={setRuntimeWindow} runtimeAutoRefresh={runtimeAutoRefresh} runtimeRefreshSec={runtimeRefreshSec} runtimeWindow={runtimeWindow} t={t} />
{t('providersIntroBefore')} oauth {t('providersIntroMiddle')} hybrid {t('providersIntroAfter')}
{providerEntries.length > 0 ? ( <>
{providerEntries.map(([name, p]) => { const auth = String(p?.auth || 'bearer'); const active = name === activeProviderName; return ( ); })}
{activeProviderEntry && ( clearOAuthCooldown(activeProviderEntry[0], credentialFile)} onDeleteOAuthAccount={(credentialFile) => deleteOAuthAccount(activeProviderEntry[0], credentialFile)} onFieldChange={(field, value) => updateProxyField(activeProviderEntry[0], field, value)} onLoadOAuthAccounts={() => loadOAuthAccountsNow(activeProviderEntry[0])} onRefreshOAuthAccount={(credentialFile) => refreshOAuthAccount(activeProviderEntry[0], credentialFile)} onRemove={() => removeProxy(activeProviderEntry[0])} onStartOAuthLogin={() => startOAuthLogin(activeProviderEntry[0], activeProviderEntry[1])} onTriggerOAuthImport={() => triggerOAuthImport(activeProviderEntry[0], activeProviderEntry[1])} proxy={activeProviderEntry[1]} runtimeItem={providerRuntimeMap[activeProviderEntry[0]]} runtimeSummary={providerRuntimeMap[activeProviderEntry[0]] ? ( clearAPIKeyCooldown(activeProviderEntry[0])} onClearHistory={() => clearProviderHistory(activeProviderEntry[0])} onExportHistory={() => exportProviderHistory(activeProviderEntry[0])} onOpenHistory={() => setRuntimeDrawerProvider(activeProviderEntry[0])} renderRuntimeEventList={renderRuntimeEventList} runtimeSectionOpen={(section) => runtimeSectionOpen(activeProviderEntry[0], section)} toggleRuntimeSection={(section) => toggleRuntimeSection(activeProviderEntry[0], section)} filterRuntimeEvents={filterRuntimeEvents} /> ) : null} t={t} /> )} ) : (
{t('configNoCustomProviders')}
)}
{runtimeDrawerProvider && providerRuntimeMap[runtimeDrawerProvider] && ( clearProviderHistory(runtimeDrawerProvider)} onClose={() => setRuntimeDrawerProvider('')} onExportHistory={() => exportProviderHistory(runtimeDrawerProvider)} renderRuntimeEventList={renderRuntimeEventList} /> )} {showDiff && setShowDiff(false)} t={t} />} ); }; export default Providers;