import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { getTokens, createToken, updateToken, deleteToken, getSiteKeyGroups, getSiteKeyGroupPricing, getTokenSupportedModels, } from '../api'; import ConfigExporter from '../components/ConfigExporter'; import { useCurrency } from '../context/SiteContext'; import toast from 'react-hot-toast'; export default function Tokens() { const { t } = useTranslation(); const { symbol, rate } = useCurrency(); const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); const [copiedId, setCopiedId] = useState(null); const [newKey, setNewKey] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(null); const [expandedTokens, setExpandedTokens] = useState({}); const [tokenModels, setTokenModels] = useState({}); // Key groups const [keyGroups, setKeyGroups] = useState([]); const [activePricingGroup, setActivePricingGroup] = useState(null); const [groupPricingCache, setGroupPricingCache] = useState({}); const [loadingGroupPricingId, setLoadingGroupPricingId] = useState(0); const [groupPricingSearch, setGroupPricingSearch] = useState(''); // Create modal const [showCreate, setShowCreate] = useState(false); const [createName, setCreateName] = useState(''); const [selectedGroupId, setSelectedGroupId] = useState(0); const [creating, setCreating] = useState(false); const load = useCallback(async () => { setLoading(true); try { const [tokensRes, groupsRes] = await Promise.all([ getTokens(), getSiteKeyGroups().catch(() => ({ data: { success: false } })), ]); if (tokensRes.data.success) setTokens(tokensRes.data.data || []); if (groupsRes.data.success) setKeyGroups(groupsRes.data.data || []); } catch (e) { /* interceptor */ } setLoading(false); }, []); useEffect(() => { load(); }, [load]); // Group by vendor_category const groupedByVendor = useMemo(() => { const map = {}; keyGroups.forEach((g) => { const cat = g.vendor_category || t('tokens.otherGroups'); if (!map[cat]) map[cat] = []; map[cat].push(g); }); return map; }, [keyGroups, t]); const openCreateFromGroup = (group) => { if (group.is_unavailable) return; setSelectedGroupId(group.id); setCreateName(group.name); setShowCreate(true); }; const openCreateDefault = () => { setSelectedGroupId(0); setCreateName(''); setShowCreate(true); }; const openGroupPricing = async (group) => { setActivePricingGroup(group); setGroupPricingSearch(''); if (groupPricingCache[group.id] || loadingGroupPricingId === group.id) { return; } setLoadingGroupPricingId(group.id); try { const res = await getSiteKeyGroupPricing(group.id); if (res.data.success) { setGroupPricingCache((prev) => ({ ...prev, [group.id]: res.data.data || { items: [], summary: null, group }, })); } } catch (e) { /* interceptor */ } setLoadingGroupPricingId(0); }; const closeGroupPricing = () => { setActivePricingGroup(null); setGroupPricingSearch(''); }; const handleCreate = async (e) => { e.preventDefault(); if (!createName.trim()) { toast.error(t('tokens.enterName')); return; } setCreating(true); try { const payload = { name: createName.trim() }; if (selectedGroupId > 0) payload.key_group_id = selectedGroupId; const res = await createToken(payload); if (res.data.success) { setCreateName(''); setShowCreate(false); setSelectedGroupId(0); const createdKey = res.data.data?.key; if (createdKey) setNewKey(createdKey); await load(); } } catch (e) { /* interceptor */ } setCreating(false); }; const handleToggle = async (token) => { try { const res = await updateToken(token.id, { status: token.status === 1 ? 2 : 1, }); if (res.data.success) { toast.success(token.status === 1 ? t('tokens.tokenDisabled') : t('tokens.tokenEnabled')); await load(); } } catch (e) { /* interceptor */ } }; const handleDelete = async () => { if (!deleteConfirm) return; try { const res = await deleteToken(deleteConfirm.id); if (res.data.success) { toast.success(t('tokens.tokenDeleted')); setDeleteConfirm(null); await load(); } } catch (e) { /* interceptor */ } }; const handleCopy = async (text) => { try { await navigator.clipboard.writeText(text); } catch (e) { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } setCopiedId(text); toast.success(t('tokens.copiedToClipboard')); setTimeout(() => setCopiedId(null), 2000); }; const parseTags = (tagsStr) => { try { return JSON.parse(tagsStr || '[]'); } catch { return []; } }; const handleToggleSupportedModels = async (tokenId) => { const isExpanded = !!expandedTokens[tokenId]; setExpandedTokens((prev) => ({ ...prev, [tokenId]: !isExpanded })); if (isExpanded) return; setTokenModels((prev) => ({ ...prev, [tokenId]: { loading: true, models: [], count: 0, provider_names: [], restricted_by_providers: false, restricted_by_models: false }, })); try { const res = await getTokenSupportedModels(tokenId); if (res.data.success) { const data = res.data.data || {}; setTokenModels((prev) => ({ ...prev, [tokenId]: { loading: false, models: data.models || [], count: data.count || 0, provider_names: data.provider_names || [], restricted_by_providers: Boolean(data.restricted_by_providers), restricted_by_models: Boolean(data.restricted_by_models), }, })); } else { setTokenModels((prev) => ({ ...prev, [tokenId]: { loading: false, error: true, models: [], count: 0, provider_names: [], restricted_by_providers: false, restricted_by_models: false }, })); } } catch (e) { setTokenModels((prev) => ({ ...prev, [tokenId]: { loading: false, error: true, models: [], count: 0, provider_names: [], restricted_by_providers: false, restricted_by_models: false }, })); } }; const hasGroups = keyGroups.length > 0; const activeGroupPricing = activePricingGroup ? groupPricingCache[activePricingGroup.id] || null : null; const filteredGroupPricingItems = useMemo(() => { const items = activeGroupPricing?.items || []; const keyword = groupPricingSearch.trim().toLowerCase(); if (!keyword) return items; return items.filter((item) => { const modelName = (item.model_name || '').toLowerCase(); const displayName = (item.display_name || '').toLowerCase(); const category = (item.category || '').toLowerCase(); return ( modelName.includes(keyword) || displayName.includes(keyword) || category.includes(keyword) ); }); }, [activeGroupPricing, groupPricingSearch]); if (loading) { return (
); } return (
{/* ========== Section 1: Create Key with Groups ========== */}

{hasGroups ? t('tokens.selectGroup') : t('tokens.title')}

{hasGroups && (

{t('tokens.selectGroupSubtitle')}

)} {/* Default (All Providers) Card */}
{/* Vendor Category Sections */} {hasGroups && Object.entries(groupedByVendor).map(([vendor, groups]) => (

{vendor}

{groups.length} {t('tokens.groupCount')}
{groups.map((group) => ( ))}
))}
{/* ========== Create Modal ========== */} {showCreate && (
{ setShowCreate(false); setSelectedGroupId(0); }}>
e.stopPropagation()}>

{t('tokens.createApiKey')}

{selectedGroupId > 0 && (() => { const g = keyGroups.find((x) => x.id === selectedGroupId); return g ? (

{t('tokens.selectedGroup')}

{g.name}

) : null; })()}
setCreateName(e.target.value)} className="input" placeholder={t('tokens.namePlaceholder')} autoFocus required />
)} {/* ========== New Key Reveal Modal ========== */} {newKey && (

{t('tokens.newApiKey')}

{t('tokens.keyWarning')}

{newKey}
)} {/* ========== Delete Confirmation Modal ========== */} {deleteConfirm && (
setDeleteConfirm(null)}>
e.stopPropagation()}>

{t('tokens.deleteToken')}

{t('tokens.deleteConfirm', { name: deleteConfirm.name })}

)} {/* ========== Section 2: My API Keys ========== */}

{t('tokens.myKeys')}

{tokens.length === 0 ? (

{t('tokens.noKeys')}

{t('tokens.noKeysHint')}

) : (
{tokens.map((token) => (

{token.name}

{token.created_time ? new Date(token.created_time * 1000).toLocaleDateString() : ''}
{token.key && (
sk-{token.key}
)} {expandedTokens[token.id] && (
{tokenModels[token.id]?.loading ? (
{t('tokens.loadingSupportedModels')}
) : tokenModels[token.id]?.error ? (

{t('tokens.loadSupportedModelsFailed')}

) : ( <>

{t('tokens.supportedModels')} ({tokenModels[token.id]?.count || 0})

{tokenModels[token.id]?.restricted_by_models && ( {t('tokens.restrictedByModels')} )} {tokenModels[token.id]?.restricted_by_providers && ( {t('tokens.restrictedByProviders')} )}
{tokenModels[token.id]?.provider_names?.length > 0 && (
{t('tokens.supportedProviders')} {tokenModels[token.id].provider_names.map((name) => ( {name} ))}
)} {tokenModels[token.id]?.models?.length > 0 ? (
{tokenModels[token.id].models.map((modelName) => ( {modelName} ))}
) : (

{t('tokens.noSupportedModels')}

)} )}
)}
))}
)}
); } /* ========== Key Group Card ========== */ function KeyGroupCard({ group, parseTags, onSelect, onViewPricing, t }) { const tags = parseTags(group.tags); const isUnavailable = group.is_unavailable; return (
!isUnavailable && onSelect(group)} >
{/* Name + badges */}
{group.name} {group.is_recommended && ( {t('tokens.recommended')} )} {isUnavailable && ( {t('tokens.unavailable')} )}
{/* Price + discount + tags */}
{group.rmb_per_usd > 0 && ( {group.rmb_per_usd} {t('tokens.rmbPerUsd')} )} {group.discount_label && ( {group.discount_label} )} {tags.map((tag, i) => ( {tag} ))}
{/* Description */} {group.description && (

{group.description}

)}
{!isUnavailable && (
{t('tokens.create')}
)}
); } function GroupPricingModal({ open, group, pricingData, items, loading, search, onSearchChange, onClose, symbol, rate, t, }) { if (!open || !group) { return null; } const displayGroup = pricingData?.group || group; const summary = pricingData?.summary; const hasItems = (pricingData?.items || []).length > 0; return (
event.stopPropagation()} >

{displayGroup.name} · {t('tokens.groupPricingTitle')}

{t('tokens.groupPricingSubtitle')}

{displayGroup.discount_label && ( {displayGroup.discount_label} )} {displayGroup.rmb_per_usd > 0 && ( {displayGroup.rmb_per_usd} {t('tokens.rmbPerUsd')} )} {summary && ( {t('tokens.groupPricingAvailableLines')}: {summary.provider_count} )} {summary && ( {t('tokens.groupPricingAvailableModels')}: {summary.model_count} )} {summary?.provider_limited && ( {t('tokens.restrictedByProviders')} )} {summary?.model_limited && ( {t('tokens.restrictedByModels')} )}
{displayGroup.description && (

{displayGroup.description}

)}

{t('tokens.groupPricingNotice')}

onSearchChange(event.target.value)} className="input lg:max-w-xs" placeholder={t('tokens.groupPricingSearchPlaceholder')} />
{loading ? (
{t('tokens.groupPricingLoading')}
) : !hasItems ? (
{t('tokens.groupPricingNoData')}
) : items.length === 0 ? (
{t('tokens.groupPricingNoMatch')}
) : (
{items.map((item) => ( ))}
{t('pricing.model')} {t('定价方式')} {t('tokens.groupPricingReferencePrice')} {t('pricing.outputPrice')} {t('pricing.cacheReadPrice')} {t('pricing.cacheCreationPrice')} {t('tokens.groupPricingLines')}
{item.display_name || item.model_name}
{(item.display_name || item.model_name) !== item.model_name && (
{item.model_name}
)} {item.category && (
{item.category}
)}
{item.billing_type === 'per_call' ? t('pricing.perCall') : t('按量计费')} {item.status !== 'healthy' ? t('pricing.unknown') : item.billing_type === 'per_call' ? formatGroupPriceRange(item.fixed_price_min, item.fixed_price_max, symbol, rate, true, t) : formatGroupPriceRange(item.input_price_min, item.input_price_max, symbol, rate, false, t)} {item.status !== 'healthy' ? '-' : item.billing_type === 'per_call' ? '-' : formatGroupPriceRange(item.output_price_min, item.output_price_max, symbol, rate, false, t)} {item.status !== 'healthy' ? '-' : item.billing_type === 'per_call' ? '-' : formatGroupPriceRange(item.cache_read_price_min, item.cache_read_price_max, symbol, rate, false, t)} {item.status !== 'healthy' ? '-' : item.billing_type === 'per_call' ? '-' : formatGroupCachePriceRange(item, symbol, rate, t)} {formatRouteCount(item.route_count, item.has_range, t)}
)}
); } function formatGroupPriceRange(min, max, symbol, rate, perCall, t) { if (min == null && max == null) { return '-'; } const low = Number(min ?? max ?? 0); const high = Number(max ?? min ?? 0); const factor = perCall ? rate : rate * 1000; const suffix = perCall ? `/${t('pricing.perCallUnit')}` : ''; const lowText = `${symbol}${(low * factor).toFixed(4)}`; const highText = `${symbol}${(high * factor).toFixed(4)}`; if (Math.abs(low - high) <= 1e-9) { return `${lowText}${suffix}`; } return `${lowText} - ${highText}${suffix}`; } function formatGroupCachePriceRange(item, symbol, rate, t) { const base = formatGroupPriceRange( item.cache_creation_price_min, item.cache_creation_price_max, symbol, rate, false, t, ); if ( item.cache_creation_price_1h_min == null && item.cache_creation_price_1h_max == null ) { return base; } const baseMin = Number(item.cache_creation_price_min ?? 0); const baseMax = Number(item.cache_creation_price_max ?? 0); const oneHourMin = Number(item.cache_creation_price_1h_min ?? 0); const oneHourMax = Number(item.cache_creation_price_1h_max ?? 0); if ( Math.abs(baseMin - oneHourMin) <= 1e-9 && Math.abs(baseMax - oneHourMax) <= 1e-9 ) { return base; } const oneHour = formatGroupPriceRange( item.cache_creation_price_1h_min, item.cache_creation_price_1h_max, symbol, rate, false, t, ); return `${t('pricing.cacheCreation5m')} ${base} / ${t('pricing.cacheCreation1h')} ${oneHour}`; } function formatRouteCount(routeCount, hasRange, t) { if (!routeCount) { return t('pricing.unknown'); } if (routeCount === 1) { return `1 ${t('tokens.groupPricingLineUnitSingle')}`; } if (!hasRange) { return `${routeCount} ${t('tokens.groupPricingLineUnit')} · ${t('tokens.groupPricingSamePrice')}`; } return `${routeCount} ${t('tokens.groupPricingLineUnit')}`; }