Files
YG_FT_Platform/web/pages/main.html

1872 lines
92 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>