|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useState, useEffect, useMemo, useCallback, memo } from 'react'; |
|
|
import { |
|
|
Card, |
|
|
Tag, |
|
|
Avatar, |
|
|
Typography, |
|
|
Tooltip, |
|
|
Modal, |
|
|
} from '@douyinfe/semi-ui'; |
|
|
import { getLobeHubIcon } from '../../../../../helpers'; |
|
|
import SearchActions from './SearchActions'; |
|
|
|
|
|
const { Paragraph } = Typography; |
|
|
|
|
|
const CONFIG = { |
|
|
CAROUSEL_INTERVAL: 2000, |
|
|
ICON_SIZE: 40, |
|
|
UNKNOWN_VENDOR: 'unknown', |
|
|
}; |
|
|
|
|
|
const THEME_COLORS = { |
|
|
allVendors: { |
|
|
primary: '37 99 235', |
|
|
background: 'rgba(59, 130, 246, 0.08)', |
|
|
}, |
|
|
specific: { |
|
|
primary: '16 185 129', |
|
|
background: 'rgba(16, 185, 129, 0.1)', |
|
|
}, |
|
|
}; |
|
|
|
|
|
const COMPONENT_STYLES = { |
|
|
tag: { |
|
|
backgroundColor: 'rgba(255,255,255,0.95)', |
|
|
color: '#1f2937', |
|
|
border: '1px solid rgba(255,255,255,0.8)', |
|
|
fontWeight: '500', |
|
|
}, |
|
|
avatarContainer: |
|
|
'w-16 h-16 rounded-2xl bg-white/90 shadow-md backdrop-blur-sm flex items-center justify-center', |
|
|
titleText: { color: 'white' }, |
|
|
descriptionText: { color: 'rgba(255,255,255,0.9)' }, |
|
|
}; |
|
|
|
|
|
const CONTENT_TEXTS = { |
|
|
unknown: { |
|
|
displayName: (t) => t('未知供应商'), |
|
|
description: (t) => |
|
|
t( |
|
|
'包含来自未知或未标明供应商的AI模型,这些模型可能来自小型供应商或开源项目。', |
|
|
), |
|
|
}, |
|
|
all: { |
|
|
description: (t) => |
|
|
t('查看所有可用的AI模型供应商,包括众多知名供应商的模型。'), |
|
|
}, |
|
|
fallback: { |
|
|
description: (t) => t('该供应商提供多种AI模型,适用于不同的应用场景。'), |
|
|
}, |
|
|
}; |
|
|
|
|
|
const getVendorDisplayName = (vendorName, t) => { |
|
|
return vendorName === CONFIG.UNKNOWN_VENDOR |
|
|
? CONTENT_TEXTS.unknown.displayName(t) |
|
|
: vendorName; |
|
|
}; |
|
|
|
|
|
const createDefaultAvatar = () => ( |
|
|
<div className={COMPONENT_STYLES.avatarContainer}> |
|
|
<Avatar size='large' color='transparent'> |
|
|
AI |
|
|
</Avatar> |
|
|
</div> |
|
|
); |
|
|
|
|
|
const getAvatarBackgroundColor = (isAllVendors) => |
|
|
isAllVendors |
|
|
? THEME_COLORS.allVendors.background |
|
|
: THEME_COLORS.specific.background; |
|
|
|
|
|
const getAvatarText = (vendorName) => |
|
|
vendorName === CONFIG.UNKNOWN_VENDOR |
|
|
? '?' |
|
|
: vendorName.charAt(0).toUpperCase(); |
|
|
|
|
|
const createAvatarContent = (vendor, isAllVendors) => { |
|
|
if (vendor.icon) { |
|
|
return getLobeHubIcon(vendor.icon, CONFIG.ICON_SIZE); |
|
|
} |
|
|
|
|
|
return ( |
|
|
<Avatar |
|
|
size='large' |
|
|
style={{ backgroundColor: getAvatarBackgroundColor(isAllVendors) }} |
|
|
> |
|
|
{getAvatarText(vendor.name)} |
|
|
</Avatar> |
|
|
); |
|
|
}; |
|
|
|
|
|
const renderVendorAvatar = (vendor, t, isAllVendors = false) => { |
|
|
if (!vendor) { |
|
|
return createDefaultAvatar(); |
|
|
} |
|
|
|
|
|
const displayName = getVendorDisplayName(vendor.name, t); |
|
|
const avatarContent = createAvatarContent(vendor, isAllVendors); |
|
|
|
|
|
return ( |
|
|
<Tooltip content={displayName} position='top'> |
|
|
<div className={COMPONENT_STYLES.avatarContainer}>{avatarContent}</div> |
|
|
</Tooltip> |
|
|
); |
|
|
}; |
|
|
|
|
|
const PricingVendorIntro = memo( |
|
|
({ |
|
|
filterVendor, |
|
|
models = [], |
|
|
allModels = [], |
|
|
t, |
|
|
selectedRowKeys = [], |
|
|
copyText, |
|
|
handleChange, |
|
|
handleCompositionStart, |
|
|
handleCompositionEnd, |
|
|
isMobile = false, |
|
|
searchValue = '', |
|
|
setShowFilterModal, |
|
|
showWithRecharge, |
|
|
setShowWithRecharge, |
|
|
currency, |
|
|
setCurrency, |
|
|
showRatio, |
|
|
setShowRatio, |
|
|
viewMode, |
|
|
setViewMode, |
|
|
tokenUnit, |
|
|
setTokenUnit, |
|
|
}) => { |
|
|
const [currentOffset, setCurrentOffset] = useState(0); |
|
|
const [descModalVisible, setDescModalVisible] = useState(false); |
|
|
const [descModalContent, setDescModalContent] = useState(''); |
|
|
|
|
|
const handleOpenDescModal = useCallback((content) => { |
|
|
setDescModalContent(content || ''); |
|
|
setDescModalVisible(true); |
|
|
}, []); |
|
|
|
|
|
const handleCloseDescModal = useCallback(() => { |
|
|
setDescModalVisible(false); |
|
|
}, []); |
|
|
|
|
|
const renderDescriptionModal = useCallback( |
|
|
() => ( |
|
|
<Modal |
|
|
title={t('供应商介绍')} |
|
|
visible={descModalVisible} |
|
|
onCancel={handleCloseDescModal} |
|
|
footer={null} |
|
|
width={isMobile ? '95%' : 600} |
|
|
bodyStyle={{ |
|
|
maxHeight: isMobile ? '70vh' : '60vh', |
|
|
overflowY: 'auto', |
|
|
}} |
|
|
> |
|
|
<div className='text-sm mb-4'>{descModalContent}</div> |
|
|
</Modal> |
|
|
), |
|
|
[descModalVisible, descModalContent, handleCloseDescModal, isMobile, t], |
|
|
); |
|
|
|
|
|
const vendorInfo = useMemo(() => { |
|
|
const vendors = new Map(); |
|
|
let unknownCount = 0; |
|
|
|
|
|
const sourceModels = |
|
|
Array.isArray(allModels) && allModels.length > 0 ? allModels : models; |
|
|
|
|
|
sourceModels.forEach((model) => { |
|
|
if (model.vendor_name) { |
|
|
const existing = vendors.get(model.vendor_name); |
|
|
if (existing) { |
|
|
existing.count++; |
|
|
} else { |
|
|
vendors.set(model.vendor_name, { |
|
|
name: model.vendor_name, |
|
|
icon: model.vendor_icon, |
|
|
description: model.vendor_description, |
|
|
count: 1, |
|
|
}); |
|
|
} |
|
|
} else { |
|
|
unknownCount++; |
|
|
} |
|
|
}); |
|
|
|
|
|
const vendorList = Array.from(vendors.values()).sort((a, b) => |
|
|
a.name.localeCompare(b.name), |
|
|
); |
|
|
|
|
|
if (unknownCount > 0) { |
|
|
vendorList.push({ |
|
|
name: CONFIG.UNKNOWN_VENDOR, |
|
|
icon: null, |
|
|
description: CONTENT_TEXTS.unknown.description(t), |
|
|
count: unknownCount, |
|
|
}); |
|
|
} |
|
|
|
|
|
return vendorList; |
|
|
}, [allModels, models, t]); |
|
|
|
|
|
const currentModelCount = models.length; |
|
|
|
|
|
useEffect(() => { |
|
|
if (filterVendor !== 'all' || vendorInfo.length <= 1) { |
|
|
setCurrentOffset(0); |
|
|
return; |
|
|
} |
|
|
|
|
|
const interval = setInterval(() => { |
|
|
setCurrentOffset((prev) => (prev + 1) % vendorInfo.length); |
|
|
}, CONFIG.CAROUSEL_INTERVAL); |
|
|
|
|
|
return () => clearInterval(interval); |
|
|
}, [filterVendor, vendorInfo.length]); |
|
|
|
|
|
const getVendorDescription = useCallback( |
|
|
(vendorKey) => { |
|
|
if (vendorKey === 'all') { |
|
|
return CONTENT_TEXTS.all.description(t); |
|
|
} |
|
|
if (vendorKey === CONFIG.UNKNOWN_VENDOR) { |
|
|
return CONTENT_TEXTS.unknown.description(t); |
|
|
} |
|
|
const vendor = vendorInfo.find((v) => v.name === vendorKey); |
|
|
return vendor?.description || CONTENT_TEXTS.fallback.description(t); |
|
|
}, |
|
|
[vendorInfo, t], |
|
|
); |
|
|
|
|
|
const createCoverStyle = useCallback( |
|
|
(primaryColor) => ({ |
|
|
'--palette-primary-darkerChannel': primaryColor, |
|
|
backgroundImage: `linear-gradient(0deg, rgba(var(--palette-primary-darkerChannel) / 80%), rgba(var(--palette-primary-darkerChannel) / 80%)), url('/cover-4.webp')`, |
|
|
backgroundSize: 'cover', |
|
|
backgroundPosition: 'center', |
|
|
backgroundRepeat: 'no-repeat', |
|
|
}), |
|
|
[], |
|
|
); |
|
|
|
|
|
const renderSearchActions = useCallback( |
|
|
() => ( |
|
|
<SearchActions |
|
|
selectedRowKeys={selectedRowKeys} |
|
|
copyText={copyText} |
|
|
handleChange={handleChange} |
|
|
handleCompositionStart={handleCompositionStart} |
|
|
handleCompositionEnd={handleCompositionEnd} |
|
|
isMobile={isMobile} |
|
|
searchValue={searchValue} |
|
|
setShowFilterModal={setShowFilterModal} |
|
|
showWithRecharge={showWithRecharge} |
|
|
setShowWithRecharge={setShowWithRecharge} |
|
|
currency={currency} |
|
|
setCurrency={setCurrency} |
|
|
showRatio={showRatio} |
|
|
setShowRatio={setShowRatio} |
|
|
viewMode={viewMode} |
|
|
setViewMode={setViewMode} |
|
|
tokenUnit={tokenUnit} |
|
|
setTokenUnit={setTokenUnit} |
|
|
t={t} |
|
|
/> |
|
|
), |
|
|
[ |
|
|
selectedRowKeys, |
|
|
copyText, |
|
|
handleChange, |
|
|
handleCompositionStart, |
|
|
handleCompositionEnd, |
|
|
isMobile, |
|
|
searchValue, |
|
|
setShowFilterModal, |
|
|
showWithRecharge, |
|
|
setShowWithRecharge, |
|
|
currency, |
|
|
setCurrency, |
|
|
showRatio, |
|
|
setShowRatio, |
|
|
viewMode, |
|
|
setViewMode, |
|
|
tokenUnit, |
|
|
setTokenUnit, |
|
|
t, |
|
|
], |
|
|
); |
|
|
|
|
|
const renderHeaderCard = useCallback( |
|
|
({ title, count, description, rightContent, primaryDarkerChannel }) => ( |
|
|
<Card |
|
|
className='!rounded-2xl shadow-sm border-0' |
|
|
cover={ |
|
|
<div |
|
|
className='relative h-full' |
|
|
style={createCoverStyle(primaryDarkerChannel)} |
|
|
> |
|
|
<div className='relative z-10 h-full flex items-center justify-between p-4'> |
|
|
<div className='flex-1 min-w-0 mr-4'> |
|
|
<div className='flex flex-row flex-wrap items-center gap-2 sm:gap-3 mb-2'> |
|
|
<h2 |
|
|
className='text-lg sm:text-xl font-bold truncate' |
|
|
style={COMPONENT_STYLES.titleText} |
|
|
> |
|
|
{title} |
|
|
</h2> |
|
|
<Tag |
|
|
style={COMPONENT_STYLES.tag} |
|
|
shape='circle' |
|
|
size='small' |
|
|
className='self-center' |
|
|
> |
|
|
{t('共 {{count}} 个模型', { count })} |
|
|
</Tag> |
|
|
</div> |
|
|
<Paragraph |
|
|
className='text-xs sm:text-sm leading-relaxed !mb-0 cursor-pointer' |
|
|
style={COMPONENT_STYLES.descriptionText} |
|
|
ellipsis={{ rows: 2 }} |
|
|
onClick={() => handleOpenDescModal(description)} |
|
|
> |
|
|
{description} |
|
|
</Paragraph> |
|
|
</div> |
|
|
|
|
|
<div className='flex-shrink-0'>{rightContent}</div> |
|
|
</div> |
|
|
</div> |
|
|
} |
|
|
> |
|
|
{renderSearchActions()} |
|
|
</Card> |
|
|
), |
|
|
[renderSearchActions, createCoverStyle, handleOpenDescModal, t], |
|
|
); |
|
|
|
|
|
const renderAllVendorsAvatar = useCallback(() => { |
|
|
const currentVendor = |
|
|
vendorInfo.length > 0 |
|
|
? vendorInfo[currentOffset % vendorInfo.length] |
|
|
: null; |
|
|
return renderVendorAvatar(currentVendor, t, true); |
|
|
}, [vendorInfo, currentOffset, t]); |
|
|
|
|
|
if (filterVendor === 'all') { |
|
|
const headerCard = renderHeaderCard({ |
|
|
title: t('全部供应商'), |
|
|
count: currentModelCount, |
|
|
description: getVendorDescription('all'), |
|
|
rightContent: renderAllVendorsAvatar(), |
|
|
primaryDarkerChannel: THEME_COLORS.allVendors.primary, |
|
|
}); |
|
|
return ( |
|
|
<> |
|
|
{headerCard} |
|
|
{renderDescriptionModal()} |
|
|
</> |
|
|
); |
|
|
} |
|
|
|
|
|
const currentVendor = vendorInfo.find((v) => v.name === filterVendor); |
|
|
if (!currentVendor) { |
|
|
return null; |
|
|
} |
|
|
|
|
|
const vendorDisplayName = getVendorDisplayName(currentVendor.name, t); |
|
|
|
|
|
const headerCard = renderHeaderCard({ |
|
|
title: vendorDisplayName, |
|
|
count: currentModelCount, |
|
|
description: |
|
|
currentVendor.description || getVendorDescription(currentVendor.name), |
|
|
rightContent: renderVendorAvatar(currentVendor, t, false), |
|
|
primaryDarkerChannel: THEME_COLORS.specific.primary, |
|
|
}); |
|
|
|
|
|
return ( |
|
|
<> |
|
|
{headerCard} |
|
|
{renderDescriptionModal()} |
|
|
</> |
|
|
); |
|
|
}, |
|
|
); |
|
|
|
|
|
PricingVendorIntro.displayName = 'PricingVendorIntro'; |
|
|
|
|
|
export default PricingVendorIntro; |
|
|
|