/** * InputOutputDisplay - Displays model inputs and outputs * Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6 */ class InputOutputDisplay { /** * @param {string} containerId - ID of the container element */ constructor(containerId) { this._containerId = containerId; this._container = document.getElementById(containerId); if (!this._container) { console.warn(`[InputOutputDisplay] Container #${containerId} not found`); } } // ─── Private ────────────────────────────────────────────────────────────── /** * Escape HTML special characters. * @param {string} str * @returns {string} */ _escapeHtml(str) { const div = document.createElement('div'); div.appendChild(document.createTextNode(String(str))); return div.innerHTML; } /** * Format a tensor shape array to a human-readable string. * Dynamic dimensions (negative numbers or strings) are shown as-is. * e.g. [1, 3, 224, 224] → "[1, 3, 224, 224]" * [-1, 3, 224, 224] → "[-1, 3, 224, 224]" * ["batch", 3, 224, 224] → "[batch, 3, 224, 224]" * @param {Array} shape * @returns {string} */ _formatShape(shape) { if (!shape || shape.length === 0) return '[]'; const dims = shape.map((d) => { if (typeof d === 'string') return d; if (d === -1 || d < 0) return 'dynamic'; return String(d); }); return `[${dims.join(', ')}]`; } /** * Resolve a numeric data type to its string name. * @param {number|string} dataType * @returns {string} */ _resolveDataType(dataType) { if (typeof dataType === 'string') return dataType; if (CONFIG && CONFIG.DATA_TYPES && CONFIG.DATA_TYPES[dataType]) { return CONFIG.DATA_TYPES[dataType]; } return String(dataType); } /** * Build the HTML for a single tensor entry (input or output). * @param {TensorInfo} tensor * @param {'input'|'output'} kind * @returns {string} */ _buildTensorItem(tensor, kind) { const badgeClass = kind === 'input' ? 'bg-primary' : 'bg-success'; const badgeLabel = kind === 'input' ? 'Input' : 'Output'; const shape = this._formatShape(tensor.shape); const dtype = this._resolveDataType(tensor.dataType); return `
${this._escapeHtml(tensor.name)} ${badgeLabel}
${this._escapeHtml(shape)} ${this._escapeHtml(dtype)}
${tensor.description ? `
${this._escapeHtml(tensor.description)}
` : ''}
`; } /** * Attach click (and keyboard) handlers to tensor items. */ _attachItemHandlers() { if (!this._container) return; const items = this._container.querySelectorAll('.tensor-item'); items.forEach((item) => { const handler = () => { const name = item.dataset.tensorName; if (name && window.EventBus) { window.EventBus.emit(CONFIG.EVENTS.NODE_SELECTED, { nodeId: name, source: 'inputOutput' }); } if (name && window.StateManager) { window.StateManager.setSelectedNodeId(name); } }; item.addEventListener('click', handler); item.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } }); }); } // ─── Public API ─────────────────────────────────────────────────────────── /** * Render inputs and outputs for a parsed model. * @param {ParsedModel} model */ render(model) { if (!this._container) return; const inputs = (model && model.inputs) ? model.inputs : []; const outputs = (model && model.outputs) ? model.outputs : []; if (inputs.length === 0 && outputs.length === 0) { this._container.innerHTML = '

No inputs or outputs found.

'; return; } let html = ''; if (inputs.length > 0) { html += `
Inputs (${inputs.length})
`; html += inputs.map((t) => this._buildTensorItem(t, 'input')).join(''); } if (outputs.length > 0) { html += `
Outputs (${outputs.length})
`; html += outputs.map((t) => this._buildTensorItem(t, 'output')).join(''); } this._container.innerHTML = html; this._attachItemHandlers(); } /** * Clear the display. */ clear() { if (!this._container) return; this._container.innerHTML = '

Select a model to view inputs and outputs

'; } } window.InputOutputDisplay = InputOutputDisplay;