/** * Adjudication Module - Main Controller * * Manages the adjudication workflow: queue navigation, item loading, * decision submission, and timing tracking. */ (function () { 'use strict'; var config = window.ADJ_CONFIG || {}; var schemes = window.ANNOTATION_SCHEMES || []; var queue = []; var currentItemId = null; var currentQueueIndex = -1; var itemStartTime = null; var currentFilter = 'pending'; var navHistory = []; var navHistoryPos = -1; // DOM references var queueList = document.getElementById('adj-queue-list'); var itemView = document.getElementById('adj-item-view'); var emptyState = document.getElementById('adj-empty-state'); var navBar = document.getElementById('adj-nav-bar'); /** * Initialize the adjudication interface */ function init() { loadQueue(currentFilter === 'all' ? null : currentFilter); bindNavigation(); bindFilters(); } /** * Load the adjudication queue from API */ function loadQueue(statusFilter) { var url = '/adjudicate/api/queue'; if (statusFilter && statusFilter !== 'all') { url += '?status=' + statusFilter; } fetch(url) .then(function (r) { return r.json(); }) .then(function (data) { queue = data.items || []; renderQueue(); updateProgress(); // If we had a current item, re-select it (skip history since it's a refresh) if (currentItemId) { var found = queue.findIndex(function (i) { return i.instance_id === currentItemId; }); if (found >= 0) { selectQueueItem(found, true); } } }) .catch(function (err) { console.error('Failed to load adjudication queue:', err); }); } /** * Render the queue sidebar */ function renderQueue() { if (!queueList) return; queueList.innerHTML = ''; if (queue.length === 0) { queueList.innerHTML = '
' + 'No items in queue
'; return; } queue.forEach(function (item, index) { var el = document.createElement('div'); el.className = 'adj-queue-item'; if (item.status === 'completed') el.className += ' completed'; if (item.status === 'skipped') el.className += ' skipped'; if (item.instance_id === currentItemId) el.className += ' active'; var agBadge = getAgreementBadge(item.overall_agreement); el.innerHTML = '
' + AdjudicationForms.escapeHtml(item.instance_id) + '
' + '
' + '' + item.num_annotators + ' annotators' + agBadge + '
'; el.addEventListener('click', function () { selectQueueItem(index); }); queueList.appendChild(el); }); } /** * Get agreement badge HTML */ function getAgreementBadge(agreement) { var pct = Math.round(agreement * 100); var cls = 'adj-agreement-low'; if (agreement >= 0.75) cls = 'adj-agreement-high'; else if (agreement >= 0.5) cls = 'adj-agreement-medium'; return '' + pct + '%'; } /** * Update progress display */ function updateProgress() { fetch('/adjudicate/api/stats') .then(function (r) { return r.json(); }) .then(function (stats) { var fill = document.getElementById('adj-progress-fill'); var text = document.getElementById('adj-progress-text'); if (fill) { fill.style.width = Math.round(stats.completion_rate * 100) + '%'; } if (text) { text.textContent = stats.completed + '/' + stats.total + ' completed (' + Math.round(stats.completion_rate * 100) + '%)'; } }); } /** * Push an item to navigation history (browser-style back/forward) */ function pushToHistory(instanceId) { // Truncate any forward history if (navHistoryPos < navHistory.length - 1) { navHistory = navHistory.slice(0, navHistoryPos + 1); } // Don't push consecutive duplicates if (navHistory.length === 0 || navHistory[navHistory.length - 1] !== instanceId) { navHistory.push(instanceId); } navHistoryPos = navHistory.length - 1; } /** * Select and load a queue item * @param {number} index - Index in the current queue array * @param {boolean} skipHistory - If true, don't push to navigation history */ function selectQueueItem(index, skipHistory) { if (index < 0 || index >= queue.length) return; currentQueueIndex = index; var item = queue[index]; currentItemId = item.instance_id; if (!skipHistory) { pushToHistory(currentItemId); } // Update active state in sidebar queueList.querySelectorAll('.adj-queue-item').forEach(function (el, i) { el.classList.toggle('active', i === index); }); loadItem(item.instance_id); } /** * Load full item data from API */ function loadItem(instanceId) { fetch('/adjudicate/api/item/' + encodeURIComponent(instanceId)) .then(function (r) { return r.json(); }) .then(function (data) { if (data.error) { console.error('Error loading item:', data.error); return; } renderItem(data); // Phase 3: render similar items and annotator signals if (data.similar_items && data.similar_items.length > 0) { renderSimilarItems(data.similar_items); } else { var simPanel = document.getElementById('adj-similar-items-panel'); if (simPanel) simPanel.style.display = 'none'; } if (data.annotator_signals) { renderAnnotatorSignals(data.annotator_signals); } itemStartTime = Date.now(); }) .catch(function (err) { console.error('Failed to load item:', err); }); } /** * Render a loaded item */ function renderItem(data) { var item = data.item; var itemText = data.item_text || ''; var itemData = data.item_data || {}; var decision = data.decision; // Show item view, hide empty state if (emptyState) emptyState.style.display = 'none'; if (itemView) itemView.style.display = 'block'; if (navBar) navBar.style.display = 'flex'; // Item header var idEl = document.getElementById('adj-item-id'); if (idEl) idEl.textContent = 'Item: ' + item.instance_id; var agEl = document.getElementById('adj-item-agreement'); if (agEl) { agEl.innerHTML = getAgreementBadge(item.overall_agreement); } var annEl = document.getElementById('adj-item-annotators'); if (annEl) annEl.textContent = item.num_annotators + ' annotators'; // Check if any schema is a span type var hasSpans = schemes.some(function(s) { return s.annotation_type === 'span'; }); // Item text - render with span overlay container if needed var textEl = document.getElementById('adj-item-text'); if (textEl) { if (hasSpans && itemText) { // Render text with a span overlay container for dashed overlays textEl.innerHTML = '
' + '
' + '
' + AdjudicationForms.escapeHtml(itemText) + '
'; } else if (typeof itemData === 'object' && Object.keys(itemData).length > 0) { // Show all fields var html = ''; Object.keys(itemData).forEach(function (key) { var val = itemData[key]; if (typeof val === 'string') { html += '
' + '' + AdjudicationForms.escapeHtml(key) + ':
' + AdjudicationForms.escapeHtml(val) + '
'; } }); textEl.innerHTML = html || AdjudicationForms.escapeHtml(itemText); } else { textEl.textContent = itemText; } } // Reset form state AdjudicationForms.resetColors(); // Clear adopted spans window._adjAdoptedSpans = {}; // Store span data globally for adopt functions window._adjSpanData = item.span_annotations || {}; // Render annotator responses + decision forms per schema renderResponsesAndForms(item); // Render span overlays on the text if we have spans if (hasSpans && item.span_annotations) { renderSpanOverlays(item.span_annotations, itemText); } // If there's an existing decision, populate it if (decision) { populateExistingDecision(decision); } // Reset metadata fields resetMetadata(); // Bind form interactions AdjudicationForms.bindFormEvents(); } /** * Render dashed span overlays for all annotators on the text */ function renderSpanOverlays(spanAnnotations, text) { var container = document.getElementById('adj-span-text-content'); var overlayContainer = document.getElementById('adj-span-overlays'); if (!container || !overlayContainer || !text) return; // Wait a frame for layout to settle requestAnimationFrame(function() { var containerRect = container.getBoundingClientRect(); overlayContainer.innerHTML = ''; // Color palette for annotators (matching chip colors but as border colors) var borderColors = [ '#3b82f6', '#22c55e', '#f59e0b', '#ec4899', '#6366f1', '#f97316', '#10b981', '#a855f7' ]; var annotatorColorIdx = 0; var annotatorColors = {}; Object.keys(spanAnnotations).forEach(function(userId) { if (!annotatorColors[userId]) { annotatorColors[userId] = borderColors[annotatorColorIdx % borderColors.length]; annotatorColorIdx++; } var spans = spanAnnotations[userId]; if (!spans || spans.length === 0) return; var color = annotatorColors[userId]; spans.forEach(function(span) { var start = span.start; var end = span.end; if (start === undefined || end === undefined) return; // Calculate positions using Range API var positions = getTextPositions(container, start, end); if (!positions || positions.length === 0) return; positions.forEach(function(pos) { var overlay = document.createElement('div'); overlay.className = 'adj-span-overlay'; overlay.style.left = pos.left + 'px'; overlay.style.top = pos.top + 'px'; overlay.style.width = pos.width + 'px'; overlay.style.height = pos.height + 'px'; overlay.style.borderColor = color; overlay.title = (span.name || 'span') + ' (' + userId + ')'; // Add a label on the first segment if (pos === positions[0]) { var label = document.createElement('span'); label.className = 'adj-span-overlay-label'; label.style.backgroundColor = color; label.textContent = (span.name || 'span') + ' (' + userId + ')'; overlay.appendChild(label); } overlayContainer.appendChild(overlay); }); }); }); }); } /** * Get pixel positions for a text range using the Range API */ function getTextPositions(container, startOffset, endOffset) { var textNode = null; var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false); var currentOffset = 0; var positions = []; // Find the text nodes and create a range var range = document.createRange(); var rangeStartSet = false; var rangeEndSet = false; while (walker.nextNode()) { var node = walker.currentNode; var nodeLen = node.textContent.length; var nodeStart = currentOffset; var nodeEnd = currentOffset + nodeLen; if (!rangeStartSet && startOffset >= nodeStart && startOffset <= nodeEnd) { range.setStart(node, startOffset - nodeStart); rangeStartSet = true; } if (!rangeEndSet && endOffset >= nodeStart && endOffset <= nodeEnd) { range.setEnd(node, endOffset - nodeStart); rangeEndSet = true; break; } currentOffset = nodeEnd; } if (!rangeStartSet || !rangeEndSet) return positions; var containerRect = container.getBoundingClientRect(); var rects = range.getClientRects(); for (var i = 0; i < rects.length; i++) { positions.push({ left: rects[i].left - containerRect.left, top: rects[i].top - containerRect.top, width: rects[i].width, height: rects[i].height }); } return positions; } /** * Render annotator response cards and decision forms for each schema */ function renderResponsesAndForms(item) { var responsesContainer = document.getElementById('adj-responses-container'); var decisionsContainer = document.getElementById('adj-decision-forms'); if (!responsesContainer || !decisionsContainer) return; responsesContainer.innerHTML = ''; decisionsContainer.innerHTML = ''; schemes.forEach(function (schema) { var schemaName = schema.name || ''; var schemaType = schema.annotation_type || ''; // Skip display-only types if (schemaType === 'pure_display' || schemaType === 'video') return; // For span type, check span_annotations instead of annotations var isSpanType = (schemaType === 'span'); var isComplexType = (schemaType === 'image_annotation' || schemaType === 'audio_annotation' || schemaType === 'video_annotation'); var agreement = item.agreement_scores[schemaName]; var agreementHtml = ''; if (agreement !== undefined && config.show_agreement_scores) { agreementHtml = getAgreementBadge(agreement); } // Annotator responses section var responseHtml = '
'; responseHtml += '
'; responseHtml += '' + AdjudicationForms.escapeHtml(schemaName) + ' (' + schemaType + ')'; responseHtml += '' + agreementHtml + ''; responseHtml += '
'; if (isSpanType) { // For spans, show summary per annotator (spans are visualized as dashed overlays on text) responseHtml += '
'; Object.keys(item.span_annotations || {}).forEach(function (userId) { var spans = item.span_annotations[userId] || []; var schemaSpans = spans.filter(function(s) { return s.schema === schemaName || !s.schema; }); if (schemaSpans.length === 0) return; var timing = AdjudicationForms.formatTime( item.behavioral_data[userId] ? (item.behavioral_data[userId].total_time_ms || 0) : 0 ); responseHtml += '
'; responseHtml += '
' + (config.show_annotator_names ? AdjudicationForms.escapeHtml(userId) : 'Annotator') + '
'; responseHtml += '
' + schemaSpans.length + ' span(s)
'; schemaSpans.forEach(function(span) { responseHtml += '
' + '' + AdjudicationForms.escapeHtml(span.name || 'span') + ' ' + '' + AdjudicationForms.escapeHtml(span.title || ('chars ' + span.start + '-' + span.end)) + '
'; }); if (config.show_timing_data && timing) { responseHtml += '
'; responseHtml += ' ' + timing; responseHtml += '
'; } responseHtml += '
'; }); responseHtml += '
'; } else if (isComplexType) { // For image/audio/video, show summary per annotator responseHtml += '
'; Object.keys(item.annotations).forEach(function (userId) { var userAnn = item.annotations[userId]; var schemaVal = userAnn[schemaName]; if (schemaVal === undefined) return; var summary = formatComplexAnnotationSummary(schemaVal, schemaType); var timing = AdjudicationForms.formatTime( item.behavioral_data[userId] ? (item.behavioral_data[userId].total_time_ms || 0) : 0 ); responseHtml += '
'; responseHtml += '
' + (config.show_annotator_names ? AdjudicationForms.escapeHtml(userId) : 'Annotator') + '
'; responseHtml += '
' + summary + '
'; if (config.show_timing_data && timing) { responseHtml += '
'; responseHtml += ' ' + timing; responseHtml += '
'; } responseHtml += '
'; }); responseHtml += '
'; } else { // Standard label annotations responseHtml += '
'; Object.keys(item.annotations).forEach(function (userId) { var userAnn = item.annotations[userId]; var schemaVal = userAnn[schemaName]; if (schemaVal === undefined) return; var valueStr = formatAnnotationValue(schemaVal); var timing = AdjudicationForms.formatTime( item.behavioral_data[userId] ? (item.behavioral_data[userId].total_time_ms || 0) : 0 ); var isFast = config.fast_decision_warning_ms > 0 && item.behavioral_data[userId] && item.behavioral_data[userId].total_time_ms > 0 && item.behavioral_data[userId].total_time_ms < config.fast_decision_warning_ms; responseHtml += '
'; responseHtml += '
' + (config.show_annotator_names ? AdjudicationForms.escapeHtml(userId) : 'Annotator') + '
'; responseHtml += '
' + AdjudicationForms.escapeHtml(valueStr) + '
'; if (config.show_timing_data && timing) { responseHtml += '
'; responseHtml += ' ' + timing; if (isFast) responseHtml += ' '; responseHtml += '
'; } responseHtml += '
'; }); responseHtml += '
'; } responseHtml += '
'; responsesContainer.innerHTML += responseHtml; // Decision form var formHtml = '
'; formHtml += '
' + AdjudicationForms.escapeHtml(schemaName) + '
'; formHtml += AdjudicationForms.renderForm(schema, item.annotations, item.behavioral_data, config, item.span_annotations); formHtml += '
'; decisionsContainer.innerHTML += formHtml; }); } /** * Format a summary of complex annotation data */ function formatComplexAnnotationSummary(val, schemaType) { try { var data = typeof val === 'string' ? JSON.parse(val) : val; if (schemaType === 'image_annotation') { if (Array.isArray(data)) return data.length + ' annotation(s)'; return '1 annotation'; } if (schemaType === 'audio_annotation' || schemaType === 'video_annotation') { if (data.segments) return data.segments.length + ' segment(s)'; return 'annotated'; } } catch(e) { /* ignore */ } return 'annotated'; } /** * Format an annotation value for display */ function formatAnnotationValue(val) { if (typeof val === 'string') return val; if (typeof val === 'number') return String(val); if (typeof val === 'object' && val !== null) { var selected = Object.keys(val).filter(function (k) { return val[k] === true || val[k] === 'true' || val[k] === 1; }); if (selected.length > 0) return selected.join(', '); return JSON.stringify(val); } return String(val); } /** * Populate forms with an existing decision */ function populateExistingDecision(decision) { var labels = decision.label_decisions || {}; Object.keys(labels).forEach(function (schema) { var val = labels[schema]; // Try radio var radioInput = document.querySelector( 'input[name="adj-radio-' + schema + '"][value="' + val + '"]' ); if (radioInput) { radioInput.checked = true; var option = radioInput.closest('.adj-radio-option'); if (option) option.classList.add('selected'); return; } // Try checkbox if (typeof val === 'object') { Object.keys(val).forEach(function (k) { if (val[k]) { var cb = document.querySelector( 'input[name="adj-check-' + schema + '"][value="' + k + '"]' ); if (cb) { cb.checked = true; var opt = cb.closest('.adj-checkbox-option'); if (opt) opt.classList.add('selected'); } } }); return; } // Try likert/slider var slider = document.querySelector('.adj-likert-input[data-schema="' + schema + '"]'); if (slider) { slider.value = val; var valDisplay = document.getElementById('adj-likert-val-' + schema); if (valDisplay) valDisplay.textContent = val; return; } // Try text var textarea = document.querySelector('textarea[data-schema="' + schema + '"]'); if (textarea) { textarea.value = String(val); } }); // Populate metadata if (decision.confidence) { var confEl = document.getElementById('adj-confidence'); if (confEl) confEl.value = decision.confidence; } if (decision.notes) { var notesEl = document.getElementById('adj-notes'); if (notesEl) notesEl.value = decision.notes; } if (decision.error_taxonomy && decision.error_taxonomy.length > 0) { decision.error_taxonomy.forEach(function (tag) { var tagEl = document.querySelector('.adj-error-tag[data-tag="' + tag + '"]'); if (tagEl) { tagEl.classList.add('selected'); var cb = tagEl.querySelector('input[type="checkbox"]'); if (cb) cb.checked = true; } }); } } /** * Reset metadata fields */ function resetMetadata() { var conf = document.getElementById('adj-confidence'); if (conf) conf.value = 'medium'; var notes = document.getElementById('adj-notes'); if (notes) notes.value = ''; document.querySelectorAll('.adj-error-tag').forEach(function (tag) { tag.classList.remove('selected'); var cb = tag.querySelector('input[type="checkbox"]'); if (cb) cb.checked = false; }); var gFlag = document.getElementById('adj-guideline-flag'); if (gFlag) gFlag.checked = false; var gNotes = document.getElementById('adj-guideline-notes'); if (gNotes) gNotes.classList.remove('visible'); var gText = document.getElementById('adj-guideline-text'); if (gText) gText.value = ''; } /** * Submit the current adjudication decision */ function submitDecision() { if (!currentItemId) return; var result = AdjudicationForms.collectDecisions(); if (Object.keys(result.decisions).length === 0) { alert('Please make at least one annotation decision before submitting.'); return; } var timeSpent = itemStartTime ? Date.now() - itemStartTime : 0; // Collect error taxonomy var errorTags = []; document.querySelectorAll('.adj-error-tag input:checked').forEach(function (cb) { errorTags.push(cb.value); }); var payload = { instance_id: currentItemId, label_decisions: result.decisions, span_decisions: result.spanDecisions || [], source: result.sources, confidence: (document.getElementById('adj-confidence') || {}).value || 'medium', notes: (document.getElementById('adj-notes') || {}).value || '', error_taxonomy: errorTags, guideline_update_flag: (document.getElementById('adj-guideline-flag') || {}).checked || false, guideline_update_notes: (document.getElementById('adj-guideline-text') || {}).value || '', time_spent_ms: timeSpent }; fetch('/adjudicate/api/submit', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }) .then(function (r) { return r.json(); }) .then(function (data) { if (data.error) { alert('Error: ' + data.error); return; } // Move to next item goToNext(); // Refresh queue loadQueue(currentFilter === 'all' ? null : currentFilter); }) .catch(function (err) { console.error('Failed to submit decision:', err); alert('Failed to submit decision. Please try again.'); }); } /** * Skip the current item */ function skipItem() { if (!currentItemId) return; fetch('/adjudicate/api/skip/' + encodeURIComponent(currentItemId), { method: 'POST' }) .then(function (r) { return r.json(); }) .then(function () { goToNext(); loadQueue(currentFilter === 'all' ? null : currentFilter); }); } /** * Navigate to next item */ function goToNext() { if (currentQueueIndex < queue.length - 1) { selectQueueItem(currentQueueIndex + 1); } else { // Try to fetch next from API fetch('/adjudicate/api/next') .then(function (r) { return r.json(); }) .then(function (data) { if (data.item) { loadQueue(currentFilter === 'all' ? null : currentFilter); } else { // No more items if (itemView) itemView.style.display = 'none'; if (emptyState) { emptyState.style.display = 'flex'; emptyState.innerHTML = '' + '
All Done!
' + '

No more items to adjudicate.

'; } if (navBar) navBar.style.display = 'none'; } }); } } /** * Navigate to previous item using navigation history */ function goToPrev() { if (navHistoryPos > 0) { navHistoryPos--; var prevId = navHistory[navHistoryPos]; currentItemId = prevId; // Try to highlight in the sidebar queue var found = queue.findIndex(function (i) { return i.instance_id === prevId; }); if (found >= 0) { currentQueueIndex = found; if (queueList) { queueList.querySelectorAll('.adj-queue-item').forEach(function (el, i) { el.classList.toggle('active', i === found); }); } } else { // Item not in current filtered queue (e.g. completed item in pending view) currentQueueIndex = -1; if (queueList) { queueList.querySelectorAll('.adj-queue-item').forEach(function (el) { el.classList.remove('active'); }); } } loadItem(prevId); } } /** * Bind navigation button events */ function bindNavigation() { var submitBtn = document.getElementById('adj-btn-submit'); if (submitBtn) { submitBtn.addEventListener('click', submitDecision); } var skipBtn = document.getElementById('adj-btn-skip'); if (skipBtn) { skipBtn.addEventListener('click', skipItem); } var prevBtn = document.getElementById('adj-btn-prev'); if (prevBtn) { prevBtn.addEventListener('click', goToPrev); } // Keyboard shortcuts document.addEventListener('keydown', function (e) { // Only handle when not in an input/textarea if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'SELECT') { return; } if (e.key === 'ArrowRight' || e.key === 'n') { e.preventDefault(); goToNext(); } else if (e.key === 'ArrowLeft' || e.key === 'p') { e.preventDefault(); goToPrev(); } else if (e.key === 'Enter' && e.ctrlKey) { e.preventDefault(); submitDecision(); } }); } /** * Render similar items panel (Phase 3) */ function renderSimilarItems(similarItems) { var panel = document.getElementById('adj-similar-items-panel'); if (!panel || !similarItems || similarItems.length === 0) { if (panel) panel.style.display = 'none'; return; } panel.style.display = 'block'; var html = '
' + '
Similar Items
' + '' + similarItems.length + ' found
'; html += '
'; similarItems.forEach(function (si, idx) { var pct = Math.round(si.similarity * 100); var scoreClass = 'adj-similarity-low'; if (pct >= 70) scoreClass = 'adj-similarity-high'; else if (pct >= 50) scoreClass = 'adj-similarity-medium'; var metaHtml = ''; if (si.decision === 'completed') { metaHtml += 'Decided'; } else if (si.consensus_label) { metaHtml += '' + AdjudicationForms.escapeHtml(si.consensus_label) + ''; } if (si.overall_agreement !== null && si.overall_agreement !== undefined) { metaHtml += getAgreementBadge(si.overall_agreement); } html += '
' + '
' + pct + '%
' + '
' + '
' + AdjudicationForms.escapeHtml(si.instance_id) + '
' + '
' + AdjudicationForms.escapeHtml(si.text_preview || '') + '
' + '
' + '
' + metaHtml + '
' + '
'; }); html += '
'; panel.innerHTML = html; // Click handler: navigate to item in queue panel.querySelectorAll('.adj-similar-item').forEach(function (el) { el.addEventListener('click', function () { var targetId = el.getAttribute('data-similar-id'); var idx = queue.findIndex(function (q) { return q.instance_id === targetId; }); if (idx >= 0) { selectQueueItem(idx); } }); }); } /** * Render annotator signal badges (Phase 3) */ function renderAnnotatorSignals(signalsData) { if (!signalsData) return; Object.keys(signalsData).forEach(function (userId) { var signals = signalsData[userId]; if (!signals || !signals.flags || signals.flags.length === 0) return; // Find the annotator card(s) for this user var cards = document.querySelectorAll( '.adj-annotator-card[data-annotator="' + userId + '"]' ); cards.forEach(function (card) { // Remove any existing signal badges var existing = card.querySelector('.adj-signal-flags'); if (existing) existing.remove(); var flagsDiv = document.createElement('div'); flagsDiv.className = 'adj-signal-flags'; signals.flags.forEach(function (flag) { var badge = document.createElement('span'); badge.className = 'adj-signal-badge adj-signal-' + (flag.severity || 'medium'); badge.title = flag.message || ''; badge.innerHTML = ' ' + AdjudicationForms.escapeHtml( (flag.type || '').replace(/_/g, ' ') ); flagsDiv.appendChild(badge); }); card.appendChild(flagsDiv); }); }); } /** * Bind filter button events */ function bindFilters() { document.querySelectorAll('.adj-filter-btn').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('.adj-filter-btn').forEach(function (b) { b.classList.remove('active'); }); this.classList.add('active'); currentFilter = this.dataset.filter || 'pending'; loadQueue(currentFilter === 'all' ? null : currentFilter); }); }); } // Initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();