| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import React, { useState, useEffect } from 'react'; |
| import { |
| Empty, |
| Skeleton, |
| Space, |
| Tag, |
| Collapsible, |
| Tabs, |
| TabPane, |
| Typography, |
| Avatar, |
| } from '@douyinfe/semi-ui'; |
| import { |
| IllustrationNoContent, |
| IllustrationNoContentDark, |
| } from '@douyinfe/semi-illustrations'; |
| import { IconChevronDown, IconChevronUp } from '@douyinfe/semi-icons'; |
| import { Settings } from 'lucide-react'; |
| import { renderModelTag, getModelCategories } from '../../../../helpers'; |
|
|
| const ModelsList = ({ t, models, modelsLoading, copyText }) => { |
| const [isModelsExpanded, setIsModelsExpanded] = useState(() => { |
| |
| const savedState = localStorage.getItem('modelsExpanded'); |
| return savedState ? JSON.parse(savedState) : false; |
| }); |
| const [activeModelCategory, setActiveModelCategory] = useState('all'); |
| const MODELS_DISPLAY_COUNT = 25; |
|
|
| |
| useEffect(() => { |
| localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); |
| }, [isModelsExpanded]); |
|
|
| return ( |
| <div className='py-4'> |
| {/* 卡片头部 */} |
| <div className='flex items-center mb-4'> |
| <Avatar size='small' color='green' className='mr-3 shadow-md'> |
| <Settings size={16} /> |
| </Avatar> |
| <div> |
| <Typography.Text className='text-lg font-medium'> |
| {t('可用模型')} |
| </Typography.Text> |
| <div className='text-xs text-gray-600'> |
| {t('查看当前可用的所有模型')} |
| </div> |
| </div> |
| </div> |
| |
| {/* 可用模型部分 */} |
| <div className='bg-gray-50 dark:bg-gray-800 rounded-xl'> |
| {modelsLoading ? ( |
| // 骨架屏加载状态 - 模拟实际加载后的布局 |
| <div className='space-y-4'> |
| {/* 模拟分类标签 */} |
| <div |
| className='mb-4' |
| style={{ borderBottom: '1px solid var(--semi-color-border)' }} |
| > |
| <div className='flex overflow-x-auto py-2 gap-2'> |
| {Array.from({ length: 8 }).map((_, index) => ( |
| <Skeleton.Button |
| key={`cat-${index}`} |
| style={{ |
| width: index === 0 ? 130 : 100 + Math.random() * 50, |
| height: 36, |
| borderRadius: 8, |
| }} |
| /> |
| ))} |
| </div> |
| </div> |
| |
| {/* 模拟模型标签列表 */} |
| <div className='flex flex-wrap gap-2'> |
| {Array.from({ length: 20 }).map((_, index) => ( |
| <Skeleton.Button |
| key={`model-${index}`} |
| style={{ |
| width: 100 + Math.random() * 100, |
| height: 32, |
| borderRadius: 16, |
| margin: '4px', |
| }} |
| /> |
| ))} |
| </div> |
| </div> |
| ) : models.length === 0 ? ( |
| <div className='py-8'> |
| <Empty |
| image={ |
| <IllustrationNoContent style={{ width: 150, height: 150 }} /> |
| } |
| darkModeImage={ |
| <IllustrationNoContentDark |
| style={{ width: 150, height: 150 }} |
| /> |
| } |
| description={t('没有可用模型')} |
| style={{ padding: '24px 0' }} |
| /> |
| </div> |
| ) : ( |
| <> |
| {/* 模型分类标签页 */} |
| <div className='mb-4'> |
| <Tabs |
| type='card' |
| activeKey={activeModelCategory} |
| onChange={(key) => setActiveModelCategory(key)} |
| className='mt-2' |
| collapsible |
| > |
| {Object.entries(getModelCategories(t)).map( |
| ([key, category]) => { |
| // 计算该分类下的模型数量 |
| const modelCount = |
| key === 'all' |
| ? models.length |
| : models.filter((model) => |
| category.filter({ model_name: model }), |
| ).length; |
| |
| if (modelCount === 0 && key !== 'all') return null; |
| |
| return ( |
| <TabPane |
| tab={ |
| <span className='flex items-center gap-2'> |
| {category.icon && ( |
| <span className='w-4 h-4'>{category.icon}</span> |
| )} |
| {category.label} |
| <Tag |
| color={ |
| activeModelCategory === key ? 'red' : 'grey' |
| } |
| size='small' |
| shape='circle' |
| > |
| {modelCount} |
| </Tag> |
| </span> |
| } |
| itemKey={key} |
| key={key} |
| /> |
| ); |
| }, |
| )} |
| </Tabs> |
| </div> |
| |
| <div className='bg-white dark:bg-gray-700 rounded-lg p-3'> |
| {(() => { |
| // 根据当前选中的分类过滤模型 |
| const categories = getModelCategories(t); |
| const filteredModels = |
| activeModelCategory === 'all' |
| ? models |
| : models.filter((model) => |
| categories[activeModelCategory].filter({ |
| model_name: model, |
| }), |
| ); |
| |
| // 如果过滤后没有模型,显示空状态 |
| if (filteredModels.length === 0) { |
| return ( |
| <Empty |
| image={ |
| <IllustrationNoContent |
| style={{ width: 120, height: 120 }} |
| /> |
| } |
| darkModeImage={ |
| <IllustrationNoContentDark |
| style={{ width: 120, height: 120 }} |
| /> |
| } |
| description={t('该分类下没有可用模型')} |
| style={{ padding: '16px 0' }} |
| /> |
| ); |
| } |
| |
| if (filteredModels.length <= MODELS_DISPLAY_COUNT) { |
| return ( |
| <Space wrap> |
| {filteredModels.map((model) => |
| renderModelTag(model, { |
| size: 'small', |
| shape: 'circle', |
| onClick: () => copyText(model), |
| }), |
| )} |
| </Space> |
| ); |
| } else { |
| return ( |
| <> |
| <Collapsible isOpen={isModelsExpanded}> |
| <Space wrap> |
| {filteredModels.map((model) => |
| renderModelTag(model, { |
| size: 'small', |
| shape: 'circle', |
| onClick: () => copyText(model), |
| }), |
| )} |
| <Tag |
| color='grey' |
| type='light' |
| className='cursor-pointer !rounded-lg' |
| onClick={() => setIsModelsExpanded(false)} |
| icon={<IconChevronUp />} |
| > |
| {t('收起')} |
| </Tag> |
| </Space> |
| </Collapsible> |
| {!isModelsExpanded && ( |
| <Space wrap> |
| {filteredModels |
| .slice(0, MODELS_DISPLAY_COUNT) |
| .map((model) => |
| renderModelTag(model, { |
| size: 'small', |
| shape: 'circle', |
| onClick: () => copyText(model), |
| }), |
| )} |
| <Tag |
| color='grey' |
| type='light' |
| className='cursor-pointer !rounded-lg' |
| onClick={() => setIsModelsExpanded(true)} |
| icon={<IconChevronDown />} |
| > |
| {t('更多')}{' '} |
| {filteredModels.length - MODELS_DISPLAY_COUNT}{' '} |
| {t('个模型')} |
| </Tag> |
| </Space> |
| )} |
| </> |
| ); |
| } |
| })()} |
| </div> |
| </> |
| )} |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default ModelsList; |
|
|