feat(aiProviders): config search and upstream sync
Changes: - Add global search supporting filters by API key, base URL, model name, alias, headers, etc. - Refactor provider sections with useMemo for efficient filtering logic - Add responsive page header layout with mobile-friendly search box - Add empty state UI for no search results (title + description) - i18n: add search_placeholder, search_empty_title, search_empty_desc entries - Sync and merge latest upstream/dev branch
This commit is contained in:
@@ -325,7 +325,10 @@
|
|||||||
"openai_test_success": "Test succeeded. The model responded.",
|
"openai_test_success": "Test succeeded. The model responded.",
|
||||||
"openai_test_failed": "Test failed",
|
"openai_test_failed": "Test failed",
|
||||||
"openai_test_select_placeholder": "Choose from current models",
|
"openai_test_select_placeholder": "Choose from current models",
|
||||||
"openai_test_select_empty": "No models configured. Add models first"
|
"openai_test_select_empty": "No models configured. Add models first",
|
||||||
|
"search_placeholder": "Search configs (keys, URLs, models...)",
|
||||||
|
"search_empty_title": "No matching configs",
|
||||||
|
"search_empty_desc": "Try a different keyword or clear the search box"
|
||||||
},
|
},
|
||||||
"auth_files": {
|
"auth_files": {
|
||||||
"title": "Auth Files Management",
|
"title": "Auth Files Management",
|
||||||
|
|||||||
@@ -325,7 +325,10 @@
|
|||||||
"openai_test_success": "测试成功,模型可用。",
|
"openai_test_success": "测试成功,模型可用。",
|
||||||
"openai_test_failed": "测试失败",
|
"openai_test_failed": "测试失败",
|
||||||
"openai_test_select_placeholder": "从当前模型列表选择",
|
"openai_test_select_placeholder": "从当前模型列表选择",
|
||||||
"openai_test_select_empty": "当前未配置模型,请先添加模型"
|
"openai_test_select_empty": "当前未配置模型,请先添加模型",
|
||||||
|
"search_placeholder": "搜索配置(密钥、地址、模型等)",
|
||||||
|
"search_empty_title": "没有匹配的配置",
|
||||||
|
"search_empty_desc": "请尝试更换关键字或清空搜索框"
|
||||||
},
|
},
|
||||||
"auth_files": {
|
"auth_files": {
|
||||||
"title": "认证文件管理",
|
"title": "认证文件管理",
|
||||||
|
|||||||
@@ -20,7 +20,53 @@
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin: 0 0 $spacing-xl 0;
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pageHeader {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: $spacing-md;
|
||||||
|
margin-bottom: $spacing-xl;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchBox {
|
||||||
|
flex: 0 1 320px;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
@include mobile {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.form-group) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchEmpty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $spacing-xl * 2;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px dashed var(--border-primary);
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchEmptyTitle {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchEmptyDesc {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { entriesToModels } from '@/components/ui/ModelInputList';
|
import { entriesToModels } from '@/components/ui/ModelInputList';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
withDisableAllModelsRule,
|
withDisableAllModelsRule,
|
||||||
withoutDisableAllModelsRule,
|
withoutDisableAllModelsRule,
|
||||||
} from '@/components/providers/utils';
|
} from '@/components/providers/utils';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
import { ampcodeApi, providersApi } from '@/services/api';
|
import { ampcodeApi, providersApi } from '@/services/api';
|
||||||
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
import { useAuthStore, useConfigStore, useNotificationStore, useThemeStore } from '@/stores';
|
||||||
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
import type { GeminiKeyConfig, OpenAIProviderConfig, ProviderKeyConfig } from '@/types';
|
||||||
@@ -50,10 +51,132 @@ export function AiProvidersPage() {
|
|||||||
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
|
const [configSwitchingKey, setConfigSwitchingKey] = useState<string | null>(null);
|
||||||
const [modal, setModal] = useState<ProviderModal | null>(null);
|
const [modal, setModal] = useState<ProviderModal | null>(null);
|
||||||
const [ampcodeBusy, setAmpcodeBusy] = useState(false);
|
const [ampcodeBusy, setAmpcodeBusy] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
const disableControls = connectionStatus !== 'connected';
|
const disableControls = connectionStatus !== 'connected';
|
||||||
const isSwitching = Boolean(configSwitchingKey);
|
const isSwitching = Boolean(configSwitchingKey);
|
||||||
|
|
||||||
|
const normalizedQuery = searchQuery.trim().toLowerCase();
|
||||||
|
|
||||||
|
const filteredGeminiKeys = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return geminiKeys.map((item, index) => ({ item, originalIndex: index }));
|
||||||
|
return geminiKeys
|
||||||
|
.map((item, index) => ({ item, originalIndex: index }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
const searchFields = [
|
||||||
|
item.apiKey,
|
||||||
|
item.prefix,
|
||||||
|
item.baseUrl,
|
||||||
|
...(item.excludedModels || []),
|
||||||
|
...Object.keys(item.headers || {}),
|
||||||
|
...Object.values(item.headers || {}),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
}, [geminiKeys, normalizedQuery]);
|
||||||
|
|
||||||
|
const filteredCodexConfigs = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return codexConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||||
|
return codexConfigs
|
||||||
|
.map((item, index) => ({ item, originalIndex: index }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
const searchFields = [
|
||||||
|
item.apiKey,
|
||||||
|
item.prefix,
|
||||||
|
item.baseUrl,
|
||||||
|
item.proxyUrl,
|
||||||
|
...(item.excludedModels || []),
|
||||||
|
...(item.models?.map((m) => m.name) || []),
|
||||||
|
...(item.models?.map((m) => m.alias) || []),
|
||||||
|
...Object.keys(item.headers || {}),
|
||||||
|
...Object.values(item.headers || {}),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
}, [codexConfigs, normalizedQuery]);
|
||||||
|
|
||||||
|
const filteredClaudeConfigs = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return claudeConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||||
|
return claudeConfigs
|
||||||
|
.map((item, index) => ({ item, originalIndex: index }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
const searchFields = [
|
||||||
|
item.apiKey,
|
||||||
|
item.prefix,
|
||||||
|
item.baseUrl,
|
||||||
|
item.proxyUrl,
|
||||||
|
...(item.excludedModels || []),
|
||||||
|
...(item.models?.map((m) => m.name) || []),
|
||||||
|
...(item.models?.map((m) => m.alias) || []),
|
||||||
|
...Object.keys(item.headers || {}),
|
||||||
|
...Object.values(item.headers || {}),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
}, [claudeConfigs, normalizedQuery]);
|
||||||
|
|
||||||
|
const filteredVertexConfigs = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return vertexConfigs.map((item, index) => ({ item, originalIndex: index }));
|
||||||
|
return vertexConfigs
|
||||||
|
.map((item, index) => ({ item, originalIndex: index }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
const searchFields = [
|
||||||
|
item.apiKey,
|
||||||
|
item.prefix,
|
||||||
|
item.baseUrl,
|
||||||
|
item.proxyUrl,
|
||||||
|
...(item.models?.map((m) => m.name) || []),
|
||||||
|
...(item.models?.map((m) => m.alias) || []),
|
||||||
|
...Object.keys(item.headers || {}),
|
||||||
|
...Object.values(item.headers || {}),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
}, [vertexConfigs, normalizedQuery]);
|
||||||
|
|
||||||
|
const filteredOpenaiProviders = useMemo(() => {
|
||||||
|
if (!normalizedQuery)
|
||||||
|
return openaiProviders.map((item, index) => ({ item, originalIndex: index }));
|
||||||
|
return openaiProviders
|
||||||
|
.map((item, index) => ({ item, originalIndex: index }))
|
||||||
|
.filter(({ item }) => {
|
||||||
|
const searchFields = [
|
||||||
|
item.name,
|
||||||
|
item.prefix,
|
||||||
|
item.baseUrl,
|
||||||
|
item.testModel,
|
||||||
|
...(item.apiKeyEntries?.map((e) => e.apiKey) || []),
|
||||||
|
...(item.apiKeyEntries?.map((e) => e.proxyUrl) || []),
|
||||||
|
...(item.models?.map((m) => m.name) || []),
|
||||||
|
...(item.models?.map((m) => m.alias) || []),
|
||||||
|
...Object.keys(item.headers || {}),
|
||||||
|
...Object.values(item.headers || {}),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
});
|
||||||
|
}, [openaiProviders, normalizedQuery]);
|
||||||
|
|
||||||
|
const showAmpcode = useMemo(() => {
|
||||||
|
if (!normalizedQuery) return true;
|
||||||
|
const ampcode = config?.ampcode;
|
||||||
|
if (!ampcode) return false;
|
||||||
|
const searchFields = [
|
||||||
|
ampcode.upstreamUrl,
|
||||||
|
ampcode.upstreamApiKey,
|
||||||
|
...(ampcode.modelMappings?.map((m) => m.from) || []),
|
||||||
|
...(ampcode.modelMappings?.map((m) => m.to) || []),
|
||||||
|
];
|
||||||
|
return searchFields.some((field) => field?.toLowerCase().includes(normalizedQuery));
|
||||||
|
}, [config?.ampcode, normalizedQuery]);
|
||||||
|
|
||||||
|
const hasSearchResults =
|
||||||
|
filteredGeminiKeys.length > 0 ||
|
||||||
|
filteredCodexConfigs.length > 0 ||
|
||||||
|
filteredClaudeConfigs.length > 0 ||
|
||||||
|
filteredVertexConfigs.length > 0 ||
|
||||||
|
filteredOpenaiProviders.length > 0 ||
|
||||||
|
showAmpcode;
|
||||||
|
|
||||||
const { keyStats, usageDetails, loadKeyStats } = useProviderStats();
|
const { keyStats, usageDetails, loadKeyStats } = useProviderStats();
|
||||||
|
|
||||||
const getErrorMessage = (err: unknown) => {
|
const getErrorMessage = (err: unknown) => {
|
||||||
@@ -507,112 +630,171 @@ export function AiProvidersPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
|
<div className={styles.pageHeader}>
|
||||||
|
<h1 className={styles.pageTitle}>{t('ai_providers.title')}</h1>
|
||||||
|
<div className={styles.searchBox}>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder={t('ai_providers.search_placeholder')}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
{error && <div className="error-box">{error}</div>}
|
{error && <div className="error-box">{error}</div>}
|
||||||
|
|
||||||
<GeminiSection
|
{normalizedQuery && !hasSearchResults && (
|
||||||
configs={geminiKeys}
|
<div className={styles.searchEmpty}>
|
||||||
keyStats={keyStats}
|
<div className={styles.searchEmptyTitle}>{t('ai_providers.search_empty_title')}</div>
|
||||||
usageDetails={usageDetails}
|
<div className={styles.searchEmptyDesc}>{t('ai_providers.search_empty_desc')}</div>
|
||||||
loading={loading}
|
</div>
|
||||||
disableControls={disableControls}
|
)}
|
||||||
isSaving={saving}
|
|
||||||
isSwitching={isSwitching}
|
|
||||||
isModalOpen={modal?.type === 'gemini'}
|
|
||||||
modalIndex={geminiModalIndex}
|
|
||||||
onAdd={() => openGeminiModal(null)}
|
|
||||||
onEdit={(index) => openGeminiModal(index)}
|
|
||||||
onDelete={deleteGemini}
|
|
||||||
onToggle={(index, enabled) => void setConfigEnabled('gemini', index, enabled)}
|
|
||||||
onCloseModal={closeModal}
|
|
||||||
onSave={saveGemini}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CodexSection
|
{filteredGeminiKeys.length > 0 && (
|
||||||
configs={codexConfigs}
|
<GeminiSection
|
||||||
keyStats={keyStats}
|
configs={filteredGeminiKeys.map(({ item }) => item)}
|
||||||
usageDetails={usageDetails}
|
keyStats={keyStats}
|
||||||
loading={loading}
|
usageDetails={usageDetails}
|
||||||
disableControls={disableControls}
|
loading={loading}
|
||||||
isSaving={saving}
|
disableControls={disableControls}
|
||||||
isSwitching={isSwitching}
|
isSaving={saving}
|
||||||
resolvedTheme={resolvedTheme}
|
isSwitching={isSwitching}
|
||||||
isModalOpen={modal?.type === 'codex'}
|
isModalOpen={modal?.type === 'gemini'}
|
||||||
modalIndex={codexModalIndex}
|
modalIndex={
|
||||||
onAdd={() => openProviderModal('codex', null)}
|
geminiModalIndex !== null
|
||||||
onEdit={(index) => openProviderModal('codex', index)}
|
? filteredGeminiKeys.findIndex(({ originalIndex }) => originalIndex === geminiModalIndex)
|
||||||
onDelete={(index) => void deleteProviderEntry('codex', index)}
|
: null
|
||||||
onToggle={(index, enabled) => void setConfigEnabled('codex', index, enabled)}
|
}
|
||||||
onCloseModal={closeModal}
|
onAdd={() => openGeminiModal(null)}
|
||||||
onSave={(form, editIndex) => saveProvider('codex', form, editIndex)}
|
onEdit={(index) => openGeminiModal(filteredGeminiKeys[index]?.originalIndex ?? index)}
|
||||||
/>
|
onDelete={(index) => deleteGemini(filteredGeminiKeys[index]?.originalIndex ?? index)}
|
||||||
|
onToggle={(index, enabled) =>
|
||||||
|
void setConfigEnabled('gemini', filteredGeminiKeys[index]?.originalIndex ?? index, enabled)
|
||||||
|
}
|
||||||
|
onCloseModal={closeModal}
|
||||||
|
onSave={saveGemini}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<ClaudeSection
|
{filteredCodexConfigs.length > 0 && (
|
||||||
configs={claudeConfigs}
|
<CodexSection
|
||||||
keyStats={keyStats}
|
configs={filteredCodexConfigs.map(({ item }) => item)}
|
||||||
usageDetails={usageDetails}
|
keyStats={keyStats}
|
||||||
loading={loading}
|
usageDetails={usageDetails}
|
||||||
disableControls={disableControls}
|
loading={loading}
|
||||||
isSaving={saving}
|
disableControls={disableControls}
|
||||||
isSwitching={isSwitching}
|
isSaving={saving}
|
||||||
isModalOpen={modal?.type === 'claude'}
|
isSwitching={isSwitching}
|
||||||
modalIndex={claudeModalIndex}
|
resolvedTheme={resolvedTheme}
|
||||||
onAdd={() => openProviderModal('claude', null)}
|
isModalOpen={modal?.type === 'codex'}
|
||||||
onEdit={(index) => openProviderModal('claude', index)}
|
modalIndex={
|
||||||
onDelete={(index) => void deleteProviderEntry('claude', index)}
|
codexModalIndex !== null
|
||||||
onToggle={(index, enabled) => void setConfigEnabled('claude', index, enabled)}
|
? filteredCodexConfigs.findIndex(({ originalIndex }) => originalIndex === codexModalIndex)
|
||||||
onCloseModal={closeModal}
|
: null
|
||||||
onSave={(form, editIndex) => saveProvider('claude', form, editIndex)}
|
}
|
||||||
/>
|
onAdd={() => openProviderModal('codex', null)}
|
||||||
|
onEdit={(index) => openProviderModal('codex', filteredCodexConfigs[index]?.originalIndex ?? index)}
|
||||||
|
onDelete={(index) =>
|
||||||
|
void deleteProviderEntry('codex', filteredCodexConfigs[index]?.originalIndex ?? index)
|
||||||
|
}
|
||||||
|
onToggle={(index, enabled) =>
|
||||||
|
void setConfigEnabled('codex', filteredCodexConfigs[index]?.originalIndex ?? index, enabled)
|
||||||
|
}
|
||||||
|
onCloseModal={closeModal}
|
||||||
|
onSave={(form, editIndex) => saveProvider('codex', form, editIndex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<VertexSection
|
{filteredClaudeConfigs.length > 0 && (
|
||||||
configs={vertexConfigs}
|
<ClaudeSection
|
||||||
keyStats={keyStats}
|
configs={filteredClaudeConfigs.map(({ item }) => item)}
|
||||||
usageDetails={usageDetails}
|
keyStats={keyStats}
|
||||||
loading={loading}
|
usageDetails={usageDetails}
|
||||||
disableControls={disableControls}
|
loading={loading}
|
||||||
isSaving={saving}
|
disableControls={disableControls}
|
||||||
isSwitching={isSwitching}
|
isSaving={saving}
|
||||||
isModalOpen={modal?.type === 'vertex'}
|
isSwitching={isSwitching}
|
||||||
modalIndex={vertexModalIndex}
|
isModalOpen={modal?.type === 'claude'}
|
||||||
onAdd={() => openVertexModal(null)}
|
modalIndex={
|
||||||
onEdit={(index) => openVertexModal(index)}
|
claudeModalIndex !== null
|
||||||
onDelete={deleteVertex}
|
? filteredClaudeConfigs.findIndex(({ originalIndex }) => originalIndex === claudeModalIndex)
|
||||||
onCloseModal={closeModal}
|
: null
|
||||||
onSave={saveVertex}
|
}
|
||||||
/>
|
onAdd={() => openProviderModal('claude', null)}
|
||||||
|
onEdit={(index) => openProviderModal('claude', filteredClaudeConfigs[index]?.originalIndex ?? index)}
|
||||||
|
onDelete={(index) =>
|
||||||
|
void deleteProviderEntry('claude', filteredClaudeConfigs[index]?.originalIndex ?? index)
|
||||||
|
}
|
||||||
|
onToggle={(index, enabled) =>
|
||||||
|
void setConfigEnabled('claude', filteredClaudeConfigs[index]?.originalIndex ?? index, enabled)
|
||||||
|
}
|
||||||
|
onCloseModal={closeModal}
|
||||||
|
onSave={(form, editIndex) => saveProvider('claude', form, editIndex)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<AmpcodeSection
|
{filteredVertexConfigs.length > 0 && (
|
||||||
config={config?.ampcode}
|
<VertexSection
|
||||||
loading={loading}
|
configs={filteredVertexConfigs.map(({ item }) => item)}
|
||||||
disableControls={disableControls}
|
keyStats={keyStats}
|
||||||
isSaving={saving}
|
usageDetails={usageDetails}
|
||||||
isSwitching={isSwitching}
|
loading={loading}
|
||||||
isBusy={ampcodeBusy}
|
disableControls={disableControls}
|
||||||
isModalOpen={modal?.type === 'ampcode'}
|
isSaving={saving}
|
||||||
onOpen={openAmpcodeModal}
|
isSwitching={isSwitching}
|
||||||
onCloseModal={closeModal}
|
isModalOpen={modal?.type === 'vertex'}
|
||||||
onBusyChange={setAmpcodeBusy}
|
modalIndex={
|
||||||
/>
|
vertexModalIndex !== null
|
||||||
|
? filteredVertexConfigs.findIndex(({ originalIndex }) => originalIndex === vertexModalIndex)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onAdd={() => openVertexModal(null)}
|
||||||
|
onEdit={(index) => openVertexModal(filteredVertexConfigs[index]?.originalIndex ?? index)}
|
||||||
|
onDelete={(index) => deleteVertex(filteredVertexConfigs[index]?.originalIndex ?? index)}
|
||||||
|
onCloseModal={closeModal}
|
||||||
|
onSave={saveVertex}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<OpenAISection
|
{showAmpcode && (
|
||||||
configs={openaiProviders}
|
<AmpcodeSection
|
||||||
keyStats={keyStats}
|
config={config?.ampcode}
|
||||||
usageDetails={usageDetails}
|
loading={loading}
|
||||||
loading={loading}
|
disableControls={disableControls}
|
||||||
disableControls={disableControls}
|
isSaving={saving}
|
||||||
isSaving={saving}
|
isSwitching={isSwitching}
|
||||||
isSwitching={isSwitching}
|
isBusy={ampcodeBusy}
|
||||||
resolvedTheme={resolvedTheme}
|
isModalOpen={modal?.type === 'ampcode'}
|
||||||
isModalOpen={modal?.type === 'openai'}
|
onOpen={openAmpcodeModal}
|
||||||
modalIndex={openaiModalIndex}
|
onCloseModal={closeModal}
|
||||||
onAdd={() => openOpenaiModal(null)}
|
onBusyChange={setAmpcodeBusy}
|
||||||
onEdit={(index) => openOpenaiModal(index)}
|
/>
|
||||||
onDelete={deleteOpenai}
|
)}
|
||||||
onCloseModal={closeModal}
|
|
||||||
onSave={saveOpenai}
|
{filteredOpenaiProviders.length > 0 && (
|
||||||
/>
|
<OpenAISection
|
||||||
|
configs={filteredOpenaiProviders.map(({ item }) => item)}
|
||||||
|
keyStats={keyStats}
|
||||||
|
usageDetails={usageDetails}
|
||||||
|
loading={loading}
|
||||||
|
disableControls={disableControls}
|
||||||
|
isSaving={saving}
|
||||||
|
isSwitching={isSwitching}
|
||||||
|
resolvedTheme={resolvedTheme}
|
||||||
|
isModalOpen={modal?.type === 'openai'}
|
||||||
|
modalIndex={
|
||||||
|
openaiModalIndex !== null
|
||||||
|
? filteredOpenaiProviders.findIndex(({ originalIndex }) => originalIndex === openaiModalIndex)
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
onAdd={() => openOpenaiModal(null)}
|
||||||
|
onEdit={(index) => openOpenaiModal(filteredOpenaiProviders[index]?.originalIndex ?? index)}
|
||||||
|
onDelete={(index) => deleteOpenai(filteredOpenaiProviders[index]?.originalIndex ?? index)}
|
||||||
|
onCloseModal={closeModal}
|
||||||
|
onSave={saveOpenai}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user