Files
YG_FT_Platform/web/pages/training-log.html
WIN-JHFT4D3SIVT\caoxiaozhu e494c4ce50 1. 修改了一些bug
2. 做了一些调整,比如启动脚本,支持了tenmsorboard
2026-01-29 15:51:45 +08:00

998 lines
44 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>
// 禁用 Tailwind 开发模式警告
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>
<link href="../lib/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<script src="../lib/chart.js/chart.min.js"></script>
<script>
// 确保 Chart.js 已加载
if (typeof Chart === 'undefined') {
// 备用:尝试动态加载
const script = document.createElement('script');
script.src = '../lib/chart.js/chart.umd.min.js';
script.onerror = function() {
console.error('Chart.js 加载失败');
};
document.head.appendChild(script);
}
</script>
<style>
.bg-primary { background-color: #1890ff; }
.text-primary { color: #1890ff; }
.border-primary { border-color: #1890ff; }
:root { --primary: #1890ff; --danger: #f5222d; --success: #52c41a; }
/* 日志样式 */
.log-content {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
line-height: 1.5;
white-space: pre-wrap;
word-wrap: break-word;
}
.log-content .error { color: #dc3545; }
.log-content .warning { color: #d97706; }
.log-content .info { color: #0891b2; }
.log-content .success { color: #16a34a; }
.log-content .progress { color: #7c3aed; font-weight: bold; }
.log-line { padding: 1px 8px; }
.log-line:hover { background-color: rgba(24, 144, 255, 0.1); }
</style>
</head>
<body class="bg-gray-50 p-6">
<!-- 页面标题 -->
<div class="bg-white rounded-lg shadow-sm w-full p-4 border-b border-gray-100 mb-4">
<div class="flex items-center justify-between">
<div class="flex items-center text-sm">
<span class="text-gray-800 font-medium">训练日志</span>
</div>
<div class="flex items-center space-x-3">
<button onclick="toggleTB()" id="tbBtn" class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 transition-colors text-sm">
<i class="fa fa-bar-chart mr-1"></i>TensorBoard
</button>
</div>
</div>
</div>
<!-- 任务信息 -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-medium text-gray-800" id="taskName">加载中...</h2>
<span id="taskStatus" class="px-3 py-1 rounded-full text-sm bg-gray-100 text-gray-600">加载中</span>
</div>
<div class="grid grid-cols-2 md:grid-cols-6 gap-4 text-sm">
<div>
<div class="text-gray-500 text-xs">基础模型</div>
<div id="baseModel" class="font-medium text-gray-800">-</div>
</div>
<div>
<div class="text-gray-500 text-xs">数据集</div>
<div id="dataset" class="font-medium text-gray-800">-</div>
</div>
<div>
<div class="text-gray-500 text-xs">创建时间</div>
<div id="createTime" class="font-medium text-gray-800">-</div>
</div>
<div>
<div class="text-gray-500 text-xs">进程ID</div>
<div id="processId" class="font-medium text-gray-800">-</div>
</div>
<div>
<div class="text-gray-500 text-xs">GPU信息</div>
<div id="taskGPU" class="font-medium text-gray-800">-</div>
</div>
<div>
<div class="text-gray-500 text-xs">最后更新</div>
<div id="lastUpdate" class="font-medium text-gray-800">-</div>
</div>
</div>
</div>
<!-- 训练曲线图表 -->
<div id="chartsContainer" class="bg-white rounded-xl shadow-md p-6 mb-6 border border-gray-100">
<div class="flex items-center mb-4">
<div class="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-blue-500 to-purple-500 text-white mr-3">
<i class="fa fa-line-chart"></i>
</div>
<h3 class="text-base font-semibold text-gray-800">训练实时曲线</h3>
<span id="chartUpdateStatus" class="ml-auto text-xs text-gray-400">自动更新中...</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="bg-gradient-to-br from-red-50 to-white rounded-lg p-4 border border-red-100">
<canvas id="lossChart" class="w-full h-48"></canvas>
</div>
<div class="bg-gradient-to-br from-blue-50 to-white rounded-lg p-4 border border-blue-100">
<canvas id="gradNormChart" class="w-full h-48"></canvas>
</div>
<div class="bg-gradient-to-br from-green-50 to-white rounded-lg p-4 border border-green-100">
<canvas id="learningRateChart" class="w-full h-48"></canvas>
</div>
</div>
</div>
<!-- 训练总结 -->
<div id="trainSummaryContainer" class="bg-white rounded-xl shadow-md p-6 mb-6 border border-gray-100">
<div class="flex items-center mb-4">
<div class="flex items-center justify-center w-8 h-8 rounded-lg bg-gradient-to-br from-green-500 to-teal-500 text-white mr-3">
<i class="fa fa-check-circle"></i>
</div>
<h3 class="text-base font-semibold text-gray-800">训练总结</h3>
<span id="trainSummaryStatus" class="ml-auto text-xs px-2 py-1 rounded-full bg-gray-100 text-gray-500">训练中</span>
</div>
<div id="trainSummaryContent" class="grid grid-cols-2 md:grid-cols-5 gap-4">
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">Epoch</div>
<div id="summaryEpoch" class="text-lg font-semibold text-gray-800">-</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">训练损失</div>
<div id="summaryTrainLoss" class="text-lg font-semibold text-gray-800">-</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">训练时长</div>
<div id="summaryTrainRuntime" class="text-lg font-semibold text-gray-800">-</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">样本/秒</div>
<div id="summarySamplesPerSec" class="text-lg font-semibold text-gray-800">-</div>
</div>
<div class="text-center p-3 bg-gray-50 rounded-lg">
<div class="text-xs text-gray-500 mb-1">步/秒</div>
<div id="summaryStepsPerSec" class="text-lg font-semibold text-gray-800">-</div>
</div>
</div>
<div id="trainSummaryFlos" class="mt-4 text-center text-xs text-gray-400">
浮点运算量 (Total FLOPS): <span id="summaryTotalFlos">-</span>
</div>
</div>
<!-- 日志内容 -->
<div class="bg-white rounded-lg shadow-sm">
<div class="flex items-center justify-between p-4 border-b border-gray-100">
<h3 class="text-base font-medium text-gray-800">实时日志</h3>
<div class="flex items-center space-x-4">
<input type="text" id="logSearchInput" placeholder="搜索日志..."
class="px-3 py-1.5 border border-gray-300 rounded text-sm focus:outline-none focus:border-primary w-48"
oninput="searchLog()">
</div>
</div>
<div class="p-4">
<div id="logMatchCount" class="text-xs text-gray-500 mb-2"></div>
<div id="logContent" class="log-content bg-gray-50 rounded p-4 max-h-[400px] overflow-y-auto text-xs">
加载日志中...
</div>
</div>
</div>
<script>
let taskId = null;
let taskInfo = null;
let trainingLogFullContent = '';
// 训练曲线数据
const lossData = { labels: [], values: [] };
const gradNormData = { labels: [], values: [] };
const learningRateData = { labels: [], values: [] };
// 图表实例
let lossChart, gradNormChart, learningRateChart;
// 初始化图表
function initCharts() {
if (typeof Chart === 'undefined') {
document.getElementById('chartsContainer').innerHTML = '<div class="text-center p-4 text-red-500"><i class="fa fa-exclamation-triangle mr-2"></i>图表库加载失败,请刷新页面重试</div>';
return;
}
// 创建渐变填充函数
function createGradient(ctx, colorStart, colorEnd) {
const gradient = ctx.createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, colorStart);
gradient.addColorStop(1, colorEnd);
return gradient;
}
// 通用样式配置
const basePointStyle = {
pointRadius: 4,
pointHoverRadius: 6,
pointBackgroundColor: '#fff',
pointBorderWidth: 2,
tension: 0.4
};
const baseLineStyle = {
borderWidth: 2.5,
borderCapStyle: 'round',
borderJoinStyle: 'round'
};
// Loss 图表
const lossCtx = document.getElementById('lossChart').getContext('2d');
const lossGradient = createGradient(lossCtx, 'rgba(239, 68, 68, 0.4)', 'rgba(239, 68, 68, 0.02)');
lossChart = new Chart(lossCtx, {
type: 'line',
data: {
labels: lossData.labels,
datasets: [{
label: 'Loss',
data: lossData.values,
borderColor: '#ef4444',
backgroundColor: lossGradient,
fill: true,
...basePointStyle,
...baseLineStyle
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500, easing: 'easeOutQuart' },
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { display: false },
title: {
display: true,
text: '📉 损失值 (Loss)',
color: '#ef4444',
font: { size: 15, weight: '600' },
padding: { bottom: 15 }
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
titleColor: '#fff',
bodyColor: '#fff',
padding: 10,
cornerRadius: 8,
displayColors: false
}
},
scales: {
x: {
title: { display: true, text: '训练步数 (Step)', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: { color: '#9ca3af', font: { size: 11 } }
},
y: {
title: { display: true, text: '损失值', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: { color: '#9ca3af', font: { size: 11 } }
}
}
}
});
// Grad Norm 图表
const gradNormCtx = document.getElementById('gradNormChart').getContext('2d');
const gradNormGradient = createGradient(gradNormCtx, 'rgba(59, 130, 246, 0.4)', 'rgba(59, 130, 246, 0.02)');
gradNormChart = new Chart(gradNormCtx, {
type: 'line',
data: {
labels: gradNormData.labels,
datasets: [{
label: 'Grad Norm',
data: gradNormData.values,
borderColor: '#3b82f6',
backgroundColor: gradNormGradient,
fill: true,
...basePointStyle,
...baseLineStyle
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500, easing: 'easeOutQuart' },
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { display: false },
title: {
display: true,
text: '📊 梯度范数 (Grad Norm)',
color: '#3b82f6',
font: { size: 15, weight: '600' },
padding: { bottom: 15 }
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
titleColor: '#fff',
bodyColor: '#fff',
padding: 10,
cornerRadius: 8,
displayColors: false
}
},
scales: {
x: {
title: { display: true, text: '训练步数 (Step)', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: { color: '#9ca3af', font: { size: 11 } }
},
y: {
title: { display: true, text: '梯度范数', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: { color: '#9ca3af', font: { size: 11 } }
}
}
}
});
// Learning Rate 图表
const lrCtx = document.getElementById('learningRateChart').getContext('2d');
const lrGradient = createGradient(lrCtx, 'rgba(34, 197, 94, 0.4)', 'rgba(34, 197, 94, 0.02)');
learningRateChart = new Chart(lrCtx, {
type: 'line',
data: {
labels: learningRateData.labels,
datasets: [{
label: 'Learning Rate',
data: learningRateData.values,
borderColor: '#22c55e',
backgroundColor: lrGradient,
fill: true,
...basePointStyle,
...baseLineStyle
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: { duration: 500, easing: 'easeOutQuart' },
interaction: { intersect: false, mode: 'index' },
plugins: {
legend: { display: false },
title: {
display: true,
text: '📈 学习率 (Learning Rate)',
color: '#22c55e',
font: { size: 15, weight: '600' },
padding: { bottom: 15 }
},
tooltip: {
backgroundColor: 'rgba(0,0,0,0.8)',
titleColor: '#fff',
bodyColor: '#fff',
padding: 10,
cornerRadius: 8,
displayColors: false,
callbacks: {
label: function(context) {
return '学习率: ' + context.parsed.y.toExponential(2);
}
}
}
},
scales: {
x: {
title: { display: true, text: '训练步数 (Step)', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: { color: '#9ca3af', font: { size: 11 } }
},
y: {
type: 'logarithmic',
title: { display: true, text: '学习率 (对数坐标)', color: '#6b7280', font: { size: 12 } },
grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false },
ticks: {
color: '#9ca3af',
font: { size: 11 },
callback: function(value) {
return value.toExponential(0);
}
}
}
}
}
});
}
// 解析日志中的指标数据
function parseMetricsFromLog(logContent) {
// 匹配 {'loss': x.xxxx, 'grad_norm': x.xxxx, 'learning_rate': x.xxxx, 'epoch': x.xx}
const metricRegex = /\{'loss':\s*([\d.]+),\s*'grad_norm':\s*([\d.]+),\s*'learning_rate':\s*([\d.e+-]+),\s*'epoch':\s*([\d.]+)\}/g;
let match;
let step = 0;
while ((match = metricRegex.exec(logContent)) !== null) {
const loss = parseFloat(match[1]);
const gradNorm = parseFloat(match[2]);
const learningRate = parseFloat(match[3]);
const epoch = parseFloat(match[4]);
// 更新数据
if (!lossData.values.includes(loss)) {
step++;
lossData.labels.push(step);
lossData.values.push(loss);
gradNormData.labels.push(step);
gradNormData.values.push(gradNorm);
learningRateData.labels.push(step);
learningRateData.values.push(learningRate);
}
}
// 更新图表
if (lossChart) {
lossChart.data.labels = lossData.labels;
lossChart.data.datasets[0].data = lossData.values;
lossChart.update('none');
}
if (gradNormChart) {
gradNormChart.data.labels = gradNormData.labels;
gradNormChart.data.datasets[0].data = gradNormData.values;
gradNormChart.update('none');
}
if (learningRateChart) {
learningRateChart.data.labels = learningRateData.labels;
learningRateChart.data.datasets[0].data = learningRateData.values;
learningRateChart.update('none');
}
}
// 解析训练总结指标
function parseTrainSummary(logContent, taskStatus) {
const summary = {
epoch: '-',
train_loss: '-',
train_runtime: '-',
samples_per_sec: '-',
steps_per_sec: '-',
total_flos: '-',
completed: false
};
// 检查任务是否已完成
if (taskStatus && taskStatus.toLowerCase() === 'completed') {
summary.completed = true;
}
// 匹配训练总结格式
const summaryRegex = /\*\*\*\*\* train metrics \*\*\*\*\*\s*[\s\S]*?epoch\s*=\s*([\d.]+)\s*[\s\S]*?total_flos\s*=\s*([\d.]+)([GMT]?)\s*[\s\S]*?train_loss\s*=\s*([\d.]+)\s*[\s\S]*?train_runtime\s*=\s*([\d:.]+)\s*[\s\S]*?train_samples_per_second\s*=\s*([\d.]+)\s*[\s\S]*?train_steps_per_second\s*=\s*([\d.]+)/;
const match = logContent.match(summaryRegex);
if (match) {
summary.epoch = match[1];
summary.total_flos = match[2] + match[3];
summary.train_loss = match[4];
summary.train_runtime = match[5];
summary.samples_per_sec = match[6];
summary.steps_per_sec = match[7];
summary.completed = true;
}
// 更新UI
const summaryEpoch = document.getElementById('summaryEpoch');
const summaryTrainLoss = document.getElementById('summaryTrainLoss');
const summaryTrainRuntime = document.getElementById('summaryTrainRuntime');
const summarySamplesPerSec = document.getElementById('summarySamplesPerSec');
const summaryStepsPerSec = document.getElementById('summaryStepsPerSec');
const summaryTotalFlos = document.getElementById('summaryTotalFlos');
if (summaryEpoch) summaryEpoch.textContent = summary.epoch;
if (summaryTrainLoss) summaryTrainLoss.textContent = summary.train_loss;
if (summaryTrainRuntime) summaryTrainRuntime.textContent = summary.train_runtime;
if (summarySamplesPerSec) summarySamplesPerSec.textContent = summary.samples_per_sec;
if (summaryStepsPerSec) summaryStepsPerSec.textContent = summary.steps_per_sec;
if (summaryTotalFlos) summaryTotalFlos.textContent = summary.total_flos;
// 更新状态标签
const statusElement = document.getElementById('trainSummaryStatus');
if (statusElement) {
if (summary.completed) {
statusElement.textContent = '已完成';
statusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-green-100 text-green-600';
} else {
statusElement.textContent = '训练中';
statusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-blue-100 text-blue-600';
}
}
}
// 带超时的 fetch
async function fetchWithTimeout(url, options = {}, timeout = 10000) {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
clearTimeout(id);
return response;
} catch (error) {
clearTimeout(id);
throw new Error(`请求超时或失败: ${error.message}`);
}
}
// 获取URL参数
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
// 获取任务ID优先从URL参数其次从sessionStorage
function getTaskId() {
let id = getQueryParam('id');
if (!id) {
try {
id = sessionStorage.getItem('trainingLogTaskId');
} catch (e) {}
}
return id;
}
// 返回模型调优列表
function goBack() {
if (window.parent && window.parent.navigateToPage) {
window.parent.navigateToPage('fine-tune');
} else {
window.location.href = 'main.html?page=fine-tune';
}
}
// 初始化
async function init() {
taskId = getTaskId();
if (!taskId) {
const taskNameEl = document.getElementById('taskName');
const logContentEl = document.getElementById('logContent');
if (taskNameEl) taskNameEl.textContent = '未指定任务ID';
if (logContentEl) logContentEl.innerHTML = '<span class="text-gray-400">请先从模型调优列表点击查看日志</span>';
return;
}
await loadTaskInfo();
await loadLogContent();
// 自动刷新每5秒
setInterval(async () => {
await loadTaskInfo();
await loadLogContent();
}, 5000);
}
// 加载任务信息
async function loadTaskInfo() {
try {
const response = await fetchWithTimeout(`${API_BASE}/fine-tune/${taskId}`);
const result = await response.json();
if (result.code === 0 && result.data) {
taskInfo = result.data;
await updateTaskInfo();
} else {
const statusElement = document.getElementById('taskStatus');
if (statusElement) {
statusElement.textContent = '获取失败';
statusElement.className = 'px-3 py-1 rounded-full text-sm bg-red-100 text-red-700';
}
}
} catch (error) {
console.error('[Task] 获取任务信息失败:', error);
const statusElement = document.getElementById('taskStatus');
if (statusElement) {
statusElement.textContent = '获取失败';
statusElement.className = 'px-3 py-1 rounded-full text-sm bg-red-100 text-red-700';
}
}
}
// 更新任务信息显示
async function updateTaskInfo() {
if (!taskInfo) return;
const taskNameElement = document.getElementById('taskName');
if (taskNameElement) {
taskNameElement.textContent = taskInfo.name || '未知任务';
}
// 更新状态
const statusElement = document.getElementById('taskStatus');
if (!statusElement) return;
const actualStatus = taskInfo.status ? taskInfo.status.toLowerCase() : 'unknown';
const statusMap = {
'pending': { text: '等待中', class: 'bg-gray-100 text-gray-600' },
'running': { text: '训练中', class: 'bg-blue-100 text-blue-600' },
'completed': { text: '已完成', class: 'bg-green-100 text-green-600' },
'failed': { text: '失败', class: 'bg-red-100 text-red-700' },
'stopped': { text: '已停止', class: 'bg-orange-100 text-orange-600' }
};
const statusConfig = statusMap[actualStatus] || { text: actualStatus, class: 'bg-gray-100 text-gray-600' };
statusElement.textContent = statusConfig.text;
statusElement.className = `px-3 py-1 rounded-full text-sm ${statusConfig.class}`;
// 更新训练总结状态
const summaryStatusElement = document.getElementById('trainSummaryStatus');
if (summaryStatusElement) {
if (actualStatus === 'completed') {
summaryStatusElement.textContent = '已完成';
summaryStatusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-green-100 text-green-600';
} else if (actualStatus === 'running') {
summaryStatusElement.textContent = '训练中';
summaryStatusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-blue-100 text-blue-600';
} else if (actualStatus === 'failed' || actualStatus === 'stopped') {
summaryStatusElement.textContent = '已停止';
summaryStatusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-gray-100 text-gray-500';
} else {
summaryStatusElement.textContent = '等待中';
summaryStatusElement.className = 'ml-auto text-xs px-2 py-1 rounded-full bg-gray-100 text-gray-500';
}
}
// 更新图表区域状态显示
const chartStatusElement = document.getElementById('chartUpdateStatus');
if (chartStatusElement) {
if (actualStatus === 'completed') {
chartStatusElement.textContent = '已完成';
chartStatusElement.className = 'ml-auto text-xs text-green-500';
} else if (actualStatus === 'running') {
chartStatusElement.textContent = '自动更新中...';
chartStatusElement.className = 'ml-auto text-xs text-gray-400';
} else {
chartStatusElement.textContent = '-';
chartStatusElement.className = 'ml-auto text-xs text-gray-400';
}
}
// 更新进度
const progressElement = document.getElementById('taskProgress');
if (progressElement && taskInfo.progress !== undefined) {
progressElement.textContent = `${taskInfo.progress}%`;
}
// 获取并显示GPU信息如果有
try {
const gpuResponse = await fetchWithTimeout(`${API_BASE}/fine-tune/progress/${taskId}`);
const gpuResult = await gpuResponse.json();
if (gpuResult.code === 0 && gpuResult.data) {
const gpuElement = document.getElementById('taskGPU');
if (gpuElement && gpuResult.data.gpu_info) {
gpuElement.textContent = gpuResult.data.gpu_info;
}
}
} catch (e) {
// GPU信息获取失败静默处理
}
// 更新数据集信息
const datasetElement = document.getElementById('dataset');
if (datasetElement && taskInfo.train_dataset_id) {
try {
const datasetResponse = await fetchWithTimeout(`${API_BASE}/dataset-manage/${taskInfo.train_dataset_id}`);
const datasetResult = await datasetResponse.json();
if (datasetResult.code === 0 && datasetResult.data) {
datasetElement.textContent = datasetResult.data.name;
} else {
datasetElement.textContent = `数据集${taskInfo.train_dataset_id}`;
}
} catch (e) {
datasetElement.textContent = `数据集${taskInfo.train_dataset_id}`;
}
} else if (datasetElement) {
datasetElement.textContent = '-';
}
// 更新最后更新时间
const lastUpdateElement = document.getElementById('lastUpdate');
if (lastUpdateElement && taskInfo.update_time) {
try {
const updateTime = new Date(taskInfo.update_time);
lastUpdateElement.textContent = updateTime.toLocaleString('zh-CN');
} catch (e) {
lastUpdateElement.textContent = taskInfo.update_time || '-';
}
}
// 其他信息
const processIdElement = document.getElementById('processId');
if (processIdElement) {
processIdElement.textContent = taskInfo.process_id || '-';
}
const createTimeElement = document.getElementById('createTime');
if (createTimeElement) {
createTimeElement.textContent = taskInfo.create_time ?
new Date(taskInfo.create_time).toLocaleString('zh-CN') : '-';
}
// 获取模型名称
if (taskInfo.base_model) {
loadModelName(taskInfo.base_model);
}
}
// 加载模型名称
async function loadModelName(modelId) {
const baseModelElement = document.getElementById('baseModel');
if (!baseModelElement) return;
try {
const response = await fetchWithTimeout(`${API_BASE}/model-manage`);
const result = await response.json();
if (result.code === 0 && result.data) {
const model = result.data.find(m => m.id == modelId);
baseModelElement.textContent = model ? model.name : `模型${modelId}`;
}
} catch (e) {
baseModelElement.textContent = `模型${modelId}`;
}
}
// 加载日志内容
async function loadLogContent() {
const logContentElement = document.getElementById('logContent');
// 检查 taskInfo 是否存在
if (!taskInfo) {
// 尝试重新加载任务信息
await loadTaskInfo();
if (!taskInfo) {
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-gray-400">无法获取任务信息</span>';
}
return;
}
}
// 检查 process_id 和 task_name
const processId = taskInfo.process_id;
const taskName = taskInfo.name || '';
if (!processId && !taskName) {
const msg = '<span class="text-gray-400">暂无日志文件 (任务未开始或无进程ID)</span>';
if (logContentElement) {
logContentElement.innerHTML = msg;
}
return;
}
try {
const response = await fetchWithTimeout(`${API_BASE}/training-log-files`);
const result = await response.json();
if (result.code === 0 && result.data) {
// 优先使用进程ID匹配文件名
let selectedFile = null;
if (processId) {
const pidStr = processId.toString();
for (const file of result.data) {
if (file.file.startsWith(pidStr + '_') || file.file.includes(`_${pidStr}_`) || file.file.endsWith(`_${pidStr}.log`)) {
selectedFile = file.file;
break;
}
}
}
// 如果没找到,尝试使用任务名称匹配
if (!selectedFile && taskName) {
for (const file of result.data) {
if (file.file.includes(taskName)) {
selectedFile = file.file;
break;
}
}
}
// 如果仍然没有找到,使用第一个文件
if (!selectedFile && result.data.length > 0) {
selectedFile = result.data[0].file;
}
if (selectedFile) {
await loadLogFileContent(selectedFile);
} else {
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-gray-400">未找到匹配的日志文件</span>';
}
}
} else {
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-gray-400">获取日志列表失败: ' + (result.message || '未知错误') + '</span>';
}
}
} catch (error) {
console.error('[Log] 获取日志列表失败:', error);
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-red-500">加载日志失败: ' + error.message + '</span>';
}
}
}
// 加载日志文件内容
async function loadLogFileContent(fileName) {
const logContentElement = document.getElementById('logContent');
try {
const response = await fetchWithTimeout(`${API_BASE}/training-log-content?file=${encodeURIComponent(fileName)}`);
const result = await response.json();
if (result.code === 0 && result.data) {
trainingLogFullContent = result.data.content || '';
renderLogContent();
// 解析并更新图表
parseMetricsFromLog(trainingLogFullContent);
// 解析并更新训练总结
const taskStatus = taskInfo ? taskInfo.status : 'running';
parseTrainSummary(trainingLogFullContent, taskStatus);
} else if (result.code === 2) {
// 文件被锁定,正在训练中
if (logContentElement) {
logContentElement.innerHTML = `
<div class="text-orange-500 p-4 text-center">
<i class="fa fa-spinner fa-spin fa-2x mb-2"></i>
<p class="text-lg">日志文件正在被训练进程占用</p>
<p class="text-sm text-gray-500 mt-1">${result.message || '训练结束后可查看完整内容'}</p>
<p class="text-xs text-gray-400 mt-2">页面将自动刷新...</p>
</div>
`;
}
} else {
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-red-500">加载日志失败: ' + (result.message || '未知错误') + '</span>';
}
}
} catch (error) {
console.error('[Log] 获取日志内容失败:', error);
if (logContentElement) {
logContentElement.innerHTML = '<span class="text-red-500">加载日志失败: ' + error.message + '</span>';
}
}
}
// 渲染日志内容
function renderLogContent() {
const logContent = document.getElementById('logContent');
if (!logContent) return;
const searchInput = document.getElementById('logSearchInput');
const searchText = searchInput ? searchInput.value.toLowerCase() : '';
if (!trainingLogFullContent) {
logContent.innerHTML = '<span class="text-gray-400">暂无日志内容</span>';
return;
}
const lines = trainingLogFullContent.split('\n');
let html = '';
let matchCount = 0;
// 只显示最后500行以提高性能
const displayLines = lines.slice(-500);
for (const line of displayLines) {
if (!line.trim()) continue;
// 搜索过滤
if (searchText && !line.toLowerCase().includes(searchText)) {
continue;
}
// 级别过滤(不再使用)
let cssClass = '';
if (line.includes('[ERROR') || line.includes('error:') || line.includes('Error:')) {
cssClass = 'error';
} else if (line.includes('[WARNING') || line.includes('warning:') || line.includes('Warning:')) {
cssClass = 'warning';
} else if (line.includes('[INFO') || line.includes('info:') || line.includes('Info:')) {
cssClass = 'info';
}
// 进度条格式高亮
if (/\d+%/.test(line)) {
cssClass = cssClass ? cssClass + ' progress' : 'progress';
}
html += `<div class="log-line ${cssClass}">${escapeHtml(line)}</div>`;
matchCount++;
}
if (matchCount === 0) {
html = '<div class="text-gray-400 p-4">没有匹配的日志</div>';
}
logContent.innerHTML = html;
logContent.scrollTop = logContent.scrollHeight;
// 更新匹配数量
document.getElementById('logMatchCount').textContent =
searchText ? `找到 ${matchCount}` : '';
}
// 搜索日志
function searchLog() {
renderLogContent();
}
// HTML转义
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
// 页面加载完成后初始化
function startApp() {
// 等待 Chart.js 加载完成最多等待5秒
let waitCount = 0;
const maxWait = 50; // 50 * 100ms = 5秒
function waitForChart() {
if (typeof Chart !== 'undefined') {
initCharts();
init();
} else if (waitCount < maxWait) {
waitCount++;
setTimeout(waitForChart, 100);
} else {
document.getElementById('chartsContainer').innerHTML = '<div class="text-center p-4 text-red-500"><i class="fa fa-exclamation-triangle mr-2"></i>图表库加载失败,请检查网络或刷新页面</div>';
// 仍然初始化其他功能
init();
}
}
// 如果已加载,直接初始化;否则等待
if (typeof Chart !== 'undefined') {
initCharts();
init();
} else {
setTimeout(waitForChart, 100);
}
}
// TensorBoard 控制
const TB_URL = 'http://10.10.10.177:6006';
function toggleTB() {
const btn = document.getElementById('tbBtn');
btn.innerHTML = '<i class="fa fa-spinner fa-spin mr-1"></i>启动中...';
btn.className = 'bg-gray-500 text-white px-4 py-2 rounded transition-colors text-sm cursor-wait';
// 调用API启动TensorBoard服务
fetch(`${API_BASE}/fine-tune/tensorboard/start`, { method: 'POST' })
.then(res => res.json())
.then(result => {
if (result.code === 0) {
// 跳转到TensorBoard页面
window.open(TB_URL, '_blank');
btn.innerHTML = '<i class="fa fa-bar-chart mr-1"></i>打开TensorBoard';
btn.className = 'bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 transition-colors text-sm';
} else {
alert('提示: ' + (result.message || '启动失败'));
btn.innerHTML = '<i class="fa fa-bar-chart mr-1"></i>TensorBoard';
btn.className = 'bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 transition-colors text-sm';
}
})
.catch(err => {
alert('提示: 启动失败 - ' + err.message);
btn.innerHTML = '<i class="fa fa-bar-chart mr-1"></i>TensorBoard';
btn.className = 'bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600 transition-colors text-sm';
});
}
// 立即尝试初始化(处理 iframe 情况)
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', startApp);
} else {
startApp();
}
</script>
</body>
</html>