| |
| |
| |
| |
| |
| |
| |
|
|
| window.AdjudicationForms = (function () { |
| 'use strict'; |
|
|
| |
| 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' |
| ]; |
|
|
| |
| 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; |
| } |
|
|
| |
| |
| |
| 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' : ''); |
| } |
|
|
| |
| |
| |
| 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 = '<span class="' + cls + '">' + |
| (isWarning ? '<i class="fas fa-exclamation-triangle"></i> ' : '') + |
| timeStr + '</span>'; |
| } |
|
|
| return '<span class="adj-annotator-chip ' + colorClass + '" ' + |
| 'data-annotator="' + annotatorId + '" title="' + annotatorId + '">' + |
| name + timingHtml + '</span>'; |
| } |
|
|
| |
| |
| |
| function getTimingMs(behavioralData, annotatorId) { |
| if (!behavioralData || !behavioralData[annotatorId]) return 0; |
| var bd = behavioralData[annotatorId]; |
| return bd.total_time_ms || bd.totalTimeMs || 0; |
| } |
|
|
| |
| |
| |
| function renderRadioForm(schema, annotations, behavioralData, config) { |
| var labels = schema.labels || []; |
| var schemaName = schema.name || ''; |
|
|
| |
| 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); |
| } |
| }); |
| } else { |
| var strVal = String(val); |
| if (!labelToAnnotators[strVal]) labelToAnnotators[strVal] = []; |
| labelToAnnotators[strVal].push(userId); |
| } |
| }); |
|
|
| var html = '<div class="adj-radio-options" data-schema="' + schemaName + '">'; |
| labels.forEach(function(l) { |
| var labelName = typeof l === 'string' ? l : l.name || l; |
| var annotators = labelToAnnotators[labelName] || []; |
|
|
| var chipsHtml = ''; |
| annotators.forEach(function(uid) { |
| var timing = getTimingMs(behavioralData, uid); |
| chipsHtml += createChip(uid, timing, config.show_annotator_names, config.fast_decision_warning_ms); |
| }); |
|
|
| html += '<label class="adj-radio-option" data-value="' + labelName + '">' + |
| '<input type="radio" name="adj-radio-' + schemaName + '" value="' + labelName + '">' + |
| '<span class="adj-radio-label">' + labelName + '</span>' + |
| '<span class="adj-annotator-chips">' + chipsHtml + '</span>' + |
| '</label>'; |
| }); |
| html += '</div>'; |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| function renderMultiselectForm(schema, annotations, behavioralData, config) { |
| var labels = schema.labels || []; |
| var schemaName = schema.name || ''; |
|
|
| |
| 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 = '<div class="adj-checkbox-options" data-schema="' + schemaName + '">'; |
| labels.forEach(function(l) { |
| var labelName = typeof l === 'string' ? l : l.name || l; |
| var annotators = labelToAnnotators[labelName] || []; |
|
|
| var chipsHtml = ''; |
| annotators.forEach(function(uid) { |
| var timing = getTimingMs(behavioralData, uid); |
| chipsHtml += createChip(uid, timing, config.show_annotator_names, config.fast_decision_warning_ms); |
| }); |
|
|
| html += '<label class="adj-checkbox-option" data-value="' + labelName + '">' + |
| '<input type="checkbox" name="adj-check-' + schemaName + '" value="' + labelName + '">' + |
| '<span class="adj-radio-label">' + labelName + '</span>' + |
| '<span class="adj-annotator-chips">' + chipsHtml + '</span>' + |
| '</label>'; |
| }); |
| html += '</div>'; |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| 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; |
|
|
| |
| 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) { |
| |
| 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 = '<div class="adj-likert-track" data-schema="' + schemaName + '">'; |
| html += '<div class="adj-likert-line"></div>'; |
|
|
| |
| Object.keys(annotatorValues).forEach(function(uid) { |
| var val = annotatorValues[uid]; |
| var pct = range > 0 ? ((val - minVal) / range * 100) : 50; |
| var colorClass = getAnnotatorColor(uid); |
| var bgStyle = ''; |
| |
| var tempDiv = document.createElement('div'); |
| tempDiv.className = colorClass; |
|
|
| html += '<div class="adj-likert-dot" style="left:' + pct + '%; background: #6366f1;" ' + |
| 'data-annotator="' + uid + '" data-value="' + val + '" ' + |
| 'title="' + uid + ': ' + val + '">' + |
| '</div>'; |
| html += '<div class="adj-likert-dot-label" style="left:' + pct + '%">' + uid + ': ' + val + '</div>'; |
| }); |
|
|
| html += '</div>'; |
| html += '<div style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">'; |
| html += '<label style="font-size: 0.85rem; font-weight: 500;">Your rating:</label>'; |
| html += '<input type="range" class="adj-likert-input form-range" ' + |
| 'name="adj-likert-' + schemaName + '" ' + |
| 'min="' + minVal + '" max="' + maxVal + '" step="1" ' + |
| 'data-schema="' + schemaName + '">'; |
| html += '<span class="adj-likert-value" id="adj-likert-val-' + schemaName + '">-</span>'; |
| html += '</div>'; |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| function renderTextForm(schema, annotations, behavioralData, config) { |
| var schemaName = schema.name || ''; |
|
|
| var html = '<div class="adj-text-responses" data-schema="' + schemaName + '">'; |
|
|
| Object.keys(annotations).forEach(function(userId) { |
| var userAnn = annotations[userId]; |
| if (!userAnn[schemaName]) return; |
|
|
| var val = userAnn[schemaName]; |
| var text = ''; |
| if (typeof val === 'object' && val !== null) { |
| text = Object.values(val).join('; '); |
| } else { |
| text = String(val); |
| } |
|
|
| var timing = getTimingMs(behavioralData, userId); |
| var timeStr = formatTime(timing); |
|
|
| html += '<div class="adj-text-response" data-annotator="' + userId + '">'; |
| html += '<div class="adj-text-response-header">'; |
| html += '<label class="adj-text-select-label">' + |
| '<input type="checkbox" class="adj-text-select-cb" data-annotator="' + userId + '" data-schema="' + schemaName + '">' + |
| '</label>'; |
| html += '<span class="adj-annotator-name">' + |
| (config.show_annotator_names ? userId : 'Annotator') + '</span>'; |
| if (timeStr) { |
| html += '<span class="adj-annotator-timing"><i class="fas fa-clock"></i> ' + timeStr + '</span>'; |
| } |
| html += '</div>'; |
| html += '<div class="adj-text-response-body">' + escapeHtml(text) + '</div>'; |
| html += '</div>'; |
| }); |
|
|
| html += '</div>'; |
| html += '<div class="adj-text-merge-bar" data-schema="' + schemaName + '">'; |
| html += '<button type="button" class="adj-text-merge-btn" data-action="select" data-schema="' + schemaName + '">' + |
| '<i class="fas fa-check"></i> Use Selected</button>'; |
| html += '<button type="button" class="adj-text-merge-btn" data-action="merge" data-schema="' + schemaName + '">' + |
| '<i class="fas fa-code-merge"></i> Merge Selected</button>'; |
| html += '</div>'; |
| html += '<div class="adj-text-decision-area">'; |
| html += '<textarea name="adj-text-' + schemaName + '" data-schema="' + schemaName + '" ' + |
| 'placeholder="Enter your decision, select a response above, or use the merge button..."></textarea>'; |
| html += '</div>'; |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| function renderSliderForm(schema, annotations, behavioralData, config) { |
| |
| return renderLikertForm(schema, annotations, behavioralData, config); |
| } |
|
|
| function escapeHtml(text) { |
| var div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
|
|
| |
| |
| |
| |
| |
| function renderSpanForm(schema, spanAnnotations, behavioralData, config) { |
| var schemaName = schema.name || ''; |
|
|
| var html = '<div class="adj-span-form" data-schema="' + schemaName + '">'; |
| html += '<div class="adj-span-instructions">' + |
| '<i class="fas fa-info-circle"></i> ' + |
| 'Annotator spans are shown as dashed highlights on the text above. ' + |
| 'Click "Adopt" to accept a span, or select text to create your own.' + |
| '</div>'; |
|
|
| |
| Object.keys(spanAnnotations).forEach(function(userId) { |
| var spans = spanAnnotations[userId]; |
| if (!spans || spans.length === 0) return; |
|
|
| var schemaSpans = spans.filter(function(s) { |
| return s.schema === schemaName || !s.schema; |
| }); |
| if (schemaSpans.length === 0) return; |
|
|
| var colorClass = getAnnotatorColor(userId); |
| var timing = getTimingMs(behavioralData, userId); |
| var timeStr = formatTime(timing); |
|
|
| html += '<div class="adj-span-annotator-group">'; |
| html += '<div class="adj-span-annotator-header">'; |
| html += '<span class="adj-annotator-chip ' + colorClass + '">' + |
| (config.show_annotator_names ? userId : 'Annotator') + |
| (timeStr ? ' <span class="adj-chip-timing">' + timeStr + '</span>' : '') + |
| '</span>'; |
| html += '<button type="button" class="adj-span-adopt-all-btn" data-annotator="' + userId + |
| '" data-schema="' + schemaName + '"><i class="fas fa-check-double"></i> Adopt All</button>'; |
| html += '</div>'; |
|
|
| schemaSpans.forEach(function(span, idx) { |
| var spanTitle = span.title || span.name || ''; |
| var spanText = spanTitle || ('chars ' + span.start + '-' + span.end); |
| html += '<div class="adj-span-item" data-span-idx="' + idx + '" data-annotator="' + userId + '">'; |
| html += '<span class="adj-span-label-badge ' + colorClass + '">' + |
| escapeHtml(span.name || 'span') + '</span>'; |
| html += '<span class="adj-span-text">' + escapeHtml(spanText) + '</span>'; |
| html += '<button type="button" class="adj-span-adopt-btn" ' + |
| 'data-annotator="' + userId + '" data-span-idx="' + idx + |
| '" data-schema="' + schemaName + '">' + |
| '<i class="fas fa-check"></i> Adopt</button>'; |
| html += '</div>'; |
| }); |
| html += '</div>'; |
| }); |
|
|
| |
| html += '<div class="adj-adopted-spans" data-schema="' + schemaName + '">'; |
| html += '<div class="adj-adopted-header"><strong>Your Decision Spans:</strong></div>'; |
| html += '<div class="adj-adopted-list" id="adj-adopted-list-' + schemaName + '">'; |
| html += '<span class="text-muted" style="font-size:0.8rem;">No spans adopted yet</span>'; |
| html += '</div>'; |
| html += '</div>'; |
| html += '</div>'; |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| |
| function renderImageForm(schema, annotations, behavioralData, config) { |
| var schemaName = schema.name || ''; |
|
|
| var html = '<div class="adj-image-form" data-schema="' + schemaName + '">'; |
| html += '<div class="adj-complex-instructions">' + |
| '<i class="fas fa-info-circle"></i> ' + |
| 'Each annotator\'s bounding boxes are listed below. Select annotations to include in your decision.' + |
| '</div>'; |
|
|
| Object.keys(annotations).forEach(function(userId) { |
| var userAnn = annotations[userId]; |
| if (!userAnn[schemaName]) return; |
|
|
| var val = userAnn[schemaName]; |
| var boxes = []; |
| try { |
| if (typeof val === 'string') boxes = JSON.parse(val); |
| else if (Array.isArray(val)) boxes = val; |
| else if (typeof val === 'object') boxes = [val]; |
| } catch(e) { } |
|
|
| if (boxes.length === 0) return; |
|
|
| var colorClass = getAnnotatorColor(userId); |
| var timing = getTimingMs(behavioralData, userId); |
| var timeStr = formatTime(timing); |
|
|
| html += '<div class="adj-complex-annotator-group">'; |
| html += '<div class="adj-complex-annotator-header">'; |
| html += '<span class="adj-annotator-chip ' + colorClass + '">' + |
| (config.show_annotator_names ? userId : 'Annotator') + |
| (timeStr ? ' <span class="adj-chip-timing">' + timeStr + '</span>' : '') + |
| '</span>'; |
| html += '<button type="button" class="adj-complex-adopt-all-btn" data-annotator="' + userId + |
| '" data-schema="' + schemaName + '" data-type="image">' + |
| '<i class="fas fa-check-double"></i> Adopt All</button>'; |
| html += '</div>'; |
|
|
| boxes.forEach(function(box, idx) { |
| var label = box.label || box.type || 'annotation'; |
| var coords = box.coordinates || {}; |
| var detail = ''; |
| if (coords.x !== undefined) { |
| detail = 'x:' + Math.round(coords.x * 100) + '% y:' + Math.round(coords.y * 100) + |
| '% w:' + Math.round((coords.width || 0) * 100) + '% h:' + Math.round((coords.height || 0) * 100) + '%'; |
| } |
| html += '<div class="adj-complex-item">'; |
| html += '<label><input type="checkbox" class="adj-complex-adopt-cb" data-annotator="' + userId + |
| '" data-idx="' + idx + '" data-schema="' + schemaName + '" data-type="image">'; |
| html += ' <span class="adj-span-label-badge ' + colorClass + '">' + escapeHtml(label) + '</span>'; |
| if (detail) html += ' <span class="adj-complex-detail">' + detail + '</span>'; |
| html += '</label></div>'; |
| }); |
| html += '</div>'; |
| }); |
|
|
| html += '</div>'; |
| return html; |
| } |
|
|
| |
| |
| |
| |
| function renderMediaForm(schema, annotations, behavioralData, config, mediaType) { |
| var schemaName = schema.name || ''; |
|
|
| var html = '<div class="adj-media-form" data-schema="' + schemaName + '" data-media-type="' + mediaType + '">'; |
| html += '<div class="adj-complex-instructions">' + |
| '<i class="fas fa-info-circle"></i> ' + |
| 'Each annotator\'s ' + mediaType + ' segments are listed below. Select segments to include in your decision.' + |
| '</div>'; |
|
|
| Object.keys(annotations).forEach(function(userId) { |
| var userAnn = annotations[userId]; |
| if (!userAnn[schemaName]) return; |
|
|
| var val = userAnn[schemaName]; |
| var data = {}; |
| try { |
| if (typeof val === 'string') data = JSON.parse(val); |
| else if (typeof val === 'object') data = val; |
| } catch(e) { } |
|
|
| var segments = data.segments || []; |
| if (segments.length === 0 && !data.id) return; |
|
|
| var colorClass = getAnnotatorColor(userId); |
| var timing = getTimingMs(behavioralData, userId); |
| var timeStr = formatTime(timing); |
|
|
| html += '<div class="adj-complex-annotator-group">'; |
| html += '<div class="adj-complex-annotator-header">'; |
| html += '<span class="adj-annotator-chip ' + colorClass + '">' + |
| (config.show_annotator_names ? userId : 'Annotator') + |
| (timeStr ? ' <span class="adj-chip-timing">' + timeStr + '</span>' : '') + |
| '</span>'; |
| html += '<button type="button" class="adj-complex-adopt-all-btn" data-annotator="' + userId + |
| '" data-schema="' + schemaName + '" data-type="' + mediaType + '">' + |
| '<i class="fas fa-check-double"></i> Adopt All</button>'; |
| html += '</div>'; |
|
|
| segments.forEach(function(seg, idx) { |
| var label = seg.label || seg.labelText || 'segment'; |
| var startT = formatMediaTime(seg.start_time || seg.startTime || 0); |
| var endT = formatMediaTime(seg.end_time || seg.endTime || 0); |
| html += '<div class="adj-complex-item">'; |
| html += '<label><input type="checkbox" class="adj-complex-adopt-cb" data-annotator="' + userId + |
| '" data-idx="' + idx + '" data-schema="' + schemaName + '" data-type="' + mediaType + '">'; |
| html += ' <span class="adj-span-label-badge ' + colorClass + '">' + escapeHtml(label) + '</span>'; |
| html += ' <span class="adj-complex-detail">' + startT + ' - ' + endT + '</span>'; |
| html += '</label></div>'; |
| }); |
|
|
| |
| if (mediaType === 'video' && data.keyframes) { |
| var keyframes = data.keyframes; |
| if (keyframes.length > 0) { |
| html += '<div class="adj-complex-sub-section">Keyframes:</div>'; |
| keyframes.forEach(function(kf, idx) { |
| var kfLabel = kf.label || 'keyframe'; |
| var kfTime = formatMediaTime(kf.time || 0); |
| html += '<div class="adj-complex-item">'; |
| html += '<label><input type="checkbox" class="adj-complex-adopt-cb" data-annotator="' + userId + |
| '" data-idx="kf-' + idx + '" data-schema="' + schemaName + '" data-type="keyframe">'; |
| html += ' <span class="adj-span-label-badge ' + colorClass + '">' + escapeHtml(kfLabel) + '</span>'; |
| html += ' <span class="adj-complex-detail">@ ' + kfTime + '</span>'; |
| html += '</label></div>'; |
| }); |
| } |
| } |
| html += '</div>'; |
| }); |
|
|
| html += '</div>'; |
| return html; |
| } |
|
|
| |
| |
| |
| function formatMediaTime(seconds) { |
| if (!seconds && seconds !== 0) return '0:00'; |
| var mins = Math.floor(seconds / 60); |
| var secs = Math.round(seconds % 60); |
| return mins + ':' + (secs < 10 ? '0' : '') + secs; |
| } |
|
|
| |
| |
| |
| function renderForm(schema, annotations, behavioralData, config, spanAnnotations) { |
| var type = schema.annotation_type || ''; |
|
|
| switch (type) { |
| case 'radio': |
| case 'select': |
| return renderRadioForm(schema, annotations, behavioralData, config); |
| case 'multiselect': |
| return renderMultiselectForm(schema, annotations, behavioralData, config); |
| case 'likert': |
| return renderLikertForm(schema, annotations, behavioralData, config); |
| case 'slider': |
| return renderSliderForm(schema, annotations, behavioralData, config); |
| case 'text': |
| case 'textbox': |
| case 'number': |
| return renderTextForm(schema, annotations, behavioralData, config); |
| case 'span': |
| return renderSpanForm(schema, spanAnnotations || {}, behavioralData, config); |
| case 'image_annotation': |
| return renderImageForm(schema, annotations, behavioralData, config); |
| case 'audio_annotation': |
| return renderMediaForm(schema, annotations, behavioralData, config, 'audio'); |
| case 'video_annotation': |
| return renderMediaForm(schema, annotations, behavioralData, config, 'video'); |
| default: |
| return '<p class="text-muted">Form not available for type: ' + type + '</p>'; |
| } |
| } |
|
|
| |
| |
| |
| function collectDecisions() { |
| var decisions = {}; |
| var sources = {}; |
| var spanDecisions = []; |
|
|
| |
| 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'; |
| } |
| }); |
|
|
| |
| 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'; |
| } |
| }); |
|
|
| |
| document.querySelectorAll('.adj-likert-input').forEach(function(input) { |
| var schema = input.dataset.schema; |
| if (input.value) { |
| decisions[schema] = parseInt(input.value); |
| sources[schema] = 'adjudicator'; |
| } |
| }); |
|
|
| |
| 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'; |
| } |
| }); |
|
|
| |
| 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'; |
| } |
| }); |
| } |
|
|
| |
| 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 }; |
| } |
|
|
| |
| |
| |
| function bindFormEvents() { |
| |
| 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; |
| }); |
| }); |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| 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(); |
| } |
| }); |
| }); |
|
|
| |
| 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; |
| } |
| }); |
| }); |
|
|
| |
| 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; |
| }); |
| }); |
|
|
| |
| document.querySelectorAll('.adj-text-response').forEach(function(resp) { |
| resp.addEventListener('click', function(e) { |
| |
| 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); |
| } |
| }); |
| }); |
|
|
| |
| 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'); |
| } |
| }); |
| }); |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| 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); |
| }); |
| }); |
|
|
| |
| 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; |
| }); |
| } |
| }); |
| }); |
|
|
| |
| 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); |
| } |
| }); |
| } |
| } |
|
|
| |
| |
| |
| 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] = []; |
|
|
| |
| 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); |
| } |
|
|
| |
| |
| |
| 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); |
| }); |
| } |
|
|
| |
| |
| |
| function removeAdoptedSpan(schema, idx) { |
| if (!window._adjAdoptedSpans || !window._adjAdoptedSpans[schema]) return; |
| window._adjAdoptedSpans[schema].splice(idx, 1); |
| renderAdoptedSpansList(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 = '<span class="text-muted" style="font-size:0.8rem;">No spans adopted yet</span>'; |
| return; |
| } |
|
|
| var html = ''; |
| spans.forEach(function(span, idx) { |
| var source = span._source || 'unknown'; |
| html += '<div class="adj-adopted-span-item">'; |
| html += '<span class="adj-span-label-badge" style="background:#dbeafe;color:#1d4ed8;">' + |
| escapeHtml(span.name || 'span') + '</span>'; |
| html += '<span class="adj-span-text">' + escapeHtml(span.title || ('chars ' + span.start + '-' + span.end)) + '</span>'; |
| html += '<span class="text-muted" style="font-size:0.7rem;">from ' + escapeHtml(source) + '</span>'; |
| html += '<button type="button" class="adj-span-remove-btn" onclick="AdjudicationForms.removeAdoptedSpan(\'' + |
| schema + '\', ' + idx + ')"><i class="fas fa-times"></i></button>'; |
| html += '</div>'; |
| }); |
| listEl.innerHTML = html; |
| } |
|
|
| |
| return { |
| renderForm: renderForm, |
| collectDecisions: collectDecisions, |
| bindFormEvents: bindFormEvents, |
| resetColors: resetColors, |
| getAnnotatorColor: getAnnotatorColor, |
| formatTime: formatTime, |
| escapeHtml: escapeHtml, |
| adoptSpan: adoptSpan, |
| adoptAllSpans: adoptAllSpans, |
| removeAdoptedSpan: removeAdoptedSpan, |
| renderAdoptedSpansList: renderAdoptedSpansList, |
| CHIP_COLORS: CHIP_COLORS |
| }; |
| })(); |
|
|