useModelsData.jsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  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 { useState, useEffect, useMemo } from 'react';
  16. import { useTranslation } from 'react-i18next';
  17. import { API, showError, showSuccess } from '../../helpers';
  18. import { ITEMS_PER_PAGE } from '../../constants';
  19. import { useTableCompactMode } from '../common/useTableCompactMode';
  20. export const useModelsData = () => {
  21. const { t } = useTranslation();
  22. const [compactMode, setCompactMode] = useTableCompactMode('models');
  23. // State management
  24. const [models, setModels] = useState([]);
  25. const [loading, setLoading] = useState(true);
  26. const [activePage, setActivePage] = useState(1);
  27. const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
  28. const [searching, setSearching] = useState(false);
  29. const [modelCount, setModelCount] = useState(0);
  30. // Modal states
  31. const [showEdit, setShowEdit] = useState(false);
  32. const [editingModel, setEditingModel] = useState({
  33. id: undefined,
  34. });
  35. // Row selection
  36. const [selectedKeys, setSelectedKeys] = useState([]);
  37. const rowSelection = {
  38. getCheckboxProps: (record) => ({
  39. name: record.model_name,
  40. }),
  41. selectedRowKeys: selectedKeys.map((model) => model.id),
  42. onChange: (selectedRowKeys, selectedRows) => {
  43. setSelectedKeys(selectedRows);
  44. },
  45. };
  46. // Form initial values
  47. const formInitValues = {
  48. searchKeyword: '',
  49. searchVendor: '',
  50. };
  51. // ---------- helpers ----------
  52. // Safely extract array items from API payload
  53. const extractItems = (payload) => {
  54. const items = payload?.items || payload || [];
  55. return Array.isArray(items) ? items : [];
  56. };
  57. // Form API reference
  58. const [formApi, setFormApi] = useState(null);
  59. // Get form values helper function
  60. const getFormValues = () => formApi?.getValues() || formInitValues;
  61. // Close edit modal
  62. const closeEdit = () => {
  63. setShowEdit(false);
  64. setTimeout(() => {
  65. setEditingModel({ id: undefined });
  66. }, 500);
  67. };
  68. // Set model format with key field
  69. const setModelFormat = (models) => {
  70. for (let i = 0; i < models.length; i++) {
  71. models[i].key = models[i].id;
  72. }
  73. setModels(models);
  74. };
  75. // Vendor list
  76. const [vendors, setVendors] = useState([]);
  77. const [vendorCounts, setVendorCounts] = useState({});
  78. const [activeVendorKey, setActiveVendorKey] = useState('all');
  79. const [showAddVendor, setShowAddVendor] = useState(false);
  80. const [showEditVendor, setShowEditVendor] = useState(false);
  81. const [editingVendor, setEditingVendor] = useState({ id: undefined });
  82. const [syncing, setSyncing] = useState(false);
  83. const [previewing, setPreviewing] = useState(false);
  84. const vendorMap = useMemo(() => {
  85. const map = {};
  86. vendors.forEach((v) => {
  87. map[v.id] = v;
  88. });
  89. return map;
  90. }, [vendors]);
  91. // Load vendor list
  92. const loadVendors = async () => {
  93. try {
  94. const res = await API.get('/api/vendors/?page_size=1000');
  95. if (res.data.success) {
  96. const items = res.data.data.items || res.data.data || [];
  97. setVendors(Array.isArray(items) ? items : []);
  98. }
  99. } catch (_) {
  100. // ignore
  101. }
  102. };
  103. // Load models data
  104. const loadModels = async (
  105. page = 1,
  106. size = pageSize,
  107. vendorKey = activeVendorKey,
  108. ) => {
  109. setLoading(true);
  110. try {
  111. let url = `/api/models/?p=${page}&page_size=${size}`;
  112. if (vendorKey && vendorKey !== 'all') {
  113. // Filter by vendor ID
  114. url = `/api/models/search?vendor=${vendorKey}&p=${page}&page_size=${size}`;
  115. }
  116. const res = await API.get(url);
  117. const { success, message, data } = res.data;
  118. if (success) {
  119. const newPageData = extractItems(data);
  120. setActivePage(data.page || page);
  121. setModelCount(data.total || newPageData.length);
  122. setModelFormat(newPageData);
  123. if (data.vendor_counts) {
  124. const sumAll = Object.values(data.vendor_counts).reduce(
  125. (acc, v) => acc + v,
  126. 0,
  127. );
  128. setVendorCounts({ ...data.vendor_counts, all: sumAll });
  129. }
  130. } else {
  131. showError(message);
  132. setModels([]);
  133. }
  134. } catch (error) {
  135. console.error(error);
  136. showError(t('获取模型列表失败'));
  137. setModels([]);
  138. }
  139. setLoading(false);
  140. };
  141. // Refresh data
  142. const refresh = async (page = activePage) => {
  143. await loadModels(page, pageSize);
  144. };
  145. // Sync upstream models/vendors for missing models only
  146. const syncUpstream = async (opts = {}) => {
  147. const locale = opts?.locale;
  148. setSyncing(true);
  149. try {
  150. const body = {};
  151. if (locale) body.locale = locale;
  152. const res = await API.post('/api/models/sync_upstream', body);
  153. const { success, message, data } = res.data || {};
  154. if (success) {
  155. const createdModels = data?.created_models || 0;
  156. const createdVendors = data?.created_vendors || 0;
  157. const skipped = (data?.skipped_models || []).length || 0;
  158. showSuccess(
  159. t(
  160. `已同步:新增 ${createdModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`,
  161. ),
  162. );
  163. await loadVendors();
  164. await refresh();
  165. } else {
  166. showError(message || t('同步失败'));
  167. }
  168. } catch (e) {
  169. showError(t('同步失败'));
  170. }
  171. setSyncing(false);
  172. };
  173. // Preview upstream differences
  174. const previewUpstreamDiff = async (opts = {}) => {
  175. const locale = opts?.locale;
  176. setPreviewing(true);
  177. try {
  178. const url = `/api/models/sync_upstream/preview${locale ? `?locale=${locale}` : ''}`;
  179. const res = await API.get(url);
  180. const { success, message, data } = res.data || {};
  181. if (success) {
  182. return data || { missing: [], conflicts: [] };
  183. }
  184. showError(message || t('预览失败'));
  185. return { missing: [], conflicts: [] };
  186. } catch (e) {
  187. showError(t('预览失败'));
  188. return { missing: [], conflicts: [] };
  189. } finally {
  190. setPreviewing(false);
  191. }
  192. };
  193. // Apply selected overwrite
  194. const applyUpstreamOverwrite = async (payloadOrArray = []) => {
  195. const isArray = Array.isArray(payloadOrArray);
  196. const overwrite = isArray ? payloadOrArray : payloadOrArray.overwrite || [];
  197. const locale = isArray ? undefined : payloadOrArray.locale;
  198. setSyncing(true);
  199. try {
  200. const body = { overwrite };
  201. if (locale) body.locale = locale;
  202. const res = await API.post('/api/models/sync_upstream', body);
  203. const { success, message, data } = res.data || {};
  204. if (success) {
  205. const createdModels = data?.created_models || 0;
  206. const updatedModels = data?.updated_models || 0;
  207. const createdVendors = data?.created_vendors || 0;
  208. const skipped = (data?.skipped_models || []).length || 0;
  209. showSuccess(
  210. t(
  211. `完成:新增 ${createdModels} 模型,更新 ${updatedModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`,
  212. ),
  213. );
  214. await loadVendors();
  215. await refresh();
  216. return true;
  217. }
  218. showError(message || t('同步失败'));
  219. return false;
  220. } catch (e) {
  221. showError(t('同步失败'));
  222. return false;
  223. } finally {
  224. setSyncing(false);
  225. }
  226. };
  227. // Search models with keyword and vendor
  228. const searchModels = async () => {
  229. const { searchKeyword = '', searchVendor = '' } = getFormValues();
  230. if (searchKeyword === '' && searchVendor === '') {
  231. // If keyword is blank, load models instead
  232. await loadModels(1, pageSize);
  233. return;
  234. }
  235. setSearching(true);
  236. try {
  237. const res = await API.get(
  238. `/api/models/search?keyword=${searchKeyword}&vendor=${searchVendor}&p=1&page_size=${pageSize}`,
  239. );
  240. const { success, message, data } = res.data;
  241. if (success) {
  242. const newPageData = extractItems(data);
  243. setActivePage(data.page || 1);
  244. setModelCount(data.total || newPageData.length);
  245. setModelFormat(newPageData);
  246. if (data.vendor_counts) {
  247. const sumAll = Object.values(data.vendor_counts).reduce(
  248. (acc, v) => acc + v,
  249. 0,
  250. );
  251. setVendorCounts({ ...data.vendor_counts, all: sumAll });
  252. }
  253. } else {
  254. showError(message);
  255. setModels([]);
  256. }
  257. } catch (error) {
  258. console.error(error);
  259. showError(t('搜索模型失败'));
  260. setModels([]);
  261. }
  262. setSearching(false);
  263. };
  264. // Manage model (enable/disable/delete)
  265. const manageModel = async (id, action, record) => {
  266. let res;
  267. switch (action) {
  268. case 'delete':
  269. res = await API.delete(`/api/models/${id}`);
  270. break;
  271. case 'enable':
  272. res = await API.put('/api/models/?status_only=true', { id, status: 1 });
  273. break;
  274. case 'disable':
  275. res = await API.put('/api/models/?status_only=true', { id, status: 0 });
  276. break;
  277. default:
  278. return;
  279. }
  280. const { success, message } = res.data;
  281. if (success) {
  282. showSuccess(t('操作成功完成!'));
  283. if (action === 'delete') {
  284. await refresh();
  285. } else {
  286. // Update local state for enable/disable
  287. setModels((prevModels) =>
  288. prevModels.map((model) =>
  289. model.id === id
  290. ? { ...model, status: action === 'enable' ? 1 : 0 }
  291. : model,
  292. ),
  293. );
  294. }
  295. } else {
  296. showError(message);
  297. }
  298. };
  299. // Handle page change
  300. const handlePageChange = (page) => {
  301. setActivePage(page);
  302. loadModels(page, pageSize, activeVendorKey);
  303. };
  304. // Reload models when activeVendorKey changes
  305. useEffect(() => {
  306. loadModels(1, pageSize, activeVendorKey);
  307. }, [activeVendorKey]);
  308. // Handle page size change
  309. const handlePageSizeChange = async (size) => {
  310. setPageSize(size);
  311. setActivePage(1);
  312. await loadModels(1, size, activeVendorKey);
  313. };
  314. // Handle row click and styling
  315. const handleRow = (record, index) => {
  316. const rowStyle =
  317. record.status !== 1
  318. ? {
  319. style: {
  320. background: 'var(--semi-color-disabled-border)',
  321. },
  322. }
  323. : {};
  324. return {
  325. ...rowStyle,
  326. onClick: (event) => {
  327. // Don't trigger row selection when clicking on buttons
  328. if (event.target.closest('button, .semi-button')) {
  329. return;
  330. }
  331. const newSelectedKeys = selectedKeys.some(
  332. (item) => item.id === record.id,
  333. )
  334. ? selectedKeys.filter((item) => item.id !== record.id)
  335. : [...selectedKeys, record];
  336. setSelectedKeys(newSelectedKeys);
  337. },
  338. };
  339. };
  340. // Batch delete models
  341. const batchDeleteModels = async () => {
  342. if (selectedKeys.length === 0) {
  343. showError(t('请至少选择一个模型'));
  344. return;
  345. }
  346. try {
  347. const deletePromises = selectedKeys.map((model) =>
  348. API.delete(`/api/models/${model.id}`),
  349. );
  350. const results = await Promise.all(deletePromises);
  351. let successCount = 0;
  352. results.forEach((res, index) => {
  353. if (res.data.success) {
  354. successCount++;
  355. } else {
  356. showError(
  357. `删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`,
  358. );
  359. }
  360. });
  361. if (successCount > 0) {
  362. showSuccess(t(`成功删除 ${successCount} 个模型`));
  363. setSelectedKeys([]);
  364. await refresh();
  365. }
  366. } catch (error) {
  367. showError(t('批量删除失败'));
  368. }
  369. };
  370. // Copy text helper
  371. const copyText = async (text) => {
  372. try {
  373. await navigator.clipboard.writeText(text);
  374. showSuccess(t('复制成功'));
  375. } catch (error) {
  376. console.error('Copy failed:', error);
  377. showError(t('复制失败'));
  378. }
  379. };
  380. // Initial load
  381. useEffect(() => {
  382. (async () => {
  383. await loadVendors();
  384. })();
  385. // eslint-disable-next-line react-hooks/exhaustive-deps
  386. }, []);
  387. return {
  388. // Data state
  389. models,
  390. loading,
  391. searching,
  392. activePage,
  393. pageSize,
  394. modelCount,
  395. // Selection state
  396. selectedKeys,
  397. rowSelection,
  398. handleRow,
  399. setSelectedKeys,
  400. // Modal state
  401. showEdit,
  402. editingModel,
  403. setEditingModel,
  404. setShowEdit,
  405. closeEdit,
  406. // Form state
  407. formInitValues,
  408. setFormApi,
  409. // Actions
  410. loadModels,
  411. searchModels,
  412. refresh,
  413. manageModel,
  414. batchDeleteModels,
  415. copyText,
  416. // Pagination
  417. setActivePage,
  418. handlePageChange,
  419. handlePageSizeChange,
  420. // UI state
  421. compactMode,
  422. setCompactMode,
  423. // Vendor data
  424. vendors,
  425. vendorMap,
  426. vendorCounts,
  427. activeVendorKey,
  428. setActiveVendorKey,
  429. showAddVendor,
  430. setShowAddVendor,
  431. showEditVendor,
  432. setShowEditVendor,
  433. editingVendor,
  434. setEditingVendor,
  435. loadVendors,
  436. // Translation
  437. t,
  438. // Upstream sync
  439. syncing,
  440. previewing,
  441. syncUpstream,
  442. previewUpstreamDiff,
  443. applyUpstreamOverwrite,
  444. };
  445. };