/** * Instance Display Manager * * Handles client-side functionality for the instance display system. * This includes image zoom, collapsible sections, and coordination * with annotation schemas that reference display fields. */ (function() { 'use strict'; /** * InstanceDisplayManager handles all display field interactions */ class InstanceDisplayManager { constructor() { this.displayContainer = document.querySelector('.instance-display-container'); this.displayFields = {}; this.spanTargets = []; if (this.displayContainer) { this.init(); } } /** * Initialize the display manager */ init() { this.collectDisplayFields(); this.initImageZoom(); this.initCollapsibleSections(); this.initSpanTargets(); this.initPerTurnRatings(); console.log('[InstanceDisplay] Initialized with', Object.keys(this.displayFields).length, 'fields'); } /** * Collect all display fields from the DOM */ collectDisplayFields() { const fields = this.displayContainer.querySelectorAll('.display-field'); fields.forEach(field => { const key = field.dataset.fieldKey; const type = field.dataset.fieldType; if (key) { this.displayFields[key] = { element: field, type: type, isSpanTarget: field.dataset.spanTarget === 'true' }; if (field.dataset.spanTarget === 'true') { this.spanTargets.push(key); } } }); } /** * Initialize image zoom functionality */ initImageZoom() { const zoomContainers = document.querySelectorAll('.image-zoom-container'); zoomContainers.forEach(container => { const img = container.querySelector('img'); const zoomIn = container.querySelector('.zoom-in'); const zoomOut = container.querySelector('.zoom-out'); const zoomReset = container.querySelector('.zoom-reset'); if (!img) return; let scale = 1; const minScale = 0.5; const maxScale = 5; const scaleStep = 1.25; const updateScale = (newScale) => { scale = Math.max(minScale, Math.min(maxScale, newScale)); img.style.transform = `scale(${scale})`; img.style.transformOrigin = 'center center'; }; if (zoomIn) { zoomIn.addEventListener('click', (e) => { e.preventDefault(); updateScale(scale * scaleStep); }); } if (zoomOut) { zoomOut.addEventListener('click', (e) => { e.preventDefault(); updateScale(scale / scaleStep); }); } if (zoomReset) { zoomReset.addEventListener('click', (e) => { e.preventDefault(); updateScale(1); }); } // Also support scroll wheel zoom when hovering container.addEventListener('wheel', (e) => { if (e.ctrlKey || e.metaKey) { e.preventDefault(); const delta = e.deltaY > 0 ? 1 / scaleStep : scaleStep; updateScale(scale * delta); } }); }); } /** * Initialize collapsible sections with persistent state */ initCollapsibleSections() { const collapsibles = document.querySelectorAll('.collapsible-text-container'); collapsibles.forEach(container => { const toggle = container.querySelector('.collapsible-toggle'); const content = container.querySelector('.collapse'); if (!toggle || !content) return; // Get the field key from the parent display-field or collapse ID const displayField = container.closest('.display-field'); const fieldKey = displayField ? displayField.dataset.fieldKey : content.id; const storageKey = `potato_collapse_${fieldKey}`; // Restore state from localStorage const savedState = localStorage.getItem(storageKey); if (savedState !== null) { const shouldBeExpanded = savedState === 'expanded'; const isCurrentlyExpanded = content.classList.contains('show'); if (shouldBeExpanded !== isCurrentlyExpanded) { // Need to toggle the state if (shouldBeExpanded) { content.classList.add('show'); toggle.setAttribute('aria-expanded', 'true'); } else { content.classList.remove('show'); toggle.setAttribute('aria-expanded', 'false'); } } } // Save state when toggled content.addEventListener('shown.bs.collapse', () => { toggle.setAttribute('aria-expanded', 'true'); localStorage.setItem(storageKey, 'expanded'); }); content.addEventListener('hidden.bs.collapse', () => { toggle.setAttribute('aria-expanded', 'false'); localStorage.setItem(storageKey, 'collapsed'); }); }); } /** * Initialize span target fields */ initSpanTargets() { // Span targets need special handling for the span annotation system // This sets up the necessary attributes and event listeners this.spanTargets.forEach(key => { const field = this.displayFields[key]; if (!field) return; const textContent = field.element.querySelector('.text-content'); if (textContent) { // Ensure the text content has the necessary attributes // for the span annotation system to work if (!textContent.id) { textContent.id = `text-content-${key}`; } } }); } /** * Initialize per-turn rating widgets in dialogue displays. * Supports both single-schema and multi-schema (multi-dimension) per-turn ratings. * Each schema stores its values in its own hidden input. */ initPerTurnRatings() { const containers = document.querySelectorAll('.has-per-turn-ratings'); containers.forEach(container => { const fieldKey = container.dataset.fieldKey; // Collect all hidden inputs keyed by schema name const hiddenInputs = {}; container.querySelectorAll('.per-turn-hidden').forEach(input => { const schemaName = input.dataset.schemaName; if (schemaName) { hiddenInputs[schemaName] = input; } }); // Fall back to single hidden input (legacy format) const singleHiddenInput = container.querySelector('.annotation-data-input:not(.per-turn-hidden)'); // Rating values keyed by schema: {schemaName: {turnIndex: value}} const ratingValuesBySchema = {}; // Handle click on rating values container.querySelectorAll('.ptr-value').forEach(el => { el.addEventListener('click', (e) => { e.preventDefault(); const turn = el.dataset.turn; const value = parseInt(el.dataset.value, 10); const schema = el.dataset.schema || ''; // Initialize schema bucket if (!ratingValuesBySchema[schema]) { ratingValuesBySchema[schema] = {}; } const schemaValues = ratingValuesBySchema[schema]; // Toggle: clicking same value deselects if (schemaValues[turn] === value) { delete schemaValues[turn]; } else { schemaValues[turn] = value; } // Update visual state for this turn + schema combination const selector = `.ptr-value[data-turn="${turn}"][data-schema="${schema}"]`; container.querySelectorAll(selector).forEach(v => { const vVal = parseInt(v.dataset.value, 10); if (schemaValues[turn] && vVal <= schemaValues[turn]) { v.classList.add('ptr-selected'); } else { v.classList.remove('ptr-selected'); } }); // Update the corresponding hidden input if (schema && hiddenInputs[schema]) { hiddenInputs[schema].value = JSON.stringify(schemaValues); } else if (singleHiddenInput) { singleHiddenInput.value = JSON.stringify(schemaValues); } console.log('[InstanceDisplay] Per-turn rating:', fieldKey, schema ? `schema=${schema}` : '', 'turn', turn, '=', schemaValues[turn] || 'cleared'); }); }); }); } /** * Get a display field by key * @param {string} key - The field key * @returns {Object|null} The field info or null */ getField(key) { return this.displayFields[key] || null; } /** * Get the source URL for a field (for images/videos/audio) * @param {string} key - The field key * @returns {string|null} The source URL or null */ getSourceUrl(key) { const field = this.getField(key); if (!field) return null; const sourceElement = field.element.querySelector('[data-source-url]'); return sourceElement ? sourceElement.dataset.sourceUrl : null; } /** * Get all span target field keys * @returns {string[]} Array of field keys that are span targets */ getSpanTargets() { return [...this.spanTargets]; } /** * Check if multiple span targets exist (multi-span mode) * @returns {boolean} */ isMultiSpanMode() { return this.spanTargets.length > 1; } /** * Get the primary text content element for span annotation * Falls back to legacy #text-content if instance_display not configured * @returns {HTMLElement|null} */ getPrimaryTextElement() { // First check for instance_display span targets if (this.spanTargets.length > 0) { const key = this.spanTargets[0]; const field = this.displayFields[key]; if (field) { return field.element.querySelector('.text-content'); } } // Fall back to legacy element return document.getElementById('text-content'); } } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { window.instanceDisplayManager = new InstanceDisplayManager(); }); } else { window.instanceDisplayManager = new InstanceDisplayManager(); } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = InstanceDisplayManager; } })();