minimind/scripts/static/js/script.js

1179 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

// 标签切换功能
function openTab(evt, tabName) {
var i, tabContent, tabLinks;
tabContent = document.getElementsByClassName("tab-content");
for (i = 0; i < tabContent.length; i++) {
tabContent[i].classList.add("hidden");
}
tabLinks = document.getElementsByClassName("tab");
for (i = 0; i < tabLinks.length; i++) {
tabLinks[i].classList.remove("active");
}
document.getElementById(tabName).classList.remove("hidden");
evt.currentTarget.classList.add("active");
// 当切换到其他标签页时,清除所有日志自动更新定时器
if (tabName !== 'processes') {
for (let processId in logTimers) {
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
}
// 如果切换到进程页面,刷新进程列表
if (tabName === 'processes') {
loadProcesses();
} else if (tabName === 'logfiles') {
loadLogFiles();
}
}
// 根据训练类型显示/隐藏特定参数
document.getElementById('train_type').addEventListener('change', function() {
const trainType = this.value;
const pretrainSftFields = document.querySelectorAll('.pretrain-sft');
const loraFields = document.querySelectorAll('.lora');
const dpoFields = document.querySelectorAll('.dpo');
const dpoParamCard = document.querySelector('.parameter-card.dpo');
pretrainSftFields.forEach(field => {
field.style.display = (trainType === 'pretrain' || trainType === 'sft') ? 'block' : 'none';
});
loraFields.forEach(field => {
field.style.display = trainType === 'lora' ? 'block' : 'none';
});
dpoFields.forEach(field => {
field.style.display = trainType === 'dpo' ? 'block' : 'none';
});
if (dpoParamCard) {
dpoParamCard.style.display = trainType === 'dpo' ? 'block' : 'none';
}
// 设置默认值
if (trainType === 'pretrain') {
document.getElementById('save_dir').value = '../out';
document.getElementById('save_weight').value = 'pretrain';
document.getElementById('epochs').value = '1';
document.getElementById('batch_size').value = '32';
document.getElementById('learning_rate').value = '5e-4';
document.getElementById('data_path').value = '../dataset/pretrain_hq.jsonl';
document.getElementById('from_weight').value = 'none';
document.getElementById('log_interval').value = '100';
document.getElementById('save_interval').value = '100';
} else if (trainType === 'sft') {
document.getElementById('save_dir').value = '../out';
document.getElementById('save_weight').value = 'full_sft';
document.getElementById('epochs').value = '2';
document.getElementById('batch_size').value = '16';
document.getElementById('learning_rate').value = '5e-7';
document.getElementById('data_path').value = '../dataset/sft_mini_512.jsonl';
document.getElementById('from_weight').value = 'pretrain';
document.getElementById('log_interval').value = '100';
document.getElementById('save_interval').value = '100';
} else if (trainType === 'lora') {
document.getElementById('save_dir').value = '../out/lora';
document.getElementById('lora_name').value = 'lora_identity';
document.getElementById('epochs').value = '50';
document.getElementById('batch_size').value = '32';
document.getElementById('learning_rate').value = '1e-4';
document.getElementById('data_path').value = '../dataset/lora_identity.jsonl';
document.getElementById('from_weight').value = 'full_sft';
document.getElementById('log_interval').value = '10';
document.getElementById('save_interval').value = '1';
} else if (trainType === 'dpo') {
document.getElementById('save_dir').value = '../out';
document.getElementById('save_weight').value = 'dpo';
document.getElementById('epochs').value = '1';
document.getElementById('batch_size').value = '4';
document.getElementById('learning_rate').value = '4e-8';
document.getElementById('data_path').value = '../dataset/dpo.jsonl';
document.getElementById('from_weight').value = 'full_sft';
document.getElementById('log_interval').value = '100';
document.getElementById('save_interval').value = '100';
document.getElementById('beta').value = '0.1';
}
});
// 初始触发一次change事件以设置默认值
document.getElementById('train_type').dispatchEvent(new Event('change'));
// 页面加载时启动进程状态轮询
window.addEventListener('load', () => {
startProcessPolling();
// 加载初始进程列表
loadProcesses();
});
// 在进程标签页切换时也触发一次检查
const tabLinks = document.querySelectorAll('.tab');
tabLinks.forEach(tab => {
tab.addEventListener('click', () => {
// 给标签页切换一些时间完成
setTimeout(() => {
const processesTab = document.querySelector('.tab.active');
if (processesTab && processesTab.textContent.includes('进程')) {
checkProcessStatusChanges();
}
}, 100);
});
});
// 添加定时轮询机制,每5秒检查一次进程状态变化
let processPollingTimer;
// 开始进程状态轮询
function startProcessPolling() {
// 清除可能存在的旧定时器
if (processPollingTimer) {
clearInterval(processPollingTimer);
}
// 每5秒检查一次进程状态
processPollingTimer = setInterval(() => {
// 只有在进程标签页打开时才检查
const processesTab = document.querySelector('.tab.active');
if (processesTab && processesTab.textContent.includes('进程')) {
checkProcessStatusChanges();
}
}, 5000);
}
// 检查进程状态变化(特别是错误状态)
function checkProcessStatusChanges() {
fetch('/processes')
.then(response => response.json())
.then(data => {
// 遍历每个进程,检查状态变化
data.forEach(process => {
const processItem = document.querySelector(`[data-process-id="${process.id}"]`);
if (processItem) {
const currentStatus = processItem.dataset.processStatus;
const newStatus = process.status;
// 如果状态发生变化,更新进程项
if (currentStatus !== newStatus) {
updateProcessItem(processItem, process);
// 如果状态变为出错,显示通知
if (newStatus === '出错') {
showNotification(`进程 ${process.train_type} 已出错`, 'error');
}
}
}
});
})
.catch(error => {
console.error('检查进程状态时出错:', error);
});
}
// 更新单个进程项
function updateProcessItem(processItem, process) {
// 更新数据属性
processItem.dataset.processStatus = process.status;
// 更新状态类和文本
const statusElement = processItem.querySelector('.process-status');
if (statusElement) {
// 移除所有状态类
statusElement.classList.remove('status-running', 'status-manual-stop', 'status-error', 'status-completed');
// 添加新的状态类
let statusClass = '';
if (process.status === '运行中') {
statusClass = 'status-running';
} else if (process.status === '手动停止') {
statusClass = 'status-manual-stop';
} else if (process.status === '出错') {
statusClass = 'status-error';
} else {
statusClass = 'status-completed';
}
statusElement.classList.add(statusClass);
statusElement.textContent = process.status;
}
// 更新停止按钮
const stopButton = processItem.querySelector('.btn-stop');
if (stopButton) {
if (!process.running) {
stopButton.remove();
}
} else if (process.running) {
// 如果按钮不存在但进程仍在运行,添加停止按钮
const buttonContainer = processItem.querySelector('div:last-child');
if (buttonContainer) {
const newStopButton = document.createElement('button');
newStopButton.className = 'btn-stop';
newStopButton.onclick = () => stopProcess(process.id);
newStopButton.textContent = '停止训练';
buttonContainer.appendChild(newStopButton);
}
}
// 处理删除按钮
const deleteButton = processItem.querySelector('.btn-delete');
if (!process.running) {
// 非运行中状态应该有删除按钮
if (!deleteButton) {
const buttonContainer = processItem.querySelector('div:last-child');
if (buttonContainer) {
const newDeleteButton = document.createElement('button');
newDeleteButton.className = 'btn-delete';
newDeleteButton.onclick = () => deleteProcess(process.id);
newDeleteButton.textContent = '删除';
buttonContainer.appendChild(newDeleteButton);
}
}
} else if (deleteButton) {
// 运行中状态不应该有删除按钮
deleteButton.remove();
}
// 如果进程不再运行中,清除日志定时器
if (!process.running && logTimers[process.id]) {
clearInterval(logTimers[process.id]);
delete logTimers[process.id];
}
}
// 加载进程列表
function loadProcesses() {
fetch('/processes')
.then(response => response.json())
.then(data => {
const processList = document.getElementById('process-list');
processList.innerHTML = '';
if (data.length === 0) {
processList.innerHTML = '<p>暂无训练进程</p>';
return;
}
// 按启动时间倒序排序,最新启动的进程显示在前面
data.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
// 按训练类型分组
const groupedProcesses = {};
data.forEach(process => {
if (!groupedProcesses[process.train_type]) {
groupedProcesses[process.train_type] = [];
}
groupedProcesses[process.train_type].push(process);
});
// 定义训练类型的显示顺序(可以根据需要调整)
const trainTypeOrder = ['pretrain', 'sft', 'lora', 'dpo'];
// 创建类型分组并添加收起/展开功能的辅助函数
function createTypeGroupWithToggle(trainType, processes) {
// 创建类型分组容器
const typeGroup = document.createElement('div');
typeGroup.className = 'process-type-group';
// 创建标题容器(包含标题文本和切换按钮)
const titleContainer = document.createElement('div');
titleContainer.className = 'process-type-header';
titleContainer.dataset.expanded = 'true'; // 默认展开状态
// 添加类型标题
const typeTitle = document.createElement('h3');
typeTitle.className = 'process-type-title';
typeTitle.textContent = getTrainTypeDisplayName(trainType);
// 添加切换按钮
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-btn';
toggleButton.innerHTML = '▼'; // 向下箭头表示展开
toggleButton.onclick = function(e) {
e.stopPropagation(); // 防止触发标题容器的点击事件
toggleGroup(titleContainer);
};
// 将标题和按钮添加到容器
titleContainer.appendChild(typeTitle);
titleContainer.appendChild(toggleButton);
// 添加点击标题也可以切换展开/收起
titleContainer.onclick = function() {
toggleGroup(titleContainer);
};
// 创建内容容器,用于容纳进程项
const contentContainer = document.createElement('div');
contentContainer.className = 'process-type-content';
// 添加该类型的所有进程
processes.forEach(process => {
addProcessItemToGroup(contentContainer, process);
});
// 将标题容器和内容容器添加到类型分组
typeGroup.appendChild(titleContainer);
typeGroup.appendChild(contentContainer);
return typeGroup;
}
// 切换分组展开/收起状态的函数
function toggleGroup(headerElement) {
const isExpanded = headerElement.dataset.expanded === 'true';
const contentContainer = headerElement.nextElementSibling;
const toggleButton = headerElement.querySelector('.toggle-btn');
if (isExpanded) {
// 收起分组
headerElement.dataset.expanded = 'false';
contentContainer.style.maxHeight = '0';
contentContainer.style.overflow = 'hidden';
toggleButton.innerHTML = '▶'; // 向右箭头表示收起
} else {
// 展开分组前先重置样式
contentContainer.style.overflow = 'hidden'; // 确保计算高度准确
contentContainer.style.maxHeight = 'none'; // 临时设置为none以获取真实高度
// 获取真实高度
const actualHeight = contentContainer.scrollHeight;
// 然后先设置为0,准备动画
contentContainer.style.maxHeight = '0';
// 强制重排
contentContainer.offsetHeight;
// 展开分组
headerElement.dataset.expanded = 'true';
contentContainer.style.maxHeight = actualHeight + 'px';
setTimeout(() => {
// 动画完成后,设置为真实高度并允许溢出
contentContainer.style.maxHeight = 'none';
contentContainer.style.overflow = 'visible';
}, 300); // 动画完成后显示溢出内容
toggleButton.innerHTML = '▼'; // 向下箭头表示展开
}
}
// 首先显示有明确顺序的训练类型
trainTypeOrder.forEach(trainType => {
if (groupedProcesses[trainType]) {
const typeGroup = createTypeGroupWithToggle(trainType, groupedProcesses[trainType]);
processList.appendChild(typeGroup);
// 从分组中移除已处理的类型
delete groupedProcesses[trainType];
}
});
// 显示剩余的训练类型(不在预定义顺序中的)
Object.keys(groupedProcesses).forEach(trainType => {
const typeGroup = createTypeGroupWithToggle(trainType, groupedProcesses[trainType]);
processList.appendChild(typeGroup);
});
});
}
// 获取训练类型的显示名称
function getTrainTypeDisplayName(trainType) {
const typeNames = {
'pretrain': '预训练 (Pretrain)',
'sft': '全参数监督微调 (SFT - Full)',
'lora': 'LoRA监督微调 (SFT - Lora)',
'dpo': '直接偏好优化 (RL - DPO)'
};
return typeNames[trainType] || trainType;
}
// 添加进程项到分组
function addProcessItemToGroup(parentElement, process) {
const processItem = document.createElement('div');
processItem.className = 'process-item';
let statusClass = '';
// 根据后端返回的status字段设置状态类
if (process.status === '运行中') {
statusClass = 'status-running';
} else if (process.status === '手动停止') {
statusClass = 'status-manual-stop';
} else if (process.status === '出错') {
statusClass = 'status-error';
} else {
statusClass = 'status-completed';
}
// 设置进程数据属性,用于后续检查状态
processItem.dataset.processId = process.id;
processItem.dataset.processStatus = process.status;
// 检查是否显示删除按钮(对于非运行中的进程)
const showDeleteButton = !process.running;
processItem.innerHTML = `
<div class="process-info">
<div>
<strong>${process.start_time}</strong>
</div>
<div>
<span class="process-status ${statusClass}">${process.status}</span>
</div>
</div>
<div>
<button class="btn-logs" onclick="showLogs('${process.id}')">查看日志</button>
<button class="btn-logs" onclick="refreshLog('${process.id}')">刷新日志</button>
${process.running ? `<button class="btn-stop" onclick="stopProcess('${process.id}')">停止训练</button>` : ''}
${showDeleteButton ? `<button class="btn-delete" onclick="deleteProcess('${process.id}')">删除</button>` : ''}
</div>
<div id="logs-${process.id}" class="logs-container hidden"></div>
`;
parentElement.appendChild(processItem);
}
// 存储训练日志定时器的ID
let logTimers = {};
// 显示日志
function showLogs(processId) {
const logsContainer = document.getElementById(`logs-${processId}`);
logsContainer.classList.toggle('hidden');
if (!logsContainer.classList.contains('hidden')) {
// 加载日志内容
loadLogContent(processId, logsContainer);
// 清除可能存在的旧定时器
if (logTimers[processId]) {
clearInterval(logTimers[processId]);
}
// 查找进程元素,获取其状态
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
const isRunning = processItem && processItem.dataset.processStatus === '运行中';
// 只有运行中的进程才设置自动刷新定时器
if (isRunning) {
logTimers[processId] = setInterval(() => {
// 检查日志容器是否可见
if (!logsContainer.classList.contains('hidden')) {
// 再次检查进程状态,确保仍然是运行中
const currentProcessItem = document.querySelector(`[data-process-id="${processId}"]`);
const stillRunning = currentProcessItem && currentProcessItem.dataset.processStatus === '运行中';
if (stillRunning) {
loadLogContent(processId, logsContainer);
} else {
// 如果进程不再运行中,清除定时器
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
}
}, 1000);
}
} else {
// 隐藏日志时清除定时器
if (logTimers[processId]) {
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
}
}
// 刷新日志
function refreshLog(processId) {
const logsContainer = document.getElementById(`logs-${processId}`);
if (!logsContainer.classList.contains('hidden')) {
loadLogContent(processId, logsContainer);
// 查找进程元素,获取其状态
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
const isRunning = processItem && processItem.dataset.processStatus === '运行中';
// 重置定时器,确保从现在开始每1秒更新一次
if (logTimers[processId]) {
clearInterval(logTimers[processId]);
}
// 只有运行中的进程才设置定时器
if (isRunning) {
logTimers[processId] = setInterval(() => {
if (!logsContainer.classList.contains('hidden')) {
// 再次检查进程状态
const currentProcessItem = document.querySelector(`[data-process-id="${processId}"]`);
const stillRunning = currentProcessItem && currentProcessItem.dataset.processStatus === '运行中';
if (stillRunning) {
loadLogContent(processId, logsContainer);
} else {
// 如果进程不再运行中,清除定时器
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
}
}, 1000);
}
}
}
// 加载日志内容
function loadLogContent(processId, logsContainer) {
fetch(`/logs/${processId}`)
.then(response => response.text())
.then(logs => {
logsContainer.textContent = logs;
logsContainer.scrollTop = logsContainer.scrollHeight;
});
}
// 显示右上角消息弹窗
function showNotification(message, type = 'success') {
// 创建通知元素
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.textContent = message;
// 添加到页面
document.body.appendChild(notification);
// 显示动画
setTimeout(() => {
notification.classList.add('show');
}, 10);
// 自动关闭
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
document.body.removeChild(notification);
}, 300);
}, 3000);
}
// 自定义确认对话框
function showConfirmDialog(message, onConfirm, onCancel = null) {
// 检查是否已存在对话框,如果有则移除
const existingDialog = document.querySelector('.custom-dialog');
if (existingDialog) {
document.body.removeChild(existingDialog);
}
// 创建对话框遮罩
const dialogOverlay = document.createElement('div');
dialogOverlay.className = 'dialog-overlay';
// 创建对话框容器
const dialogContainer = document.createElement('div');
dialogContainer.className = 'custom-dialog';
// 设置对话框内容
dialogContainer.innerHTML = `
<div class="dialog-content">
<div class="dialog-message">${message}</div>
<div class="dialog-actions">
<button class="dialog-button dialog-cancel">取消</button>
<button class="dialog-button dialog-confirm">确认</button>
</div>
</div>
`;
// 添加到页面
dialogOverlay.appendChild(dialogContainer);
document.body.appendChild(dialogOverlay);
// 添加动画类
setTimeout(() => {
dialogOverlay.classList.add('show');
dialogContainer.classList.add('show');
}, 10);
// 绑定确认按钮事件
const confirmBtn = dialogContainer.querySelector('.dialog-confirm');
confirmBtn.addEventListener('click', () => {
if (onConfirm) onConfirm();
closeDialog(dialogOverlay);
});
// 绑定取消按钮事件
const cancelBtn = dialogContainer.querySelector('.dialog-cancel');
cancelBtn.addEventListener('click', () => {
if (onCancel) onCancel();
closeDialog(dialogOverlay);
});
// 点击遮罩层也可以取消
dialogOverlay.addEventListener('click', (e) => {
if (e.target === dialogOverlay) {
if (onCancel) onCancel();
closeDialog(dialogOverlay);
}
});
}
// 关闭对话框函数
function closeDialog(dialogOverlay) {
dialogOverlay.classList.remove('show');
const dialogContainer = dialogOverlay.querySelector('.custom-dialog');
dialogContainer.classList.remove('show');
setTimeout(() => {
if (dialogOverlay.parentNode) {
document.body.removeChild(dialogOverlay);
}
}, 300);
}
// 删除进程
function deleteProcess(processId) {
showConfirmDialog(
'确定要删除这个训练进程吗?此操作不可恢复。',
() => {
// 确认删除
fetch(`/delete/${processId}`, {
method: 'POST'
})
.then(response => {
// 检查响应状态
if (!response.ok) {
throw new Error('删除请求失败');
}
return response.json().catch(() => ({})); // 即使没有JSON响应也继续
})
.then(() => {
// 从UI中移除进程项
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
if (processItem) {
// 添加移除动画
processItem.style.transition = 'opacity 0.3s, transform 0.3s';
processItem.style.opacity = '0';
processItem.style.transform = 'translateX(-20px)';
// 动画完成后移除
setTimeout(() => {
// 获取内容容器和类型分组
const contentContainer = processItem.closest('.process-type-content');
const typeGroup = contentContainer ? contentContainer.closest('.process-type-group') : null;
if (processItem.parentNode) {
processItem.parentNode.removeChild(processItem);
}
// 检查内容容器中是否还有进程项
if (contentContainer) {
const remainingProcessesInGroup = contentContainer.querySelectorAll('.process-item');
// 如果内容容器中没有进程项了,移除整个分组
if (remainingProcessesInGroup.length === 0 && typeGroup) {
setTimeout(() => {
// 添加分组移除动画
typeGroup.style.transition = 'opacity 0.3s, transform 0.3s';
typeGroup.style.opacity = '0';
typeGroup.style.transform = 'translateY(-10px)';
setTimeout(() => {
if (typeGroup.parentNode) {
typeGroup.parentNode.removeChild(typeGroup);
}
// 检查是否还有进程项
const remainingProcesses = document.querySelectorAll('.process-item');
if (remainingProcesses.length === 0) {
const processList = document.getElementById('process-list');
processList.innerHTML = '<p>暂无训练进程</p>';
}
}, 300);
}, 100);
} else {
// 重新计算内容容器的高度(如果分组是展开的)
const headerElement = contentContainer.previousElementSibling;
if (headerElement && headerElement.dataset.expanded === 'true') {
contentContainer.style.maxHeight = contentContainer.scrollHeight + 'px';
}
// 检查是否还有进程项
const remainingProcesses = document.querySelectorAll('.process-item');
if (remainingProcesses.length === 0) {
const processList = document.getElementById('process-list');
processList.innerHTML = '<p>暂无训练进程</p>';
}
}
} else {
// 检查是否还有进程项
const remainingProcesses = document.querySelectorAll('.process-item');
if (remainingProcesses.length === 0) {
const processList = document.getElementById('process-list');
processList.innerHTML = '<p>暂无训练进程</p>';
}
}
}, 300);
}
// 清除日志定时器
if (logTimers[processId]) {
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
showNotification('训练进程已删除', 'success');
})
.catch(error => {
console.error('删除进程时出错:', error);
showNotification('删除进程失败,请刷新页面重试', 'error');
});
}
);
}
// 停止进程
function stopProcess(processId) {
showConfirmDialog(
'确定要停止这个训练进程吗?',
() => {
// 确认停止
fetch(`/stop/${processId}`, {
method: 'POST'
})
.then(() => {
// 立即给用户反馈,设置为手动停止状态
const processItem = document.querySelector(`[data-process-id="${processId}"]`);
if (processItem) {
// 先设置为手动停止状态(即时反馈)
processItem.dataset.processStatus = '手动停止';
const statusElement = processItem.querySelector('.process-status');
if (statusElement) {
statusElement.classList.remove('status-running', 'status-error', 'status-completed');
statusElement.classList.add('status-manual-stop');
statusElement.textContent = '手动停止';
}
// 移除停止按钮
const stopButton = processItem.querySelector('.btn-stop');
if (stopButton) {
stopButton.remove();
}
// 清除日志定时器
if (logTimers[processId]) {
clearInterval(logTimers[processId]);
delete logTimers[processId];
}
}
// 显示通知
showNotification('训练进程已停止', 'info');
// 为了确保状态准确,仍然获取完整列表并更新单个进程
fetch('/processes')
.then(response => response.json())
.then(data => {
// 从完整列表中找到特定进程
const updatedProcess = data.find(p => p.id === processId);
if (updatedProcess && processItem) {
// 使用updateProcessItem函数更新进程(如果需要的话)
updateProcessItem(processItem, updatedProcess);
}
})
.catch(error => {
console.error('获取进程列表更新单个进程失败:', error);
});
})
.catch(() => {
showNotification('停止进程失败', 'error');
});
},
() => {
// 取消停止,可选的回调
showNotification('已取消停止操作', 'info');
}
);
}
// 表单提交处理
document.getElementById('train-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = Object.fromEntries(formData.entries());
// 立即显示加载通知
showNotification('正在启动训练...', 'info');
// 延迟1秒后发送请求,确保两个通知之间有间隔
setTimeout(() => {
fetch('/train', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
if (!response.ok) {
throw new Error('网络响应异常');
}
return response.json();
})
.then(result => {
if (result.success) {
showNotification('训练已开始!', 'success');
// 延迟1秒后切换到进程标签页,确保进程信息已更新
setTimeout(() => {
// 获取进程标签按钮并触发点击
const processTab = document.querySelector('.tab[onclick*="processes"]');
if (processTab) {
processTab.click();
}
}, 1000);
} else {
// 显示错误通知
showNotification('训练启动失败:' + result.error, 'error');
}
})
.catch(error => {
// 显示等待通知
showNotification('启动训练中,请耐心等待...', 'info');
});
}, 1000); // 1秒延迟确保两个通知之间有间隔
});
// 加载日志文件列表
function loadLogFiles() {
fetch('/logfiles')
.then(response => response.json())
.then(data => {
const logfilesList = document.getElementById('logfiles-list');
logfilesList.innerHTML = '';
if (data.length === 0) {
logfilesList.innerHTML = '<p>暂无日志文件</p>';
return;
}
// 按日期倒序排序
data.sort((a, b) => new Date(b.modified_time) - new Date(a.modified_time));
// 按训练类型分组
const groupedLogFiles = {};
data.forEach(logfile => {
// 从文件名提取训练类型
let trainType = '自定义训练';
if (logfile.filename.includes('train_pretrain_')) {
trainType = 'pretrain';
} else if (logfile.filename.includes('train_sft_')) {
trainType = 'sft';
} else if (logfile.filename.includes('train_lora_')) {
trainType = 'lora';
} else if (logfile.filename.includes('train_dpo_')) {
trainType = 'dpo';
} else if (logfile.filename.includes('train_ppo_')) {
trainType = 'ppo';
} else if (logfile.filename.includes('train_grpo_')) {
trainType = 'grpo';
} else if (logfile.filename.includes('train_spo_')) {
trainType = 'spo';
}
// 为日志文件添加训练类型信息
logfile.train_type = trainType;
if (!groupedLogFiles[trainType]) {
groupedLogFiles[trainType] = [];
}
groupedLogFiles[trainType].push(logfile);
});
// 定义训练类型的显示顺序(与进程显示顺序保持一致)
const trainTypeOrder = ['pretrain', 'sft', 'lora', 'dpo', 'ppo', 'grpo', 'spo', '未知'];
// 创建类型分组并添加收起/展开功能的辅助函数
function createTypeGroupWithToggle(trainType, logfiles) {
// 创建类型分组容器
const typeGroup = document.createElement('div');
typeGroup.className = 'process-type-group';
// 创建标题容器(包含标题文本和切换按钮)
const titleContainer = document.createElement('div');
titleContainer.className = 'process-type-header';
titleContainer.dataset.expanded = 'true'; // 默认展开状态
// 添加类型标题
const typeTitle = document.createElement('h3');
typeTitle.className = 'process-type-title';
typeTitle.textContent = getTrainTypeDisplayName(trainType);
// 添加切换按钮
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-btn';
toggleButton.innerHTML = '▼'; // 向下箭头表示展开
toggleButton.onclick = function(e) {
e.stopPropagation(); // 防止触发标题容器的点击事件
toggleGroup(titleContainer);
};
// 将标题和按钮添加到容器
titleContainer.appendChild(typeTitle);
titleContainer.appendChild(toggleButton);
// 添加点击标题也可以切换展开/收起
titleContainer.onclick = function() {
toggleGroup(titleContainer);
};
// 创建内容容器,用于容纳日志文件项
const contentContainer = document.createElement('div');
contentContainer.className = 'process-type-content';
// 添加该类型的所有日志文件
logfiles.forEach(logfile => {
addLogFileItemToGroup(contentContainer, logfile);
});
// 将标题容器和内容容器添加到类型分组
typeGroup.appendChild(titleContainer);
typeGroup.appendChild(contentContainer);
return typeGroup;
}
// 切换分组展开/收起状态的函数
function toggleGroup(headerElement) {
const isExpanded = headerElement.dataset.expanded === 'true';
const contentContainer = headerElement.nextElementSibling;
const toggleButton = headerElement.querySelector('.toggle-btn');
if (isExpanded) {
// 收起分组
headerElement.dataset.expanded = 'false';
contentContainer.style.maxHeight = '0';
contentContainer.style.overflow = 'hidden';
toggleButton.innerHTML = '▶'; // 向右箭头表示收起
} else {
// 展开分组前先重置样式
contentContainer.style.overflow = 'hidden'; // 确保计算高度准确
contentContainer.style.maxHeight = 'none'; // 临时设置为none以获取真实高度
// 获取真实高度
const actualHeight = contentContainer.scrollHeight;
// 然后先设置为0,准备动画
contentContainer.style.maxHeight = '0';
// 强制重排
contentContainer.offsetHeight;
// 展开分组
headerElement.dataset.expanded = 'true';
contentContainer.style.maxHeight = actualHeight + 'px';
setTimeout(() => {
// 动画完成后,设置为真实高度并允许溢出
contentContainer.style.maxHeight = 'none';
contentContainer.style.overflow = 'visible';
}, 300); // 动画完成后显示溢出内容
toggleButton.innerHTML = '▼'; // 向下箭头表示展开
}
}
// 首先显示有明确顺序的训练类型
trainTypeOrder.forEach(trainType => {
if (groupedLogFiles[trainType]) {
const typeGroup = createTypeGroupWithToggle(trainType, groupedLogFiles[trainType]);
logfilesList.appendChild(typeGroup);
// 从分组中移除已处理的类型
delete groupedLogFiles[trainType];
}
});
// 显示剩余的训练类型(不在预定义顺序中的)
Object.keys(groupedLogFiles).forEach(trainType => {
const typeGroup = createTypeGroupWithToggle(trainType, groupedLogFiles[trainType]);
logfilesList.appendChild(typeGroup);
});
});
}
// 添加日志文件项到分组
function addLogFileItemToGroup(parentElement, logfile) {
const fileItem = document.createElement('div');
fileItem.className = 'process-item';
fileItem.innerHTML = `
<div class="process-info">
<div>
<strong>${logfile.filename}</strong>
</div>
<div>
<span class="process-status status-completed">已保存</span>
<span style="margin-left: 10px; color: #999; font-size: 0.9em;">${logfile.modified_time}</span>
</div>
</div>
<div>
<button class="btn-logs" onclick="viewLogFile('${logfile.filename}', this)">查看日志</button>
<button class="btn-delete" onclick="deleteLogFile('${logfile.filename}', this)">删除</button>
</div>
<div id="log-content-${logfile.filename.replace(/\./g, '-')}" class="logs-container hidden"></div>
`;
parentElement.appendChild(fileItem);
}
// 删除日志文件
function deleteLogFile(filename, button) {
// 显示确认对话框
showConfirmDialog(
`确定要删除日志文件 "${filename}" 吗?此操作无法恢复。`,
function() {
// 用户确认删除
const processItem = button.closest('.process-item');
const contentContainer = processItem.closest('.process-type-content');
const typeGroup = contentContainer.closest('.process-type-group');
// 显示加载状态
const originalText = button.textContent;
button.textContent = '删除中...';
button.disabled = true;
// 发送删除请求到服务器
fetch(`/delete-logfile/${encodeURIComponent(filename)}`, {
method: 'DELETE'
})
.then(response => {
if (!response.ok) {
throw new Error('删除失败');
}
return response.json();
})
.then(data => {
if (data.success) {
// 从UI中移除日志项
processItem.remove();
// 检查是否需要移除整个分组
if (contentContainer.children.length === 0) {
typeGroup.remove();
} else {
// 更新内容容器的高度
const headerElement = contentContainer.previousElementSibling;
if (headerElement && headerElement.dataset.expanded === 'true') {
// 临时设置为none以获取真实高度
contentContainer.style.maxHeight = 'none';
const actualHeight = contentContainer.scrollHeight;
contentContainer.style.maxHeight = actualHeight + 'px';
}
}
// 显示成功通知
showNotification(`日志文件 "${filename}" 已成功删除`);
} else {
throw new Error(data.message || '删除失败');
}
})
.catch(error => {
console.error('删除日志失败:', error);
showNotification(`删除失败: ${error.message}`, 'error');
// 恢复按钮状态
button.textContent = originalText;
button.disabled = false;
});
},
function() {
// 用户取消删除,不执行任何操作
}
);
}
// 查看日志文件内容
function viewLogFile(filename, button) {
// 生成安全的文件名,用于元素ID
const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, '_');
const idSafeFilename = safeFilename.replace(/\./g, '-');
// 查找对应的日志容器
const processItem = button.closest('.process-item');
const logContainer = processItem.querySelector(`#log-content-${idSafeFilename}`);
// 获取内容容器和头部元素
const contentContainer = processItem.closest('.process-type-content');
const headerElement = contentContainer ? contentContainer.previousElementSibling : null;
// 确保日志所在的分组是展开状态
if (contentContainer && headerElement && headerElement.dataset.expanded !== 'true') {
// 如果分组是折叠的,先展开分组
toggleGroup(headerElement);
}
// 切换日志容器的显示状态
if (logContainer.classList.contains('hidden')) {
logContainer.classList.remove('hidden');
// 如果是首次加载,获取日志内容
if (logContainer.textContent.trim() === '') {
logContainer.textContent = '加载中...';
fetch(`/logfile-content/${encodeURIComponent(filename)}`)
.then(response => {
if (!response.ok) {
throw new Error('获取日志失败');
}
return response.text();
})
.then(logs => {
// 保留空白格式,使用textContent而不是innerHTML
logContainer.textContent = logs;
logContainer.scrollTop = 0;
// 更新内容容器的高度
updateContentContainerHeight();
})
.catch(error => {
console.error('获取日志失败:', error);
logContainer.innerHTML = `<p class="error">获取日志失败: ${error.message}</p>`;
// 更新内容容器的高度
updateContentContainerHeight();
});
} else {
// 如果不是首次加载,直接更新内容容器的高度
updateContentContainerHeight();
}
} else {
logContainer.classList.add('hidden');
// 更新内容容器的高度
updateContentContainerHeight();
}
// 辅助函数:更新内容容器的高度
function updateContentContainerHeight() {
if (contentContainer && headerElement && headerElement.dataset.expanded === 'true') {
// 保存当前的maxHeight设置
const currentMaxHeight = contentContainer.style.maxHeight;
// 暂时设置为none以获取真实高度
contentContainer.style.maxHeight = 'none';
// 获取真实高度
const actualHeight = contentContainer.scrollHeight;
// 检查是否需要重新设置高度
if (currentMaxHeight === 'none' || parseInt(currentMaxHeight) !== actualHeight) {
contentContainer.style.maxHeight = actualHeight + 'px';
// 短暂延迟后再设置为none,确保高度变化正确应用
setTimeout(() => {
if (headerElement.dataset.expanded === 'true') {
contentContainer.style.maxHeight = 'none';
}
}, 300);
} else {
// 如果高度没有变化,恢复之前的设置
contentContainer.style.maxHeight = currentMaxHeight;
}
}
}
}