new-api / web /src /hooks /models /useModelsData.jsx
liuzhao521
Deploy New API v0.9.25+ (commit b47cf4ef) to HuggingFace Spaces
4674012
/*
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 <https://www.gnu.org/licenses/>.
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,
};
};