Files
YG_FT_Platform/web/pages/main.html

3451 lines
189 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>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<style>
.sidebar-section-title {
padding: 0.5rem 1rem;
font-size: 0.75rem;
color: rgba(191, 203, 217, 0.7);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.nav-link:hover {
background-color: rgba(0, 21, 41, 0.2);
}
.sidebar-item-active {
background-color: rgba(24, 144, 255, 0.1);
color: #1890ff;
border-left: 4px solid #1890ff;
}
.table-row-hover:hover {
background-color: #f9fafb;
transition: background-color 0.2s;
}
.table-header-bg {
background-color: #fafafa !important;
}
.card-radio {
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.card-radio.active {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.card-radio:hover {
border-color: #d1d5db;
}
.form-input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
}
.form-input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.form-label {
display: block;
font-size: 0.875rem;
font-weight: 500;
color: #374151;
margin-bottom: 0.25rem;
}
.form-select {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s, outline 0.2s;
appearance: none;
background-color: white;
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e");
background-position: right 0.5rem center;
background-repeat: no-repeat;
background-size: 1.5em 1.5em;
padding-right: 2.5rem;
}
.form-select:focus {
border-color: #1890ff;
outline: none;
}
.icon-option {
width: 2.5rem;
height: 2.5rem;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.icon-option:hover {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.icon-option.selected {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.1);
}
.tab-active {
background-color: rgba(24, 144, 255, 0.1);
color: #1890ff;
font-weight: 500;
}
.radio-dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 50%;
background-color: transparent;
transition: all 0.2s;
}
.upload-area:hover,
.upload-area.drag-over {
border-color: #1890ff;
background-color: rgba(24, 144, 255, 0.05);
}
.bg-primary {
background-color: #1890ff;
}
.text-primary {
color: #1890ff;
}
.border-primary {
border-color: #1890ff;
}
.text-danger {
color: #f5222d;
}
.hover\:bg-primary\/90:hover {
background-color: rgba(24, 144, 255, 0.9);
}
:root {
--primary: #1890ff;
--danger: #f5222d;
--success: #52c41a;
}
/* 侧边栏滑块动画 */
.sidebar-slider {
position: absolute;
width: 4px;
height: 0;
background-color: var(--primary);
border-radius: 0 2px 2px 0;
transition: top 0.3s cubic-bezier(0.4, 0, 0.2, 1),
height 0.3s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
z-index: 10;
}
/* 菜单项相对定位 */
.nav-item-wrapper {
position: relative;
}
/* 选中项背景动画 */
.nav-link {
position: relative;
z-index: 1;
}
</style>
</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="main.html?page=my-models" data-page="my-models" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database 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="#" data-page="data-generate" class="nav-link flex items-center px-4 py-2.5 hover:bg-[#001529]/20 transition-colors">
<i class="fa fa-database 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="config" 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="#" data-page="logs" 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 基础地址 - 使用 config.yaml 中的 app.port (7861)
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;
// 日志自动刷新相关变量
let logRefreshTimer = null;
let logCountdownTimer = null;
let logCurrentInterval = 10;
let logFullContent = ''; // 存储完整日志内容
// 设置自动刷新间隔
function setRefreshInterval() {
const select = document.getElementById('logRefreshInterval');
const countdownEl = document.getElementById('logRefreshCountdown');
const secondsEl = document.getElementById('countdownNumber');
if (!select) return;
logCurrentInterval = parseInt(select.value) || 10;
// 清除之前的定时器
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
// 如果选择关闭,不显示倒计时
if (select.value === '0') {
countdownEl.classList.add('hidden');
return;
}
// 显示倒计时
countdownEl.classList.remove('hidden');
secondsEl.textContent = logCurrentInterval;
// 启动倒计时
let countdown = logCurrentInterval;
logCountdownTimer = setInterval(() => {
countdown--;
if (countdown <= 0) {
countdown = logCurrentInterval;
}
secondsEl.textContent = countdown;
}, 1000);
// 启动自动刷新
logRefreshTimer = setInterval(() => {
refreshLogs();
}, logCurrentInterval * 1000);
}
// 获取系统性能监控数据
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);
}
}
// 页面加载时获取监控数据并每30秒刷新
fetchSystemMetrics();
setInterval(fetchSystemMetrics, 30000);
// 各功能模块的表格配置
const tableConfigs = {
'fine-tune': {
title: '模型调优',
api: 'fine-tune',
hasCreate: true,
createText: '创建训练任务',
columns: [
{ title: '任务名称', key: 'name' },
{ title: '基础模型', key: 'base_model', render: (val, row) => `<span class="model-name-cell" data-model-id="${val}">加载中...</span>` },
{ title: '状态', key: 'status', render: (val) => `<span class="px-2 py-1 rounded text-xs ${val === 'running' ? 'bg-green-100 text-green-700' : val === 'failed' ? 'bg-red-100 text-red-700' : 'bg-gray-100 text-gray-700'}">${val}</span>` },
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
],
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: ['compare', '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: '其他工具',
isTools: true,
defaultTools: [
{ id: 'data-generate', name: '数据生成', icon: 'fa-database', description: '基于LLM生成微调数据集' },
{ id: 'json2jsonl', name: 'JSON转JSONL', icon: 'fa-code', description: '将JSON文件转换为JSONL格式' },
{ id: 'md-convert', name: '转换Markdown', icon: 'fa-file-text', description: '将Markdown文件转换为训练数据' }
],
customTools: []
},
'model-manage': {
title: '模型管理',
api: 'model-manage',
hasCreate: 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']
},
'config': {
title: '平台性能',
skipFetch: true,
hasCreate: false,
isHardwareMonitor: true
},
'logs': {
title: '查看日志',
skipFetch: true,
hasCreate: false,
isLogViewer: 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) {
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();
// 离开日志页面时停止自动刷新
stopLogAutoRefresh();
// 离开模型调优页面时停止进度刷新
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.isHardwareMonitor) {
// 硬件监控页面使用模拟数据不调用API
container.innerHTML = renderConfigPage(config, null);
} else if (config.isLogViewer) {
// 日志查看页面
container.innerHTML = renderLogViewerPage(config);
initLogViewer();
} else if (config.isForm) {
const data = await fetchData(`${API_BASE}/${config.api}`);
container.innerHTML = renderConfigPage(config, data);
} else if (config.isTools) {
container.innerHTML = renderToolsPage(config);
} else {
let data = await fetchData(`${API_BASE}/${config.api}`);
// 如果配置了 dataPath从返回数据中提取指定字段
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);
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">${error.message}</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) {
showConfirm('确认删除', '确定要删除这条记录吗?', async () => {
try {
const response = await fetch(`${API_BASE}/${api}/${id}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.code === 0) {
// 刷新当前页面
clearSelection(); // 清除选中状态
const activeLink = document.querySelector('.nav-link.sidebar-item-active');
if (activeLink) {
loadPage(activeLink.dataset.page);
}
} 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');
}
// 更新模型用途
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) {
// 全选当前页面的所有数据
currentPageData.forEach(item => selectedItems.add(item.id));
} else {
// 取消全选,移除当前页面所有数据的选中状态
currentPageData.forEach(item => selectedItems.delete(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];
if (config.api === 'model-manage' || config.api === 'dataset-manage') {
document.getElementById('page-content').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) {
const protocol = window.location.protocol;
const hostname = window.location.hostname;
window.open(`${protocol}//${hostname}:7861/api/dataset-manage/download/${datasetId}`, '_blank');
}
// 开始模型对比
async function startCompare(id) {
// 跳转到模型对比聊天页面(通过主框架加载)
window.location.href = `main.html?page=model-compare-chat&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) {
const createButton = config.hasCreate ? `
<button onclick="showCreateModal('${config.api}')" class="bg-primary text-white px-3 py-1.5 rounded text-sm hover:bg-primary/90 transition-colors">
<i class="fa fa-plus mr-1"></i>${config.createText}
</button>
` : '';
// 搜索框(模型管理和数据集管理)
const searchBox = (config.api === 'model-manage' || config.api === 'dataset-manage') ? `
<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 = config.api === 'model-manage' || config.api === 'dataset-manage';
// 批量删除按钮(仅当有选中项时显示)
const batchDeleteButton = supportsMultiSelect && selectedItems.size > 0 ? `
<button onclick="batchDeleteItems('${config.api}')" 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 columns = config.columns;
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, '${config.api}')">
</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>
${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.id) ? 'bg-blue-50' : ''}">
${supportsMultiSelect ? `
<td class="px-4 py-4 text-sm text-center">
<input type="checkbox" class="w-4 h-4 text-primary rounded border-gray-300 cursor-pointer"
${selectedItems.has(item.id) ? 'checked' : ''}
onchange="toggleItemSelection(${item.id}, '${config.api}')">
</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">
${config.actions.map(action => {
let onclick = '';
let btnClass = 'text-primary hover:text-primary/80';
// 对于 fine-tune 的停止按钮,检查状态
if (action === 'stop' && config.api === 'fine-tune') {
// 状态为 completed 或 failed 时隐藏停止按钮
if (item.status === 'completed' || item.status === 'failed') {
return '';
}
onclick = `stopItem(${item.id})`;
btnClass = 'text-orange-500 hover:text-orange-600';
} else if (action === 'delete') {
onclick = `deleteItem('${config.api}', ${item.id})`;
btnClass = 'text-danger hover:text-danger/80';
} else if (action === 'edit') {
onclick = `editItem('${config.api}', ${item.id})`;
} else if (action === 'preview' && config.api === 'dataset-manage') {
onclick = `window.location.href = 'dataset-preview.html?id=${item.id}'`;
} else if (action === 'download' && config.api === 'dataset-manage') {
onclick = `downloadDataset('${item.id}')`;
} else if (action === 'compare' && config.api === 'model-compare') {
onclick = `startCompare(${item.id})`;
} else if (action === 'logs' && config.api === 'fine-tune') {
onclick = `navigateToTrainingLog(${item.id})`;
} else if (action === 'view' && config.api === 'model-manage/trained-models') {
onclick = `viewTrainedModel('${item.name}', '${item.train_methods?.[0]?.name || '-'}', '${item.path || ''}')`;
} else {
onclick = `showMessage('提示', '${actionLabels[action] || action}功能开发中...', 'info')`;
}
return `<button onclick="${onclick}" class="${btnClass}">${actionLabels[action] || action}</button>`;
}).join('')}
</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>
`;
}
// 渲染日志查看页面
function renderLogViewerPage(config) {
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-2">
<button onclick="refreshLogs()" class="px-3 py-1.5 text-sm bg-primary text-white rounded hover:bg-primary/90 transition-colors">
<i class="fa fa-refresh mr-1"></i>刷新
</button>
</div>
</div>
<div class="p-4">
<!-- 日志类型切换 -->
<div class="flex items-center mb-4">
<div class="flex bg-gray-100 rounded-lg p-1">
<button id="logTabSystem" onclick="switchLogTab('system')" class="px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary">
系统日志
</button>
<button id="logTabTraining" onclick="switchLogTab('training')" class="px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800">
训练日志
</button>
</div>
</div>
<!-- 系统日志选项 -->
<div id="systemLogOptions">
<!-- 日期选择 -->
<div class="flex items-center flex-wrap gap-4 mb-4">
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">选择日期:</label>
<input type="date" id="logDatePicker" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none" onchange="loadLogFiles()">
</div>
<div class="flex items-center">
<label class="text-sm text-gray-600 mr-3">自动刷新:</label>
<select id="logRefreshInterval" onchange="setRefreshInterval()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="0">关闭</option>
<option value="5">5秒</option>
<option value="10" selected>10秒</option>
<option value="30">30秒</option>
<option value="60">60秒</option>
</select>
</div>
<div id="logRefreshCountdown" class="text-sm text-gray-500 hidden">
<i class="fa fa-clock-o mr-1"></i><span>下次刷新: <span id="countdownNumber">10</span>秒</span>
</div>
</div>
<!-- 日志类型选择 -->
<div class="flex items-center mb-4">
<label class="text-sm text-gray-600 mr-3">日志类型:</label>
<select id="logTypeSelect" onchange="loadSelectedLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="">请选择日志文件</option>
</select>
</div>
</div>
<!-- 训练日志选项(初始隐藏) -->
<div id="trainingLogOptions" class="hidden">
<div class="flex items-center mb-4">
<label class="text-sm text-gray-600 mr-3">训练日志:</label>
<select id="trainingLogSelect" onchange="loadSelectedTrainingLog()" class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none flex-1">
<option value="">请选择训练日志</option>
</select>
</div>
</div>
<!-- 日志内容显示 -->
<div class="border border-gray-200 rounded-lg">
<div class="flex items-center justify-between px-4 py-2 bg-gray-50 border-b border-gray-200">
<span class="text-sm text-gray-600" id="logFileInfo">请选择日志文件</span>
<div class="flex items-center space-x-3">
<input type="text" id="logSearchInput" placeholder="搜索日志..." oninput="filterLogContent()" class="px-2 py-1 text-xs border border-gray-300 rounded focus:border-primary focus:outline-none">
<span id="logMatchCount" class="text-xs text-gray-500"></span>
<button onclick="clearLogContent()" class="text-xs text-gray-500 hover:text-primary">清空</button>
</div>
</div>
<pre id="logContent" class="p-4 text-xs font-mono bg-gray-900 text-gray-100 overflow-auto max-h-[600px]" style="white-space: pre-wrap; word-wrap: break-word;">日志内容将在这里显示...</pre>
</div>
</div>
</div>
`;
}
// 当前日志类型system 或 training
let currentLogTab = 'system';
// 切换日志类型标签
function switchLogTab(tab) {
currentLogTab = tab;
const systemTab = document.getElementById('logTabSystem');
const trainingTab = document.getElementById('logTabTraining');
const systemOptions = document.getElementById('systemLogOptions');
const trainingOptions = document.getElementById('trainingLogOptions');
if (tab === 'system') {
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
systemOptions.classList.remove('hidden');
trainingOptions.classList.add('hidden');
loadLogFiles();
} else {
trainingTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors bg-white shadow-sm text-primary';
systemTab.className = 'px-4 py-1.5 text-sm rounded-md transition-colors text-gray-600 hover:text-gray-800';
trainingOptions.classList.remove('hidden');
systemOptions.classList.add('hidden');
loadTrainingLogFiles();
}
}
// 初始化日志查看器
function initLogViewer() {
const datePicker = document.getElementById('logDatePicker');
if (datePicker) {
const today = new Date().toISOString().split('T')[0];
datePicker.value = today;
}
// 加载默认日志类型
loadLogFiles();
// 启动自动刷新
setRefreshInterval();
}
// 加载训练日志文件列表
async function loadTrainingLogFiles() {
const logSelect = document.getElementById('trainingLogSelect');
if (!logSelect) return;
logSelect.innerHTML = '<option value="">加载中...</option>';
try {
const response = await fetch(`${API_BASE}/training-log-files`);
const result = await response.json();
if (result.code === 0 && result.data) {
logSelect.innerHTML = '<option value="">请选择训练日志</option>';
result.data.forEach(log => {
const option = document.createElement('option');
option.value = log.file;
option.textContent = `${log.name} (PID: ${log.pid}, ${log.date}, ${log.size})`;
logSelect.appendChild(option);
});
// 如果有日志文件,自动加载第一个
if (result.data.length > 0) {
logSelect.value = result.data[0].file;
loadSelectedTrainingLog();
} else {
document.getElementById('logContent').textContent = '暂无训练日志';
document.getElementById('logFileInfo').textContent = '无训练日志';
}
} else {
logSelect.innerHTML = '<option value="">暂无训练日志</option>';
document.getElementById('logContent').textContent = '暂无训练日志';
document.getElementById('logFileInfo').textContent = '无训练日志';
}
} catch (error) {
console.error('加载训练日志列表失败:', error);
logSelect.innerHTML = '<option value="">加载失败</option>';
document.getElementById('logContent').textContent = '加载训练日志列表失败: ' + error.message;
}
}
// 加载选中的训练日志
async function loadSelectedTrainingLog() {
const logSelect = document.getElementById('trainingLogSelect');
const logFile = logSelect.value;
const logContent = document.getElementById('logContent');
const logFileInfo = document.getElementById('logFileInfo');
if (!logFile) {
logContent.textContent = '请选择训练日志';
logFileInfo.textContent = '无训练日志';
return;
}
logContent.textContent = '加载中...';
logFileInfo.textContent = '加载中...';
try {
const response = await fetch(`${API_BASE}/training-log-content?file=${encodeURIComponent(logFile)}`);
const result = await response.json();
if (result.code === 0 && result.data) {
logFullContent = result.data.content || '';
logContent.textContent = logFullContent || '(空日志)';
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
// 清空搜索
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
// 滚动到底部
scrollToLogBottom();
} else {
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
logFileInfo.textContent = '加载失败';
}
} catch (error) {
console.error('加载训练日志内容失败:', error);
logContent.textContent = '加载失败: ' + error.message;
logFileInfo.textContent = '加载失败';
}
}
// 加载日志文件列表
async function loadLogFiles() {
const datePicker = document.getElementById('logDatePicker');
const logTypeSelect = document.getElementById('logTypeSelect');
const selectedDate = datePicker ? datePicker.value : new Date().toISOString().split('T')[0];
if (!logTypeSelect) return;
logTypeSelect.innerHTML = '<option value="">加载中...</option>';
try {
const response = await fetch(`${API_BASE}/log-files?date=${selectedDate}`);
const result = await response.json();
if (result.code === 0 && result.data) {
logTypeSelect.innerHTML = '<option value="">请选择日志文件</option>';
result.data.forEach(log => {
const option = document.createElement('option');
option.value = log.file;
option.textContent = log.name + ' (' + log.size + ')';
logTypeSelect.appendChild(option);
});
// 如果有日志文件,自动加载第一个
if (result.data.length > 0) {
logTypeSelect.value = result.data[0].file;
loadSelectedLog();
} else {
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
document.getElementById('logContent').textContent = '该日期暂无日志文件';
document.getElementById('logFileInfo').textContent = '无日志文件';
}
} else {
logTypeSelect.innerHTML = '<option value="">暂无日志文件</option>';
document.getElementById('logContent').textContent = '该日期暂无日志文件';
document.getElementById('logFileInfo').textContent = '无日志文件';
}
} catch (error) {
console.error('加载日志文件列表失败:', error);
logTypeSelect.innerHTML = '<option value="">加载失败</option>';
document.getElementById('logContent').textContent = '加载日志文件列表失败: ' + error.message;
}
}
// 加载选中的日志
async function loadSelectedLog() {
const logTypeSelect = document.getElementById('logTypeSelect');
const logFile = logTypeSelect.value;
const logContent = document.getElementById('logContent');
const logFileInfo = document.getElementById('logFileInfo');
if (!logFile) {
logContent.textContent = '请选择日志文件';
logFileInfo.textContent = '无日志文件';
return;
}
logContent.textContent = '加载中...';
logFileInfo.textContent = '加载中...';
try {
const response = await fetch(`${API_BASE}/log-content?file=${encodeURIComponent(logFile)}`);
const result = await response.json();
if (result.code === 0) {
logFullContent = result.data.content || '';
logContent.textContent = logFullContent || '(空日志)';
logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')';
// 清空搜索框和匹配计数
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
// 滚动到最底部
scrollToLogBottom();
} else {
logContent.textContent = '加载失败: ' + (result.message || '未知错误');
logFileInfo.textContent = '加载失败';
}
} catch (error) {
console.error('加载日志内容失败:', error);
logContent.textContent = '加载日志内容失败: ' + error.message;
logFileInfo.textContent = '加载失败';
}
}
// 刷新日志
function refreshLogs() {
if (currentLogTab === 'system') {
loadLogFiles();
if (document.getElementById('logTypeSelect').value) {
loadSelectedLog();
}
} else {
loadTrainingLogFiles();
}
// 重置倒计时
const select = document.getElementById('logRefreshInterval');
const secondsEl = document.getElementById('countdownNumber');
if (select && select.value !== '0' && secondsEl) {
secondsEl.textContent = logCurrentInterval;
}
}
// 停止日志自动刷新(离开页面时调用)
function stopLogAutoRefresh() {
if (logRefreshTimer) {
clearInterval(logRefreshTimer);
logRefreshTimer = null;
}
if (logCountdownTimer) {
clearInterval(logCountdownTimer);
logCountdownTimer = null;
}
}
// 滚动到日志底部
function scrollToLogBottom() {
const logContent = document.getElementById('logContent');
if (logContent) {
logContent.scrollTop = logContent.scrollHeight;
}
}
// 过滤日志内容
function filterLogContent() {
const searchInput = document.getElementById('logSearchInput');
const matchCount = document.getElementById('logMatchCount');
const logContent = document.getElementById('logContent');
if (!searchInput || !matchCount || !logContent) return;
const keyword = searchInput.value.trim();
if (!keyword) {
logContent.textContent = logFullContent || '(空日志)';
matchCount.textContent = '';
scrollToLogBottom();
return;
}
const lines = logFullContent.split('\n');
const matchingLines = lines.filter(line => line.toLowerCase().includes(keyword.toLowerCase()));
if (matchingLines.length > 0) {
logContent.textContent = matchingLines.join('\n');
matchCount.textContent = `(${matchingLines.length}条匹配)`;
// 滚动到最底部查看最新匹配
scrollToLogBottom();
} else {
logContent.textContent = '未找到匹配的日志';
matchCount.textContent = '(0条匹配)';
}
}
// 清空日志内容显示
function clearLogContent() {
document.getElementById('logContent').textContent = '日志内容将在这里显示...';
document.getElementById('logFileInfo').textContent = '请选择日志文件';
document.getElementById('logTypeSelect').value = '';
document.getElementById('logSearchInput').value = '';
document.getElementById('logMatchCount').textContent = '';
logFullContent = '';
}
// 渲染工具卡片页面
function renderToolsPage(config) {
// 渲染单个工具卡片
const renderToolCard = (tool, canDelete = false, isCustom = false) => `
<div class="relative group border border-gray-200 rounded-lg p-6 cursor-pointer hover:border-primary hover:shadow-md transition-all ${canDelete ? '' : ''}" onclick="navigateToTool('${tool.id}', '${tool.url || ''}', ${isCustom})">
<div class="w-12 h-12 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
<i class="fa ${tool.icon} text-xl text-primary"></i>
</div>
<h3 class="text-base font-medium text-gray-800 mb-2">${tool.name}</h3>
<p class="text-sm text-gray-500">${tool.description}</p>
${canDelete ? `
<button onclick="event.stopPropagation(); editCustomTool('${tool.id}')" class="absolute top-2 right-10 w-6 h-6 rounded-full bg-gray-100 hover:bg-blue-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="修改">
<i class="fa fa-pencil text-gray-400 hover:text-blue-500 text-xs"></i>
</button>
<button onclick="event.stopPropagation(); deleteCustomTool('${tool.id}')" class="absolute top-2 right-2 w-6 h-6 rounded-full bg-gray-100 hover:bg-red-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity z-10" title="删除">
<i class="fa fa-times text-gray-400 hover:text-red-500 text-xs"></i>
</button>
` : ''}
</div>
`;
const defaultCards = config.defaultTools.map(t => renderToolCard(t, false, false)).join('');
const customCards = config.customTools.map(t => renderToolCard(t, true, true)).join('');
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>
<button onclick="showCreateToolModal()" 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>
</div>
<div class="p-6">
<!-- 默认工具 -->
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">默认工具</h3>
<div class="grid grid-cols-3 gap-6 mb-8">
${defaultCards || '<p class="text-gray-400 text-sm col-span-3">暂无默认工具</p>'}
</div>
<!-- 自定义工具 -->
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">自定义工具</h3>
<div class="grid grid-cols-3 gap-6">
${customCards || '<p class="text-gray-400 text-sm col-span-3">暂无自定义工具,点击右上角添加</p>'}
</div>
</div>
</div>
`;
}
// 删除自定义工具
function deleteCustomTool(toolId) {
showConfirm('确认删除', '确定要删除这个自定义工具吗?', () => {
const config = tableConfigs['data-generate'];
config.customTools = config.customTools.filter(t => t.id !== toolId);
// 保存到 localStorage
localStorage.setItem('customTools', JSON.stringify(config.customTools));
document.getElementById('page-content').innerHTML = renderToolsPage(config);
// 删除成功,无需额外弹窗提示
});
}
// 修改自定义工具
function editCustomTool(toolId) {
const config = tableConfigs['data-generate'];
const tool = config.customTools.find(t => t.id === toolId);
if (tool) {
// 将工具信息存储到 localStorage编辑页面读取
localStorage.setItem('editTool', JSON.stringify(tool));
window.location.href = 'custom-tool-create.html?edit=true';
}
}
// 显示创建工具弹窗
function showCreateToolModal() {
window.location.href = 'custom-tool-create.html';
}
// 跳转到工具页面
function navigateToTool(toolId, url, isCustom = false) {
if (isCustom && url) {
// 自定义工具使用配置的URL
if (url.startsWith('http')) {
window.open(url, '_blank');
} else {
window.location.href = url;
}
} else if (toolId === 'data-generate') {
window.location.href = 'data-generate.html';
} else if (toolId === 'json2jsonl') {
showMessage('提示', 'JSON转JSONL 工具开发中...', 'info');
} else if (toolId === 'md-convert') {
showMessage('提示', '转换Markdown 工具开发中...', 'info');
} else {
showMessage('提示', `${toolId} 功能开发中...`, 'info');
}
}
// 渲染配置页面(硬件监控)
function renderConfigPage(config, data) {
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 text-sm text-gray-500">
<i class="fa fa-refresh mr-2"></i>
<span class="mr-2">刷新频率:</span>
<select id="refreshInterval" onchange="changeRefreshRate()" class="px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="1000">1秒</option>
<option value="3000">3秒</option>
<option value="5000" selected>5秒</option>
</select>
</div>
</div>
<div class="p-6">
<!-- 第一行CPU、内存、磁盘 -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
<!-- CPU监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center mr-3">
<i class="fa fa-microchip text-blue-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">CPU 使用率</h3>
<p class="text-xs text-gray-500" id="cpuCores">4 核心</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="cpuPercent">0%</span>
</div>
</div>
<!-- CPU进度条 -->
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="cpuBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 to-blue-500 transition-all duration-500" style="width: 0%"></div>
</div>
<!-- 核心使用率 -->
<div class="mt-4 grid grid-cols-4 gap-2" id="cpuCoresList">
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心1</div>
<div class="text-sm font-medium text-gray-800" id="core1">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心2</div>
<div class="text-sm font-medium text-gray-800" id="core2">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心3</div>
<div class="text-sm font-medium text-gray-800" id="core3">0%</div>
</div>
<div class="text-center p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">核心4</div>
<div class="text-sm font-medium text-gray-800" id="core4">0%</div>
</div>
</div>
</div>
<!-- 内存监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center mr-3">
<i class="fa fa-database text-purple-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">内存使用</h3>
<p class="text-xs text-gray-500" id="memoryTotal">总计: 16 GB</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="memoryPercent">0%</span>
</div>
</div>
<!-- 内存进度条 -->
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="memoryBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-yellow-400 to-orange-500 transition-all duration-500" style="width: 0%"></div>
</div>
<!-- 内存详情 -->
<div class="mt-4 flex justify-between text-sm">
<div class="text-center">
<div class="text-xs text-gray-500">已用</div>
<div class="text-sm font-medium text-gray-800" id="memoryUsed">0 GB</div>
</div>
<div class="text-center">
<div class="text-xs text-gray-500">可用</div>
<div class="text-sm font-medium text-gray-800" id="memoryAvailable">0 GB</div>
</div>
<div class="text-center">
<div class="text-xs text-gray-500">缓存</div>
<div class="text-sm font-medium text-gray-800" id="memoryCached">0 GB</div>
</div>
</div>
</div>
<!-- 磁盘监控 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center mr-3">
<i class="fa fa-hdd-o text-green-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">磁盘使用</h3>
<p class="text-xs text-gray-500" id="diskTotal">SSD 512 GB</p>
</div>
</div>
<div class="text-right">
<span class="text-2xl font-medium text-gray-800" id="diskPercent">0%</span>
</div>
</div>
<!-- 磁盘进度条 -->
<div class="relative h-4 bg-gray-100 rounded-full overflow-hidden">
<div id="diskBar" class="absolute left-0 top-0 h-full bg-gradient-to-r from-emerald-400 to-teal-500 transition-all duration-500" style="width: 0%"></div>
</div>
<!-- 磁盘详情 -->
<div class="mt-4 grid grid-cols-3 gap-2 text-center">
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">已用空间</div>
<div class="text-sm font-medium text-gray-800" id="diskUsed">0 GB</div>
</div>
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">可用空间</div>
<div class="text-sm font-medium text-gray-800" id="diskAvailable">0 GB</div>
</div>
<div class="p-2 bg-gray-50 rounded">
<div class="text-xs text-gray-500">读写速度</div>
<div class="text-sm font-medium text-gray-800" id="diskSpeed">0 MB/s</div>
</div>
</div>
</div>
</div>
<!-- 第二行GPU监控横向占满2列网格 -->
<div class="mb-6">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center mr-3">
<i class="fa fa-microchip text-red-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">GPU监控</h3>
<p class="text-xs text-gray-500" id="gpuCount">多GPU并行监控</p>
</div>
</div>
<!-- GPU列表2列网格滚动 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4" id="gpuList" style="max-height: 400px; overflow-y: auto;">
<!-- GPU卡片由initGPUList()动态生成 -->
</div>
</div>
<!-- 第三行网络和系统2列 -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 网络流量 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-cyan-100 flex items-center justify-center mr-3">
<i class="fa fa-globe text-cyan-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">网络流量</h3>
<p class="text-xs text-gray-500">实时带宽使用</p>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="p-3 bg-blue-50 rounded-lg">
<div class="flex items-center mb-2">
<i class="fa fa-arrow-down text-blue-600 mr-2"></i>
<span class="text-xs text-gray-600">下载速度</span>
</div>
<div class="text-lg font-medium text-gray-800" id="downloadSpeed">0 MB/s</div>
</div>
<div class="p-3 bg-green-50 rounded-lg">
<div class="flex items-center mb-2">
<i class="fa fa-arrow-up text-green-600 mr-2"></i>
<span class="text-xs text-gray-600">上传速度</span>
</div>
<div class="text-lg font-medium text-gray-800" id="uploadSpeed">0 MB/s</div>
</div>
</div>
<div class="mt-4 flex justify-between text-xs text-gray-500">
<span>总流入: <span id="totalDownload">0 GB</span></span>
<span>总流出: <span id="totalUpload">0 GB</span></span>
</div>
</div>
<!-- 系统信息 -->
<div class="border border-gray-200 rounded-lg p-5">
<div class="flex items-center mb-4">
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center mr-3">
<i class="fa fa-info-circle text-indigo-600 text-lg"></i>
</div>
<div>
<h3 class="text-sm font-medium text-gray-800">系统信息</h3>
<p class="text-xs text-gray-500">服务器状态</p>
</div>
</div>
<div class="space-y-3 text-sm">
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">操作系统</span>
<span class="text-gray-800 font-medium" id="osInfo">Ubuntu 22.04 LTS</span>
</div>
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">运行时间</span>
<span class="text-gray-800 font-medium" id="uptime">0 天 0 时 0 分</span>
</div>
<div class="flex justify-between py-2 border-b border-gray-100">
<span class="text-gray-500">进程数</span>
<span class="text-gray-800 font-medium" id="processCount">0</span>
</div>
<div class="flex justify-between py-2">
<span class="text-gray-500">负载均值</span>
<span class="text-gray-800 font-medium" id="loadAvg">0.00, 0.00, 0.00</span>
</div>
</div>
</div>
</div>
</div>
</div>
`;
}
// 刷新间隔定时器
let refreshTimer = null;
let currentRefreshInterval = 5000;
// 刷新硬件信息使用真实API
async function refreshHardwareInfo() {
try {
const response = await fetch(`${API_BASE}/system-info`);
const result = await response.json();
if (result.code === 0 && result.data) {
const data = result.data;
// 更新CPU
const cpu = data.cpu || {};
const cpuPercent = cpu.percent || 0;
document.getElementById('cpuPercent').textContent = cpuPercent + '%';
document.getElementById('cpuBar').style.width = cpuPercent + '%';
document.getElementById('cpuCores').textContent = (cpu.cores || 0) + ' 核心';
// 更新内存
const mem = data.memory || {};
const memUsed = mem.used_gb || 0;
const memTotal = mem.total_gb || 0;
const memPercent = mem.percent || 0;
document.getElementById('memoryPercent').textContent = memPercent + '%';
document.getElementById('memoryBar').style.width = memPercent + '%';
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
document.getElementById('memoryAvailable').textContent = (mem.available_gb || 0) + ' GB';
document.getElementById('memoryCached').textContent = (mem.cached_gb || 0) + ' GB';
// 更新磁盘
const disk = data.disk || {};
const diskUsed = disk.used_gb || 0;
const diskTotal = disk.total_gb || 0;
const diskPercent = disk.percent || 0;
document.getElementById('diskPercent').textContent = diskPercent + '%';
document.getElementById('diskBar').style.width = diskPercent + '%';
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
// 更新网络
const net = data.network || {};
document.getElementById('totalDownload').textContent = (net.download_mb || 0) + ' GB';
document.getElementById('totalUpload').textContent = (net.upload_mb || 0) + ' GB';
// 更新系统信息
const sys = data.system || {};
const uptime = sys.uptime_seconds || 0;
const days = Math.floor(uptime / 86400);
const hours = Math.floor((uptime % 86400) / 3600);
const mins = Math.floor((uptime % 3600) / 60);
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
document.getElementById('processCount').textContent = sys.process_count || 0;
// 更新GPU信息传入真实数据
updateGPUInfo(data.gpu || []);
}
} catch (error) {
console.error('获取系统信息失败:', error);
// 如果API调用失败使用模拟数据作为后备
useMockData();
}
}
// 使用模拟数据当API不可用时
function useMockData() {
// 更新CPU
const cpuUsage = Math.floor(Math.random() * 30) + 20;
document.getElementById('cpuPercent').textContent = cpuUsage + '%';
document.getElementById('cpuBar').style.width = cpuUsage + '%';
document.getElementById('core1').textContent = Math.floor(Math.random() * 40 + 20) + '%';
document.getElementById('core2').textContent = Math.floor(Math.random() * 40 + 15) + '%';
document.getElementById('core3').textContent = Math.floor(Math.random() * 40 + 25) + '%';
document.getElementById('core4').textContent = Math.floor(Math.random() * 40 + 10) + '%';
// 更新内存
const memUsed = (Math.random() * 4 + 6).toFixed(1);
const memTotal = 16;
const memPercent = Math.floor((memUsed / memTotal) * 100);
document.getElementById('memoryPercent').textContent = memPercent + '%';
document.getElementById('memoryBar').style.width = memPercent + '%';
document.getElementById('memoryUsed').textContent = memUsed + ' GB';
document.getElementById('memoryAvailable').textContent = (memTotal - memUsed).toFixed(1) + ' GB';
document.getElementById('memoryCached').textContent = (Math.random() * 3 + 1).toFixed(1) + ' GB';
// 更新磁盘
const diskUsed = Math.floor(Math.random() * 100 + 150);
const diskTotal = 512;
const diskPercent = Math.floor((diskUsed / diskTotal) * 100);
document.getElementById('diskPercent').textContent = diskPercent + '%';
document.getElementById('diskBar').style.width = diskPercent + '%';
document.getElementById('diskUsed').textContent = diskUsed + ' GB';
document.getElementById('diskAvailable').textContent = (diskTotal - diskUsed) + ' GB';
document.getElementById('diskSpeed').textContent = (Math.random() * 500 + 100).toFixed(0) + ' MB/s';
// 更新多GPU信息
updateGPUInfo();
// 更新网络
document.getElementById('downloadSpeed').textContent = (Math.random() * 100 + 10).toFixed(1) + ' MB/s';
document.getElementById('uploadSpeed').textContent = (Math.random() * 50 + 5).toFixed(1) + ' MB/s';
document.getElementById('totalDownload').textContent = (Math.random() * 500 + 100).toFixed(1) + ' GB';
document.getElementById('totalUpload').textContent = (Math.random() * 200 + 50).toFixed(1) + ' GB';
// 更新系统信息
const days = Math.floor(Math.random() * 30);
const hours = Math.floor(Math.random() * 24);
const mins = Math.floor(Math.random() * 60);
document.getElementById('uptime').textContent = days + ' 天 ' + hours + ' 时 ' + mins + ' 分';
document.getElementById('processCount').textContent = Math.floor(Math.random() * 200 + 100);
document.getElementById('loadAvg').textContent = (Math.random() * 2).toFixed(2) + ', ' + (Math.random() * 1.5).toFixed(2) + ', ' + (Math.random() * 1).toFixed(2);
}
// GPU配置 - 支持模拟1-8块GPU
const GPU_COUNT = 4; // 可配置GPU数量
const gpuConfigs = [
{ name: 'NVIDIA RTX 3090', memory: 24 },
{ name: 'NVIDIA RTX 4090', memory: 24 },
{ name: 'NVIDIA A100', memory: 80 },
{ name: 'NVIDIA V100', memory: 32 },
{ name: 'NVIDIA T4', memory: 16 },
{ name: 'NVIDIA L40S', memory: 48 },
{ name: 'NVIDIA H100', memory: 80 },
{ name: 'NVIDIA RTX 4080', memory: 16 }
];
// 初始化GPU列表获取真实数据
async function initGPUList() {
try {
const response = await fetch(`${API_BASE}/system-info`);
const result = await response.json();
const gpuData = (result.data && result.data.gpu) || [];
updateGPUInfo(gpuData);
} catch (error) {
console.error('初始化GPU列表失败:', error);
useMockGPUData();
}
}
// 更新GPU信息
function updateGPUInfo(gpuData) {
// 如果有真实数据,使用真实数据
if (gpuData && gpuData.length > 0) {
const gpuCount = gpuData.length;
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
let totalUsedMemory = 0;
let totalMemory = 0;
// 重新初始化GPU列表
const gpuList = document.getElementById('gpuList');
if (gpuList) {
let gpuCardsHTML = '';
for (let i = 0; i < gpuCount; i++) {
const gpu = gpuData[i];
totalUsedMemory += gpu.memory_used_gb;
totalMemory += gpu.memory_total_gb;
gpuCardsHTML += `
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center min-w-0">
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
<i class="fa fa-microchip text-red-600 text-xs"></i>
</div>
<div class="min-w-0">
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${gpu.name}">${gpu.name}</div>
<div class="text-[10px] text-gray-400">PCIe</div>
</div>
</div>
<div class="text-right flex-shrink-0 ml-2">
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpu.gpu_percent}%</span>
</div>
</div>
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpu.gpu_percent}%"></div>
</div>
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">显存</div>
<div class="font-medium text-gray-700" id="gpuMem${i}">${gpu.memory_used_gb}/${gpu.memory_total_gb} GB</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">温度</div>
<div class="font-medium ${gpu.temperature >= 80 ? 'text-red-600' : gpu.temperature >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${gpu.temperature}°C</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">功耗</div>
<div class="font-medium text-gray-700" id="gpuPower${i}">${gpu.power_w} W</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">Fan</div>
<div class="font-medium text-gray-700" id="gpuFan${i}">${gpu.fan_speed || 0}%</div>
</div>
</div>
<div class="mt-1 grid grid-cols-2 gap-1 text-center text-[9px] text-gray-400">
<div>Clock: <span id="gpuClock${i}">${gpu.clock_mhz || 0} MHz</span></div>
<div>Driver: <span id="gpuDriver${i}">${gpu.driver_version || '-'}</span></div>
</div>
</div>
`;
}
gpuList.innerHTML = gpuCardsHTML;
}
// 更新总显存
const gpuTotalMem = document.getElementById('gpuTotalMemory');
if (gpuTotalMem) {
gpuTotalMem.textContent = `${totalUsedMemory}/${totalMemory} GB`;
}
return;
}
// 没有真实数据,使用模拟数据
useMockGPUData();
}
// 使用模拟GPU数据
function useMockGPUData() {
const gpuCount = Math.min(GPU_COUNT, 8);
let totalUsedMemory = 0;
let totalMemory = 0;
// 重新初始化GPU列表
const gpuList = document.getElementById('gpuList');
if (gpuList) {
let gpuCardsHTML = '';
for (let i = 0; i < gpuCount; i++) {
const config = gpuConfigs[i % gpuConfigs.length];
const gpuUsage = Math.floor(Math.random() * 60 + 20);
const memUsed = (Math.random() * config.memory * 0.7 + config.memory * 0.1).toFixed(1);
const temp = Math.floor(Math.random() * 30 + 40);
const power = Math.floor(Math.random() * 150 + 100);
const fan = Math.floor(gpuUsage + Math.random() * 10);
totalUsedMemory += parseFloat(memUsed);
totalMemory += config.memory;
gpuCardsHTML += `
<div class="border border-gray-200 rounded-lg p-2 bg-gradient-to-br from-gray-50 to-gray-100" id="gpuCard${i}">
<div class="flex items-center justify-between mb-1">
<div class="flex items-center min-w-0">
<div class="w-6 h-6 rounded bg-red-100 flex items-center justify-center mr-2 flex-shrink-0">
<i class="fa fa-microchip text-red-600 text-xs"></i>
</div>
<div class="min-w-0">
<div class="text-xs font-medium text-gray-800 truncate" id="gpuName${i}" title="${config.name}">${config.name}</div>
<div class="text-[10px] text-gray-400">PCIe ${Math.floor(Math.random() * 4 + 1)}:00.0</div>
</div>
</div>
<div class="text-right flex-shrink-0 ml-2">
<span class="text-sm font-bold text-gray-800" id="gpuPercent${i}">${gpuUsage}%</span>
</div>
</div>
<div class="relative h-1.5 bg-gray-200 rounded-full overflow-hidden mb-2">
<div id="gpuBar${i}" class="absolute left-0 top-0 h-full bg-gradient-to-r from-green-400 via-yellow-400 to-red-400 transition-all duration-500" style="width: ${gpuUsage}%"></div>
</div>
<div class="grid grid-cols-4 gap-1 text-center text-[10px]">
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">显存</div>
<div class="font-medium text-gray-700" id="gpuMem${i}">${parseFloat(memUsed).toFixed(1)}/${config.memory} GB</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">温度</div>
<div class="font-medium ${temp >= 80 ? 'text-red-600' : temp >= 70 ? 'text-yellow-600' : 'text-gray-800'}" id="gpuTemp${i}">${temp}°C</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">功耗</div>
<div class="font-medium text-gray-700" id="gpuPower${i}">${power} W</div>
</div>
<div class="bg-white/80 rounded py-0.5">
<div class="text-gray-400">Fan</div>
<div class="font-medium text-gray-700" id="gpuFan${i}">${fan}%</div>
</div>
</div>
</div>
`;
}
gpuList.innerHTML = gpuCardsHTML;
document.getElementById('gpuCount').textContent = `检测到 ${gpuCount} 块 GPU`;
}
// 更新总显存
const gpuTotalMem = document.getElementById('gpuTotalMemory');
if (gpuTotalMem) {
gpuTotalMem.textContent = `${totalUsedMemory.toFixed(1)}/${totalMemory} GB`;
}
}
// 启动硬件监控自动刷新
function startRefreshTimer() {
if (refreshTimer) {
clearInterval(refreshTimer);
}
refreshTimer = setInterval(refreshHardwareInfo, currentRefreshInterval);
}
// 改变刷新频率
function changeRefreshRate() {
const select = document.getElementById('refreshInterval');
currentRefreshInterval = parseInt(select.value);
startRefreshTimer();
}
// 页面加载后初始化并启动定时器
document.addEventListener('DOMContentLoaded', function() {
// 只在平台性能页面初始化GPU列表
const gpuCountEl = document.getElementById('gpuCount');
if (gpuCountEl) {
initGPUList();
startRefreshTimer();
}
});
function saveConfig() {
showMessage('提示', '配置保存功能开发中...', 'info');
}
// 当前页面状态
let currentPage = 'fine-tune';
let currentParentPage = null;
let selectedItems = new Set(); // 存储选中的项ID
let currentPageData = []; // 存储当前页面数据
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');
}
}
// 返回列表页
function goBack() {
if (currentParentPage) {
currentPage = currentParentPage;
currentParentPage = null;
loadPage(currentPage);
} else {
loadPage('fine-tune');
}
}
// 渲染创建训练任务页面
function renderFineTuneCreatePage() {
return `
<div class="bg-white rounded-lg shadow-sm">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<div class="flex items-center">
<a href="#" onclick="goBack()" class="text-gray-500 hover:text-gray-700 flex items-center px-3 py-1.5 rounded hover:bg-gray-100">
<i class="fa fa-arrow-left"></i>
<span class="ml-1">上一步</span>
</a>
</div>
<div class="flex items-center text-sm">
<span class="text-primary cursor-pointer hover:underline" onclick="goBack()">模型调优</span>
<span class="mx-2 text-gray-300">/</span>
<span class="text-gray-800 font-medium">创建训练任务</span>
</div>
</div>
<div class="p-6">
<form id="createForm">
<!-- 基本信息 -->
<div class="mb-6">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">基本信息</h3>
<div class="flex items-center">
<label class="w-24 text-sm text-gray-600">任务名称</label>
<div class="flex-1 max-w-md">
<input type="text" name="name" class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入任务名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="nameCount">0</span> / 50</p>
</div>
</div>
</div>
<!-- 训练配置 -->
<div class="mb-10">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练配置</h3>
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方式</label>
<div class="grid grid-cols-3 gap-4">
<label class="card-radio active cursor-pointer" data-value="SFT">
<div class="flex items-start">
<input type="radio" name="train_type" value="SFT" checked class="mt-1 mr-2 w-4 h-4 text-primary border-primary focus:ring-primary">
<div>
<div class="font-medium text-sm">SFT 微调训练</div>
<div class="text-xs text-gray-400 mt-1">在监督指令下,增强模型指令跟随的能力</div>
</div>
</div>
</label>
<label class="card-radio cursor-pointer" data-value="DPO">
<div class="flex items-start">
<input type="radio" name="train_type" value="DPO" class="mt-1 mr-2 w-4 h-4 text-primary border-primary focus:ring-primary">
<div>
<div class="font-medium text-sm">DPO 偏好训练</div>
<div class="text-xs text-gray-400 mt-1">引入人类反馈,降低幻觉</div>
</div>
</div>
</label>
<label class="card-radio cursor-pointer" data-value="CPT">
<div class="flex items-start">
<input type="radio" name="train_type" value="CPT" class="mt-1 mr-2 w-4 h-4 text-primary border-primary focus:ring-primary">
<div>
<div class="font-medium text-sm">CPT 继续预训练</div>
<div class="text-xs text-gray-400 mt-1">通过无标注数据进行无监督训练</div>
</div>
</div>
</label>
</div>
</div>
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">选择模型</label>
<div class="flex items-center space-x-6 mb-3">
<label class="flex items-center">
<input type="radio" name="model_source" value="preset" checked class="mr-2">
<span class="text-sm">预置模型</span>
</label>
<label class="flex items-center">
<input type="radio" name="model_source" value="custom" class="mr-2">
<span class="text-sm">自定义模型</span>
</label>
</div>
<div>
<select name="base_model" id="baseModelSelect" class="w-96 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none">
<option value="">请选择</option>
<option value="qwen-3-4b-instruct">通义千问3-4B-Instruct</option>
<option value="qwen-7b-instruct">通义千问7B-Instruct</option>
<option value="qwen-14b-instruct">通义千问14B-Instruct</option>
<option value="llama2-7b">Llama2-7B</option>
<option value="llama2-13b">Llama2-13B</option>
</select>
</div>
</div>
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练方法</label>
<div class="flex items-center space-x-6" id="trainMethodContainer">
<label class="flex items-center opacity-50 cursor-not-allowed" id="trainMethodLora">
<input type="radio" name="train_method" value="lora" class="mr-2" disabled>
<span class="text-sm">高效训练</span>
</label>
<label class="flex items-center opacity-50 cursor-not-allowed" id="trainMethodFull">
<input type="radio" name="train_method" value="full" class="mr-2" disabled>
<span class="text-sm">全参训练</span>
</label>
</div>
</div>
<!-- 参数配置组件 - LoRA高效训练 -->
<div id="paramConfigPanelLora" class="hidden">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="text-sm font-semibold text-gray-700">参数配置</h3>
<div class="flex items-center space-x-3">
<button type="button" onclick="resetDefaultConfig('lora')" class="text-gray-500 text-sm flex items-center hover:text-primary transition-colors">
<i class="fa fa-rotate-left mr-1"></i>
<span>还原配置</span>
</button>
<button type="button" onclick="toggleParamTable('lora')" id="toggleParamBtnLora" class="text-primary text-sm flex items-center hover:underline">
<i class="fa fa-chevron-up mr-1"></i>
<span id="toggleParamTextLora">收起配置</span>
</button>
</div>
</div>
<div id="paramTableContainerLora" class="overflow-x-auto">
<table class="w-full border-collapse text-sm">
<thead>
<tr class="bg-blue-50">
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700 w-40">参数名称</th>
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700 w-64">配置</th>
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">batch_size</td>
<td class="p-3 border border-gray-200">
<input type="number" name="batch_size_lora" value="16" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[8,1024], step:8</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">批次大小代表模型训练过程中模型更新模型参数的数据步长可理解为模型每看多少数据即更新一次模型参数一般建议的批次大小为16/32</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">learning_rate</td>
<td class="p-3 border border-gray-200">
<input type="text" name="learning_rate_lora" value="3e-4" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率,代表每次更新数据的增量参数权重,学习率数值越大参数变化越大,对模型影响越大</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">n_epochs</td>
<td class="p-3 border border-gray-200">
<input type="number" name="n_epochs_lora" value="3" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[1,200], step:1</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">循环次数代表模型训练过程中模型学习数据集的次数可理解为看几遍数据一般建议的范围是1-3遍即可</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">save_steps</td>
<td class="p-3 border border-gray-200">
<input type="number" name="save_steps_lora" value="50" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[1,2147483647]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">保存步数,训练阶段模型的保存间隔步长,用于阶段性保存模型权重</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">lora_alpha</td>
<td class="p-3 border border-gray-200">
<input type="number" name="lora_alpha" value="32" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">LoRa阿尔法LoRa训练中的缩放系数用于调整初始化训练权重使其与预训练权重接近或保持一致</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">lora_dropout</td>
<td class="p-3 border border-gray-200">
<input type="number" name="lora_dropout" step="0.01" value="0.1" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[0,0.2]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">LoRa丢弃率配置训练过程中随机丢弃或忽略神经元的比率防止过拟合提高模型泛化能力</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">lora_rank</td>
<td class="p-3 border border-gray-200">
<input type="number" name="lora_rank" value="8" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">LoRa秩值LoRa训练中的秩大小影响LoRa训练中自身数据对模型作用程度秩越大作用越大需要依据数据量选择合适的秩</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">lr_scheduler_type</td>
<td class="p-3 border border-gray-200">
<select name="lr_scheduler_type_lora" class="w-36 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="linear">linear</option>
<option value="cosine">cosine</option>
<option value="constant">constant</option>
</select>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率调整策略,选择不同的学习率策略,动态地改变模型在训练过程中更新权重时所采用的学习率大小</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">max_length</td>
<td class="p-3 border border-gray-200">
<input type="number" name="max_length_lora" value="8192" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[500,131072]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">序列长度,单个训练数据样本的最大长度,超出配置长度将丢弃</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">warmup_ratio</td>
<td class="p-3 border border-gray-200">
<input type="number" name="warmup_ratio_lora" step="0.01" value="0.05" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[0,1]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率预热比例,学习率预热阶段占总训练步数的比例</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">weight_decay</td>
<td class="p-3 border border-gray-200">
<input type="number" name="weight_decay_lora" step="0.01" value="0.01" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[0,0.2]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">权重衰减用于在优化过程中对模型参数施加L2正则化防止过拟合</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 参数配置组件 - 全参训练 -->
<div id="paramConfigPanelFull" class="hidden">
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
<div class="flex justify-between items-center mb-4">
<h3 class="text-sm font-semibold text-gray-700">参数配置</h3>
<div class="flex items-center space-x-3">
<button type="button" onclick="resetDefaultConfig('full')" class="text-gray-500 text-sm flex items-center hover:text-primary transition-colors">
<i class="fa fa-rotate-left mr-1"></i>
<span>还原配置</span>
</button>
<button type="button" onclick="toggleParamTable('full')" id="toggleParamBtnFull" class="text-primary text-sm flex items-center hover:underline">
<i class="fa fa-chevron-up mr-1"></i>
<span id="toggleParamTextFull">收起配置</span>
</button>
</div>
</div>
<div id="paramTableContainerFull" class="overflow-x-auto">
<table class="w-full border-collapse text-sm">
<thead>
<tr class="bg-blue-50">
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700 w-40">参数名称</th>
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700 w-64">配置</th>
<th class="text-left p-3 border border-gray-200 font-medium text-gray-700">说明</th>
</tr>
</thead>
<tbody>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">batch_size</td>
<td class="p-3 border border-gray-200">
<input type="number" name="batch_size_full" value="16" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[8,1024], step:8</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">批次大小代表模型训练过程中模型更新模型参数的数据步长可理解为模型每看多少数据即更新一次模型参数一般建议的批次大小为16/32表示模型每看16或32条数据即更新一次参数</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">learning_rate</td>
<td class="p-3 border border-gray-200">
<input type="text" name="learning_rate_full" value="1e-5" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率,代表每次更新数据的增量参数权重,学习率数值越大参数变化越大,对模型影响越大</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">n_epochs</td>
<td class="p-3 border border-gray-200">
<input type="number" name="n_epochs_full" value="3" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[1,200], step:1</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">循环次数代表模型训练过程中模型学习数据集的次数可理解为看几遍数据一般建议的范围是1-3遍即可可依据需求进行调整</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">save_steps</td>
<td class="p-3 border border-gray-200">
<input type="number" name="save_steps_full" value="50" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[1,2147483647]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">保存步数,训练阶段模型的保存间隔步长,用于阶段性保存模型权重</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">lr_scheduler_type</td>
<td class="p-3 border border-gray-200">
<select name="lr_scheduler_type_full" class="w-36 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<option value="linear">linear</option>
<option value="cosine">cosine</option>
<option value="constant">constant</option>
</select>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率调整策略,选择不同的学习率策略,动态地改变模型在训练过程中更新权重时所采用的学习率大小</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">max_length</td>
<td class="p-3 border border-gray-200">
<input type="number" name="max_length_full" value="8192" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[500,131072]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">序列长度,单个训练数据样本的最大长度,超出配置长度将丢弃</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">warmup_ratio</td>
<td class="p-3 border border-gray-200">
<input type="number" name="warmup_ratio_full" step="0.01" value="0.05" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[0,1]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">学习率预热比例,学习率预热阶段占总训练步数的比例</td>
</tr>
<tr>
<td class="p-3 border border-gray-200 text-gray-700">weight_decay</td>
<td class="p-3 border border-gray-200">
<input type="number" name="weight_decay_full" step="0.01" value="0.01" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
<span class="text-xs text-gray-400 ml-2">[0,0.2]</span>
</td>
<td class="p-3 border border-gray-200 text-gray-500 text-sm">权重衰减用于在优化过程中对模型参数施加L2正则化防止过拟合</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 数据配置 -->
<div class="mb-10">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">数据配置</h3>
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">训练集</label>
<div>
<select name="dataset_id" class="w-96 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none">
<option value="">请选择</option>
</select>
</div>
</div>
<div class="mb-6">
<label class="block text-sm text-gray-600 mb-3">验证集 <span class="text-red-500">*</span></label>
<div class="flex items-center space-x-6 mb-3">
<label class="flex items-center">
<input type="radio" name="valid_split" value="auto" checked class="mr-2" onchange="toggleValidSetPanel('auto')">
<span class="text-sm">自动切分</span>
</label>
<label class="flex items-center">
<input type="radio" name="valid_split" value="custom" class="mr-2" onchange="toggleValidSetPanel('custom')">
<span class="text-sm">选择数据集</span>
</label>
</div>
<!-- 自动切分面板 -->
<div id="validAutoPanel" class="flex items-center">
<span class="text-sm text-gray-600 mr-2">从当前训练集随机分割</span>
<input type="number" name="valid_ratio" value="10" class="w-16 px-2 py-1 border border-gray-300 rounded text-sm text-center focus:border-primary focus:outline-none">
<span class="text-sm text-gray-600 ml-2">% 作为验证集</span>
</div>
<!-- 选择数据集面板 -->
<div id="validCustomPanel" class="hidden">
<select name="valid_dataset_id" id="validDatasetSelect" class="w-96 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none">
<option value="">请选择验证数据集</option>
</select>
</div>
</div>
</div>
<!-- 训练产出 -->
<div class="mb-10">
<h3 class="text-sm font-semibold text-gray-700 mb-4 pb-2 border-b border-gray-100">训练产出</h3>
<div class="mb-4">
<label class="block text-sm text-gray-600 mb-3">模型名称</label>
<div>
<input type="text" name="output_model_name" class="w-96 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:border-primary focus:outline-none" placeholder="请输入模型名称" maxlength="50">
<p class="text-xs text-gray-400 mt-1"><span id="modelNameCount">0</span> / 50</p>
</div>
</div>
<div class="mb-4">
<div class="flex items-center">
<span class="text-sm text-gray-600 mr-2">模型加密</span>
<span class="px-2 py-0.5 bg-green-100 text-green-700 text-xs rounded">安全升级</span>
</div>
<p class="text-xs text-gray-400 mt-1">为保障您的数据安全,平台会为导出的模型文件开启 OSS 服务端加密</p>
</div>
</div>
<!-- 底部按钮 -->
<div class="flex items-center justify-between pt-6 border-t border-gray-100">
<div class="flex items-center space-x-3">
<button type="button" onclick="submitCreateForm()" class="px-4 py-2 bg-primary text-white rounded-lg text-sm hover:bg-primary/90">
开始训练
</button>
<button type="button" onclick="goBack()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg text-sm hover:bg-gray-300">
取消
</button>
</div>
<div class="flex items-center text-sm">
<a href="#" class="text-primary hover:underline">训练费用 (预估)</a>
<span class="mx-2 text-gray-300">|</span>
<a href="#" class="text-primary hover:underline">计算详情</a>
</div>
</div>
</form>
</div>
</div>
`;
}
// 绑定创建页面事件
function bindCreatePageEvents() {
// 卡片式单选框
document.querySelectorAll('.card-radio').forEach(card => {
card.addEventListener('click', () => {
const parent = card.parentElement;
parent.querySelectorAll('.card-radio').forEach(c => c.classList.remove('active'));
card.classList.add('active');
card.querySelector('input').checked = true;
});
});
// 任务名称字数统计
const nameInput = document.querySelector('input[name="name"]');
if (nameInput) {
nameInput.addEventListener('input', () => {
const countEl = document.getElementById('nameCount');
if (countEl) countEl.textContent = nameInput.value.length;
});
}
// 模型名称字数统计
const modelNameInput = document.querySelector('input[name="output_model_name"]');
if (modelNameInput) {
modelNameInput.addEventListener('input', () => {
const countEl = document.getElementById('modelNameCount');
if (countEl) countEl.textContent = modelNameInput.value.length;
});
}
// 选择模型后启用训练方法
const baseModelSelect = document.getElementById('baseModelSelect');
if (baseModelSelect) {
baseModelSelect.addEventListener('change', () => {
const trainMethodLora = document.getElementById('trainMethodLora');
const trainMethodFull = document.getElementById('trainMethodFull');
const paramConfigPanelLora = document.getElementById('paramConfigPanelLora');
const paramConfigPanelFull = document.getElementById('paramConfigPanelFull');
if (baseModelSelect.value) {
// 启用训练方法
trainMethodLora.classList.remove('opacity-50', 'cursor-not-allowed');
trainMethodFull.classList.remove('opacity-50', 'cursor-not-allowed');
trainMethodLora.querySelector('input').disabled = false;
trainMethodFull.querySelector('input').disabled = false;
} else {
// 禁用训练方法
trainMethodLora.classList.add('opacity-50', 'cursor-not-allowed');
trainMethodFull.classList.add('opacity-50', 'cursor-not-allowed');
trainMethodLora.querySelector('input').disabled = true;
trainMethodFull.querySelector('input').disabled = true;
// 隐藏参数配置面板
paramConfigPanelLora.classList.add('hidden');
paramConfigPanelFull.classList.add('hidden');
// 取消选择训练方法
document.querySelectorAll('input[name="train_method"]').forEach(input => input.checked = false);
}
});
}
// 训练方法选择后显示/隐藏对应的参数配置面板
document.querySelectorAll('input[name="train_method"]').forEach(input => {
input.addEventListener('change', () => {
const paramConfigPanelLora = document.getElementById('paramConfigPanelLora');
const paramConfigPanelFull = document.getElementById('paramConfigPanelFull');
if (input.checked && input.value === 'lora') {
paramConfigPanelLora.classList.remove('hidden');
paramConfigPanelFull.classList.add('hidden');
} else if (input.checked && input.value === 'full') {
paramConfigPanelLora.classList.add('hidden');
paramConfigPanelFull.classList.remove('hidden');
} else {
paramConfigPanelLora.classList.add('hidden');
paramConfigPanelFull.classList.add('hidden');
}
});
});
}
// 加载数据集列表
async function loadDatasets() {
try {
const response = await fetch(`${API_BASE}/dataset-manage`);
const result = await response.json();
if (result.code === 0) {
// 训练集下拉框
const trainSelect = document.querySelector('select[name="dataset_id"]');
if (trainSelect) {
trainSelect.innerHTML = '<option value="">请选择</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
// 验证集下拉框
const validSelect = document.querySelector('select[name="valid_dataset_id"]');
if (validSelect) {
validSelect.innerHTML = '<option value="">请选择验证数据集</option>' +
result.data.map(d => `<option value="${d.id}">${d.name}</option>`).join('');
}
}
} catch (e) {
console.error('加载数据集失败:', e);
}
}
// 切换验证集面板显示
function toggleValidSetPanel(type) {
const autoPanel = document.getElementById('validAutoPanel');
const customPanel = document.getElementById('validCustomPanel');
if (type === 'auto') {
autoPanel.classList.remove('hidden');
customPanel.classList.add('hidden');
} else {
autoPanel.classList.add('hidden');
customPanel.classList.remove('hidden');
}
}
// 恢复默认配置
function resetDefaultConfig(type) {
if (type === 'lora') {
const defaults = {
'batch_size_lora': '16',
'learning_rate_lora': '3e-4',
'n_epochs_lora': '3',
'save_steps_lora': '50',
'lora_alpha': '32',
'lora_dropout': '0.1',
'lora_rank': '8',
'lr_scheduler_type_lora': 'linear',
'max_length_lora': '8192',
'warmup_ratio_lora': '0.05',
'weight_decay_lora': '0.01'
};
for (const [name, value] of Object.entries(defaults)) {
const input = document.querySelector(`input[name="${name}"]`);
if (input) input.value = value;
const select = document.querySelector(`select[name="${name}"]`);
if (select) select.value = value;
}
} else {
const defaults = {
'batch_size_full': '16',
'learning_rate_full': '1e-5',
'n_epochs_full': '3',
'save_steps_full': '50',
'lr_scheduler_type_full': 'linear',
'max_length_full': '8192',
'warmup_ratio_full': '0.05',
'weight_decay_full': '0.01'
};
for (const [name, value] of Object.entries(defaults)) {
const input = document.querySelector(`input[name="${name}"]`);
if (input) input.value = value;
const select = document.querySelector(`select[name="${name}"]`);
if (select) select.value = value;
}
}
}
// 收起/展开参数配置表格
function toggleParamTable(type) {
const container = type === 'lora' ? document.getElementById('paramTableContainerLora') : document.getElementById('paramTableContainerFull');
const btn = type === 'lora' ? document.getElementById('toggleParamBtnLora') : document.getElementById('toggleParamBtnFull');
const icon = btn.querySelector('i');
const text = type === 'lora' ? document.getElementById('toggleParamTextLora') : document.getElementById('toggleParamTextFull');
if (container.classList.contains('hidden')) {
container.classList.remove('hidden');
icon.className = 'fa fa-chevron-up mr-1';
text.textContent = '收起配置';
} else {
container.classList.add('hidden');
icon.className = 'fa fa-chevron-down mr-1';
text.textContent = '展开配置';
}
}
// ============ 自定义消息弹窗 ============
// 显示消息弹窗 - 使用 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 = '';
}
// 查看已训练模型详情 - 跳转到推理页面
window.viewTrainedModel = function(name, method, path) {
// 跳转到推理测试页面main.html在pages目录下所以直接用文件名
window.location.href = `model-inference.html?model=${encodeURIComponent(name)}&method=${encodeURIComponent(method)}`;
};
// 确认弹窗(两个按钮)- 使用 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>
`;
}
}
}
// ============ Web日志系统 ============
const webLogger = {
_currentPage: 'main',
// 初始化当前页面名称
init: function(pageName) {
this._currentPage = pageName || 'unknown';
},
// 发送日志到服务器
_sendLog: async function(level, message) {
try {
await fetch(`${API_BASE}/web-log`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
level: level,
message: message,
page: this._currentPage,
timestamp: new Date().toISOString()
})
});
} catch (e) {
// 发送失败时只记录到控制台
console.warn('日志发送失败:', e);
}
},
info: function(message) {
console.log(`[INFO] ${message}`);
this._sendLog('info', message);
},
error: function(message) {
console.error(`[ERROR] ${message}`);
this._sendLog('error', message);
},
warning: function(message) {
console.warn(`[WARNING] ${message}`);
this._sendLog('warning', message);
},
debug: function(message) {
console.debug(`[DEBUG] ${message}`);
this._sendLog('debug', message);
}
};
// 页面加载完成后初始化日志
document.addEventListener('DOMContentLoaded', function() {
// 获取当前页面名称
const path = window.location.pathname;
const pageName = path.split('/').pop().replace('.html', '') || 'main';
webLogger.init(pageName);
webLogger.info('页面加载完成');
});
</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>
</body>
</html>