// DXA Quality Assessment Web Interface const API_BASE = ''; let results = []; let sortColumn = null; let sortDirection = 'asc'; // DOM Elements const dropZone = document.getElementById('dropZone'); const fileInput = document.getElementById('fileInput'); const progressSection = document.getElementById('progressSection'); const progressBar = document.getElementById('progressBar'); const progressText = document.getElementById('progressText'); const progressPercent = document.getElementById('progressPercent'); const resultsSection = document.getElementById('resultsSection'); const resultsTable = document.getElementById('resultsTable'); const emptyResults = document.getElementById('emptyResults'); const errorSection = document.getElementById('errorSection'); const errorMessage = document.getElementById('errorMessage'); const statusBanner = document.getElementById('statusBanner'); const statusIcon = document.getElementById('statusIcon'); const statusText = document.getElementById('statusText'); // Theme toggle const themeToggle = document.getElementById('themeToggle'); if (themeToggle) { themeToggle.addEventListener('click', () => { document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', document.documentElement.classList.contains('dark') ? 'dark' : 'light'); }); // Load saved theme if (localStorage.getItem('theme') === 'dark' || (!localStorage.getItem('theme') && window.matchMedia('(prefers-color-scheme: dark)').matches)) { document.documentElement.classList.add('dark'); } } // Check API health async function checkHealth() { try { const response = await fetch(`${API_BASE}/api/v1/health`); const data = await response.json(); if (data.status === 'ok') { statusIcon.className = 'w-3 h-3 rounded-full bg-green-500'; statusText.textContent = `Сервис готов • Модель: ${data.model_loaded ? 'загружена' : 'не загружена'} • Устройство: ${data.device}`; } else { statusIcon.className = 'w-3 h-3 rounded-full bg-yellow-500'; statusText.textContent = 'Проблемы с сервисом'; } } catch (e) { statusIcon.className = 'w-3 h-3 rounded-full bg-red-500'; statusText.textContent = 'Сервис недоступен'; } } // Drag and drop dropZone.addEventListener('click', () => fileInput.click()); dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); }); dropZone.addEventListener('dragleave', () => { dropZone.classList.remove('drag-over'); }); dropZone.addEventListener('drop', (e) => { e.preventDefault(); dropZone.classList.remove('drag-over'); handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', (e) => { handleFiles(e.target.files); }); // Filter inputs const searchInput = document.getElementById('searchInput'); const filterRegion = document.getElementById('filterRegion'); const filterQuality = document.getElementById('filterQuality'); searchInput.addEventListener('input', renderTable); filterRegion.addEventListener('change', renderTable); filterQuality.addEventListener('change', renderTable); // Sort headers document.querySelectorAll('th[data-sort]').forEach(th => { th.addEventListener('click', () => { const column = th.dataset.sort; if (sortColumn === column) { sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; } else { sortColumn = column; sortDirection = 'asc'; } renderTable(); updateSortIcons(); }); }); function updateSortIcons() { document.querySelectorAll('th[data-sort]').forEach(th => { const icon = th.querySelector('i'); if (th.dataset.sort === sortColumn) { icon.className = `fas fa-sort-${sortDirection === 'asc' ? 'up' : 'down'} ml-1`; } else { icon.className = 'fas fa-sort ml-1'; } }); } // Handle files async function handleFiles(files) { if (files.length === 0) return; results = []; errorSection.classList.add('hidden'); resultsSection.classList.remove('hidden'); progressSection.classList.remove('hidden'); const dcmFiles = Array.from(files).filter(f => f.name.toLowerCase().endsWith('.dcm') || f.type === 'application/dicom' || f.name.toLowerCase().includes('dcm') ); if (dcmFiles.length === 0) { showError('DICOM файлы не найдены. Пожалуйста, выберите файлы с расширением .dcm'); progressSection.classList.add('hidden'); return; } progressText.textContent = `Анализ ${dcmFiles.length} файлов...`; progressBar.style.width = '0%'; progressPercent.textContent = '0%'; let processed = 0; for (const file of dcmFiles) { try { const formData = new FormData(); formData.append('file', file); const response = await fetch(`${API_BASE}/api/v1/analyze`, { method: 'POST', body: formData }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const data = await response.json(); results.push({ filename: file.name, ...data }); } catch (e) { results.push({ filename: file.name, quality_class: -1, quality_label: 'Error', processing_status: `Failure: ${e.message}`, confidence: 0 }); } processed++; const percent = Math.round((processed / dcmFiles.length) * 100); progressBar.style.width = `${percent}%`; progressPercent.textContent = `${percent}%`; } progressSection.classList.add('hidden'); updateStats(); renderTable(); } // Update stats function updateStats() { const total = results.length; const ok = results.filter(r => r.quality_class === 0).length; const violation = results.filter(r => r.quality_class === 1).length; const avgConfidence = results.length > 0 ? (results.reduce((sum, r) => sum + (r.confidence || 0), 0) / results.length * 100).toFixed(1) : 0; document.getElementById('totalCount').textContent = total; document.getElementById('okCount').textContent = ok; document.getElementById('violationCount').textContent = violation; document.getElementById('accuracy').textContent = `${avgConfidence}%`; } // Filter and sort results function getFilteredResults() { let filtered = [...results]; // Search filter const search = searchInput.value.toLowerCase(); if (search) { filtered = filtered.filter(r => (r.filename || '').toLowerCase().includes(search) || (r.study_uid || '').toLowerCase().includes(search) || (r.image_uid || '').toLowerCase().includes(search) ); } // Region filter const region = filterRegion.value; if (region) { filtered = filtered.filter(r => r.anatomical_region === region); } // Quality filter const quality = filterQuality.value; if (quality !== '') { filtered = filtered.filter(r => String(r.quality_class) === quality); } // Sort if (sortColumn) { filtered.sort((a, b) => { let aVal = a[sortColumn] || ''; let bVal = b[sortColumn] || ''; if (typeof aVal === 'number' && typeof bVal === 'number') { return sortDirection === 'asc' ? aVal - bVal : bVal - aVal; } aVal = String(aVal).toLowerCase(); bVal = String(bVal).toLowerCase(); if (sortDirection === 'asc') { return aVal.localeCompare(bVal); } return bVal.localeCompare(aVal); }); } return filtered; } // Render table function renderTable() { const filtered = getFilteredResults(); if (filtered.length === 0) { resultsTable.innerHTML = ''; emptyResults.classList.remove('hidden'); return; } emptyResults.classList.add('hidden'); resultsTable.innerHTML = filtered.map(r => { const qualityClass = r.quality_class === 0 ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : r.quality_class === 1 ? 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400' : 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300'; const qualityLabel = r.quality_class === 0 ? 'OK' : r.quality_class === 1 ? 'Нарушение' : 'Ошибка'; const regionLabel = r.anatomical_region === 'spine' ? 'Позвоночник' : r.anatomical_region === 'hip' ? 'Бедро' : r.anatomical_region || '—'; const confidence = r.confidence ? (r.confidence * 100).toFixed(1) + '%' : '—'; return `