/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { API, showError, showSuccess } from '../../helpers'; import { ITEMS_PER_PAGE } from '../../constants'; import { useTableCompactMode } from '../common/useTableCompactMode'; export const useModelsData = () => { const { t } = useTranslation(); const [compactMode, setCompactMode] = useTableCompactMode('models'); // State management const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [activePage, setActivePage] = useState(1); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [searching, setSearching] = useState(false); const [modelCount, setModelCount] = useState(0); // Modal states const [showEdit, setShowEdit] = useState(false); const [editingModel, setEditingModel] = useState({ id: undefined, }); // Row selection const [selectedKeys, setSelectedKeys] = useState([]); const rowSelection = { getCheckboxProps: (record) => ({ name: record.model_name, }), selectedRowKeys: selectedKeys.map((model) => model.id), onChange: (selectedRowKeys, selectedRows) => { setSelectedKeys(selectedRows); }, }; // Form initial values const formInitValues = { searchKeyword: '', searchVendor: '', }; // ---------- helpers ---------- // Safely extract array items from API payload const extractItems = (payload) => { const items = payload?.items || payload || []; return Array.isArray(items) ? items : []; }; // Form API reference const [formApi, setFormApi] = useState(null); // Get form values helper function const getFormValues = () => formApi?.getValues() || formInitValues; // Close edit modal const closeEdit = () => { setShowEdit(false); setTimeout(() => { setEditingModel({ id: undefined }); }, 500); }; // Set model format with key field const setModelFormat = (models) => { for (let i = 0; i < models.length; i++) { models[i].key = models[i].id; } setModels(models); }; // Vendor list const [vendors, setVendors] = useState([]); const [vendorCounts, setVendorCounts] = useState({}); const [activeVendorKey, setActiveVendorKey] = useState('all'); const [showAddVendor, setShowAddVendor] = useState(false); const [showEditVendor, setShowEditVendor] = useState(false); const [editingVendor, setEditingVendor] = useState({ id: undefined }); const [syncing, setSyncing] = useState(false); const [previewing, setPreviewing] = useState(false); const vendorMap = useMemo(() => { const map = {}; vendors.forEach((v) => { map[v.id] = v; }); return map; }, [vendors]); // Load vendor list const loadVendors = async () => { try { const res = await API.get('/api/vendors/?page_size=1000'); if (res.data.success) { const items = res.data.data.items || res.data.data || []; setVendors(Array.isArray(items) ? items : []); } } catch (_) { // ignore } }; // Load models data const loadModels = async ( page = 1, size = pageSize, vendorKey = activeVendorKey, ) => { setLoading(true); try { let url = `/api/models/?p=${page}&page_size=${size}`; if (vendorKey && vendorKey !== 'all') { // Filter by vendor ID url = `/api/models/search?vendor=${vendorKey}&p=${page}&page_size=${size}`; } const res = await API.get(url); const { success, message, data } = res.data; if (success) { const newPageData = extractItems(data); setActivePage(data.page || page); setModelCount(data.total || newPageData.length); setModelFormat(newPageData); if (data.vendor_counts) { const sumAll = Object.values(data.vendor_counts).reduce( (acc, v) => acc + v, 0, ); setVendorCounts({ ...data.vendor_counts, all: sumAll }); } } else { showError(message); setModels([]); } } catch (error) { console.error(error); showError(t('获取模型列表失败')); setModels([]); } setLoading(false); }; // Refresh data const refresh = async (page = activePage) => { await loadModels(page, pageSize); }; // Sync upstream models/vendors for missing models only const syncUpstream = async (opts = {}) => { const locale = opts?.locale; setSyncing(true); try { const body = {}; if (locale) body.locale = locale; const res = await API.post('/api/models/sync_upstream', body); const { success, message, data } = res.data || {}; if (success) { const createdModels = data?.created_models || 0; const createdVendors = data?.created_vendors || 0; const skipped = (data?.skipped_models || []).length || 0; showSuccess( t( `已同步:新增 ${createdModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`, ), ); await loadVendors(); await refresh(); } else { showError(message || t('同步失败')); } } catch (e) { showError(t('同步失败')); } setSyncing(false); }; // Preview upstream differences const previewUpstreamDiff = async (opts = {}) => { const locale = opts?.locale; setPreviewing(true); try { const url = `/api/models/sync_upstream/preview${locale ? `?locale=${locale}` : ''}`; const res = await API.get(url); const { success, message, data } = res.data || {}; if (success) { return data || { missing: [], conflicts: [] }; } showError(message || t('预览失败')); return { missing: [], conflicts: [] }; } catch (e) { showError(t('预览失败')); return { missing: [], conflicts: [] }; } finally { setPreviewing(false); } }; // Apply selected overwrite const applyUpstreamOverwrite = async (payloadOrArray = []) => { const isArray = Array.isArray(payloadOrArray); const overwrite = isArray ? payloadOrArray : payloadOrArray.overwrite || []; const locale = isArray ? undefined : payloadOrArray.locale; setSyncing(true); try { const body = { overwrite }; if (locale) body.locale = locale; const res = await API.post('/api/models/sync_upstream', body); const { success, message, data } = res.data || {}; if (success) { const createdModels = data?.created_models || 0; const updatedModels = data?.updated_models || 0; const createdVendors = data?.created_vendors || 0; const skipped = (data?.skipped_models || []).length || 0; showSuccess( t( `完成:新增 ${createdModels} 模型,更新 ${updatedModels} 模型,新增 ${createdVendors} 供应商,跳过 ${skipped} 项`, ), ); await loadVendors(); await refresh(); return true; } showError(message || t('同步失败')); return false; } catch (e) { showError(t('同步失败')); return false; } finally { setSyncing(false); } }; // Search models with keyword and vendor const searchModels = async () => { const { searchKeyword = '', searchVendor = '' } = getFormValues(); if (searchKeyword === '' && searchVendor === '') { // If keyword is blank, load models instead await loadModels(1, pageSize); return; } setSearching(true); try { const res = await API.get( `/api/models/search?keyword=${searchKeyword}&vendor=${searchVendor}&p=1&page_size=${pageSize}`, ); const { success, message, data } = res.data; if (success) { const newPageData = extractItems(data); setActivePage(data.page || 1); setModelCount(data.total || newPageData.length); setModelFormat(newPageData); if (data.vendor_counts) { const sumAll = Object.values(data.vendor_counts).reduce( (acc, v) => acc + v, 0, ); setVendorCounts({ ...data.vendor_counts, all: sumAll }); } } else { showError(message); setModels([]); } } catch (error) { console.error(error); showError(t('搜索模型失败')); setModels([]); } setSearching(false); }; // Manage model (enable/disable/delete) const manageModel = async (id, action, record) => { let res; switch (action) { case 'delete': res = await API.delete(`/api/models/${id}`); break; case 'enable': res = await API.put('/api/models/?status_only=true', { id, status: 1 }); break; case 'disable': res = await API.put('/api/models/?status_only=true', { id, status: 0 }); break; default: return; } const { success, message } = res.data; if (success) { showSuccess(t('操作成功完成!')); if (action === 'delete') { await refresh(); } else { // Update local state for enable/disable setModels((prevModels) => prevModels.map((model) => model.id === id ? { ...model, status: action === 'enable' ? 1 : 0 } : model, ), ); } } else { showError(message); } }; // Handle page change const handlePageChange = (page) => { setActivePage(page); loadModels(page, pageSize, activeVendorKey); }; // Reload models when activeVendorKey changes useEffect(() => { loadModels(1, pageSize, activeVendorKey); }, [activeVendorKey]); // Handle page size change const handlePageSizeChange = async (size) => { setPageSize(size); setActivePage(1); await loadModels(1, size, activeVendorKey); }; // Handle row click and styling const handleRow = (record, index) => { const rowStyle = record.status !== 1 ? { style: { background: 'var(--semi-color-disabled-border)', }, } : {}; return { ...rowStyle, onClick: (event) => { // Don't trigger row selection when clicking on buttons if (event.target.closest('button, .semi-button')) { return; } const newSelectedKeys = selectedKeys.some( (item) => item.id === record.id, ) ? selectedKeys.filter((item) => item.id !== record.id) : [...selectedKeys, record]; setSelectedKeys(newSelectedKeys); }, }; }; // Batch delete models const batchDeleteModels = async () => { if (selectedKeys.length === 0) { showError(t('请至少选择一个模型')); return; } try { const deletePromises = selectedKeys.map((model) => API.delete(`/api/models/${model.id}`), ); const results = await Promise.all(deletePromises); let successCount = 0; results.forEach((res, index) => { if (res.data.success) { successCount++; } else { showError( `删除模型 ${selectedKeys[index].model_name} 失败: ${res.data.message}`, ); } }); if (successCount > 0) { showSuccess(t(`成功删除 ${successCount} 个模型`)); setSelectedKeys([]); await refresh(); } } catch (error) { showError(t('批量删除失败')); } }; // Copy text helper const copyText = async (text) => { try { await navigator.clipboard.writeText(text); showSuccess(t('复制成功')); } catch (error) { console.error('Copy failed:', error); showError(t('复制失败')); } }; // Initial load useEffect(() => { (async () => { await loadVendors(); })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return { // Data state models, loading, searching, activePage, pageSize, modelCount, // Selection state selectedKeys, rowSelection, handleRow, setSelectedKeys, // Modal state showEdit, editingModel, setEditingModel, setShowEdit, closeEdit, // Form state formInitValues, setFormApi, // Actions loadModels, searchModels, refresh, manageModel, batchDeleteModels, copyText, // Pagination setActivePage, handlePageChange, handlePageSizeChange, // UI state compactMode, setCompactMode, // Vendor data vendors, vendorMap, vendorCounts, activeVendorKey, setActiveVendorKey, showAddVendor, setShowAddVendor, showEditVendor, setShowEditVendor, editingVendor, setEditingVendor, loadVendors, // Translation t, // Upstream sync syncing, previewing, syncUpstream, previewUpstreamDiff, applyUpstreamOverwrite, }; };