Spaces:
Sleeping
Sleeping
| const API = ''; | |
| function escapeHtml(text) { | |
| if (text == null) return ''; | |
| return String(text) | |
| .replace(/&/g, '&') | |
| .replace(/</g, '<') | |
| .replace(/>/g, '>') | |
| .replace(/"/g, '"'); | |
| } | |
| function formatApiError(detail) { | |
| if (!detail) return null; | |
| if (typeof detail === 'string') return detail; | |
| if (Array.isArray(detail)) { | |
| return detail.map((d) => (d && d.msg) || JSON.stringify(d)).join('; '); | |
| } | |
| return String(detail); | |
| } | |
| async function ddaApi(method, path, options = {}) { | |
| const headers = { ...options.headers }; | |
| if (options.body && !(options.body instanceof FormData)) { | |
| headers['Content-Type'] = 'application/json'; | |
| } | |
| const res = await fetch(API + path, { method, headers, credentials: 'include', ...options }); | |
| const text = await res.text(); | |
| let data = null; | |
| try { data = text ? JSON.parse(text) : null; } catch (_) {} | |
| if (!res.ok) throw new Error(formatApiError(data?.detail) || res.statusText || 'Request failed'); | |
| return data; | |
| } | |
| function showDdaError(msg) { | |
| const el = document.getElementById('dda-error'); | |
| if (!el) return; | |
| el.textContent = msg; | |
| el.classList.remove('hidden'); | |
| } | |
| function hideDdaError() { | |
| document.getElementById('dda-error')?.classList.add('hidden'); | |
| } | |
| function showDdaSuccess(msg) { | |
| const el = document.getElementById('dda-success'); | |
| if (!el) return; | |
| el.textContent = msg; | |
| el.classList.remove('hidden'); | |
| setTimeout(() => el.classList.add('hidden'), 4000); | |
| } | |
| function formatBytes(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'; | |
| } | |
| let ddaConfig = null; | |
| const selectedNode = { id: null, path: '' }; | |
| window.ddaState = { | |
| get config() { return ddaConfig; }, | |
| get localCfg() { return window._localCfg; }, | |
| get selectedNode() { return { ...selectedNode }; }, | |
| get userRole() { return window._ddaUserRole || 'analyst'; }, | |
| set userRole(r) { window._ddaUserRole = r; }, | |
| setNode(n) { selectedNode.id = n.id; selectedNode.path = n.path || ''; }, | |
| clearNode() { selectedNode.id = null; selectedNode.path = ''; }, | |
| refreshImages: () => loadLibraryImages(), | |
| rescan: () => rescanLibrary(), | |
| }; | |
| document.querySelectorAll('.dda-tab').forEach((btn) => { | |
| btn.addEventListener('click', () => { | |
| document.querySelectorAll('.dda-tab').forEach((b) => b.classList.remove('active')); | |
| document.querySelectorAll('.dda-panel').forEach((p) => p.classList.remove('active')); | |
| btn.classList.add('active'); | |
| const tab = btn.dataset.tab; | |
| document.getElementById('tab-' + tab)?.classList.add('active'); | |
| if (tab === 'detect' && typeof loadCompareLibraryGrid === 'function') { | |
| loadCompareLibraryGrid(); | |
| } | |
| }); | |
| }); | |
| async function rescanLibrary() { | |
| const data = await ddaApi('POST', '/api/dda/local/rescan'); | |
| if (typeof renderAllTrees === 'function') renderAllTrees({ tree: data.tree }, []); | |
| else if (typeof renderTree === 'function') renderTree({ tree: data.tree }, []); | |
| if (typeof populateManageNodeSelect === 'function') populateManageNodeSelect(); | |
| await loadLibraryImages(); | |
| if (typeof loadCompareLibraryGrid === 'function') await loadCompareLibraryGrid(); | |
| return data; | |
| } | |
| async function initDda() { | |
| hideDdaError(); | |
| try { | |
| try { | |
| const me = await ddaApi('GET', '/api/dda/me'); | |
| window.ddaState.userRole = me.role || 'analyst'; | |
| } catch (_) { | |
| window.ddaState.userRole = 'analyst'; | |
| } | |
| ddaConfig = await ddaApi('GET', '/api/dda/config'); | |
| const localCfg = await ddaApi('GET', '/api/dda/local/config'); | |
| window._localCfg = localCfg; | |
| document.getElementById('btn-manage-library')?.classList.toggle('hidden', window.ddaState.userRole !== 'admin'); | |
| const hint = document.getElementById('lib-config-hint'); | |
| if (hint) hint.textContent = localCfg.geotiffEnabled ? 'GeoTIFF ready' : 'GeoTIFF limited'; | |
| const pathEl = document.getElementById('lib-path-display'); | |
| if (pathEl) { | |
| pathEl.textContent = [ | |
| 'Storage root:', | |
| localCfg.storageRoot || localCfg.writablePath || '', | |
| 'Layout: {zone}/{area}/…/Images/file.tif', | |
| ].filter(Boolean).join('\n'); | |
| } | |
| const folderPath = document.getElementById('lib-folder-path'); | |
| if (folderPath) { | |
| folderPath.textContent = localCfg.isHosted | |
| ? 'Hugging Face — tree library' | |
| : `Storage: ${localCfg.storageRoot || ''}`; | |
| } | |
| const instr = document.getElementById('lib-instructions'); | |
| if (instr && localCfg.instructions) instr.textContent = localCfg.instructions; | |
| const uploadLimit = document.getElementById('upload-limit-hint'); | |
| if (uploadLimit && localCfg.maxUploadGb) { | |
| uploadLimit.textContent = `Select a tree node and image type. Max ${localCfg.maxUploadGb} GB per file.`; | |
| } | |
| const resHint = document.getElementById('dda-detect-res-hint'); | |
| if (resHint && localCfg.detectionMaxSide) { | |
| resHint.textContent = `Detection runs at up to ${localCfg.detectionMaxSide}px per side.`; | |
| } | |
| const urlTab = new URLSearchParams(window.location.search).get('tab'); | |
| if (urlTab) document.querySelector(`.dda-tab[data-tab="${urlTab}"]`)?.click(); | |
| if (typeof loadTree === 'function') await loadTree(); | |
| await loadLibraryImages(); | |
| } catch (err) { | |
| showDdaError(err.message || 'Failed to load library'); | |
| } | |
| } | |
| function selectionTitle() { | |
| return selectedNode.path || 'All images'; | |
| } | |
| async function loadLibraryImages() { | |
| const grid = document.getElementById('lib-grid'); | |
| const title = document.getElementById('lib-grid-title'); | |
| if (!grid) return; | |
| const q = document.getElementById('lib-filter')?.value?.trim() || ''; | |
| const params = new URLSearchParams(); | |
| if (selectedNode.id) params.set('node_id', String(selectedNode.id)); | |
| if (q) params.set('q', q); | |
| if (title) title.textContent = `Images — ${selectionTitle()}`; | |
| try { | |
| const items = await ddaApi('GET', '/api/dda/local/images?' + params.toString()); | |
| window.ddaState.libraryItems = items; | |
| if (!items.length) { | |
| grid.innerHTML = `<p class="dim">No images in <strong>${escapeHtml(selectionTitle())}</strong>. Select a node and upload, or use Manage to create folders.</p>`; | |
| return; | |
| } | |
| grid.innerHTML = items.map((img) => { | |
| const thumb = img.thumbUrl || ''; | |
| const crumb = img.breadcrumb || img.nodePath || img.filename; | |
| return ` | |
| <div class="dda-card-img" draggable="true" data-image-path="${img.path.replace(/"/g, '"')}" title="${escapeHtml(img.filename)}"> | |
| ${thumb ? `<img src="${thumb}" alt="" loading="lazy" />` : '<div class="meta">No preview</div>'} | |
| <div class="meta"> | |
| <span class="dim dda-crumb">${escapeHtml(crumb)}</span><br/> | |
| <span class="dim">${img.imageType || ''} · ${formatBytes(img.fileSizeBytes)}</span> | |
| </div> | |
| </div>`; | |
| }).join(''); | |
| grid.querySelectorAll('.dda-card-img').forEach((card) => { | |
| card.addEventListener('dragstart', (e) => { | |
| e.dataTransfer.setData('application/x-dda-image-path', card.dataset.imagePath); | |
| e.dataTransfer.setData('text/plain', card.dataset.imagePath); | |
| }); | |
| }); | |
| if (typeof loadCompareLibraryGrid === 'function') loadCompareLibraryGrid(); | |
| } catch (err) { | |
| grid.innerHTML = `<p class="dim">Could not load images: ${err.message}</p>`; | |
| } | |
| } | |
| document.getElementById('lib-filter')?.addEventListener('input', () => { | |
| clearTimeout(window._libFilterTimer); | |
| window._libFilterTimer = setTimeout(loadLibraryImages, 300); | |
| }); | |
| document.getElementById('btn-refresh-lib')?.addEventListener('click', async () => { | |
| const btn = document.getElementById('btn-refresh-lib'); | |
| btn.disabled = true; | |
| try { | |
| const data = await rescanLibrary(); | |
| 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`); | |
| showDdaSuccess(`Library synced — ${parts.join(', ')}.`); | |
| } catch (err) { | |
| showDdaError(err.message); | |
| } finally { | |
| btn.disabled = false; | |
| } | |
| }); | |
| initDda(); | |