mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 11:10:49 +08:00
feat: replace AI provider modals with dedicated edit pages
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user