1872 lines
92 KiB
HTML
1872 lines
92 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>模型调优 - 远光软件微调平台</title>
|
||
<script src="../lib/tailwindcss/tailwind.js"></script>
|
||
<script>
|
||
if (typeof console !== 'undefined' && console.warn) {
|
||
const originalWarn = console.warn;
|
||
console.warn = function(...args) {
|
||
if (args[0] && args[0].includes && args[0].includes('cdn.tailwindcss.com')) {
|
||
return;
|
||
}
|
||
originalWarn.apply(console, args);
|
||
};
|
||
}
|
||
</script>
|
||
<script src="../lib/marked.min.js"></script>
|
||
<!-- 外部 JS 模块 -->
|
||
<script src="../js/api.js"></script>
|
||
<script src="../js/utils.js"></script>
|
||
<script src="../js/services/system.js"></script>
|
||
<script src="../js/services/training.js"></script>
|
||
<script src="../js/components/table.js"></script>
|
||
<script src="../js/pages/render.js"></script>
|
||
<script src="../js/main.js"></script>
|
||
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||
<link href="../css/main.css" rel="stylesheet">
|
||
</head>
|
||
<body class="antialiased bg-gray-50 flex h-screen overflow-hidden">
|
||
<!-- 侧边导航 -->
|
||
<aside class="w-64 text-[#bfcbd9] flex-shrink-0 hidden md:block flex flex-col h-full" style="background-color: #001529;">
|
||
<!-- 平台LOGO区域 -->
|
||
<div class="pt-5 pb-3 border-b border-[#001529]/30 flex items-center justify-center pl-2">
|
||
<img src="../assets/logo/logo.png" alt="Logo" class="w-8 h-8 object-contain mr-2">
|
||
<span class="text-white font-medium text-base">远光软件微调平台</span>
|
||
</div></parameter>
|
||
|
||
<!-- 导航主区域 -->
|
||
<nav class="flex-1 overflow-y-auto py-2 relative">
|
||
<!-- 滑块指示器 -->
|
||
<div class="sidebar-slider" id="sidebar-slider"></div>
|
||
|
||
<!-- 第一分区:模型服务 -->
|
||
<div class="sidebar-section-title">模型服务</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="fine-tune" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-cogs w-5 text-center"></i>
|
||
<span class="ml-2">模型调优</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-eval" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-line-chart w-5 text-center"></i>
|
||
<span class="ml-2">模型评测</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-compare" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-server w-5 text-center"></i>
|
||
<span class="ml-2">模型对比</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 第二分区:资源管理 -->
|
||
<div class="sidebar-section-title mt-6">资源管理</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="model-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-cube w-5 text-center"></i>
|
||
<span class="ml-2">模型管理</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="#" data-page="dataset-manage" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-file-text w-5 text-center"></i>
|
||
<span class="ml-2">数据集管理</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="tools.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-wrench w-5 text-center"></i>
|
||
<span class="ml-2">其他工具</span>
|
||
</a>
|
||
</div>
|
||
|
||
<!-- 第三分区:系统设置 -->
|
||
<div class="sidebar-section-title mt-6">系统设置</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="hardware.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-bar-chart w-5 text-center"></i>
|
||
<span class="ml-2">平台性能</span>
|
||
</a>
|
||
</div>
|
||
<div class="nav-item-wrapper">
|
||
<a href="logs.html" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
|
||
<i class="fa fa-file-text w-5 text-center"></i>
|
||
<span class="ml-2">查看日志</span>
|
||
</a>
|
||
</div>
|
||
</nav>
|
||
|
||
<!-- 底部信息区域 -->
|
||
<div class="p-4 border-t border-[#001529]/30 text-xs mt-auto">
|
||
<div class="mb-2 text-[#bfcbd9]/80">默认业务空间</div>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-[#bfcbd9]">版本 v1.0.0</span>
|
||
<i class="fa fa-question-circle-o text-[#bfcbd9]/70"></i>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
|
||
<!-- 主内容区 -->
|
||
<div class="flex-1 flex flex-col overflow-hidden">
|
||
<!-- 顶部导航 -->
|
||
<header class="bg-white border-b border-gray-200 shadow-sm">
|
||
<div class="flex items-center justify-between px-6 h-14">
|
||
<div class="flex items-center space-x-6">
|
||
<!-- 返回按钮(仅外部页面显示) -->
|
||
<button id="pageBackBtn" class="hidden text-gray-500 hover:text-gray-700 flex items-center transition-colors" onclick="goBackToList()">
|
||
<i class="fa fa-arrow-left mr-1"></i>
|
||
<span>返回</span>
|
||
</button>
|
||
<button class="md:hidden text-gray-500 hover:text-gray-700">
|
||
<i class="fa fa-bars"></i>
|
||
</button>
|
||
</div>
|
||
<div class="flex items-center space-x-4">
|
||
<!-- 系统性能监控 -->
|
||
<a href="?page=config" class="flex items-center space-x-4 text-xs text-gray-500 hover:text-primary transition-colors">
|
||
<div class="flex items-center" title="CPU使用率">
|
||
<i class="fa fa-microchip mr-1 text-blue-500"></i>
|
||
<span id="cpuUsage">--</span>%
|
||
</div>
|
||
<div class="flex items-center" title="内存使用率">
|
||
<i class="fa fa-database mr-1 text-green-500"></i>
|
||
<span id="memUsage">--</span>%
|
||
</div>
|
||
<div class="flex items-center" title="磁盘使用率">
|
||
<i class="fa fa-hdd-o mr-1 text-orange-500"></i>
|
||
<span id="diskUsage">--</span>%
|
||
</div>
|
||
</a>
|
||
<div class="h-6 w-px bg-gray-200"></div>
|
||
<div class="relative group">
|
||
<img src="https://picsum.photos/id/1005/32/32" class="w-8 h-8 rounded-full cursor-pointer" alt="用户头像">
|
||
<div class="absolute right-0 top-full pt-2 hidden group-hover:block z-50">
|
||
<div class="bg-white rounded shadow-lg py-1 border border-gray-100 min-w-[140px]">
|
||
<a href="login.html" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-50 whitespace-nowrap">
|
||
<i class="fa fa-sign-out mr-1"></i>退出登录
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<!-- 内容区域 -->
|
||
<main class="flex-1 overflow-y-auto p-6 bg-gray-50">
|
||
<div id="page-content">
|
||
<!-- 内容由 JavaScript 动态加载 -->
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script>
|
||
// API 基础地址
|
||
if (typeof window.getApiBase !== 'function') {
|
||
window.getApiBase = () => {
|
||
const protocol = window.location.protocol;
|
||
const hostname = window.location.hostname;
|
||
return `${protocol}//${hostname}:7861/api`;
|
||
};
|
||
}
|
||
if (typeof window.API_BASE === 'undefined') {
|
||
window.API_BASE = window.getApiBase();
|
||
}
|
||
const API_BASE = window.API_BASE;
|
||
|
||
// 获取系统性能监控数据
|
||
async function fetchSystemMetrics() {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/health`);
|
||
const result = await response.json();
|
||
if (result.code === 0 && result.data) {
|
||
const data = result.data;
|
||
// 更新CPU使用率
|
||
const cpuEl = document.getElementById('cpuUsage');
|
||
if (cpuEl && data.cpu_percent !== undefined) {
|
||
cpuEl.textContent = data.cpu_percent;
|
||
cpuEl.className = data.cpu_percent > 80 ? 'text-red-500 font-medium' : '';
|
||
}
|
||
// 更新内存使用率
|
||
const memEl = document.getElementById('memUsage');
|
||
if (memEl && data.memory_percent !== undefined) {
|
||
memEl.textContent = data.memory_percent;
|
||
memEl.className = data.memory_percent > 80 ? 'text-red-500 font-medium' : '';
|
||
}
|
||
// 更新磁盘使用率
|
||
const diskEl = document.getElementById('diskUsage');
|
||
if (diskEl && data.disk_percent !== undefined) {
|
||
diskEl.textContent = data.disk_percent;
|
||
diskEl.className = data.disk_percent > 80 ? 'text-red-500 font-medium' : '';
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('获取系统监控数据失败:', error);
|
||
}
|
||
}
|
||
|
||
// 页面加载时获取监控数据,并定期刷新
|
||
fetchSystemMetrics();
|
||
setInterval(fetchSystemMetrics, 30000);
|
||
|
||
// 各功能模块的表格配置
|
||
const tableConfigs = {
|
||
'fine-tune': {
|
||
title: '模型调优',
|
||
api: 'fine-tune',
|
||
hasCreate: true,
|
||
createText: '创建训练任务',
|
||
columns: [
|
||
{ title: '任务名称', key: 'name' },
|
||
{ 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']
|
||
},
|
||
'my-models': {
|
||
title: '我的模型',
|
||
api: 'model-manage/trained-models',
|
||
dataPath: 'models', // 特殊处理:API返回的是 {data: {models: [...]}} 格式
|
||
hasCreate: false,
|
||
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') : '-' }
|
||
],
|
||
actions: ['view', 'delete']
|
||
},
|
||
'model-eval': {
|
||
title: '模型评测',
|
||
isExternalPage: true,
|
||
createConfig: {
|
||
page: 'model-eval-create',
|
||
hasCreate: true,
|
||
createText: '新建评测'
|
||
}
|
||
},
|
||
'model-compare': {
|
||
title: '模型对比',
|
||
api: 'model-compare',
|
||
hasCreate: true,
|
||
createText: '新建对比',
|
||
columns: [
|
||
{ title: '对比名称', key: 'model_name' },
|
||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||
{ title: '相关模型', key: 'models', render: (val) => {
|
||
if (!val) return '-';
|
||
try {
|
||
const modelIds = typeof val === 'string' ? JSON.parse(val) : val;
|
||
return getModelNames(modelIds);
|
||
} catch { return '-'; }
|
||
}},
|
||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||
],
|
||
actions: ['startCompare', 'delete']
|
||
},
|
||
'dataset-manage': {
|
||
title: '数据集管理',
|
||
api: 'dataset-manage',
|
||
hasCreate: true,
|
||
createText: '上传数据集',
|
||
columns: [
|
||
{ title: '数据集名称', key: 'name' },
|
||
{ title: '数据类型', key: 'type', render: (val) => {
|
||
const textMap = {
|
||
'train': '训练数据',
|
||
'test': '测试数据',
|
||
'eval': '评测数据',
|
||
'val': '验证数据',
|
||
'other': '其他'
|
||
};
|
||
const displayText = textMap[val?.toLowerCase()] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '存储位置', key: 'storage_type', render: (val) => {
|
||
const textMap = {
|
||
'local': '本地存储',
|
||
'minio': 'MinIO',
|
||
'cloud': '云存储'
|
||
};
|
||
const displayText = textMap[val] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-green-100 text-green-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '大小', key: 'size', render: (val) => (val && val !== '0 B' && val !== '0') ? val : '-' },
|
||
{ title: '数据条数', key: 'count', render: (val) => val || 0 },
|
||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||
],
|
||
actions: ['preview', 'download', 'delete']
|
||
},
|
||
'data-generate': {
|
||
title: '其他工具',
|
||
skipFetch: true,
|
||
hasCreate: false,
|
||
isExternalPage: true
|
||
},
|
||
'model-manage': {
|
||
title: '模型管理',
|
||
api: 'model-manage',
|
||
hasCreate: true,
|
||
hasModelTabs: true,
|
||
createText: '添加模型',
|
||
columns: [
|
||
{ title: '模型名称', key: 'name' },
|
||
{ title: '模型类型', key: 'type', render: (val) => {
|
||
const textMap = {
|
||
'LLM': '大语言模型',
|
||
'CV': '计算机视觉',
|
||
'NLP': '自然语言处理',
|
||
'Embedding': '向量模型',
|
||
'Other': '其他'
|
||
};
|
||
const displayText = textMap[val] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-blue-100 text-blue-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '用途', key: 'purpose', render: (val) => {
|
||
const purposeMap = {
|
||
'training': { text: '训练', class: 'bg-blue-100 text-blue-700' },
|
||
'inference': { text: '推理', class: 'bg-green-100 text-green-700' },
|
||
'evaluation': { text: '评测', class: 'bg-purple-100 text-purple-700' }
|
||
};
|
||
const display = purposeMap[val] || { text: val || '-', class: 'bg-gray-100 text-gray-700' };
|
||
return '<span class="px-2 py-1 rounded text-xs ' + display.class + '">' + display.text + '</span>';
|
||
}},
|
||
{ title: '模型来源', key: 'model_source', render: (val) => {
|
||
const textMap = {
|
||
'local': '本地模型',
|
||
'api': '在线模型',
|
||
'online': '在线模型'
|
||
};
|
||
const displayText = textMap[val] || val || '-';
|
||
return '<span class="px-2 py-1 rounded text-xs bg-gray-100 text-gray-700">' + displayText + '</span>';
|
||
}},
|
||
{ title: '描述', key: 'description', render: (val) => val || '-' },
|
||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||
],
|
||
actions: ['edit', 'delete'],
|
||
// 训练模型 tab 的列配置
|
||
trainedColumns: [
|
||
{ 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') : '-' }
|
||
],
|
||
trainedActions: ['view', 'delete']
|
||
},
|
||
'logs': {
|
||
title: '查看日志',
|
||
skipFetch: true,
|
||
hasCreate: false,
|
||
isExternalPage: true
|
||
},
|
||
'model-compare-chat': {
|
||
title: '模型对比',
|
||
skipFetch: true,
|
||
hasCreate: false,
|
||
isExternalPage: true
|
||
},
|
||
'model-compare-result': {
|
||
title: '对比结果',
|
||
skipFetch: true,
|
||
hasCreate: false,
|
||
isExternalPage: true
|
||
},
|
||
'training-log': {
|
||
title: '训练日志',
|
||
skipFetch: true,
|
||
hasCreate: false,
|
||
isExternalPage: true
|
||
}
|
||
};
|
||
|
||
// 操作按钮映射
|
||
const actionLabels = {
|
||
'stop': '停止',
|
||
'logs': '查看日志',
|
||
'delete': '删除',
|
||
'deploy': '部署',
|
||
'eval': '评测',
|
||
'report': '查看报告',
|
||
'scale': '扩容',
|
||
'preview': '预览',
|
||
'download': '下载',
|
||
'detail': '详情',
|
||
'edit': '编辑',
|
||
'compare': '开始对话',
|
||
'chat': '对话',
|
||
'view': '合并权重'
|
||
};
|
||
|
||
// 训练进度缓存
|
||
let trainingProgressCache = {};
|
||
let progressRefreshTimer = null;
|
||
|
||
// 渲染训练进度
|
||
function renderTrainingProgress(val, row) {
|
||
const progressData = trainingProgressCache[row.id];
|
||
if (progressData && progressData.status === 'running') {
|
||
if (progressData.progress > 0) {
|
||
return `
|
||
<div class="flex flex-col">
|
||
<span class="text-sm font-medium text-primary">${progressData.progress}%</span>
|
||
<span class="text-xs text-gray-500">${progressData.step || ''} ${progressData.speed || ''}</span>
|
||
<span class="text-xs text-gray-400">ETA: ${progressData.eta || '--:--'}</span>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
return `${val || 0}%`;
|
||
}
|
||
|
||
// 刷新训练进度
|
||
async function refreshTrainingProgress() {
|
||
if (currentPage !== 'fine-tune') return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/fine-tune`);
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0 && result.data) {
|
||
// 刷新运行中或已完成的任务(有进度信息)
|
||
const activeTasks = result.data.filter(task =>
|
||
task.status === 'running' || task.status === 'pending'
|
||
);
|
||
|
||
for (const task of activeTasks) {
|
||
try {
|
||
// 并行获取进度和PID状态
|
||
const [progressResponse, statusResponse] = await Promise.all([
|
||
fetch(`${API_BASE}/fine-tune/progress/${task.id}`),
|
||
fetch(`${API_BASE}/fine-tune/${task.id}`)
|
||
]);
|
||
const progressResult = await progressResponse.json();
|
||
const statusResult = await statusResponse.json();
|
||
|
||
if (progressResult.code === 0 && progressResult.data) {
|
||
trainingProgressCache[task.id] = progressResult.data;
|
||
}
|
||
|
||
// 如果状态已改变(PID已结束),更新表格中的状态显示
|
||
if (statusResult.code === 0 && statusResult.data) {
|
||
const actualStatus = statusResult.data.status;
|
||
if (task.status !== actualStatus) {
|
||
// 找到对应的行并更新状态
|
||
const row = document.querySelector(`tr[data-id="${task.id}"]`);
|
||
if (row) {
|
||
const statusCell = row.querySelector('td:nth-child(3)');
|
||
if (statusCell) {
|
||
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${actualStatus === 'running' ? 'bg-green-100 text-green-700' : actualStatus === 'failed' ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'}">${actualStatus}</span>`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn(`获取任务 ${task.id} 信息失败:`, e);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('刷新训练进度失败:', error);
|
||
}
|
||
}
|
||
|
||
// 检查并更新任务状态(用于 fine-tune 页面)
|
||
async function checkAndUpdateTaskStatus() {
|
||
if (currentPage !== 'fine-tune') return;
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/fine-tune`);
|
||
const result = await response.json();
|
||
|
||
if (result.code === 0 && result.data) {
|
||
// 获取所有 running 状态的任务
|
||
const runningTasks = result.data.filter(task => task.status === 'running');
|
||
|
||
for (const task of runningTasks) {
|
||
try {
|
||
// 调用 status API 获取实际状态(会检查 PID)
|
||
const statusResponse = await fetch(`${API_BASE}/fine-tune/${task.id}`);
|
||
const statusResult = await statusResponse.json();
|
||
|
||
if (statusResult.code === 0 && statusResult.data) {
|
||
const actualStatus = statusResult.data.status;
|
||
// 如果实际状态不是 running,更新表格显示
|
||
if (actualStatus !== 'running') {
|
||
const row = document.querySelector(`tr[data-id="${task.id}"]`);
|
||
if (row) {
|
||
const statusCell = row.querySelector('td:nth-child(3)');
|
||
if (statusCell) {
|
||
const statusClass = actualStatus === 'failed'
|
||
? 'bg-red-100 text-red-700'
|
||
: 'bg-blue-100 text-blue-700';
|
||
statusCell.innerHTML = `<span class="px-2 py-1 rounded text-xs ${statusClass}">${actualStatus}</span>`;
|
||
console.log(`[Status] 任务 ${task.id} 状态已更新: running -> ${actualStatus}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn(`检查任务 ${task.id} 状态失败:`, e);
|
||
}
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.warn('检查任务状态失败:', error);
|
||
}
|
||
}
|
||
|
||
// 页面加载完成后初始化
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
// 从 localStorage 加载自定义工具
|
||
const savedCustomTools = localStorage.getItem('customTools');
|
||
if (savedCustomTools) {
|
||
tableConfigs['data-generate'].customTools = JSON.parse(savedCustomTools);
|
||
}
|
||
|
||
// 加载模型列表缓存(用于模型对比显示模型名称)
|
||
loadModelListCache();
|
||
|
||
// 检查URL参数
|
||
const urlParams = new URLSearchParams(window.location.search);
|
||
const pageParam = urlParams.get('page');
|
||
|
||
// 确定加载的页面
|
||
let defaultPage = 'fine-tune'; // 默认页面
|
||
|
||
if (pageParam) {
|
||
// URL有参数,优先使用URL参数
|
||
defaultPage = pageParam;
|
||
} else {
|
||
// 没有URL参数,使用 sessionStorage 或 localStorage
|
||
// 优先使用 sessionStorage(仅当前会话有效)
|
||
const sessionPage = sessionStorage.getItem('lastPage');
|
||
const localPage = localStorage.getItem('lastPage');
|
||
const savedPage = sessionPage || localPage;
|
||
|
||
if (savedPage && tableConfigs[savedPage]) {
|
||
defaultPage = savedPage;
|
||
}
|
||
// 否则保持默认 'fine-tune'
|
||
}
|
||
|
||
// 保存到 sessionStorage(当前会话)
|
||
sessionStorage.setItem('lastPage', defaultPage);
|
||
|
||
loadPage(defaultPage);
|
||
|
||
// 启动训练进度自动刷新(每5秒)
|
||
progressRefreshTimer = setInterval(() => {
|
||
refreshTrainingProgress();
|
||
checkAndUpdateTaskStatus();
|
||
}, 5000);
|
||
|
||
// 更新侧边栏高亮状态
|
||
document.querySelectorAll('.nav-link').forEach(link => {
|
||
if (link.dataset.page === defaultPage) {
|
||
link.classList.add('sidebar-item-active');
|
||
link.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
|
||
} else {
|
||
link.classList.remove('sidebar-item-active');
|
||
link.classList.add('hover:bg-[#001529]/20', 'transition-colors');
|
||
}
|
||
});
|
||
|
||
// 初始化滑块位置
|
||
initSlider();
|
||
// 设置初始选中项的滑块位置
|
||
setTimeout(() => {
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink) {
|
||
updateSliderPosition(activeLink.closest('.nav-item-wrapper'));
|
||
}
|
||
}, 50);
|
||
|
||
// 绑定导航点击事件
|
||
document.querySelectorAll('.nav-link').forEach(link => {
|
||
link.addEventListener('click', function(e) {
|
||
// 如果是外部页面链接(直接 href 到 .html),允许默认行为
|
||
const href = this.getAttribute('href');
|
||
if (href && href.endsWith('.html')) {
|
||
return; // 允许浏览器默认跳转
|
||
}
|
||
|
||
e.preventDefault();
|
||
const page = this.dataset.page;
|
||
loadPage(page);
|
||
|
||
// 保存当前页面到 sessionStorage(当前会话有效,关闭浏览器后失效)
|
||
sessionStorage.setItem('lastPage', page);
|
||
localStorage.setItem('lastPage', page); // 同时保存到 localStorage
|
||
|
||
// 更新高亮状态
|
||
document.querySelectorAll('.nav-link').forEach(l => {
|
||
l.classList.remove('sidebar-item-active');
|
||
l.classList.add('hover:bg-[#001529]/20', 'transition-colors');
|
||
});
|
||
this.classList.add('sidebar-item-active');
|
||
this.classList.remove('hover:bg-[#001529]/20', 'transition-colors');
|
||
|
||
// 更新滑块位置
|
||
updateSliderPosition(this.closest('.nav-item-wrapper'));
|
||
});
|
||
});
|
||
});
|
||
|
||
// 初始化滑块
|
||
function initSlider() {
|
||
const slider = document.getElementById('sidebar-slider');
|
||
if (!slider) return;
|
||
// 确保滑块初始不可见
|
||
slider.style.height = '0';
|
||
slider.style.opacity = '0';
|
||
}
|
||
|
||
// 更新滑块位置
|
||
function updateSliderPosition(targetWrapper) {
|
||
const slider = document.getElementById('sidebar-slider');
|
||
if (!slider || !targetWrapper) return;
|
||
|
||
const nav = document.querySelector('nav');
|
||
const navRect = nav.getBoundingClientRect();
|
||
const wrapperRect = targetWrapper.getBoundingClientRect();
|
||
|
||
// 计算相对于nav的位置
|
||
const top = wrapperRect.top - navRect.top + nav.scrollTop;
|
||
const height = wrapperRect.height;
|
||
|
||
// 应用新位置(带动画)
|
||
slider.style.top = top + 'px';
|
||
slider.style.height = height + 'px';
|
||
slider.style.opacity = '1';
|
||
}
|
||
|
||
// 加载页面内容
|
||
async function loadPage(pageName) {
|
||
// 切换页面时清除选中状态
|
||
clearSelection();
|
||
|
||
// 离开模型调优页面时停止进度刷新
|
||
if (currentPage === 'fine-tune' && pageName !== 'fine-tune') {
|
||
if (progressRefreshTimer) {
|
||
clearInterval(progressRefreshTimer);
|
||
progressRefreshTimer = null;
|
||
}
|
||
}
|
||
|
||
const container = document.getElementById('page-content');
|
||
const config = tableConfigs[pageName];
|
||
|
||
if (!config) return;
|
||
|
||
// 显示加载中
|
||
container.innerHTML = `
|
||
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
|
||
<i class="fa fa-spinner fa-spin text-3xl text-primary"></i>
|
||
<p class="mt-2 text-gray-500">加载中...</p>
|
||
</div>
|
||
`;
|
||
|
||
// 显示/隐藏返回按钮(外部页面显示,普通页面隐藏)
|
||
const backBtn = document.getElementById('pageBackBtn');
|
||
if (config.isExternalPage) {
|
||
backBtn.classList.remove('hidden');
|
||
} else {
|
||
backBtn.classList.add('hidden');
|
||
}
|
||
|
||
try {
|
||
// 渲染页面
|
||
if (config.isExternalPage) {
|
||
// 外部页面,直接通过 fetch 加载 HTML(添加时间戳禁用缓存)
|
||
const response = await fetch(`${pageName}.html?t=${Date.now()}`);
|
||
if (response.ok) {
|
||
const html = await response.text();
|
||
// 提取所有内联脚本内容(没有src属性的script标签)
|
||
const scriptRegex = /<script\b(?![^>]*\bsrc)[^>]*>([\s\S]*?)<\/script>/g;
|
||
const scriptContents = [];
|
||
let match;
|
||
while ((match = scriptRegex.exec(html)) !== null) {
|
||
scriptContents.push(match[1]);
|
||
}
|
||
const scriptContent = scriptContents.join('\n');
|
||
// 移除所有script标签后插入HTML
|
||
const htmlWithoutScript = html.replace(/<script\b[^>]*>[\s\S]*?<\/script>/g, '');
|
||
|
||
// 如果有创建配置,添加 Tab 切换和创建按钮
|
||
let headerHtml = '';
|
||
if (config.createConfig && config.createConfig.hasCreate) {
|
||
headerHtml = `
|
||
<div class="bg-white rounded-lg shadow-sm mb-6 p-4 border-b border-gray-100 flex items-center justify-between">
|
||
<div class="flex items-center space-x-8">
|
||
<button class="tab-btn active pb-3 text-sm font-medium flex items-center text-primary" data-tab="tasks" onclick="switchTab(this, 'tasks')">
|
||
<i class="fa fa-tasks mr-2"></i>评测任务
|
||
</button>
|
||
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="leaderboard" onclick="switchTab(this, 'leaderboard')">
|
||
<i class="fa fa-trophy mr-2"></i>排行榜
|
||
</button>
|
||
<button class="tab-btn pb-3 text-sm font-medium flex items-center text-gray-500" data-tab="dimensions" onclick="switchTab(this, 'dimensions')">
|
||
<i class="fa fa-sliders mr-2"></i>评测维度
|
||
</button>
|
||
</div>
|
||
<div id="headerActionButtons" style="min-height: 36px;">
|
||
<button onclick="navigateToPage('${config.createConfig.page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||
<i class="fa fa-plus mr-2"></i>${config.createConfig.createText}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.tab-btn { position: relative; transition: all 0.2s; }
|
||
.tab-btn.active { color: #1890ff; }
|
||
.tab-btn.active::after { content: ''; position: absolute; bottom: -16px; left: 0; right: 0; height: 2px; background-color: #1890ff; }
|
||
.tab-btn:hover:not(.active) { color: #1890ff; }
|
||
</style>
|
||
`;
|
||
}
|
||
|
||
container.innerHTML = headerHtml + htmlWithoutScript;
|
||
// 执行脚本 - 使用 script 元素注入,使函数在全局作用域可用
|
||
if (scriptContent && scriptContent.trim()) {
|
||
try {
|
||
// 移除可能存在的旧脚本容器
|
||
const oldScript = document.getElementById('externalPageScript');
|
||
if (oldScript) oldScript.remove();
|
||
|
||
// 创建新的 script 元素
|
||
const scriptEl = document.createElement('script');
|
||
scriptEl.id = 'externalPageScript';
|
||
scriptEl.textContent = scriptContent;
|
||
document.body.appendChild(scriptEl);
|
||
} catch (e) {
|
||
console.error('执行脚本失败:', e);
|
||
}
|
||
}
|
||
} else {
|
||
throw new Error('页面加载失败');
|
||
}
|
||
} else if (config.isForm) {
|
||
const data = await fetchData(`${API_BASE}/${config.api}`);
|
||
container.innerHTML = renderFormPage(config, data);
|
||
} else {
|
||
// 模型管理页面根据tab选择不同的API
|
||
let apiUrl = `${API_BASE}/${config.api}`;
|
||
if (config.hasModelTabs) {
|
||
if (currentModelTab === 'trained') {
|
||
apiUrl = `${API_BASE}/model-manage/trained-models`;
|
||
}
|
||
}
|
||
let data = await fetchData(apiUrl);
|
||
// 如果配置了 dataPath,从返回数据中提取指定字段
|
||
// 训练模型 tab 需要从 {models: [...]} 中提取数据
|
||
if (config.hasModelTabs && currentModelTab === 'trained' && typeof data === 'object' && data !== null) {
|
||
data = data.models || [];
|
||
} else if (config.dataPath && typeof data === 'object' && data !== null) {
|
||
data = data[config.dataPath] || [];
|
||
}
|
||
currentPageData = data; // 保存当前页面数据
|
||
container.innerHTML = renderTablePage(config, data);
|
||
|
||
// 异步更新模型名称单元格
|
||
setTimeout(() => {
|
||
const modelCells = container.querySelectorAll('.model-name-cell');
|
||
modelCells.forEach(cell => {
|
||
const modelId = cell.getAttribute('data-model-id');
|
||
if (modelId) {
|
||
fetchAndUpdateModelName(modelId, cell);
|
||
}
|
||
});
|
||
}, 0);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载数据失败:', error);
|
||
// 详细的错误分类
|
||
let errorMsg = error.message || '未知错误';
|
||
let errorHint = '请检查后端服务是否启动';
|
||
|
||
if (errorMsg.includes('页面加载失败')) {
|
||
errorHint = '页面文件不存在,请检查路由配置';
|
||
} else if (errorMsg.includes('获取数据失败') || errorMsg.includes('Failed to fetch')) {
|
||
errorHint = 'API 服务不可用,请检查后端服务是否启动';
|
||
}
|
||
|
||
container.innerHTML = `
|
||
<div class="bg-white rounded-lg shadow-sm mb-6 p-8 text-center">
|
||
<i class="fa fa-exclamation-circle text-3xl text-danger"></i>
|
||
<p class="mt-2 text-gray-500">加载数据失败</p>
|
||
<p class="text-sm text-gray-400 mt-1">${errorMsg}</p>
|
||
<p class="text-xs text-gray-500 mt-2">${errorHint}</p>
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
// 获取 API 数据
|
||
async function fetchData(url) {
|
||
const response = await fetch(url);
|
||
const result = await response.json();
|
||
if (result.code !== 0) {
|
||
throw new Error(result.message || '获取数据失败');
|
||
}
|
||
return result.data || [];
|
||
}
|
||
|
||
// 删除数据
|
||
async function deleteItem(api, id) {
|
||
// 如果是我的模型,提示删除合并模型
|
||
const confirmMessage = api === 'model-manage/trained-models'
|
||
? '是否删除合并模型?'
|
||
: '确定要删除这条记录吗?';
|
||
|
||
showConfirm('确认删除', confirmMessage, async () => {
|
||
try {
|
||
// 如果是我的模型,调用删除本地训练模型的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 {
|
||
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');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 停止训练任务
|
||
async function stopItem(taskId) {
|
||
showConfirm('确认停止', '确定要停止这个训练任务吗?进程将被终止。', async () => {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/fine-tune/stop/${taskId}`, {
|
||
method: 'POST'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
showMessage('成功', '训练任务已停止', 'success');
|
||
// 刷新当前页面
|
||
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');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 跳转到训练日志二级页面
|
||
function navigateToTrainingLog(taskId) {
|
||
// 设置 sessionStorage 传递 taskId
|
||
sessionStorage.setItem('trainingLogTaskId', taskId.toString());
|
||
// 跳转到日志页面
|
||
navigateToPage('training-log');
|
||
}
|
||
|
||
// 查看训练日志 - 跳转到日志页面
|
||
async function viewTrainingLog(taskId, taskName) {
|
||
// 跳转到日志页面
|
||
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 {
|
||
const response = await fetch(`${API_BASE}/model-manage/${id}/purpose`, {
|
||
method: 'PUT',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ purpose })
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
// 刷新当前页面
|
||
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');
|
||
}
|
||
}
|
||
|
||
// 切换单个项的选中状态
|
||
function toggleItemSelection(id, api) {
|
||
if (selectedItems.has(id)) {
|
||
selectedItems.delete(id);
|
||
} else {
|
||
selectedItems.add(id);
|
||
}
|
||
// 重新渲染当前页面以更新UI
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 切换全选/取消全选
|
||
function toggleSelectAll(checkbox, api) {
|
||
// 使用保存的当前页面数据
|
||
if (checkbox.checked) {
|
||
// 全选当前页面的所有数据(支持 name 或 id)
|
||
currentPageData.forEach(item => selectedItems.add(item.name || item.id));
|
||
} else {
|
||
// 取消全选,移除当前页面所有数据的选中状态
|
||
currentPageData.forEach(item => selectedItems.delete(item.name || item.id));
|
||
}
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 清除所有选中项
|
||
function clearSelection() {
|
||
selectedItems.clear();
|
||
refreshCurrentPage();
|
||
}
|
||
|
||
// 批量删除选中的项
|
||
function batchDeleteItems(api) {
|
||
if (selectedItems.size === 0) {
|
||
showMessage('提示', '请先选择要删除的项', 'warning');
|
||
return;
|
||
}
|
||
|
||
showConfirm('批量删除', `确定要删除选中的 ${selectedItems.size} 条记录吗?`, async () => {
|
||
const ids = Array.from(selectedItems);
|
||
let successCount = 0;
|
||
let failCount = 0;
|
||
|
||
for (const id of ids) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/${api}/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
successCount++;
|
||
} else {
|
||
failCount++;
|
||
}
|
||
} catch (error) {
|
||
failCount++;
|
||
}
|
||
}
|
||
|
||
clearSelection();
|
||
|
||
// 刷新当前页面
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink) {
|
||
loadPage(activeLink.dataset.page);
|
||
}
|
||
|
||
if (failCount === 0) {
|
||
showMessage('成功', `成功删除 ${successCount} 条记录`, 'success');
|
||
} else {
|
||
showMessage('部分失败', `成功删除 ${successCount} 条,${failCount} 条删除失败`, 'warning');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 刷新当前页面(重新渲染)
|
||
function refreshCurrentPage() {
|
||
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
|
||
if (activeLink) {
|
||
const pageName = activeLink.dataset.page;
|
||
const config = tableConfigs[pageName];
|
||
|
||
// 检查 config 和 api 是否存在
|
||
if (config && (config.api === 'model-manage' || config.api === 'dataset-manage' || config.hasModelTabs)) {
|
||
const container = document.getElementById('page-content');
|
||
if (container && typeof renderTablePage === 'function') {
|
||
container.innerHTML = renderTablePage(config, currentPageData);
|
||
// 恢复复选框状态
|
||
updateCheckboxStates();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 更新复选框状态(保持选中状态)
|
||
function updateCheckboxStates() {
|
||
const checkboxes = document.querySelectorAll('tbody input[type="checkbox"]');
|
||
checkboxes.forEach(cb => {
|
||
const id = parseInt(cb.getAttribute('onchange').match(/toggleItemSelection\((\d+)/)?.[1] || cb.getAttribute('onchange').match(/toggleItemSelection\(([^,]+)/)?.[1]);
|
||
if (selectedItems.has(id)) {
|
||
cb.checked = true;
|
||
cb.closest('tr').classList.add('bg-blue-50');
|
||
} else {
|
||
cb.checked = false;
|
||
cb.closest('tr').classList.remove('bg-blue-50');
|
||
}
|
||
});
|
||
|
||
// 更新批量操作栏的显示状态
|
||
const batchActions = document.getElementById('batchActions');
|
||
if (batchActions) {
|
||
if (selectedItems.size > 0) {
|
||
batchActions.classList.remove('hidden');
|
||
batchActions.querySelector('strong').textContent = selectedItems.size;
|
||
} else {
|
||
batchActions.classList.add('hidden');
|
||
}
|
||
}
|
||
|
||
// 更新批量删除按钮
|
||
const batchDeleteBtn = document.querySelector('#batchActions button[onclick^="batchDeleteItems"]');
|
||
if (batchDeleteBtn) {
|
||
if (selectedItems.size > 0) {
|
||
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (${selectedItems.size})`;
|
||
} else {
|
||
batchDeleteBtn.innerHTML = `<i class="fa fa-trash mr-1"></i>批量删除 (0)`;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 编辑数据集
|
||
function editItem(api, id) {
|
||
if (api === 'dataset-manage') {
|
||
// 跳转到数据集创建页面进行编辑
|
||
window.location.href = `dataset-create.html?id=${id}`;
|
||
} else if (api === 'model-manage') {
|
||
// 跳转到模型创建页面进行编辑
|
||
window.location.href = `model-manage-create.html?id=${id}`;
|
||
} else {
|
||
showMessage('提示', '编辑功能开发中...', 'info');
|
||
}
|
||
}
|
||
|
||
// 下载数据集(打包下载)
|
||
function downloadDataset(datasetId) {
|
||
window.open(`${API_BASE}/dataset-manage/download/${datasetId}`, '_blank');
|
||
}
|
||
|
||
// 开始模型对比
|
||
async function startCompare(id) {
|
||
// 跳转到模型对比聊天页面(独立页面)
|
||
window.location.href = `model-compare-chat.html?id=${id}`;
|
||
}
|
||
|
||
// 筛选表格
|
||
function filterTable() {
|
||
const searchInput = document.getElementById('tableSearchInput');
|
||
if (!searchInput) return;
|
||
|
||
const keyword = searchInput.value.toLowerCase().trim();
|
||
const tbody = document.querySelector('#page-content table tbody');
|
||
if (!tbody) return;
|
||
|
||
const rows = tbody.querySelectorAll('tr');
|
||
rows.forEach(row => {
|
||
const text = row.querySelector('td')?.textContent?.toLowerCase() || '';
|
||
if (keyword === '' || text.includes(keyword)) {
|
||
row.style.display = '';
|
||
} else {
|
||
row.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
// 渲染表格页面
|
||
function renderTablePage(config, data) {
|
||
// 根据 tab 选择列配置和当前 API
|
||
let columns;
|
||
let currentApi = config.api;
|
||
if (config.hasModelTabs && currentModelTab === 'trained') {
|
||
columns = config.trainedColumns || config.columns;
|
||
currentApi = 'model-manage/trained-models';
|
||
} else {
|
||
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 = (currentApi === 'model-manage' || currentApi === '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"
|
||
oninput="filterTable()">
|
||
<i class="fa fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i>
|
||
</div>
|
||
` : '';
|
||
|
||
// 是否支持多选
|
||
const supportsMultiSelect = currentApi === 'model-manage' || currentApi === 'model-manage/trained-models' || config.api === 'dataset-manage' || config.api === 'fine-tune';
|
||
|
||
// 创建按钮(根据 tab 类型决定是否显示)
|
||
let createButton = '';
|
||
if (config.hasModelTabs && currentModelTab === 'trained') {
|
||
// 训练模型 tab 不显示创建按钮
|
||
createButton = '';
|
||
} else if (config.hasModelTabs && currentModelTab === 'config') {
|
||
// 配置模型 tab 显示创建按钮
|
||
createButton = `
|
||
<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>
|
||
`;
|
||
} else if (config.api === 'model-manage') {
|
||
createButton = `
|
||
<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>
|
||
`;
|
||
} else if (config.api === 'dataset-manage') {
|
||
createButton = `
|
||
<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>
|
||
`;
|
||
} else if (config.api === 'fine-tune') {
|
||
createButton = `
|
||
<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>
|
||
`;
|
||
} else if (config.api === 'model-compare') {
|
||
createButton = `
|
||
<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>
|
||
`;
|
||
}
|
||
|
||
// 批量删除按钮(仅当有选中项时显示)
|
||
const batchDeleteButton = supportsMultiSelect && selectedItems.size > 0 ? `
|
||
<button onclick="batchDeleteItems('${currentApi}')" class="bg-red-500 text-white px-4 py-2 rounded text-sm hover:bg-red-600 transition-colors font-medium shadow-sm">
|
||
<i class="fa fa-trash mr-1"></i>批量删除 (${selectedItems.size})
|
||
</button>
|
||
` : '';
|
||
|
||
const hasData = data && data.length > 0;
|
||
|
||
// 多选列头
|
||
const selectAllHeader = supportsMultiSelect ? `
|
||
<th class="px-4 py-3 text-center font-medium w-10">
|
||
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
|
||
onchange="toggleSelectAll(this, '${currentApi}')">
|
||
</th>
|
||
` : '';
|
||
|
||
return `
|
||
<div class="bg-white rounded-lg shadow-sm mb-6">
|
||
<div class="flex items-center justify-between p-4 border-b border-gray-100">
|
||
<h2 class="text-lg font-medium">${config.title}</h2>
|
||
<div class="flex items-center space-x-3">
|
||
${searchBox}
|
||
${createButton}
|
||
</div>
|
||
</div>
|
||
${config.hasModelTabs ? `
|
||
<div class="px-4 border-b border-gray-100">
|
||
<div class="flex space-x-1" id="modelTabs">
|
||
<button onclick="switchModelTab('config')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${currentModelTab === 'config' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
|
||
<i class="fa fa-cog mr-1"></i>配置模型
|
||
</button>
|
||
<button onclick="switchModelTab('trained')" class="tab-btn px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${currentModelTab === 'trained' ? 'bg-primary text-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'}">
|
||
<i class="fa fa-rocket mr-1"></i>训练模型
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<style>
|
||
.tab-btn { position: relative; transition: all 0.2s; }
|
||
</style>
|
||
` : ''}
|
||
${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">
|
||
<span>已选择 <strong>${selectedItems.size}</strong> 项</span>
|
||
<button onclick="clearSelection()" class="ml-3 text-blue-500 hover:text-blue-700 underline">取消选择</button>
|
||
</div>
|
||
<div class="flex items-center space-x-2">
|
||
${batchDeleteButton}
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
<div class="overflow-x-auto">
|
||
<table class="w-full">
|
||
<thead>
|
||
<tr class="table-header-bg text-gray-500 text-sm">
|
||
${selectAllHeader}
|
||
${columns.map(col => `<th class="px-4 py-3 text-center font-medium">${col.title}</th>`).join('')}
|
||
<th class="px-4 py-3 text-center font-medium">操作</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${hasData ? data.map(item => `
|
||
<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.name || item.id) ? 'checked' : ''}
|
||
onchange="toggleItemSelection('${item.name || item.id}', '${currentApi}')">
|
||
</td>
|
||
` : ''}
|
||
${columns.map(col => `
|
||
<td class="px-4 py-4 text-sm text-center">
|
||
${col.render ? col.render(item[col.key], item) : (item[col.key] || '-')}
|
||
</td>
|
||
`).join('')}
|
||
<td class="px-4 py-4 text-sm text-center">
|
||
<div class="flex justify-center space-x-2">
|
||
${currentApi === '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('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除任务</button>
|
||
` : (currentApi === 'model-manage/trained-models' ? `
|
||
${getMergeButtonHtml(item.name, item.train_methods?.[0]?.name || 'lora', item.base_model_path || '', item.merged, item.merging)}
|
||
<button onclick="deleteTrainedWeight('${item.name}')" class="bg-orange-500 text-white px-3 py-1 rounded text-xs hover:bg-orange-600">删除权重</button>
|
||
${(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('${currentApi}', '${item.name || item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除模型</button>
|
||
` : ''}
|
||
` : (currentApi === '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('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||
` : (currentApi === '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('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||
` : (currentApi === 'model-compare' ? `
|
||
<button onclick="startCompare('${item.id}')" class="bg-blue-500 text-white px-3 py-1 rounded text-xs hover:bg-blue-600">开始对比</button>
|
||
<button onclick="deleteItem('${currentApi}', '${item.id}')" class="bg-red-500 text-white px-3 py-1 rounded text-xs hover:bg-red-600">删除</button>
|
||
` : ''))))}
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
`).join('') : ''}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
${!hasData ? `
|
||
<div class="p-8 text-center text-gray-400">
|
||
<div class="py-12">
|
||
<i class="fa fa-inbox text-5xl mb-4 text-gray-300"></i>
|
||
<p class="text-gray-500">暂无数据</p>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}
|
||
|
||
// 切换模型管理tab
|
||
function switchModelTab(tab) {
|
||
currentModelTab = tab;
|
||
// 清除选中状态
|
||
clearSelection();
|
||
// 重新加载模型管理页面
|
||
loadPage('model-manage');
|
||
}
|
||
|
||
// 渲染工具卡片页面
|
||
|
||
|
||
// 当前页面状态(其他变量在开头已声明)
|
||
let currentParentPage = null;
|
||
let modelListCache = []; // 模型列表缓存
|
||
|
||
// 加载模型列表缓存
|
||
async function loadModelListCache() {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/model-manage`);
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
modelListCache = result.data || [];
|
||
}
|
||
} catch (e) {
|
||
console.error('加载模型列表失败:', e);
|
||
modelListCache = [];
|
||
}
|
||
}
|
||
|
||
// 根据模型ID获取模型名称(同步版本,用于表格渲染)
|
||
function getModelName(modelId) {
|
||
if (!modelId) return '-';
|
||
|
||
// 尝试多种方式匹配(处理类型不一致的情况)
|
||
const model = modelListCache.find(m =>
|
||
m.id == modelId ||
|
||
m.id === String(modelId) ||
|
||
m.id === Number(modelId)
|
||
);
|
||
|
||
if (model) {
|
||
return model.name;
|
||
}
|
||
|
||
// 如果缓存中没有找到,尝试直接通过 API 获取单个模型
|
||
// 这是一个备用方案,不会阻塞渲染
|
||
return `模型${modelId}`;
|
||
}
|
||
|
||
// 异步获取模型名称并更新 DOM(用于表格渲染后的更新)
|
||
async function fetchAndUpdateModelName(modelId, cellElement) {
|
||
if (!modelId) {
|
||
cellElement.textContent = '-';
|
||
return;
|
||
}
|
||
|
||
// 先尝试从缓存中找
|
||
let model = modelListCache.find(m =>
|
||
m.id == modelId ||
|
||
m.id === String(modelId) ||
|
||
m.id === Number(modelId)
|
||
);
|
||
|
||
// 如果缓存中没有,尝试直接获取
|
||
if (!model) {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/model-manage`);
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
modelListCache = result.data || [];
|
||
model = modelListCache.find(m =>
|
||
m.id == modelId ||
|
||
m.id === String(modelId) ||
|
||
m.id === Number(modelId)
|
||
);
|
||
}
|
||
} catch (e) {
|
||
console.error('获取模型列表失败:', e);
|
||
}
|
||
}
|
||
|
||
if (model) {
|
||
cellElement.textContent = model.name;
|
||
} else {
|
||
cellElement.textContent = `模型${modelId}`;
|
||
}
|
||
}
|
||
|
||
// 根据模型ID列表获取模型名称列表
|
||
function getModelNames(modelIds) {
|
||
if (!modelIds || !Array.isArray(modelIds)) return '-';
|
||
return modelIds.map(id => getModelName(id)).join(', ');
|
||
}
|
||
|
||
// 页面初始化时加载模型列表缓存
|
||
|
||
// 显示创建表单页面
|
||
function showCreateModal(apiType) {
|
||
if (apiType === 'fine-tune') {
|
||
window.location.href = 'fine-tune-create.html';
|
||
} else if (apiType === 'model-manage') {
|
||
window.location.href = 'model-manage-create.html';
|
||
} else if (apiType === 'model-eval') {
|
||
window.location.href = 'model-eval-create.html';
|
||
} else if (apiType === 'dataset-manage') {
|
||
window.location.href = 'dataset-create.html';
|
||
} else if (apiType === 'model-compare') {
|
||
window.location.href = 'model-compare-create.html';
|
||
} else {
|
||
showMessage('提示', '该功能开发中...', 'info');
|
||
}
|
||
}
|
||
|
||
// 返回列表页 - 全局
|
||
window.goBack = function() {
|
||
if (currentParentPage) {
|
||
currentPage = currentParentPage;
|
||
currentParentPage = null;
|
||
loadPage(currentPage);
|
||
} else {
|
||
loadPage('fine-tune');
|
||
}
|
||
}
|
||
|
||
// ============ 自定义消息弹窗 ============
|
||
// 显示消息弹窗 - 使用 window 确保全局可访问
|
||
window.showMessage = function(title, message, type = 'info', onConfirm) {
|
||
const modal = document.getElementById('customModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const modalMessage = document.getElementById('modalMessage');
|
||
const modalIcon = document.getElementById('modalIcon');
|
||
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
||
const modalConfirmBtn2 = document.getElementById('modalConfirmBtn2');
|
||
const modalBtnGroup = document.getElementById('modalBtnGroup');
|
||
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
|
||
|
||
// 设置标题
|
||
modalTitle.textContent = title;
|
||
modalTitle.className = 'text-lg font-medium text-gray-800 mb-2';
|
||
|
||
// 设置消息
|
||
modalMessage.innerHTML = message;
|
||
|
||
// 设置图标和按钮颜色
|
||
if (type === 'success') {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-green-100 flex items-center justify-center"><i class="fa fa-check text-xl text-green-600"></i></div>';
|
||
} else if (type === 'error') {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-red-100 flex items-center justify-center"><i class="fa fa-times text-xl text-red-600"></i></div>';
|
||
} else if (type === 'warning') {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
|
||
} else {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-info text-xl text-blue-600"></i></div>';
|
||
}
|
||
|
||
// 单按钮模式
|
||
modalBtnGroup.classList.add('hidden');
|
||
modalSingleBtnGroup.classList.remove('hidden');
|
||
const confirmBtn = modalConfirmBtn2;
|
||
if (type === 'error') {
|
||
confirmBtn.className = 'px-6 py-2 bg-red-500 text-white rounded-lg hover:bg-red-600 transition-colors';
|
||
} else {
|
||
confirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
|
||
}
|
||
|
||
// 显示弹窗
|
||
modal.classList.remove('hidden');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// 保存回调到按钮属性
|
||
confirmBtn._onConfirm = onConfirm;
|
||
|
||
// 使用 function 而不是箭头函数
|
||
confirmBtn.onclick = function() {
|
||
closeModal();
|
||
const callback = this._onConfirm;
|
||
if (typeof callback === 'function') {
|
||
callback();
|
||
}
|
||
this._onConfirm = null;
|
||
};
|
||
}
|
||
|
||
// 关闭消息弹窗
|
||
function closeModal() {
|
||
const modal = document.getElementById('customModal');
|
||
modal.classList.add('hidden');
|
||
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 storageKey = 'merge_status_' + name;
|
||
const tempStatus = sessionStorage.getItem(storageKey);
|
||
const tempStatusTime = sessionStorage.getItem(storageKey + '_time');
|
||
console.log('[DEBUG] getMergeButtonHtml:', name, 'tempStatus:', tempStatus, 'merged:', merged, 'merging:', merging);
|
||
|
||
// 检查临时状态是否过期(超过5分钟视为过期)
|
||
const now = Date.now();
|
||
const statusExpired = tempStatusTime && (now - parseInt(tempStatusTime)) > 5 * 60 * 1000;
|
||
|
||
// 如果状态过期或无效,清除并视为无状态
|
||
if (statusExpired || (tempStatus && !tempStatusTime)) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
// 继续检查后端状态
|
||
} else if (tempStatus === 'merging') {
|
||
// 如果后端已经完成合并但前端状态未更新,清除临时状态
|
||
if (merged) {
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
} else {
|
||
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(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
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(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
}
|
||
// 如果后端返回已合并,显示成功
|
||
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)
|
||
const storageKey = 'merge_status_' + name;
|
||
sessionStorage.setItem(storageKey, 'merging');
|
||
sessionStorage.setItem(storageKey + '_time', Date.now().toString());
|
||
// 刷新表格显示合并中状态
|
||
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(storageKey, 'success');
|
||
// 延迟刷新表格,用后端真实状态替换前端状态
|
||
setTimeout(() => loadTableData(), 1500);
|
||
} else {
|
||
// 清除合并状态
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
showMessage('失败', result.message || '合并失败', 'error');
|
||
loadTableData();
|
||
}
|
||
} catch (error) {
|
||
console.error('[DEBUG] 合并失败:', error);
|
||
// 清除合并状态
|
||
sessionStorage.removeItem(storageKey);
|
||
sessionStorage.removeItem(storageKey + '_time');
|
||
showMessage('错误', '合并失败: ' + error.message, 'error');
|
||
loadTableData();
|
||
}
|
||
}
|
||
|
||
// 合并模型权重(保留兼容)
|
||
window.viewTrainedModel = function(name, method, path) {
|
||
startMerge(name, method, path);
|
||
};
|
||
|
||
// 导出模型权重(打包下载)
|
||
function exportModel(modelName) {
|
||
// 直接跳转到导出接口下载文件
|
||
window.open(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}/export`, '_blank');
|
||
}
|
||
|
||
// 删除已训练模型的权重
|
||
window.deleteTrainedWeight = function(modelName) {
|
||
showConfirm('确认删除', `确定要删除模型 "${modelName}" 的权重文件吗?合并模型不受影响。`, async () => {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/model-manage/trained-models/${encodeURIComponent(modelName)}?type=lora`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
showMessage('成功', '权重已删除', 'success');
|
||
// 清除该模型的合并状态缓存,让前端重新从后端获取状态
|
||
sessionStorage.removeItem('merge_status_' + modelName);
|
||
sessionStorage.removeItem('merge_status_' + modelName + '_time');
|
||
} else {
|
||
showMessage('错误', result.message || '删除失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除权重失败:', error);
|
||
showMessage('错误', '删除失败: ' + error.message, 'error');
|
||
}
|
||
});
|
||
};
|
||
|
||
// 编辑模型 - 全局
|
||
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 确保全局可访问
|
||
window.showConfirm = function(title, message, onConfirm, onCancel, type = 'info') {
|
||
const modal = document.getElementById('customModal');
|
||
const modalTitle = document.getElementById('modalTitle');
|
||
const modalMessage = document.getElementById('modalMessage');
|
||
const modalIcon = document.getElementById('modalIcon');
|
||
const modalConfirmBtn = document.getElementById('modalConfirmBtn');
|
||
const modalCancelBtn = document.getElementById('modalCancelBtn');
|
||
const modalBtnGroup = document.getElementById('modalBtnGroup');
|
||
const modalSingleBtnGroup = document.getElementById('modalSingleBtnGroup');
|
||
|
||
if (!modalConfirmBtn) {
|
||
console.error('modalConfirmBtn not found');
|
||
return;
|
||
}
|
||
|
||
modalTitle.textContent = title;
|
||
modalMessage.innerHTML = message;
|
||
|
||
// 根据类型设置图标
|
||
if (type === 'warning') {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-yellow-100 flex items-center justify-center"><i class="fa fa-exclamation text-xl text-yellow-600"></i></div>';
|
||
} else {
|
||
modalIcon.innerHTML = '<div class="w-12 h-12 mx-auto mb-4 rounded-full bg-blue-100 flex items-center justify-center"><i class="fa fa-question text-xl text-blue-600"></i></div>';
|
||
}
|
||
|
||
// 显示双按钮组,隐藏单按钮组
|
||
modalBtnGroup.classList.remove('hidden');
|
||
modalSingleBtnGroup.classList.add('hidden');
|
||
modalConfirmBtn.textContent = '确定';
|
||
modalConfirmBtn.className = 'px-6 py-2 bg-primary text-white rounded-lg hover:bg-primary/90 transition-colors';
|
||
|
||
modal.classList.remove('hidden');
|
||
document.body.style.overflow = 'hidden';
|
||
|
||
// 保存回调到按钮属性
|
||
modalConfirmBtn._onConfirm = onConfirm;
|
||
modalConfirmBtn._onCancel = onCancel;
|
||
|
||
// 使用 function 而不是箭头函数,确保 this 指向正确
|
||
modalConfirmBtn.onclick = function() {
|
||
closeModal();
|
||
const callback = this._onConfirm;
|
||
if (typeof callback === 'function') {
|
||
callback();
|
||
}
|
||
this._onConfirm = null;
|
||
this._onCancel = null;
|
||
};
|
||
|
||
modalCancelBtn.onclick = function() {
|
||
closeModal();
|
||
const callback = this._onCancel || modalConfirmBtn._onCancel;
|
||
if (typeof callback === 'function') {
|
||
callback();
|
||
}
|
||
modalConfirmBtn._onConfirm = null;
|
||
modalConfirmBtn._onCancel = null;
|
||
};
|
||
}
|
||
|
||
// 提交创建表单
|
||
async function submitCreateForm() {
|
||
const form = document.getElementById('createForm');
|
||
const formData = new FormData(form);
|
||
const data = {
|
||
name: formData.get('name'),
|
||
base_model: formData.get('base_model'),
|
||
train_type: formData.get('train_type'),
|
||
train_method: formData.get('train_method'),
|
||
dataset_id: formData.get('dataset_id'),
|
||
valid_split: formData.get('valid_split'),
|
||
valid_ratio: parseInt(formData.get('valid_ratio')) || 10,
|
||
output_model_name: formData.get('output_model_name'),
|
||
status: 'pending',
|
||
progress: 0
|
||
};
|
||
|
||
if (!data.name) {
|
||
showMessage('提示', '请输入任务名称', 'warning');
|
||
return;
|
||
}
|
||
if (!data.base_model) {
|
||
showMessage('提示', '请选择基础模型', 'warning');
|
||
return;
|
||
}
|
||
if (!data.dataset_id) {
|
||
showMessage('提示', '请选择训练集', 'warning');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch(`${API_BASE}/fine-tune`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify(data)
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
showMessage('成功', '训练任务创建成功!', 'success', () => {
|
||
goBack();
|
||
});
|
||
} else {
|
||
showMessage('错误', result.message || '创建失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
showMessage('错误', '创建失败: ' + error.message, 'error');
|
||
}
|
||
}
|
||
|
||
// 跳转到页面
|
||
function navigateToPage(pageName) {
|
||
// 如果页面名以 -create 结尾,直接跳转到 HTML 页面
|
||
if (pageName.endsWith('-create')) {
|
||
window.location.href = `${pageName}.html`;
|
||
} else {
|
||
window.location.href = `main.html?page=${pageName}`;
|
||
}
|
||
}
|
||
|
||
// 返回到列表页(外部页面用)
|
||
function goBackToList() {
|
||
navigateToPage('fine-tune');
|
||
}
|
||
|
||
// 添加评测维度
|
||
function addDimension() {
|
||
window.location.href = 'model-dimension-create.html';
|
||
}
|
||
|
||
// 删除评测维度
|
||
async function deleteDimension(id) {
|
||
showConfirm('确认删除', '确定要删除此评测维度吗?', async () => {
|
||
try {
|
||
const response = await fetch(`${API_BASE}/dimension/${id}`, {
|
||
method: 'DELETE'
|
||
});
|
||
const result = await response.json();
|
||
if (result.code === 0) {
|
||
showMessage('成功', '删除成功', 'success', () => {
|
||
// 刷新维度列表 - 切换到 dimensions tab
|
||
switchTab(document.querySelector('[data-tab="dimensions"]'), 'dimensions');
|
||
});
|
||
} else {
|
||
showMessage('错误', result.message || '删除失败', 'error');
|
||
}
|
||
} catch (error) {
|
||
console.error('删除维度失败:', error);
|
||
showMessage('错误', '删除失败: ' + error.message, 'error');
|
||
}
|
||
});
|
||
}
|
||
|
||
// 切换 Tab
|
||
function switchTab(btn, tabId) {
|
||
// 更新按钮状态
|
||
const parent = btn.parentElement;
|
||
parent.querySelectorAll('.tab-btn').forEach(b => {
|
||
b.classList.remove('active', 'text-primary');
|
||
b.classList.add('text-gray-500');
|
||
});
|
||
btn.classList.add('active');
|
||
btn.classList.remove('text-gray-500');
|
||
|
||
// 更新内容显示 - 需要通知外部页面
|
||
if (typeof window.switchTabContent === 'function') {
|
||
window.switchTabContent(tabId);
|
||
}
|
||
|
||
// 更新右侧按钮 - 从 tableConfigs 获取当前页面配置
|
||
const btnContainer = document.getElementById('headerActionButtons');
|
||
const currentConfig = tableConfigs[currentPage];
|
||
if (btnContainer) {
|
||
if (tabId === 'tasks') {
|
||
const page = currentConfig?.createConfig?.page || 'model-eval-create';
|
||
const text = currentConfig?.createConfig?.createText || '新建评测';
|
||
btnContainer.innerHTML = `
|
||
<button onclick="navigateToPage('${page}')" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||
<i class="fa fa-plus mr-2"></i>${text}
|
||
</button>
|
||
`;
|
||
} else if (tabId === 'leaderboard') {
|
||
// 排行榜隐藏按钮,但保留占位元素避免高度变化
|
||
btnContainer.innerHTML = '<span class="invisible px-4 py-2 rounded-lg">占位</span>';
|
||
} else if (tabId === 'dimensions') {
|
||
btnContainer.innerHTML = `
|
||
<button onclick="addDimension()" class="bg-primary text-white px-4 py-2 rounded-lg text-sm hover:bg-primary/90 transition-colors flex items-center">
|
||
<i class="fa fa-plus mr-2"></i>添加维度
|
||
</button>
|
||
`;
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<!-- 自定义消息弹窗 -->
|
||
<div id="customModal" class="hidden fixed inset-0 bg-black/50 z-50 flex items-center justify-center" onclick="if(event.target === this) closeModal();">
|
||
<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">
|
||
<div id="modalIcon"></div>
|
||
<h3 id="modalTitle" class="text-base font-medium text-gray-800 mb-2"></h3>
|
||
<p id="modalMessage" class="text-gray-600 text-sm"></p>
|
||
</div>
|
||
<div id="modalBtnGroup" class="hidden px-6 pb-6 flex flex-col space-y-2 mx-4">
|
||
<button id="modalConfirmBtn" class="px-4 py-2 w-full text-white rounded transition-colors text-sm">
|
||
确定
|
||
</button>
|
||
<button id="modalCancelBtn" class="px-4 py-2 w-full border border-gray-300 text-gray-700 rounded hover:bg-gray-50 transition-colors text-sm">
|
||
取消
|
||
</button>
|
||
</div>
|
||
<div id="modalSingleBtnGroup" class="px-6 pb-6 flex justify-center">
|
||
<button id="modalConfirmBtn2" class="px-6 py-2 w-full text-white rounded transition-colors text-sm max-w-[160px]">
|
||
确定
|
||
</button>
|
||
</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>
|