import { getLogFiles, getLogFileContent, deleteLogFile as apiDeleteLogFile } from '../services/apiClient.js'; import { el } from '../utils/dom.js'; import { showNotification } from '../ui/notify.js'; import { showConfirmDialog } from '../ui/dialog.js'; export function loadLogFiles() { return getLogFiles().then((data) => { const list = document.getElementById('logfiles-list'); list.innerHTML = ''; if (data.length === 0) { list.innerHTML = '

暂无日志文件

'; return; } data.sort((a, b) => new Date(b.modified_time) - new Date(a.modified_time)); const groups = {}; data.forEach((f) => { let type = '自定义训练'; const n = f.filename; if (n.includes('train_pretrain_')) type = 'pretrain'; else if (n.includes('train_sft_')) type = 'sft'; else if (n.includes('train_lora_')) type = 'lora'; else if (n.includes('train_dpo_')) type = 'dpo'; else if (n.includes('train_ppo_')) type = 'ppo'; else if (n.includes('train_grpo_')) type = 'grpo'; else if (n.includes('train_spo_')) type = 'spo'; f.train_type = type; if (!groups[type]) groups[type] = []; groups[type].push(f); }); const order = ['pretrain', 'sft', 'lora', 'dpo', 'ppo', 'grpo', 'spo', '未知']; [...order.filter((t) => groups[t]), ...Object.keys(groups).filter((t) => !order.includes(t))].forEach((t) => { list.appendChild(createTypeGroupWithToggle(t, groups[t])); }); }); } function createTypeGroupWithToggle(trainType, files) { const group = el('div', { class: 'process-type-group' }); const header = el('div', { class: 'process-type-header' }); header.dataset.expanded = 'true'; const title = el('h3', { class: 'process-type-title', text: getTrainTypeDisplayName(trainType) }); const toggle = el('button', { class: 'toggle-btn' }); toggle.innerHTML = '▼'; toggle.onclick = (e) => { e.stopPropagation(); toggleGroup(header); }; header.appendChild(title); header.appendChild(toggle); header.onclick = () => toggleGroup(header); const content = el('div', { class: 'process-type-content' }); files.forEach((f) => addLogFileItemToGroup(content, f)); group.appendChild(header); group.appendChild(content); return group; } function toggleGroup(header) { const expanded = header.dataset.expanded === 'true'; const content = header.nextElementSibling; const toggle = header.querySelector('.toggle-btn'); if (expanded) { header.dataset.expanded = 'false'; content.style.maxHeight = '0'; content.style.overflow = 'hidden'; toggle.innerHTML = '▶'; } else { content.style.overflow = 'hidden'; content.style.maxHeight = 'none'; const h = content.scrollHeight; content.style.maxHeight = '0'; content.offsetHeight; header.dataset.expanded = 'true'; content.style.maxHeight = h + 'px'; setTimeout(() => { content.style.maxHeight = 'none'; content.style.overflow = 'visible'; }, 300); toggle.innerHTML = '▼'; } } function getTrainTypeDisplayName(trainType) { const names = { pretrain: '预训练 (Pretrain)', sft: '全参数监督微调 (SFT - Full)', lora: 'LoRA监督微调 (SFT - Lora)', dpo: '直接偏好优化 (RL - DPO)', ppo: 'PPO', grpo: 'GRPO', spo: 'SPO', }; return names[trainType] || trainType; } function addLogFileItemToGroup(parent, logfile) { const item = el('div', { class: 'process-item' }); item.innerHTML = `
${logfile.filename}
已保存 ${logfile.modified_time}
`; parent.appendChild(item); bindItemButtons(item, logfile); } function bindItemButtons(item, logfile) { const viewBtn = item.querySelector('[data-view]'); if (viewBtn) viewBtn.addEventListener('click', () => viewLogFile(logfile.filename, viewBtn)); const delBtn = item.querySelector('[data-del]'); if (delBtn) delBtn.addEventListener('click', () => deleteLogFile(logfile.filename, delBtn)); } function deleteLogFile(filename, button) { showConfirmDialog(`确定要删除日志文件 "${filename}" 吗?此操作无法恢复。`, () => { const item = button.closest('.process-item'); const content = item.closest('.process-type-content'); const group = content.closest('.process-type-group'); const original = button.textContent; button.textContent = '删除中...'; button.disabled = true; apiDeleteLogFile(filename) .then((data) => { if (data.success) { item.remove(); if (content.children.length === 0) group.remove(); else { const header = content.previousElementSibling; if (header && header.dataset.expanded === 'true') { content.style.maxHeight = 'none'; const h = content.scrollHeight; content.style.maxHeight = h + 'px'; } } showNotification(`日志文件 "${filename}" 已成功删除`); } else throw new Error(data.message || '删除失败'); }) .catch((e) => { showNotification(`删除失败: ${e.message}`, 'error'); button.textContent = original; button.disabled = false; }); }); } function viewLogFile(filename, button) { const safe = filename.replace(/[^a-zA-Z0-9_.-]/g, '_').replace(/\./g, '-'); const item = button.closest('.process-item'); const container = item.querySelector(`#log-content-${safe}`); const content = item.closest('.process-type-content'); const header = content ? content.previousElementSibling : null; if (content && header && header.dataset.expanded !== 'true') toggleGroup(header); if (container.classList.contains('hidden')) { container.classList.remove('hidden'); container.textContent = '加载中...'; getLogFileContent(filename) .then((logs) => { container.textContent = logs; container.scrollTop = 0; updateContentHeight(content, header); }) .catch((e) => { container.textContent = `获取日志失败: ${e.message}`; updateContentHeight(content, header); }); } else { container.classList.add('hidden'); updateContentHeight(content, header); } } function updateContentHeight(content, header) { if (content && header && header.dataset.expanded === 'true') { const current = content.style.maxHeight; content.style.maxHeight = 'none'; const h = content.scrollHeight; if (current === 'none' || parseInt(current) !== h) { content.style.maxHeight = h + 'px'; setTimeout(() => { if (header.dataset.expanded === 'true') content.style.maxHeight = 'none'; }, 300); } else content.style.maxHeight = current; } }