Spaces:
Running
Running
| /** | |
| * ComparisonView - Side-by-side model comparison | |
| * Requirements: 14.1, 14.2, 14.3, 14.4, 14.5 | |
| */ | |
| class ComparisonView { | |
| /** | |
| * @param {string} containerId - ID of the container element (#modelDetailsContainer) | |
| */ | |
| constructor(containerId) { | |
| this._containerId = containerId; | |
| this._container = document.getElementById(containerId); | |
| this._originalContent = null; | |
| if (!this._container) { | |
| console.warn(`[ComparisonView] Container #${containerId} not found`); | |
| } | |
| } | |
| // βββ Private Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Escape HTML special characters. | |
| * @param {string} str | |
| * @returns {string} | |
| */ | |
| _escapeHtml(str) { | |
| const div = document.createElement('div'); | |
| div.appendChild(document.createTextNode(String(str == null ? '' : str))); | |
| return div.innerHTML; | |
| } | |
| /** | |
| * Return display value or a "Not available" placeholder. | |
| * @param {*} value | |
| * @returns {string} | |
| */ | |
| _displayValue(value) { | |
| if (value === null || value === undefined || value === '') { | |
| return '<span class="text-muted fst-italic">Not available</span>'; | |
| } | |
| return this._escapeHtml(String(value)); | |
| } | |
| /** | |
| * Format bytes to human-readable string. | |
| * @param {number} bytes | |
| * @returns {string} | |
| */ | |
| _formatBytes(bytes) { | |
| if (!bytes || bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`; | |
| } | |
| /** | |
| * Format a tensor shape array. | |
| * @param {Array<number|string>} shape | |
| * @returns {string} | |
| */ | |
| _formatShape(shape) { | |
| if (!shape || shape.length === 0) return '[]'; | |
| const dims = shape.map((d) => { | |
| if (typeof d === 'string') return d; | |
| if (d < 0) return 'dynamic'; | |
| return String(d); | |
| }); | |
| return `[${dims.join(', ')}]`; | |
| } | |
| /** | |
| * Resolve numeric data type to string. | |
| * @param {number|string} dataType | |
| * @returns {string} | |
| */ | |
| _resolveDataType(dataType) { | |
| if (typeof dataType === 'string') return dataType; | |
| if (window.CONFIG && CONFIG.DATA_TYPES && CONFIG.DATA_TYPES[dataType]) { | |
| return CONFIG.DATA_TYPES[dataType]; | |
| } | |
| return String(dataType == null ? '' : dataType); | |
| } | |
| /** | |
| * Wrap a value in a highlight span if it differs from the other value. | |
| * @param {string} val - escaped/display value for this model | |
| * @param {string} otherVal - escaped/display value for the other model | |
| * @returns {string} | |
| */ | |
| _maybeHighlight(val, otherVal) { | |
| if (val !== otherVal) { | |
| return `<span class="comparison-diff">${val}</span>`; | |
| } | |
| return val; | |
| } | |
| /** | |
| * Build a metadata comparison table row. | |
| * @param {string} label | |
| * @param {*} val1 | |
| * @param {*} val2 | |
| * @returns {string} | |
| */ | |
| _metaRow(label, val1, val2) { | |
| const d1 = this._displayValue(val1); | |
| const d2 = this._displayValue(val2); | |
| const isDiff = String(val1 ?? '') !== String(val2 ?? ''); | |
| const c1 = isDiff ? `<span class="comparison-diff">${d1}</span>` : d1; | |
| const c2 = isDiff ? `<span class="comparison-diff">${d2}</span>` : d2; | |
| return ` | |
| <tr> | |
| <th scope="row" class="text-nowrap pe-2 text-secondary small" style="width:30%">${this._escapeHtml(label)}</th> | |
| <td class="pe-2">${c1}</td> | |
| <td>${c2}</td> | |
| </tr>`; | |
| } | |
| /** | |
| * Build metadata section HTML. | |
| * @param {object} meta1 | |
| * @param {object} meta2 | |
| * @returns {string} | |
| */ | |
| _renderMetadataSection(meta1, meta2) { | |
| meta1 = meta1 || {}; | |
| meta2 = meta2 || {}; | |
| const fields = [ | |
| ['Producer Name', meta1.producerName, meta2.producerName], | |
| ['Producer Version', meta1.producerVersion, meta2.producerVersion], | |
| ['Opset Version', meta1.opsetVersion, meta2.opsetVersion], | |
| ['IR Version', meta1.irVersion, meta2.irVersion], | |
| ['File Size', | |
| meta1.fileSize ? this._formatBytes(meta1.fileSize) : null, | |
| meta2.fileSize ? this._formatBytes(meta2.fileSize) : null], | |
| ]; | |
| const rows = fields.map(([label, v1, v2]) => this._metaRow(label, v1, v2)).join(''); | |
| return ` | |
| <div class="card mb-3"> | |
| <div class="card-header py-2"> | |
| <h6 class="mb-0"><i class="fas fa-info-circle me-1"></i>Metadata</h6> | |
| </div> | |
| <div class="card-body p-0"> | |
| <table class="table table-sm table-borderless mb-0"> | |
| <thead class="table-light"> | |
| <tr> | |
| <th style="width:30%"></th> | |
| <th class="text-primary">Model 1</th> | |
| <th class="text-success">Model 2</th> | |
| </tr> | |
| </thead> | |
| <tbody>${rows}</tbody> | |
| </table> | |
| </div> | |
| </div>`; | |
| } | |
| /** | |
| * Serialize a tensor to a comparable string key. | |
| * @param {object} tensor | |
| * @returns {string} | |
| */ | |
| _tensorKey(tensor) { | |
| return `${tensor.name}|${this._formatShape(tensor.shape)}|${this._resolveDataType(tensor.dataType)}`; | |
| } | |
| /** | |
| * Build tensor list rows for comparison. | |
| * @param {Array} tensors1 | |
| * @param {Array} tensors2 | |
| * @param {'input'|'output'} kind | |
| * @returns {string} | |
| */ | |
| _renderTensorSection(tensors1, tensors2, kind) { | |
| tensors1 = tensors1 || []; | |
| tensors2 = tensors2 || []; | |
| const keys1 = new Set(tensors1.map((t) => this._tensorKey(t))); | |
| const keys2 = new Set(tensors2.map((t) => this._tensorKey(t))); | |
| const allNames = new Set([ | |
| ...tensors1.map((t) => t.name), | |
| ...tensors2.map((t) => t.name), | |
| ]); | |
| const map1 = Object.fromEntries(tensors1.map((t) => [t.name, t])); | |
| const map2 = Object.fromEntries(tensors2.map((t) => [t.name, t])); | |
| const title = kind === 'input' ? 'Inputs' : 'Outputs'; | |
| const icon = kind === 'input' ? 'fa-arrow-right' : 'fa-arrow-left'; | |
| const headerClass = kind === 'input' ? 'text-primary' : 'text-success'; | |
| const rows = [...allNames].map((name) => { | |
| const t1 = map1[name]; | |
| const t2 = map2[name]; | |
| const shape1 = t1 ? this._formatShape(t1.shape) : 'β'; | |
| const shape2 = t2 ? this._formatShape(t2.shape) : 'β'; | |
| const dtype1 = t1 ? this._resolveDataType(t1.dataType) : 'β'; | |
| const dtype2 = t2 ? this._resolveDataType(t2.dataType) : 'β'; | |
| const shapeDiff = shape1 !== shape2; | |
| const dtypeDiff = dtype1 !== dtype2; | |
| const missingIn1 = !t1; | |
| const missingIn2 = !t2; | |
| const cell1 = missingIn1 | |
| ? '<span class="text-muted fst-italic">Not present</span>' | |
| : `<span class="${shapeDiff ? 'comparison-diff' : ''}">${this._escapeHtml(shape1)}</span> | |
| <span class="text-muted mx-1">Β·</span> | |
| <span class="${dtypeDiff ? 'comparison-diff' : ''}">${this._escapeHtml(dtype1)}</span>`; | |
| const cell2 = missingIn2 | |
| ? '<span class="text-muted fst-italic">Not present</span>' | |
| : `<span class="${shapeDiff ? 'comparison-diff' : ''}">${this._escapeHtml(shape2)}</span> | |
| <span class="text-muted mx-1">Β·</span> | |
| <span class="${dtypeDiff ? 'comparison-diff' : ''}">${this._escapeHtml(dtype2)}</span>`; | |
| const rowClass = (missingIn1 || missingIn2 || shapeDiff || dtypeDiff) ? 'table-warning' : ''; | |
| return ` | |
| <tr class="${rowClass}"> | |
| <td class="text-truncate small fw-medium" style="max-width:160px" title="${this._escapeHtml(name)}">${this._escapeHtml(name)}</td> | |
| <td class="small">${cell1}</td> | |
| <td class="small">${cell2}</td> | |
| </tr>`; | |
| }).join(''); | |
| const emptyMsg = allNames.size === 0 | |
| ? `<tr><td colspan="3" class="text-muted fst-italic text-center">No ${title.toLowerCase()} found</td></tr>` | |
| : ''; | |
| return ` | |
| <div class="card mb-3"> | |
| <div class="card-header py-2"> | |
| <h6 class="mb-0 ${headerClass}"><i class="fas ${icon} me-1"></i>${title}</h6> | |
| </div> | |
| <div class="card-body p-0"> | |
| <table class="table table-sm table-borderless mb-0"> | |
| <thead class="table-light"> | |
| <tr> | |
| <th style="width:35%">Name</th> | |
| <th class="text-primary">Model 1</th> | |
| <th class="text-success">Model 2</th> | |
| </tr> | |
| </thead> | |
| <tbody>${rows}${emptyMsg}</tbody> | |
| </table> | |
| </div> | |
| </div>`; | |
| } | |
| // βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Enter comparison mode: save current DOM, render diff view. | |
| * @param {ParsedModel} model1 | |
| * @param {ParsedModel} model2 | |
| */ | |
| enter(model1, model2) { | |
| if (!this._container) return; | |
| // Save original content so we can restore it on exit | |
| this._originalContent = this._container.innerHTML; | |
| // Update state | |
| if (window.StateManager) { | |
| StateManager.setViewMode('comparison'); | |
| StateManager.setComparisonModel(model2); | |
| } | |
| this.renderDiff(model1, model2); | |
| // Emit event | |
| if (window.EventBus) { | |
| EventBus.emit(CONFIG.EVENTS.COMPARISON_STARTED, { model1, model2 }); | |
| } | |
| } | |
| /** | |
| * Exit comparison mode: restore original single-model view. | |
| */ | |
| exit() { | |
| if (!this._container) return; | |
| if (this._originalContent !== null) { | |
| this._container.innerHTML = this._originalContent; | |
| this._originalContent = null; | |
| } | |
| // Update state | |
| if (window.StateManager) { | |
| StateManager.setViewMode('single'); | |
| StateManager.setComparisonModel(null); | |
| } | |
| // Emit event | |
| if (window.EventBus) { | |
| EventBus.emit(CONFIG.EVENTS.COMPARISON_ENDED, {}); | |
| } | |
| } | |
| /** | |
| * Render the diff view for two models. | |
| * @param {ParsedModel} model1 | |
| * @param {ParsedModel} model2 | |
| */ | |
| renderDiff(model1, model2) { | |
| if (!this._container) return; | |
| const meta1 = (model1 && model1.metadata) ? model1.metadata : {}; | |
| const meta2 = (model2 && model2.metadata) ? model2.metadata : {}; | |
| const name1 = meta1.fileName || 'Model 1'; | |
| const name2 = meta2.fileName || 'Model 2'; | |
| const inputs1 = (model1 && model1.inputs) ? model1.inputs : []; | |
| const inputs2 = (model2 && model2.inputs) ? model2.inputs : []; | |
| const outputs1 = (model1 && model1.outputs) ? model1.outputs : []; | |
| const outputs2 = (model2 && model2.outputs) ? model2.outputs : []; | |
| const html = ` | |
| <div id="comparisonViewRoot" class="comparison-view"> | |
| <!-- Header bar --> | |
| <div class="d-flex align-items-center justify-content-between mb-3 p-3 bg-light border rounded"> | |
| <div class="d-flex align-items-center gap-3"> | |
| <i class="fas fa-columns text-secondary fs-5"></i> | |
| <div> | |
| <span class="fw-semibold text-primary me-2"> | |
| <i class="fas fa-circle me-1" style="font-size:0.6rem"></i>Model 1: | |
| </span> | |
| <span class="text-dark">${this._escapeHtml(name1)}</span> | |
| </div> | |
| <span class="text-muted">vs</span> | |
| <div> | |
| <span class="fw-semibold text-success me-2"> | |
| <i class="fas fa-circle me-1" style="font-size:0.6rem"></i>Model 2: | |
| </span> | |
| <span class="text-dark">${this._escapeHtml(name2)}</span> | |
| </div> | |
| </div> | |
| <button id="exitComparisonBtn" class="btn btn-outline-secondary btn-sm" aria-label="Exit comparison mode"> | |
| <i class="fas fa-times me-1"></i>Exit Comparison | |
| </button> | |
| </div> | |
| <!-- Legend --> | |
| <div class="mb-3 small text-muted"> | |
| <span class="comparison-diff px-1 me-1">highlighted</span> = values differ between models | |
| </div> | |
| <!-- Metadata comparison --> | |
| ${this._renderMetadataSection(meta1, meta2)} | |
| <!-- Inputs comparison --> | |
| ${this._renderTensorSection(inputs1, inputs2, 'input')} | |
| <!-- Outputs comparison --> | |
| ${this._renderTensorSection(outputs1, outputs2, 'output')} | |
| </div>`; | |
| this._container.innerHTML = html; | |
| // Wire up exit button | |
| const exitBtn = this._container.querySelector('#exitComparisonBtn'); | |
| if (exitBtn) { | |
| exitBtn.addEventListener('click', () => this.exit()); | |
| } | |
| } | |
| } | |
| window.ComparisonView = ComparisonView; | |