ModelsList.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. /*
  2. Copyright (C) 2025 QuantumNous
  3. This program is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU Affero General Public License as
  5. published by the Free Software Foundation, either version 3 of the
  6. License, or (at your option) any later version.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU Affero General Public License for more details.
  11. You should have received a copy of the GNU Affero General Public License
  12. along with this program. If not, see <https://www.gnu.org/licenses/>.
  13. For commercial licensing, please contact support@quantumnous.com
  14. */
  15. import React, { useState, useEffect } from 'react';
  16. import {
  17. Empty,
  18. Skeleton,
  19. Space,
  20. Tag,
  21. Collapsible,
  22. Tabs,
  23. TabPane,
  24. Typography,
  25. Avatar
  26. } from '@douyinfe/semi-ui';
  27. import { IllustrationNoContent, IllustrationNoContentDark } from '@douyinfe/semi-illustrations';
  28. import {
  29. IconChevronDown,
  30. IconChevronUp
  31. } from '@douyinfe/semi-icons';
  32. import { Settings } from 'lucide-react';
  33. import { renderModelTag, getModelCategories } from '../../../../helpers';
  34. const ModelsList = ({ t, models, modelsLoading, copyText }) => {
  35. const [isModelsExpanded, setIsModelsExpanded] = useState(() => {
  36. // Initialize from localStorage if available
  37. const savedState = localStorage.getItem('modelsExpanded');
  38. return savedState ? JSON.parse(savedState) : false;
  39. });
  40. const [activeModelCategory, setActiveModelCategory] = useState('all');
  41. const MODELS_DISPLAY_COUNT = 25; // 默认显示的模型数量
  42. // Save models expanded state to localStorage whenever it changes
  43. useEffect(() => {
  44. localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded));
  45. }, [isModelsExpanded]);
  46. return (
  47. <div className="py-4">
  48. {/* 卡片头部 */}
  49. <div className="flex items-center mb-4">
  50. <Avatar size="small" color="green" className="mr-3 shadow-md">
  51. <Settings size={16} />
  52. </Avatar>
  53. <div>
  54. <Typography.Text className="text-lg font-medium">{t('可用模型')}</Typography.Text>
  55. <div className="text-xs text-gray-600">{t('查看当前可用的所有模型')}</div>
  56. </div>
  57. </div>
  58. {/* 可用模型部分 */}
  59. <div className="bg-gray-50 dark:bg-gray-800 rounded-xl">
  60. {modelsLoading ? (
  61. // 骨架屏加载状态 - 模拟实际加载后的布局
  62. <div className="space-y-4">
  63. {/* 模拟分类标签 */}
  64. <div className="mb-4" style={{ borderBottom: '1px solid var(--semi-color-border)' }}>
  65. <div className="flex overflow-x-auto py-2 gap-2">
  66. {Array.from({ length: 8 }).map((_, index) => (
  67. <Skeleton.Button key={`cat-${index}`} style={{
  68. width: index === 0 ? 130 : 100 + Math.random() * 50,
  69. height: 36,
  70. borderRadius: 8
  71. }} />
  72. ))}
  73. </div>
  74. </div>
  75. {/* 模拟模型标签列表 */}
  76. <div className="flex flex-wrap gap-2">
  77. {Array.from({ length: 20 }).map((_, index) => (
  78. <Skeleton.Button
  79. key={`model-${index}`}
  80. style={{
  81. width: 100 + Math.random() * 100,
  82. height: 32,
  83. borderRadius: 16,
  84. margin: '4px'
  85. }}
  86. />
  87. ))}
  88. </div>
  89. </div>
  90. ) : models.length === 0 ? (
  91. <div className="py-8">
  92. <Empty
  93. image={<IllustrationNoContent style={{ width: 150, height: 150 }} />}
  94. darkModeImage={<IllustrationNoContentDark style={{ width: 150, height: 150 }} />}
  95. description={t('没有可用模型')}
  96. style={{ padding: '24px 0' }}
  97. />
  98. </div>
  99. ) : (
  100. <>
  101. {/* 模型分类标签页 */}
  102. <div className="mb-4">
  103. <Tabs
  104. type="card"
  105. activeKey={activeModelCategory}
  106. onChange={key => setActiveModelCategory(key)}
  107. className="mt-2"
  108. collapsible
  109. >
  110. {Object.entries(getModelCategories(t)).map(([key, category]) => {
  111. // 计算该分类下的模型数量
  112. const modelCount = key === 'all'
  113. ? models.length
  114. : models.filter(model => category.filter({ model_name: model })).length;
  115. if (modelCount === 0 && key !== 'all') return null;
  116. return (
  117. <TabPane
  118. tab={
  119. <span className="flex items-center gap-2">
  120. {category.icon && <span className="w-4 h-4">{category.icon}</span>}
  121. {category.label}
  122. <Tag
  123. color={activeModelCategory === key ? 'red' : 'grey'}
  124. size='small'
  125. shape='circle'
  126. >
  127. {modelCount}
  128. </Tag>
  129. </span>
  130. }
  131. itemKey={key}
  132. key={key}
  133. />
  134. );
  135. })}
  136. </Tabs>
  137. </div>
  138. <div className="bg-white dark:bg-gray-700 rounded-lg p-3">
  139. {(() => {
  140. // 根据当前选中的分类过滤模型
  141. const categories = getModelCategories(t);
  142. const filteredModels = activeModelCategory === 'all'
  143. ? models
  144. : models.filter(model => categories[activeModelCategory].filter({ model_name: model }));
  145. // 如果过滤后没有模型,显示空状态
  146. if (filteredModels.length === 0) {
  147. return (
  148. <Empty
  149. image={<IllustrationNoContent style={{ width: 120, height: 120 }} />}
  150. darkModeImage={<IllustrationNoContentDark style={{ width: 120, height: 120 }} />}
  151. description={t('该分类下没有可用模型')}
  152. style={{ padding: '16px 0' }}
  153. />
  154. );
  155. }
  156. if (filteredModels.length <= MODELS_DISPLAY_COUNT) {
  157. return (
  158. <Space wrap>
  159. {filteredModels.map((model) => (
  160. renderModelTag(model, {
  161. size: 'small',
  162. shape: 'circle',
  163. onClick: () => copyText(model),
  164. })
  165. ))}
  166. </Space>
  167. );
  168. } else {
  169. return (
  170. <>
  171. <Collapsible isOpen={isModelsExpanded}>
  172. <Space wrap>
  173. {filteredModels.map((model) => (
  174. renderModelTag(model, {
  175. size: 'small',
  176. shape: 'circle',
  177. onClick: () => copyText(model),
  178. })
  179. ))}
  180. <Tag
  181. color='grey'
  182. type='light'
  183. className="cursor-pointer !rounded-lg"
  184. onClick={() => setIsModelsExpanded(false)}
  185. icon={<IconChevronUp />}
  186. >
  187. {t('收起')}
  188. </Tag>
  189. </Space>
  190. </Collapsible>
  191. {!isModelsExpanded && (
  192. <Space wrap>
  193. {filteredModels
  194. .slice(0, MODELS_DISPLAY_COUNT)
  195. .map((model) => (
  196. renderModelTag(model, {
  197. size: 'small',
  198. shape: 'circle',
  199. onClick: () => copyText(model),
  200. })
  201. ))}
  202. <Tag
  203. color='grey'
  204. type='light'
  205. className="cursor-pointer !rounded-lg"
  206. onClick={() => setIsModelsExpanded(true)}
  207. icon={<IconChevronDown />}
  208. >
  209. {t('更多')} {filteredModels.length - MODELS_DISPLAY_COUNT} {t('个模型')}
  210. </Tag>
  211. </Space>
  212. )}
  213. </>
  214. );
  215. }
  216. })()}
  217. </div>
  218. </>
  219. )}
  220. </div>
  221. </div>
  222. );
  223. };
  224. export default ModelsList;