模型开始训练界面以及查看日志功能完善
This commit is contained in:
@@ -260,6 +260,11 @@
|
||||
<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>
|
||||
@@ -304,28 +309,6 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 会话超时检查(5分钟)
|
||||
const SESSION_TIMEOUT = 5 * 60 * 1000; // 5分钟
|
||||
function checkSession() {
|
||||
const loginTime = localStorage.getItem('loginTime');
|
||||
if (!loginTime || (Date.now() - parseInt(loginTime)) > SESSION_TIMEOUT) {
|
||||
// 会话过期,清除并跳转到登录页
|
||||
localStorage.removeItem('loginTime');
|
||||
localStorage.removeItem('username');
|
||||
window.location.href = 'login.html';
|
||||
return false;
|
||||
}
|
||||
// 更新登录时间(用户有活动时续期)
|
||||
localStorage.setItem('loginTime', Date.now());
|
||||
return true;
|
||||
}
|
||||
|
||||
// 页面加载时检查会话
|
||||
if (!checkSession()) {
|
||||
// 阻止页面渲染
|
||||
document.body.innerHTML = '';
|
||||
}
|
||||
|
||||
// API 基础地址 - 使用 config.yaml 中的 app.port (7861)
|
||||
const getApiBase = () => {
|
||||
const protocol = window.location.protocol;
|
||||
@@ -430,9 +413,8 @@
|
||||
createText: '创建训练任务',
|
||||
columns: [
|
||||
{ title: '任务名称', key: 'name' },
|
||||
{ title: '基础模型', key: 'base_model' },
|
||||
{ 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: 'progress', render: (val) => `${val || 0}%` },
|
||||
{ title: '创建时间', key: 'create_time', render: (val) => val ? new Date(val).toLocaleString('zh-CN') : '-' }
|
||||
],
|
||||
actions: ['stop', 'logs', 'delete']
|
||||
@@ -586,6 +568,12 @@
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isExternalPage: true
|
||||
},
|
||||
'training-log': {
|
||||
title: '训练日志',
|
||||
skipFetch: true,
|
||||
hasCreate: false,
|
||||
isExternalPage: true
|
||||
}
|
||||
};
|
||||
|
||||
@@ -606,6 +594,124 @@
|
||||
'chat': '对话'
|
||||
};
|
||||
|
||||
// 训练进度缓存
|
||||
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 加载自定义工具
|
||||
@@ -645,6 +751,12 @@
|
||||
|
||||
loadPage(defaultPage);
|
||||
|
||||
// 启动训练进度自动刷新(每5秒)
|
||||
progressRefreshTimer = setInterval(() => {
|
||||
refreshTrainingProgress();
|
||||
checkAndUpdateTaskStatus();
|
||||
}, 5000);
|
||||
|
||||
// 更新侧边栏高亮状态
|
||||
document.querySelectorAll('.nav-link').forEach(link => {
|
||||
if (link.dataset.page === defaultPage) {
|
||||
@@ -727,6 +839,14 @@
|
||||
// 离开日志页面时停止自动刷新
|
||||
stopLogAutoRefresh();
|
||||
|
||||
// 离开模型调优页面时停止进度刷新
|
||||
if (currentPage === 'fine-tune' && pageName !== 'fine-tune') {
|
||||
if (progressRefreshTimer) {
|
||||
clearInterval(progressRefreshTimer);
|
||||
progressRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
const container = document.getElementById('page-content');
|
||||
const config = tableConfigs[pageName];
|
||||
|
||||
@@ -740,6 +860,14 @@
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 显示/隐藏返回按钮(外部页面显示,普通页面隐藏)
|
||||
const backBtn = document.getElementById('pageBackBtn');
|
||||
if (config.isExternalPage) {
|
||||
backBtn.classList.remove('hidden');
|
||||
} else {
|
||||
backBtn.classList.add('hidden');
|
||||
}
|
||||
|
||||
try {
|
||||
// 渲染页面
|
||||
if (config.isExternalPage) {
|
||||
@@ -747,9 +875,14 @@
|
||||
const response = await fetch(`${pageName}.html?t=${Date.now()}`);
|
||||
if (response.ok) {
|
||||
const html = await response.text();
|
||||
// 只提取内联脚本内容(没有src属性的script标签)
|
||||
const scriptMatch = html.match(/<script\b(?![^>]*\bsrc)[^>]*>([\s\S]*?)<\/script>/);
|
||||
const scriptContent = scriptMatch ? scriptMatch[1] : '';
|
||||
// 提取所有内联脚本内容(没有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, '');
|
||||
|
||||
@@ -785,10 +918,18 @@
|
||||
}
|
||||
|
||||
container.innerHTML = headerHtml + htmlWithoutScript;
|
||||
// 执行脚本
|
||||
// 执行脚本 - 使用 script 元素注入,使函数在全局作用域可用
|
||||
if (scriptContent && scriptContent.trim()) {
|
||||
try {
|
||||
eval(scriptContent);
|
||||
// 移除可能存在的旧脚本容器
|
||||
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);
|
||||
}
|
||||
@@ -812,6 +953,17 @@
|
||||
const data = await fetchData(`${API_BASE}/${config.api}`);
|
||||
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);
|
||||
@@ -859,6 +1011,44 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 停止训练任务
|
||||
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 {
|
||||
@@ -1133,7 +1323,7 @@
|
||||
` : ''}
|
||||
${columns.map(col => `
|
||||
<td class="px-4 py-4 text-sm text-center">
|
||||
${col.render ? col.render(item[col.key]) : (item[col.key] || '-')}
|
||||
${col.render ? col.render(item[col.key], item) : (item[col.key] || '-')}
|
||||
</td>
|
||||
`).join('')}
|
||||
<td class="px-4 py-4 text-sm text-center">
|
||||
@@ -1141,7 +1331,16 @@
|
||||
${config.actions.map(action => {
|
||||
let onclick = '';
|
||||
let btnClass = 'text-primary hover:text-primary/80';
|
||||
if (action === 'delete') {
|
||||
|
||||
// 对于 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') {
|
||||
@@ -1152,6 +1351,8 @@
|
||||
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 {
|
||||
onclick = `showMessage('提示', '${actionLabels[action] || action}功能开发中...', 'info')`;
|
||||
}
|
||||
@@ -1189,33 +1390,59 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4">
|
||||
<!-- 日期和刷新间隔选择 -->
|
||||
<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 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 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>
|
||||
</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 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 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="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 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">
|
||||
@@ -1233,24 +1460,131 @@
|
||||
`;
|
||||
}
|
||||
|
||||
// 当前日志类型: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();
|
||||
}
|
||||
// 加载默认日志类型
|
||||
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.value;
|
||||
const selectedDate = datePicker ? datePicker.value : new Date().toISOString().split('T')[0];
|
||||
|
||||
if (!logTypeSelect) return;
|
||||
logTypeSelect.innerHTML = '<option value="">加载中...</option>';
|
||||
|
||||
try {
|
||||
@@ -1269,6 +1603,10 @@
|
||||
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>';
|
||||
@@ -1324,9 +1662,13 @@
|
||||
|
||||
// 刷新日志
|
||||
function refreshLogs() {
|
||||
loadLogFiles();
|
||||
if (document.getElementById('logTypeSelect').value) {
|
||||
loadSelectedLog();
|
||||
if (currentLogTab === 'system') {
|
||||
loadLogFiles();
|
||||
if (document.getElementById('logTypeSelect').value) {
|
||||
loadSelectedLog();
|
||||
}
|
||||
} else {
|
||||
loadTrainingLogFiles();
|
||||
}
|
||||
// 重置倒计时
|
||||
const select = document.getElementById('logRefreshInterval');
|
||||
@@ -2057,10 +2399,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 根据模型ID获取模型名称
|
||||
// 根据模型ID获取模型名称(同步版本,用于表格渲染)
|
||||
function getModelName(modelId) {
|
||||
const model = modelListCache.find(m => m.id === modelId);
|
||||
return model ? model.name : `模型${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列表获取模型名称列表
|
||||
@@ -2255,12 +2650,12 @@
|
||||
<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">eval_steps</td>
|
||||
<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="eval_steps_lora" value="50" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
|
||||
<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>
|
||||
<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>
|
||||
@@ -2375,12 +2770,12 @@
|
||||
<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">eval_steps</td>
|
||||
<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="eval_steps_full" value="50" class="w-24 px-2 py-1 border border-gray-300 rounded text-sm focus:border-primary focus:outline-none">
|
||||
<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>
|
||||
<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>
|
||||
@@ -2629,7 +3024,7 @@
|
||||
'batch_size_lora': '16',
|
||||
'learning_rate_lora': '3e-4',
|
||||
'n_epochs_lora': '3',
|
||||
'eval_steps_lora': '50',
|
||||
'save_steps_lora': '50',
|
||||
'lora_alpha': '32',
|
||||
'lora_dropout': '0.1',
|
||||
'lora_rank': '8',
|
||||
@@ -2649,7 +3044,7 @@
|
||||
'batch_size_full': '16',
|
||||
'learning_rate_full': '1e-5',
|
||||
'n_epochs_full': '3',
|
||||
'eval_steps_full': '50',
|
||||
'save_steps_full': '50',
|
||||
'lr_scheduler_type_full': 'linear',
|
||||
'max_length_full': '8192',
|
||||
'warmup_ratio_full': '0.05',
|
||||
@@ -2755,6 +3150,7 @@
|
||||
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');
|
||||
@@ -2771,7 +3167,9 @@
|
||||
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';
|
||||
|
||||
@@ -2863,6 +3261,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
// 返回到列表页(外部页面用)
|
||||
function goBackToList() {
|
||||
navigateToPage('fine-tune');
|
||||
}
|
||||
|
||||
// 添加评测维度
|
||||
function addDimension() {
|
||||
window.location.href = 'model-dimension-create.html';
|
||||
|
||||
Reference in New Issue
Block a user