mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-03 11:20:50 +08:00
207 lines
8.1 KiB
TypeScript
207 lines
8.1 KiB
TypeScript
import { Fragment, useMemo } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Button } from '@/components/ui/Button';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { IconCheck, IconX } from '@/components/ui/icons';
|
|
import iconOpenaiLight from '@/assets/icons/openai-light.svg';
|
|
import iconOpenaiDark from '@/assets/icons/openai-dark.svg';
|
|
import type { OpenAIProviderConfig } from '@/types';
|
|
import { maskApiKey } from '@/utils/format';
|
|
import { calculateStatusBarData, type KeyStats, type UsageDetail } from '@/utils/usage';
|
|
import styles from '@/pages/AiProvidersPage.module.scss';
|
|
import { ProviderList } from '../ProviderList';
|
|
import { ProviderStatusBar } from '../ProviderStatusBar';
|
|
import { getOpenAIProviderStats, getStatsBySource } from '../utils';
|
|
import type { OpenAIFormState } from '../types';
|
|
import { OpenAIModal } from './OpenAIModal';
|
|
|
|
interface OpenAISectionProps {
|
|
configs: OpenAIProviderConfig[];
|
|
keyStats: KeyStats;
|
|
usageDetails: UsageDetail[];
|
|
loading: boolean;
|
|
disableControls: boolean;
|
|
isSaving: boolean;
|
|
isSwitching: boolean;
|
|
resolvedTheme: string;
|
|
isModalOpen: boolean;
|
|
modalIndex: number | null;
|
|
onAdd: () => void;
|
|
onEdit: (index: number) => void;
|
|
onDelete: (index: number) => void;
|
|
onCloseModal: () => void;
|
|
onSave: (data: OpenAIFormState, index: number | null) => Promise<void>;
|
|
}
|
|
|
|
export function OpenAISection({
|
|
configs,
|
|
keyStats,
|
|
usageDetails,
|
|
loading,
|
|
disableControls,
|
|
isSaving,
|
|
isSwitching,
|
|
resolvedTheme,
|
|
isModalOpen,
|
|
modalIndex,
|
|
onAdd,
|
|
onEdit,
|
|
onDelete,
|
|
onCloseModal,
|
|
onSave,
|
|
}: OpenAISectionProps) {
|
|
const { t } = useTranslation();
|
|
const actionsDisabled = disableControls || isSaving || isSwitching;
|
|
|
|
const statusBarCache = useMemo(() => {
|
|
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
|
|
|
configs.forEach((provider) => {
|
|
const allKeys = (provider.apiKeyEntries || []).map((entry) => entry.apiKey).filter(Boolean);
|
|
const filteredDetails = usageDetails.filter((detail) => allKeys.includes(detail.source));
|
|
cache.set(provider.name, calculateStatusBarData(filteredDetails));
|
|
});
|
|
|
|
return cache;
|
|
}, [configs, usageDetails]);
|
|
|
|
const initialData = modalIndex !== null ? configs[modalIndex] : undefined;
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
title={
|
|
<span className={styles.cardTitle}>
|
|
<img
|
|
src={resolvedTheme === 'dark' ? iconOpenaiDark : iconOpenaiLight}
|
|
alt=""
|
|
className={styles.cardTitleIcon}
|
|
/>
|
|
{t('ai_providers.openai_title')}
|
|
</span>
|
|
}
|
|
extra={
|
|
<Button size="sm" onClick={onAdd} disabled={actionsDisabled}>
|
|
{t('ai_providers.openai_add_button')}
|
|
</Button>
|
|
}
|
|
>
|
|
<ProviderList<OpenAIProviderConfig>
|
|
items={configs}
|
|
loading={loading}
|
|
keyField={(item) => item.name}
|
|
emptyTitle={t('ai_providers.openai_empty_title')}
|
|
emptyDescription={t('ai_providers.openai_empty_desc')}
|
|
onEdit={onEdit}
|
|
onDelete={onDelete}
|
|
actionsDisabled={actionsDisabled}
|
|
renderContent={(item) => {
|
|
const stats = getOpenAIProviderStats(item.apiKeyEntries, keyStats, maskApiKey);
|
|
const headerEntries = Object.entries(item.headers || {});
|
|
const apiKeyEntries = item.apiKeyEntries || [];
|
|
const statusData = statusBarCache.get(item.name) || calculateStatusBarData([]);
|
|
|
|
return (
|
|
<Fragment>
|
|
<div className="item-title">{item.name}</div>
|
|
{item.prefix && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
|
|
<span className={styles.fieldValue}>{item.prefix}</span>
|
|
</div>
|
|
)}
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.base_url')}:</span>
|
|
<span className={styles.fieldValue}>{item.baseUrl}</span>
|
|
</div>
|
|
{headerEntries.length > 0 && (
|
|
<div className={styles.headerBadgeList}>
|
|
{headerEntries.map(([key, value]) => (
|
|
<span key={key} className={styles.headerBadge}>
|
|
<strong>{key}:</strong> {value}
|
|
</span>
|
|
))}
|
|
</div>
|
|
)}
|
|
{apiKeyEntries.length > 0 && (
|
|
<div className={styles.apiKeyEntriesSection}>
|
|
<div className={styles.apiKeyEntriesLabel}>
|
|
{t('ai_providers.openai_keys_count')}: {apiKeyEntries.length}
|
|
</div>
|
|
<div className={styles.apiKeyEntryList}>
|
|
{apiKeyEntries.map((entry, entryIndex) => {
|
|
const entryStats = getStatsBySource(entry.apiKey, keyStats, maskApiKey);
|
|
return (
|
|
<div key={entryIndex} className={styles.apiKeyEntryCard}>
|
|
<span className={styles.apiKeyEntryIndex}>{entryIndex + 1}</span>
|
|
<span className={styles.apiKeyEntryKey}>{maskApiKey(entry.apiKey)}</span>
|
|
{entry.proxyUrl && (
|
|
<span className={styles.apiKeyEntryProxy}>{entry.proxyUrl}</span>
|
|
)}
|
|
<div className={styles.apiKeyEntryStats}>
|
|
<span
|
|
className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatSuccess}`}
|
|
>
|
|
<IconCheck size={12} /> {entryStats.success}
|
|
</span>
|
|
<span
|
|
className={`${styles.apiKeyEntryStat} ${styles.apiKeyEntryStatFailure}`}
|
|
>
|
|
<IconX size={12} /> {entryStats.failure}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className={styles.fieldRow} style={{ marginTop: '8px' }}>
|
|
<span className={styles.fieldLabel}>{t('ai_providers.openai_models_count')}:</span>
|
|
<span className={styles.fieldValue}>{item.models?.length || 0}</span>
|
|
</div>
|
|
{item.models?.length ? (
|
|
<div className={styles.modelTagList}>
|
|
{item.models.map((model) => (
|
|
<span key={model.name} className={styles.modelTag}>
|
|
<span className={styles.modelName}>{model.name}</span>
|
|
{model.alias && model.alias !== model.name && (
|
|
<span className={styles.modelAlias}>{model.alias}</span>
|
|
)}
|
|
</span>
|
|
))}
|
|
</div>
|
|
) : null}
|
|
{item.testModel && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>Test Model:</span>
|
|
<span className={styles.fieldValue}>{item.testModel}</span>
|
|
</div>
|
|
)}
|
|
<div className={styles.cardStats}>
|
|
<span className={`${styles.statPill} ${styles.statSuccess}`}>
|
|
{t('stats.success')}: {stats.success}
|
|
</span>
|
|
<span className={`${styles.statPill} ${styles.statFailure}`}>
|
|
{t('stats.failure')}: {stats.failure}
|
|
</span>
|
|
</div>
|
|
<ProviderStatusBar statusData={statusData} />
|
|
</Fragment>
|
|
);
|
|
}}
|
|
/>
|
|
</Card>
|
|
|
|
<OpenAIModal
|
|
isOpen={isModalOpen}
|
|
editIndex={modalIndex}
|
|
initialData={initialData}
|
|
onClose={onCloseModal}
|
|
onSave={onSave}
|
|
isSaving={isSaving}
|
|
/>
|
|
</>
|
|
);
|
|
}
|