1. 修改了合并模型导出模型的逻辑
2. 修改了一些冗余的bug 3. 页面上表格的调整
This commit is contained in:
@@ -429,9 +429,10 @@
|
||||
createText: '创建训练任务',
|
||||
columns: [
|
||||
{ title: '任务名称', key: 'name' },
|
||||
{ title: '基础模型', key: 'base_model', render: (val, row) => `<span class="model-name-cell" data-model-id="${val}">加载中...</span>` },
|
||||
{ title: '状态', key: 'status', render: (val) => `<span class="px-2 py-1 rounded text-xs ${val === 'running' ? 'bg-green-100 text-green-700' : val === 'failed' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}">${val}</span>` },
|
||||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||||
{ title: '任务状态', key: 'status', render: (val) => `<span class="px-2 py-1 rounded text-xs ${val === 'running' ? 'bg-green-100 text-green-700' : val === 'failed' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}">${val}</span>` },
|
||||
{ title: '训练方式', key: 'train_type', render: (val) => val === 'SFT' ? 'SFT 微调训练' : (val === 'DPO' ? 'DPO 偏好训练' : (val === 'CPT' ? 'CPT 继续预训练' : '-')) },
|
||||
{ title: '训练模板', key: 'template', render: (val) => val || '-' },
|
||||
{ title: '基座模型', key: 'base_model', render: (val, row) => `<span class="model-name-cell" data-model-id="${val}">加载中...</span>` }
|
||||
],
|
||||
actions: ['stop', 'logs', 'delete']
|
||||
},
|
||||
@@ -608,7 +609,7 @@
|
||||
'edit': '编辑',
|
||||
'compare': '开始对话',
|
||||
'chat': '对话',
|
||||
'view': '去推理'
|
||||
'view': '合并权重'
|
||||
};
|
||||
|
||||
// 训练进度缓存
|
||||
@@ -1010,21 +1011,47 @@
|
||||
|
||||
// 删除数据
|
||||
async function deleteItem(api, id) {
|
||||
showConfirm('确认删除', '确定要删除这条记录吗?', async () => {
|
||||
// 如果是我的模型,提示删除合并模型
|
||||
const confirmMessage = api === 'model-manage/trained-models'
|
||||
? '是否删除合并模型?'
|
||||
: '确定要删除这条记录吗?';
|
||||
|
||||
showConfirm('确认删除', confirmMessage, async () => {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/${api}/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
// 刷新当前页面
|
||||
clearSelection(); // 清除选中状态
|
||||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||||
if (activeLink) {
|
||||
loadPage(activeLink.dataset.page);
|
||||
// 如果是我的模型,调用删除本地训练模型的API
|
||||
if (api === 'model-manage/trained-models') {
|
||||
const response = await fetch(`${API_BASE}/model-manage/trained-models/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
showMessage('成功', '删除成功', 'success');
|
||||
// 清除合并状态缓存
|
||||
sessionStorage.removeItem('merge_status_' + id);
|
||||
// 刷新当前页面
|
||||
clearSelection();
|
||||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||||
if (activeLink) {
|
||||
loadPage(activeLink.dataset.page);
|
||||
}
|
||||
} else {
|
||||
showMessage('错误', result.message || '删除失败', 'error');
|
||||
}
|
||||
} else {
|
||||
showMessage('错误', result.message || '删除失败', 'error');
|
||||
const response = await fetch(`${API_BASE}/${api}/${id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result.code === 0) {
|
||||
// 刷新当前页面
|
||||
clearSelection(); // 清除选中状态
|
||||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||||
if (activeLink) {
|
||||
loadPage(activeLink.dataset.page);
|
||||
}
|
||||
} else {
|
||||
showMessage('错误', result.message || '删除失败', 'error');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('错误', '删除失败: ' + error.message, 'error');
|
||||
@@ -1070,6 +1097,15 @@
|
||||
loadPage('logs');
|
||||
}
|
||||
|
||||
// 查看调优任务日志 - 跳转到training-log.html页面
|
||||
function viewFineTuneLogs(taskId, taskName) {
|
||||
// 保存 taskId 到 sessionStorage
|
||||
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
|
||||
sessionStorage.setItem('trainingLogTaskName', taskName);
|
||||
// 跳转到日志页面
|
||||
navigateToPage('training-log');
|
||||
}
|
||||
|
||||
// 更新模型用途
|
||||
async function updateModelPurpose(id, purpose) {
|
||||
try {
|
||||
@@ -1108,11 +1144,11 @@
|
||||
function toggleSelectAll(checkbox, api) {
|
||||
// 使用保存的当前页面数据
|
||||
if (checkbox.checked) {
|
||||
// 全选当前页面的所有数据
|
||||
currentPageData.forEach(item => selectedItems.add(item.id));
|
||||
// 全选当前页面的所有数据(支持 name 或 id)
|
||||
currentPageData.forEach(item => selectedItems.add(item.name || item.id));
|
||||
} else {
|
||||
// 取消全选,移除当前页面所有数据的选中状态
|
||||
currentPageData.forEach(item => selectedItems.delete(item.id));
|
||||
currentPageData.forEach(item => selectedItems.delete(item.name || item.id));
|
||||
}
|
||||
refreshCurrentPage();
|
||||
}
|
||||
@@ -1266,14 +1302,16 @@
|
||||
|
||||
// 渲染表格页面
|
||||
function renderTablePage(config, data) {
|
||||
const createButton = config.hasCreate ? `
|
||||
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-plus mr-1"></i>${config.createText}
|
||||
</button>
|
||||
` : '';
|
||||
// 使用配置中的列定义,如果没有则使用默认列
|
||||
const columns = config.columns || [
|
||||
{ title: '模型名称', key: 'name' },
|
||||
{ title: '训练方法', key: 'train_methods', render: (val) => val && val[0] ? val[0].name : '-' },
|
||||
{ title: '基座模型', key: 'base_model_path', render: (val) => `<span class="text-xs text-gray-500 truncate block" title="${val}">${val || '-'}</span>` },
|
||||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||||
];
|
||||
|
||||
// 搜索框(模型管理和数据集管理)
|
||||
const searchBox = (config.api === 'model-manage' || config.api === 'dataset-manage') ? `
|
||||
// 搜索框
|
||||
const searchBox = (config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune') ? `
|
||||
<div class="relative">
|
||||
<input type="text" id="tableSearchInput" placeholder="搜索${config.title}..."
|
||||
class="w-72 pl-9 pr-3 py-1.5 rounded border border-gray-300 text-sm focus:outline-none focus:border-primary focus:ring-1 focus:border-primary"
|
||||
@@ -1283,7 +1321,22 @@
|
||||
` : '';
|
||||
|
||||
// 是否支持多选(模型管理和数据集管理)
|
||||
const supportsMultiSelect = config.api === 'model-manage' || config.api === 'dataset-manage';
|
||||
const supportsMultiSelect = config.api === 'model-manage' || config.api === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
|
||||
|
||||
// 创建按钮(根据API类型决定是否显示)
|
||||
const createButton = config.api === 'model-manage' ? `
|
||||
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-plus mr-1"></i>新建模型
|
||||
</button>
|
||||
` : (config.api === 'dataset-manage' ? `
|
||||
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-plus mr-1"></i>创建数据集
|
||||
</button>
|
||||
` : (config.api === 'fine-tune' ? `
|
||||
<button onclick="navigateToPage('fine-tune-create')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-plus mr-1"></i>新建调优任务
|
||||
</button>
|
||||
` : ''));
|
||||
|
||||
// 批量删除按钮(仅当有选中项时显示)
|
||||
const batchDeleteButton = supportsMultiSelect && selectedItems.size > 0 ? `
|
||||
@@ -1292,7 +1345,6 @@
|
||||
</button>
|
||||
` : '';
|
||||
|
||||
const columns = config.columns;
|
||||
const hasData = data && data.length > 0;
|
||||
|
||||
// 多选列头
|
||||
@@ -1312,6 +1364,11 @@
|
||||
${createButton}
|
||||
</div>
|
||||
</div>
|
||||
${config.api === 'model-manage/trained-models' ? `
|
||||
<div class="px-4 border-b border-gray-100">
|
||||
<div class="flex space-x-1" id="modelTabs">
|
||||
</div>
|
||||
` : ''}
|
||||
${supportsMultiSelect ? `
|
||||
<div id="batchActions" class="px-4 py-2 bg-blue-50 border-b border-blue-100 flex items-center justify-between ${selectedItems.size > 0 ? '' : 'hidden'}">
|
||||
<div class="flex items-center text-sm text-blue-700">
|
||||
@@ -1334,12 +1391,12 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
${hasData ? data.map(item => `
|
||||
<tr class="border-b border-gray-100 table-row-hover ${selectedItems.has(item.id) ? 'bg-blue-50' : ''}">
|
||||
<tr class="border-b border-gray-100 table-row-hover ${selectedItems.has(item.name || item.id) ? 'bg-blue-50' : ''}">
|
||||
${supportsMultiSelect ? `
|
||||
<td class="px-4 py-4 text-sm text-center">
|
||||
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
|
||||
${selectedItems.has(item.id) ? 'checked' : ''}
|
||||
onchange="toggleItemSelection(${item.id}, '${config.api}')">
|
||||
${selectedItems.has(item.name || item.id) ? 'checked' : ''}
|
||||
onchange="toggleItemSelection('${item.name || item.id}', '${config.api}')">
|
||||
</td>
|
||||
` : ''}
|
||||
${columns.map(col => `
|
||||
@@ -1349,38 +1406,23 @@
|
||||
`).join('')}
|
||||
<td class="px-4 py-4 text-sm text-center">
|
||||
<div class="flex justify-center space-x-2">
|
||||
${config.actions.map(action => {
|
||||
let onclick = '';
|
||||
let btnClass = 'text-primary hover:text-primary/80';
|
||||
|
||||
// 对于 fine-tune 的停止按钮,检查状态
|
||||
if (action === 'stop' && config.api === 'fine-tune') {
|
||||
// 状态为 completed 或 failed 时隐藏停止按钮
|
||||
if (item.status === 'completed' || item.status === 'failed') {
|
||||
return '';
|
||||
}
|
||||
onclick = `stopItem(${item.id})`;
|
||||
btnClass = 'text-orange-500 hover:text-orange-600';
|
||||
} else if (action === 'delete') {
|
||||
onclick = `deleteItem('${config.api}', ${item.id})`;
|
||||
btnClass = 'text-danger hover:text-danger/80';
|
||||
} else if (action === 'edit') {
|
||||
onclick = `editItem('${config.api}', ${item.id})`;
|
||||
} else if (action === 'preview' && config.api === 'dataset-manage') {
|
||||
onclick = `window.location.href = 'dataset-preview.html?id=${item.id}'`;
|
||||
} else if (action === 'download' && config.api === 'dataset-manage') {
|
||||
onclick = `downloadDataset('${item.id}')`;
|
||||
} else if (action === 'compare' && config.api === 'model-compare') {
|
||||
onclick = `startCompare(${item.id})`;
|
||||
} else if (action === 'logs' && config.api === 'fine-tune') {
|
||||
onclick = `navigateToTrainingLog(${item.id})`;
|
||||
} else if (action === 'view' && config.api === 'model-manage/trained-models') {
|
||||
onclick = `viewTrainedModel('${item.name}', '${item.train_methods?.[0]?.name || '-'}', '${item.path || ''}')`;
|
||||
} else {
|
||||
onclick = `showMessage('提示', '${actionLabels[action] || action}功能开发中...', 'info')`;
|
||||
}
|
||||
return `<button onclick="${onclick}" class="${btnClass}">${actionLabels[action] || action}</button>`;
|
||||
}).join('')}
|
||||
${config.api === 'fine-tune' ? `
|
||||
<button onclick="viewFineTuneLogs('${item.id}', '${item.name}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">查看日志</button>
|
||||
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
|
||||
` : (config.api === 'model-manage/trained-models' ? `
|
||||
${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)}
|
||||
${(item.merged && !item.merging) ? `
|
||||
<button onclick="exportModel('${item.name}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">导出权重</button>
|
||||
<button onclick="deleteItem('${config.api}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||||
` : ''}
|
||||
` : (config.api === 'model-manage' ? `
|
||||
<button onclick="editModel('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">编辑</button>
|
||||
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||||
` : (config.api === 'dataset-manage' ? `
|
||||
<button onclick="previewDataset('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">预览</button>
|
||||
<button onclick="downloadDataset('${item.id}')" class="bg-green-500 text-white px-3 py-1 rounded text-xs hover:bg-green-600">下载</button>
|
||||
<button onclick="deleteItem('${config.api}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||||
` : '')))}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -2506,8 +2548,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 返回列表页
|
||||
function goBack() {
|
||||
// 返回列表页 - 全局
|
||||
window.goBack = function() {
|
||||
if (currentParentPage) {
|
||||
currentPage = currentParentPage;
|
||||
currentParentPage = null;
|
||||
@@ -3164,10 +3206,113 @@
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 查看已训练模型详情 - 跳转到推理页面
|
||||
// 刷新表格数据 - 重新加载当前页面(必须在 viewTrainedModel 之前定义)
|
||||
window.loadTableData = function() {
|
||||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||||
if (activeLink) {
|
||||
loadPage(activeLink.dataset.page);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取合并按钮HTML(根据合并状态显示不同按钮)
|
||||
function getMergeButtonHtml(name, method, path, merged, merging) {
|
||||
// 优先检查 sessionStorage 中的临时状态(用于前端实时显示)
|
||||
const tempStatus = sessionStorage.getItem('merge_status_' + name);
|
||||
console.log('[DEBUG] getMergeButtonHtml:', name, 'tempStatus:', tempStatus, 'merged:', merged, 'merging:', merging);
|
||||
|
||||
// 如果前端正在合并中,显示合并中
|
||||
if (tempStatus === 'merging') {
|
||||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
|
||||
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
|
||||
</button>`;
|
||||
}
|
||||
// 如果后端返回正在合并中(锁文件存在)
|
||||
if (merging) {
|
||||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed flex items-center" disabled>
|
||||
<i class="fa fa-spinner fa-spin mr-1"></i>合并中...
|
||||
</button>`;
|
||||
}
|
||||
// 如果前端成功状态且后端也返回已合并,显示成功
|
||||
if (tempStatus === 'success' && merged) {
|
||||
// 清除临时成功状态,让后端状态接管
|
||||
sessionStorage.removeItem('merge_status_' + name);
|
||||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
|
||||
}
|
||||
// 如果前端成功状态但后端返回未合并,说明目录被删除,重置状态
|
||||
if (tempStatus === 'success' && !merged) {
|
||||
sessionStorage.removeItem('merge_status_' + name);
|
||||
}
|
||||
// 如果后端返回已合并,显示成功
|
||||
if (merged) {
|
||||
return `<button class="bg-gray-300 text-gray-500 px-3 py-1 rounded text-xs cursor-not-allowed" disabled>合并成功</button>`;
|
||||
}
|
||||
return `<button onclick="startMerge('${name}', '${method}', '${path}')" class="bg-primary text-white px-3 py-1 rounded text-xs hover:bg-primary/90">合并权重</button>`;
|
||||
}
|
||||
|
||||
// 启动合并任务
|
||||
async function startMerge(name, method, path) {
|
||||
// 先设置状态为"合并中"(存储到 sessionStorage)
|
||||
sessionStorage.setItem('merge_status_' + name, 'merging');
|
||||
// 刷新表格显示合并中状态
|
||||
loadTableData();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE}/model-manage/merge`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model_name: name,
|
||||
train_method: method || 'lora',
|
||||
base_model_path: path
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.code === 0) {
|
||||
// 设置为成功状态,确保即使后端还没更新也能显示成功
|
||||
sessionStorage.setItem('merge_status_' + name, 'success');
|
||||
// 延迟刷新表格,用后端真实状态替换前端状态
|
||||
setTimeout(() => loadTableData(), 1500);
|
||||
} else {
|
||||
// 清除合并状态
|
||||
sessionStorage.removeItem('merge_status_' + name);
|
||||
showMessage('失败', result.message || '合并失败', 'error');
|
||||
loadTableData();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DEBUG] 合并失败:', error);
|
||||
// 清除合并状态
|
||||
sessionStorage.removeItem('merge_status_' + name);
|
||||
showMessage('错误', '合并失败: ' + error.message, 'error');
|
||||
loadTableData();
|
||||
}
|
||||
}
|
||||
|
||||
// 合并模型权重(保留兼容)
|
||||
window.viewTrainedModel = function(name, method, path) {
|
||||
// 跳转到推理测试页面(main.html在pages目录下,所以直接用文件名)
|
||||
window.location.href = `model-inference.html?model=${encodeURIComponent(name)}&method=${encodeURIComponent(method)}`;
|
||||
startMerge(name, method, path);
|
||||
};
|
||||
|
||||
// 导出模型权重(打包下载)
|
||||
function exportModel(modelName) {
|
||||
// 直接跳转到导出接口下载文件
|
||||
window.open(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
|
||||
}
|
||||
|
||||
// 编辑模型 - 全局
|
||||
window.editModel = function(modelId) {
|
||||
window.location.href = `model-manage-create.html?id=${modelId}`;
|
||||
};
|
||||
|
||||
// 预览数据集 - 全局
|
||||
window.previewDataset = function(datasetId) {
|
||||
window.location.href = `dataset-preview.html?id=${datasetId}`;
|
||||
};
|
||||
|
||||
// 下载数据集 - 全局
|
||||
window.downloadDataset = function(datasetId) {
|
||||
window.open(`${API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
|
||||
};
|
||||
|
||||
// 确认弹窗(两个按钮)- 使用 window 确保全局可访问
|
||||
@@ -3446,5 +3591,15 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载中弹窗 -->
|
||||
<div id="loadingModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center">
|
||||
<div class="bg-white rounded-xl shadow-xl max-w-sm w-full mx-4 overflow-hidden transform transition-all">
|
||||
<div class="flex flex-col items-center justify-center min-h-[160px] py-6">
|
||||
<i class="fa fa-spinner fa-spin text-3xl text-primary mb-4"></i>
|
||||
<p id="loadingMessage" class="text-gray-600 text-sm">正在处理...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user