document.addEventListener('DOMContentLoaded', () => { // DOM Elements const tabs = document.querySelectorAll('.tab-btn'); const tabContents = document.querySelectorAll('.tab-content'); const configForm = document.getElementById('config-form'); // Toggles and fields const splitEnabled = document.getElementById('split-enabled'); const splitRatio = document.getElementById('split-ratio'); const splitClasses = document.getElementById('split-classes'); // Dataset selectors const datasetSelect = document.getElementById('dataset-select'); const datasetCustomWrapper = document.getElementById('dataset-custom-wrapper'); // Model selectors const modelSelect = document.getElementById('model-select'); const modelCustomWrapper = document.getElementById('model-custom-wrapper'); const augmentationEnabled = document.getElementById('augmentation-enabled'); const augmentationInputs = document.querySelectorAll('.augmentation-fields input, .augmentation-fields select'); const mlflowEnabled = document.getElementById('mlflow-enabled'); const mlflowInputs = document.querySelectorAll('.mlflow-fields input'); const trackingUriInput = document.getElementById('tracking-uri'); const mlflowHeaderLink = document.getElementById('mlflow-header-link'); // --- Dynamic Model Selection --- const standardModels = { detect: ['yolo11n.pt', 'yolo11s.pt', 'yolo11m.pt', 'yolo11l.pt', 'yolo11x.pt'], segment: ['yolo11n-seg.pt', 'yolo11s-seg.pt', 'yolo11m-seg.pt', 'yolo11l-seg.pt', 'yolo11x-seg.pt'], classify: ['yolo11n-cls.pt', 'yolo11s-cls.pt', 'yolo11m-cls.pt', 'yolo11l-cls.pt', 'yolo11x-cls.pt'], pose: ['yolo11n-pose.pt', 'yolo11s-pose.pt', 'yolo11m-pose.pt', 'yolo11l-pose.pt', 'yolo11x-pose.pt'], obb: ['yolo11n-obb.pt', 'yolo11s-obb.pt', 'yolo11m-obb.pt', 'yolo11l-obb.pt', 'yolo11x-obb.pt'] }; let discoveredModels = []; function updateModelOptions() { const task = taskSelect.value; const stdModels = standardModels[task] || []; const currentSelectVal = modelSelect.value; modelSelect.innerHTML = ''; // Group 1: Standard Models const stdGroup = document.createElement('optgroup'); stdGroup.label = 'Стандартные модели'; stdModels.forEach(model => { const opt = document.createElement('option'); opt.value = model; opt.textContent = model; stdGroup.appendChild(opt); }); modelSelect.appendChild(stdGroup); // Group 2: Discovered Models const localModels = discoveredModels.filter(m => !stdModels.includes(m.name)); if (localModels.length > 0) { const localGroup = document.createElement('optgroup'); localGroup.label = 'Локальные/скачанные модели'; localModels.forEach(m => { const opt = document.createElement('option'); opt.value = m.name; opt.textContent = m.name; localGroup.appendChild(opt); }); modelSelect.appendChild(localGroup); } // Custom Option const customOpt = document.createElement('option'); customOpt.value = '__custom__'; customOpt.textContent = 'Указать модель вручную...'; modelSelect.appendChild(customOpt); // Match selection if valid const allAvailable = [...stdModels, ...localModels.map(m => m.name)]; if (allAvailable.includes(currentSelectVal)) { modelSelect.value = currentSelectVal; } else { modelSelect.value = stdModels[0] || '__custom__'; } updateModelFieldsState(); } function updateModelFieldsState() { const val = modelSelect.value; const modelInput = document.getElementById('model'); if (val === '__custom__') { modelCustomWrapper.style.display = 'block'; } else { modelCustomWrapper.style.display = 'none'; modelInput.value = val; } } const taskSelect = document.getElementById('task'); // Session Controls const sessionSelect = document.getElementById('session-select'); const sessionNameInput = document.getElementById('session-name'); const sessionSaveBtn = document.getElementById('session-save-btn'); const sessionDeleteBtn = document.getElementById('session-delete-btn'); // Control elements const startBtn = document.getElementById('start-btn'); const stopBtn = document.getElementById('stop-btn'); // Status elements const statusCard = document.getElementById('status-card'); const statusTitle = document.getElementById('status-title'); const statusText = document.getElementById('status-text'); const statusTimer = document.getElementById('status-timer'); const progressBarFill = document.getElementById('progress-bar-fill'); const progressText = document.getElementById('progress-text'); const progressEta = document.getElementById('progress-eta'); // Logs const logContainer = document.getElementById('log-container'); const autoscrollCheck = document.getElementById('autoscroll'); const clearLogBtn = document.getElementById('clear-log-btn'); // Chart const ctx = document.getElementById('metricsChart').getContext('2d'); let metricsChart = null; // State variables let trainingTimer = null; let secondsElapsed = 0; let socket = null; let isTrainingActive = false; // --- View Tab Switching --- const viewBtns = document.querySelectorAll('.view-btn'); const views = document.querySelectorAll('.workspace-view'); viewBtns.forEach(btn => { btn.addEventListener('click', () => { viewBtns.forEach(b => b.classList.remove('active')); views.forEach(v => { v.classList.remove('active'); }); btn.classList.add('active'); const viewId = btn.dataset.view; const view = document.getElementById(viewId); view.classList.add('active'); localStorage.setItem('active_workspace_view', viewId); }); }); const savedView = localStorage.getItem('active_workspace_view'); if (savedView) { const viewBtn = Array.from(viewBtns).find(b => b.dataset.view === savedView); if (viewBtn) viewBtn.click(); } // --- Tab Switching --- tabs.forEach(tab => { tab.addEventListener('click', () => { tabs.forEach(t => t.classList.remove('active')); tabContents.forEach(c => c.classList.remove('active')); tab.classList.add('active'); // Check if it's a sub-tab (has data-tab) if (tab.dataset.tab) { const contentId = `tab-${tab.dataset.tab}`; const content = document.getElementById(contentId); if (content) content.classList.add('active'); localStorage.setItem('active_tab', tab.dataset.tab); } }); }); // Restore active tab on load const savedTab = localStorage.getItem('active_tab'); if (savedTab) { const tabBtn = Array.from(tabs).find(t => t.dataset.tab === savedTab); if (tabBtn) { tabBtn.click(); } } // --- Toggles & Constraints --- function updateSplitFields() { const isClassify = taskSelect.value === 'classify'; if (isClassify && splitEnabled.checked) { splitEnabled.checked = false; showNotification('Для classify укажите готовый каталог с train/val по классам.', 'warning'); } splitEnabled.disabled = isClassify; const disabled = !splitEnabled.checked || isClassify; splitRatio.disabled = disabled; splitClasses.disabled = disabled; } function updateAugmentationFields() { const disabled = !augmentationEnabled.checked; augmentationInputs.forEach(input => { input.disabled = disabled; }); } function updateMlflowFields() { const disabled = !mlflowEnabled.checked; mlflowInputs.forEach(input => { input.disabled = disabled; }); updateMlflowHeaderLink(); } function updateMlflowHeaderLink() { const uri = trackingUriInput.value.trim(); if (uri.startsWith('http://') || uri.startsWith('https://')) { mlflowHeaderLink.href = uri; mlflowHeaderLink.style.opacity = '1'; mlflowHeaderLink.style.pointerEvents = 'auto'; } else { mlflowHeaderLink.href = 'http://localhost:5000'; mlflowHeaderLink.style.opacity = '0.5'; } } trackingUriInput.addEventListener('input', updateMlflowHeaderLink); splitEnabled.addEventListener('change', updateSplitFields); taskSelect.addEventListener('change', () => { updateSplitFields(); updateModelOptions(); }); augmentationEnabled.addEventListener('change', updateAugmentationFields); mlflowEnabled.addEventListener('change', updateMlflowFields); // --- Logger --- function addLogLine(message, level = 'info') { const line = document.createElement('div'); line.className = `log-line log-level-${level.toLowerCase()}`; line.textContent = message; logContainer.appendChild(line); if (autoscrollCheck.checked) { logContainer.scrollTop = logContainer.scrollHeight; } } clearLogBtn.addEventListener('click', () => { logContainer.innerHTML = ''; }); // --- Chart.js Integration --- function initChart(datasets = []) { if (metricsChart) { metricsChart.destroy(); } metricsChart = new Chart(ctx, { type: 'line', data: { labels: [], datasets: datasets }, options: { responsive: true, maintainAspectRatio: false, scales: { x: { title: { display: true, text: 'Эпоха', color: '#a1a1aa' }, grid: { color: '#27272a' }, ticks: { color: '#a1a1aa' } }, y: { title: { display: true, text: 'Значение', color: '#a1a1aa' }, grid: { color: '#27272a' }, ticks: { color: '#a1a1aa' } } }, plugins: { legend: { labels: { color: '#f4f4f5', font: { family: 'Outfit' } } } } } }); } function updateChart(epoch, metrics) { if (!metricsChart) { initChart(); } let labelIndex = metricsChart.data.labels.indexOf(epoch); if (labelIndex === -1) { metricsChart.data.labels.push(epoch); labelIndex = metricsChart.data.labels.length - 1; metricsChart.data.datasets.forEach(dataset => dataset.data.push(null)); } const colors = ['#f97316', '#10b981', '#3b82f6', '#eab308', '#a855f7']; Object.entries(metrics).forEach(([key, value]) => { if (key === 'epoch') return; let dataset = metricsChart.data.datasets.find(item => item.label === key); if (!dataset) { const color = colors[metricsChart.data.datasets.length % colors.length]; dataset = { label: key, data: Array(metricsChart.data.labels.length).fill(null), borderColor: color, backgroundColor: color + '22', tension: 0.15, fill: false }; metricsChart.data.datasets.push(dataset); } dataset.data[labelIndex] = value; }); metricsChart.update(); } // --- Timer UI --- function startTimer() { stopTimer(); secondsElapsed = 0; trainingTimer = setInterval(() => { secondsElapsed++; const h = String(Math.floor(secondsElapsed / 3600)).padStart(2, '0'); const m = String(Math.floor((secondsElapsed % 3600) / 60)).padStart(2, '0'); const s = String(secondsElapsed % 60).padStart(2, '0'); statusTimer.textContent = `${h}:${m}:${s}`; }, 1000); } function stopTimer() { if (trainingTimer) { clearInterval(trainingTimer); trainingTimer = null; } } // --- WebSocket Sync --- function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/ws`; socket = new WebSocket(wsUrl); socket.onopen = () => { addLogLine('Соединение с сервером установлено.', 'info'); }; socket.onclose = () => { addLogLine('Соединение потеряно. Повторная попытка через 5 секунд...', 'warning'); setTimeout(connectWebSocket, 5000); }; socket.onerror = (err) => { console.error('WS Error:', err); }; socket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'init') { updateUIStatus(data.status); // Load logs logContainer.innerHTML = ''; data.logs.forEach(([levelCode, msg]) => { const level = levelCode.replace('__LOG_LEVEL_', '').replace('__', '').toLowerCase(); addLogLine(msg, level); }); // Draw initial chart points initChart(); if (data.metrics && data.metrics.length > 0) { data.metrics.forEach(m => { updateChart(m.epoch, m); }); } // Sync progress if (data.status === 'training' || data.status === 'stopping') { updateProgress(data.epoch, data.total_epochs); } } else if (data.type === 'status') { updateUIStatus(data.status); if (data.output_dir) { addLogLine(`Результаты сохранены: ${data.output_dir}`, 'success'); } } else if (data.type === 'log') { const autoscrollCheck = document.getElementById('autoscroll'); if (data.level === 'progress') { let lastLine = logContainer.lastElementChild; if (lastLine && lastLine.classList.contains('log-line-progress')) { lastLine.textContent = data.message; } else { const line = document.createElement('div'); line.className = 'log-line log-line-progress log-level-info'; line.textContent = data.message; logContainer.appendChild(line); } } else { let lastLine = logContainer.lastElementChild; if (lastLine && lastLine.classList.contains('log-line-progress')) { lastLine.classList.remove('log-line-progress'); } addLogLine(data.message, data.level); } if (autoscrollCheck && autoscrollCheck.checked) { logContainer.scrollTop = logContainer.scrollHeight; } } else if (data.type === 'progress') { updateProgress(data.epoch, data.total_epochs, data.message); if (data.metrics) { updateChart(data.epoch, data.metrics); } } }; } function updateUIStatus(status) { statusCard.className = `status-${status}`; switch (status) { case 'idle': statusTitle.textContent = 'ГОТОВО К ЗАПУСКУ'; statusText.textContent = 'Проверьте параметры и начните обучение.'; isTrainingActive = false; startBtn.disabled = false; stopBtn.disabled = true; stopTimer(); break; case 'preparing': statusTitle.textContent = 'ПОДГОТОВКА'; statusText.textContent = 'Загрузка модели, разметки и настройка окружения...'; isTrainingActive = true; startBtn.disabled = true; stopBtn.disabled = false; startTimer(); initChart(); break; case 'training': statusTitle.textContent = 'ОБУЧЕНИЕ'; isTrainingActive = true; startBtn.disabled = true; stopBtn.disabled = false; if (!trainingTimer) startTimer(); break; case 'stopping': statusTitle.textContent = 'ОСТАНОВКА'; statusText.textContent = 'Остановка процессов обучения. Дождитесь закрытия...'; isTrainingActive = true; startBtn.disabled = true; stopBtn.disabled = true; break; case 'finished': // Compatibility with sessions created by older versions. case 'succeeded': statusTitle.textContent = 'ГОТОВО'; statusText.textContent = 'Обучение успешно завершено.'; isTrainingActive = false; startBtn.disabled = false; stopBtn.disabled = true; stopTimer(); break; case 'cancelled': statusTitle.textContent = 'ОСТАНОВЛЕНО'; statusText.textContent = 'Обучение остановлено пользователем.'; isTrainingActive = false; startBtn.disabled = false; stopBtn.disabled = true; stopTimer(); break; case 'failed': statusTitle.textContent = 'ОШИБКА'; statusText.textContent = 'Процесс завершился с ошибкой. Проверьте логи.'; isTrainingActive = false; startBtn.disabled = false; stopBtn.disabled = true; stopTimer(); break; } } function updateProgress(epoch, total, message = '') { const percent = total > 0 ? (epoch / total) * 100 : 0; progressBarFill.style.width = `${percent}%`; progressText.textContent = `Эпохи: ${epoch} / ${total}`; if (message) { statusText.textContent = message; } } // --- Read/Write Configurations --- function readNumber(id, fallback, integer = false) { const rawValue = document.getElementById(id).value; const value = integer ? Number.parseInt(rawValue, 10) : Number.parseFloat(rawValue); return Number.isNaN(value) ? fallback : value; } function getFormConfig() { return { dataset: document.getElementById('dataset').value.trim(), model: document.getElementById('model').value.trim(), task: taskSelect.value, epochs: readNumber('epochs', 100, true), image_size: readNumber('image-size', 640, true), batch_size: readNumber('batch-size', 16, true), device: document.getElementById('device').value.trim(), workers: readNumber('workers', 8, true), patience: readNumber('patience', 100, true), project: document.getElementById('project').value.trim() || 'runs/train', run_name: document.getElementById('run-name').value.trim(), split: { enabled: splitEnabled.checked, train_ratio: readNumber('split-ratio', 0.8), classes_path: splitClasses.value.trim() }, augmentation: { enabled: augmentationEnabled.checked, hsv_h: readNumber('hsv-h', 0.015), hsv_s: readNumber('hsv-s', 0.7), hsv_v: readNumber('hsv-v', 0.4), degrees: readNumber('degrees', 0), translate: readNumber('translate', 0.1), scale: readNumber('scale', 0.5), shear: readNumber('shear', 0), perspective: readNumber('perspective', 0), close_mosaic: readNumber('close-mosaic', 10, true), flipud: readNumber('flipud', 0), fliplr: readNumber('fliplr', 0.5), bgr: readNumber('bgr', 0), mosaic: readNumber('mosaic', 1), mixup: readNumber('mixup', 0), cutmix: readNumber('cutmix', 0), copy_paste: readNumber('copy-paste', 0), erasing: readNumber('erasing', 0.4), copy_paste_mode: document.getElementById('copy-paste-mode').value, auto_augment: document.getElementById('auto-augment').value }, mlflow: { enabled: mlflowEnabled.checked, tracking_uri: document.getElementById('tracking-uri').value.trim(), experiment_name: document.getElementById('experiment-name').value.trim(), run_name: document.getElementById('mlflow-run-name').value.trim() } }; } function applyConfig(data) { taskSelect.value = data.task || 'detect'; updateModelOptions(); const modelVal = data.model || 'yolo11n.pt'; const task = data.task || 'detect'; const stdModels = standardModels[task] || []; const allAvailable = [...stdModels, ...discoveredModels.map(m => m.name)]; if (allAvailable.includes(modelVal)) { modelSelect.value = modelVal; modelCustomWrapper.style.display = 'none'; document.getElementById('model').value = modelVal; } else { modelSelect.value = '__custom__'; modelCustomWrapper.style.display = 'block'; document.getElementById('model').value = modelVal; } const matchedDataset = discoveredDatasets.find(d => d.path === data.dataset); if (matchedDataset) { datasetSelect.value = data.dataset; datasetCustomWrapper.style.display = 'none'; document.getElementById('dataset').value = data.dataset; } else { datasetSelect.value = '__custom__'; datasetCustomWrapper.style.display = 'block'; document.getElementById('dataset').value = data.dataset || ''; } // Split splitEnabled.checked = data.split?.enabled ?? false; splitRatio.value = data.split?.train_ratio ?? 0.8; splitClasses.value = data.split?.classes_path || ''; // Training params document.getElementById('epochs').value = data.epochs ?? 100; document.getElementById('image-size').value = data.image_size ?? 640; document.getElementById('batch-size').value = data.batch_size ?? 16; document.getElementById('device').value = data.device || ''; document.getElementById('workers').value = data.workers ?? 8; document.getElementById('patience').value = data.patience ?? 100; document.getElementById('project').value = data.project || 'runs/train'; document.getElementById('run-name').value = data.run_name || ''; // Augmentation augmentationEnabled.checked = data.augmentation?.enabled !== false; if (data.augmentation) { document.getElementById('hsv-h').value = data.augmentation.hsv_h ?? 0.015; document.getElementById('hsv-s').value = data.augmentation.hsv_s ?? 0.7; document.getElementById('hsv-v').value = data.augmentation.hsv_v ?? 0.4; document.getElementById('degrees').value = data.augmentation.degrees ?? 0.0; document.getElementById('translate').value = data.augmentation.translate ?? 0.1; document.getElementById('scale').value = data.augmentation.scale ?? 0.5; document.getElementById('shear').value = data.augmentation.shear ?? 0.0; document.getElementById('perspective').value = data.augmentation.perspective ?? 0.0; document.getElementById('close-mosaic').value = data.augmentation.close_mosaic ?? 10; document.getElementById('flipud').value = data.augmentation.flipud ?? 0.0; document.getElementById('fliplr').value = data.augmentation.fliplr ?? 0.5; document.getElementById('bgr').value = data.augmentation.bgr ?? 0.0; document.getElementById('mosaic').value = data.augmentation.mosaic ?? 1.0; document.getElementById('mixup').value = data.augmentation.mixup ?? 0.0; document.getElementById('cutmix').value = data.augmentation.cutmix ?? 0.0; document.getElementById('copy-paste').value = data.augmentation.copy_paste ?? 0.0; document.getElementById('erasing').value = data.augmentation.erasing ?? 0.4; document.getElementById('copy-paste-mode').value = data.augmentation.copy_paste_mode || 'flip'; document.getElementById('auto-augment').value = data.augmentation.auto_augment || 'randaugment'; } // MLflow mlflowEnabled.checked = data.mlflow?.enabled !== false; if (data.mlflow) { document.getElementById('tracking-uri').value = data.mlflow.tracking_uri || 'sqlite:///mlflow.db'; document.getElementById('experiment-name').value = data.mlflow.experiment_name || 'yolo-webui'; document.getElementById('mlflow-run-name').value = data.mlflow.run_name || ''; } // Sync disables updateSplitFields(); updateAugmentationFields(); updateMlflowFields(); } // --- Load Configuration (Last Run or Defaults) --- async function loadInitialConfig() { // 1. Try loading draft configuration from localStorage const draft = localStorage.getItem('draft_config'); if (draft) { try { const data = JSON.parse(draft); applyConfig(data); addLogLine('Восстановлены последние измененные параметры.', 'info'); return; } catch (e) { // Ignore and fall back } } // 2. First check if last_run exists try { const lastRes = await fetch('/api/sessions/last_run'); if (lastRes.ok) { const data = await lastRes.json(); applyConfig(data); addLogLine('Загружена конфигурация последнего запуска.', 'info'); return; } } catch (e) { // Silence fail to fall back to defaults } // 3. Fall back to defaults try { const res = await fetch('/api/config/defaults'); if (!res.ok) throw new Error('Failed to fetch defaults'); const data = await res.json(); applyConfig(data); } catch (err) { console.error('Error loading defaults:', err); showNotification('Ошибка загрузки настроек по умолчанию', 'error'); } } // --- Sessions Management --- async function loadSessionsList() { try { const res = await fetch('/api/sessions'); if (!res.ok) throw new Error(); const names = await res.json(); // Re-populate select const currentValue = sessionSelect.value; sessionSelect.innerHTML = ''; names.forEach(name => { const opt = document.createElement('option'); opt.value = name; opt.textContent = name; sessionSelect.appendChild(opt); }); // Restore selection if still exists const savedProfile = localStorage.getItem('selected_profile') || ""; const finalValue = currentValue || savedProfile; if (names.includes(finalValue)) { sessionSelect.value = finalValue; sessionDeleteBtn.disabled = false; } else { sessionSelect.value = ""; sessionDeleteBtn.disabled = true; } } catch (e) { console.error("Failed to load sessions list:", e); } } // --- Datasets Auto-Discovery --- let discoveredDatasets = []; async function loadDatasetsList() { try { const res = await fetch('/api/datasets'); if (!res.ok) throw new Error(); discoveredDatasets = await res.json(); // Re-populate select datasetSelect.innerHTML = ''; discoveredDatasets.forEach(item => { const opt = document.createElement('option'); opt.value = item.path; opt.textContent = `${item.name} (${item.type === 'directory' ? 'Папка' : 'Конфиг'})`; datasetSelect.appendChild(opt); }); // Add custom path option const customOpt = document.createElement('option'); customOpt.value = '__custom__'; customOpt.textContent = 'Указать путь вручную...'; datasetSelect.appendChild(customOpt); updateDatasetFieldsState(); } catch (e) { console.error("Failed to load datasets list:", e); datasetSelect.innerHTML = ''; updateDatasetFieldsState(); } } function updateDatasetFieldsState() { const val = datasetSelect.value; const datasetInput = document.getElementById('dataset'); if (val === '__custom__') { datasetCustomWrapper.style.display = 'block'; } else { datasetCustomWrapper.style.display = 'none'; datasetInput.value = val; } } datasetSelect.addEventListener('change', updateDatasetFieldsState); async function loadModelsList() { try { const res = await fetch('/api/models'); if (!res.ok) throw new Error(); discoveredModels = await res.json(); updateExportModelOptions(); } catch (e) { console.error("Failed to load models list:", e); } } modelSelect.addEventListener('change', updateModelFieldsState); // --- Export Model Selection --- const exportModelSelect = document.getElementById('export-model-select'); const exportModelCustomWrapper = document.getElementById('export-model-custom-wrapper'); const exportModelInput = document.getElementById('export-model'); function updateExportModelOptions() { exportModelSelect.innerHTML = ''; if (discoveredModels.length > 0) { const localGroup = document.createElement('optgroup'); localGroup.label = 'Локальные/обученные модели'; discoveredModels.forEach(m => { const opt = document.createElement('option'); opt.value = m.path; opt.textContent = m.source === 'runs' ? `[RUNS] ${m.name}` : m.name; localGroup.appendChild(opt); }); exportModelSelect.appendChild(localGroup); } const customOpt = document.createElement('option'); customOpt.value = '__custom__'; customOpt.textContent = 'Указать путь вручную...'; exportModelSelect.appendChild(customOpt); updateExportModelFieldsState(); } function updateExportModelFieldsState() { const val = exportModelSelect.value; if (val === '__custom__') { exportModelCustomWrapper.style.display = 'block'; } else { exportModelCustomWrapper.style.display = 'none'; exportModelInput.value = val; } } exportModelSelect.addEventListener('change', updateExportModelFieldsState); sessionSelect.addEventListener('change', async () => { const name = sessionSelect.value; localStorage.setItem('selected_profile', name); if (!name) { sessionDeleteBtn.disabled = true; localStorage.removeItem('draft_config'); // Reset draft await loadInitialConfig(); return; } sessionDeleteBtn.disabled = false; try { const res = await fetch(`/api/sessions/${name}`); if (!res.ok) throw new Error(); const data = await res.json(); applyConfig(data); localStorage.setItem('draft_config', JSON.stringify(data)); showNotification(`Профиль "${name}" успешно загружен.`, 'success'); } catch (e) { showNotification('Не удалось загрузить выбранный профиль.', 'error'); } }); sessionSaveBtn.addEventListener('click', async () => { const name = sessionNameInput.value.trim().replace(/[^a-zA-Z0-9_\-]/g, ""); if (!name) { showNotification('Введите корректное имя профиля (латиница, цифры, дефисы).', 'warning'); return; } if (name === "last_run") { showNotification('Имя "last_run" зарезервировано бэкендом.', 'warning'); return; } const config = getFormConfig(); try { const res = await fetch(`/api/sessions/${name}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); if (!res.ok) throw new Error(); showNotification(`Профиль "${name}" сохранен.`, 'success'); sessionNameInput.value = ""; localStorage.setItem('selected_profile', name); localStorage.setItem('draft_config', JSON.stringify(config)); await loadSessionsList(); sessionSelect.value = name; sessionDeleteBtn.disabled = false; } catch (e) { showNotification('Не удалось сохранить профиль.', 'error'); } }); sessionDeleteBtn.addEventListener('click', async () => { const name = sessionSelect.value; if (!name) return; if (!confirm(`Вы действительно хотите удалить профиль "${name}"?`)) return; try { const res = await fetch(`/api/sessions/${name}`, { method: 'DELETE' }); if (!res.ok) throw new Error(); showNotification(`Профиль "${name}" удален.`, 'success'); sessionSelect.value = ""; sessionDeleteBtn.disabled = true; localStorage.removeItem('selected_profile'); localStorage.removeItem('draft_config'); await loadSessionsList(); await loadInitialConfig(); } catch (e) { console.error('Delete profile error:', e); showNotification('Не удалось удалить профиль.', 'error'); } }); // --- Form submit --- async function startTraining() { if (isTrainingActive) return; const config = getFormConfig(); try { const res = await fetch('/api/train/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const data = await res.json(); if (!res.ok) { throw new Error(data.detail || 'Failed to start training'); } showNotification('Обучение успешно запущено!', 'success'); } catch (err) { console.error('Start error:', err); showNotification(err.message, 'error'); } } async function stopTraining() { if (!isTrainingActive) return; try { const res = await fetch('/api/train/stop', { method: 'POST' }); if (!res.ok) { const data = await res.json(); throw new Error(data.detail || 'Failed to stop training'); } showNotification('Запрос на остановку отправлен.', 'info'); } catch (err) { console.error('Stop error:', err); showNotification(err.message, 'error'); } } if (configForm) { configForm.addEventListener('input', () => { const config = getFormConfig(); localStorage.setItem('draft_config', JSON.stringify(config)); }); configForm.addEventListener('change', () => { const config = getFormConfig(); localStorage.setItem('draft_config', JSON.stringify(config)); }); } startBtn.addEventListener('click', startTraining); stopBtn.addEventListener('click', stopTraining); // --- Helper Notification System --- function showNotification(message, type = 'info') { const toast = document.createElement('div'); toast.style.position = 'fixed'; toast.style.bottom = '20px'; toast.style.right = '20px'; toast.style.padding = '12px 20px'; toast.style.borderRadius = '8px'; toast.style.fontFamily = 'Outfit'; toast.style.fontSize = '0.9rem'; toast.style.fontWeight = '500'; toast.style.zIndex = '9999'; toast.style.boxShadow = '0 10px 25px rgba(0,0,0,0.5)'; toast.style.animation = 'slideIn 0.3s cubic-bezier(0.4, 0, 0.2, 1)'; toast.style.maxWidth = '350px'; if (type === 'success') { toast.style.backgroundColor = 'var(--success)'; toast.style.color = '#000'; } else if (type === 'error') { toast.style.backgroundColor = 'var(--error)'; toast.style.color = '#fff'; } else if (type === 'warning') { toast.style.backgroundColor = 'var(--warning)'; toast.style.color = '#000'; } else { toast.style.backgroundColor = 'var(--accent)'; toast.style.color = '#fff'; } toast.textContent = message; document.body.appendChild(toast); setTimeout(() => { toast.style.animation = 'fadeOut 0.5s ease forwards'; setTimeout(() => toast.remove(), 500); }, 4000); } // Add keyframes dynamically if not in stylesheet const styleSheet = document.createElement("style"); styleSheet.innerText = ` @keyframes slideIn { from { transform: translateY(100%) scale(0.9); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } } @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } } `; document.head.appendChild(styleSheet); // --- Export Logic --- const exportStartBtn = document.getElementById('export-start-btn'); const exportStopBtn = document.getElementById('export-stop-btn'); const exportStatusCard = document.getElementById('export-status-card'); const exportStatusTitle = document.getElementById('export-status-title'); const exportStatusText = document.getElementById('export-status-text'); const exportLogContainer = document.getElementById('export-log-container'); const exportClearLogBtn = document.getElementById('export-clear-log-btn'); const exportAutoscrollCheck = document.getElementById('export-autoscroll'); let isExportActive = false; let exportSocket = null; function addExportLogLine(message, level = 'info') { const line = document.createElement('div'); line.className = `log-line log-level-${level.toLowerCase()}`; line.textContent = message; exportLogContainer.appendChild(line); if (exportAutoscrollCheck.checked) { exportLogContainer.scrollTop = exportLogContainer.scrollHeight; } } exportClearLogBtn.addEventListener('click', () => { exportLogContainer.innerHTML = ''; }); function connectExportWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/export/ws`; exportSocket = new WebSocket(wsUrl); exportSocket.onopen = () => { addExportLogLine('Соединение с сервером установлено.', 'info'); }; exportSocket.onclose = () => { addExportLogLine('Соединение потеряно. Повторная попытка через 5 секунд...', 'warning'); setTimeout(connectExportWebSocket, 5000); }; exportSocket.onerror = (err) => { console.error('Export WS Error:', err); }; exportSocket.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'init') { updateExportUIStatus(data.status); exportLogContainer.innerHTML = ''; data.logs.forEach(([levelCode, msg]) => { const level = levelCode.replace('__LOG_LEVEL_', '').replace('__', '').toLowerCase(); addExportLogLine(msg, level); }); } else if (data.type === 'status') { updateExportUIStatus(data.status); if (data.output_dir) { addExportLogLine(`Результаты сохранены: ${data.output_dir}`, 'success'); } } else if (data.type === 'log') { addExportLogLine(data.message, data.level); } }; } function updateExportUIStatus(status) { exportStatusCard.className = `status-${status}`; switch (status) { case 'idle': exportStatusTitle.textContent = 'ГОТОВО К ЭКСПОРТУ'; exportStatusText.textContent = 'Выберите модель и формат для начала.'; isExportActive = false; exportStartBtn.disabled = false; exportStopBtn.disabled = true; break; case 'preparing': case 'exporting': exportStatusTitle.textContent = 'ЭКСПОРТ'; exportStatusText.textContent = 'Выполняется экспорт модели...'; isExportActive = true; exportStartBtn.disabled = true; exportStopBtn.disabled = false; break; case 'stopping': exportStatusTitle.textContent = 'ОСТАНОВКА'; exportStatusText.textContent = 'Остановка процесса экспорта...'; isExportActive = true; exportStartBtn.disabled = true; exportStopBtn.disabled = true; break; case 'succeeded': exportStatusTitle.textContent = 'ГОТОВО'; exportStatusText.textContent = 'Экспорт успешно завершен.'; isExportActive = false; exportStartBtn.disabled = false; exportStopBtn.disabled = true; break; case 'cancelled': exportStatusTitle.textContent = 'ОСТАНОВЛЕНО'; exportStatusText.textContent = 'Экспорт остановлен пользователем.'; isExportActive = false; exportStartBtn.disabled = false; exportStopBtn.disabled = true; break; case 'failed': exportStatusTitle.textContent = 'ОШИБКА'; exportStatusText.textContent = 'Экспорт завершился с ошибкой.'; isExportActive = false; exportStartBtn.disabled = false; exportStopBtn.disabled = true; break; } } async function startExport() { if (isExportActive) return; const modelVal = document.getElementById('export-model').value.trim(); if (!modelVal) { showNotification('Пожалуйста, выберите или укажите модель для экспорта.', 'warning'); return; } const config = { model: modelVal, format: document.getElementById('export-format').value, imgsz: readNumber('export-imgsz', 640, true), half: document.getElementById('export-half').checked, int8: document.getElementById('export-int8').checked, dynamic: document.getElementById('export-dynamic').checked }; try { const res = await fetch('/api/export/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const data = await res.json(); if (!res.ok) { throw new Error(data.detail || 'Failed to start export'); } showNotification('Экспорт успешно запущен!', 'success'); } catch (err) { console.error('Export start error:', err); showNotification(err.message, 'error'); } } async function stopExport() { if (!isExportActive) return; try { const res = await fetch('/api/export/stop', { method: 'POST' }); if (!res.ok) { const data = await res.json(); throw new Error(data.detail || 'Failed to stop export'); } showNotification('Запрос на остановку отправлен.', 'info'); } catch (err) { console.error('Export stop error:', err); showNotification(err.message, 'error'); } } exportStartBtn.addEventListener('click', startExport); exportStopBtn.addEventListener('click', stopExport); // Initial load sequence loadDatasetsList().then(() => { return loadModelsList(); }).then(() => { return loadInitialConfig(); }).then(() => { loadSessionsList(); connectWebSocket(); connectExportWebSocket(); initChart(); }); });