/** Change Detection tab — pick library images and run comparison. */ const compareState = { t1: null, t2: null, pickingSlot: null, selectedNode: null, allLibraryItems: [] }; function compareFormatBytes(n) { if (n >= 1024 ** 3) return (n / 1024 ** 3).toFixed(1) + ' GB'; if (n >= 1024 ** 2) return (n / 1024 ** 2).toFixed(1) + ' MB'; return (n / 1024).toFixed(0) + ' KB'; } function thumbUrlFor(path) { return `/api/dda/local/thumb?path=${encodeURIComponent(path)}`; } function encodePath(path) { return encodeURIComponent(path || ''); } function decodePath(encoded) { try { return decodeURIComponent(encoded || ''); } catch (_) { return encoded || ''; } } function ensureDdaState() { window.ddaState = window.ddaState || {}; return window.ddaState; } function updateRunButton() { const btn = document.getElementById('btn-run-job'); if (btn) btn.disabled = !(compareState.t1 && compareState.t2); } function renderSlotPreview(slotKey, selection) { const wrap = document.getElementById(slotKey === 't1' ? 'slot-t1-preview' : 'slot-t2-preview'); const slot = document.getElementById(slotKey === 't1' ? 'slot-t1' : 'slot-t2'); if (!wrap || !slot) return; if (!selection) { wrap.innerHTML = '
No image selected
'; slot.classList.remove('filled'); return; } slot.classList.add('filled'); const thumb = selection.thumbUrl || thumbUrlFor(selection.path); wrap.innerHTML = `No images in ${escapeHtml(compareSelectionTitle())}. Select another folder or click Refresh to sync from disk.
`; return; } grid.innerHTML = items.map((img) => { const thumb = img.thumbUrl || thumbUrlFor(img.path); const enc = encodePath(img.path); const t1Sel = compareState.t1?.path === img.path ? ' selected-t1' : ''; const t2Sel = compareState.t2?.path === img.path ? ' selected-t2' : ''; const safeName = img.filename.replace(/ ${thumb ? `Loading library images…
'; if (title) title.textContent = `Pick from library — ${compareSelectionTitle()}`; updateCompareFolderPath(); try { const allItems = await ddaApi('GET', '/api/dda/local/images'); compareState.allLibraryItems = allItems; populateSelects(allItems); const params = new URLSearchParams(); if (compareState.selectedNode?.id) params.set('node_id', String(compareState.selectedNode.id)); const gridItems = compareState.selectedNode?.id ? await ddaApi('GET', '/api/dda/local/images?' + params.toString()) : allItems; ensureDdaState().libraryItems = gridItems; renderCompareGrid(gridItems); } catch (err) { grid.innerHTML = `Could not load images: ${err.message}
`; if (typeof showDdaError === 'function') showDdaError(err.message); } } async function openPicker(slotKey) { compareState.pickingSlot = slotKey; const modal = document.getElementById('dda-picker-modal'); const list = document.getElementById('dda-picker-list'); const title = document.getElementById('dda-picker-title'); if (!modal || !list) return; if (title) { title.textContent = slotKey === 't1' ? 'Select Old Image (T1)' : 'Select New Image (T2)'; } list.innerHTML = 'Loading library…
'; modal.classList.remove('hidden'); let items = ensureDdaState().libraryItems || []; try { if (compareState.selectedNode?.id) { const params = new URLSearchParams(); params.set('node_id', String(compareState.selectedNode.id)); items = await ddaApi('GET', '/api/dda/local/images?' + params.toString()); } else { items = compareState.allLibraryItems.length ? compareState.allLibraryItems : await ddaApi('GET', '/api/dda/local/images'); compareState.allLibraryItems = items; } populateSelects(compareState.allLibraryItems.length ? compareState.allLibraryItems : items); } catch (err) { list.innerHTML = `Could not load images: ${err.message}
`; return; } if (!items.length) { list.innerHTML = 'No images in library. Add files under library_sources/YEAR/ and refresh.
'; } else { list.innerHTML = items.map((img) => { const enc = encodePath(img.path); return ` `; }).join(''); list.querySelectorAll('.dda-picker-item').forEach((btn) => { btn.addEventListener('click', () => { const slot = compareState.pickingSlot; if (!slot) return; setSlot(slot, findLibraryItem(btn.dataset.path)); closePicker(); }); }); } } function closePicker() { document.getElementById('dda-picker-modal')?.classList.add('hidden'); compareState.pickingSlot = null; } function setupCompareInteractions() { const slotsWrap = document.querySelector('.dda-compare-slots'); if (slotsWrap) { slotsWrap.addEventListener('click', (e) => { const pickBtn = e.target.closest('.dda-slot-pick'); if (pickBtn) { e.preventDefault(); openPicker(pickBtn.dataset.pick); } }); } document.getElementById('select-t1')?.addEventListener('change', (e) => { const path = e.target.value; if (!path) { compareState.t1 = null; renderSlotPreview('t1', null); updateRunButton(); refreshCompareLibrarySelection(); return; } setSlot('t1', findLibraryItem(path)); }); document.getElementById('select-t2')?.addEventListener('change', (e) => { const path = e.target.value; if (!path) { compareState.t2 = null; renderSlotPreview('t2', null); updateRunButton(); refreshCompareLibrarySelection(); return; } setSlot('t2', findLibraryItem(path)); }); const grid = document.getElementById('compare-lib-grid'); if (grid) { grid.addEventListener('click', (e) => { const btn = e.target.closest('[data-assign]'); if (!btn) return; e.preventDefault(); e.stopPropagation(); setSlot(btn.dataset.assign, findLibraryItem(btn.dataset.path)); }); } ['slot-t1', 'slot-t2'].forEach((id) => { const el = document.getElementById(id); if (!el) return; el.addEventListener('dragover', (e) => { e.preventDefault(); el.classList.add('drag-over'); }); el.addEventListener('dragleave', () => el.classList.remove('drag-over')); el.addEventListener('drop', (e) => { e.preventDefault(); el.classList.remove('drag-over'); const path = e.dataTransfer.getData('application/x-dda-image-path') || e.dataTransfer.getData('text/plain'); if (!path) return; setSlot(id === 'slot-t1' ? 't1' : 't2', findLibraryItem(path)); }); }); document.getElementById('btn-compare-refresh')?.addEventListener('click', async () => { const btn = document.getElementById('btn-compare-refresh'); btn.disabled = true; try { const data = await window.ddaState.rescan(); const sync = data.sync || {}; const parts = [`${data.totalImages || 0} image(s)`]; if (sync.nodesCreated) parts.push(`${sync.nodesCreated} folder(s) imported`); if (sync.imagesIndexed) parts.push(`${sync.imagesIndexed} image(s) indexed`); if (typeof showDdaSuccess === 'function') showDdaSuccess(`Library synced — ${parts.join(', ')}.`); await loadCompareLibraryGrid(); } catch (err) { if (typeof showDdaError === 'function') showDdaError(err.message); } finally { btn.disabled = false; } }); document.getElementById('dda-picker-close')?.addEventListener('click', closePicker); document.getElementById('dda-picker-modal')?.addEventListener('click', (e) => { if (e.target.id === 'dda-picker-modal') closePicker(); }); document.getElementById('btn-run-job')?.addEventListener('click', runLibraryDetection); const notifyCb = document.getElementById('dda-detect-notify'); const notifyEmail = document.getElementById('dda-detect-notify-email'); notifyCb?.addEventListener('change', () => { if (notifyEmail) notifyEmail.classList.toggle('hidden', !notifyCb.checked); }); document.addEventListener('keydown', (e) => { if (e.key !== 'Escape') return; const picker = document.getElementById('dda-picker-modal'); if (picker && !picker.classList.contains('hidden')) closePicker(); }); } function showDetectResult(data) { if (typeof showDdaResult === 'function') showDdaResult(data); else if (typeof showDdaError === 'function') showDdaError('Result viewer failed to load.'); } function setDetectProgress(pct, stage) { const fill = document.getElementById('detect-progress-fill'); const label = document.getElementById('detect-progress-label'); const clamped = Math.max(0, Math.min(100, Number(pct) || 0)); if (fill) fill.style.width = `${clamped}%`; const text = stage ? `${stage} — ${clamped}%` : `${clamped}%`; if (label) label.textContent = text; } function showDetectProgress() { const wrap = document.getElementById('detect-progress'); wrap?.classList.remove('hidden'); setDetectProgress(0, 'Starting detection'); } function hideDetectProgress(delayMs = 0) { const hide = () => document.getElementById('detect-progress')?.classList.add('hidden'); if (delayMs > 0) setTimeout(hide, delayMs); else hide(); } async function runDetectionWithFallback(form) { setDetectProgress(2, 'Queuing detection job'); try { const queued = await ddaApi('POST', '/api/dda/jobs', { body: form }); return await pollJobUntilDone(queued.jobId); } catch (err) { const msg = String(err.message || ''); const useSync = msg.includes('Not Found') || msg.includes('404') || msg.includes('503') || msg.includes('409') || msg.includes('busy') || msg.includes('Internal Server Error') || msg.includes('NOT NULL'); if (!useSync) throw err; return runSyncDetectionWithProgress(form); } } async function runSyncDetectionWithProgress(form) { let pct = 5; setDetectProgress(pct, 'Running detection (sync)'); const timer = setInterval(() => { pct = Math.min(92, pct + (pct < 50 ? 4 : 2)); setDetectProgress(pct, 'Running detection (sync)'); }, 1500); try { const result = await ddaApi('POST', '/api/dda/detect/from-library', { body: form }); setDetectProgress(100, 'Complete'); return { result, jobId: null }; } finally { clearInterval(timer); } } async function pollJobUntilDone(jobId) { const maxAttempts = 600; for (let i = 0; i < maxAttempts; i++) { const job = await ddaApi('GET', `/api/dda/jobs/${jobId}`); const status = job.status; const pct = job.progressPct ?? (status === 'queued' ? 0 : 10); const stage = job.progressStage || (status === 'queued' ? 'Queued' : 'Running'); setDetectProgress(pct, stage); if (status === 'completed') { setDetectProgress(100, 'Complete'); if (job.result) { if (typeof window.refreshDdaNotifications === 'function') window.refreshDdaNotifications(); return { result: job.result, jobId }; } if (job.runId) { const data = await ddaApi('GET', `/api/history/${job.runId}`); if (typeof window.refreshDdaNotifications === 'function') window.refreshDdaNotifications(); return { result: data, jobId }; } if (job.resultError) throw new Error(job.resultError); } if (status === 'failed') throw new Error(job.errorMessage || 'Detection job failed'); await new Promise((r) => setTimeout(r, 2000)); } throw new Error('Detection timed out. Check Reports tab for job status.'); } async function runLibraryDetection() { if (!compareState.t1 || !compareState.t2) { if (typeof showDdaError === 'function') showDdaError('Select both T1 (old) and T2 (new) images first.'); return; } const btn = document.getElementById('btn-run-job'); if (typeof hideDdaError === 'function') hideDdaError(); btn.disabled = true; showDetectProgress(); const form = new FormData(); form.append('base_path', compareState.t1.path); form.append('comparison_path', compareState.t2.path); form.append('title', `${compareState.t1.filename} vs ${compareState.t2.filename}`); form.append('method', document.getElementById('dda-detect-method')?.value || 'AI-Based Deep Learning'); form.append('enable_registration', String(document.getElementById('dda-detect-registration')?.checked !== false)); form.append('enable_normalization', String(document.getElementById('dda-detect-normalization')?.checked !== false)); form.append('detection_sensitivity', String(Math.max(0, Math.min(1, Number(document.getElementById('dda-detect-sensitivity')?.value ?? 0.45))))); const minArea = Number(document.getElementById('dda-detect-min-area')?.value ?? 150); if (!Number.isNaN(minArea) && minArea >= 50) form.append('min_region_area', String(Math.round(minArea))); const notifyCb = document.getElementById('dda-detect-notify'); const notifyEmail = document.getElementById('dda-detect-notify-email'); if (notifyCb?.checked && notifyEmail?.value?.trim()) { form.append('notify_email', notifyEmail.value.trim()); } try { const { result: data, jobId } = await runDetectionWithFallback(form); if (jobId && typeof window.markDdaJobSeen === 'function') window.markDdaJobSeen(jobId); showDetectResult(data); if (typeof showDdaSuccess === 'function') { let msg = 'Detection complete.'; if (data.notificationSent) msg += ' Report email sent.'; showDdaSuccess(msg); } if (typeof loadReportsList === 'function') loadReportsList(); } catch (err) { if (typeof showDdaError === 'function') showDdaError(err.message || 'Detection failed'); } finally { btn.disabled = !(compareState.t1 && compareState.t2); hideDetectProgress(2000); } } let compareInitialized = false; function initCompareTab() { if (!compareInitialized) { if (typeof registerCompareTreeSidebar === 'function') { registerCompareTreeSidebar({ containerId: 'compare-tree', searchId: 'compare-tree-search', allBtnId: 'btn-compare-tree-all', getSelectedNode: () => compareState.selectedNode, onNodeSelect: (node) => { compareState.selectedNode = node; loadCompareLibraryGrid(); }, onClearNode: () => { compareState.selectedNode = null; loadCompareLibraryGrid(); }, }); } setupCompareInteractions(); compareInitialized = true; } updateRunButton(); if (typeof loadTree === 'function') { loadTree().then(() => loadCompareLibraryGrid()).catch(() => loadCompareLibraryGrid()); } else { loadCompareLibraryGrid(); } } window.loadCompareLibraryGrid = loadCompareLibraryGrid; window.initCompareTab = initCompareTab; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initCompareTab); } else { initCompareTab(); }