/**
* 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 = '
' +
'
' +
'
' +
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 += '';
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 = '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 = '';
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 = '