| @@ -33,7 +33,8 @@ func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) { | |||
| err := DB.Table("abilities"). | |||
| Select("abilities.*, channels.type as channel_type"). | |||
| Joins("left join channels on abilities.channel_id = channels.id"). | |||
| Where("abilities.enabled = ?", true). | |||
| Joins("left join models on abilities.model = models.model_name"). | |||
| Where("abilities.enabled = ? and models.model_name is NOT null", true). | |||
| Scan(&abilities).Error | |||
| return abilities, err | |||
| } | |||
| @@ -15,6 +15,17 @@ const ( | |||
| NameRuleSuffix | |||
| ) | |||
| const ( | |||
| ModelTypeChat = 1 + iota | |||
| ModelTypeImage | |||
| ModelTypeAudio | |||
| ModelTypeVideo | |||
| ModelTypeEmbedding | |||
| ModelTypeRerank | |||
| ModelTypeVision | |||
| ModelTypeOther | |||
| ) | |||
| type BoundChannel struct { | |||
| Name string `json:"name"` | |||
| Type int `json:"type"` | |||
| @@ -26,6 +37,7 @@ type Model struct { | |||
| Description string `json:"description,omitempty" gorm:"type:text"` | |||
| Icon string `json:"icon,omitempty" gorm:"type:varchar(128)"` | |||
| Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"` | |||
| Type int `json:"type" gorm:"default:0"` | |||
| VendorID int `json:"vendor_id,omitempty" gorm:"index"` | |||
| Endpoints string `json:"endpoints,omitempty" gorm:"type:text"` | |||
| Status int `json:"status" gorm:"default:1"` | |||
| @@ -28,6 +28,7 @@ type Pricing struct { | |||
| EnableGroup []string `json:"enable_groups"` | |||
| SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"` | |||
| PricingVersion string `json:"pricing_version,omitempty"` | |||
| Type int `json:"type"` | |||
| } | |||
| type PricingVendor struct { | |||
| @@ -279,13 +280,14 @@ func updatePricing() { | |||
| // 补充模型元数据(描述、标签、供应商、状态) | |||
| if meta, ok := metaMap[model]; ok { | |||
| // 若模型被禁用(status!=1),则直接跳过,不返回给前端 | |||
| if meta.Status != 1 { | |||
| if meta.Status != 1 || meta.Type == 0 { | |||
| continue | |||
| } | |||
| pricing.Description = meta.Description | |||
| pricing.Icon = meta.Icon | |||
| pricing.Tags = meta.Tags | |||
| pricing.VendorID = meta.VendorID | |||
| pricing.Type = meta.Type | |||
| } | |||
| modelPrice, findPrice := ratio_setting.GetModelPrice(model, false) | |||
| if findPrice { | |||
| @@ -0,0 +1,140 @@ | |||
| /* | |||
| Copyright (C) 2025 QuantumNous | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU Affero General Public License as | |||
| published by the Free Software Foundation, either version 3 of the | |||
| License, or (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU Affero General Public License for more details. | |||
| You should have received a copy of the GNU Affero General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| For commercial licensing, please contact support@quantumnous.com | |||
| */ | |||
| import React, { useRef } from 'react'; | |||
| import { Button, Tag, Skeleton } from '@douyinfe/semi-ui'; | |||
| import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons'; | |||
| import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime'; | |||
| /** | |||
| * 横向筛选项行,通过左右箭头翻页 | |||
| * @param {string} title 标题 | |||
| * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 选项列表 | |||
| * @param {*} activeValue 当前选中值 | |||
| * @param {(value:any)=>void} onChange 选择回调 | |||
| * @param {boolean} loading 加载中 | |||
| * @param {Function} t i18n | |||
| */ | |||
| const HorizontalFilterRow = ({ | |||
| title, | |||
| items = [], | |||
| activeValue, | |||
| onChange, | |||
| loading = false, | |||
| t = (v) => v, | |||
| }) => { | |||
| const scrollRef = useRef(null); | |||
| const showSkeleton = useMinimumLoadingTime(loading); | |||
| const handleScroll = (dir) => { | |||
| const el = scrollRef.current; | |||
| if (!el) return; | |||
| const step = el.clientWidth * 0.8; | |||
| el.scrollBy({ | |||
| left: dir === 'left' ? -step : step, | |||
| behavior: 'smooth', | |||
| }); | |||
| }; | |||
| if (showSkeleton) { | |||
| return ( | |||
| <div className='mb-6'> | |||
| <Skeleton.Title active style={{ width: 60, marginBottom: 8 }} /> | |||
| <div className='flex gap-2'> | |||
| {[1, 2, 3, 4, 5].map((i) => ( | |||
| <Skeleton.Button | |||
| key={i} | |||
| active | |||
| style={{ width: 80, height: 32 }} | |||
| /> | |||
| ))} | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| return ( | |||
| <div className='mb-6 last:mb-0'> | |||
| <div className='text-sm font-medium text-semi-color-text-1 mb-3'> | |||
| {title} | |||
| </div> | |||
| <div className='flex items-center gap-2'> | |||
| <Button | |||
| type='tertiary' | |||
| theme='borderless' | |||
| icon={<IconChevronLeft />} | |||
| onClick={() => handleScroll('left')} | |||
| className='flex-shrink-0' | |||
| aria-label={t('向左')} | |||
| /> | |||
| <div | |||
| ref={scrollRef} | |||
| className='flex-1 min-w-0 flex gap-2 overflow-x-auto overflow-y-hidden scroll-smooth scrollbar-hide' | |||
| style={{ | |||
| scrollbarWidth: 'none', | |||
| msOverflowStyle: 'none', | |||
| WebkitOverflowScrolling: 'touch', | |||
| }} | |||
| > | |||
| {items.map((item) => { | |||
| const isDisabled = | |||
| item.disabled || | |||
| (typeof item.tagCount === 'number' && item.tagCount === 0); | |||
| const isActive = activeValue === item.value; | |||
| return ( | |||
| <Button | |||
| key={item.value} | |||
| onClick={() => !isDisabled && onChange(item.value)} | |||
| theme={isActive ? 'solid' : 'borderless'} | |||
| type={isActive ? 'primary' : 'tertiary'} | |||
| disabled={isDisabled} | |||
| className='flex-shrink-0 !rounded-full' | |||
| > | |||
| <span className='flex items-center gap-2'> | |||
| {item.icon} | |||
| <span>{item.label}</span> | |||
| {item.tagCount !== undefined && ( | |||
| <Tag | |||
| color={isActive ? 'white' : 'grey'} | |||
| shape='circle' | |||
| size='small' | |||
| > | |||
| {item.tagCount} | |||
| </Tag> | |||
| )} | |||
| </span> | |||
| </Button> | |||
| ); | |||
| })} | |||
| </div> | |||
| <Button | |||
| type='tertiary' | |||
| theme='borderless' | |||
| icon={<IconChevronRight />} | |||
| onClick={() => handleScroll('right')} | |||
| className='flex-shrink-0' | |||
| aria-label={t('向右')} | |||
| /> | |||
| </div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default HorizontalFilterRow; | |||
| @@ -74,13 +74,14 @@ const PricingCardView = ({ | |||
| selectedRowKeys = [], | |||
| setSelectedRowKeys, | |||
| openModelDetail, | |||
| gridColsClass = 'grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3', | |||
| showPagination = true, | |||
| }) => { | |||
| const showSkeleton = useMinimumLoadingTime(loading); | |||
| const startIndex = (currentPage - 1) * pageSize; | |||
| const paginatedModels = filteredModels.slice( | |||
| startIndex, | |||
| startIndex + pageSize, | |||
| ); | |||
| const paginatedModels = showPagination | |||
| ? filteredModels.slice(startIndex, startIndex + pageSize) | |||
| : filteredModels; | |||
| const getModelKey = (model) => model.key ?? model.model_name ?? model.id; | |||
| const isMobile = useIsMobile(); | |||
| @@ -234,7 +235,7 @@ const PricingCardView = ({ | |||
| return ( | |||
| <div className='px-2 pt-2'> | |||
| <div className='grid grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3 gap-4'> | |||
| <div className={`grid ${gridColsClass} gap-4`}> | |||
| {paginatedModels.map((model, index) => { | |||
| const modelKey = getModelKey(model); | |||
| const isSelected = selectedRowKeys.includes(modelKey); | |||
| @@ -357,7 +358,7 @@ const PricingCardView = ({ | |||
| </div> | |||
| {/* 分页 */} | |||
| {filteredModels.length > 0 && ( | |||
| {showPagination && filteredModels.length > 0 && ( | |||
| <div className='flex justify-center mt-6 py-4 border-t pricing-pagination-divider'> | |||
| <Pagination | |||
| currentPage={currentPage} | |||
| @@ -39,6 +39,7 @@ export const useModelPricingData = () => { | |||
| const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string | |||
| const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string | |||
| const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string | |||
| const [filterType, setFilterType] = useState('all'); // 模型类型筛选: 'all' | number (model_meta.ModelType) | |||
| const [pageSize, setPageSize] = useState(20); | |||
| const [currentPage, setCurrentPage] = useState(1); | |||
| const [currency, setCurrency] = useState('USD'); | |||
| @@ -135,6 +136,12 @@ export const useModelPricingData = () => { | |||
| }); | |||
| } | |||
| // 模型类型筛选(pricing.Type / model.Type) | |||
| if (filterType !== 'all') { | |||
| const typeNum = Number(filterType); | |||
| result = result.filter((model) => model.type === typeNum); | |||
| } | |||
| // 搜索筛选 | |||
| if (searchValue.length > 0) { | |||
| const searchTerm = searchValue.toLowerCase(); | |||
| @@ -159,6 +166,7 @@ export const useModelPricingData = () => { | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterTag, | |||
| filterType, | |||
| ]); | |||
| const rowSelection = useMemo( | |||
| @@ -321,6 +329,7 @@ export const useModelPricingData = () => { | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterTag, | |||
| filterType, | |||
| searchValue, | |||
| ]); | |||
| @@ -350,6 +359,8 @@ export const useModelPricingData = () => { | |||
| setFilterVendor, | |||
| filterTag, | |||
| setFilterTag, | |||
| filterType, | |||
| setFilterType, | |||
| pageSize, | |||
| setPageSize, | |||
| currentPage, | |||
| @@ -37,6 +37,7 @@ export const usePricingFilterCounts = ({ | |||
| filterEndpointType = 'all', | |||
| filterVendor = 'all', | |||
| filterTag = 'all', | |||
| filterType = 'all', | |||
| searchValue = '', | |||
| }) => { | |||
| // 均使用同一份模型列表,避免创建新引用 | |||
| @@ -84,6 +85,12 @@ export const usePricingFilterCounts = ({ | |||
| if (!tagsArr.includes(filterTag.toLowerCase())) return false; | |||
| } | |||
| // 模型类型(pricing.Type / model.Type) | |||
| if (!ignore.includes('type') && filterType !== 'all') { | |||
| const typeNum = Number(filterType); | |||
| if (model.type !== typeNum) return false; | |||
| } | |||
| // 搜索 | |||
| if (!ignore.includes('search') && searchValue) { | |||
| const term = searchValue.toLowerCase(); | |||
| @@ -112,6 +119,7 @@ export const usePricingFilterCounts = ({ | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterTag, | |||
| filterType, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| @@ -124,6 +132,7 @@ export const usePricingFilterCounts = ({ | |||
| filterQuotaType, | |||
| filterVendor, | |||
| filterTag, | |||
| filterType, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| @@ -136,6 +145,7 @@ export const usePricingFilterCounts = ({ | |||
| filterQuotaType, | |||
| filterEndpointType, | |||
| filterTag, | |||
| filterType, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| @@ -148,6 +158,20 @@ export const usePricingFilterCounts = ({ | |||
| filterQuotaType, | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterType, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| const typeModels = useMemo( | |||
| () => allModels.filter((m) => matchesFilters(m, ['type'])), | |||
| [ | |||
| allModels, | |||
| filterGroup, | |||
| filterQuotaType, | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterTag, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| @@ -160,6 +184,7 @@ export const usePricingFilterCounts = ({ | |||
| filterEndpointType, | |||
| filterVendor, | |||
| filterTag, | |||
| filterType, | |||
| searchValue, | |||
| ], | |||
| ); | |||
| @@ -170,5 +195,6 @@ export const usePricingFilterCounts = ({ | |||
| vendorModels, | |||
| groupCountModels, | |||
| tagModels, | |||
| typeModels, | |||
| }; | |||
| }; | |||
| @@ -0,0 +1,197 @@ | |||
| /* | |||
| Copyright (C) 2025 QuantumNous | |||
| This program is free software: you can redistribute it and/or modify | |||
| it under the terms of the GNU Affero General Public License as | |||
| published by the Free Software Foundation, either version 3 of the | |||
| License, or (at your option) any later version. | |||
| This program is distributed in the hope that it will be useful, | |||
| but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
| GNU Affero General Public License for more details. | |||
| You should have received a copy of the GNU Affero General Public License | |||
| along with this program. If not, see <https://www.gnu.org/licenses/>. | |||
| For commercial licensing, please contact support@quantumnous.com | |||
| */ | |||
| import React, { useMemo, useState } from 'react'; | |||
| import { ImagePreview } from '@douyinfe/semi-ui'; | |||
| import HorizontalFilterRow from '../../components/common/ui/HorizontalFilterRow'; | |||
| import PricingCardView from '../../components/table/model-pricing/view/card/PricingCardView'; | |||
| import ModelDetailSideSheet from '../../components/table/model-pricing/modal/ModelDetailSideSheet'; | |||
| import { useModelPricingData } from '../../hooks/model-pricing/useModelPricingData'; | |||
| import { usePricingFilterCounts } from '../../hooks/model-pricing/usePricingFilterCounts'; | |||
| import { getLobeHubIcon } from '../../helpers'; | |||
| // 与 model_meta.go ModelType 常量一致:1=Chat, 2=Image, 3=Audio, 4=Video, 5=Embedding, 6=Rerank, 7=Vision, 8=Other | |||
| const MODEL_TYPE_LIST = [ | |||
| { value: 1, labelKey: '对话' }, | |||
| { value: 2, labelKey: '图像' }, | |||
| { value: 3, labelKey: '音频' }, | |||
| { value: 4, labelKey: '视频' }, | |||
| { value: 5, labelKey: '嵌入' }, | |||
| { value: 6, labelKey: '重排序' }, | |||
| { value: 7, labelKey: '视觉' }, | |||
| { value: 8, labelKey: '其他' }, | |||
| ]; | |||
| const HomePricingFilters = ({ t }) => { | |||
| const [showRatio, setShowRatio] = useState(false); | |||
| const pricingData = useModelPricingData(); | |||
| const { | |||
| vendorModels, | |||
| typeModels, | |||
| } = usePricingFilterCounts({ | |||
| models: pricingData.models, | |||
| filterGroup: pricingData.filterGroup, | |||
| filterQuotaType: pricingData.filterQuotaType, | |||
| filterEndpointType: pricingData.filterEndpointType, | |||
| filterVendor: pricingData.filterVendor, | |||
| filterTag: pricingData.filterTag, | |||
| filterType: pricingData.filterType, | |||
| searchValue: pricingData.searchValue, | |||
| }); | |||
| const vendorItems = useMemo(() => { | |||
| const models = pricingData.models; | |||
| const vendorSet = new Set(); | |||
| const vendorIcons = new Map(); | |||
| let hasUnknown = false; | |||
| (models || []).forEach((m) => { | |||
| if (m.vendor_name) { | |||
| vendorSet.add(m.vendor_name); | |||
| if (m.vendor_icon && !vendorIcons.has(m.vendor_name)) { | |||
| vendorIcons.set(m.vendor_name, m.vendor_icon); | |||
| } | |||
| } else { | |||
| hasUnknown = true; | |||
| } | |||
| }); | |||
| const vendors = Array.from(vendorSet).sort(); | |||
| const getCount = (v) => { | |||
| if (v === 'all') return vendorModels.length; | |||
| if (v === 'unknown') return vendorModels.filter((m) => !m.vendor_name).length; | |||
| return vendorModels.filter((m) => m.vendor_name === v).length; | |||
| }; | |||
| const items = [ | |||
| { value: 'all', label: t('全部供应商'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, | |||
| ]; | |||
| vendors.forEach((v) => { | |||
| const count = getCount(v); | |||
| items.push({ | |||
| value: v, | |||
| label: v, | |||
| icon: vendorIcons.get(v) ? getLobeHubIcon(vendorIcons.get(v), 16) : null, | |||
| tagCount: count, | |||
| disabled: count === 0, | |||
| }); | |||
| }); | |||
| if (hasUnknown) { | |||
| items.push({ | |||
| value: 'unknown', | |||
| label: t('未知供应商'), | |||
| tagCount: getCount('unknown'), | |||
| disabled: getCount('unknown') === 0, | |||
| }); | |||
| } | |||
| return items; | |||
| }, [pricingData.models, vendorModels, t]); | |||
| const typeItems = useMemo(() => { | |||
| const models = pricingData.models; | |||
| const getCount = (typeVal) => { | |||
| if (typeVal === 'all') return typeModels.length; | |||
| const n = Number(typeVal); | |||
| return typeModels.filter((m) => m.type === n).length; | |||
| }; | |||
| const items = [ | |||
| { value: 'all', label: t('全部类型'), tagCount: getCount('all'), disabled: (models || []).length === 0 }, | |||
| ]; | |||
| MODEL_TYPE_LIST.forEach(({ value, labelKey }) => { | |||
| const count = getCount(value); | |||
| items.push({ | |||
| value: String(value), | |||
| label: t(labelKey), | |||
| tagCount: count, | |||
| disabled: count === 0, | |||
| }); | |||
| }); | |||
| return items; | |||
| }, [pricingData.models, typeModels, t]); | |||
| return ( | |||
| <div className='w-full mt-8 md:mt-12'> | |||
| <div className='rounded-xl border border-semi-color-border bg-semi-color-bg-0 p-4'> | |||
| <HorizontalFilterRow | |||
| title={t('供应商')} | |||
| items={vendorItems} | |||
| activeValue={pricingData.filterVendor} | |||
| onChange={pricingData.setFilterVendor} | |||
| loading={pricingData.loading} | |||
| t={t} | |||
| /> | |||
| <HorizontalFilterRow | |||
| title={t('模型类型')} | |||
| items={typeItems} | |||
| activeValue={pricingData.filterType} | |||
| onChange={pricingData.setFilterType} | |||
| loading={pricingData.loading} | |||
| t={t} | |||
| /> | |||
| </div> | |||
| <div className='mt-6'> | |||
| <PricingCardView | |||
| filteredModels={pricingData.filteredModels} | |||
| loading={pricingData.loading} | |||
| rowSelection={null} | |||
| pageSize={pricingData.pageSize} | |||
| setPageSize={pricingData.setPageSize} | |||
| currentPage={pricingData.currentPage} | |||
| setCurrentPage={pricingData.setCurrentPage} | |||
| selectedGroup={pricingData.selectedGroup} | |||
| groupRatio={pricingData.groupRatio} | |||
| copyText={pricingData.copyText} | |||
| setModalImageUrl={pricingData.setModalImageUrl} | |||
| setIsModalOpenurl={pricingData.setIsModalOpenurl} | |||
| currency={pricingData.currency} | |||
| tokenUnit={pricingData.tokenUnit} | |||
| displayPrice={pricingData.displayPrice} | |||
| showRatio={showRatio} | |||
| t={t} | |||
| selectedRowKeys={[]} | |||
| openModelDetail={pricingData.openModelDetail} | |||
| gridColsClass='grid-cols-1 md:grid-cols-2 lg:grid-cols-3' | |||
| showPagination={false} | |||
| /> | |||
| </div> | |||
| <ImagePreview | |||
| src={pricingData.modalImageUrl} | |||
| visible={pricingData.isModalOpenurl} | |||
| onVisibleChange={(visible) => pricingData.setIsModalOpenurl(visible)} | |||
| /> | |||
| <ModelDetailSideSheet | |||
| visible={pricingData.showModelDetail} | |||
| onClose={pricingData.closeModelDetail} | |||
| modelData={pricingData.selectedModel} | |||
| groupRatio={pricingData.groupRatio} | |||
| usableGroup={pricingData.usableGroup} | |||
| currency={pricingData.currency} | |||
| tokenUnit={pricingData.tokenUnit} | |||
| displayPrice={pricingData.displayPrice} | |||
| showRatio={showRatio} | |||
| vendorsMap={pricingData.vendorsMap} | |||
| endpointMap={pricingData.endpointMap} | |||
| autoGroups={pricingData.autoGroups} | |||
| t={t} | |||
| /> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default HomePricingFilters; | |||
| @@ -40,6 +40,7 @@ import { | |||
| } from '@douyinfe/semi-icons'; | |||
| import { Link } from 'react-router-dom'; | |||
| import NoticeModal from '../../components/layout/NoticeModal'; | |||
| import HomePricingFilters from './HomePricingFilters'; | |||
| import { | |||
| Moonshot, | |||
| OpenAI, | |||
| @@ -162,9 +163,9 @@ const Home = () => { | |||
| {/* 背景模糊晕染球 */} | |||
| <div className='blur-ball blur-ball-indigo' /> | |||
| <div className='blur-ball blur-ball-teal' /> | |||
| <div className='flex items-center justify-center h-full px-4 py-20 md:py-24 lg:py-32 mt-10'> | |||
| <div className='flex flex-col items-center w-full px-4 py-20 md:py-24 lg:py-32 mt-10'> | |||
| {/* 居中内容区 */} | |||
| <div className='flex flex-col items-center justify-center text-center max-w-4xl mx-auto'> | |||
| <div className='flex flex-col items-center justify-center text-center max-w-4xl mx-auto w-full'> | |||
| <div className='flex flex-col items-center justify-center mb-6 md:mb-8'> | |||
| <h1 | |||
| className={`text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold text-semi-color-text-0 leading-tight ${isChinese ? 'tracking-wide md:tracking-wider' : ''}`} | |||
| @@ -252,84 +253,10 @@ const Home = () => { | |||
| )} | |||
| </div> | |||
| {/* 框架兼容性图标 */} | |||
| <div className='mt-12 md:mt-16 lg:mt-20 w-full'> | |||
| <div className='flex items-center mb-6 md:mb-8 justify-center'> | |||
| <Text | |||
| type='tertiary' | |||
| className='text-lg md:text-xl lg:text-2xl font-light' | |||
| > | |||
| {t('支持众多的大模型供应商')} | |||
| </Text> | |||
| </div> | |||
| <div className='flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:gap-6 lg:gap-8 max-w-5xl mx-auto px-4'> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Moonshot size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <OpenAI size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <XAI size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Zhipu.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Volcengine.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Cohere.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Claude.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Gemini.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Suno size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Minimax.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Wenxin.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Spark.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Qingyan.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <DeepSeek.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Qwen.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Midjourney size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Grok size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <AzureAI.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Hunyuan.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Xinference.Color size={40} /> | |||
| </div> | |||
| <div className='w-8 h-8 sm:w-10 sm:h-10 md:w-12 md:h-12 flex items-center justify-center'> | |||
| <Typography.Text className='!text-lg sm:!text-xl md:!text-2xl lg:!text-3xl font-bold'> | |||
| 30+ | |||
| </Typography.Text> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| {/* 模型广场筛选项与列表 */} | |||
| <div className='w-full max-w-6xl mt-8 px-4'> | |||
| <HomePricingFilters t={t} /> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| @@ -29,6 +29,11 @@ export default defineConfig({ | |||
| resolve: { | |||
| alias: { | |||
| '@': path.resolve(__dirname, './src'), | |||
| // 绕过 semi-ui exports 限制,dist/css/semi.css 不在 package.json exports 中 | |||
| '@douyinfe/semi-ui/dist/css/semi.css': path.resolve( | |||
| __dirname, | |||
| 'node_modules/@douyinfe/semi-ui/dist/css/semi.css', | |||
| ), | |||
| }, | |||
| }, | |||
| plugins: [ | |||