ModelTestModal.jsx 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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 from 'react';
  16. import {
  17. Modal,
  18. Button,
  19. Input,
  20. Table,
  21. Tag,
  22. Typography
  23. } from '@douyinfe/semi-ui';
  24. import { IconSearch } from '@douyinfe/semi-icons';
  25. import { copy, showError, showInfo, showSuccess } from '../../../../helpers';
  26. import { MODEL_TABLE_PAGE_SIZE } from '../../../../constants';
  27. const ModelTestModal = ({
  28. showModelTestModal,
  29. currentTestChannel,
  30. handleCloseModal,
  31. isBatchTesting,
  32. batchTestModels,
  33. modelSearchKeyword,
  34. setModelSearchKeyword,
  35. selectedModelKeys,
  36. setSelectedModelKeys,
  37. modelTestResults,
  38. testingModels,
  39. testChannel,
  40. modelTablePage,
  41. setModelTablePage,
  42. allSelectingRef,
  43. isMobile,
  44. t
  45. }) => {
  46. const hasChannel = Boolean(currentTestChannel);
  47. const filteredModels = hasChannel
  48. ? currentTestChannel.models
  49. .split(',')
  50. .filter((model) =>
  51. model.toLowerCase().includes(modelSearchKeyword.toLowerCase())
  52. )
  53. : [];
  54. const handleCopySelected = () => {
  55. if (selectedModelKeys.length === 0) {
  56. showError(t('请先选择模型!'));
  57. return;
  58. }
  59. copy(selectedModelKeys.join(',')).then((ok) => {
  60. if (ok) {
  61. showSuccess(t('已复制 ${count} 个模型').replace('${count}', selectedModelKeys.length));
  62. } else {
  63. showError(t('复制失败,请手动复制'));
  64. }
  65. });
  66. };
  67. const handleSelectSuccess = () => {
  68. if (!currentTestChannel) return;
  69. const successKeys = currentTestChannel.models
  70. .split(',')
  71. .filter((m) => m.toLowerCase().includes(modelSearchKeyword.toLowerCase()))
  72. .filter((m) => {
  73. const result = modelTestResults[`${currentTestChannel.id}-${m}`];
  74. return result && result.success;
  75. });
  76. if (successKeys.length === 0) {
  77. showInfo(t('暂无成功模型'));
  78. }
  79. setSelectedModelKeys(successKeys);
  80. };
  81. const columns = [
  82. {
  83. title: t('模型名称'),
  84. dataIndex: 'model',
  85. render: (text) => (
  86. <div className="flex items-center">
  87. <Typography.Text strong>{text}</Typography.Text>
  88. </div>
  89. )
  90. },
  91. {
  92. title: t('状态'),
  93. dataIndex: 'status',
  94. render: (text, record) => {
  95. const testResult = modelTestResults[`${currentTestChannel.id}-${record.model}`];
  96. const isTesting = testingModels.has(record.model);
  97. if (isTesting) {
  98. return (
  99. <Tag color='blue' shape='circle'>
  100. {t('测试中')}
  101. </Tag>
  102. );
  103. }
  104. if (!testResult) {
  105. return (
  106. <Tag color='grey' shape='circle'>
  107. {t('未开始')}
  108. </Tag>
  109. );
  110. }
  111. return (
  112. <div className="flex items-center gap-2">
  113. <Tag
  114. color={testResult.success ? 'green' : 'red'}
  115. shape='circle'
  116. >
  117. {testResult.success ? t('成功') : t('失败')}
  118. </Tag>
  119. {testResult.success && (
  120. <Typography.Text type="tertiary">
  121. {t('请求时长: ${time}s').replace('${time}', testResult.time.toFixed(2))}
  122. </Typography.Text>
  123. )}
  124. </div>
  125. );
  126. }
  127. },
  128. {
  129. title: '',
  130. dataIndex: 'operate',
  131. render: (text, record) => {
  132. const isTesting = testingModels.has(record.model);
  133. return (
  134. <Button
  135. type='tertiary'
  136. onClick={() => testChannel(currentTestChannel, record.model)}
  137. loading={isTesting}
  138. size='small'
  139. >
  140. {t('测试')}
  141. </Button>
  142. );
  143. }
  144. }
  145. ];
  146. const dataSource = (() => {
  147. if (!hasChannel) return [];
  148. const start = (modelTablePage - 1) * MODEL_TABLE_PAGE_SIZE;
  149. const end = start + MODEL_TABLE_PAGE_SIZE;
  150. return filteredModels.slice(start, end).map((model) => ({
  151. model,
  152. key: model,
  153. }));
  154. })();
  155. return (
  156. <Modal
  157. title={hasChannel ? (
  158. <div className="flex flex-col gap-2 w-full">
  159. <div className="flex items-center gap-2">
  160. <Typography.Text strong className="!text-[var(--semi-color-text-0)] !text-base">
  161. {currentTestChannel.name} {t('渠道的模型测试')}
  162. </Typography.Text>
  163. <Typography.Text type="tertiary" size="small">
  164. {t('共')} {currentTestChannel.models.split(',').length} {t('个模型')}
  165. </Typography.Text>
  166. </div>
  167. </div>
  168. ) : null}
  169. visible={showModelTestModal}
  170. onCancel={handleCloseModal}
  171. footer={hasChannel ? (
  172. <div className="flex justify-end">
  173. {isBatchTesting ? (
  174. <Button
  175. type='danger'
  176. onClick={handleCloseModal}
  177. >
  178. {t('停止测试')}
  179. </Button>
  180. ) : (
  181. <Button
  182. type='tertiary'
  183. onClick={handleCloseModal}
  184. >
  185. {t('取消')}
  186. </Button>
  187. )}
  188. <Button
  189. onClick={batchTestModels}
  190. loading={isBatchTesting}
  191. disabled={isBatchTesting}
  192. >
  193. {isBatchTesting ? t('测试中...') : t('批量测试${count}个模型').replace(
  194. '${count}',
  195. filteredModels.length
  196. )}
  197. </Button>
  198. </div>
  199. ) : null}
  200. maskClosable={!isBatchTesting}
  201. className="!rounded-lg"
  202. size={isMobile ? 'full-width' : 'large'}
  203. >
  204. {hasChannel && (<div className="model-test-scroll">
  205. {/* 搜索与操作按钮 */}
  206. <div className="flex items-center justify-end gap-2 w-full mb-2">
  207. <Input
  208. placeholder={t('搜索模型...')}
  209. value={modelSearchKeyword}
  210. onChange={(v) => {
  211. setModelSearchKeyword(v);
  212. setModelTablePage(1);
  213. }}
  214. className="!w-full"
  215. prefix={<IconSearch />}
  216. showClear
  217. />
  218. <Button onClick={handleCopySelected}>
  219. {t('复制已选')}
  220. </Button>
  221. <Button
  222. type='tertiary'
  223. onClick={handleSelectSuccess}
  224. >
  225. {t('选择成功')}
  226. </Button>
  227. </div>
  228. <Table
  229. columns={columns}
  230. dataSource={dataSource}
  231. rowSelection={{
  232. selectedRowKeys: selectedModelKeys,
  233. onChange: (keys) => {
  234. if (allSelectingRef.current) {
  235. allSelectingRef.current = false;
  236. return;
  237. }
  238. setSelectedModelKeys(keys);
  239. },
  240. onSelectAll: (checked) => {
  241. allSelectingRef.current = true;
  242. setSelectedModelKeys(checked ? filteredModels : []);
  243. },
  244. }}
  245. pagination={{
  246. currentPage: modelTablePage,
  247. pageSize: MODEL_TABLE_PAGE_SIZE,
  248. total: filteredModels.length,
  249. showSizeChanger: false,
  250. onPageChange: (page) => setModelTablePage(page),
  251. }}
  252. />
  253. </div>)}
  254. </Modal>
  255. );
  256. };
  257. export default ModelTestModal;