| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240 |
- /*
- 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;
|