useModelsData.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398
  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 vendorMap = useMemo(() => {
  83. const map = {};
  84. vendors.forEach(v => {
  85. map[v.id] = v;
  86. });
  87. return map;
  88. }, [vendors]);
  89. // Load vendor list
  90. const loadVendors = async () => {
  91. try {
  92. const res = await API.get('/api/vendors/?page_size=1000');
  93. if (res.data.success) {
  94. const items = res.data.data.items || res.data.data || [];
  95. setVendors(Array.isArray(items) ? items : []);
  96. }
  97. } catch (_) {
  98. // ignore
  99. }
  100. };
  101. // Load models data
  102. const loadModels = async (page = 1, size = pageSize, vendorKey = activeVendorKey) => {
  103. setLoading(true);
  104. try {
  105. let url = `/api/models/?p=${page}&page_size=${size}`;
  106. if (vendorKey && vendorKey !== 'all') {
  107. // Filter by vendor ID
  108. url = `/api/models/search?vendor=${vendorKey}&p=${page}&page_size=${size}`;
  109. }
  110. const res = await API.get(url);
  111. const { success, message, data } = res.data;
  112. if (success) {
  113. const newPageData = extractItems(data);
  114. setActivePage(data.page || page);
  115. setModelCount(data.total || newPageData.length);
  116. setModelFormat(newPageData);
  117. // Refresh vendor counts only when viewing 'all' to preserve other counts
  118. if (vendorKey === 'all') {
  119. updateVendorCounts(newPageData);
  120. }
  121. } else {
  122. showError(message);
  123. setModels([]);
  124. }
  125. } catch (error) {
  126. console.error(error);
  127. showError(t('获取模型列表失败'));
  128. setModels([]);
  129. }
  130. setLoading(false);
  131. };
  132. // Fetch vendor counts separately to keep tab numbers accurate
  133. const refreshVendorCounts = async () => {
  134. try {
  135. // Load all models (large page_size) to compute counts for every vendor
  136. const res = await API.get('/api/models/?p=1&page_size=100000');
  137. if (res.data.success) {
  138. const newItems = extractItems(res.data.data);
  139. updateVendorCounts(newItems);
  140. }
  141. } catch (_) {
  142. // ignore count refresh errors
  143. }
  144. };
  145. // Refresh data
  146. const refresh = async (page = activePage) => {
  147. await loadModels(page, pageSize);
  148. // When not viewing 'all', tab counts need a separate refresh
  149. if (activeVendorKey !== 'all') {
  150. await refreshVendorCounts();
  151. }
  152. };
  153. // Search models with keyword and vendor
  154. const searchModels = async () => {
  155. const { searchKeyword = '', searchVendor = '' } = getFormValues();
  156. if (searchKeyword === '' && searchVendor === '') {
  157. // If keyword is blank, load models instead
  158. await loadModels(1, pageSize);
  159. return;
  160. }
  161. setSearching(true);
  162. try {
  163. const res = await API.get(
  164. `/api/models/search?keyword=${searchKeyword}&vendor=${searchVendor}&p=1&page_size=${pageSize}`,
  165. );
  166. const { success, message, data } = res.data;
  167. if (success) {
  168. const newPageData = extractItems(data);
  169. setActivePage(data.page || 1);
  170. setModelCount(data.total || newPageData.length);
  171. setModelFormat(newPageData);
  172. } else {
  173. showError(message);
  174. setModels([]);
  175. }
  176. } catch (error) {
  177. console.error(error);
  178. showError(t('搜索模型失败'));
  179. setModels([]);
  180. }
  181. setSearching(false);
  182. };
  183. // Manage model (enable/disable/delete)
  184. const manageModel = async (id, action, record) => {
  185. let res;
  186. switch (action) {
  187. case 'delete':
  188. res = await API.delete(`/api/models/${id}`);
  189. break;
  190. case 'enable':
  191. res = await API.put('/api/models/?status_only=true', { id, status: 1 });
  192. break;
  193. case 'disable':
  194. res = await API.put('/api/models/?status_only=true', { id, status: 0 });
  195. break;
  196. default:
  197. return;
  198. }
  199. const { success, message } = res.data;
  200. if (success) {
  201. showSuccess(t('操作成功完成!'));
  202. if (action === 'delete') {
  203. await refresh();
  204. } else {
  205. // Update local state for enable/disable
  206. setModels(prevModels =>
  207. prevModels.map(model =>
  208. model.id === id ? { ...model, status: action === 'enable' ? 1 : 0 } : model
  209. )
  210. );
  211. }
  212. } else {
  213. showError(message);
  214. }
  215. };
  216. // Update vendor counts
  217. const updateVendorCounts = (models) => {
  218. const counts = { all: models.length };
  219. models.forEach(model => {
  220. if (model.vendor_id) {
  221. counts[model.vendor_id] = (counts[model.vendor_id] || 0) + 1;
  222. }
  223. });
  224. setVendorCounts(counts);
  225. };
  226. // Handle page change
  227. const handlePageChange = (page) => {
  228. setActivePage(page);
  229. loadModels(page, pageSize, activeVendorKey);
  230. };
  231. // Reload models when activeVendorKey changes
  232. useEffect(() => {
  233. loadModels(1, pageSize, activeVendorKey);
  234. }, [activeVendorKey]);
  235. // Handle page size change
  236. const handlePageSizeChange = async (size) => {
  237. setPageSize(size);
  238. setActivePage(1);
  239. await loadModels(1, size, activeVendorKey);
  240. };
  241. // Handle row click
  242. const handleRow = (record, index) => {
  243. return {
  244. onClick: (event) => {
  245. // Don't trigger row selection when clicking on buttons
  246. if (event.target.closest('button, .semi-button')) {
  247. return;
  248. }
  249. const newSelectedKeys = selectedKeys.some(item => item.id === record.id)
  250. ? selectedKeys.filter(item => item.id !== record.id)
  251. : [...selectedKeys, record];
  252. setSelectedKeys(newSelectedKeys);
  253. },
  254. };
  255. };
  256. // Batch delete models
  257. const batchDeleteModels = async () => {
  258. if (selectedKeys.length === 0) {
  259. showError(t('请至少选择一个模型'));
  260. return;
  261. }
  262. try {
  263. const deletePromises = selectedKeys.map(model =>
  264. API.delete(`/api/models/${model.id}`)
  265. );
  266. const results = await Promise.all(deletePromises);
  267. let successCount = 0;
  268. results.forEach((res, index) => {
  269. if (res.data.success) {
  270. successCount++;
  271. } else {
  272. showError(`删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`);
  273. }
  274. });
  275. if (successCount > 0) {
  276. showSuccess(t(`成功删除 ${successCount} 个模型`));
  277. setSelectedKeys([]);
  278. await refresh();
  279. }
  280. } catch (error) {
  281. showError(t('批量删除失败'));
  282. }
  283. };
  284. // Copy text helper
  285. const copyText = async (text) => {
  286. try {
  287. await navigator.clipboard.writeText(text);
  288. showSuccess(t('复制成功'));
  289. } catch (error) {
  290. console.error('Copy failed:', error);
  291. showError(t('复制失败'));
  292. }
  293. };
  294. // Initial load
  295. useEffect(() => {
  296. loadVendors();
  297. loadModels();
  298. }, []);
  299. return {
  300. // Data state
  301. models,
  302. loading,
  303. searching,
  304. activePage,
  305. pageSize,
  306. modelCount,
  307. // Selection state
  308. selectedKeys,
  309. rowSelection,
  310. handleRow,
  311. // Modal state
  312. showEdit,
  313. editingModel,
  314. setEditingModel,
  315. setShowEdit,
  316. closeEdit,
  317. // Form state
  318. formInitValues,
  319. setFormApi,
  320. // Actions
  321. loadModels,
  322. searchModels,
  323. refresh,
  324. manageModel,
  325. batchDeleteModels,
  326. copyText,
  327. // Pagination
  328. handlePageChange,
  329. handlePageSizeChange,
  330. // UI state
  331. compactMode,
  332. setCompactMode,
  333. // Vendor data
  334. vendors,
  335. vendorMap,
  336. vendorCounts,
  337. activeVendorKey,
  338. setActiveVendorKey,
  339. showAddVendor,
  340. setShowAddVendor,
  341. showEditVendor,
  342. setShowEditVendor,
  343. editingVendor,
  344. setEditingVendor,
  345. loadVendors,
  346. // Translation
  347. t,
  348. };
  349. };