mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
214 lines
8.5 KiB
TypeScript
214 lines
8.5 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 { ToggleSwitch } from '@/components/ui/ToggleSwitch';
|
|
import iconCodex from '@/assets/icons/codex.svg';
|
|
import type { ProviderKeyConfig } from '@/types';
|
|
import { maskApiKey } from '@/utils/format';
|
|
import { calculateStatusBarData, type KeyStats } from '@/utils/usage';
|
|
import { type UsageDetailsByAuthIndex, type UsageDetailsBySource } from '@/utils/usageIndex';
|
|
import styles from '@/pages/AiProvidersPage.module.scss';
|
|
import { ProviderList } from '../ProviderList';
|
|
import { ProviderStatusBar } from '../ProviderStatusBar';
|
|
import {
|
|
collectUsageDetailsForIdentity,
|
|
getProviderConfigKey,
|
|
getStatsForIdentity,
|
|
hasDisableAllModelsRule,
|
|
} from '../utils';
|
|
|
|
interface CodexSectionProps {
|
|
configs: ProviderKeyConfig[];
|
|
keyStats: KeyStats;
|
|
usageDetailsBySource: UsageDetailsBySource;
|
|
usageDetailsByAuthIndex: UsageDetailsByAuthIndex;
|
|
loading: boolean;
|
|
disableControls: boolean;
|
|
isSwitching: boolean;
|
|
onAdd: () => void;
|
|
onEdit: (index: number) => void;
|
|
onDelete: (index: number) => void;
|
|
onToggle: (index: number, enabled: boolean) => void;
|
|
}
|
|
|
|
export function CodexSection({
|
|
configs,
|
|
keyStats,
|
|
usageDetailsBySource,
|
|
usageDetailsByAuthIndex,
|
|
loading,
|
|
disableControls,
|
|
isSwitching,
|
|
onAdd,
|
|
onEdit,
|
|
onDelete,
|
|
onToggle,
|
|
}: CodexSectionProps) {
|
|
const { t } = useTranslation();
|
|
const actionsDisabled = disableControls || loading || isSwitching;
|
|
const toggleDisabled = disableControls || loading || isSwitching;
|
|
|
|
const statusBarCache = useMemo(() => {
|
|
const cache = new Map<string, ReturnType<typeof calculateStatusBarData>>();
|
|
|
|
configs.forEach((config, index) => {
|
|
if (!config.apiKey) return;
|
|
const configKey = getProviderConfigKey(config, index);
|
|
cache.set(
|
|
configKey,
|
|
calculateStatusBarData(
|
|
collectUsageDetailsForIdentity(
|
|
{ authIndex: config.authIndex, apiKey: config.apiKey, prefix: config.prefix },
|
|
usageDetailsBySource,
|
|
usageDetailsByAuthIndex
|
|
)
|
|
)
|
|
);
|
|
});
|
|
|
|
return cache;
|
|
}, [configs, usageDetailsByAuthIndex, usageDetailsBySource]);
|
|
|
|
return (
|
|
<>
|
|
<Card
|
|
title={
|
|
<span className={styles.cardTitle}>
|
|
<img src={iconCodex} alt="" className={styles.cardTitleIcon} />
|
|
{t('ai_providers.codex_title')}
|
|
</span>
|
|
}
|
|
extra={
|
|
<Button size="sm" onClick={onAdd} disabled={actionsDisabled}>
|
|
{t('ai_providers.codex_add_button')}
|
|
</Button>
|
|
}
|
|
>
|
|
<ProviderList<ProviderKeyConfig>
|
|
items={configs}
|
|
loading={loading}
|
|
keyField={(item, index) => getProviderConfigKey(item, index)}
|
|
emptyTitle={t('ai_providers.codex_empty_title')}
|
|
emptyDescription={t('ai_providers.codex_empty_desc')}
|
|
onEdit={(_, index) => onEdit(index)}
|
|
onDelete={(_, index) => onDelete(index)}
|
|
actionsDisabled={actionsDisabled}
|
|
getRowDisabled={(item) => hasDisableAllModelsRule(item.excludedModels)}
|
|
renderExtraActions={(item, index) => (
|
|
<ToggleSwitch
|
|
label={t('ai_providers.config_toggle_label')}
|
|
checked={!hasDisableAllModelsRule(item.excludedModels)}
|
|
disabled={toggleDisabled}
|
|
onChange={(value) => void onToggle(index, value)}
|
|
/>
|
|
)}
|
|
renderContent={(item, index) => {
|
|
const stats = getStatsForIdentity(
|
|
{ authIndex: item.authIndex, apiKey: item.apiKey, prefix: item.prefix },
|
|
keyStats
|
|
);
|
|
const headerEntries = Object.entries(item.headers || {});
|
|
const configDisabled = hasDisableAllModelsRule(item.excludedModels);
|
|
const excludedModels = item.excludedModels ?? [];
|
|
const statusData =
|
|
statusBarCache.get(getProviderConfigKey(item, index)) || calculateStatusBarData([]);
|
|
|
|
return (
|
|
<Fragment>
|
|
<div className="item-title">{t('ai_providers.codex_item_title')}</div>
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.api_key')}:</span>
|
|
<span className={styles.fieldValue}>{maskApiKey(item.apiKey)}</span>
|
|
</div>
|
|
{item.priority !== undefined && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.priority')}:</span>
|
|
<span className={styles.fieldValue}>{item.priority}</span>
|
|
</div>
|
|
)}
|
|
{item.prefix && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.prefix')}:</span>
|
|
<span className={styles.fieldValue}>{item.prefix}</span>
|
|
</div>
|
|
)}
|
|
{item.baseUrl && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.base_url')}:</span>
|
|
<span className={styles.fieldValue}>{item.baseUrl}</span>
|
|
</div>
|
|
)}
|
|
{item.proxyUrl && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('common.proxy_url')}:</span>
|
|
<span className={styles.fieldValue}>{item.proxyUrl}</span>
|
|
</div>
|
|
)}
|
|
{item.websockets !== undefined && (
|
|
<div className={styles.fieldRow}>
|
|
<span className={styles.fieldLabel}>{t('ai_providers.codex_websockets_label')}:</span>
|
|
<span className={styles.fieldValue}>{item.websockets ? t('common.yes') : t('common.no')}</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>
|
|
)}
|
|
{configDisabled && (
|
|
<div className="status-badge warning" style={{ marginTop: 8, marginBottom: 0 }}>
|
|
{t('ai_providers.config_disabled_badge')}
|
|
</div>
|
|
)}
|
|
{item.models?.length ? (
|
|
<div className={styles.modelTagList}>
|
|
<span className={styles.modelCountLabel}>
|
|
{t('ai_providers.codex_models_count')}: {item.models.length}
|
|
</span>
|
|
{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}
|
|
{excludedModels.length ? (
|
|
<div className={styles.excludedModelsSection}>
|
|
<div className={styles.excludedModelsLabel}>
|
|
{t('ai_providers.excluded_models_count', { count: excludedModels.length })}
|
|
</div>
|
|
<div className={styles.modelTagList}>
|
|
{excludedModels.map((model) => (
|
|
<span key={model} className={`${styles.modelTag} ${styles.excludedModelTag}`}>
|
|
<span className={styles.modelName}>{model}</span>
|
|
</span>
|
|
))}
|
|
</div>
|
|
</div>
|
|
) : null}
|
|
<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>
|
|
</>
|
|
);
|
|
}
|