MissingModelsModal.jsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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, { useEffect, useState } from 'react';
  16. import {
  17. Modal,
  18. Table,
  19. Spin,
  20. Button,
  21. Typography,
  22. Empty,
  23. Input,
  24. } from '@douyinfe/semi-ui';
  25. import {
  26. IllustrationNoResult,
  27. IllustrationNoResultDark,
  28. } from '@douyinfe/semi-illustrations';
  29. import { IconSearch } from '@douyinfe/semi-icons';
  30. import { API, showError } from '../../../../helpers';
  31. import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants';
  32. import { useIsMobile } from '../../../../hooks/common/useIsMobile';
  33. const MissingModelsModal = ({ visible, onClose, onConfigureModel, t }) => {
  34. const [loading, setLoading] = useState(false);
  35. const [missingModels, setMissingModels] = useState([]);
  36. const [searchKeyword, setSearchKeyword] = useState('');
  37. const [currentPage, setCurrentPage] = useState(1);
  38. const isMobile = useIsMobile();
  39. const fetchMissing = async () => {
  40. setLoading(true);
  41. try {
  42. const res = await API.get('/api/models/missing');
  43. if (res.data.success) {
  44. setMissingModels(res.data.data || []);
  45. } else {
  46. showError(res.data.message);
  47. }
  48. } catch (_) {
  49. showError(t('获取未配置模型失败'));
  50. }
  51. setLoading(false);
  52. };
  53. useEffect(() => {
  54. if (visible) {
  55. fetchMissing();
  56. setSearchKeyword('');
  57. setCurrentPage(1);
  58. } else {
  59. setMissingModels([]);
  60. }
  61. }, [visible]);
  62. // 过滤和分页逻辑
  63. const filteredModels = missingModels.filter((model) =>
  64. model.toLowerCase().includes(searchKeyword.toLowerCase()),
  65. );
  66. const dataSource = (() => {
  67. const start = (currentPage - 1) * MODEL_TABLE_PAGE_SIZE;
  68. const end = start + MODEL_TABLE_PAGE_SIZE;
  69. return filteredModels.slice(start, end).map((model) => ({
  70. model,
  71. key: model,
  72. }));
  73. })();
  74. const columns = [
  75. {
  76. title: t('模型名称'),
  77. dataIndex: 'model',
  78. render: (text) => (
  79. <div className='flex items-center'>
  80. <Typography.Text strong>{text}</Typography.Text>
  81. </div>
  82. ),
  83. },
  84. {
  85. title: '',
  86. dataIndex: 'operate',
  87. fixed: 'right',
  88. width: 120,
  89. render: (text, record) => (
  90. <Button
  91. type='primary'
  92. size='small'
  93. onClick={() => onConfigureModel(record.model)}
  94. >
  95. {t('配置')}
  96. </Button>
  97. ),
  98. },
  99. ];
  100. return (
  101. <Modal
  102. title={
  103. <div className='flex flex-col gap-2 w-full'>
  104. <div className='flex items-center gap-2'>
  105. <Typography.Text
  106. strong
  107. className='!text-[var(--semi-color-text-0)] !text-base'
  108. >
  109. {t('未配置的模型列表')}
  110. </Typography.Text>
  111. <Typography.Text type='tertiary' size='small'>
  112. {t('共')} {missingModels.length} {t('个未配置模型')}
  113. </Typography.Text>
  114. </div>
  115. </div>
  116. }
  117. visible={visible}
  118. onCancel={onClose}
  119. footer={null}
  120. size={isMobile ? 'full-width' : 'medium'}
  121. className='!rounded-lg'
  122. >
  123. <Spin spinning={loading}>
  124. {missingModels.length === 0 && !loading ? (
  125. <Empty
  126. image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
  127. darkModeImage={
  128. <IllustrationNoResultDark style={{ width: 150, height: 150 }} />
  129. }
  130. description={t('暂无缺失模型')}
  131. style={{ padding: 30 }}
  132. />
  133. ) : (
  134. <div className='missing-models-content'>
  135. {/* 搜索框 */}
  136. <div className='flex items-center justify-end gap-2 w-full mb-4'>
  137. <Input
  138. placeholder={t('搜索模型...')}
  139. value={searchKeyword}
  140. onChange={(v) => {
  141. setSearchKeyword(v);
  142. setCurrentPage(1);
  143. }}
  144. className='!w-full'
  145. prefix={<IconSearch />}
  146. showClear
  147. />
  148. </div>
  149. {/* 表格 */}
  150. {filteredModels.length > 0 ? (
  151. <Table
  152. columns={columns}
  153. dataSource={dataSource}
  154. pagination={{
  155. currentPage: currentPage,
  156. pageSize: MODEL_TABLE_PAGE_SIZE,
  157. total: filteredModels.length,
  158. showSizeChanger: false,
  159. onPageChange: (page) => setCurrentPage(page),
  160. }}
  161. />
  162. ) : (
  163. <Empty
  164. image={
  165. <IllustrationNoResult style={{ width: 100, height: 100 }} />
  166. }
  167. darkModeImage={
  168. <IllustrationNoResultDark
  169. style={{ width: 100, height: 100 }}
  170. />
  171. }
  172. description={
  173. searchKeyword ? t('未找到匹配的模型') : t('暂无缺失模型')
  174. }
  175. style={{ padding: 20 }}
  176. />
  177. )}
  178. </div>
  179. )}
  180. </Spin>
  181. </Modal>
  182. );
  183. };
  184. export default MissingModelsModal;