| |
|
|
| |
| function debugLog(...args) { |
| if (window.config && window.config.debug) { |
| console.log(...args); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| function registerAnnotation(element) { |
| debugLog('[COMPAT] registerAnnotation called for:', element?.id); |
| |
| |
| } |
|
|
| |
| |
| |
| function registerTextAnnotation(element) { |
| debugLog('[COMPAT] registerTextAnnotation called for:', element?.id); |
| |
| } |
|
|
| function debugWarn(...args) { |
| if (window.config && window.config.debug) { |
| console.warn(...args); |
| } |
| } |
|
|
| |
| let currentInstance = null; |
| let currentAnnotations = {}; |
| let userState = null; |
| let isLoading = false; |
| let textSaveTimer = null; |
| let currentSpanAnnotations = []; |
| let debugLastInstanceId = null; |
| let debugOverlayCount = 0; |
|
|
| |
| let hasAttemptedForwardValidation = false; |
|
|
| |
| const boundEventHandlers = { |
| spanManagerMouseUp: null, |
| spanManagerKeyUp: null, |
| robustTextSelectionMouseUp: null, |
| robustTextSelectionKeyUp: null |
| }; |
|
|
| let aiAssistantManger = new AIAssistantManager(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| function flushPendingSave() { |
| if (!textSaveTimer || !currentInstance) return; |
| clearTimeout(textSaveTimer); |
| textSaveTimer = null; |
|
|
| syncAnnotationsFromDOM(); |
|
|
| const labelAnnotations = {}; |
| for (const [schema, labels] of Object.entries(currentAnnotations)) { |
| for (const [label, value] of Object.entries(labels)) { |
| labelAnnotations[`${schema}:${label}`] = value; |
| } |
| } |
|
|
| const payload = JSON.stringify({ |
| instance_id: currentInstance.id, |
| annotations: labelAnnotations, |
| span_annotations: extractSpanAnnotationsFromDOM() |
| }); |
|
|
| navigator.sendBeacon('/updateinstance', |
| new Blob([payload], {type: 'application/json'})); |
| } |
|
|
| window.addEventListener('beforeunload', flushPendingSave); |
| document.addEventListener('visibilitychange', function() { |
| if (document.visibilityState === 'hidden') flushPendingSave(); |
| }); |
|
|
| |
| let deepDebugState = { |
| navigationCalls: 0, |
| instanceIdChanges: [], |
| overlayStates: [], |
| spanManagerCalls: [], |
| lastAction: null, |
| timestamp: new Date().toISOString() |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| class FormLayoutManager { |
| constructor() { |
| this.config = null; |
| this.initialized = false; |
| } |
|
|
| |
| |
| |
| |
| initialize(layoutConfig = {}) { |
| this.config = this.mergeDefaults(layoutConfig); |
| this.applyGridProperties(); |
| this.wrapFormsInLayoutContainer(); |
| this.setupGroups(); |
| this.applyOrdering(); |
| this.setupResponsiveBreakpoints(); |
| this.initialized = true; |
| debugLog('[FormLayoutManager] Initialized with config:', this.config); |
| } |
|
|
| |
| |
| |
| mergeDefaults(config) { |
| return { |
| grid: { |
| columns: 2, |
| gap: '1rem', |
| row_gap: null, |
| align_items: 'start', |
| ...config?.grid |
| }, |
| breakpoints: { |
| mobile: 480, |
| tablet: 768, |
| ...config?.breakpoints |
| }, |
| styling: { |
| align_items: 'start', |
| content_align: 'left', |
| group_background_odd: '#fafafa', |
| group_background_even: '#f8f9fc', |
| group_padding: '0.5rem 0.75rem', |
| form_padding: '0.375rem 0.5rem', |
| ...config?.styling |
| }, |
| groups: config?.groups || [], |
| order: config?.order || null |
| }; |
| } |
|
|
| |
| |
| |
| applyGridProperties() { |
| const root = document.documentElement; |
|
|
| |
| root.style.setProperty('--layout-columns', this.config.grid.columns); |
| root.style.setProperty('--layout-gap', this.config.grid.gap); |
| root.style.setProperty('--layout-row-gap', this.config.grid.row_gap || this.config.grid.gap); |
|
|
| |
| const alignItems = this.config.styling.align_items || this.config.grid.align_items || 'start'; |
| root.style.setProperty('--layout-align', alignItems); |
|
|
| |
| root.style.setProperty('--layout-content-align', this.config.styling.content_align); |
|
|
| |
| root.style.setProperty('--group-bg-odd', this.config.styling.group_background_odd); |
| root.style.setProperty('--group-bg-even', this.config.styling.group_background_even); |
|
|
| |
| root.style.setProperty('--group-padding', this.config.styling.group_padding); |
| root.style.setProperty('--form-padding', this.config.styling.form_padding); |
| } |
|
|
| |
| |
| |
| wrapFormsInLayoutContainer() { |
| const container = document.getElementById('annotation-forms'); |
| if (!container) return; |
|
|
| |
| if (container.querySelector('.annotation-forms-layout')) { |
| debugLog('[FormLayoutManager] Layout container already exists'); |
| return; |
| } |
|
|
| const wrapper = document.createElement('div'); |
| wrapper.className = 'annotation-forms-layout'; |
|
|
| |
| const forms = container.querySelectorAll('.annotation-form'); |
| if (forms.length === 0) { |
| debugLog('[FormLayoutManager] No annotation forms found'); |
| return; |
| } |
|
|
| |
| forms.forEach(form => { |
| |
| if (!form.hasAttribute('data-grid-columns')) { |
| form.setAttribute('data-grid-columns', '1'); |
| } |
| wrapper.appendChild(form); |
| }); |
|
|
| |
| const pairwiseDisplay = container.querySelector('.pairwise-items-display-container'); |
| if (pairwiseDisplay) { |
| pairwiseDisplay.after(wrapper); |
| } else { |
| container.insertBefore(wrapper, container.firstChild); |
| } |
|
|
| debugLog('[FormLayoutManager] Wrapped', forms.length, 'forms in layout container'); |
| } |
|
|
| |
| |
| |
| setupGroups() { |
| if (!this.config.groups || this.config.groups.length === 0) return; |
|
|
| const container = document.querySelector('.annotation-forms-layout') || |
| document.querySelector('.annotation-forms-grid'); |
| if (!container) return; |
|
|
| this.config.groups.forEach(groupConfig => { |
| const groupElement = this.createGroupElement(groupConfig, container); |
| if (groupElement) { |
| |
| groupConfig.schemas.forEach(schemaName => { |
| const form = container.querySelector(`[data-schema-name="${schemaName}"]`); |
| if (form) { |
| const content = groupElement.querySelector('.annotation-form-group-content'); |
| if (content) { |
| content.appendChild(form); |
| } |
| } |
| }); |
|
|
| |
| container.appendChild(groupElement); |
| } |
| }); |
|
|
| debugLog('[FormLayoutManager] Setup', this.config.groups.length, 'groups'); |
| } |
|
|
| |
| |
| |
| createGroupElement(groupConfig, container) { |
| const group = document.createElement('div'); |
| group.className = 'annotation-form-group'; |
| group.id = `group-${groupConfig.id}`; |
|
|
| |
| if (groupConfig.background_color) { |
| group.style.setProperty('--group-bg', groupConfig.background_color); |
| group.style.backgroundColor = groupConfig.background_color; |
| } |
|
|
| if (groupConfig.collapsed_default) { |
| group.classList.add('collapsed'); |
| } |
|
|
| let headerHtml = ` |
| <div class="annotation-form-group-header"> |
| <div> |
| ${groupConfig.title ? `<h4 class="annotation-form-group-title">${this.escapeHtml(groupConfig.title)}</h4>` : ''} |
| ${groupConfig.description ? `<p class="annotation-form-group-description">${this.escapeHtml(groupConfig.description)}</p>` : ''} |
| </div> |
| `; |
|
|
| if (groupConfig.collapsible) { |
| headerHtml += ` |
| <button type="button" class="annotation-form-group-toggle" aria-label="Toggle group"> |
| <i class="fas fa-chevron-down"></i> |
| </button> |
| `; |
| } |
|
|
| headerHtml += '</div>'; |
|
|
| group.innerHTML = headerHtml + '<div class="annotation-form-group-content"></div>'; |
|
|
| |
| if (groupConfig.collapsible) { |
| const toggle = group.querySelector('.annotation-form-group-toggle'); |
| toggle.addEventListener('click', () => { |
| group.classList.toggle('collapsed'); |
| }); |
| } |
|
|
| return group; |
| } |
|
|
| |
| |
| |
| applyOrdering() { |
| const container = document.querySelector('.annotation-forms-layout') || |
| document.querySelector('.annotation-forms-grid'); |
| if (!container) return; |
|
|
| |
| if (this.config.order && Array.isArray(this.config.order)) { |
| this.config.order.forEach((schemaName, index) => { |
| const form = container.querySelector(`[data-schema-name="${schemaName}"]`); |
| if (form) { |
| form.style.order = index; |
| } |
| }); |
| } |
|
|
| |
| const formsWithOrder = container.querySelectorAll('[data-grid-order]'); |
| formsWithOrder.forEach(form => { |
| const order = parseInt(form.getAttribute('data-grid-order'), 10); |
| if (!isNaN(order)) { |
| form.style.order = order; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| setupResponsiveBreakpoints() { |
| const mobile = this.config.breakpoints.mobile; |
| const tablet = this.config.breakpoints.tablet; |
|
|
| |
| if (mobile !== 480 || tablet !== 768) { |
| const styleId = 'layout-breakpoints-custom'; |
| let styleEl = document.getElementById(styleId); |
| if (!styleEl) { |
| styleEl = document.createElement('style'); |
| styleEl.id = styleId; |
| document.head.appendChild(styleEl); |
| } |
|
|
| styleEl.textContent = ` |
| @media (max-width: ${mobile}px) { |
| .annotation-forms-layout, |
| .annotation-forms-grid { |
| --layout-columns: 1 !important; |
| } |
| .annotation-forms-layout .annotation-form[data-grid-columns], |
| .annotation-forms-grid .annotation-form[data-grid-columns] { |
| grid-column: span 1 !important; |
| } |
| } |
| @media (min-width: ${mobile + 1}px) and (max-width: ${tablet}px) { |
| .annotation-forms-layout .annotation-form[data-grid-columns="3"], |
| .annotation-forms-layout .annotation-form[data-grid-columns="4"], |
| .annotation-forms-layout .annotation-form[data-grid-columns="5"], |
| .annotation-forms-layout .annotation-form[data-grid-columns="6"], |
| .annotation-forms-grid .annotation-form[data-grid-columns="3"], |
| .annotation-forms-grid .annotation-form[data-grid-columns="4"], |
| .annotation-forms-grid .annotation-form[data-grid-columns="5"], |
| .annotation-forms-grid .annotation-form[data-grid-columns="6"] { |
| grid-column: span 2; |
| } |
| } |
| `; |
| } |
| } |
|
|
| |
| |
| |
| escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
| } |
|
|
| |
| window.formLayoutManager = new FormLayoutManager(); |
|
|
| |
| |
| |
| function logDeepDebug(action, extraData = {}) { |
| |
| if (!window.config || !window.config.debug) { |
| return; |
| } |
|
|
| const state = { |
| timestamp: new Date().toISOString(), |
| action: action, |
| currentInstanceId: currentInstance?.id, |
| debugLastInstanceId: debugLastInstanceId, |
| isLoading: isLoading, |
| overlayCount: getCurrentOverlayCount(), |
| spanManagerExists: !!window.spanManager, |
| spanManagerInitialized: window.spanManager?.isInitialized, |
| ...extraData |
| }; |
|
|
| debugLog(`[DEEP DEBUG NAV] ${action}:`, state); |
| deepDebugState.lastAction = action; |
| deepDebugState.timestamp = new Date().toISOString(); |
|
|
| |
| if (extraData.newInstanceId || extraData.currentInstanceId) { |
| deepDebugState.instanceIdChanges.push({ |
| timestamp: new Date().toISOString(), |
| from: debugLastInstanceId, |
| to: extraData.newInstanceId || extraData.currentInstanceId, |
| action: action |
| }); |
| } |
|
|
| |
| deepDebugState.overlayStates.push({ |
| timestamp: new Date().toISOString(), |
| action: action, |
| overlayCount: getCurrentOverlayCount(), |
| instanceId: currentInstance?.id |
| }); |
|
|
| |
| if (deepDebugState.instanceIdChanges.length > 20) { |
| deepDebugState.instanceIdChanges = deepDebugState.instanceIdChanges.slice(-20); |
| } |
| if (deepDebugState.overlayStates.length > 20) { |
| deepDebugState.overlayStates = deepDebugState.overlayStates.slice(-20); |
| } |
| } |
|
|
| |
| |
| |
| function getCurrentOverlayCount() { |
| const spanOverlays = document.getElementById('span-overlays'); |
| return spanOverlays ? spanOverlays.children.length : 0; |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', function () { |
| |
| |
| if (window.config && !window.config.is_annotation_page) { |
| |
| |
| currentInstance = { id: '__phase_page__', text: '', displayed_text: '' }; |
| window.currentInstance = currentInstance; |
| currentAnnotations = {}; |
|
|
| setLoading(false); |
| setupInputEventListeners(); |
| validateRequiredFields(); |
| return; |
| } |
| loadCurrentInstance(); |
| setupEventListeners(); |
| |
| validateRequiredFields(); |
| |
| initializeSpanManagerIntegration(); |
| |
| if (typeof initDisplayLogic === 'function') { |
| initDisplayLogic(); |
| } |
| |
| |
| const layoutConfig = window.config?.ui_config?.layout || window.config?.layout; |
| if (layoutConfig) { |
| window.formLayoutManager.initialize(layoutConfig); |
| } |
| |
| initPairwiseAnnotation(); |
| |
| initBwsAnnotation(); |
| }); |
|
|
| |
| |
| |
| function trackOverlayCreation(overlay, context = 'unknown') { |
| if (!window.config || !window.config.debug) return; |
|
|
| debugLog(`[DEBUG] OVERLAY CREATED in ${context}:`, { |
| className: overlay.className, |
| id: overlay.id, |
| parentId: overlay.parentElement?.id, |
| timestamp: new Date().toISOString() |
| }); |
|
|
| |
| const totalOverlays = document.querySelectorAll('.span-overlay').length; |
| debugLog(`[DEBUG] TOTAL OVERLAYS after creation: ${totalOverlays}`); |
| } |
|
|
| function trackOverlayRemoval(overlay, context = 'unknown') { |
| if (!window.config || !window.config.debug) return; |
|
|
| debugLog(`[DEBUG] OVERLAY REMOVED in ${context}:`, { |
| className: overlay.className, |
| id: overlay.id, |
| timestamp: new Date().toISOString() |
| }); |
|
|
| |
| const totalOverlays = document.querySelectorAll('.span-overlay').length; |
| debugLog(`[DEBUG] TOTAL OVERLAYS after removal: ${totalOverlays}`); |
| } |
|
|
| function debugTrackOverlays(action, instanceId = null) { |
| if (!window.config || !window.config.debug) return; |
|
|
| const spanOverlays = document.getElementById('span-overlays'); |
| const overlayCount = spanOverlays ? spanOverlays.children.length : 0; |
| const instanceText = document.getElementById('instance-text'); |
| const textContent = document.getElementById('text-content'); |
|
|
| debugLog(`[DEBUG OVERLAY TRACKING] ${action}:`, { |
| instanceId: instanceId || currentInstance?.id, |
| lastInstanceId: debugLastInstanceId, |
| overlayCount: overlayCount, |
| spanOverlaysExists: !!spanOverlays, |
| instanceTextExists: !!instanceText, |
| textContentExists: !!textContent, |
| spanOverlaysHTML: spanOverlays ? spanOverlays.innerHTML.substring(0, 200) + '...' : 'null', |
| timestamp: new Date().toISOString() |
| }); |
|
|
| debugOverlayCount = overlayCount; |
| if (instanceId) debugLastInstanceId = instanceId; |
| } |
|
|
| |
| function debugVerifyOverlayCleanup() { |
| if (!window.config || !window.config.debug) return; |
|
|
| const spanOverlays = document.getElementById('span-overlays'); |
| if (!spanOverlays) { |
| debugWarn('[DEBUG] span-overlays container not found during cleanup verification'); |
| return; |
| } |
|
|
| const overlayCount = spanOverlays.children.length; |
| debugLog(`[DEBUG] Overlay cleanup verification:`, { |
| overlayCount: overlayCount, |
| containerEmpty: overlayCount === 0, |
| containerInnerHTML: spanOverlays.innerHTML, |
| containerChildren: Array.from(spanOverlays.children).map(child => ({ |
| tagName: child.tagName, |
| className: child.className, |
| dataset: child.dataset |
| })) |
| }); |
|
|
| if (overlayCount > 0) { |
| debugWarn('[DEBUG] WARNING: Overlays still present after expected cleanup!'); |
| } |
| } |
|
|
| function setupEventListeners() { |
| |
| document.querySelectorAll('.annotation-form').forEach(function(form) { |
| form.addEventListener('submit', function(e) { e.preventDefault(); }); |
| }); |
|
|
| |
| const goToBtn = document.getElementById('go-to-btn'); |
| const goToInput = document.getElementById('go_to'); |
| if (goToBtn && goToInput) { |
| goToBtn.addEventListener('click', function () { |
| const goToValue = goToInput.value; |
| if (goToValue && goToValue > 0) { |
| |
| navigateToInstance(parseInt(goToValue) - 1); |
| } |
| }); |
|
|
| |
| goToInput.addEventListener('keypress', function (e) { |
| if (e.key === 'Enter') { |
| goToBtn.click(); |
| } |
| }); |
| } |
|
|
| |
| document.addEventListener('keydown', function (e) { |
| |
| const inputType = e.target.getAttribute('type'); |
| const isTextInput = e.target.tagName === 'TEXTAREA' || |
| (e.target.tagName === 'INPUT' && inputType !== 'radio' && inputType !== 'checkbox'); |
|
|
| if (isTextInput) { |
| return; |
| } |
|
|
| switch (e.key) { |
| case 'ArrowLeft': |
| e.preventDefault(); |
| navigateToPrevious(); |
| break; |
| case 'ArrowRight': |
| e.preventDefault(); |
| navigateToNext(); |
| break; |
| } |
| }); |
|
|
| |
| document.addEventListener('keyup', function (e) { |
| |
| const activeElement = document.activeElement; |
| const activeId = activeElement.id; |
| const activeType = activeElement.getAttribute('type'); |
| const isTextInput = activeElement.tagName === 'TEXTAREA' || |
| activeId === 'go_to' || |
| (activeElement.tagName === 'INPUT' && activeType !== 'radio' && activeType !== 'checkbox'); |
|
|
| if (isTextInput) { |
| return; |
| } |
|
|
| const key = e.key.toLowerCase(); |
|
|
| |
| const checkboxes = document.querySelectorAll('input[type="checkbox"]'); |
| for (const checkbox of checkboxes) { |
| const dataKey = checkbox.getAttribute('data-key'); |
| if (dataKey && key === dataKey.toLowerCase()) { |
| checkbox.checked = !checkbox.checked; |
| |
| checkbox.dispatchEvent(new Event('change', { bubbles: true })); |
| if (checkbox.onclick) { |
| checkbox.onclick.apply(checkbox); |
| } |
| return; |
| } |
| } |
|
|
| |
| const radios = document.querySelectorAll('input[type="radio"]'); |
| for (const radio of radios) { |
| const dataKey = radio.getAttribute('data-key'); |
| if (dataKey && key === dataKey.toLowerCase()) { |
| radio.checked = true; |
| |
| radio.dispatchEvent(new Event('change', { bubbles: true })); |
| if (radio.onclick) { |
| radio.onclick.apply(radio); |
| } |
| return; |
| } |
| } |
|
|
| |
| const pairwiseTiles = document.querySelectorAll('.pairwise-tile'); |
| for (const tile of pairwiseTiles) { |
| const dataKey = tile.getAttribute('data-key'); |
| if (dataKey && key === dataKey) { |
| selectPairwiseTile(tile); |
| return; |
| } |
| } |
|
|
| |
| const pairwiseButtons = document.querySelectorAll('.pairwise-tie-btn, .pairwise-neither-btn'); |
| for (const btn of pairwiseButtons) { |
| const dataKey = btn.getAttribute('data-key'); |
| if (dataKey && key === dataKey) { |
| selectPairwiseOption(btn); |
| return; |
| } |
| } |
|
|
| |
| const bwsTiles = document.querySelectorAll('.bws-tile'); |
| for (const tile of bwsTiles) { |
| const dataKey = tile.getAttribute('data-key'); |
| if (dataKey && key === dataKey) { |
| selectBwsTile(tile); |
| return; |
| } |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function initializeSpanManagerIntegration() { |
| |
| const checkSpanManager = () => { |
| if (window.spanManager && window.spanManager.isInitialized) { |
| debugLog('Annotation.js: Span manager integration initialized'); |
| setupSpanLabelSelector(); |
| } else { |
| setTimeout(checkSpanManager, 100); |
| } |
| }; |
| checkSpanManager(); |
| } |
|
|
| |
| |
| |
| |
| function setupSpanLabelSelector() { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - ENTRY POINT'); |
|
|
| |
| const spanLabelCheckboxes = document.querySelectorAll('input[name*="span_label"]'); |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Found span label checkboxes:', spanLabelCheckboxes.length); |
|
|
| if (spanLabelCheckboxes.length === 0) { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - No span label checkboxes found'); |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - EXIT POINT (no checkboxes)'); |
| return; |
| } |
|
|
| |
| spanLabelCheckboxes.forEach((checkbox, index) => { |
| debugLog(`🔍 [DEBUG] setupSpanLabelSelector() - Setting up checkbox ${index}:`, { |
| name: checkbox.name, |
| id: checkbox.id, |
| value: checkbox.value |
| }); |
|
|
| |
| const observer = new MutationObserver((mutations) => { |
| mutations.forEach((mutation) => { |
| if (mutation.type === 'attributes' && mutation.attributeName === 'checked') { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Checkbox checked attribute changed:', { |
| id: checkbox.id, |
| oldValue: mutation.oldValue, |
| newValue: checkbox.checked, |
| stack: new Error().stack |
| }); |
| } |
| }); |
| }); |
|
|
| observer.observe(checkbox, { |
| attributes: true, |
| attributeOldValue: true, |
| attributeFilter: ['checked'] |
| }); |
|
|
| |
| const originalChecked = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'checked'); |
| Object.defineProperty(checkbox, 'checked', { |
| get: function() { |
| return originalChecked.get.call(this); |
| }, |
| set: function(value) { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Checkbox checked property being set:', { |
| id: this.id, |
| oldValue: originalChecked.get.call(this), |
| newValue: value, |
| stack: new Error().stack |
| }); |
| originalChecked.set.call(this, value); |
| } |
| }); |
|
|
| |
| if (!checkbox.hasAttribute('data-span-label-setup')) { |
| checkbox.addEventListener('change', function () { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Checkbox changed:', { |
| name: this.name, |
| checked: this.checked, |
| value: this.value |
| }); |
|
|
| |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Change event stack trace:', new Error().stack); |
|
|
| |
| |
| if (this.checked && this.hasAttribute('data-just-checked')) { |
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - Ignoring change event for just-checked checkbox'); |
| this.removeAttribute('data-just-checked'); |
| return; |
| } |
|
|
| |
| |
| |
| |
| }); |
|
|
| |
| checkbox.setAttribute('data-span-label-setup', 'true'); |
| } |
| }); |
|
|
| debugLog('🔍 [DEBUG] setupSpanLabelSelector() - EXIT POINT (setup complete)'); |
| } |
|
|
|
|
|
|
| |
| |
| |
| function checkForSpanAnnotations() { |
| if (!currentInstance || !currentInstance.annotation_scheme) { |
| return false; |
| } |
|
|
| |
| for (const schema of Object.values(currentInstance.annotation_scheme)) { |
| if (schema.type === 'span') { |
| return true; |
| } |
| } |
| return false; |
| } |
|
|
| |
| |
| |
| function getSpanLabelsFromScheme() { |
| const labels = []; |
|
|
| if (!currentInstance || !currentInstance.annotation_scheme) { |
| return labels; |
| } |
|
|
| for (const [schemaName, schema] of Object.entries(currentInstance.annotation_scheme)) { |
| if (schema.type === 'span' && schema.labels) { |
| labels.push(...schema.labels); |
| } |
| } |
|
|
| return labels; |
| } |
|
|
| |
| |
| |
| async function loadSpanAnnotations() { |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - ENTRY POINT'); |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - currentInstance:', currentInstance); |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - currentInstance.id:', currentInstance?.id); |
|
|
| if (!currentInstance || !currentInstance.id) { |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - EXIT POINT (no currentInstance or id)'); |
| return; |
| } |
|
|
| try { |
| |
| if (!window.spanManager) { |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - Initializing span manager'); |
| initializeSpanManagerIntegration(); |
| } |
|
|
| |
| await new Promise(resolve => { |
| const checkSpanManager = () => { |
| if (window.spanManager) { |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - Span manager ready'); |
| resolve(); |
| } else { |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - Span manager not ready, retrying...'); |
| setTimeout(checkSpanManager, 100); |
| } |
| }; |
| checkSpanManager(); |
| }); |
|
|
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - About to call spanManager.loadAnnotations()'); |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - Instance ID for API call:', currentInstance.id); |
|
|
| |
| await window.spanManager.loadAnnotations(currentInstance.id); |
|
|
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - spanManager.loadAnnotations() completed'); |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - EXIT POINT (success)'); |
| } catch (error) { |
| console.error('🔍 [DEBUG] loadSpanAnnotations() - Error loading span annotations:', error); |
| debugLog('🔍 [DEBUG] loadSpanAnnotations() - EXIT POINT (error)'); |
| } |
| } |
|
|
| async function loadCurrentInstance() { |
| |
| hasAttemptedForwardValidation = false; |
|
|
| try { |
| setLoading(true); |
| showError(false); |
|
|
| |
| debugTrackOverlays('START_LOAD_CURRENT_INSTANCE'); |
|
|
| |
| const instanceTextElement = document.getElementById('instance-text'); |
| const instanceIdElement = document.getElementById('instance_id'); |
|
|
| if (!instanceTextElement) { |
| throw new Error('Instance text element not found'); |
| } |
|
|
| |
| const instanceText = instanceTextElement.innerHTML; |
|
|
| |
| const instanceId = instanceIdElement ? instanceIdElement.value : null; |
| debugLog(`🔍 [DEBUG] loadCurrentInstance: Read instance_id from DOM: '${instanceId}'`); |
|
|
| if (!instanceText || instanceText.trim() === '') { |
| showError(true, 'No instance text available'); |
| return; |
| } |
|
|
| |
| currentInstance = { |
| id: instanceId, |
| text: instanceTextElement.textContent || instanceTextElement.innerText, |
| displayed_text: instanceText |
| }; |
|
|
| |
| window.currentInstance = currentInstance; |
|
|
| |
| if (window.interactionTracker && instanceId) { |
| window.interactionTracker.setInstanceId(instanceId); |
| } |
|
|
| |
| const progressCounter = document.getElementById('progress-counter'); |
| if (progressCounter) { |
| const progressText = progressCounter.textContent; |
| const match = progressText.match(/(\d+)\/(\d+)/); |
| if (match) { |
| const annotated = parseInt(match[1]); |
| const total = parseInt(match[2]); |
| userState = { |
| assignments: { |
| annotated: annotated, |
| total: total |
| }, |
| annotations: { |
| by_instance: {} |
| } |
| }; |
| } |
| } |
|
|
| updateProgressDisplay(); |
| updateInstanceDisplay(); |
|
|
| |
| |
| clearAllFormInputs(); |
|
|
| restoreSpanAnnotationsFromHTML(); |
| loadAnnotations(); |
| |
| |
| |
| if (window.MemoPanel && typeof window.MemoPanel.reload === 'function') { |
| window.MemoPanel.reload(); |
| } |
| generateAnnotationForms(); |
| aiAssistantManger.getAiAssistantName(); |
|
|
| |
| populatePairwiseTileContent(); |
|
|
| |
| await populateDynamicSchemaContent(); |
|
|
| |
| |
| |
| |
| |
| if (window.CodebookPanel && typeof window.CodebookPanel.onInstance === 'function') { |
| window.CodebookPanel.onInstance(); |
| } |
|
|
| |
| debugLog('🔍 [DEBUG] loadCurrentInstance() - About to call loadSpanAnnotations()'); |
| debugLog('🔍 [DEBUG] loadCurrentInstance() - currentInstance.id:', currentInstance?.id); |
| await loadSpanAnnotations(); |
| debugLog('🔍 [DEBUG] loadCurrentInstance() - loadSpanAnnotations() completed'); |
|
|
| |
| setTimeout(() => { |
| populateInputValues(); |
| }, 0); |
|
|
| } catch (error) { |
| console.error('Error loading current instance:', error); |
| showError(true, error.message); |
| } finally { |
| setLoading(false); |
| } |
| } |
|
|
| function updateProgressDisplay() { |
| |
| |
| debugLog('Progress display updated from server-rendered HTML'); |
| } |
|
|
| function updateInstanceDisplay() { |
| |
| |
| const instanceIdInput = document.getElementById('instance_id'); |
| if (instanceIdInput && currentInstance && currentInstance.id) { |
| const oldValue = instanceIdInput.value; |
| instanceIdInput.value = currentInstance.id; |
| debugLog(`🔍 [DEBUG] updateInstanceDisplay: Updated instance_id from '${oldValue}' to '${currentInstance.id}'`); |
|
|
| |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| if (isFirefox) { |
| debugLog('🔍 [DEBUG] updateInstanceDisplay: Firefox detected - forcing input update'); |
|
|
| |
| const tempValue = instanceIdInput.value; |
| instanceIdInput.value = ''; |
| instanceIdInput.value = tempValue; |
|
|
| |
| instanceIdInput.dispatchEvent(new Event('input', { bubbles: true })); |
| instanceIdInput.dispatchEvent(new Event('change', { bubbles: true })); |
|
|
| |
| instanceIdInput.offsetHeight; |
|
|
| debugLog(`🔍 [DEBUG] updateInstanceDisplay: Firefox input update completed`); |
| } |
| } else { |
| debugLog(`🔍 [DEBUG] updateInstanceDisplay: Could not update instance_id - input: ${!!instanceIdInput}, currentInstance: ${!!currentInstance}, currentInstance.id: ${currentInstance?.id}`); |
| } |
| debugLog('[DEBUG] updateInstanceDisplay: Instance display updated from server'); |
| } |
|
|
| |
| function clearAllFormInputs() { |
| debugLog('🔍 Clearing all form inputs'); |
|
|
| |
| const textInputs = document.querySelectorAll('input[type="text"], textarea.annotation-input'); |
| textInputs.forEach(input => { |
| input.value = ''; |
| }); |
|
|
| |
| const radioInputs = document.querySelectorAll('input[type="radio"]'); |
| radioInputs.forEach(input => { |
| input.checked = false; |
| }); |
|
|
| |
| const checkboxInputs = document.querySelectorAll('input[type="checkbox"]'); |
| checkboxInputs.forEach(input => { |
| input.checked = false; |
| }); |
|
|
| |
| const sliderInputs = document.querySelectorAll('input[type="range"]'); |
| sliderInputs.forEach(input => { |
| input.value = input.getAttribute('min') || input.getAttribute('starting_value') || '0'; |
| const valueDisplay = document.getElementById(`${input.name}-value`); |
| if (valueDisplay) { |
| valueDisplay.textContent = input.value; |
| } |
| }); |
|
|
| |
| const selectInputs = document.querySelectorAll('select.annotation-input'); |
| selectInputs.forEach(input => { |
| input.selectedIndex = 0; |
| }); |
|
|
| |
| const numberInputs = document.querySelectorAll('input[type="number"].annotation-input'); |
| numberInputs.forEach(input => { |
| input.value = ''; |
| }); |
|
|
| |
| |
| const hiddenAnnotationInputs = document.querySelectorAll('input[type="hidden"].annotation-input'); |
| hiddenAnnotationInputs.forEach(input => { |
| if (input.getAttribute('data-server-set') !== 'true') { |
| input.value = ''; |
| input.removeAttribute('data-modified'); |
| debugLog('🔍 Cleared hidden annotation input (browser-cached):', input.getAttribute('name')); |
| } else { |
| debugLog('🔍 Preserving server-provided hidden annotation input:', input.getAttribute('name')); |
| } |
| }); |
|
|
| |
| document.querySelectorAll('.bws-tile.selected').forEach( |
| tile => tile.classList.remove('selected') |
| ); |
|
|
| |
| document.querySelectorAll('.ranking-list .ranking-item').forEach((item, idx) => { |
| const rank = item.querySelector('.ranking-rank'); |
| if (rank) rank.textContent = idx + 1; |
| }); |
|
|
| |
| document.querySelectorAll('.hier-checkbox').forEach(cb => { |
| cb.checked = false; |
| }); |
| document.querySelectorAll('.hier-selected-tags').forEach(tags => { |
| tags.innerHTML = ''; |
| }); |
|
|
| |
| document.querySelectorAll('.traj-correctness-btn.selected').forEach(btn => { |
| btn.classList.remove('selected'); |
| }); |
| document.querySelectorAll('.traj-error-details').forEach(div => { |
| div.style.display = 'none'; |
| }); |
| document.querySelectorAll('.traj-step-status').forEach(el => { |
| el.textContent = ''; |
| el.className = 'traj-step-status'; |
| }); |
| if (window._trajState) { |
| Object.keys(window._trajState).forEach(k => { |
| window._trajState[k] = { steps: [] }; |
| }); |
| } |
|
|
| |
| if (window._trajEditState) { |
| Object.keys(window._trajEditState).forEach(k => { |
| window._trajEditState[k] = { entries: {}, final_answer: null }; |
| }); |
| } |
|
|
| |
| |
| |
| |
| const annotationDataInputs = document.querySelectorAll('input.annotation-data-input'); |
| annotationDataInputs.forEach(input => { |
| |
| if (input.getAttribute('data-server-set') !== 'true') { |
| input.value = ''; |
| debugLog('🔍 Cleared annotation data input (browser-cached):', input.id); |
| } else { |
| debugLog('🔍 Preserving server-provided annotation data:', input.id); |
| } |
| }); |
|
|
| |
| |
| const imageContainers = document.querySelectorAll('.image-annotation-container'); |
| imageContainers.forEach(container => { |
| if (container.annotationManager && typeof container.annotationManager.clearAnnotations === 'function') { |
| |
| const schemaName = container.getAttribute('data-schema'); |
| const hiddenInput = schemaName ? document.getElementById('input-' + schemaName) : null; |
|
|
| |
| if (!hiddenInput || hiddenInput.getAttribute('data-server-set') !== 'true') { |
| container.annotationManager.clearAnnotations(); |
| debugLog('🔍 Cleared image annotation manager for container (no server data)'); |
| } else { |
| debugLog('🔍 Preserving image annotation manager (has server data)'); |
| } |
| } |
| }); |
|
|
| debugLog('✅ All form inputs cleared'); |
| } |
|
|
| async function loadAnnotations() { |
| try { |
| debugLog('🔍 Loading annotations for instance:', currentInstance.id); |
|
|
| |
| |
| |
| |
|
|
| currentAnnotations = {}; |
|
|
| |
| |
| const checkboxInputs = document.querySelectorAll('input[type="checkbox"]'); |
| checkboxInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| |
| const serverChecked = input.hasAttribute('checked'); |
| |
| input.checked = serverChecked; |
| if (schema && labelName && serverChecked) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| const radioInputs = document.querySelectorAll('input[type="radio"]'); |
| radioInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| |
| const serverChecked = input.hasAttribute('checked'); |
| |
| input.checked = serverChecked; |
| if (schema && labelName && serverChecked) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| |
| |
| const textInputs = document.querySelectorAll('input[type="text"], textarea.annotation-input'); |
| textInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| |
| |
| |
| let serverValue; |
| if (input.tagName.toLowerCase() === 'textarea') { |
| serverValue = input.textContent || ''; |
| } else { |
| serverValue = input.getAttribute('value') || ''; |
| } |
| |
| input.value = serverValue; |
| if (schema && labelName && serverValue) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = serverValue; |
| } |
| }); |
|
|
| |
| const numberInputs = document.querySelectorAll('input[type="number"].annotation-input'); |
| numberInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| const serverValue = input.getAttribute('value'); |
| if (serverValue) { |
| input.value = serverValue; |
| } |
| if (schema && labelName && serverValue) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = serverValue; |
| } |
| }); |
|
|
| |
| const sliderInputs = document.querySelectorAll('input[type="range"]'); |
| sliderInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| |
| const serverValue = input.getAttribute('value'); |
| if (serverValue) { |
| input.value = serverValue; |
| } |
| if (schema && labelName) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| |
| const selectInputs = document.querySelectorAll('select.annotation-input'); |
| selectInputs.forEach(select => { |
| const schema = select.getAttribute('schema'); |
| const labelName = select.getAttribute('label_name'); |
| |
| const selectedOption = select.querySelector('option[selected]'); |
| if (selectedOption) { |
| |
| select.value = selectedOption.value; |
| } |
| if (schema && labelName && select.value) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = select.value; |
| } |
| }); |
|
|
| |
| |
| |
| |
| |
| const hiddenInputs = document.querySelectorAll('input[type="hidden"].annotation-input'); |
| hiddenInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| const isServerSet = input.hasAttribute('data-server-set'); |
| if (isServerSet) { |
| |
| const serverValue = input.getAttribute('value') || ''; |
| input.value = serverValue; |
| if (schema && labelName && serverValue) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = serverValue; |
| } |
| } else { |
| |
| input.value = ''; |
| } |
| }); |
|
|
| |
| |
| const annotationDataInputs = document.querySelectorAll('input.annotation-data-input'); |
| annotationDataInputs.forEach(input => { |
| if (input.name && input.value && input.getAttribute('data-server-set') === 'true') { |
| const schema = input.name; |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema]['_data'] = input.value; |
| } |
| }); |
|
|
| debugLog('🔍 Annotations loaded from DOM:', currentAnnotations); |
| } catch (error) { |
| console.error('❌ Error loading annotations:', error); |
| currentAnnotations = {}; |
| } |
| } |
|
|
| function generateAnnotationForms() { |
| const formsContainer = document.getElementById('annotation-forms'); |
|
|
| |
| |
| setupInputEventListeners(); |
| validateRequiredFields(); |
| } |
|
|
| async function saveAnnotations() { |
| if (!currentInstance || !currentInstance.id) { |
| return; |
| } |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackSave(currentInstance.id); |
| } |
|
|
| try { |
| const headers = { |
| 'Content-Type': 'application/json', |
| }; |
|
|
| |
| if (window.config && window.config.api_key) { |
| headers['X-API-Key'] = window.config.api_key; |
| } |
|
|
| |
| |
| syncAnnotationsFromDOM(); |
| |
| const spanAnnotations = extractSpanAnnotationsFromDOM(); |
| debugLog('[DEBUG] saveAnnotations: spanAnnotations to send:', spanAnnotations); |
|
|
| |
| const labelAnnotations = {}; |
| for (const [schema, labels] of Object.entries(currentAnnotations)) { |
| for (const [label, value] of Object.entries(labels)) { |
| const key = `${schema}:${label}`; |
| labelAnnotations[key] = value; |
| } |
| } |
| |
| const hiddenInputs = document.querySelectorAll('.annotation-data-input'); |
| hiddenInputs.forEach(input => { |
| if (input.name && input.value) { |
| |
| |
| const key = `${input.name}:::_data`; |
| labelAnnotations[key] = input.value; |
| debugLog('[DEBUG] saveAnnotations: collected hidden input:', input.name, '=', input.value.substring(0, 100) + '...'); |
| } |
| }); |
|
|
| const response = await fetch('/updateinstance', { |
| method: 'POST', |
| headers: headers, |
| body: JSON.stringify({ |
| instance_id: currentInstance.id, |
| annotations: labelAnnotations, |
| span_annotations: spanAnnotations |
| }) |
| }); |
|
|
| if (response.ok) { |
| |
| const responseText = await response.text(); |
| try { |
| const result = JSON.parse(responseText); |
| debugLog('[DEBUG] saveAnnotations: annotations saved:', result); |
| handleQualityControlResponse(result); |
| } catch (jsonError) { |
| console.error('[DEBUG] saveAnnotations: JSON parse error:', jsonError); |
| console.error('[DEBUG] saveAnnotations: Response text (first 500 chars):', responseText.substring(0, 500)); |
| |
| } |
| } else { |
| console.warn('[DEBUG] saveAnnotations: failed to save annotations:', await response.text()); |
| return false; |
| } |
|
|
| return true; |
|
|
| } catch (error) { |
| console.error('Error saving annotations:', error); |
| showError(true, 'Failed to save annotations: ' + error.message); |
| return false; |
| } |
| } |
|
|
| async function navigateToPrevious() { |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - ENTRY POINT'); |
| deepDebugState.navigationCalls++; |
|
|
| |
| hasAttemptedForwardValidation = false; |
|
|
| logDeepDebug('navigateToPrevious_start', { |
| currentInstanceId: currentInstance?.id, |
| overlayCount: getCurrentOverlayCount() |
| }); |
|
|
| if (isLoading) { |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Navigation blocked, still loading'); |
| return; |
| } |
|
|
| setLoading(true); |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Loading set to true'); |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackNavigation('prev', currentInstance?.id, null); |
| } |
|
|
| try { |
| |
| clearTimeout(textSaveTimer); |
| textSaveTimer = null; |
|
|
| |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Saving annotations before navigation'); |
| const saveSucceeded = await saveAnnotations(); |
| if (saveSucceeded === false) { |
| showNotification('Failed to save annotations. Please try again.', 'error'); |
| setLoading(false); |
| return; |
| } |
|
|
| |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Is Firefox:', isFirefox); |
|
|
| if (isFirefox) { |
| debugLog('[DEEP DEBUG NAV] Firefox detected - forcing overlay cleanup before navigation'); |
| const spanOverlays = document.getElementById('span-overlays'); |
| if (spanOverlays) { |
| const beforeCount = spanOverlays.children.length; |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Before Firefox cleanup:', beforeCount, 'overlays'); |
|
|
| |
| while (spanOverlays.firstChild) { |
| const child = spanOverlays.firstChild; |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Removing overlay child:', child.className, child.id); |
|
|
| |
| if (typeof trackOverlayRemoval === 'function') { |
| trackOverlayRemoval(child, 'navigateToPrevious Firefox cleanup'); |
| } |
|
|
| spanOverlays.removeChild(child); |
| } |
|
|
| |
| spanOverlays.offsetHeight; |
| const afterCount = spanOverlays.children.length; |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - After Firefox cleanup:', afterCount, 'overlays'); |
|
|
| |
| const remainingOverlays = document.querySelectorAll('.span-overlay'); |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Remaining overlays via querySelectorAll:', remainingOverlays.length); |
|
|
| if (remainingOverlays.length > 0) { |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - WARNING: Overlays still exist after cleanup!'); |
| remainingOverlays.forEach((overlay, index) => { |
| debugLog(`[DEEP DEBUG NAV] navigateToPrevious - Remaining overlay ${index}:`, overlay.className, overlay.id); |
| }); |
| } |
| } else { |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - No span-overlays container found'); |
| } |
| } |
|
|
| |
| const response = await fetch('/annotate', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| action: 'prev_instance', |
| instance_id: currentInstance?.id |
| }) |
| }); |
|
|
| if (response.ok) { |
| debugLog('[DEEP DEBUG NAV] navigateToPrevious - Navigation successful, reloading page'); |
|
|
| if (window.spanManager && typeof window.spanManager.onInstanceChange === 'function') { |
| window.spanManager.onInstanceChange(currentInstance?.id); |
| } |
|
|
| logDeepDebug('navigateToPrevious_success', { |
| currentInstanceId: currentInstance?.id, |
| overlayCount: getCurrentOverlayCount() |
| }); |
|
|
| |
| setTimeout(() => { |
| window.location.reload(); |
| }, 100); |
| } else { |
| console.error('[DEEP DEBUG NAV] navigateToPrevious - Navigation failed:', response.status); |
| setLoading(false); |
| } |
| } catch (error) { |
| console.error('[DEEP DEBUG NAV] navigateToPrevious - Navigation error:', error); |
| setLoading(false); |
| } |
| } |
|
|
| |
| |
| |
| |
| async function handleNavigationResponseError(response) { |
| console.error('[NAV] Navigation failed:', response.status); |
| if (response.status === 400) { |
| try { |
| const data = await response.json(); |
| if (data.status === 'validation_error') { |
| const schemas = (data.unsatisfied_schemas || []).join(', '); |
| showNotification(data.message || `Required annotations not completed: ${schemas}`, 'error'); |
| |
| hasAttemptedForwardValidation = true; |
| validateRequiredFields({ showErrors: true }); |
| return; |
| } |
| } catch (e) { |
| |
| } |
| } |
| showNotification('Navigation failed. Please try again.', 'error'); |
| } |
|
|
| async function navigateToNext() { |
| debugLog('[DEEP DEBUG NAV] navigateToNext - ENTRY POINT'); |
| deepDebugState.navigationCalls++; |
|
|
| logDeepDebug('navigateToNext_start', { |
| currentInstanceId: currentInstance?.id, |
| overlayCount: getCurrentOverlayCount() |
| }); |
|
|
| if (isLoading) { |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Navigation blocked, still loading'); |
| return; |
| } |
|
|
| |
| hasAttemptedForwardValidation = true; |
| if (!validateRequiredFields({ showErrors: true })) { |
| debugLog('[NAV] navigateToNext - blocked by client-side validation'); |
| return; |
| } |
|
|
| setLoading(true); |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Loading set to true'); |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackNavigation('next', currentInstance?.id, null); |
| } |
|
|
| try { |
| |
| clearTimeout(textSaveTimer); |
| textSaveTimer = null; |
|
|
| |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Saving annotations before navigation'); |
| const saveSucceeded = await saveAnnotations(); |
| if (saveSucceeded === false) { |
| showNotification('Failed to save annotations. Please try again.', 'error'); |
| setLoading(false); |
| return; |
| } |
|
|
| |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Is Firefox:', isFirefox); |
|
|
| if (isFirefox) { |
| debugLog('[DEEP DEBUG NAV] Firefox detected - forcing overlay cleanup before navigation'); |
| const spanOverlays = document.getElementById('span-overlays'); |
| if (spanOverlays) { |
| const beforeCount = spanOverlays.children.length; |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Before Firefox cleanup:', beforeCount, 'overlays'); |
|
|
| |
| while (spanOverlays.firstChild) { |
| const child = spanOverlays.firstChild; |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Removing overlay child:', child.className, child.id); |
|
|
| |
| if (typeof trackOverlayRemoval === 'function') { |
| trackOverlayRemoval(child, 'navigateToNext Firefox cleanup'); |
| } |
|
|
| spanOverlays.removeChild(child); |
| } |
|
|
| |
| spanOverlays.offsetHeight; |
| const afterCount = spanOverlays.children.length; |
| debugLog('[DEEP DEBUG NAV] navigateToNext - After Firefox cleanup:', afterCount, 'overlays'); |
|
|
| |
| const remainingOverlays = document.querySelectorAll('.span-overlay'); |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Remaining overlays via querySelectorAll:', remainingOverlays.length); |
|
|
| if (remainingOverlays.length > 0) { |
| debugLog('[DEEP DEBUG NAV] navigateToNext - WARNING: Overlays still exist after cleanup!'); |
| remainingOverlays.forEach((overlay, index) => { |
| debugLog(`[DEEP DEBUG NAV] navigateToNext - Remaining overlay ${index}:`, overlay.className, overlay.id); |
| }); |
| } |
| } else { |
| debugLog('[DEEP DEBUG NAV] navigateToNext - No span-overlays container found'); |
| } |
| } |
|
|
| |
| const response = await fetch('/annotate', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| action: 'next_instance', |
| instance_id: currentInstance?.id |
| }) |
| }); |
|
|
| if (response.ok) { |
| debugLog('[DEEP DEBUG NAV] navigateToNext - Navigation successful, reloading page'); |
|
|
| if (window.spanManager && typeof window.spanManager.onInstanceChange === 'function') { |
| window.spanManager.onInstanceChange(currentInstance?.id); |
| } |
|
|
| logDeepDebug('navigateToNext_success', { |
| currentInstanceId: currentInstance?.id, |
| overlayCount: getCurrentOverlayCount() |
| }); |
|
|
| |
| setTimeout(() => { |
| window.location.reload(); |
| }, 100); |
| } else { |
| handleNavigationResponseError(response); |
| setLoading(false); |
| } |
| } catch (error) { |
| console.error('[DEEP DEBUG NAV] navigateToNext - Navigation error:', error); |
| setLoading(false); |
| } |
| } |
|
|
| async function navigateToInstance(instanceIndex) { |
| if (isLoading) { |
| return; |
| } |
|
|
| |
| const currentIdx = currentInstance ? currentInstance.index : 0; |
| if (instanceIndex > currentIdx) { |
| hasAttemptedForwardValidation = true; |
| if (!validateRequiredFields({ showErrors: true })) { |
| debugLog('[NAV] navigateToInstance - blocked by client-side validation'); |
| return; |
| } |
| } |
|
|
| try { |
| setLoading(true); |
|
|
| |
| clearTimeout(textSaveTimer); |
| textSaveTimer = null; |
|
|
| |
| debugLog('[DEEP DEBUG NAV] navigateToInstance - Saving annotations before navigation'); |
| const saveSucceeded = await saveAnnotations(); |
| if (saveSucceeded === false) { |
| showNotification('Failed to save annotations. Please try again.', 'error'); |
| setLoading(false); |
| return; |
| } |
|
|
| |
| debugTrackOverlays('BEFORE_GO_TO_NAVIGATION', currentInstance?.id); |
|
|
| |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| debugLog('🔍 [DEBUG] navigateToInstance() - Is Firefox:', isFirefox); |
|
|
| if (isFirefox) { |
| debugLog('🔍 [DEBUG] Firefox detected - forcing overlay cleanup before navigation'); |
| const spanOverlays = document.getElementById('span-overlays'); |
| if (spanOverlays) { |
| debugLog('🔍 [DEBUG] navigateToInstance() - Before Firefox cleanup:', spanOverlays.children.length, 'overlays'); |
|
|
| |
| while (spanOverlays.firstChild) { |
| const child = spanOverlays.firstChild; |
| debugLog('🔍 [DEBUG] navigateToInstance() - Removing overlay child:', child.className, child.id); |
|
|
| |
| if (typeof trackOverlayRemoval === 'function') { |
| trackOverlayRemoval(child, 'navigateToInstance Firefox cleanup'); |
| } |
|
|
| spanOverlays.removeChild(child); |
| } |
|
|
| |
| spanOverlays.offsetHeight; |
| debugLog('🔍 [DEBUG] navigateToInstance() - After Firefox cleanup:', spanOverlays.children.length, 'overlays'); |
| } else { |
| debugLog('🔍 [DEBUG] navigateToInstance() - No span-overlays container found'); |
| } |
| } |
|
|
| const headers = { |
| 'Content-Type': 'application/json', |
| }; |
| if (window.config.api_key) { |
| headers['X-API-Key'] = window.config.api_key; |
| } |
| const response = await fetch('/annotate', { |
| method: 'POST', |
| headers: headers, |
| body: JSON.stringify({ |
| action: 'go_to', |
| go_to: instanceIndex |
| }) |
| }); |
|
|
| if (response.ok) { |
| debugLog('🔍 [DEBUG] navigateToInstance() - Navigation successful, about to reload page'); |
| |
| const spanOverlays = document.getElementById('span-overlays'); |
| if (spanOverlays) { |
| debugLog('🔍 [DEBUG] navigateToInstance() - Before clearing overlays:', spanOverlays.children.length, 'overlays'); |
| debugLog('🔍 [DEBUG] navigateToInstance() - Clearing span overlays before page reload'); |
| spanOverlays.innerHTML = ''; |
| debugLog('🔍 [DEBUG] navigateToInstance() - After clearing overlays:', spanOverlays.children.length, 'overlays'); |
| debugVerifyOverlayCleanup(); |
| } else { |
| debugLog('🔍 [DEBUG] navigateToInstance() - No span-overlays container found'); |
| } |
| |
| window.location.reload(); |
| } else { |
| await handleNavigationResponseError(response); |
| } |
| } catch (error) { |
| console.error('Error navigating to instance:', error); |
| showError(true, error.message); |
| } finally { |
| setLoading(false); |
| } |
| } |
|
|
| function validateRequiredFields(options) { |
| |
| |
| const showErrors = (options && options.showErrors) || hasAttemptedForwardValidation; |
|
|
| |
| const requiredInputs = document.querySelectorAll( |
| 'input[validation="required"], input[validation="required_label"], ' + |
| 'select[validation="required"], textarea[validation="required"]' |
| ); |
| let allRequiredFilled = true; |
| const unfilledSchemas = []; |
|
|
| |
| const formGroups = {}; |
| requiredInputs.forEach(input => { |
| const form = input.closest('.annotation-form'); |
| const schemaName = form ? (form.getAttribute('data-schema-name') || form.id) : null; |
| if (!schemaName) return; |
| if (!formGroups[schemaName]) { |
| formGroups[schemaName] = { form: form, radios: {}, others: [] }; |
| } |
| if (input.type === 'radio') { |
| const name = input.name; |
| if (!formGroups[schemaName].radios[name]) { |
| formGroups[schemaName].radios[name] = []; |
| } |
| formGroups[schemaName].radios[name].push(input); |
| } else { |
| formGroups[schemaName].others.push(input); |
| } |
| }); |
|
|
| |
| for (const [schemaName, group] of Object.entries(formGroups)) { |
| let schemaFilled = true; |
|
|
| |
| for (const [name, inputs] of Object.entries(group.radios)) { |
| if (!inputs.some(input => input.checked)) { |
| schemaFilled = false; |
| break; |
| } |
| } |
|
|
| |
| for (const input of group.others) { |
| if (input.type === 'range') { |
| |
| if (input.getAttribute('data-modified') !== 'true') { |
| schemaFilled = false; |
| break; |
| } |
| } else if (!input.value || input.value.trim() === '') { |
| schemaFilled = false; |
| break; |
| } |
| } |
|
|
| if (!schemaFilled) { |
| allRequiredFilled = false; |
| const legend = group.form.querySelector('legend'); |
| const label = legend ? legend.textContent.trim() : schemaName; |
| unfilledSchemas.push({ name: schemaName, label: label }); |
| } |
|
|
| |
| if (showErrors && group.form) { |
| group.form.classList.toggle('required-unfilled', !schemaFilled); |
| } |
| } |
|
|
| |
| if (showErrors) { |
| updateRequiredFieldsError(unfilledSchemas); |
| } |
|
|
| return allRequiredFilled; |
| } |
|
|
| function updateRequiredFieldsError(unfilledSchemas) { |
| let errorDiv = document.getElementById('required-fields-error'); |
|
|
| if (unfilledSchemas.length === 0) { |
| if (errorDiv) { |
| errorDiv.style.display = 'none'; |
| } |
| return; |
| } |
|
|
| |
| if (!errorDiv) { |
| errorDiv = document.createElement('div'); |
| errorDiv.id = 'required-fields-error'; |
| errorDiv.className = 'required-fields-error'; |
| const navDiv = document.querySelector('.potato-nav'); |
| if (navDiv) { |
| navDiv.parentNode.insertBefore(errorDiv, navDiv); |
| } |
| } |
|
|
| const labels = unfilledSchemas.map(s => `<strong>${s.label}</strong>`).join(', '); |
| const plural = unfilledSchemas.length > 1; |
| errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> Please answer the required question${plural ? 's' : ''}: ${labels}`; |
| errorDiv.style.display = 'block'; |
| } |
|
|
| function setLoading(loading) { |
| isLoading = loading; |
| const loadingState = document.getElementById('loading-state'); |
| const mainContent = document.getElementById('main-content'); |
| const prevBtn = document.getElementById('prev-btn'); |
| const nextBtn = document.getElementById('next-btn'); |
|
|
| if (loading) { |
| loadingState.style.display = 'block'; |
| mainContent.style.display = 'none'; |
| if (prevBtn) prevBtn.disabled = true; |
| nextBtn.disabled = true; |
| } else { |
| loadingState.style.display = 'none'; |
| mainContent.style.display = 'block'; |
| if (prevBtn) prevBtn.disabled = false; |
| |
| |
| nextBtn.disabled = false; |
| validateRequiredFields(); |
| } |
| } |
|
|
| function showError(show, message = '', options = {}) { |
| const errorState = document.getElementById('error-state'); |
| const errorMessage = document.getElementById('error-message-text'); |
| const mainContent = document.getElementById('main-content'); |
| const retryBtn = document.getElementById('error-retry-btn'); |
| const doneLink = document.getElementById('error-done-link'); |
|
|
| if (show) { |
| errorState.style.display = 'block'; |
| mainContent.style.display = 'none'; |
| errorMessage.textContent = message; |
| |
| if (options.permanent) { |
| if (retryBtn) retryBtn.style.display = 'none'; |
| if (doneLink) doneLink.style.display = 'inline-flex'; |
| } else { |
| if (retryBtn) retryBtn.style.display = ''; |
| if (doneLink) doneLink.style.display = 'none'; |
| } |
| } else { |
| errorState.style.display = 'none'; |
| mainContent.style.display = 'block'; |
| } |
| } |
|
|
| |
| function updateAnnotation(schema, label, value) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][label] = value; |
| } |
|
|
| |
| |
| |
| |
| function syncAnnotationsFromDOM() { |
| |
| const checkboxes = document.querySelectorAll('input[type="checkbox"].annotation-input'); |
| checkboxes.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName) { |
| if (input.checked) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } else { |
| |
| if (currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| delete currentAnnotations[schema][labelName]; |
| if (Object.keys(currentAnnotations[schema]).length === 0) { |
| delete currentAnnotations[schema]; |
| } |
| } |
| } |
| } |
| }); |
|
|
| |
| const radios = document.querySelectorAll('input[type="radio"].annotation-input'); |
| const radioSchemas = new Set(); |
| radios.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| if (schema) radioSchemas.add(schema); |
| }); |
| radioSchemas.forEach(schema => { delete currentAnnotations[schema]; }); |
| radios.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && input.checked) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| const textInputs = document.querySelectorAll('input[type="text"].annotation-input, textarea.annotation-input'); |
| textInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && input.value) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| const sliders = document.querySelectorAll('input[type="range"].annotation-input'); |
| sliders.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| const selects = document.querySelectorAll('select.annotation-input'); |
| selects.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && input.value) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| const numberInputs = document.querySelectorAll('input[type="number"].annotation-input'); |
| numberInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && input.value) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| |
| |
| |
| |
| const hiddenInputs = document.querySelectorAll('input[type="hidden"].annotation-input'); |
| hiddenInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| const isModified = input.hasAttribute('data-modified') || input.hasAttribute('data-server-set'); |
| if (schema && labelName && input.value && isModified) { |
| if (!currentAnnotations[schema]) { |
| currentAnnotations[schema] = {}; |
| } |
| currentAnnotations[schema][labelName] = input.value; |
| } |
| }); |
|
|
| debugLog('[DEBUG] syncAnnotationsFromDOM: synced annotations:', currentAnnotations); |
| } |
|
|
| |
| function whetherNone(checkbox) { |
| |
| |
| var x = document.getElementsByClassName(checkbox.className); |
| var i; |
| for (i = 0; i < x.length; i++) { |
| if (checkbox.value == "None" && x[i].value != "None") x[i].checked = false; |
| if (checkbox.value != "None" && x[i].value == "None") x[i].checked = false; |
| } |
| |
| handleInputChange(checkbox); |
| } |
|
|
| |
| function setupInputEventListeners() { |
| |
| const inputs = document.querySelectorAll('.annotation-input'); |
|
|
| inputs.forEach(input => { |
| const inputType = input.type; |
| const tagName = input.tagName.toLowerCase(); |
|
|
| if (inputType === 'text' || tagName === 'textarea') { |
| |
| let timer; |
| input.addEventListener('input', function (event) { |
| clearTimeout(timer); |
| timer = setTimeout(() => { |
| handleInputChange(event.target); |
| }, 1000); |
| }); |
| debugLog(`Set up event listener for ${tagName} element:`, input.id); |
| } else if (inputType === 'radio' || inputType === 'checkbox') { |
| |
| input.addEventListener('change', function (event) { |
| handleInputChange(event.target); |
| }); |
| } else if (inputType === 'range') { |
| |
| input.addEventListener('input', function (event) { |
| const valueDisplay = document.getElementById(`${input.name}-value`); |
| if (valueDisplay) { |
| valueDisplay.textContent = event.target.value; |
| } |
| handleInputChange(event.target); |
| }); |
| } else if (tagName === 'select') { |
| |
| input.addEventListener('change', function (event) { |
| handleInputChange(event.target); |
| }); |
| } else if (inputType === 'number') { |
| |
| let timer; |
| input.addEventListener('input', function (event) { |
| clearTimeout(timer); |
| timer = setTimeout(() => { |
| handleInputChange(event.target); |
| }, 1000); |
| }); |
| } else if (inputType === 'hidden') { |
| |
| input.addEventListener('change', function (event) { |
| handleInputChange(event.target); |
| }); |
| debugLog(`Set up event listener for hidden input:`, input.id); |
| } |
| }); |
| } |
|
|
| function handleInputChange(element) { |
| const schema = element.getAttribute('schema'); |
| const labelName = element.getAttribute('label_name'); |
| const inputType = element.type; |
| const tagName = element.tagName.toLowerCase(); |
|
|
| debugLog(`handleInputChange called for ${tagName} element:`, element.id, 'schema:', schema, 'label:', labelName); |
|
|
| if (!schema || !labelName) { |
| console.warn('Missing schema or label_name for input:', element); |
| return; |
| } |
|
|
| |
| validateRequiredFields(); |
|
|
| let value; |
|
|
| if (inputType === 'radio') { |
| |
| if (element.checked) { |
| const oldValue = currentAnnotations[schema] ? currentAnnotations[schema][labelName] : null; |
| |
| currentAnnotations[schema] = {}; |
| value = element.value; |
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackAnnotationChange(schema, labelName, 'select', oldValue, value, 'user'); |
| } |
| } else { |
| return; |
| } |
| } else if (inputType === 'checkbox') { |
| |
| if (element.checked) { |
| value = element.value; |
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackAnnotationChange(schema, labelName, 'select', null, value, 'user'); |
| } |
| } else { |
| |
| const oldValue = currentAnnotations[schema] ? currentAnnotations[schema][labelName] : null; |
| if (currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| delete currentAnnotations[schema][labelName]; |
| |
| if (Object.keys(currentAnnotations[schema]).length === 0) { |
| delete currentAnnotations[schema]; |
| } |
| } |
| debugLog(`Removed annotation: ${schema}.${labelName}`); |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackAnnotationChange(schema, labelName, 'deselect', oldValue, null, 'user'); |
| } |
|
|
| |
| clearTimeout(textSaveTimer); |
| textSaveTimer = setTimeout(() => { |
| saveAnnotations(); |
| }, 500); |
| return; |
| } |
| } else { |
| |
| const oldValue = currentAnnotations[schema] ? currentAnnotations[schema][labelName] : null; |
| value = element.value; |
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackAnnotationChange(schema, labelName, 'update', oldValue, value, 'user'); |
| } |
| } |
|
|
| |
| updateAnnotation(schema, labelName, value); |
| debugLog(`Updated annotation: ${schema}.${labelName} = ${value}`); |
|
|
| |
| if (displayLogicManager) { |
| displayLogicManager.evaluateForSchema(schema); |
| } |
|
|
| |
| clearTimeout(textSaveTimer); |
| textSaveTimer = setTimeout(() => { |
| saveAnnotations(); |
| }, 500); |
| } |
|
|
| function populateInputValues() { |
| if (!currentAnnotations) return; |
|
|
| debugLog('🔍 Populating input values with annotations:', currentAnnotations); |
|
|
| |
| const textInputs = document.querySelectorAll('input[type="text"], textarea.annotation-input'); |
| debugLog('🔍 Found text inputs and textareas:', textInputs.length); |
|
|
| textInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| debugLog('🔍 Checking input:', input.id, 'schema:', schema, 'label:', labelName); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| input.value = currentAnnotations[schema][labelName]; |
| debugLog(`✅ Populated ${input.tagName} ${input.id} with value:`, currentAnnotations[schema][labelName]); |
| } else { |
| debugLog(`❌ Could not populate ${input.tagName} ${input.id}:`, { |
| hasSchema: !!schema, |
| hasLabelName: !!labelName, |
| hasSchemaInAnnotations: !!(currentAnnotations[schema]), |
| hasLabelInSchema: !!(currentAnnotations[schema] && currentAnnotations[schema][labelName]) |
| }); |
| } |
| }); |
|
|
| |
| const radioInputs = document.querySelectorAll('input[type="radio"]'); |
| radioInputs.forEach(input => { |
| |
| |
| |
| |
| if (input.getAttribute('data-server-set') === 'true') return; |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| input.checked = (currentAnnotations[schema][labelName] === input.value); |
| debugLog(`Populated radio ${input.id}: ${input.checked ? 'checked' : 'unchecked'}`); |
| } |
| }); |
|
|
| |
| const checkboxInputs = document.querySelectorAll('input[type="checkbox"]'); |
| checkboxInputs.forEach(input => { |
| |
| |
| |
| |
| |
| if (input.getAttribute('data-server-set') === 'true') return; |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema]) { |
| |
| const hasAnnotation = currentAnnotations[schema][labelName] === input.value; |
| input.checked = hasAnnotation; |
| debugLog(`Populated checkbox ${input.id}: ${hasAnnotation ? 'checked' : 'unchecked'}`); |
| } |
| }); |
|
|
| |
| const sliderInputs = document.querySelectorAll('input[type="range"]'); |
| sliderInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| input.value = currentAnnotations[schema][labelName]; |
| const valueDisplay = document.getElementById(`${input.name}-value`); |
| if (valueDisplay) { |
| valueDisplay.textContent = currentAnnotations[schema][labelName]; |
| } |
| |
| input.dispatchEvent(new Event('input', { bubbles: true })); |
| debugLog(`Populated slider ${input.id} with value:`, currentAnnotations[schema][labelName]); |
| } |
| }); |
|
|
| |
| const selectInputs = document.querySelectorAll('select.annotation-input'); |
| selectInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| input.value = currentAnnotations[schema][labelName]; |
| debugLog(`Populated select ${input.id} with value:`, currentAnnotations[schema][labelName]); |
| } |
| }); |
|
|
| |
| const numberInputs = document.querySelectorAll('input[type="number"].annotation-input'); |
| numberInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| input.value = currentAnnotations[schema][labelName]; |
| |
| input.dispatchEvent(new Event('input', { bubbles: true })); |
| debugLog(`Populated number ${input.id} with value:`, currentAnnotations[schema][labelName]); |
| } |
| }); |
|
|
| |
| restorePairwiseAnnotations(); |
|
|
| |
| restoreBwsAnnotations(); |
|
|
| |
| restoreRankingAnnotations(); |
|
|
| |
| restoreHierarchicalAnnotations(); |
|
|
| |
| restoreSoftLabelDisplays(); |
|
|
| |
| restoreRangeSliderDisplays(); |
|
|
| |
| restoreSemanticDifferentialAnnotations(); |
|
|
| |
| restoreTextEditAnnotations(); |
|
|
| |
| restoreExtractiveQaAnnotations(); |
|
|
| |
| restoreErrorSpanAnnotations(); |
|
|
| |
| restoreCardSortAnnotations(); |
|
|
| |
| restoreTrajectoryEvalAnnotations(); |
|
|
| |
| restoreTrajectoryEditAnnotations(); |
|
|
| |
| updateAllCharCounters(); |
|
|
| validateRequiredFields(); |
| } |
|
|
| |
| |
| |
| function restoreRankingAnnotations() { |
| const hiddenInputs = document.querySelectorAll('.ranking-order-input'); |
| hiddenInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| const savedOrder = currentAnnotations[schema][labelName]; |
| input.value = savedOrder; |
| input.setAttribute('data-modified', 'true'); |
| input.setAttribute('data-server-set', 'true'); |
| |
| const list = input.closest('fieldset').querySelector('.ranking-list'); |
| if (list) { |
| const order = savedOrder.split(','); |
| const items = Array.from(list.querySelectorAll('.ranking-item')); |
| order.forEach((val, idx) => { |
| const item = items.find(it => it.getAttribute('data-value') === val); |
| if (item) { |
| list.appendChild(item); |
| item.querySelector('.ranking-rank').textContent = idx + 1; |
| } |
| }); |
| } |
| debugLog('Restored ranking annotation for', schema); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreHierarchicalAnnotations() { |
| const hiddenInputs = document.querySelectorAll('.hier-selected-input'); |
| hiddenInputs.forEach(input => { |
| const schema = input.getAttribute('schema'); |
| const labelName = input.getAttribute('label_name'); |
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| const savedLabels = currentAnnotations[schema][labelName]; |
| input.value = savedLabels; |
| input.setAttribute('data-modified', 'true'); |
| input.setAttribute('data-server-set', 'true'); |
| |
| const selected = savedLabels.split(',').map(s => s.trim()).filter(Boolean); |
| const tree = input.closest('fieldset').querySelector('.hier-tree'); |
| if (tree) { |
| tree.querySelectorAll('.hier-checkbox').forEach(cb => { |
| cb.checked = selected.includes(cb.value); |
| }); |
| |
| const tagsContainer = tree.parentElement.querySelector('.hier-selected-tags'); |
| if (tagsContainer) { |
| tagsContainer.innerHTML = selected.map(s => |
| '<span class="hier-tag">' + s + '</span>' |
| ).join(''); |
| } |
| } |
| debugLog('Restored hierarchical annotation for', schema); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreSoftLabelDisplays() { |
| document.querySelectorAll('.shadcn-soft-label-container').forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| const total = parseInt(form.getAttribute('data-soft-label-total')) || 100; |
| const sliders = form.querySelectorAll('.soft-label-slider'); |
| sliders.forEach((s, idx) => { |
| const valEl = document.getElementById('soft-label-val-' + s.id); |
| if (valEl) valEl.textContent = s.value; |
| const bar = document.getElementById('soft-label-bar-' + schema + '-' + idx); |
| if (bar) bar.style.width = (parseInt(s.value) / total * 100) + '%'; |
| }); |
| |
| const sum = Array.from(sliders).reduce((acc, s) => acc + parseInt(s.value), 0); |
| const allocEl = document.getElementById('soft-label-allocated-' + schema); |
| if (allocEl) { |
| allocEl.innerHTML = 'Allocated: <strong>' + sum + '</strong> / ' + total; |
| } |
| const remEl = document.getElementById('soft-label-remaining-' + schema); |
| if (remEl) { |
| remEl.innerHTML = 'Remaining: <strong>' + (total - sum) + '</strong>'; |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreRangeSliderDisplays() { |
| document.querySelectorAll('.shadcn-range-slider-container').forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema) return; |
|
|
| |
| const lowInput = form.querySelector('[data-range-slider-role="low"]'); |
| const highInput = form.querySelector('[data-range-slider-role="high"]'); |
| if (!lowInput || !highInput) return; |
|
|
| let lowVal, highVal; |
| if (currentAnnotations[schema]) { |
| lowVal = currentAnnotations[schema]['range_low']; |
| highVal = currentAnnotations[schema]['range_high']; |
| } |
|
|
| |
| if (lowVal != null && highVal != null) { |
| const renderFn = window['rangeSliderRender_' + schema]; |
| if (renderFn) { |
| renderFn(parseInt(lowVal), parseInt(highVal)); |
| } |
| } |
|
|
| |
| lowInput.setAttribute('data-modified', 'true'); |
| highInput.setAttribute('data-modified', 'true'); |
| }); |
| } |
|
|
| |
| |
| |
| function restoreSemanticDifferentialAnnotations() { |
| const forms = document.querySelectorAll('.shadcn-semantic-differential-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
| const radios = form.querySelectorAll('.semantic-differential-radio'); |
| radios.forEach(radio => { |
| const labelName = radio.getAttribute('label_name'); |
| if (labelName && currentAnnotations[schema][labelName]) { |
| if (radio.value === currentAnnotations[schema][labelName]) { |
| radio.checked = true; |
| } |
| } |
| }); |
| }); |
| } |
|
|
| |
| |
| |
| function restoreTextEditAnnotations() { |
| const forms = document.querySelectorAll('.shadcn-text-edit-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.text-edit-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const data = JSON.parse(currentAnnotations[schema][labelName]); |
| if (data && data.edited_text !== undefined) { |
| const editor = form.querySelector('.text-edit-textarea'); |
| if (editor) { |
| editor.value = data.edited_text; |
| |
| if (typeof window.textEditOnInput === 'function') { |
| window.textEditOnInput(schema); |
| } |
| } |
| } |
| hiddenInput.value = currentAnnotations[schema][labelName]; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
| } catch (e) { |
| debugLog('Error restoring text edit annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreExtractiveQaAnnotations() { |
| const forms = document.querySelectorAll('.shadcn-extractive-qa-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.eqa-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const data = JSON.parse(currentAnnotations[schema][labelName]); |
| hiddenInput.value = currentAnnotations[schema][labelName]; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
|
|
| if (data.unanswerable) { |
| document.getElementById(schema + '-answer-text').textContent = 'Unanswerable'; |
| var unansBtn = document.getElementById(schema + '-unanswerable'); |
| if (unansBtn) unansBtn.classList.add('eqa-unanswerable-active'); |
| } else if (data.answer_text) { |
| document.getElementById(schema + '-answer-text').textContent = data.answer_text; |
| |
| var container = document.getElementById(schema + '-passage'); |
| if (container && data.start >= 0 && data.end > data.start) { |
| var text = container.textContent; |
| var color = container.dataset.highlightColor || '#FFEB3B'; |
| container.innerHTML = text.substring(0, data.start) + |
| '<span class="eqa-highlight" style="background-color:' + color + '">' + |
| text.substring(data.start, data.end) + '</span>' + |
| text.substring(data.end); |
| } |
| } |
| } catch (e) { |
| debugLog('Error restoring extractive QA annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreErrorSpanAnnotations() { |
| const forms = document.querySelectorAll('.shadcn-error-span-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.error-span-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const data = JSON.parse(currentAnnotations[schema][labelName]); |
| hiddenInput.value = currentAnnotations[schema][labelName]; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
|
|
| if (data.errors && typeof window._errorSpanGetState === 'function') { |
| var state = window._errorSpanGetState(schema); |
| state.errors = data.errors; |
| window._errorSpanUpdateDisplay(schema); |
| } |
| } catch (e) { |
| debugLog('Error restoring error span annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function restoreCardSortAnnotations() { |
| const forms = document.querySelectorAll('.shadcn-card-sort-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.card-sort-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const data = JSON.parse(currentAnnotations[schema][labelName]); |
| hiddenInput.value = currentAnnotations[schema][labelName]; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
|
|
| |
| if (data && typeof data === 'object') { |
| Object.keys(data).forEach(function(groupName) { |
| var items = data[groupName]; |
| var groups = form.querySelectorAll('.card-sort-group'); |
| groups.forEach(function(g) { |
| if (g.dataset.group === groupName) { |
| var container = g.querySelector('.card-sort-group-items'); |
| items.forEach(function(text) { |
| |
| var source = form.querySelector('.card-sort-source-items'); |
| var cards = source ? source.querySelectorAll('.card-sort-card') : []; |
| cards.forEach(function(c) { |
| if (c.textContent.trim() === text) { |
| container.appendChild(c); |
| } |
| }); |
| }); |
| } |
| }); |
| }); |
| if (typeof window._cardSortUpdateCounts === 'function') { |
| window._cardSortUpdateCounts(schema); |
| } |
| } |
| } catch (e) { |
| debugLog('Error restoring card sort annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| function restoreTrajectoryEvalAnnotations() { |
| const forms = document.querySelectorAll('.trajectory-eval-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.trajectory-eval-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const data = JSON.parse(currentAnnotations[schema][labelName]); |
| hiddenInput.value = currentAnnotations[schema][labelName]; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
|
|
| if (data.steps && typeof window._trajGetState === 'function') { |
| var state = window._trajGetState(); |
| state.steps = data.steps; |
| if (typeof window._trajBuildStepCards === 'function') { |
| window._trajBuildStepCards(); |
| } |
| if (typeof window._trajRestoreVisualState === 'function') { |
| window._trajRestoreVisualState(); |
| } |
| } |
| } catch (e) { |
| debugLog('Error restoring trajectory eval annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| function restoreTrajectoryEditAnnotations() { |
| const forms = document.querySelectorAll('.trajectory-edit-container'); |
| forms.forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| const hiddenInput = form.querySelector('.trajectory-edit-data-input'); |
| if (!hiddenInput) return; |
|
|
| const labelName = hiddenInput.getAttribute('label_name'); |
| if (!labelName || !currentAnnotations[schema][labelName]) return; |
|
|
| try { |
| const raw = currentAnnotations[schema][labelName]; |
| const data = JSON.parse(raw); |
| hiddenInput.value = raw; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
|
|
| if (window._trajEditState) { |
| const st = window._trajEditState[schema] || { entries: {}, final_answer: null }; |
| st.entries = {}; |
| (data.steps || []).forEach(e => { |
| st.entries[e.step_index + '::' + e.field] = e; |
| }); |
| st.final_answer = data.final_answer || null; |
| window._trajEditState[schema] = st; |
| } |
| if (typeof window._trajEditBuild === 'function') window._trajEditBuild(); |
| if (typeof window._trajEditRestore === 'function') window._trajEditRestore(); |
| } catch (e) { |
| debugLog('Error restoring trajectory edit annotation:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| function updateAllCharCounters() { |
| const counters = document.querySelectorAll('.shadcn-textbox-char-counter'); |
| counters.forEach(counter => { |
| const inputId = counter.dataset.inputId; |
| const minChars = parseInt(counter.dataset.minChars || '0', 10); |
| const input = document.getElementById(inputId); |
| if (!input) return; |
|
|
| const len = (input.value || input.textContent || '').length; |
| const countSpan = counter.querySelector('.shadcn-textbox-char-count'); |
| if (countSpan) countSpan.textContent = len; |
|
|
| if (minChars > 0) { |
| counter.classList.toggle('char-count-met', len >= minChars); |
| counter.classList.toggle('char-count-unmet', len < minChars); |
| } |
|
|
| |
| if (!input.dataset.charCounterBound) { |
| input.dataset.charCounterBound = 'true'; |
| input.addEventListener('input', function() { |
| const l = (input.value || input.textContent || '').length; |
| if (countSpan) countSpan.textContent = l; |
| if (minChars > 0) { |
| counter.classList.toggle('char-count-met', l >= minChars); |
| counter.classList.toggle('char-count-unmet', l < minChars); |
| } |
| }); |
| } |
| }); |
| } |
|
|
| |
| function onlyOne(checkbox) { |
| debugLog('🔍 [DEBUG] onlyOne() called with checkbox:', { |
| id: checkbox.id, |
| name: checkbox.name, |
| value: checkbox.value, |
| checked: checkbox.checked, |
| className: checkbox.className |
| }); |
|
|
| var x = document.getElementsByClassName(checkbox.className); |
| debugLog('🔍 [DEBUG] onlyOne() - Found elements with same class:', x.length); |
|
|
| var i; |
| for (i = 0; i < x.length; i++) { |
| debugLog('🔍 [DEBUG] onlyOne() - Processing element:', { |
| id: x[i].id, |
| value: x[i].value, |
| checked: x[i].checked, |
| willUncheck: x[i].value != checkbox.value |
| }); |
|
|
| if (x[i].value != checkbox.value) { |
| debugLog('🔍 [DEBUG] onlyOne() - Unchecking element:', x[i].id); |
| x[i].checked = false; |
| } |
| } |
| |
| debugLog('🔍 [DEBUG] onlyOne() - Setting clicked checkbox to checked:', checkbox.id); |
| checkbox.setAttribute('data-just-checked', 'true'); |
| checkbox.checked = true; |
|
|
| |
| setTimeout(() => { |
| if (checkbox.hasAttribute('data-just-checked')) { |
| debugLog('🔍 [DEBUG] onlyOne() - Removing data-just-checked flag after timeout'); |
| checkbox.removeAttribute('data-just-checked'); |
| } |
| }, 100); |
| } |
|
|
| function extractSpanAnnotationsFromDOM() { |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] extractSpanAnnotationsFromDOM called'); |
|
|
| const overlays = document.querySelectorAll('.span-overlay'); |
| const spanAnnotations = []; |
|
|
| for (const overlay of overlays) { |
| const schema = overlay.getAttribute('data-schema'); |
| const label = overlay.getAttribute('data-label'); |
| const start = parseInt(overlay.getAttribute('data-start')); |
| const end = parseInt(overlay.getAttribute('data-end')); |
| const title = overlay.querySelector('.span-label')?.textContent?.trim() || label; |
|
|
| |
| const segments = document.querySelectorAll('.text-segment'); |
| let coveredText = ''; |
| for (const segment of segments) { |
| const segStart = parseInt(segment.getAttribute('data-start')); |
| const segEnd = parseInt(segment.getAttribute('data-end')); |
| const spanIds = segment.getAttribute('data-span-ids')?.split(',') || []; |
|
|
| |
| if (overlay.getAttribute('data-annotation-id') && |
| spanIds.includes(overlay.getAttribute('data-annotation-id'))) { |
| coveredText += segment.textContent; |
| } |
| } |
|
|
| const targetField = overlay.getAttribute('data-target-field') || ''; |
|
|
| |
| const spanId = overlay.getAttribute('data-annotation-id') || |
| overlay.getAttribute('data-span-id'); |
|
|
| spanAnnotations.push({ |
| schema: schema, |
| name: label, |
| start: start, |
| end: end, |
| title: title, |
| value: coveredText, |
| target_field: targetField, |
| id: spanId |
| }); |
| } |
|
|
| debugLog('[DEBUG] extractSpanAnnotationsFromDOM: found', spanAnnotations.length, 'spans:', spanAnnotations); |
| return spanAnnotations; |
| } |
|
|
| function alignSpanOverlays() { |
| |
| |
| |
| |
| debugLog('[DEBUG] alignSpanOverlays called'); |
|
|
| const overlays = document.querySelectorAll('.span-overlay'); |
| const segments = Array.from(document.querySelectorAll('.text-segment')); |
| const container = document.querySelector('.span-annotation-container'); |
|
|
| if (!container) { |
| console.warn('[DEBUG] alignSpanOverlays: No .span-annotation-container found'); |
| return; |
| } |
|
|
| for (const overlay of overlays) { |
| const annotationId = overlay.getAttribute('data-annotation-id'); |
| if (!annotationId) { |
| console.warn('[DEBUG] alignSpanOverlays: Overlay missing data-annotation-id'); |
| continue; |
| } |
|
|
| |
| const coveredSegments = segments.filter(segment => { |
| const spanIds = segment.getAttribute('data-span-ids')?.split(',') || []; |
| return spanIds.includes(annotationId); |
| }); |
|
|
| if (coveredSegments.length === 0) { |
| console.warn('[DEBUG] alignSpanOverlays: No segments found for overlay', annotationId); |
| continue; |
| } |
|
|
| |
| let minLeft = Infinity; |
| let maxRight = -Infinity; |
| let minTop = Infinity; |
| let maxBottom = -Infinity; |
|
|
| for (const segment of coveredSegments) { |
| const rect = segment.getBoundingClientRect(); |
| const containerRect = container.getBoundingClientRect(); |
|
|
| const relativeLeft = rect.left - containerRect.left; |
| const relativeRight = rect.right - containerRect.left; |
| const relativeTop = rect.top - containerRect.top; |
| const relativeBottom = rect.bottom - containerRect.top; |
|
|
| minLeft = Math.min(minLeft, relativeLeft); |
| maxRight = Math.max(maxRight, relativeRight); |
| minTop = Math.min(minTop, relativeTop); |
| maxBottom = Math.max(maxBottom, relativeBottom); |
| } |
|
|
| |
| overlay.style.left = minLeft + 'px'; |
| overlay.style.top = minTop + 'px'; |
| overlay.style.width = (maxRight - minLeft) + 'px'; |
| overlay.style.height = (maxBottom - minTop) + 'px'; |
| overlay.style.backgroundColor = 'rgba(255, 230, 230, 0.3)'; |
| overlay.style.border = '1px solid rgba(255, 230, 230, 0.8)'; |
|
|
| debugLog('[DEBUG] alignSpanOverlays: Positioned overlay', annotationId, 'at', |
| minLeft, minTop, maxRight - minLeft, maxBottom - minTop); |
| } |
| } |
|
|
| |
| function getSelectionIndicesOverlay() { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] getSelectionIndicesOverlay called'); |
|
|
| var selection = window.getSelection(); |
| if (!selection.rangeCount) { |
| debugLog('[DEBUG] getSelectionIndicesOverlay: No selection range'); |
| return { start: 0, end: 0 }; |
| } |
|
|
| var range = selection.getRangeAt(0); |
| var container = document.getElementById('text-content'); |
|
|
| if (!container) { |
| debugLog('[DEBUG] getSelectionIndicesOverlay: No text-content container found'); |
| return { start: 0, end: 0 }; |
| } |
|
|
| |
| if (typeof calculateTextOffsetsFromSelection === 'function') { |
| const offsets = calculateTextOffsetsFromSelection(container, range); |
| debugLog('[DEBUG] getSelectionIndicesOverlay: Using unified approach, offsets:', offsets); |
| return offsets; |
| } |
|
|
| |
| debugLog('[DEBUG] getSelectionIndicesOverlay: Using fallback approach'); |
| return getOriginalTextOffsetsOverlay(container, range); |
| } |
|
|
| |
| function changeSpanLabel(checkbox, schema, spanLabel, spanTitle, spanColor, targetField) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] changeSpanLabel called:', { schema, spanLabel, spanTitle, spanColor, targetField, checked: checkbox.checked }); |
|
|
| |
| if (window.spanManager && window.spanManager.isInitialized) { |
| debugLog('[DEBUG] changeSpanLabel: Using new span manager'); |
|
|
| |
| window.spanManager.selectLabel(spanLabel, schema, targetField); |
|
|
| |
| const textContainer = document.getElementById('instance-text'); |
| if (textContainer) { |
| |
| if (!boundEventHandlers.spanManagerMouseUp) { |
| boundEventHandlers.spanManagerMouseUp = window.spanManager.handleTextSelection.bind(window.spanManager); |
| boundEventHandlers.spanManagerKeyUp = window.spanManager.handleTextSelection.bind(window.spanManager); |
| } |
|
|
| |
| textContainer.removeEventListener('mouseup', boundEventHandlers.spanManagerMouseUp); |
| textContainer.removeEventListener('keyup', boundEventHandlers.spanManagerKeyUp); |
|
|
| |
| if (checkbox.checked) { |
| textContainer.addEventListener('mouseup', boundEventHandlers.spanManagerMouseUp); |
| textContainer.addEventListener('keyup', boundEventHandlers.spanManagerKeyUp); |
| debugLog('[DEBUG] changeSpanLabel: Text selection handlers added for span manager'); |
| } |
| } |
| } else { |
| |
| debugLog('[DEBUG] changeSpanLabel: Span manager not ready; deferring selection to manager'); |
| const waitAndSelect = () => { |
| if (window.spanManager && window.spanManager.isInitialized) { |
| window.spanManager.selectLabel(spanLabel, schema, targetField); |
| return true; |
| } |
| return false; |
| }; |
| if (!waitAndSelect()) { |
| let retries = 0; |
| const timer = setInterval(() => { |
| if (waitAndSelect() || ++retries > 20) clearInterval(timer); |
| }, 100); |
| } |
| } |
|
|
| |
| setTimeout(() => { |
| debugLog('[DEBUG] changeSpanLabel: Checkbox state after execution:', { |
| id: checkbox.id, |
| checked: checkbox.checked, |
| name: checkbox.name, |
| value: checkbox.value |
| }); |
| }, 0); |
| } |
|
|
| function surroundSelection(schema, labelName, title, selectionColor) { |
| |
| surroundSelectionOverlay(schema, labelName, title, selectionColor); |
| } |
|
|
| function restoreSpanAnnotationsFromHTML() { |
| |
| restoreSpanAnnotationsFromHTMLOverlay(); |
| } |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
| |
|
|
| |
| |
| |
|
|
| |
|
|
| |
| |
|
|
| function getOriginalTextOffsetsOverlay(container, range) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay called'); |
|
|
| |
| if (typeof calculateTextOffsetsFromSelection === 'function') { |
| const offsets = calculateTextOffsetsFromSelection(container, range); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: Using unified approach, offsets:', offsets); |
| return offsets; |
| } |
|
|
| |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: Using fallback approach'); |
|
|
| |
| var originalText = container.getAttribute('data-original-text'); |
| if (!originalText) { |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: WARNING - no data-original-text attribute found, falling back to DOM text'); |
| originalText = container.textContent || container.innerText; |
| } |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: originalText from data attribute:', originalText); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: originalText length:', originalText.length); |
|
|
| |
| var selectedText = window.getSelection().toString(); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: selectedText:', selectedText); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: selectedText length:', selectedText.length); |
|
|
| |
| var startIndex = originalText.indexOf(selectedText); |
| var endIndex = startIndex + selectedText.length; |
|
|
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: mapped indices:', { startIndex, endIndex }); |
|
|
| |
| if (startIndex !== -1) { |
| var extractedText = originalText.substring(startIndex, endIndex); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: extracted text using indices:', extractedText); |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: extracted text matches selected text:', extractedText === selectedText); |
| } else { |
| debugLog('[DEBUG] getOriginalTextOffsetsOverlay: WARNING - selected text not found in original text!'); |
| } |
|
|
| return { start: startIndex, end: endIndex }; |
| } |
|
|
| |
| function surroundSelectionOverlay(schema, labelName, title, selectionColor) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] surroundSelectionOverlay called:', { schema, labelName, title, selectionColor }); |
|
|
| |
| |
| if (window.getSelection().rangeCount == 0) { |
| debugLog('[DEBUG] surroundSelectionOverlay: No selection range found'); |
| return; |
| } |
| var range = window.getSelection().getRangeAt(0); |
|
|
| if (range.startOffset == range.endOffset) { |
| debugLog('[DEBUG] surroundSelectionOverlay: Selection start and end offsets are the same'); |
| return; |
| } |
|
|
| |
| var instance_id = document.getElementById("instance_id").value; |
| debugLog('[DEBUG] surroundSelectionOverlay: Instance ID:', instance_id); |
|
|
| if (window.getSelection) { |
| var sel = window.getSelection(); |
|
|
| |
| |
| if (!sel.anchorNode.parentElement) { |
| debugLog('[DEBUG] surroundSelectionOverlay: No anchor node parent element'); |
| return; |
| } |
|
|
| |
| |
| if (sel.rangeCount && sel.toString().trim().length > 0) { |
| debugLog('[DEBUG] surroundSelectionOverlay: Valid selection found, creating span'); |
|
|
| |
| var selText = window.getSelection().toString().trim(); |
| debugLog('[DEBUG] surroundSelectionOverlay: Selected text:', selText); |
|
|
| |
| var startEnd = getSelectionIndicesOverlay(); |
| debugLog('[DEBUG] surroundSelectionOverlay: Selection indices:', startEnd); |
|
|
| |
| var post_req = { |
| type: "span", |
| schema: schema, |
| state: [ |
| { |
| name: labelName, |
| start: startEnd["start"], |
| end: startEnd["end"], |
| title: title, |
| value: selText |
| } |
| ], |
| instance_id: instance_id |
| }; |
|
|
| debugLog('[DEBUG] surroundSelectionOverlay: Sending span annotation request:', post_req); |
|
|
| |
| fetch('/updateinstance', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(post_req) |
| }) |
| .then(response => { |
| if (response.ok) { |
| debugLog('[DEBUG] surroundSelectionOverlay: Span annotation created successfully'); |
| |
| location.reload(); |
| } else { |
| console.error('[DEBUG] surroundSelectionOverlay: Failed to create span annotation:', response.status); |
| return response.json().then(error => { |
| console.error('[DEBUG] surroundSelectionOverlay: Error details:', error); |
| }); |
| } |
| }) |
| .catch(error => { |
| console.error('[DEBUG] surroundSelectionOverlay: Network error:', error); |
| }); |
|
|
| |
| sel.empty(); |
| debugLog('[DEBUG] surroundSelectionOverlay: Span creation request sent, page will reload'); |
| } else { |
| debugLog('[DEBUG] surroundSelectionOverlay: No valid selection found'); |
| } |
| } |
| } |
|
|
| |
| function changeSpanLabelOverlay(checkbox, schema, spanLabel, spanTitle, spanColor) { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| debugLog('[DEBUG] changeSpanLabelOverlay called:', { schema, spanLabel, spanTitle, spanColor, checked: checkbox.checked }); |
|
|
| |
| document.onmouseup = function (e) { |
| var senderElement = e.target; |
| |
| if (senderElement.getAttribute("class") == "span-close") { |
| e.stopPropagation(); |
| return true; |
| } |
| if (checkbox.checked) { |
| debugLog('[DEBUG] changeSpanLabelOverlay: Mouse up event - checkbox is checked, calling surroundSelectionOverlay'); |
| surroundSelectionOverlay(schema, spanLabel, spanTitle, spanColor); |
| } else { |
| debugLog('[DEBUG] changeSpanLabelOverlay: Mouse up event - checkbox is not checked'); |
| } |
| }; |
| } |
|
|
| |
| function restoreSpanAnnotationsFromHTMLOverlay() { |
| |
| |
| |
| |
| |
| const container = document.querySelector('.span-annotation-container'); |
| if (!container) return; |
|
|
| const overlayElements = container.querySelectorAll('.span-overlay'); |
| const found = []; |
|
|
| overlayElements.forEach(overlay => { |
| const schema = overlay.getAttribute('data-schema'); |
| const name = overlay.getAttribute('data-label'); |
| const start = parseInt(overlay.getAttribute('data-start')); |
| const end = parseInt(overlay.getAttribute('data-end')); |
| const annotationId = overlay.getAttribute('data-annotation-id'); |
|
|
| found.push({ |
| schema, |
| name, |
| title: name, |
| start, |
| end, |
| id: annotationId, |
| value: '' |
| }); |
| }); |
|
|
| currentSpanAnnotations = found; |
| debugLog('[DEBUG] restoreSpanAnnotationsFromHTMLOverlay: found', found.length, 'spans:', found); |
| } |
|
|
| |
| |
| |
|
|
| |
| let spanColors = {}; |
| let originalText = ''; |
| let spanAnnotations = []; |
|
|
| |
| |
| |
| function initializeRobustSpanAnnotation() { |
| debugLog('[ROBUST SPAN] Initializing robust span annotation system'); |
|
|
| |
| loadSpanColors(); |
|
|
| |
| setupRobustSpanSelection(); |
|
|
| |
| renderSpansRobust(); |
| } |
|
|
| |
| |
| |
| async function loadSpanColors() { |
| try { |
| |
| if (userState && userState.config && userState.config.ui && userState.config.ui.spans) { |
| const configColors = userState.config.ui.spans.span_colors; |
| |
| spanColors = {}; |
| for (const schema in configColors) { |
| for (const label in configColors[schema]) { |
| spanColors[label] = configColors[schema][label]; |
| } |
| } |
| } else { |
| |
| spanColors = { |
| 'happy': '(255, 230, 230)', |
| 'sad': '(230, 243, 255)', |
| 'angry': '(255, 230, 204)', |
| 'surprised': '(230, 255, 230)', |
| 'neutral': '(240, 240, 240)' |
| }; |
| } |
| debugLog('[ROBUST SPAN] Loaded colors:', spanColors); |
| } catch (error) { |
| console.error('[ROBUST SPAN] Error loading colors:', error); |
| } |
| } |
|
|
| |
| |
| |
| function setupRobustSpanSelection() { |
| const textContainer = document.getElementById('instance-text'); |
| if (!textContainer) { |
| console.warn('[ROBUST SPAN] No text container found'); |
| return; |
| } |
|
|
| |
| textContainer.removeEventListener('mouseup', handleRobustTextSelection); |
| textContainer.removeEventListener('keyup', handleRobustTextSelection); |
|
|
| |
| textContainer.addEventListener('mouseup', handleRobustTextSelection); |
| textContainer.addEventListener('keyup', handleRobustTextSelection); |
|
|
| debugLog('[ROBUST SPAN] Text selection handlers set up'); |
| } |
|
|
| |
| |
| |
| function handleRobustTextSelection() { |
| const selection = window.getSelection(); |
| if (!selection.rangeCount || selection.isCollapsed) return; |
|
|
| |
| const activeSpanLabel = getActiveSpanLabel(); |
| if (!activeSpanLabel) { |
| debugLog('[ROBUST SPAN] No active span label selected'); |
| return; |
| } |
|
|
| const range = selection.getRangeAt(0); |
| const selectedText = selection.toString().trim(); |
| if (!selectedText) return; |
|
|
| |
| if (!activeSpanLabel.targetField) { |
| const container = range.startContainer.parentElement; |
| const spanTargetEl = container ? container.closest('[id^="text-content-"]') : null; |
| if (spanTargetEl) { |
| const fieldKey = spanTargetEl.id.replace('text-content-', ''); |
| if (fieldKey) activeSpanLabel.targetField = fieldKey; |
| } |
| } |
|
|
| |
| const start = getRobustTextPosition(selectedText, range); |
| const end = start + selectedText.length; |
|
|
| debugLog('[ROBUST SPAN] Creating span:', { |
| text: selectedText, |
| start: start, |
| end: end, |
| label: activeSpanLabel.label, |
| schema: activeSpanLabel.schema, |
| targetField: activeSpanLabel.targetField |
| }); |
|
|
| |
| createRobustSpanAnnotation(selectedText, start, end, activeSpanLabel); |
|
|
| |
| selection.removeAllRanges(); |
| } |
|
|
| |
| |
| |
| function getActiveSpanLabel() { |
| const spanCheckboxes = document.querySelectorAll('input[type="checkbox"][name*="span_label"]:checked'); |
| if (spanCheckboxes.length === 0) return null; |
|
|
| |
| const checkbox = spanCheckboxes[0]; |
| |
| const nameMatch = checkbox.name.match(/span_label:::(.+)/); |
| if (!nameMatch) return null; |
|
|
| const schema = nameMatch[1]; |
| |
| const idParts = checkbox.id.split('_'); |
| const label = idParts.length >= 2 ? idParts.slice(1).join('_') : checkbox.value; |
| const targetField = checkbox.getAttribute('data-target-field') || ''; |
|
|
| return { label, schema, targetField }; |
| } |
|
|
| |
| |
| |
| function getRobustTextPosition(selectedText, range) { |
| |
| if (!currentInstance || !currentInstance.text) { |
| console.warn('[ROBUST SPAN] No original text available'); |
| return 0; |
| } |
|
|
| const originalText = currentInstance.text; |
|
|
| |
| let indices = []; |
| let idx = originalText.indexOf(selectedText); |
| while (idx !== -1) { |
| indices.push(idx); |
| idx = originalText.indexOf(selectedText, idx + 1); |
| } |
|
|
| if (indices.length === 0) { |
| console.warn('[ROBUST SPAN] Selected text not found in original text'); |
| return 0; |
| } |
|
|
| if (indices.length === 1) { |
| return indices[0]; |
| } |
|
|
| |
| |
| debugLog('[ROBUST SPAN] Multiple occurrences found, using first:', indices[0]); |
| return indices[0]; |
| } |
|
|
| |
| |
| |
| async function createRobustSpanAnnotation(spanText, start, end, label) { |
| try { |
| |
| const labelName = typeof label === 'object' ? label.label : label; |
| const schema = typeof label === 'object' ? label.schema : 'emotion'; |
| const targetField = typeof label === 'object' ? (label.targetField || '') : ''; |
|
|
| debugLog('[ROBUST SPAN] Creating annotation:', { spanText, start, end, label: labelName, schema, targetField }); |
|
|
| |
| const postData = { |
| type: "span", |
| schema: schema, |
| state: [ |
| { |
| name: labelName, |
| start: start, |
| end: end, |
| title: labelName, |
| value: spanText, |
| target_field: targetField |
| } |
| ], |
| instance_id: currentInstance.id |
| }; |
|
|
| const response = await fetch('/updateinstance', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(postData) |
| }); |
|
|
| if (response.ok) { |
| debugLog('[ROBUST SPAN] Span annotation created successfully'); |
| |
| await loadCurrentInstance(); |
| } else { |
| console.error('[ROBUST SPAN] Failed to create span annotation:', await response.text()); |
| } |
| } catch (error) { |
| console.error('[ROBUST SPAN] Error creating span annotation:', error); |
| } |
| } |
|
|
| |
| |
| |
| function renderSpansRobust() { |
| const textContainer = document.getElementById('instance-text'); |
| if (!textContainer || !currentInstance) { |
| console.warn('[ROBUST SPAN] Cannot render spans - missing container or instance'); |
| return; |
| } |
|
|
| |
| originalText = currentInstance.text || ''; |
| if (!originalText) { |
| console.warn('[ROBUST SPAN] No original text available'); |
| return; |
| } |
|
|
| |
| spanAnnotations = []; |
| if (userState && userState.annotations && userState.annotations.by_instance) { |
| const instanceAnnotations = userState.annotations.by_instance[currentInstance.id]; |
| if (instanceAnnotations) { |
| |
| for (const [key, value] of Object.entries(instanceAnnotations)) { |
| |
| |
| if (typeof value === 'object' && value.start !== undefined && value.end !== undefined) { |
| spanAnnotations.push({ |
| id: key, |
| span: value.value || '', |
| label: value.name || key, |
| start: value.start, |
| end: value.end |
| }); |
| } |
| } |
| } |
| } |
|
|
| debugLog('[ROBUST SPAN] Rendering spans:', spanAnnotations); |
|
|
| if (spanAnnotations.length === 0) { |
| |
| textContainer.innerHTML = escapeHtml(originalText); |
| return; |
| } |
|
|
| |
| const html = renderTextWithSpans(originalText, spanAnnotations); |
| textContainer.innerHTML = html; |
| } |
|
|
| |
| |
| |
| function renderTextWithSpans(text, annotations) { |
| |
| const boundaries = []; |
| annotations.forEach(annotation => { |
| boundaries.push({ position: annotation.start, type: 'start', annotation }); |
| boundaries.push({ position: annotation.end, type: 'end', annotation }); |
| }); |
|
|
| |
| boundaries.sort((a, b) => a.position - b.position); |
|
|
| |
| let html = ''; |
| let currentPos = 0; |
| let openSpans = []; |
|
|
| boundaries.forEach(boundary => { |
| |
| if (boundary.position > currentPos) { |
| html += escapeHtml(text.substring(currentPos, boundary.position)); |
| } |
|
|
| if (boundary.type === 'start') { |
| |
| const backgroundColor = getSpanColor(boundary.annotation.label); |
| const span = `<span class="span-highlight" data-annotation-id="${boundary.annotation.id}" data-label="${boundary.annotation.label}" style="background-color: ${backgroundColor}"><span class="span-delete" onclick="deleteRobustSpan('${boundary.annotation.id}')">×</span><span class="span-label">${boundary.annotation.label}</span>`; |
| html += span; |
| openSpans.push(boundary.annotation); |
| } else { |
| |
| html += '</span>'; |
| |
| const index = openSpans.findIndex(span => span.id === boundary.annotation.id); |
| if (index !== -1) { |
| openSpans.splice(index, 1); |
| } |
| } |
|
|
| currentPos = boundary.position; |
| }); |
|
|
| |
| if (currentPos < text.length) { |
| html += escapeHtml(text.substring(currentPos)); |
| } |
|
|
| |
| openSpans.forEach(() => { |
| html += '</span>'; |
| }); |
|
|
| return html; |
| } |
|
|
| |
| |
| |
| function getSpanColor(label) { |
| const color = spanColors[label]; |
| if (!color) return '#f0f0f0'; |
|
|
| |
| const rgb = color.match(/\((\d+),\s*(\d+),\s*(\d+)\)/); |
| if (rgb) { |
| const r = parseInt(rgb[1]); |
| const g = parseInt(rgb[2]); |
| const b = parseInt(rgb[3]); |
| return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`; |
| } |
|
|
| return '#f0f0f0'; |
| } |
|
|
| |
| |
| |
| async function deleteRobustSpan(annotationId) { |
| try { |
| debugLog('[ROBUST SPAN] Deleting span:', annotationId); |
|
|
| |
| const annotation = spanAnnotations.find(a => a.id === annotationId); |
| if (!annotation) { |
| console.warn('[ROBUST SPAN] Annotation not found:', annotationId); |
| return; |
| } |
|
|
| |
| const postData = { |
| type: "span", |
| schema: "emotion", |
| state: [ |
| { |
| name: annotation.label, |
| start: annotation.start, |
| end: annotation.end, |
| title: annotation.label, |
| value: null |
| } |
| ], |
| instance_id: currentInstance.id |
| }; |
|
|
| const response = await fetch('/updateinstance', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(postData) |
| }); |
|
|
| if (response.ok) { |
| debugLog('[ROBUST SPAN] Span annotation deleted successfully'); |
| |
| await loadCurrentInstance(); |
| } else { |
| console.error('[ROBUST SPAN] Failed to delete span annotation:', await response.text()); |
| } |
| } catch (error) { |
| console.error('[ROBUST SPAN] Error deleting span annotation:', error); |
| } |
| } |
|
|
| |
| |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', function () { |
| |
| |
| |
| |
| |
| }); |
|
|
| |
| |
| |
| async function deleteSpanAnnotation(annotationId, label, start, end) { |
| try { |
| debugLog('[SPAN DELETE] Deleting span:', { annotationId, label, start, end }); |
|
|
| |
| const postData = { |
| type: "span", |
| schema: "emotion", |
| state: [ |
| { |
| name: label, |
| start: start, |
| end: end, |
| title: label, |
| value: null |
| } |
| ], |
| instance_id: currentInstance.id |
| }; |
|
|
| const response = await fetch('/updateinstance', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify(postData) |
| }); |
|
|
| if (response.ok) { |
| debugLog('[SPAN DELETE] Span annotation deleted successfully'); |
| |
| await loadCurrentInstance(); |
| } else { |
| console.error('[SPAN DELETE] Failed to delete span annotation:', await response.text()); |
| } |
| } catch (error) { |
| console.error('[SPAN DELETE] Error deleting span annotation:', error); |
| } |
| } |
|
|
| |
| async function debugAndClearSpans() { |
| debugLog('🔍 [DEBUG] debugAndClearSpans() - ENTRY POINT'); |
|
|
| if (!currentInstance || !currentInstance.id) { |
| debugLog('🔍 [DEBUG] debugAndClearSpans() - No current instance'); |
| return; |
| } |
|
|
| debugLog(`🔍 [DEBUG] debugAndClearSpans() - Current instance ID: ${currentInstance.id}`); |
|
|
| try { |
| |
| const response = await fetch(`/api/spans/${currentInstance.id}`); |
| if (response.ok) { |
| const data = await response.json(); |
| debugLog(`🔍 [DEBUG] debugAndClearSpans() - Current spans for instance ${currentInstance.id}:`, data.spans); |
|
|
| if (data.spans && data.spans.length > 0) { |
| debugLog(`🔍 [DEBUG] debugAndClearSpans() - Found ${data.spans.length} spans, clearing them...`); |
|
|
| |
| const clearResponse = await fetch(`/api/spans/${currentInstance.id}/clear`, { |
| method: 'POST', |
| credentials: 'include' |
| }); |
|
|
| if (clearResponse.ok) { |
| const clearData = await clearResponse.json(); |
| debugLog(`🔍 [DEBUG] debugAndClearSpans() - Cleared ${clearData.spans_cleared} spans`); |
|
|
| |
| debugLog('🔍 [DEBUG] debugAndClearSpans() - Reloading page...'); |
| window.location.reload(); |
| } else { |
| console.error('🔍 [DEBUG] debugAndClearSpans() - Failed to clear spans:', await clearResponse.text()); |
| } |
| } else { |
| debugLog(`🔍 [DEBUG] debugAndClearSpans() - No spans found for instance ${currentInstance.id}`); |
| } |
| } else { |
| console.error('🔍 [DEBUG] debugAndClearSpans() - Failed to get spans:', await response.text()); |
| } |
| } catch (error) { |
| console.error('🔍 [DEBUG] debugAndClearSpans() - Error:', error); |
| } |
| } |
|
|
| |
| window.debugAndClearSpans = debugAndClearSpans; |
|
|
| |
| function debugInstanceId() { |
| debugLog('🔍 [DEBUG] debugInstanceId() - ENTRY POINT'); |
|
|
| |
| const domInstanceId = document.getElementById('instance_id'); |
| const domValue = domInstanceId ? domInstanceId.value : 'not found'; |
| debugLog(`🔍 [DEBUG] debugInstanceId() - DOM instance_id value: '${domValue}'`); |
|
|
| |
| const currentInstanceId = currentInstance ? currentInstance.id : 'not set'; |
| debugLog(`🔍 [DEBUG] debugInstanceId() - currentInstance.id: '${currentInstanceId}'`); |
|
|
| |
| if (domValue === currentInstanceId) { |
| debugLog('🔍 [DEBUG] debugInstanceId() - ✅ DOM and currentInstance match'); |
| } else { |
| debugLog('🔍 [DEBUG] debugInstanceId() - ❌ DOM and currentInstance do NOT match'); |
| } |
|
|
| |
| if (currentInstance && currentInstance.id) { |
| debugLog(`🔍 [DEBUG] debugInstanceId() - API would be called with: /api/spans/${currentInstance.id}`); |
| } |
| } |
|
|
| |
| window.debugInstanceId = debugInstanceId; |
|
|
| |
| function debugAndFixInstanceId() { |
| debugLog('🔍 [DEBUG] debugAndFixInstanceId() - ENTRY POINT'); |
|
|
| |
| const domInstanceId = document.getElementById('instance_id'); |
| const domValue = domInstanceId ? domInstanceId.value : 'not found'; |
| debugLog(`🔍 [DEBUG] debugAndFixInstanceId() - Current DOM instance_id: '${domValue}'`); |
|
|
| |
| debugLog('🔍 [DEBUG] debugAndFixInstanceId() - Attempting to force hard refresh...'); |
|
|
| |
| if (window.caches) { |
| caches.keys().then(names => { |
| names.forEach(name => { |
| debugLog(`🔍 [DEBUG] debugAndFixInstanceId() - Clearing cache: ${name}`); |
| caches.delete(name); |
| }); |
| }); |
| } |
|
|
| |
| const currentUrl = window.location.href; |
| const separator = currentUrl.includes('?') ? '&' : '?'; |
| const newUrl = currentUrl + separator + '_t=' + Date.now(); |
| debugLog(`🔍 [DEBUG] debugAndFixInstanceId() - Redirecting to: ${newUrl}`); |
|
|
| |
| window.location.href = newUrl; |
| } |
|
|
| |
| function checkPageCache() { |
| debugLog('🔍 [DEBUG] checkPageCache() - ENTRY POINT'); |
|
|
| |
| if (window.performance && window.performance.navigation) { |
| const navigationType = window.performance.navigation.type; |
| debugLog(`🔍 [DEBUG] checkPageCache() - Navigation type: ${navigationType}`); |
|
|
| if (navigationType === 1) { |
| debugLog('🔍 [DEBUG] checkPageCache() - Page was reloaded'); |
| } else if (navigationType === 2) { |
| debugLog('🔍 [DEBUG] checkPageCache() - Page was loaded from back/forward cache'); |
| } else { |
| debugLog('🔍 [DEBUG] checkPageCache() - Page was loaded normally'); |
| } |
| } |
|
|
| |
| if (window.performance && window.performance.getEntriesByType) { |
| const navigationEntries = window.performance.getEntriesByType('navigation'); |
| if (navigationEntries.length > 0) { |
| const entry = navigationEntries[0]; |
| debugLog(`🔍 [DEBUG] checkPageCache() - Transfer size: ${entry.transferSize}`); |
| debugLog(`🔍 [DEBUG] checkPageCache() - Encoded body size: ${entry.encodedBodySize}`); |
|
|
| if (entry.transferSize === 0 && entry.encodedBodySize > 0) { |
| debugLog('🔍 [DEBUG] checkPageCache() - Page was loaded from cache!'); |
| } else { |
| debugLog('🔍 [DEBUG] checkPageCache() - Page was loaded from network'); |
| } |
| } |
| } |
| } |
|
|
| |
| window.debugAndFixInstanceId = debugAndFixInstanceId; |
| window.checkPageCache = checkPageCache; |
|
|
| |
| async function clearErroneousSpans() { |
| debugLog('🔍 [DEBUG] clearErroneousSpans() - ENTRY POINT'); |
|
|
| if (!currentInstance || !currentInstance.id) { |
| debugLog('🔍 [DEBUG] clearErroneousSpans() - No current instance'); |
| return; |
| } |
|
|
| debugLog(`🔍 [DEBUG] clearErroneousSpans() - Current instance ID: ${currentInstance.id}`); |
|
|
| try { |
| |
| const response = await fetch(`/api/spans/${currentInstance.id}/clear`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| } |
| }); |
|
|
| if (response.ok) { |
| const result = await response.json(); |
| debugLog(`🔍 [DEBUG] clearErroneousSpans() - Clear result:`, result); |
|
|
| |
| debugLog('🔍 [DEBUG] clearErroneousSpans() - Reloading page to get fresh data'); |
| window.location.reload(); |
| } else { |
| console.error(`🔍 [DEBUG] clearErroneousSpans() - Clear failed:`, response.status); |
| } |
| } catch (error) { |
| console.error(`🔍 [DEBUG] clearErroneousSpans() - Error:`, error); |
| } |
| } |
|
|
| |
| window.clearErroneousSpans = clearErroneousSpans; |
|
|
| |
| function firefoxInstanceIdFix() { |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| if (!isFirefox) { |
| return; |
| } |
|
|
| debugLog('🔍 [DEBUG] firefoxInstanceIdFix: Starting Firefox-specific instance_id fix'); |
|
|
| |
| setTimeout(() => { |
| const instanceIdInput = document.getElementById('instance_id'); |
| if (!instanceIdInput) { |
| debugLog('🔍 [DEBUG] firefoxInstanceIdFix: No instance_id input found'); |
| return; |
| } |
|
|
| |
| const currentInstanceId = currentInstance?.id; |
| const domInstanceId = instanceIdInput.value; |
|
|
| debugLog(`🔍 [DEBUG] firefoxInstanceIdFix: DOM instance_id: '${domInstanceId}', currentInstance.id: '${currentInstanceId}'`); |
|
|
| if (currentInstanceId && domInstanceId !== currentInstanceId) { |
| debugLog('🔍 [DEBUG] firefoxInstanceIdFix: Mismatch detected - fixing instance_id'); |
|
|
| |
| instanceIdInput.value = currentInstanceId; |
|
|
| |
| instanceIdInput.dispatchEvent(new Event('input', { bubbles: true })); |
| instanceIdInput.dispatchEvent(new Event('change', { bubbles: true })); |
|
|
| |
| instanceIdInput.offsetHeight; |
|
|
| debugLog(`🔍 [DEBUG] firefoxInstanceIdFix: Fixed instance_id to '${currentInstanceId}'`); |
| } else { |
| debugLog('🔍 [DEBUG] firefoxInstanceIdFix: No mismatch detected'); |
| } |
| }, 100); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', firefoxInstanceIdFix); |
| window.addEventListener('load', firefoxInstanceIdFix); |
|
|
| |
| function testFirefoxInstanceIdFix() { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: Testing Firefox instance_id fix'); |
|
|
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| debugLog(`🔍 [DEBUG] testFirefoxInstanceIdFix: Is Firefox: ${isFirefox}`); |
|
|
| const instanceIdInput = document.getElementById('instance_id'); |
| if (!instanceIdInput) { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: No instance_id input found'); |
| return; |
| } |
|
|
| const domInstanceId = instanceIdInput.value; |
| const currentInstanceId = currentInstance?.id; |
|
|
| debugLog(`🔍 [DEBUG] testFirefoxInstanceIdFix: DOM instance_id: '${domInstanceId}'`); |
| debugLog(`🔍 [DEBUG] testFirefoxInstanceIdFix: currentInstance.id: '${currentInstanceId}'`); |
|
|
| if (domInstanceId === currentInstanceId) { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: ✅ Instance IDs match'); |
| } else { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: ❌ Instance IDs do not match'); |
|
|
| |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: Attempting to fix...'); |
| firefoxInstanceIdFix(); |
|
|
| |
| setTimeout(() => { |
| const newDomInstanceId = instanceIdInput.value; |
| debugLog(`🔍 [DEBUG] testFirefoxInstanceIdFix: After fix - DOM instance_id: '${newDomInstanceId}'`); |
|
|
| if (newDomInstanceId === currentInstanceId) { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: ✅ Fix successful'); |
| } else { |
| debugLog('🔍 [DEBUG] testFirefoxInstanceIdFix: ❌ Fix failed'); |
| } |
| }, 200); |
| } |
| } |
|
|
| |
| window.testFirefoxInstanceIdFix = testFirefoxInstanceIdFix; |
|
|
| |
| function aggressiveFirefoxInstanceIdFix() { |
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| if (!isFirefox) { |
| return; |
| } |
|
|
| debugLog('🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Starting aggressive Firefox fix'); |
|
|
| |
| setTimeout(() => { |
| |
| const instanceIdInput = document.getElementById('instance_id'); |
| if (!instanceIdInput) { |
| debugLog('🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: No instance_id input found'); |
| return; |
| } |
|
|
| |
| const currentDomValue = instanceIdInput.value; |
| debugLog(`🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Current DOM value: '${currentDomValue}'`); |
|
|
| |
| |
| let correctInstanceId = null; |
|
|
| |
| const scriptTags = document.querySelectorAll('script'); |
| for (const script of scriptTags) { |
| const content = script.textContent || script.innerHTML; |
| if (content.includes('instance_id') || content.includes('currentInstance')) { |
| debugLog('🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Found script with instance data'); |
| |
| const match = content.match(/instance_id['"]?\s*[:=]\s*['"]([^'"]+)['"]/); |
| if (match) { |
| correctInstanceId = match[1]; |
| debugLog(`🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Found instance_id in script: '${correctInstanceId}'`); |
| break; |
| } |
| } |
| } |
|
|
| |
| if (!correctInstanceId) { |
| |
| const urlParams = new URLSearchParams(window.location.search); |
| const urlInstanceId = urlParams.get('instance_id'); |
| if (urlInstanceId) { |
| correctInstanceId = urlInstanceId; |
| debugLog(`🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Found instance_id in URL: '${correctInstanceId}'`); |
| } |
| } |
|
|
| |
| if (!correctInstanceId) { |
| debugLog('🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: No instance_id found, trying API call'); |
|
|
| |
| fetch('/api/current_instance', { |
| method: 'GET', |
| headers: { |
| 'Content-Type': 'application/json', |
| } |
| }) |
| .then(response => response.json()) |
| .then(data => { |
| if (data && data.instance_id) { |
| correctInstanceId = data.instance_id; |
| debugLog(`🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: Got instance_id from API: '${correctInstanceId}'`); |
| applyInstanceIdFix(instanceIdInput, correctInstanceId); |
| } |
| }) |
| .catch(error => { |
| debugLog('🔍 [DEBUG] aggressiveFirefoxInstanceIdFix: API call failed:', error); |
| }); |
| } else { |
| |
| applyInstanceIdFix(instanceIdInput, correctInstanceId); |
| } |
| }, 200); |
| } |
|
|
| |
| function applyInstanceIdFix(instanceIdInput, correctInstanceId) { |
| const currentValue = instanceIdInput.value; |
|
|
| if (currentValue !== correctInstanceId) { |
| debugLog(`🔍 [DEBUG] applyInstanceIdFix: Fixing instance_id from '${currentValue}' to '${correctInstanceId}'`); |
|
|
| |
| instanceIdInput.value = correctInstanceId; |
|
|
| |
| instanceIdInput.dispatchEvent(new Event('input', { bubbles: true })); |
| instanceIdInput.dispatchEvent(new Event('change', { bubbles: true })); |
| instanceIdInput.dispatchEvent(new Event('blur', { bubbles: true })); |
|
|
| |
| instanceIdInput.offsetHeight; |
|
|
| |
| if (window.currentInstance) { |
| window.currentInstance.id = correctInstanceId; |
| debugLog(`🔍 [DEBUG] applyInstanceIdFix: Updated window.currentInstance.id to '${correctInstanceId}'`); |
| } |
|
|
| |
| if (typeof currentInstance !== 'undefined' && currentInstance) { |
| currentInstance.id = correctInstanceId; |
| debugLog(`🔍 [DEBUG] applyInstanceIdFix: Updated currentInstance.id to '${correctInstanceId}'`); |
| } |
|
|
| debugLog(`🔍 [DEBUG] applyInstanceIdFix: Fix applied successfully`); |
| } else { |
| debugLog(`🔍 [DEBUG] applyInstanceIdFix: No fix needed, instance_id is already correct: '${currentValue}'`); |
| } |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', aggressiveFirefoxInstanceIdFix); |
| window.addEventListener('load', aggressiveFirefoxInstanceIdFix); |
|
|
| |
| document.addEventListener('visibilitychange', () => { |
| if (!document.hidden) { |
| setTimeout(aggressiveFirefoxInstanceIdFix, 100); |
| } |
| }); |
|
|
| |
| function testAggressiveFirefoxFix() { |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: Testing aggressive Firefox fix'); |
|
|
| const isFirefox = navigator.userAgent.toLowerCase().includes('firefox'); |
| debugLog(`🔍 [DEBUG] testAggressiveFirefoxFix: Is Firefox: ${isFirefox}`); |
|
|
| if (!isFirefox) { |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: Not Firefox, skipping test'); |
| return; |
| } |
|
|
| |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: Calling aggressiveFirefoxInstanceIdFix'); |
| aggressiveFirefoxInstanceIdFix(); |
|
|
| |
| setTimeout(() => { |
| const instanceIdInput = document.getElementById('instance_id'); |
| if (!instanceIdInput) { |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: No instance_id input found'); |
| return; |
| } |
|
|
| const finalInstanceId = instanceIdInput.value; |
| const currentInstanceId = currentInstance?.id; |
|
|
| debugLog(`🔍 [DEBUG] testAggressiveFirefoxFix: Final DOM instance_id: '${finalInstanceId}'`); |
| debugLog(`🔍 [DEBUG] testAggressiveFirefoxFix: currentInstance.id: '${currentInstanceId}'`); |
|
|
| if (finalInstanceId === currentInstanceId) { |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: ✅ Fix successful - instance IDs match'); |
| } else { |
| debugLog('🔍 [DEBUG] testAggressiveFirefoxFix: ❌ Fix failed - instance IDs do not match'); |
| } |
| }, 500); |
| } |
|
|
|
|
|
|
|
|
| |
| |
| |
| |
| |
| async function jumpToUnannotatedPrev() { |
| debugLog('[NAV] jumpToUnannotatedPrev - ENTRY POINT'); |
|
|
| if (isLoading) { |
| debugLog('[NAV] jumpToUnannotatedPrev - Navigation blocked, still loading'); |
| return; |
| } |
|
|
| setLoading(true); |
| debugLog('[NAV] jumpToUnannotatedPrev - Loading set to true'); |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackNavigation('jump_to_unannotated_prev', currentInstance?.id, null); |
| } |
|
|
| try { |
| |
| debugLog('[NAV] jumpToUnannotatedPrev - Saving annotations before navigation'); |
| await saveAnnotations(); |
|
|
| |
| const response = await fetch('/annotate', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| action: 'jump_to_unannotated_prev', |
| instance_id: currentInstance?.id |
| }) |
| }); |
|
|
| if (response.ok) { |
| |
| const contentType = response.headers.get('content-type'); |
| if (contentType && contentType.includes('application/json')) { |
| const result = await response.json(); |
| if (result.status === 'no_unannotated') { |
| debugLog('[NAV] jumpToUnannotatedPrev - All items annotated'); |
| showNotification('All items have been annotated!', 'info'); |
| setLoading(false); |
| return; |
| } |
| } |
|
|
| debugLog('[NAV] jumpToUnannotatedPrev - Navigation successful, reloading page'); |
| window.location.reload(); |
| } else { |
| console.error('[NAV] jumpToUnannotatedPrev - Navigation failed:', response.status); |
| setLoading(false); |
| } |
| } catch (error) { |
| console.error('[NAV] jumpToUnannotatedPrev - Navigation error:', error); |
| setLoading(false); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| async function jumpToUnannotated() { |
| debugLog('[NAV] jumpToUnannotated - ENTRY POINT'); |
|
|
| if (isLoading) { |
| debugLog('[NAV] jumpToUnannotated - Navigation blocked, still loading'); |
| return; |
| } |
|
|
| |
| hasAttemptedForwardValidation = true; |
| if (!validateRequiredFields({ showErrors: true })) { |
| debugLog('[NAV] jumpToUnannotated - blocked by client-side validation'); |
| return; |
| } |
|
|
| setLoading(true); |
| debugLog('[NAV] jumpToUnannotated - Loading set to true'); |
|
|
| |
| if (window.interactionTracker) { |
| window.interactionTracker.trackNavigation('jump_to_unannotated', currentInstance?.id, null); |
| } |
|
|
| try { |
| |
| debugLog('[NAV] jumpToUnannotated - Saving annotations before navigation'); |
| await saveAnnotations(); |
|
|
| |
| const response = await fetch('/annotate', { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| }, |
| body: JSON.stringify({ |
| action: 'jump_to_unannotated', |
| instance_id: currentInstance?.id |
| }) |
| }); |
|
|
| if (response.ok) { |
| |
| const contentType = response.headers.get('content-type'); |
| if (contentType && contentType.includes('application/json')) { |
| const result = await response.json(); |
| if (result.status === 'no_unannotated') { |
| debugLog('[NAV] jumpToUnannotated - All items annotated'); |
| showNotification('All items have been annotated!', 'info'); |
| setLoading(false); |
| return; |
| } |
| } |
|
|
| debugLog('[NAV] jumpToUnannotated - Navigation successful, reloading page'); |
| window.location.reload(); |
| } else { |
| await handleNavigationResponseError(response); |
| setLoading(false); |
| } |
| } catch (error) { |
| console.error('[NAV] jumpToUnannotated - Navigation error:', error); |
| setLoading(false); |
| } |
| } |
|
|
| function handleQualityControlResponse(result) { |
| if (!result || typeof result !== 'object') { |
| return; |
| } |
|
|
| const qcResult = result.qc_result && typeof result.qc_result === 'object' |
| ? result.qc_result |
| : null; |
| const message = result.warning_message || result.message || (qcResult && qcResult.message); |
|
|
| const isBlocked = result.status === 'blocked' || (qcResult && qcResult.blocked); |
| if (isBlocked) { |
| showNotification(message || 'You have been blocked.', 'error'); |
| showError(true, message || 'You have been blocked due to quality control checks. Your session has ended.', { permanent: true }); |
| return; |
| } |
|
|
| const isWarning = (result.warning || (qcResult && qcResult.warning)) && |
| !(qcResult && qcResult.passed === true); |
| if (isWarning) { |
| showNotification(message || 'Please read items carefully before answering.', 'warning'); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| function showNotification(message, type = 'info') { |
| |
| let container = document.getElementById('notification-container'); |
| if (!container) { |
| container = document.createElement('div'); |
| container.id = 'notification-container'; |
| container.style.cssText = 'position: fixed; top: 80px; right: 20px; z-index: 9999;'; |
| document.body.appendChild(container); |
| } |
|
|
| |
| const notification = document.createElement('div'); |
| notification.className = `notification notification-${type}`; |
| notification.style.cssText = ` |
| padding: 12px 20px; |
| margin-bottom: 10px; |
| border-radius: 6px; |
| font-size: 14px; |
| font-weight: 500; |
| box-shadow: 0 4px 12px rgba(0,0,0,0.15); |
| animation: slideIn 0.3s ease; |
| background-color: ${type === 'info' ? '#e0f2fe' : type === 'success' ? '#dcfce7' : type === 'warning' ? '#fef3c7' : '#fee2e2'}; |
| color: ${type === 'info' ? '#0369a1' : type === 'success' ? '#166534' : type === 'warning' ? '#92400e' : '#dc2626'}; |
| border: 1px solid ${type === 'info' ? '#7dd3fc' : type === 'success' ? '#86efac' : type === 'warning' ? '#fcd34d' : '#fca5a5'}; |
| `; |
| notification.textContent = message; |
|
|
| container.appendChild(notification); |
|
|
| |
| setTimeout(() => { |
| notification.style.animation = 'slideOut 0.3s ease'; |
| setTimeout(() => notification.remove(), 300); |
| }, 4000); |
| } |
|
|
| |
| window.testAggressiveFirefoxFix = testAggressiveFirefoxFix; |
| window.navigateToNext = navigateToNext; |
| window.navigateToPrevious = navigateToPrevious; |
| window.jumpToUnannotated = jumpToUnannotated; |
| window.jumpToUnannotatedPrev = jumpToUnannotatedPrev; |
| window.showNotification = showNotification; |
| window.handleQualityControlResponse = handleQualityControlResponse; |
| window.loadCurrentInstance = loadCurrentInstance; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function initPairwiseAnnotation() { |
| debugLog('[PAIRWISE] Initializing pairwise annotation'); |
|
|
| |
| document.querySelectorAll('.pairwise-tile').forEach(tile => { |
| tile.addEventListener('click', function() { |
| selectPairwiseTile(this); |
| }); |
|
|
| |
| tile.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| selectPairwiseTile(this); |
| } |
| }); |
| }); |
|
|
| |
| document.querySelectorAll('.pairwise-tie-btn, .pairwise-neither-btn').forEach(btn => { |
| btn.addEventListener('click', function() { |
| selectPairwiseOption(this); |
| }); |
| }); |
|
|
| |
| document.querySelectorAll('.pairwise-reason-cb').forEach(cb => { |
| cb.addEventListener('change', function() { |
| const schema = this.getAttribute('data-schema'); |
| savePairwiseJustification(schema); |
| }); |
| }); |
|
|
| |
| populatePairwiseTileContent(); |
|
|
| debugLog('[PAIRWISE] Initialization complete'); |
| } |
|
|
| |
| |
| |
| function populatePairwiseTileContent() { |
| const pairwiseForms = document.querySelectorAll('.annotation-form.pairwise'); |
|
|
| if (pairwiseForms.length === 0) { |
| return; |
| } |
|
|
| |
| let items = null; |
| const instanceText = document.getElementById('instance-text'); |
|
|
| if (instanceText) { |
| const textContent = instanceText.querySelector('#text-content'); |
| const contentElement = textContent || instanceText; |
|
|
| |
| const listItems = contentElement.querySelectorAll('[data-item-index]'); |
| if (listItems.length >= 2) { |
| items = Array.from(listItems).map(el => el.textContent.trim()); |
| } |
|
|
| |
| if (!items) { |
| const html = contentElement.innerHTML; |
| const parts = html.split(/<br\s*\/?>\s*<br\s*\/?>/i); |
| if (parts.length >= 2) { |
| items = parts.map(part => { |
| const temp = document.createElement('div'); |
| temp.innerHTML = part; |
| let text = temp.textContent || ''; |
| text = text.replace(/^[A-Z]\.\s*/, '').trim(); |
| return text; |
| }).filter(t => t.length > 0); |
| if (items.length < 2) items = null; |
| } |
| } |
|
|
| |
| if (!items) { |
| const rawText = contentElement.textContent || ''; |
| const regex = /([A-Z])\.\s*([\s\S]*?)(?=(?:[A-Z]\.\s)|$)/g; |
| const matches = []; |
| let match; |
| while ((match = regex.exec(rawText)) !== null) { |
| const text = match[2].trim(); |
| if (text.length > 0) matches.push(text); |
| } |
| if (matches.length >= 2) items = matches; |
| } |
| } |
|
|
| |
| if (!items) { |
| const firstForm = pairwiseForms[0]; |
| const itemsKey = firstForm.getAttribute('data-items-key') || 'text'; |
| if (window.currentInstanceData && window.currentInstanceData[itemsKey]) { |
| const data = window.currentInstanceData[itemsKey]; |
| if (Array.isArray(data) && data.length >= 2) items = data; |
| } |
| } |
|
|
| if (items && items.length >= 2) { |
| |
| createPairwiseItemsDisplay(items, pairwiseForms[0]); |
|
|
| |
| wrapPairwiseFormsInFlexContainer(pairwiseForms); |
|
|
| |
| const instanceTextContainer = document.querySelector('.instance-text-container'); |
| if (instanceTextContainer) { |
| instanceTextContainer.style.display = 'none'; |
| } |
| |
| const textHeading = document.querySelector('h5.mb-3'); |
| if (textHeading && textHeading.textContent.includes('Text to Annotate')) { |
| textHeading.style.display = 'none'; |
| } |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| function wrapPairwiseFormsInFlexContainer(pairwiseForms) { |
| |
| if (window.formLayoutManager && window.formLayoutManager.initialized) { |
| debugLog('[wrapPairwiseFormsInFlexContainer] Skipping - FormLayoutManager active'); |
| return; |
| } |
|
|
| |
| if (document.querySelector('.annotation-forms-layout') || |
| document.querySelector('.annotation-forms-grid')) { |
| return; |
| } |
|
|
| |
| const annotationFormsContainer = document.getElementById('annotation-forms'); |
| if (!annotationFormsContainer) return; |
|
|
| const allForms = annotationFormsContainer.querySelectorAll('.annotation-form'); |
| if (allForms.length < 2) { |
| return; |
| } |
|
|
| |
| const wrapper = document.createElement('div'); |
| wrapper.className = 'pairwise-forms-wrapper annotation-forms-grid'; |
|
|
| |
| const pairwiseDisplay = annotationFormsContainer.querySelector('.pairwise-items-display-container'); |
| if (pairwiseDisplay) { |
| pairwiseDisplay.after(wrapper); |
| } else { |
| annotationFormsContainer.insertBefore(wrapper, annotationFormsContainer.firstChild); |
| } |
|
|
| |
| allForms.forEach(form => { |
| |
| if (!form.hasAttribute('data-grid-columns')) { |
| form.setAttribute('data-grid-columns', '1'); |
| } |
| wrapper.appendChild(form); |
| }); |
| } |
|
|
| |
| |
| |
| function createPairwiseItemsDisplay(items, referenceForm) { |
| |
| if (document.querySelector('.pairwise-items-display-container')) { |
| |
| const boxes = document.querySelectorAll('.pairwise-items-display-container .pairwise-item-box'); |
| boxes.forEach((box, index) => { |
| if (index < items.length) box.textContent = items[index]; |
| }); |
| return; |
| } |
|
|
| |
| const labels = ['Response A', 'Response B']; |
|
|
| |
| const displayContainer = document.createElement('div'); |
| displayContainer.className = 'pairwise-items-display-container'; |
| displayContainer.innerHTML = ` |
| <div class="pairwise-items-display"> |
| <div class="pairwise-item-wrapper"> |
| <div class="pairwise-item-title">${labels[0]}</div> |
| <div class="pairwise-item-box">${escapeHtml(items[0])}</div> |
| </div> |
| <div class="pairwise-item-wrapper"> |
| <div class="pairwise-item-title">${labels[1]}</div> |
| <div class="pairwise-item-box">${escapeHtml(items[1])}</div> |
| </div> |
| </div> |
| `; |
|
|
| |
| const annotationForms = document.getElementById('annotation-forms'); |
| if (annotationForms) { |
| annotationForms.insertBefore(displayContainer, annotationForms.firstChild); |
| } |
| } |
|
|
| |
| |
| |
| function escapeHtml(text) { |
| const div = document.createElement('div'); |
| div.textContent = text; |
| return div.innerHTML; |
| } |
|
|
| |
| |
| |
| |
| function selectPairwiseTile(tile) { |
| const schema = tile.getAttribute('data-schema'); |
| const value = tile.getAttribute('data-value'); |
| const dimension = tile.getAttribute('data-dimension'); |
| const form = tile.closest('form'); |
|
|
| if (!form) return; |
|
|
| debugLog(`[PAIRWISE] Selecting tile: schema=${schema}, value=${value}, dim=${dimension || 'none'}`); |
|
|
| |
| const scope = dimension ? tile.closest('.pairwise-dimension-row') : form; |
|
|
| |
| scope.querySelectorAll('.pairwise-tile').forEach(t => t.classList.remove('selected')); |
| scope.querySelectorAll('.pairwise-tie-btn, .pairwise-neither-btn').forEach(b => b.classList.remove('selected')); |
|
|
| |
| tile.classList.add('selected'); |
|
|
| |
| let hiddenInput; |
| if (dimension) { |
| hiddenInput = scope.querySelector(`.pairwise-dim-input[data-dimension="${dimension}"]`); |
| } else { |
| hiddenInput = form.querySelector('.pairwise-value'); |
| } |
| if (hiddenInput) { |
| hiddenInput.value = value; |
| |
| registerAnnotation(hiddenInput); |
| |
| hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); |
| } |
|
|
| |
| validateRequiredFields(); |
| } |
|
|
| |
| |
| |
| |
| function selectPairwiseOption(btn) { |
| const schema = btn.getAttribute('data-schema'); |
| const value = btn.getAttribute('data-value'); |
| const dimension = btn.getAttribute('data-dimension'); |
| const form = btn.closest('form'); |
|
|
| if (!form) return; |
|
|
| debugLog(`[PAIRWISE] Selecting option: schema=${schema}, value=${value}, dim=${dimension || 'none'}`); |
|
|
| |
| const scope = dimension ? btn.closest('.pairwise-dimension-row') : form; |
|
|
| |
| scope.querySelectorAll('.pairwise-tile').forEach(t => t.classList.remove('selected')); |
| scope.querySelectorAll('.pairwise-tie-btn, .pairwise-neither-btn').forEach(b => b.classList.remove('selected')); |
|
|
| |
| btn.classList.add('selected'); |
|
|
| |
| let hiddenInput; |
| if (dimension) { |
| hiddenInput = scope.querySelector(`.pairwise-dim-input[data-dimension="${dimension}"]`); |
| } else { |
| hiddenInput = form.querySelector('.pairwise-value'); |
| } |
| if (hiddenInput) { |
| hiddenInput.value = value; |
| |
| registerAnnotation(hiddenInput); |
| |
| hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); |
| } |
|
|
| |
| validateRequiredFields(); |
| } |
|
|
| |
| |
| |
| |
| function updatePairwiseScaleDisplay(slider) { |
| const form = slider.closest('form'); |
| if (!form) return; |
|
|
| const valueDisplay = form.querySelector('.pairwise-scale-current-value'); |
| if (valueDisplay) { |
| valueDisplay.textContent = slider.value; |
| } |
| } |
|
|
| |
| |
| |
| |
| function restorePairwiseAnnotations() { |
| if (!currentAnnotations) return; |
|
|
| debugLog('[PAIRWISE] Restoring pairwise annotations'); |
|
|
| |
| document.querySelectorAll('.annotation-form.pairwise-binary').forEach(form => { |
| const hiddenInput = form.querySelector('.pairwise-value'); |
| if (!hiddenInput) return; |
|
|
| const schema = hiddenInput.getAttribute('schema'); |
| const labelName = hiddenInput.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| const savedValue = currentAnnotations[schema][labelName]; |
| hiddenInput.value = savedValue; |
|
|
| |
| if (savedValue === 'tie' || savedValue === 'neither') { |
| const optionBtn = form.querySelector(`.pairwise-tie-btn[data-value="${savedValue}"], .pairwise-neither-btn[data-value="${savedValue}"]`); |
| if (optionBtn) { |
| optionBtn.classList.add('selected'); |
| } |
| } else { |
| const tile = form.querySelector(`.pairwise-tile[data-value="${savedValue}"]`); |
| if (tile) { |
| tile.classList.add('selected'); |
| } |
| } |
|
|
| debugLog(`[PAIRWISE] Restored binary selection: ${schema}/${labelName} = ${savedValue}`); |
| } |
| }); |
|
|
| |
| document.querySelectorAll('.annotation-form.pairwise-scale').forEach(form => { |
| const slider = form.querySelector('.pairwise-scale-slider'); |
| if (!slider) return; |
|
|
| const schema = slider.getAttribute('schema'); |
| const labelName = slider.getAttribute('label_name'); |
|
|
| if (schema && labelName && currentAnnotations[schema] && currentAnnotations[schema][labelName]) { |
| const savedValue = currentAnnotations[schema][labelName]; |
| slider.value = savedValue; |
| updatePairwiseScaleDisplay(slider); |
|
|
| debugLog(`[PAIRWISE] Restored scale value: ${schema}/${labelName} = ${savedValue}`); |
| } |
| }); |
|
|
| |
| document.querySelectorAll('.annotation-form.pairwise-multi-dimension').forEach(form => { |
| const schema = form.getAttribute('data-schema-name'); |
| if (!schema || !currentAnnotations[schema]) return; |
|
|
| form.querySelectorAll('.pairwise-dim-input').forEach(input => { |
| const dim = input.getAttribute('data-dimension'); |
| if (!dim || !currentAnnotations[schema][dim]) return; |
| const val = currentAnnotations[schema][dim]; |
| input.value = val; |
|
|
| const row = input.closest('.pairwise-dimension-row'); |
| if (!row) return; |
|
|
| row.querySelectorAll('.pairwise-tile, .pairwise-tie-btn').forEach(t => t.classList.remove('selected')); |
| if (val === 'tie') { |
| const btn = row.querySelector('.pairwise-tie-btn'); |
| if (btn) btn.classList.add('selected'); |
| } else { |
| const tile = row.querySelector(`.pairwise-tile[data-value="${val}"]`); |
| if (tile) tile.classList.add('selected'); |
| } |
| debugLog(`[PAIRWISE] Restored multi-dim: ${schema}/${dim} = ${val}`); |
| }); |
| }); |
|
|
| |
| document.querySelectorAll('.pairwise-justification').forEach(div => { |
| const schema = div.getAttribute('data-schema'); |
| if (!schema || !currentAnnotations[schema] || !currentAnnotations[schema]['justification']) return; |
|
|
| try { |
| const jdata = JSON.parse(currentAnnotations[schema]['justification']); |
| const hiddenInput = div.querySelector('.pairwise-justification-value'); |
| if (hiddenInput) { |
| hiddenInput.value = currentAnnotations[schema]['justification']; |
| hiddenInput.setAttribute('data-server-set', 'true'); |
| hiddenInput.setAttribute('data-modified', 'true'); |
| } |
|
|
| if (jdata.reasons) { |
| div.querySelectorAll('.pairwise-reason-cb').forEach(cb => { |
| cb.checked = jdata.reasons.includes(cb.value); |
| }); |
| } |
| if (jdata.rationale) { |
| const ta = div.querySelector('.pairwise-rationale-textarea'); |
| if (ta) { |
| ta.value = jdata.rationale; |
| updatePairwiseRationaleCounter(ta); |
| } |
| } |
| } catch(e) { |
| debugLog('[PAIRWISE] Error restoring justification:', e); |
| } |
| }); |
| } |
|
|
| |
| |
| |
| |
| function updatePairwiseRationaleCounter(textarea) { |
| const schema = textarea.getAttribute('data-schema'); |
| const minChars = parseInt(textarea.getAttribute('data-min-chars') || '0', 10); |
| const counter = textarea.closest('.pairwise-justification').querySelector('.pairwise-rationale-counter'); |
| if (!counter) return; |
| const len = textarea.value.length; |
| counter.textContent = `${len} / ${minChars} characters`; |
| counter.classList.toggle('insufficient', len < minChars && minChars > 0); |
|
|
| |
| savePairwiseJustification(schema); |
| } |
|
|
| |
| |
| |
| function savePairwiseJustification(schema) { |
| const div = document.querySelector(`.pairwise-justification[data-schema="${schema}"]`); |
| if (!div) return; |
|
|
| const reasons = []; |
| div.querySelectorAll('.pairwise-reason-cb:checked').forEach(cb => reasons.push(cb.value)); |
| const ta = div.querySelector('.pairwise-rationale-textarea'); |
| const rationale = ta ? ta.value : ''; |
|
|
| const data = JSON.stringify({ reasons: reasons, rationale: rationale }); |
| const input = div.querySelector('.pairwise-justification-value'); |
| if (input) { |
| input.value = data; |
| input.setAttribute('data-modified', 'true'); |
| input.dispatchEvent(new Event('change', { bubbles: true })); |
| } |
| } |
|
|
| |
| window.initPairwiseAnnotation = initPairwiseAnnotation; |
| window.selectPairwiseTile = selectPairwiseTile; |
| window.selectPairwiseOption = selectPairwiseOption; |
| window.updatePairwiseScaleDisplay = updatePairwiseScaleDisplay; |
| window.restorePairwiseAnnotations = restorePairwiseAnnotations; |
| window.updatePairwiseRationaleCounter = updatePairwiseRationaleCounter; |
| window.savePairwiseJustification = savePairwiseJustification; |
|
|
| |
| |
| |
|
|
| |
| |
| |
| |
| function initBwsAnnotation() { |
| const bwsForms = document.querySelectorAll('.annotation-form.bws'); |
| if (bwsForms.length === 0) return; |
|
|
| debugLog('[BWS] Initializing BWS annotation'); |
|
|
| |
| document.querySelectorAll('.bws-tile').forEach(tile => { |
| tile.addEventListener('click', function() { |
| selectBwsTile(this); |
| }); |
| tile.addEventListener('keydown', function(e) { |
| if (e.key === 'Enter' || e.key === ' ') { |
| e.preventDefault(); |
| selectBwsTile(this); |
| } |
| }); |
| }); |
|
|
| |
| populateBwsItemsDisplay(); |
|
|
| debugLog('[BWS] Initialization complete'); |
| } |
|
|
| |
| |
| |
| function populateBwsItemsDisplay() { |
| const bwsForms = document.querySelectorAll('.annotation-form.bws'); |
| if (bwsForms.length === 0) return; |
|
|
| |
| let bwsItems = null; |
| const bwsItemsScript = document.getElementById('bws_items'); |
| if (bwsItemsScript) { |
| try { |
| bwsItems = JSON.parse(bwsItemsScript.textContent); |
| } catch (e) { |
| debugLog('[BWS] Error parsing bws_items JSON:', e); |
| } |
| } |
|
|
| if (!bwsItems || !Array.isArray(bwsItems) || bwsItems.length === 0) { |
| debugLog('[BWS] No BWS items data found'); |
| return; |
| } |
|
|
| debugLog('[BWS] Populating items display with', bwsItems.length, 'items'); |
|
|
| |
| bwsForms.forEach(form => { |
| const displayContainer = form.querySelector('.bws-items-display'); |
| if (!displayContainer) return; |
|
|
| let html = '<div class="bws-items-list">'; |
| bwsItems.forEach(item => { |
| const pos = escapeHtml(item.position || ''); |
| const text = escapeHtml(item.text || ''); |
| html += ` |
| <div class="bws-item" data-position="${pos}"> |
| <span class="bws-item-label">${pos}.</span> |
| <span class="bws-item-text">${text}</span> |
| </div>`; |
| }); |
| html += '</div>'; |
| displayContainer.innerHTML = html; |
| }); |
|
|
| |
| const instanceTextContainer = document.querySelector('.instance-text-container'); |
| if (instanceTextContainer) { |
| instanceTextContainer.style.display = 'none'; |
| } |
| const textHeading = document.querySelector('h5.mb-3'); |
| if (textHeading && textHeading.textContent.includes('Text to Annotate')) { |
| textHeading.style.display = 'none'; |
| } |
| } |
|
|
| |
| |
| |
| |
| function selectBwsTile(tile) { |
| const schema = tile.getAttribute('data-schema'); |
| const value = tile.getAttribute('data-value'); |
| const role = tile.getAttribute('data-role'); |
| const form = tile.closest('form'); |
|
|
| if (!form) return; |
|
|
| debugLog(`[BWS] Selecting tile: schema=${schema}, value=${value}, role=${role}`); |
|
|
| |
| const otherRole = role === 'best' ? 'worst' : 'best'; |
| const otherRoleClass = role === 'best' ? '.bws-worst-tile' : '.bws-best-tile'; |
| const otherSelected = form.querySelector(`${otherRoleClass}.selected`); |
| if (otherSelected && otherSelected.getAttribute('data-value') === value) { |
| debugLog(`[BWS] Blocked: cannot select same item as both best and worst`); |
| return; |
| } |
|
|
| |
| const roleClass = role === 'best' ? '.bws-best-tile' : '.bws-worst-tile'; |
| form.querySelectorAll(roleClass).forEach(t => t.classList.remove('selected')); |
|
|
| |
| tile.classList.add('selected'); |
|
|
| |
| form.querySelectorAll(otherRoleClass).forEach(t => { |
| if (t.getAttribute('data-value') === value) { |
| t.classList.add('bws-disabled'); |
| } else { |
| t.classList.remove('bws-disabled'); |
| } |
| }); |
|
|
| |
| const labelName = role; |
| const hiddenInput = form.querySelector(`.bws-value[label_name="${labelName}"]`); |
| if (hiddenInput) { |
| hiddenInput.value = value; |
| hiddenInput.setAttribute('data-modified', 'true'); |
| registerAnnotation(hiddenInput); |
| hiddenInput.dispatchEvent(new Event('change', { bubbles: true })); |
| } |
|
|
| |
| validateRequiredFields(); |
| } |
|
|
| |
| |
| |
| |
| function validateBwsSelection(form) { |
| const bestInput = form.querySelector('.bws-value[label_name="best"]'); |
| const worstInput = form.querySelector('.bws-value[label_name="worst"]'); |
| const errorDiv = form.querySelector('.bws-validation-error'); |
|
|
| if (!bestInput || !worstInput) return; |
|
|
| const bestVal = bestInput.value; |
| const worstVal = worstInput.value; |
|
|
| if (bestVal && worstVal && bestVal === worstVal) { |
| if (errorDiv) errorDiv.style.display = 'block'; |
| |
| |
| worstInput.value = ''; |
| form.querySelectorAll('.bws-worst-tile').forEach(t => t.classList.remove('selected')); |
| registerAnnotation(worstInput); |
| debugLog('[BWS] Validation error: best == worst, cleared worst'); |
| } else { |
| if (errorDiv) errorDiv.style.display = 'none'; |
| } |
| } |
|
|
| |
| |
| |
| |
| function restoreBwsAnnotations() { |
| if (!currentAnnotations) return; |
|
|
| debugLog('[BWS] Restoring BWS annotations'); |
|
|
| document.querySelectorAll('.annotation-form.bws').forEach(form => { |
| |
| const bestInput = form.querySelector('.bws-value[label_name="best"]'); |
| if (bestInput) { |
| const schema = bestInput.getAttribute('schema'); |
| if (schema && currentAnnotations[schema] && currentAnnotations[schema]['best']) { |
| const savedValue = currentAnnotations[schema]['best']; |
| bestInput.value = savedValue; |
| bestInput.setAttribute('data-modified', 'true'); |
| const tile = form.querySelector(`.bws-best-tile[data-value="${savedValue}"]`); |
| if (tile) tile.classList.add('selected'); |
| debugLog(`[BWS] Restored best: ${schema}/best = ${savedValue}`); |
| } |
| } |
|
|
| |
| const worstInput = form.querySelector('.bws-value[label_name="worst"]'); |
| if (worstInput) { |
| const schema = worstInput.getAttribute('schema'); |
| if (schema && currentAnnotations[schema] && currentAnnotations[schema]['worst']) { |
| const savedValue = currentAnnotations[schema]['worst']; |
| worstInput.value = savedValue; |
| worstInput.setAttribute('data-modified', 'true'); |
| const tile = form.querySelector(`.bws-worst-tile[data-value="${savedValue}"]`); |
| if (tile) tile.classList.add('selected'); |
| debugLog(`[BWS] Restored worst: ${schema}/worst = ${savedValue}`); |
| } |
| } |
| }); |
| } |
|
|
| |
| window.initBwsAnnotation = initBwsAnnotation; |
| window.selectBwsTile = selectBwsTile; |
| window.restoreBwsAnnotations = restoreBwsAnnotations; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| async function populateDynamicSchemaContent() { |
| |
| const dynamicForms = document.querySelectorAll( |
| '.shadcn-extractive-qa-container, .shadcn-text-edit-container, ' + |
| '.shadcn-error-span-container, .shadcn-card-sort-container, ' + |
| '.shadcn-conjoint-container' |
| ); |
| if (dynamicForms.length === 0) return; |
|
|
| |
| let instanceData = null; |
| try { |
| const resp = await fetch('/api/instance_data'); |
| if (resp.ok) { |
| instanceData = resp.json ? await resp.json() : null; |
| } |
| } catch (e) { |
| |
| debugLog('[DynamicSchema] Could not fetch instance data, using fallback'); |
| } |
|
|
| |
| if (!instanceData) { |
| try { |
| const instanceDataScript = document.getElementById('instance_data'); |
| if (instanceDataScript) { |
| instanceData = JSON.parse(instanceDataScript.textContent); |
| } |
| } catch (e) { |
| debugLog('[DynamicSchema] Could not parse embedded instance_data'); |
| } |
| } |
|
|
| |
| if (!instanceData) { |
| try { |
| const instanceScript = document.getElementById('instance'); |
| if (instanceScript) { |
| instanceData = JSON.parse(instanceScript.textContent); |
| } |
| } catch (e) { |
| debugLog('[DynamicSchema] Could not parse embedded instance data'); |
| } |
| } |
|
|
| if (!instanceData) { |
| debugLog('[DynamicSchema] No instance data available'); |
| return; |
| } |
|
|
| |
| window.currentInstanceData = instanceData; |
|
|
| |
| const instanceTextEl = document.getElementById('text-content') || document.getElementById('instance-text'); |
| const instanceText = instanceTextEl ? (instanceTextEl.textContent || '') : ''; |
|
|
| dynamicForms.forEach(form => { |
| const type = form.getAttribute('data-annotation-type'); |
| const schemaName = form.getAttribute('data-schema-name'); |
|
|
| switch (type) { |
| case 'extractive_qa': |
| populateExtractiveQa(form, schemaName, instanceData, instanceText); |
| break; |
| case 'text_edit': |
| populateTextEdit(form, schemaName, instanceData); |
| break; |
| case 'error_span': |
| populateErrorSpan(form, schemaName, instanceData, instanceText); |
| break; |
| case 'card_sort': |
| populateCardSort(form, schemaName, instanceData); |
| break; |
| case 'conjoint': |
| populateConjoint(form, schemaName, instanceData); |
| break; |
| } |
| }); |
| } |
|
|
| function populateExtractiveQa(form, schemaName, data, instanceText) { |
| |
| const questionField = form.getAttribute('data-question-field') || 'question'; |
| const questionEl = form.querySelector('.eqa-question-text'); |
| if (questionEl && data[questionField]) { |
| questionEl.textContent = data[questionField]; |
| } |
|
|
| |
| const passageField = form.getAttribute('data-passage-field') || 'passage'; |
| const passageEl = document.getElementById(schemaName + '-passage'); |
| if (passageEl) { |
| const passageText = data[passageField] || instanceText; |
| if (passageText && !passageEl.textContent.trim()) { |
| passageEl.textContent = passageText; |
| } |
| } |
| } |
|
|
| function populateTextEdit(form, schemaName, data) { |
| const sourceField = form.getAttribute('data-source-field'); |
| if (!sourceField || !data[sourceField]) return; |
|
|
| const sourceText = data[sourceField]; |
|
|
| |
| const sourceEl = document.getElementById(schemaName + '-source-text'); |
| if (sourceEl && !sourceEl.textContent.trim()) { |
| sourceEl.textContent = sourceText; |
| } |
|
|
| |
| const editor = document.getElementById(schemaName + '-editor'); |
| if (editor && !editor.value.trim()) { |
| editor.value = sourceText; |
| } |
| } |
|
|
| function populateErrorSpan(form, schemaName, data, instanceText) { |
| const textContainer = document.getElementById(schemaName + '-text'); |
| if (textContainer && !textContainer.textContent.trim()) { |
| textContainer.textContent = instanceText; |
| } |
|
|
| |
| |
| const instanceTextSection = document.getElementById('instance-text'); |
| if (instanceTextSection) { |
| instanceTextSection.style.display = 'none'; |
| } |
| } |
|
|
| function populateCardSort(form, schemaName, data) { |
| const itemsField = form.getAttribute('data-items-field') || 'items'; |
| const items = data[itemsField]; |
| if (!items || !Array.isArray(items)) return; |
|
|
| const sourceItems = document.getElementById(schemaName + '-source-items'); |
| if (!sourceItems || sourceItems.children.length > 0) return; |
|
|
| items.forEach(function(item, idx) { |
| const card = document.createElement('div'); |
| card.className = 'card-sort-card'; |
| card.draggable = true; |
| card.setAttribute('data-card-text', item); |
| card.textContent = item; |
| card.ondragstart = function(e) { |
| e.dataTransfer.setData('text/plain', item); |
| e.dataTransfer.setData('application/x-source-group', '__source__'); |
| card.classList.add('card-sort-dragging'); |
| }; |
| card.ondragend = function() { |
| card.classList.remove('card-sort-dragging'); |
| }; |
| sourceItems.appendChild(card); |
| }); |
| } |
|
|
| function populateConjoint(form, schemaName, data) { |
| |
| const profilesField = form.getAttribute('data-profiles-field'); |
| let profiles = null; |
|
|
| if (profilesField && data[profilesField]) { |
| profiles = data[profilesField]; |
| } |
|
|
| |
| if (!profiles) { |
| |
| const attrCells = form.querySelectorAll('.conjoint-attr-value'); |
| if (attrCells.length === 0) return; |
|
|
| |
| const attrs = {}; |
| attrCells.forEach(cell => { |
| const attrName = cell.getAttribute('data-attr'); |
| if (attrName && !attrs[attrName]) attrs[attrName] = []; |
| }); |
|
|
| |
| const formScript = form.closest('.annotation_schema'); |
| const scriptEl = formScript ? formScript.querySelector('script') : null; |
| let configData = null; |
| if (scriptEl) { |
| try { |
| |
| const scriptText = scriptEl.textContent; |
| const match = scriptText.match(/var\s+conjointConfig\s*=\s*(\{[^;]+\})/); |
| if (match) configData = JSON.parse(match[1]); |
| } catch (e) {} |
| } |
|
|
| |
| if (configData && configData.attributes) { |
| const profileCards = form.querySelectorAll('.conjoint-profile-card'); |
| profileCards.forEach((card, idx) => { |
| const profileNum = card.getAttribute('data-profile'); |
| configData.attributes.forEach(attr => { |
| const cell = card.querySelector(`.conjoint-attr-value[data-attr="${attr.name}"]`); |
| if (cell && attr.levels && attr.levels.length > 0) { |
| |
| |
| const instanceId = document.getElementById('instance_id'); |
| const seed = (instanceId ? instanceId.value.length : 0) + idx; |
| const levelIdx = (seed + attr.name.length + idx * 7) % attr.levels.length; |
| cell.textContent = attr.levels[levelIdx]; |
| } |
| }); |
| }); |
| } |
| } else if (Array.isArray(profiles)) { |
| |
| const profileCards = form.querySelectorAll('.conjoint-profile-card'); |
| profiles.forEach((profile, idx) => { |
| if (idx < profileCards.length) { |
| const card = profileCards[idx]; |
| Object.keys(profile).forEach(attrName => { |
| const cell = card.querySelector(`.conjoint-attr-value[data-attr="${attrName}"]`); |
| if (cell) cell.textContent = profile[attrName]; |
| }); |
| } |
| }); |
| } |
| } |
|
|