- Ran: bun run eslint:fix && bun run lint:fix - Inserted AGPL license header via eslint-plugin-header - Enforced no-multiple-empty-lines and other lint rules - Formatted code using Prettier v3 (@so1ve/prettier-config) - No functional changes; formatting-only baseline across JS/JSX files
281 lines
9.9 KiB
JavaScript
281 lines
9.9 KiB
JavaScript
/*
|
|
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, { 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(() => {
|
|
// Initialize from localStorage if available
|
|
const savedState = localStorage.getItem('modelsExpanded');
|
|
return savedState ? JSON.parse(savedState) : false;
|
|
});
|
|
const [activeModelCategory, setActiveModelCategory] = useState('all');
|
|
const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
|
|
|
|
// Save models expanded state to localStorage whenever it changes
|
|
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;
|