ModelSelectModal.jsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. import React, { useState, useEffect } from 'react';
  2. import { useIsMobile } from '../../../../hooks/common/useIsMobile.js';
  3. import { Modal, Checkbox, Spin, Input, Typography, Empty, Tabs, Collapse } from '@douyinfe/semi-ui';
  4. import {
  5. IllustrationNoResult,
  6. IllustrationNoResultDark
  7. } from '@douyinfe/semi-illustrations';
  8. import { IconSearch } from '@douyinfe/semi-icons';
  9. import { useTranslation } from 'react-i18next';
  10. import { getModelCategories } from '../../../../helpers/render';
  11. const ModelSelectModal = ({ visible, models = [], selected = [], onConfirm, onCancel }) => {
  12. const { t } = useTranslation();
  13. const [checkedList, setCheckedList] = useState(selected);
  14. const [keyword, setKeyword] = useState('');
  15. const [activeTab, setActiveTab] = useState('new');
  16. const isMobile = useIsMobile();
  17. const filteredModels = models.filter((m) => m.toLowerCase().includes(keyword.toLowerCase()));
  18. // 分类模型:新获取的模型和已有模型
  19. const newModels = filteredModels.filter(model => !selected.includes(model));
  20. const existingModels = filteredModels.filter(model => selected.includes(model));
  21. // 同步外部选中值
  22. useEffect(() => {
  23. if (visible) {
  24. setCheckedList(selected);
  25. }
  26. }, [visible, selected]);
  27. // 当模型列表变化时,设置默认tab
  28. useEffect(() => {
  29. if (visible) {
  30. // 默认显示新获取模型tab,如果没有新模型则显示已有模型
  31. const hasNewModels = newModels.length > 0;
  32. setActiveTab(hasNewModels ? 'new' : 'existing');
  33. }
  34. }, [visible, newModels.length, selected]);
  35. const handleOk = () => {
  36. onConfirm && onConfirm(checkedList);
  37. };
  38. // 按厂商分类模型
  39. const categorizeModels = (models) => {
  40. const categories = getModelCategories(t);
  41. const categorizedModels = {};
  42. const uncategorizedModels = [];
  43. models.forEach(model => {
  44. let foundCategory = false;
  45. for (const [key, category] of Object.entries(categories)) {
  46. if (key !== 'all' && category.filter({ model_name: model })) {
  47. if (!categorizedModels[key]) {
  48. categorizedModels[key] = {
  49. label: category.label,
  50. icon: category.icon,
  51. models: []
  52. };
  53. }
  54. categorizedModels[key].models.push(model);
  55. foundCategory = true;
  56. break;
  57. }
  58. }
  59. if (!foundCategory) {
  60. uncategorizedModels.push(model);
  61. }
  62. });
  63. // 如果有未分类模型,添加到"其他"分类
  64. if (uncategorizedModels.length > 0) {
  65. categorizedModels['other'] = {
  66. label: t('其他'),
  67. icon: null,
  68. models: uncategorizedModels
  69. };
  70. }
  71. return categorizedModels;
  72. };
  73. const newModelsByCategory = categorizeModels(newModels);
  74. const existingModelsByCategory = categorizeModels(existingModels);
  75. // Tab列表配置
  76. const tabList = [
  77. ...(newModels.length > 0 ? [{
  78. tab: `${t('新获取的模型')} (${newModels.length})`,
  79. itemKey: 'new'
  80. }] : []),
  81. ...(existingModels.length > 0 ? [{
  82. tab: `${t('已有的模型')} (${existingModels.length})`,
  83. itemKey: 'existing'
  84. }] : [])
  85. ];
  86. // 处理分类全选/取消全选
  87. const handleCategorySelectAll = (categoryModels, isChecked) => {
  88. let newCheckedList = [...checkedList];
  89. if (isChecked) {
  90. // 全选:添加该分类下所有未选中的模型
  91. categoryModels.forEach(model => {
  92. if (!newCheckedList.includes(model)) {
  93. newCheckedList.push(model);
  94. }
  95. });
  96. } else {
  97. // 取消全选:移除该分类下所有已选中的模型
  98. newCheckedList = newCheckedList.filter(model => !categoryModels.includes(model));
  99. }
  100. setCheckedList(newCheckedList);
  101. };
  102. // 检查分类是否全选
  103. const isCategoryAllSelected = (categoryModels) => {
  104. return categoryModels.length > 0 && categoryModels.every(model => checkedList.includes(model));
  105. };
  106. // 检查分类是否部分选中
  107. const isCategoryIndeterminate = (categoryModels) => {
  108. const selectedCount = categoryModels.filter(model => checkedList.includes(model)).length;
  109. return selectedCount > 0 && selectedCount < categoryModels.length;
  110. };
  111. const renderModelsByCategory = (modelsByCategory, categoryKeyPrefix) => {
  112. const categoryEntries = Object.entries(modelsByCategory);
  113. if (categoryEntries.length === 0) return null;
  114. // 生成所有面板的key,确保都展开
  115. const allActiveKeys = categoryEntries.map((_, index) => `${categoryKeyPrefix}_${index}`);
  116. return (
  117. <Collapse activeKey={allActiveKeys}>
  118. {categoryEntries.map(([key, categoryData], index) => (
  119. <Collapse.Panel
  120. key={`${categoryKeyPrefix}_${index}`}
  121. itemKey={`${categoryKeyPrefix}_${index}`}
  122. header={`${categoryData.label} (${categoryData.models.length})`}
  123. extra={
  124. <Checkbox
  125. checked={isCategoryAllSelected(categoryData.models)}
  126. indeterminate={isCategoryIndeterminate(categoryData.models)}
  127. onChange={(e) => {
  128. e.stopPropagation(); // 防止触发面板折叠
  129. handleCategorySelectAll(categoryData.models, e.target.checked);
  130. }}
  131. onClick={(e) => e.stopPropagation()} // 防止点击checkbox时折叠面板
  132. />
  133. }
  134. >
  135. <div className="flex items-center gap-2 mb-3">
  136. {categoryData.icon}
  137. <Typography.Text type="secondary" size="small">
  138. {t('已选择 {{selected}} / {{total}}', {
  139. selected: categoryData.models.filter(model => checkedList.includes(model)).length,
  140. total: categoryData.models.length
  141. })}
  142. </Typography.Text>
  143. </div>
  144. <div className="grid grid-cols-2 gap-x-4">
  145. {categoryData.models.map((model) => (
  146. <Checkbox key={model} value={model} className="my-1">
  147. {model}
  148. </Checkbox>
  149. ))}
  150. </div>
  151. </Collapse.Panel>
  152. ))}
  153. </Collapse>
  154. );
  155. };
  156. return (
  157. <Modal
  158. header={
  159. <div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-4 py-4">
  160. <Typography.Title heading={5} className="m-0">
  161. {t('选择模型')}
  162. </Typography.Title>
  163. <div className="flex-shrink-0">
  164. <Tabs
  165. type="slash"
  166. size="small"
  167. tabList={tabList}
  168. activeKey={activeTab}
  169. onChange={(key) => setActiveTab(key)}
  170. />
  171. </div>
  172. </div>
  173. }
  174. visible={visible}
  175. onOk={handleOk}
  176. onCancel={onCancel}
  177. okText={t('确定')}
  178. cancelText={t('取消')}
  179. size={isMobile ? 'full-width' : 'large'}
  180. closeOnEsc
  181. maskClosable
  182. centered
  183. >
  184. <Input
  185. prefix={<IconSearch size={14} />}
  186. placeholder={t('搜索模型')}
  187. value={keyword}
  188. onChange={(v) => setKeyword(v)}
  189. showClear
  190. />
  191. <Spin spinning={!models || models.length === 0}>
  192. <div style={{ maxHeight: 400, overflowY: 'auto', paddingRight: 8 }}>
  193. {filteredModels.length === 0 ? (
  194. <Empty
  195. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  196. darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
  197. description={t('暂无匹配模型')}
  198. style={{ padding: 30 }}
  199. />
  200. ) : (
  201. <Checkbox.Group value={checkedList} onChange={(vals) => setCheckedList(vals)}>
  202. {activeTab === 'new' && newModels.length > 0 && (
  203. <div>
  204. {renderModelsByCategory(newModelsByCategory, 'new')}
  205. </div>
  206. )}
  207. {activeTab === 'existing' && existingModels.length > 0 && (
  208. <div>
  209. {renderModelsByCategory(existingModelsByCategory, 'existing')}
  210. </div>
  211. )}
  212. </Checkbox.Group>
  213. )}
  214. </div>
  215. </Spin>
  216. <Typography.Text type="secondary" size="small" className="block text-right mt-4">
  217. <div className="flex items-center justify-end gap-2">
  218. {(() => {
  219. const currentModels = activeTab === 'new' ? newModels : existingModels;
  220. const currentSelected = currentModels.filter(model => checkedList.includes(model)).length;
  221. const isAllSelected = currentModels.length > 0 && currentSelected === currentModels.length;
  222. const isIndeterminate = currentSelected > 0 && currentSelected < currentModels.length;
  223. return (
  224. <>
  225. <span>
  226. {t('已选择 {{selected}} / {{total}}', {
  227. selected: currentSelected,
  228. total: currentModels.length
  229. })}
  230. </span>
  231. <Checkbox
  232. checked={isAllSelected}
  233. indeterminate={isIndeterminate}
  234. onChange={(e) => {
  235. handleCategorySelectAll(currentModels, e.target.checked);
  236. }}
  237. />
  238. </>
  239. );
  240. })()}
  241. </div>
  242. </Typography.Text>
  243. </Modal>
  244. );
  245. };
  246. export default ModelSelectModal;