diff --git a/scripts/static/css/style.css b/scripts/static/css/style.css index 60b2ea4..9563084 100644 --- a/scripts/static/css/style.css +++ b/scripts/static/css/style.css @@ -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; } diff --git a/scripts/static/images/logo2.png b/scripts/static/images/logo2.png new file mode 100644 index 0000000..9a0b3e2 Binary files /dev/null and b/scripts/static/images/logo2.png differ diff --git a/scripts/static/js/script.js b/scripts/static/js/script.js index ea9bcd7..02a726c 100644 --- a/scripts/static/js/script.js +++ b/scripts/static/js/script.js @@ -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,48 +254,186 @@ function loadProcesses() { return; } + // 按启动时间倒序排序,最新启动的进程显示在前面 + data.sort((a, b) => new Date(b.start_time) - new Date(a.start_time)); + + // 按训练类型分组 + const groupedProcesses = {}; data.forEach(process => { - const processItem = document.createElement('div'); - processItem.className = 'process-item'; - - let statusClass = ''; - // 根据后端返回的status字段设置状态类 - if (process.status === '运行中') { - statusClass = 'status-running'; - } else if (process.status === '手动停止') { - statusClass = 'status-manual-stop'; - } else if (process.status === '出错') { - statusClass = 'status-error'; - } else { - statusClass = 'status-completed'; + 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'; - // 设置进程数据属性,用于后续检查状态 - processItem.dataset.processId = process.id; - processItem.dataset.processStatus = process.status; + // 创建标题容器(包含标题文本和切换按钮) + const titleContainer = document.createElement('div'); + titleContainer.className = 'process-type-header'; + titleContainer.dataset.expanded = 'true'; // 默认展开状态 - processItem.innerHTML = ` -
暂无训练进程
'; + } + }, 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 = '暂无训练进程
'; + } + } + } else { + // 检查是否还有进程项 + const remainingProcesses = document.querySelectorAll('.process-item'); + if (remainingProcesses.length === 0) { + const processList = document.getElementById('process-list'); + processList.innerHTML = '暂无训练进程
'; + } + } + }, 300); + } + + // 清除日志定时器 + if (logTimers[processId]) { + clearInterval(logTimers[processId]); + delete logTimers[processId]; + } + + showNotification('训练进程已删除', 'success'); + }) + .catch(error => { + console.error('删除进程时出错:', error); + showNotification('删除进程失败,请刷新页面重试', 'error'); + }); + } + ); +} + // 停止进程 function stopProcess(processId) { - if (confirm('确定要停止这个训练进程吗?')) { - fetch(`/stop/${processId}`, { - method: 'POST' - }) - .then(() => { - loadProcesses(); - }); - } + showConfirmDialog( + '确定要停止这个训练进程吗?', + () => { + // 确认停止 + fetch(`/stop/${processId}`, { + method: 'POST' + }) + .then(() => { + // 立即给用户反馈,设置为手动停止状态 + const processItem = document.querySelector(`[data-process-id="${processId}"]`); + if (processItem) { + // 先设置为手动停止状态(即时反馈) + processItem.dataset.processStatus = '手动停止'; + const statusElement = processItem.querySelector('.process-status'); + if (statusElement) { + statusElement.classList.remove('status-running', 'status-error', 'status-completed'); + statusElement.classList.add('status-manual-stop'); + statusElement.textContent = '手动停止'; + } + + // 移除停止按钮 + const stopButton = processItem.querySelector('.btn-stop'); + if (stopButton) { + stopButton.remove(); + } + + // 清除日志定时器 + if (logTimers[processId]) { + clearInterval(logTimers[processId]); + delete logTimers[processId]; + } + } + + // 显示通知 + showNotification('训练进程已停止', 'info'); + + // 为了确保状态准确,仍然获取完整列表并更新单个进程 + fetch('/processes') + .then(response => response.json()) + .then(data => { + // 从完整列表中找到特定进程 + const updatedProcess = data.find(p => p.id === processId); + if (updatedProcess && processItem) { + // 使用updateProcessItem函数更新进程(如果需要的话) + updateProcessItem(processItem, updatedProcess); + } + }) + .catch(error => { + console.error('获取进程列表更新单个进程失败:', error); + }); + }) + .catch(() => { + showNotification('停止进程失败', 'error'); + }); + }, + () => { + // 取消停止,可选的回调 + showNotification('已取消停止操作', 'info'); + } + ); } // 表单提交处理 @@ -268,22 +797,46 @@ document.getElementById('train-form').addEventListener('submit', function(e) { const formData = new FormData(this); const data = Object.fromEntries(formData.entries()); - fetch('/train', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }) - .then(response => response.json()) - .then(result => { - if (result.success) { - alert('训练已开始!'); - openTab(event, 'processes'); - } else { - alert('训练启动失败:' + result.error); - } - }); + // 立即显示加载通知 + showNotification('正在启动训练...', 'info'); + + // 延迟1秒后发送请求,确保两个通知之间有间隔 + setTimeout(() => { + fetch('/train', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + .then(response => { + if (!response.ok) { + throw new Error('网络响应异常'); + } + return response.json(); + }) + .then(result => { + if (result.success) { + showNotification('训练已开始!', 'success'); + + // 延迟1秒后切换到进程标签页,确保进程信息已更新 + setTimeout(() => { + // 获取进程标签按钮并触发点击 + const processTab = document.querySelector('.tab[onclick*="processes"]'); + if (processTab) { + processTab.click(); + } + }, 1000); + } else { + // 显示错误通知 + showNotification('训练启动失败:' + result.error, 'error'); + } + }) + .catch(error => { + // 显示等待通知 + showNotification('启动训练中,请耐心等待...', 'info'); + }); + }, 1000); // 1秒延迟确保两个通知之间有间隔 }); // 加载日志文件列表 @@ -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_')) { @@ -323,43 +875,305 @@ function loadLogFiles() { } else if (logfile.filename.includes('train_spo_')) { trainType = 'spo'; } - - fileItem.innerHTML = ` -获取日志失败: ${error.message}
`; + + // 更新内容容器的高度 + 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; + } + } } } \ No newline at end of file diff --git a/scripts/templates/index.html b/scripts/templates/index.html index 63cc1a8..7d5b342 100644 --- a/scripts/templates/index.html +++ b/scripts/templates/index.html @@ -7,7 +7,10 @@ -
+