update logfile && process
parent
5bef3f08ab
commit
48a81d99bb
|
|
@ -8,13 +8,28 @@ body {
|
|||
background-color: #121212;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 头部样式 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 60px;
|
||||
margin-right: 20px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.tabs {
|
||||
display: flex;
|
||||
|
|
@ -33,15 +48,23 @@ h1 {
|
|||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
color: #cccccc;
|
||||
color: #888888;
|
||||
width: 30%;
|
||||
text-align: center;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.tab.active {
|
||||
background: linear-gradient(135deg, #4a148c 0%, #8e24aa 100%);
|
||||
background: linear-gradient(135deg, #9c27b0 0%, #e040fb 100%);
|
||||
color: white;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
||||
opacity: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.tab:hover {
|
||||
color: #aaaaaa;
|
||||
opacity: 0.9;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.form-container {
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
|
|
@ -56,12 +79,13 @@ h1 {
|
|||
.parameter-card {
|
||||
background-color: #2d2d2d;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
padding: 24px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
padding-left: 5%;
|
||||
padding-right: 5%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.parameter-card:hover {
|
||||
|
|
@ -71,13 +95,13 @@ h1 {
|
|||
|
||||
/* 卡片标题样式 */
|
||||
.card-title {
|
||||
color: #e0e0e0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: bold;
|
||||
color: #ffffff;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #404040;
|
||||
border-bottom: 1px solid #e040fb;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
|
@ -118,14 +142,15 @@ label {
|
|||
display: block;
|
||||
margin-bottom: 5px;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
opacity: 0.8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
input[type="text"], input[type="number"], select {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -137,7 +162,7 @@ input[type="text"], input[type="number"], select {
|
|||
/* 确保textarea也适应两列布局 */
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px 15px;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #444;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
|
|
@ -200,14 +225,69 @@ button:active {
|
|||
.logs-container:hover {
|
||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.process-type-group {
|
||||
margin-bottom: 30px;
|
||||
background-color: rgba(30, 30, 30, 0.9);
|
||||
border-radius: 15px;
|
||||
border: 1px solid #444;
|
||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 标题容器样式 */
|
||||
.process-type-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.process-type-title {
|
||||
margin: 0;
|
||||
color: #e0e0e0;
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #8e24aa;
|
||||
padding-bottom: 8px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
/* 切换按钮样式 */
|
||||
.toggle-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #e0e0e0;
|
||||
font-size: 0.8em;
|
||||
cursor: pointer;
|
||||
padding: 5px 10px;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.3s, transform 0.2s;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.toggle-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 内容容器样式 */
|
||||
.process-type-content {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
transition: max-height 0.3s ease-in-out;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.process-item {
|
||||
background: rgba(30, 30, 30, 0.9);
|
||||
background: rgba(40, 40, 40, 0.9);
|
||||
padding: 20px;
|
||||
margin-bottom: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
border: 1px solid #444;
|
||||
border: 1px solid #555;
|
||||
}
|
||||
|
||||
.process-item:hover {
|
||||
|
|
@ -261,6 +341,24 @@ button:active {
|
|||
margin-right: 10px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.btn-delete {
|
||||
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
||||
padding: 8px 15px;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
}
|
||||
.btn-delete:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 10px rgba(244, 67, 54, 0.3);
|
||||
}
|
||||
.btn-delete:disabled {
|
||||
background: linear-gradient(135deg, #cccccc 0%, #aaaaaa 100%);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-logs:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 10px rgba(79, 172, 254, 0.3);
|
||||
|
|
@ -270,12 +368,13 @@ button:active {
|
|||
}
|
||||
.section-title {
|
||||
color: #ffffff;
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
margin-bottom: 25px;
|
||||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 3px solid rgba(142, 36, 170, 0.3);
|
||||
border-bottom: 1px solid #e040fb;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 添加滚动条样式 */
|
||||
|
|
@ -309,6 +408,131 @@ button:active {
|
|||
}
|
||||
}
|
||||
|
||||
/* 自定义确认对话框样式 */
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.dialog-overlay.show {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.custom-dialog {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
||||
max-width: 400px;
|
||||
width: 90%;
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
transition: transform 0.3s ease, opacity 0.3s ease;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.custom-dialog.show {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.dialog-message {
|
||||
color: #ffffff;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.dialog-button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.dialog-cancel {
|
||||
background: linear-gradient(135deg, #4a4a4a 0%, #333333 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dialog-cancel:hover {
|
||||
background: linear-gradient(135deg, #5a5a5a 0%, #444444 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.dialog-confirm {
|
||||
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.dialog-confirm:hover {
|
||||
background: linear-gradient(135deg, #7a21db 0%, #3585ff 100%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* 消息弹窗样式 */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 15px 25px;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
transition: all 0.3s ease;
|
||||
max-width: 400px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 显示状态 */
|
||||
.notification.show {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 成功通知样式 */
|
||||
.notification-success {
|
||||
background: linear-gradient(135deg, #4caf50 0%, #81c784 100%);
|
||||
}
|
||||
|
||||
/* 错误通知样式 */
|
||||
.notification-error {
|
||||
background: linear-gradient(135deg, #f44336 0%, #ef5350 100%);
|
||||
}
|
||||
|
||||
/* 信息通知样式 */
|
||||
.notification-info {
|
||||
background: linear-gradient(135deg, #2196f3 0%, #64b5f6 100%);
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
animation: fadeIn 0.5s ease-out;
|
||||
}
|
||||
|
|
@ -319,6 +543,21 @@ button:active {
|
|||
padding: 10px;
|
||||
}
|
||||
|
||||
.header {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 50px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 615 KiB |
|
|
@ -100,6 +100,147 @@ document.getElementById('train_type').addEventListener('change', function() {
|
|||
// 初始触发一次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')
|
||||
|
|
@ -113,7 +254,143 @@ function loadProcesses() {
|
|||
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';
|
||||
|
||||
|
|
@ -133,10 +410,13 @@ function loadProcesses() {
|
|||
processItem.dataset.processId = process.id;
|
||||
processItem.dataset.processStatus = process.status;
|
||||
|
||||
// 检查是否显示删除按钮(对于非运行中的进程)
|
||||
const showDeleteButton = !process.running;
|
||||
|
||||
processItem.innerHTML = `
|
||||
<div class="process-info">
|
||||
<div>
|
||||
<strong>${process.train_type}</strong> - ${process.start_time}
|
||||
<strong>${process.start_time}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="process-status ${statusClass}">${process.status}</span>
|
||||
|
|
@ -146,13 +426,12 @@ function loadProcesses() {
|
|||
<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>
|
||||
`;
|
||||
|
||||
processList.appendChild(processItem);
|
||||
});
|
||||
});
|
||||
parentElement.appendChild(processItem);
|
||||
}
|
||||
|
||||
// 存储训练日志定时器的ID
|
||||
|
|
@ -250,16 +529,266 @@ function loadLogContent(processId, logsContainer) {
|
|||
});
|
||||
}
|
||||
|
||||
// 显示右上角消息弹窗
|
||||
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) {
|
||||
if (confirm('确定要停止这个训练进程吗?')) {
|
||||
showConfirmDialog(
|
||||
'确定要停止这个训练进程吗?',
|
||||
() => {
|
||||
// 确认停止
|
||||
fetch(`/stop/${processId}`, {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(() => {
|
||||
loadProcesses();
|
||||
});
|
||||
// 立即给用户反馈,设置为手动停止状态
|
||||
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');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// 表单提交处理
|
||||
|
|
@ -268,6 +797,11 @@ document.getElementById('train-form').addEventListener('submit', function(e) {
|
|||
const formData = new FormData(this);
|
||||
const data = Object.fromEntries(formData.entries());
|
||||
|
||||
// 立即显示加载通知
|
||||
showNotification('正在启动训练...', 'info');
|
||||
|
||||
// 延迟1秒后发送请求,确保两个通知之间有间隔
|
||||
setTimeout(() => {
|
||||
fetch('/train', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
|
@ -275,15 +809,34 @@ document.getElementById('train-form').addEventListener('submit', function(e) {
|
|||
},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('网络响应异常');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('训练已开始!');
|
||||
openTab(event, 'processes');
|
||||
} else {
|
||||
alert('训练启动失败:' + result.error);
|
||||
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秒延迟确保两个通知之间有间隔
|
||||
});
|
||||
|
||||
// 加载日志文件列表
|
||||
|
|
@ -302,12 +855,11 @@ function loadLogFiles() {
|
|||
// 按日期倒序排序
|
||||
data.sort((a, b) => new Date(b.modified_time) - new Date(a.modified_time));
|
||||
|
||||
// 按训练类型分组
|
||||
const groupedLogFiles = {};
|
||||
data.forEach(logfile => {
|
||||
const fileItem = document.createElement('div');
|
||||
fileItem.className = 'process-item';
|
||||
|
||||
// 从文件名提取训练类型
|
||||
let trainType = '未知';
|
||||
let trainType = '自定义训练';
|
||||
if (logfile.filename.includes('train_pretrain_')) {
|
||||
trainType = 'pretrain';
|
||||
} else if (logfile.filename.includes('train_sft_')) {
|
||||
|
|
@ -324,11 +876,136 @@ function loadLogFiles() {
|
|||
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>${trainType}</strong> - ${logfile.filename}
|
||||
<strong>${logfile.filename}</strong>
|
||||
</div>
|
||||
<div>
|
||||
<span class="process-status status-completed">已保存</span>
|
||||
|
|
@ -337,29 +1014,166 @@ function loadLogFiles() {
|
|||
</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>
|
||||
`;
|
||||
|
||||
logfilesList.appendChild(fileItem);
|
||||
});
|
||||
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) {
|
||||
const safeFilename = filename.replace(/\./g, '-');
|
||||
const logContainer = button.closest('.process-item').querySelector(`#log-content-${safeFilename}`);
|
||||
logContainer.classList.toggle('hidden');
|
||||
// 生成安全的文件名,用于元素ID
|
||||
const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
||||
const idSafeFilename = safeFilename.replace(/\./g, '-');
|
||||
|
||||
if (!logContainer.classList.contains('hidden') && logContainer.textContent === '') {
|
||||
logContainer.textContent = '加载日志中...';
|
||||
// 查找对应的日志容器
|
||||
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 => response.text())
|
||||
.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,10 @@
|
|||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<img src="/static/images/logo2.png" alt="MiniMind Logo" class="logo">
|
||||
<h1>MiniMind Training Lab</h1>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
<button class="tab active" onclick="openTab(event, 'train')">开始训练</button>
|
||||
|
|
@ -26,10 +29,10 @@
|
|||
<div class="form-group">
|
||||
<label for="train_type">训练类型:</label>
|
||||
<select id="train_type" name="train_type" required>
|
||||
<option value="pretrain">预训练 (Pretrain)</option>
|
||||
<option value="sft">监督微调 (Full SFT)</option>
|
||||
<option value="lora">LoRA 微调</option>
|
||||
<option value="dpo">强化学习 (RL-DPO)</option>
|
||||
<option value="pretrain">Pretrain</option>
|
||||
<option value="sft">SFT - Full</option>
|
||||
<option value="lora">SFT - Lora</option>
|
||||
<option value="dpo">RL - DPO</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
|
|
@ -238,6 +238,28 @@ def get_logfile_content(filename):
|
|||
return f'读取日志失败: {str(e)}'
|
||||
return '日志文件不存在'
|
||||
|
||||
@app.route('/delete-logfile/<filename>', methods=['DELETE'])
|
||||
def delete_logfile(filename):
|
||||
# 获取脚本所在目录的绝对路径
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
# 构建logfile目录的绝对路径
|
||||
log_dir = os.path.join(script_dir, '../logfile')
|
||||
log_dir = os.path.abspath(log_dir)
|
||||
|
||||
# 安全检查:防止路径遍历攻击
|
||||
if '..' in filename or '/' in filename or '\\' in filename:
|
||||
return jsonify({'success': False, 'message': '非法的文件名'})
|
||||
|
||||
log_file = os.path.join(log_dir, filename)
|
||||
if os.path.exists(log_file) and os.path.isfile(log_file):
|
||||
try:
|
||||
os.remove(log_file)
|
||||
return jsonify({'success': True, 'message': '日志文件删除成功'})
|
||||
except Exception as e:
|
||||
print(f"删除日志文件失败: {str(e)}")
|
||||
return jsonify({'success': False, 'message': f'删除失败: {str(e)}'})
|
||||
return jsonify({'success': False, 'message': '日志文件不存在'})
|
||||
|
||||
|
||||
@app.route('/stop/<process_id>', methods=['POST'])
|
||||
def stop(process_id):
|
||||
|
|
@ -260,5 +282,40 @@ def stop(process_id):
|
|||
return jsonify({'success': True})
|
||||
return jsonify({'success': False})
|
||||
|
||||
@app.route('/delete/<process_id>', methods=['POST'])
|
||||
def delete(process_id):
|
||||
if process_id in training_processes:
|
||||
# 确保进程已经停止
|
||||
if training_processes[process_id]['running']:
|
||||
# 如果进程还在运行,先停止它
|
||||
try:
|
||||
process = training_processes[process_id]['process']
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
except Exception as e:
|
||||
print(f"停止进程失败: {str(e)}")
|
||||
|
||||
# 从进程字典中删除
|
||||
del training_processes[process_id]
|
||||
|
||||
# 可选:删除对应的日志文件
|
||||
try:
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
log_dir = os.path.join(script_dir, '../logfile')
|
||||
log_dir = os.path.abspath(log_dir)
|
||||
|
||||
if os.path.exists(log_dir):
|
||||
for filename in os.listdir(log_dir):
|
||||
if filename.endswith(f'{process_id}.log'):
|
||||
os.remove(os.path.join(log_dir, filename))
|
||||
except Exception as e:
|
||||
print(f"删除日志文件失败: {str(e)}")
|
||||
|
||||
return jsonify({'success': True})
|
||||
return jsonify({'success': False})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||
Loading…
Reference in New Issue