feat: replace AI provider modals with dedicated edit pages

This commit is contained in:
LTbinglingfeng
2026-01-30 01:30:36 +08:00
parent 34b6d114d3
commit 5c85df486e
23 changed files with 2536 additions and 645 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { entriesToModels } from '@/components/ui/ModelInputList';
import { useNavigate } from 'react-router-dom';
import {
AmpcodeSection,
ClaudeSection,
@@ -9,25 +9,19 @@ import {
OpenAISection,
VertexSection,
useProviderStats,
type GeminiFormState,
type OpenAIFormState,
type ProviderFormState,
type ProviderModal,
type VertexFormState,
} from '@/components/providers';
import {
parseExcludedModels,
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 { buildHeaderObject } from '@/utils/headers';
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);
@@ -46,10 +40,7 @@ export function AiProvidersPage() {
const [vertexConfigs, setVertexConfigs] = useState<ProviderKeyConfig[]>([]);
const [openaiProviders, setOpenaiProviders] = useState<OpenAIProviderConfig[]>([]);
const [saving, setSaving] = useState(false);
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
const [modal, setModal] = useState<ProviderModal | null>(null);
const [ampcodeBusy, setAmpcodeBusy] = useState(false);
const disableControls = connectionStatus !== 'connected';
const isSwitching = Boolean(configSwitchingKey);
@@ -120,62 +111,12 @@ export function AiProvidersPage() {
config?.openaiCompatibility,
]);
const closeModal = () => {
setModal(null);
};
const openGeminiModal = (index: number | null) => {
setModal({ type: 'gemini', index });
};
const openProviderModal = (type: 'codex' | 'claude', index: number | null) => {
setModal({ type, index });
};
const openVertexModal = (index: number | null) => {
setModal({ type: 'vertex', index });
};
const openAmpcodeModal = () => {
setModal({ type: 'ampcode', index: null });
};
const openOpenaiModal = (index: number | null) => {
setModal({ type: 'openai', index });
};
const saveGemini = async (form: GeminiFormState, editIndex: number | null) => {
setSaving(true);
try {
const payload: GeminiKeyConfig = {
apiKey: form.apiKey.trim(),
prefix: form.prefix?.trim() || undefined,
baseUrl: form.baseUrl?.trim() || undefined,
headers: buildHeaderObject(form.headers),
excludedModels: parseExcludedModels(form.excludedText),
};
const nextList =
editIndex !== null
? geminiKeys.map((item, idx) => (idx === editIndex ? payload : item))
: [...geminiKeys, payload];
await providersApi.saveGeminiKeys(nextList);
setGeminiKeys(nextList);
updateConfigValue('gemini-api-key', nextList);
clearCache('gemini-api-key');
const message =
editIndex !== null
? t('notification.gemini_key_updated')
: t('notification.gemini_key_added');
showNotification(message, 'success');
closeModal();
} catch (err: unknown) {
const message = getErrorMessage(err);
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
} finally {
setSaving(false);
}
};
const openEditor = useCallback(
(path: string) => {
navigate(path, { state: { fromAiProviders: true } });
},
[navigate]
);
const deleteGemini = async (index: number) => {
const entry = geminiKeys[index];
@@ -293,68 +234,6 @@ export function AiProvidersPage() {
}
};
const saveProvider = async (
type: 'codex' | 'claude',
form: ProviderFormState,
editIndex: number | null
) => {
const trimmedBaseUrl = (form.baseUrl ?? '').trim();
const baseUrl = trimmedBaseUrl || undefined;
if (type === 'codex' && !baseUrl) {
showNotification(t('notification.codex_base_url_required'), 'error');
return;
}
setSaving(true);
try {
const source = type === 'codex' ? codexConfigs : claudeConfigs;
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
? source.map((item, idx) => (idx === editIndex ? payload : item))
: [...source, payload];
if (type === 'codex') {
await providersApi.saveCodexConfigs(nextList);
setCodexConfigs(nextList);
updateConfigValue('codex-api-key', nextList);
clearCache('codex-api-key');
const message =
editIndex !== null
? t('notification.codex_config_updated')
: t('notification.codex_config_added');
showNotification(message, 'success');
} else {
await providersApi.saveClaudeConfigs(nextList);
setClaudeConfigs(nextList);
updateConfigValue('claude-api-key', nextList);
clearCache('claude-api-key');
const message =
editIndex !== null
? t('notification.claude_config_updated')
: t('notification.claude_config_added');
showNotification(message, 'success');
}
closeModal();
} catch (err: unknown) {
const message = getErrorMessage(err);
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
} finally {
setSaving(false);
}
};
const deleteProviderEntry = async (type: 'codex' | 'claude', index: number) => {
const source = type === 'codex' ? codexConfigs : claudeConfigs;
const entry = source[index];
@@ -389,55 +268,6 @@ export function AiProvidersPage() {
});
};
const saveVertex = async (form: VertexFormState, editIndex: number | null) => {
const trimmedBaseUrl = (form.baseUrl ?? '').trim();
const baseUrl = trimmedBaseUrl || undefined;
if (!baseUrl) {
showNotification(t('notification.vertex_base_url_required'), 'error');
return;
}
setSaving(true);
try {
const payload: ProviderKeyConfig = {
apiKey: form.apiKey.trim(),
prefix: form.prefix?.trim() || undefined,
baseUrl,
proxyUrl: form.proxyUrl?.trim() || undefined,
headers: buildHeaderObject(form.headers),
models: form.modelEntries
.map((entry) => {
const name = entry.name.trim();
const alias = entry.alias.trim();
if (!name || !alias) return null;
return { name, alias };
})
.filter(Boolean) as ProviderKeyConfig['models'],
};
const nextList =
editIndex !== null
? vertexConfigs.map((item, idx) => (idx === editIndex ? payload : item))
: [...vertexConfigs, payload];
await providersApi.saveVertexConfigs(nextList);
setVertexConfigs(nextList);
updateConfigValue('vertex-api-key', nextList);
clearCache('vertex-api-key');
const message =
editIndex !== null
? t('notification.vertex_config_updated')
: t('notification.vertex_config_added');
showNotification(message, 'success');
closeModal();
} catch (err: unknown) {
const message = getErrorMessage(err);
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
} finally {
setSaving(false);
}
};
const deleteVertex = async (index: number) => {
const entry = vertexConfigs[index];
if (!entry) return;
@@ -462,47 +292,6 @@ export function AiProvidersPage() {
});
};
const saveOpenai = async (form: OpenAIFormState, editIndex: number | null) => {
setSaving(true);
try {
const payload: OpenAIProviderConfig = {
name: form.name.trim(),
prefix: form.prefix?.trim() || undefined,
baseUrl: form.baseUrl.trim(),
headers: buildHeaderObject(form.headers),
apiKeyEntries: form.apiKeyEntries.map((entry) => ({
apiKey: entry.apiKey.trim(),
proxyUrl: entry.proxyUrl?.trim() || undefined,
headers: entry.headers,
})),
};
if (form.testModel) payload.testModel = form.testModel.trim();
const models = entriesToModels(form.modelEntries);
if (models.length) payload.models = models;
const nextList =
editIndex !== null
? openaiProviders.map((item, idx) => (idx === editIndex ? payload : item))
: [...openaiProviders, payload];
await providersApi.saveOpenAIProviders(nextList);
setOpenaiProviders(nextList);
updateConfigValue('openai-compatibility', nextList);
clearCache('openai-compatibility');
const message =
editIndex !== null
? t('notification.openai_provider_updated')
: t('notification.openai_provider_added');
showNotification(message, 'success');
closeModal();
} catch (err: unknown) {
const message = getErrorMessage(err);
showNotification(`${t('notification.update_failed')}: ${message}`, 'error');
} finally {
setSaving(false);
}
};
const deleteOpenai = async (index: number) => {
const entry = openaiProviders[index];
if (!entry) return;
@@ -527,12 +316,6 @@ export function AiProvidersPage() {
});
};
const geminiModalIndex = modal?.type === 'gemini' ? modal.index : null;
const codexModalIndex = modal?.type === 'codex' ? modal.index : null;
const claudeModalIndex = modal?.type === 'claude' ? modal.index : null;
const vertexModalIndex = modal?.type === 'vertex' ? modal.index : null;
const openaiModalIndex = modal?.type === 'openai' ? modal.index : null;
return (
<div className={styles.container}>
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
@@ -545,16 +328,11 @@ export function AiProvidersPage() {
usageDetails={usageDetails}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
isModalOpen={modal?.type === 'gemini'}
modalIndex={geminiModalIndex}
onAdd={() => openGeminiModal(null)}
onEdit={(index) => openGeminiModal(index)}
onAdd={() => openEditor('/ai-providers/gemini/new')}
onEdit={(index) => openEditor(`/ai-providers/gemini/${index}`)}
onDelete={deleteGemini}
onToggle={(index, enabled) => void setConfigEnabled('gemini', index, enabled)}
onCloseModal={closeModal}
onSave={saveGemini}
/>
<CodexSection
@@ -563,17 +341,12 @@ export function AiProvidersPage() {
usageDetails={usageDetails}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
resolvedTheme={resolvedTheme}
isModalOpen={modal?.type === 'codex'}
modalIndex={codexModalIndex}
onAdd={() => openProviderModal('codex', null)}
onEdit={(index) => openProviderModal('codex', index)}
onAdd={() => 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)}
onCloseModal={closeModal}
onSave={(form, editIndex) => saveProvider('codex', form, editIndex)}
/>
<ClaudeSection
@@ -582,16 +355,11 @@ export function AiProvidersPage() {
usageDetails={usageDetails}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
isModalOpen={modal?.type === 'claude'}
modalIndex={claudeModalIndex}
onAdd={() => openProviderModal('claude', null)}
onEdit={(index) => openProviderModal('claude', index)}
onAdd={() => 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)}
onCloseModal={closeModal}
onSave={(form, editIndex) => saveProvider('claude', form, editIndex)}
/>
<VertexSection
@@ -600,28 +368,18 @@ export function AiProvidersPage() {
usageDetails={usageDetails}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
isModalOpen={modal?.type === 'vertex'}
modalIndex={vertexModalIndex}
onAdd={() => openVertexModal(null)}
onEdit={(index) => openVertexModal(index)}
onAdd={() => openEditor('/ai-providers/vertex/new')}
onEdit={(index) => openEditor(`/ai-providers/vertex/${index}`)}
onDelete={deleteVertex}
onCloseModal={closeModal}
onSave={saveVertex}
/>
<AmpcodeSection
config={config?.ampcode}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
isBusy={ampcodeBusy}
isModalOpen={modal?.type === 'ampcode'}
onOpen={openAmpcodeModal}
onCloseModal={closeModal}
onBusyChange={setAmpcodeBusy}
onEdit={() => openEditor('/ai-providers/ampcode')}
/>
<OpenAISection
@@ -630,16 +388,11 @@ export function AiProvidersPage() {
usageDetails={usageDetails}
loading={loading}
disableControls={disableControls}
isSaving={saving}
isSwitching={isSwitching}
resolvedTheme={resolvedTheme}
isModalOpen={modal?.type === 'openai'}
modalIndex={openaiModalIndex}
onAdd={() => openOpenaiModal(null)}
onEdit={(index) => openOpenaiModal(index)}
onAdd={() => openEditor('/ai-providers/openai/new')}
onEdit={(index) => openEditor(`/ai-providers/openai/${index}`)}
onDelete={deleteOpenai}
onCloseModal={closeModal}
onSave={saveOpenai}
/>
</div>
</div>