update logfile && process
parent
5bef3f08ab
commit
48a81d99bb
|
|
@ -8,13 +8,28 @@ body {
|
||||||
background-color: #121212;
|
background-color: #121212;
|
||||||
min-height: 100vh;
|
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 {
|
h1 {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||||
font-size: 2.5em;
|
font-size: 2.5em;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
margin: 0;
|
||||||
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -33,15 +48,23 @@ h1 {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: #cccccc;
|
color: #888888;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
.tab.active {
|
.tab.active {
|
||||||
background: linear-gradient(135deg, #4a148c 0%, #8e24aa 100%);
|
background: linear-gradient(135deg, #9c27b0 0%, #e040fb 100%);
|
||||||
color: white;
|
color: white;
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
|
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 {
|
.form-container {
|
||||||
background: rgba(30, 30, 30, 0.9);
|
background: rgba(30, 30, 30, 0.9);
|
||||||
|
|
@ -56,12 +79,13 @@ h1 {
|
||||||
.parameter-card {
|
.parameter-card {
|
||||||
background-color: #2d2d2d;
|
background-color: #2d2d2d;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 20px;
|
padding: 24px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 16px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
padding-left: 5%;
|
padding-left: 5%;
|
||||||
padding-right: 5%;
|
padding-right: 5%;
|
||||||
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-card:hover {
|
.parameter-card:hover {
|
||||||
|
|
@ -71,13 +95,13 @@ h1 {
|
||||||
|
|
||||||
/* 卡片标题样式 */
|
/* 卡片标题样式 */
|
||||||
.card-title {
|
.card-title {
|
||||||
color: #e0e0e0;
|
color: #ffffff;
|
||||||
font-size: 1.4rem;
|
font-size: 18px;
|
||||||
font-weight: bold;
|
font-weight: 700;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
border-bottom: 1px solid #404040;
|
border-bottom: 1px solid #e040fb;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,14 +142,15 @@ label {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
font-size: 0.9rem;
|
font-size: 13px;
|
||||||
|
opacity: 0.8;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
input[type="text"], input[type="number"], select {
|
input[type="text"], input[type="number"], select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 15px;
|
padding: 8px 12px;
|
||||||
border: 2px solid #444;
|
border: 2px solid #444;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -137,7 +162,7 @@ input[type="text"], input[type="number"], select {
|
||||||
/* 确保textarea也适应两列布局 */
|
/* 确保textarea也适应两列布局 */
|
||||||
textarea {
|
textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 12px 15px;
|
padding: 8px 12px;
|
||||||
border: 2px solid #444;
|
border: 2px solid #444;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -200,14 +225,69 @@ button:active {
|
||||||
.logs-container:hover {
|
.logs-container:hover {
|
||||||
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3);
|
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 {
|
.process-item {
|
||||||
background: rgba(30, 30, 30, 0.9);
|
background: rgba(40, 40, 40, 0.9);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
border: 1px solid #444;
|
border: 1px solid #555;
|
||||||
}
|
}
|
||||||
|
|
||||||
.process-item:hover {
|
.process-item:hover {
|
||||||
|
|
@ -261,6 +341,24 @@ button:active {
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
border-radius: 6px;
|
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 {
|
.btn-logs:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
box-shadow: 0 4px 10px rgba(79, 172, 254, 0.3);
|
box-shadow: 0 4px 10px rgba(79, 172, 254, 0.3);
|
||||||
|
|
@ -270,12 +368,13 @@ button:active {
|
||||||
}
|
}
|
||||||
.section-title {
|
.section-title {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
font-size: 20px;
|
font-size: 18px;
|
||||||
margin-bottom: 25px;
|
margin-bottom: 25px;
|
||||||
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
text-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||||
font-weight: 600;
|
font-weight: 700;
|
||||||
padding-bottom: 10px;
|
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 {
|
.tab-content {
|
||||||
animation: fadeIn 0.5s ease-out;
|
animation: fadeIn 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -319,6 +543,21 @@ button:active {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 50px;
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
flex-direction: column;
|
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事件以设置默认值
|
// 初始触发一次change事件以设置默认值
|
||||||
document.getElementById('train_type').dispatchEvent(new Event('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() {
|
function loadProcesses() {
|
||||||
fetch('/processes')
|
fetch('/processes')
|
||||||
|
|
@ -113,7 +254,143 @@ function loadProcesses() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按启动时间倒序排序,最新启动的进程显示在前面
|
||||||
|
data.sort((a, b) => new Date(b.start_time) - new Date(a.start_time));
|
||||||
|
|
||||||
|
// 按训练类型分组
|
||||||
|
const groupedProcesses = {};
|
||||||
data.forEach(process => {
|
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');
|
const processItem = document.createElement('div');
|
||||||
processItem.className = 'process-item';
|
processItem.className = 'process-item';
|
||||||
|
|
||||||
|
|
@ -133,10 +410,13 @@ function loadProcesses() {
|
||||||
processItem.dataset.processId = process.id;
|
processItem.dataset.processId = process.id;
|
||||||
processItem.dataset.processStatus = process.status;
|
processItem.dataset.processStatus = process.status;
|
||||||
|
|
||||||
|
// 检查是否显示删除按钮(对于非运行中的进程)
|
||||||
|
const showDeleteButton = !process.running;
|
||||||
|
|
||||||
processItem.innerHTML = `
|
processItem.innerHTML = `
|
||||||
<div class="process-info">
|
<div class="process-info">
|
||||||
<div>
|
<div>
|
||||||
<strong>${process.train_type}</strong> - ${process.start_time}
|
<strong>${process.start_time}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="process-status ${statusClass}">${process.status}</span>
|
<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="showLogs('${process.id}')">查看日志</button>
|
||||||
<button class="btn-logs" onclick="refreshLog('${process.id}')">刷新日志</button>
|
<button class="btn-logs" onclick="refreshLog('${process.id}')">刷新日志</button>
|
||||||
${process.running ? `<button class="btn-stop" onclick="stopProcess('${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>
|
||||||
<div id="logs-${process.id}" class="logs-container hidden"></div>
|
<div id="logs-${process.id}" class="logs-container hidden"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
processList.appendChild(processItem);
|
parentElement.appendChild(processItem);
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 存储训练日志定时器的ID
|
// 存储训练日志定时器的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) {
|
function stopProcess(processId) {
|
||||||
if (confirm('确定要停止这个训练进程吗?')) {
|
showConfirmDialog(
|
||||||
|
'确定要停止这个训练进程吗?',
|
||||||
|
() => {
|
||||||
|
// 确认停止
|
||||||
fetch(`/stop/${processId}`, {
|
fetch(`/stop/${processId}`, {
|
||||||
method: 'POST'
|
method: 'POST'
|
||||||
})
|
})
|
||||||
.then(() => {
|
.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 formData = new FormData(this);
|
||||||
const data = Object.fromEntries(formData.entries());
|
const data = Object.fromEntries(formData.entries());
|
||||||
|
|
||||||
|
// 立即显示加载通知
|
||||||
|
showNotification('正在启动训练...', 'info');
|
||||||
|
|
||||||
|
// 延迟1秒后发送请求,确保两个通知之间有间隔
|
||||||
|
setTimeout(() => {
|
||||||
fetch('/train', {
|
fetch('/train', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
|
|
@ -275,15 +809,34 @@ document.getElementById('train-form').addEventListener('submit', function(e) {
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
.then(response => response.json())
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('网络响应异常');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
alert('训练已开始!');
|
showNotification('训练已开始!', 'success');
|
||||||
openTab(event, 'processes');
|
|
||||||
} else {
|
// 延迟1秒后切换到进程标签页,确保进程信息已更新
|
||||||
alert('训练启动失败:' + result.error);
|
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));
|
data.sort((a, b) => new Date(b.modified_time) - new Date(a.modified_time));
|
||||||
|
|
||||||
|
// 按训练类型分组
|
||||||
|
const groupedLogFiles = {};
|
||||||
data.forEach(logfile => {
|
data.forEach(logfile => {
|
||||||
const fileItem = document.createElement('div');
|
|
||||||
fileItem.className = 'process-item';
|
|
||||||
|
|
||||||
// 从文件名提取训练类型
|
// 从文件名提取训练类型
|
||||||
let trainType = '未知';
|
let trainType = '自定义训练';
|
||||||
if (logfile.filename.includes('train_pretrain_')) {
|
if (logfile.filename.includes('train_pretrain_')) {
|
||||||
trainType = 'pretrain';
|
trainType = 'pretrain';
|
||||||
} else if (logfile.filename.includes('train_sft_')) {
|
} else if (logfile.filename.includes('train_sft_')) {
|
||||||
|
|
@ -324,11 +876,136 @@ function loadLogFiles() {
|
||||||
trainType = '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 = `
|
fileItem.innerHTML = `
|
||||||
<div class="process-info">
|
<div class="process-info">
|
||||||
<div>
|
<div>
|
||||||
<strong>${trainType}</strong> - ${logfile.filename}
|
<strong>${logfile.filename}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span class="process-status status-completed">已保存</span>
|
<span class="process-status status-completed">已保存</span>
|
||||||
|
|
@ -337,29 +1014,166 @@ function loadLogFiles() {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button class="btn-logs" onclick="viewLogFile('${logfile.filename}', this)">查看日志</button>
|
<button class="btn-logs" onclick="viewLogFile('${logfile.filename}', this)">查看日志</button>
|
||||||
|
<button class="btn-delete" onclick="deleteLogFile('${logfile.filename}', this)">删除</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="log-content-${logfile.filename.replace(/\./g, '-')}" class="logs-container hidden"></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) {
|
function viewLogFile(filename, button) {
|
||||||
const safeFilename = filename.replace(/\./g, '-');
|
// 生成安全的文件名,用于元素ID
|
||||||
const logContainer = button.closest('.process-item').querySelector(`#log-content-${safeFilename}`);
|
const safeFilename = filename.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
||||||
logContainer.classList.toggle('hidden');
|
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)}`)
|
fetch(`/logfile-content/${encodeURIComponent(filename)}`)
|
||||||
.then(response => response.text())
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('获取日志失败');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
})
|
||||||
.then(logs => {
|
.then(logs => {
|
||||||
|
// 保留空白格式,使用textContent而不是innerHTML
|
||||||
logContainer.textContent = logs;
|
logContainer.textContent = logs;
|
||||||
logContainer.scrollTop = 0;
|
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">
|
<link rel="stylesheet" href="/static/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<img src="/static/images/logo2.png" alt="MiniMind Logo" class="logo">
|
||||||
<h1>MiniMind Training Lab</h1>
|
<h1>MiniMind Training Lab</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" onclick="openTab(event, 'train')">开始训练</button>
|
<button class="tab active" onclick="openTab(event, 'train')">开始训练</button>
|
||||||
|
|
@ -26,10 +29,10 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="train_type">训练类型:</label>
|
<label for="train_type">训练类型:</label>
|
||||||
<select id="train_type" name="train_type" required>
|
<select id="train_type" name="train_type" required>
|
||||||
<option value="pretrain">预训练 (Pretrain)</option>
|
<option value="pretrain">Pretrain</option>
|
||||||
<option value="sft">监督微调 (Full SFT)</option>
|
<option value="sft">SFT - Full</option>
|
||||||
<option value="lora">LoRA 微调</option>
|
<option value="lora">SFT - Lora</option>
|
||||||
<option value="dpo">强化学习 (RL-DPO)</option>
|
<option value="dpo">RL - DPO</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
||||||
|
|
@ -238,6 +238,28 @@ def get_logfile_content(filename):
|
||||||
return f'读取日志失败: {str(e)}'
|
return f'读取日志失败: {str(e)}'
|
||||||
return '日志文件不存在'
|
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'])
|
@app.route('/stop/<process_id>', methods=['POST'])
|
||||||
def stop(process_id):
|
def stop(process_id):
|
||||||
|
|
@ -260,5 +282,40 @@ def stop(process_id):
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
return jsonify({'success': False})
|
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__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5000, debug=True)
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
Loading…
Reference in New Issue