/** * Adjudication Forms Module * * Custom compact decision forms for each annotation schema type. * These forms show annotator choices inline and let the adjudicator * pick from existing annotations or create their own. */ window.AdjudicationForms = (function () { 'use strict'; // Color classes for annotator chips const CHIP_COLORS = [ 'adj-color-0', 'adj-color-1', 'adj-color-2', 'adj-color-3', 'adj-color-4', 'adj-color-5', 'adj-color-6', 'adj-color-7' ]; // Map annotator names to consistent colors let annotatorColorMap = {}; let colorIndex = 0; function getAnnotatorColor(annotatorId) { if (!(annotatorId in annotatorColorMap)) { annotatorColorMap[annotatorId] = CHIP_COLORS[colorIndex % CHIP_COLORS.length]; colorIndex++; } return annotatorColorMap[annotatorId]; } function resetColors() { annotatorColorMap = {}; colorIndex = 0; } /** * Format milliseconds as human-readable time */ function formatTime(ms) { if (!ms || ms <= 0) return ''; var seconds = Math.round(ms / 1000); if (seconds < 60) return seconds + 's'; var minutes = Math.floor(seconds / 60); var secs = seconds % 60; return minutes + 'm' + (secs > 0 ? secs + 's' : ''); } /** * Create annotator chip HTML */ function createChip(annotatorId, timing, showName, fastWarningMs) { var colorClass = getAnnotatorColor(annotatorId); var name = showName ? annotatorId : 'Annotator'; var timeStr = formatTime(timing); var isWarning = fastWarningMs > 0 && timing > 0 && timing < fastWarningMs; var timingHtml = ''; if (timeStr) { var cls = isWarning ? 'adj-chip-timing adj-timing-warning' : 'adj-chip-timing'; timingHtml = '' + (isWarning ? ' ' : '') + timeStr + ''; } return '' + name + timingHtml + ''; } /** * Get timing data for an annotator on this item */ function getTimingMs(behavioralData, annotatorId) { if (!behavioralData || !behavioralData[annotatorId]) return 0; var bd = behavioralData[annotatorId]; return bd.total_time_ms || bd.totalTimeMs || 0; } /** * Render a radio/single-choice schema form */ function renderRadioForm(schema, annotations, behavioralData, config) { var labels = schema.labels || []; var schemaName = schema.name || ''; // Build label -> annotators mapping var labelToAnnotators = {}; labels.forEach(function(l) { var labelName = typeof l === 'string' ? l : l.name || l; labelToAnnotators[labelName] = []; }); Object.keys(annotations).forEach(function(userId) { var userAnn = annotations[userId]; if (!userAnn[schemaName]) return; var val = userAnn[schemaName]; // Handle different annotation formats if (typeof val === 'object' && val !== null) { Object.keys(val).forEach(function(k) { if (val[k] === true || val[k] === 'true' || val[k] === 1) { if (!labelToAnnotators[k]) labelToAnnotators[k] = []; labelToAnnotators[k].push(userId); } }); } else { var strVal = String(val); if (!labelToAnnotators[strVal]) labelToAnnotators[strVal] = []; labelToAnnotators[strVal].push(userId); } }); var html = '
'; return html; } /** * Render a multiselect/checkbox schema form */ function renderMultiselectForm(schema, annotations, behavioralData, config) { var labels = schema.labels || []; var schemaName = schema.name || ''; // Build label -> annotators mapping var labelToAnnotators = {}; labels.forEach(function(l) { var labelName = typeof l === 'string' ? l : l.name || l; labelToAnnotators[labelName] = []; }); Object.keys(annotations).forEach(function(userId) { var userAnn = annotations[userId]; if (!userAnn[schemaName]) return; var val = userAnn[schemaName]; if (typeof val === 'object' && val !== null) { Object.keys(val).forEach(function(k) { if (val[k] === true || val[k] === 'true' || val[k] === 1) { if (!labelToAnnotators[k]) labelToAnnotators[k] = []; labelToAnnotators[k].push(userId); } }); } }); var html = ''; return html; } /** * Render a likert scale form */ function renderLikertForm(schema, annotations, behavioralData, config) { var schemaName = schema.name || ''; var minVal = schema.min_value || 1; var maxVal = schema.max_value || 5; var size = schema.size || maxVal; // Collect annotator values var annotatorValues = {}; Object.keys(annotations).forEach(function(userId) { var userAnn = annotations[userId]; if (!userAnn[schemaName]) return; var val = userAnn[schemaName]; if (typeof val === 'object' && val !== null) { // Find the selected value in the dict Object.keys(val).forEach(function(k) { if (val[k] === true || val[k] === 'true' || val[k] === 1) { annotatorValues[userId] = parseInt(k); } }); } else { annotatorValues[userId] = parseInt(val) || 0; } }); var range = maxVal - minVal; var html = 'Form not available for type: ' + type + '
'; } } /** * Collect decision values from all forms */ function collectDecisions() { var decisions = {}; var sources = {}; var spanDecisions = []; // Radio forms document.querySelectorAll('.adj-radio-options').forEach(function(container) { var schema = container.dataset.schema; var checked = container.querySelector('input[type="radio"]:checked'); if (checked) { decisions[schema] = checked.value; sources[schema] = 'adjudicator'; } }); // Checkbox forms document.querySelectorAll('.adj-checkbox-options').forEach(function(container) { var schema = container.dataset.schema; var checked = container.querySelectorAll('input[type="checkbox"]:checked'); if (checked.length > 0) { var selected = {}; checked.forEach(function(cb) { selected[cb.value] = true; }); decisions[schema] = selected; sources[schema] = 'adjudicator'; } }); // Likert/slider forms document.querySelectorAll('.adj-likert-input').forEach(function(input) { var schema = input.dataset.schema; if (input.value) { decisions[schema] = parseInt(input.value); sources[schema] = 'adjudicator'; } }); // Text forms document.querySelectorAll('.adj-text-decision-area textarea').forEach(function(textarea) { var schema = textarea.dataset.schema; if (textarea.value.trim()) { decisions[schema] = textarea.value.trim(); sources[schema] = 'adjudicator'; } }); // Span decisions (collected from adopted spans store) if (window._adjAdoptedSpans) { Object.keys(window._adjAdoptedSpans).forEach(function(schema) { var spans = window._adjAdoptedSpans[schema]; if (spans && spans.length > 0) { spanDecisions = spanDecisions.concat(spans); sources[schema] = 'adjudicator'; } }); } // Image/audio/video complex decisions (from checkboxes) document.querySelectorAll('.adj-image-form, .adj-media-form').forEach(function(form) { var schema = form.dataset.schema; var checked = form.querySelectorAll('.adj-complex-adopt-cb:checked'); if (checked.length > 0) { var adopted = []; checked.forEach(function(cb) { adopted.push({ annotator: cb.dataset.annotator, idx: cb.dataset.idx, type: cb.dataset.type }); }); decisions[schema] = { adopted_annotations: adopted }; sources[schema] = 'adjudicator'; } }); return { decisions: decisions, sources: sources, spanDecisions: spanDecisions }; } /** * Bind interactive behaviors to forms */ function bindFormEvents() { // Radio option styling document.querySelectorAll('.adj-radio-option').forEach(function(option) { option.addEventListener('click', function() { var container = this.closest('.adj-radio-options'); container.querySelectorAll('.adj-radio-option').forEach(function(o) { o.classList.remove('selected'); }); this.classList.add('selected'); this.querySelector('input[type="radio"]').checked = true; }); }); // Checkbox option styling document.querySelectorAll('.adj-checkbox-option').forEach(function(option) { option.addEventListener('click', function(e) { if (e.target.type === 'checkbox') return; var cb = this.querySelector('input[type="checkbox"]'); cb.checked = !cb.checked; this.classList.toggle('selected', cb.checked); }); option.querySelector('input[type="checkbox"]').addEventListener('change', function() { option.classList.toggle('selected', this.checked); }); }); // Annotator chip clicks (auto-select that option) document.querySelectorAll('.adj-annotator-chip').forEach(function(chip) { chip.addEventListener('click', function(e) { e.stopPropagation(); var option = this.closest('.adj-radio-option, .adj-checkbox-option'); if (option) { option.click(); } }); }); // Likert dot clicks document.querySelectorAll('.adj-likert-dot').forEach(function(dot) { dot.addEventListener('click', function() { var val = this.dataset.value; var container = this.closest('.adj-likert-track'); var schema = container.dataset.schema; 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; } }); }); // Likert slider value display document.querySelectorAll('.adj-likert-input').forEach(function(slider) { slider.addEventListener('input', function() { var schema = this.dataset.schema; var valDisplay = document.getElementById('adj-likert-val-' + schema); if (valDisplay) valDisplay.textContent = this.value; }); }); // Text response checkbox toggle styling document.querySelectorAll('.adj-text-response').forEach(function(resp) { resp.addEventListener('click', function(e) { // Don't toggle if clicking the checkbox itself if (e.target.type === 'checkbox') { this.classList.toggle('selected', e.target.checked); return; } var cb = this.querySelector('.adj-text-select-cb'); if (cb) { cb.checked = !cb.checked; this.classList.toggle('selected', cb.checked); } }); }); // Text merge buttons document.querySelectorAll('.adj-text-merge-btn').forEach(function(btn) { btn.addEventListener('click', function() { var action = this.dataset.action; var schema = this.dataset.schema; var container = document.querySelector('.adj-text-responses[data-schema="' + schema + '"]'); var textarea = document.querySelector('textarea[data-schema="' + schema + '"]'); if (!container || !textarea) return; var selected = container.querySelectorAll('.adj-text-select-cb:checked'); if (selected.length === 0) { alert('Please select at least one response.'); return; } var texts = []; selected.forEach(function(cb) { var resp = cb.closest('.adj-text-response'); var body = resp ? resp.querySelector('.adj-text-response-body') : null; if (body) texts.push(body.textContent); }); if (action === 'select') { textarea.value = texts[0] || ''; } else if (action === 'merge') { textarea.value = texts.join('\n\n---\n\n'); } }); }); // Error taxonomy toggles document.querySelectorAll('.adj-error-tag').forEach(function(tag) { tag.addEventListener('click', function(e) { if (e.target.type === 'checkbox') { this.classList.toggle('selected', e.target.checked); return; } var cb = this.querySelector('input[type="checkbox"]'); cb.checked = !cb.checked; this.classList.toggle('selected', cb.checked); }); }); // Span adopt buttons document.querySelectorAll('.adj-span-adopt-btn').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); var annotator = this.dataset.annotator; var spanIdx = parseInt(this.dataset.spanIdx); var schema = this.dataset.schema; adoptSpan(schema, annotator, spanIdx); }); }); // Span adopt all buttons document.querySelectorAll('.adj-span-adopt-all-btn').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); var annotator = this.dataset.annotator; var schema = this.dataset.schema; adoptAllSpans(schema, annotator); }); }); // Complex type adopt all buttons document.querySelectorAll('.adj-complex-adopt-all-btn').forEach(function(btn) { btn.addEventListener('click', function() { var annotator = this.dataset.annotator; var schema = this.dataset.schema; var group = this.closest('.adj-complex-annotator-group'); if (group) { group.querySelectorAll('.adj-complex-adopt-cb').forEach(function(cb) { if (cb.dataset.annotator === annotator) cb.checked = true; }); } }); }); // Guideline flag toggle var guidelineFlag = document.getElementById('adj-guideline-flag'); if (guidelineFlag) { guidelineFlag.addEventListener('change', function() { var notes = document.getElementById('adj-guideline-notes'); if (notes) { notes.classList.toggle('visible', this.checked); } }); } } /** * Adopt a single span from an annotator's list */ function adoptSpan(schema, annotator, spanIdx) { if (!window._adjSpanData) return; var annotatorSpans = (window._adjSpanData[annotator] || []).filter(function(s) { return s.schema === schema || !s.schema; }); if (spanIdx >= annotatorSpans.length) return; var span = JSON.parse(JSON.stringify(annotatorSpans[spanIdx])); span._source = annotator; if (!window._adjAdoptedSpans) window._adjAdoptedSpans = {}; if (!window._adjAdoptedSpans[schema]) window._adjAdoptedSpans[schema] = []; // Avoid duplicates var isDupe = window._adjAdoptedSpans[schema].some(function(s) { return s.start === span.start && s.end === span.end && s.name === span.name; }); if (!isDupe) { window._adjAdoptedSpans[schema].push(span); } renderAdoptedSpansList(schema); } /** * Adopt all spans from an annotator */ function adoptAllSpans(schema, annotator) { if (!window._adjSpanData) return; var annotatorSpans = (window._adjSpanData[annotator] || []).filter(function(s) { return s.schema === schema || !s.schema; }); annotatorSpans.forEach(function(span, idx) { adoptSpan(schema, annotator, idx); }); } /** * Remove an adopted span */ function removeAdoptedSpan(schema, idx) { if (!window._adjAdoptedSpans || !window._adjAdoptedSpans[schema]) return; window._adjAdoptedSpans[schema].splice(idx, 1); renderAdoptedSpansList(schema); } /** * Render the list of adopted spans for a schema */ function renderAdoptedSpansList(schema) { var listEl = document.getElementById('adj-adopted-list-' + schema); if (!listEl) return; var spans = (window._adjAdoptedSpans && window._adjAdoptedSpans[schema]) || []; if (spans.length === 0) { listEl.innerHTML = 'No spans adopted yet'; return; } var html = ''; spans.forEach(function(span, idx) { var source = span._source || 'unknown'; html += '