import { useCallback, useEffect, useMemo, useState } from 'react'; import { useLocation, useNavigate, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { HeaderInputList } from '@/components/ui/HeaderInputList'; import { useEdgeSwipeBack } from '@/hooks/useEdgeSwipeBack'; import { SecondaryScreenShell } from '@/components/common/SecondaryScreenShell'; import { providersApi } from '@/services/api'; import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores'; import type { ProviderKeyConfig } from '@/types'; import { buildHeaderObject, headersToEntries } from '@/utils/headers'; import { entriesToModels } from '@/components/ui/modelInputListUtils'; import { excludedModelsToText, parseExcludedModels } from '@/components/providers/utils'; import type { ProviderFormState } from '@/components/providers'; import layoutStyles from './AiProvidersEditLayout.module.scss'; type LocationState = { fromAiProviders?: boolean } | null; const buildEmptyForm = (): ProviderFormState => ({ apiKey: '', prefix: '', baseUrl: '', proxyUrl: '', headers: [], models: [], excludedModels: [], modelEntries: [{ name: '', alias: '' }], excludedText: '', }); const parseIndexParam = (value: string | undefined) => { if (!value) return null; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : null; }; export function AiProvidersCodexEditPage() { const { t } = useTranslation(); const navigate = useNavigate(); const location = useLocation(); const params = useParams<{ index?: string }>(); const { showNotification } = useNotificationStore(); const connectionStatus = useAuthStore((state) => state.connectionStatus); const disableControls = connectionStatus !== 'connected'; const fetchConfig = useConfigStore((state) => state.fetchConfig); const updateConfigValue = useConfigStore((state) => state.updateConfigValue); const clearCache = useConfigStore((state) => state.clearCache); const [configs, setConfigs] = useState([]); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); const [form, setForm] = useState(() => buildEmptyForm()); const hasIndexParam = typeof params.index === 'string'; const editIndex = useMemo(() => parseIndexParam(params.index), [params.index]); const invalidIndexParam = hasIndexParam && editIndex === null; const initialData = useMemo(() => { if (editIndex === null) return undefined; return configs[editIndex]; }, [configs, editIndex]); const invalidIndex = editIndex !== null && !initialData; const title = editIndex !== null ? t('ai_providers.codex_edit_modal_title') : t('ai_providers.codex_add_modal_title'); const handleBack = useCallback(() => { const state = location.state as LocationState; if (state?.fromAiProviders) { navigate(-1); return; } navigate('/ai-providers', { replace: true }); }, [location.state, navigate]); const swipeRef = useEdgeSwipeBack({ onBack: handleBack }); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { handleBack(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [handleBack]); useEffect(() => { let cancelled = false; setLoading(true); setError(''); fetchConfig('codex-api-key') .then((value) => { if (cancelled) return; setConfigs(Array.isArray(value) ? (value as ProviderKeyConfig[]) : []); }) .catch((err: unknown) => { if (cancelled) return; const message = err instanceof Error ? err.message : ''; setError(message || t('notification.refresh_failed')); }) .finally(() => { if (cancelled) return; setLoading(false); }); return () => { cancelled = true; }; }, [fetchConfig, t]); useEffect(() => { if (loading) return; if (initialData) { setForm({ ...initialData, headers: headersToEntries(initialData.headers), modelEntries: (initialData.models || []).map((model) => ({ name: model.name, alias: model.alias ?? '', })), excludedText: excludedModelsToText(initialData.excludedModels), }); return; } setForm(buildEmptyForm()); }, [initialData, loading]); const canSave = !disableControls && !saving && !loading && !invalidIndexParam && !invalidIndex; const handleSave = useCallback(async () => { if (!canSave) return; const trimmedBaseUrl = (form.baseUrl ?? '').trim(); const baseUrl = trimmedBaseUrl || undefined; if (!baseUrl) { showNotification(t('notification.codex_base_url_required'), 'error'); return; } setSaving(true); setError(''); try { const payload: ProviderKeyConfig = { apiKey: form.apiKey.trim(), prefix: form.prefix?.trim() || undefined, baseUrl, proxyUrl: form.proxyUrl?.trim() || undefined, headers: buildHeaderObject(form.headers), models: entriesToModels(form.modelEntries), excludedModels: parseExcludedModels(form.excludedText), }; const nextList = editIndex !== null ? configs.map((item, idx) => (idx === editIndex ? payload : item)) : [...configs, payload]; await providersApi.saveCodexConfigs(nextList); updateConfigValue('codex-api-key', nextList); clearCache('codex-api-key'); showNotification( editIndex !== null ? t('notification.codex_config_updated') : t('notification.codex_config_added'), 'success' ); handleBack(); } catch (err: unknown) { const message = err instanceof Error ? err.message : ''; setError(message); showNotification(`${t('notification.update_failed')}: ${message}`, 'error'); } finally { setSaving(false); } }, [ canSave, clearCache, configs, editIndex, form, handleBack, showNotification, t, updateConfigValue, ]); return ( {t('common.save')} } isLoading={loading} loadingLabel={t('common.loading')} > {error &&
{error}
} {invalidIndexParam || invalidIndex ? (
{t('common.invalid_provider_index')}
) : ( <> setForm((prev) => ({ ...prev, apiKey: e.target.value }))} disabled={disableControls || saving} /> setForm((prev) => ({ ...prev, prefix: e.target.value }))} hint={t('ai_providers.prefix_hint')} disabled={disableControls || saving} /> setForm((prev) => ({ ...prev, baseUrl: e.target.value }))} disabled={disableControls || saving} /> setForm((prev) => ({ ...prev, proxyUrl: e.target.value }))} disabled={disableControls || saving} /> setForm((prev) => ({ ...prev, headers: entries }))} addLabel={t('common.custom_headers_add')} keyPlaceholder={t('common.custom_headers_key_placeholder')} valuePlaceholder={t('common.custom_headers_value_placeholder')} removeButtonTitle={t('common.delete')} removeButtonAriaLabel={t('common.delete')} disabled={disableControls || saving} />