Spaces:
Paused
Paused
| /** | |
| * Option Highlight Manager | |
| * | |
| * Manages LLM-based option highlighting for discrete annotation tasks. | |
| * Dims less-likely options while keeping top-k most likely options at full opacity. | |
| */ | |
| class OptionHighlightManager { | |
| constructor() { | |
| this.config = null; | |
| this.cache = new Map(); // Client-side cache: annotationId -> highlights | |
| this.loadingStates = new Set(); // Track loading states per annotation | |
| this.initialized = false; | |
| } | |
| /** | |
| * Initialize the manager with configuration from the server. | |
| */ | |
| async initialize() { | |
| if (this.initialized) { | |
| return; | |
| } | |
| try { | |
| const response = await fetch('/api/option_highlights/config'); | |
| if (!response.ok) { | |
| console.warn('[OptionHighlight] Failed to load config:', response.status); | |
| return; | |
| } | |
| this.config = await response.json(); | |
| console.log('[OptionHighlight] Config loaded:', this.config); | |
| if (!this.config.enabled) { | |
| console.log('[OptionHighlight] Feature disabled'); | |
| return; | |
| } | |
| this.initialized = true; | |
| // Apply highlights on page load if auto_apply is enabled | |
| if (this.config.auto_apply) { | |
| // Wait for DOM to be ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => this.applyAllHighlights()); | |
| } else { | |
| // Small delay to ensure annotation forms are rendered | |
| setTimeout(() => this.applyAllHighlights(), 100); | |
| } | |
| } | |
| // Trigger prefetch for upcoming items | |
| this.triggerPrefetch(); | |
| } catch (error) { | |
| console.error('[OptionHighlight] Error initializing:', error); | |
| } | |
| } | |
| /** | |
| * Check if a schema should have option highlighting applied. | |
| * @param {string} schemaName - The annotation schema name | |
| * @returns {boolean} | |
| */ | |
| shouldApplyToSchema(schemaName) { | |
| if (!this.config || !this.config.enabled) { | |
| return false; | |
| } | |
| // If schemas filter is not set, apply to all discrete schemas | |
| if (this.config.schemas === null || this.config.schemas === undefined) { | |
| return true; | |
| } | |
| return this.config.schemas.includes(schemaName); | |
| } | |
| /** | |
| * Apply highlights to all annotation forms on the page. | |
| */ | |
| async applyAllHighlights() { | |
| if (!this.config || !this.config.enabled) { | |
| return; | |
| } | |
| const forms = document.querySelectorAll('.annotation-form'); | |
| console.log('[OptionHighlight] Applying to', forms.length, 'forms'); | |
| for (const form of forms) { | |
| const annotationId = form.getAttribute('data-annotation-id'); | |
| const schemaName = form.id || form.dataset.schemaName || ''; | |
| const annotationType = form.dataset.annotationType || ''; | |
| // Only apply to discrete option types | |
| const discreteTypes = ['radio', 'multiselect', 'likert', 'select']; | |
| if (!discreteTypes.includes(annotationType)) { | |
| continue; | |
| } | |
| if (this.shouldApplyToSchema(schemaName)) { | |
| await this.applyHighlightsToForm(form, annotationId); | |
| } | |
| } | |
| } | |
| /** | |
| * Apply highlights to a specific annotation form. | |
| * @param {HTMLElement} form - The annotation form element | |
| * @param {string|number} annotationId - The annotation ID | |
| */ | |
| async applyHighlightsToForm(form, annotationId) { | |
| // Skip if already loading | |
| if (this.loadingStates.has(annotationId)) { | |
| return; | |
| } | |
| this.loadingStates.add(annotationId); | |
| try { | |
| // Check cache first | |
| let highlights = this.cache.get(annotationId); | |
| if (!highlights) { | |
| // Fetch from server | |
| highlights = await this.fetchHighlights(annotationId); | |
| if (highlights && !highlights.error) { | |
| this.cache.set(annotationId, highlights); | |
| } | |
| } | |
| if (!highlights || highlights.error) { | |
| console.warn('[OptionHighlight] No highlights for annotation', annotationId, highlights?.error); | |
| return; | |
| } | |
| // Apply visual highlighting | |
| this.applyVisualHighlights(form, highlights); | |
| } catch (error) { | |
| console.error('[OptionHighlight] Error applying highlights:', error); | |
| } finally { | |
| this.loadingStates.delete(annotationId); | |
| } | |
| } | |
| /** | |
| * Fetch highlights from the server API. | |
| * @param {string|number} annotationId - The annotation ID | |
| * @returns {Promise<Object>} Highlight data | |
| */ | |
| async fetchHighlights(annotationId) { | |
| try { | |
| const response = await fetch(`/api/option_highlights/${annotationId}`); | |
| if (!response.ok) { | |
| return { error: `HTTP ${response.status}` }; | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| return { error: error.message }; | |
| } | |
| } | |
| /** | |
| * Apply visual highlighting to form options. | |
| * @param {HTMLElement} form - The annotation form element | |
| * @param {Object} highlights - The highlight data from the server | |
| */ | |
| applyVisualHighlights(form, highlights) { | |
| const highlightedOptions = highlights.highlighted || []; | |
| const dimOpacity = this.config?.dim_opacity || 0.4; | |
| console.log('[OptionHighlight] Applying visual highlights:', highlightedOptions); | |
| // Find all option elements in the form | |
| const optionContainers = form.querySelectorAll( | |
| '.shadcn-radio-option, .shadcn-checkbox-option, .shadcn-span-option, ' + | |
| '.option-item, .likert-option, .form-check' | |
| ); | |
| // Add AI highlighting active indicator to the form | |
| form.classList.add('ai-highlighting-active'); | |
| optionContainers.forEach(container => { | |
| // Get the input element | |
| const input = container.querySelector('input[type="radio"], input[type="checkbox"]'); | |
| if (!input) return; | |
| const value = input.value; | |
| const labelElement = container.querySelector('label') || container; | |
| const labelText = labelElement.textContent?.trim() || ''; | |
| // Check if this option should be highlighted | |
| const isHighlighted = highlightedOptions.some(opt => { | |
| const optLower = String(opt).toLowerCase(); | |
| const valueLower = String(value).toLowerCase(); | |
| const labelLower = labelText.toLowerCase(); | |
| return optLower === valueLower || | |
| labelLower.includes(optLower) || | |
| optLower.includes(valueLower); | |
| }); | |
| // Apply appropriate classes | |
| if (isHighlighted) { | |
| container.classList.add('option-highlighted'); | |
| container.classList.remove('option-dimmed'); | |
| } else { | |
| container.classList.add('option-dimmed'); | |
| container.classList.remove('option-highlighted'); | |
| container.style.setProperty('--dim-opacity', dimOpacity); | |
| } | |
| }); | |
| } | |
| /** | |
| * Clear all highlights from the page. | |
| */ | |
| clearAllHighlights() { | |
| // Remove classes from all option containers | |
| document.querySelectorAll('.option-highlighted, .option-dimmed').forEach(el => { | |
| el.classList.remove('option-highlighted', 'option-dimmed'); | |
| el.style.removeProperty('--dim-opacity'); | |
| }); | |
| // Remove active indicator from forms | |
| document.querySelectorAll('.ai-highlighting-active').forEach(el => { | |
| el.classList.remove('ai-highlighting-active'); | |
| }); | |
| // Clear cache | |
| this.cache.clear(); | |
| } | |
| /** | |
| * Clear highlights for a specific annotation. | |
| * @param {string|number} annotationId - The annotation ID | |
| */ | |
| clearHighlights(annotationId) { | |
| const form = document.querySelector(`.annotation-form[data-annotation-id="${CSS.escape(String(annotationId))}"]`); | |
| if (!form) return; | |
| form.querySelectorAll('.option-highlighted, .option-dimmed').forEach(el => { | |
| el.classList.remove('option-highlighted', 'option-dimmed'); | |
| el.style.removeProperty('--dim-opacity'); | |
| }); | |
| form.querySelectorAll('.option-highlight-indicator').forEach(el => { | |
| el.remove(); | |
| }); | |
| form.classList.remove('ai-highlighting-active'); | |
| this.cache.delete(annotationId); | |
| } | |
| /** | |
| * Trigger server-side prefetching of upcoming items. | |
| */ | |
| async triggerPrefetch() { | |
| if (!this.config || !this.config.enabled) { | |
| return; | |
| } | |
| try { | |
| await fetch('/api/option_highlights/prefetch', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ count: this.config.prefetch_count }) | |
| }); | |
| console.log('[OptionHighlight] Prefetch triggered'); | |
| } catch (error) { | |
| console.warn('[OptionHighlight] Prefetch trigger failed:', error); | |
| } | |
| } | |
| /** | |
| * Refresh highlights (e.g., after navigation). | |
| */ | |
| async refresh() { | |
| if (!this.config || !this.config.enabled) { | |
| return; | |
| } | |
| // Clear existing highlights | |
| this.clearAllHighlights(); | |
| // Re-apply if auto_apply is enabled | |
| if (this.config.auto_apply) { | |
| await this.applyAllHighlights(); | |
| } | |
| // Trigger prefetch for next items | |
| this.triggerPrefetch(); | |
| } | |
| /** | |
| * Toggle highlighting on/off for the current page. | |
| */ | |
| toggle() { | |
| if (!this.config) { | |
| return; | |
| } | |
| const hasHighlights = document.querySelector('.option-highlighted, .option-dimmed'); | |
| if (hasHighlights) { | |
| this.clearAllHighlights(); | |
| } else { | |
| this.applyAllHighlights(); | |
| } | |
| } | |
| } | |
| // Create global instance | |
| window.optionHighlightManager = new OptionHighlightManager(); | |
| // Initialize when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => { | |
| window.optionHighlightManager.initialize(); | |
| }); | |
| } else { | |
| window.optionHighlightManager.initialize(); | |
| } | |