/** * TFLiteViewer - Displays TFLite file content * Shows summary, operator list, and tensor list for parsed TFLite files. * Supports search, sort, and dtype color badges. * Requirements: 3.1, 3.2, 3.3, 3.4, 4.1, 4.2, 4.3, 5.1, 5.2, 5.3, 5.4, 5.5, 5.6 */ class TFLiteViewer { /** * @param {string} containerId - ID of the main container (modelDetailsContainer) */ constructor(containerId) { this._containerId = containerId; this._container = document.getElementById(containerId); this._data = null; this._fileName = null; this._isVisible = false; // Sort state this._sortField = null; // 'name' or 'size' this._sortAsc = true; // Search state this._searchQuery = ''; this._debounceTimer = null; // Created panel elements this._summaryCard = null; this._operatorsCard = null; this._tensorListCard = null; // Bound handlers for cleanup this._boundHandlers = []; // ONNX panel IDs to hide/show this._onnxPanelSelectors = [ '#graphContainer', '#nodeDetailContainer', '#layerStatsContainer', '#modelComplexityContainer', '#opsetCheckerContainer', '#flopsEstimatorContainer' ]; // IDs of parent cards/wrappers that contain ONNX panels this._onnxCardSelectors = [ '#metadataContainer', '#inputOutputContainer', '#initializerContainer' ]; // Dtype color map for TFLite types this._dtypeColors = { 'FLOAT32': '#198754', // green 'FLOAT16': '#fd7e14', // orange 'BFLOAT16': '#e35d6a', // pink 'FLOAT64': '#0d6efd', // blue 'INT8': '#6f42c1', // purple 'INT16': '#6f42c1', 'INT32': '#6f42c1', 'INT64': '#6f42c1', 'INT4': '#6f42c1', 'UINT8': '#20c997', // teal 'UINT16': '#20c997', 'UINT32': '#20c997', 'UINT64': '#20c997', 'BOOL': '#6c757d', // gray 'STRING': '#dc3545', // red 'COMPLEX64': '#0dcaf0', // cyan 'COMPLEX128': '#0dcaf0' }; if (!this._container) { console.warn(`[TFLiteViewer] Container #${containerId} not found`); } } // ─── Public API ─────────────────────────────────────────────────────────── /** * Render TFLite data into the container. * @param {TFLiteModelData} data - Parsed TFLite model data * @param {string} fileName */ render(data, fileName) { if (!this._container) return; if (!data) { console.warn('[TFLiteViewer] No data provided'); return; } this._data = data; this._fileName = fileName; this._isVisible = true; this._searchQuery = ''; this._sortField = null; this._sortAsc = true; // Hide all ONNX-specific panels (Req 3.3) this._hideOnnxPanels(); // Also hide SafeTensors panels if visible this._hideSafeTensorsPanels(); // Remove previous TFLite panels this._removePanels(); const tensors = data.tensors || []; const operators = data.operators || []; // Create Summary card (Req 3.1, 3.2) this._summaryCard = this._createSummaryCard(data, tensors, operators); this._container.prepend(this._summaryCard); // Create Operator List card (Req 4.1, 4.2, 4.3) this._operatorsCard = this._createOperatorsCard(operators); this._summaryCard.after(this._operatorsCard); // Create Tensor List card (Req 5.1, 5.2, 5.3, 5.4, 5.5, 5.6) this._tensorListCard = this._createTensorListCard(tensors); this._operatorsCard.after(this._tensorListCard); } /** * Hide TFLite panels and restore ONNX panels. (Req 3.4) */ hide() { if (!this._isVisible) return; this._isVisible = false; this._removePanels(); this._showOnnxPanels(); } /** * Destroy: clean up event listeners and panels. */ destroy() { this._clearDebounce(); this._removePanels(); this._boundHandlers.forEach(({ el, event, fn }) => { el.removeEventListener(event, fn); }); this._boundHandlers = []; this._data = null; this._fileName = null; this._isVisible = false; } // ─── Private: Panel Management ──────────────────────────────────────────── _removePanels() { [this._summaryCard, this._operatorsCard, this._tensorListCard].forEach(el => { if (el && el.parentNode) el.parentNode.removeChild(el); }); this._summaryCard = null; this._operatorsCard = null; this._tensorListCard = null; this._clearDebounce(); } _hideOnnxPanels() { this._onnxPanelSelectors.forEach(sel => { const el = document.querySelector(sel); if (el) { const wrapper = el.closest('.col-12, .col-md-6, .col-md-4, .col-md-8, .card'); if (wrapper) { wrapper.setAttribute('data-tflite-hidden', 'true'); wrapper.style.display = 'none'; } } }); this._onnxCardSelectors.forEach(sel => { const el = document.querySelector(sel); if (el) { const wrapper = el.closest('.col-12, .col-md-6'); if (wrapper) { wrapper.setAttribute('data-tflite-hidden', 'true'); wrapper.style.display = 'none'; } } }); } _showOnnxPanels() { const hidden = document.querySelectorAll('[data-tflite-hidden="true"]'); hidden.forEach(el => { el.removeAttribute('data-tflite-hidden'); el.style.display = ''; }); } _hideSafeTensorsPanels() { const stPanels = document.querySelectorAll('[data-st-panel]'); stPanels.forEach(el => { el.style.display = 'none'; }); } // ─── Private: Summary Card (Req 3.1, 3.2) ───────────────────────────────── _createSummaryCard(data, tensors, operators) { const wrapper = document.createElement('div'); wrapper.className = 'col-12 mb-4'; wrapper.setAttribute('data-tflite-panel', 'summary'); const totalOperators = operators.length; const totalTensors = tensors.length; const totalParams = tensors.reduce((sum, t) => { if (!t.shape || t.shape.length === 0) return sum; return sum + t.shape.reduce((p, d) => p * d, 1); }, 0); const totalBytes = tensors.reduce((sum, t) => sum + (t.byteSize || 0), 0); const version = data.version != null ? data.version : 'N/A'; const description = data.description || 'N/A'; wrapper.innerHTML = `
TFLite Model Summary — ${this._escapeHtml(this._fileName || '')}
${totalOperators}
Operators
${totalTensors}
Tensors
${this._formatParamCount(totalParams)}
Parameters
${this._formatBytes(totalBytes)}
Memory Footprint
${this._escapeHtml(String(version))}
Version
${this._escapeHtml(String(description))}
Description
`; return wrapper; } // ─── Private: Operators Card (Req 4.1, 4.2, 4.3) ────────────────────────── _createOperatorsCard(operators) { const wrapper = document.createElement('div'); wrapper.className = 'col-12 mb-4'; wrapper.setAttribute('data-tflite-panel', 'operators'); // Count occurrences of each operator const opCounts = {}; operators.forEach(op => { const name = op.opcodeName || 'UNKNOWN'; opCounts[name] = (opCounts[name] || 0) + 1; }); const uniqueOps = Object.keys(opCounts).length; let bodyHtml; if (operators.length === 0) { // Req 4.3: empty operators bodyHtml = '

Không tìm thấy operator nào

'; } else { const rows = Object.entries(opCounts) .sort((a, b) => b[1] - a[1]) .map(([name, count]) => { return `${this._escapeHtml(name)}${count}`; }).join(''); bodyHtml = `
${uniqueOps} unique operator type${uniqueOps !== 1 ? 's' : ''}
${rows}
Operator Count
`; } wrapper.innerHTML = `
Operator List
${bodyHtml}
`; return wrapper; } // ─── Private: Tensor List Card (Req 5.1, 5.2, 5.3, 5.4, 5.5, 5.6) ──────── _createTensorListCard(tensors) { const wrapper = document.createElement('div'); wrapper.className = 'col-12 mb-4'; wrapper.setAttribute('data-tflite-panel', 'tensors'); wrapper.innerHTML = `
Tensor List
Name Shape Dtype Size
`; // Render initial tensor rows this._renderTensorRows(wrapper, tensors); // Attach search handler (Req 5.2 — 300ms debounce) const searchInput = wrapper.querySelector('.tflite-tensor-search'); if (searchInput) { const handler = () => { this._clearDebounce(); this._debounceTimer = setTimeout(() => { this._searchQuery = searchInput.value; this._renderTensorRows(wrapper, this._getFilteredSortedTensors()); }, 300); }; searchInput.addEventListener('input', handler); this._boundHandlers.push({ el: searchInput, event: 'input', fn: handler }); } // Attach sort handlers (Req 5.3) const sortNameBtn = wrapper.querySelector('.tflite-sort-name'); const sortSizeBtn = wrapper.querySelector('.tflite-sort-size'); if (sortNameBtn) { const handler = () => { if (this._sortField === 'name') { this._sortAsc = !this._sortAsc; } else { this._sortField = 'name'; this._sortAsc = true; } this._updateSortButtons(wrapper); this._renderTensorRows(wrapper, this._getFilteredSortedTensors()); }; sortNameBtn.addEventListener('click', handler); this._boundHandlers.push({ el: sortNameBtn, event: 'click', fn: handler }); } if (sortSizeBtn) { const handler = () => { if (this._sortField === 'size') { this._sortAsc = !this._sortAsc; } else { this._sortField = 'size'; this._sortAsc = false; // default: largest first } this._updateSortButtons(wrapper); this._renderTensorRows(wrapper, this._getFilteredSortedTensors()); }; sortSizeBtn.addEventListener('click', handler); this._boundHandlers.push({ el: sortSizeBtn, event: 'click', fn: handler }); } return wrapper; } _getFilteredSortedTensors() { let tensors = (this._data && this._data.tensors) || []; // Filter by search query (Req 5.2) if (this._searchQuery.trim()) { const q = this._searchQuery.trim().toLowerCase(); tensors = tensors.filter(t => (t.name || '').toLowerCase().includes(q)); } // Sort (Req 5.3) if (this._sortField === 'name') { tensors = tensors.slice().sort((a, b) => { const cmp = (a.name || '').localeCompare(b.name || ''); return this._sortAsc ? cmp : -cmp; }); } else if (this._sortField === 'size') { tensors = tensors.slice().sort((a, b) => { const cmp = (a.byteSize || 0) - (b.byteSize || 0); return this._sortAsc ? cmp : -cmp; }); } return tensors; } _renderTensorRows(wrapper, tensors) { const tbody = wrapper.querySelector('.tflite-tensor-tbody'); const countEl = wrapper.querySelector('.tflite-tensor-count'); const allTensors = (this._data && this._data.tensors) || []; if (!tbody) return; // Update count display (Req 5.5) if (countEl) { if (this._searchQuery.trim()) { countEl.textContent = `Showing ${tensors.length} / ${allTensors.length} tensors`; } else { countEl.textContent = `${allTensors.length} tensor${allTensors.length !== 1 ? 's' : ''}`; } } // Empty state: no tensors at all if (allTensors.length === 0) { tbody.innerHTML = 'Không có tensor nào'; return; } // Empty state: search with no results (Req 5.4) if (tensors.length === 0 && this._searchQuery.trim()) { tbody.innerHTML = 'Không tìm thấy tensor phù hợp'; return; } const rows = tensors.map(t => { const color = this._dtypeColors[t.dtype] || '#6c757d'; const shapeStr = t.shape ? `[${t.shape.join(', ')}]` : '[]'; const sizeStr = this._formatBytes(t.byteSize); return ` ${this._escapeHtml(t.name || '')} ${this._escapeHtml(shapeStr)} ${this._escapeHtml(t.dtype || 'UNKNOWN')} ${sizeStr} `; }).join(''); tbody.innerHTML = rows; } _updateSortButtons(wrapper) { const nameBtn = wrapper.querySelector('.tflite-sort-name'); const sizeBtn = wrapper.querySelector('.tflite-sort-size'); [nameBtn, sizeBtn].forEach(btn => { if (btn) { btn.classList.remove('btn-secondary'); btn.classList.add('btn-outline-secondary'); } }); if (this._sortField === 'name' && nameBtn) { nameBtn.classList.remove('btn-outline-secondary'); nameBtn.classList.add('btn-secondary'); const icon = nameBtn.querySelector('i'); if (icon) { icon.className = this._sortAsc ? 'fas fa-sort-alpha-down me-1' : 'fas fa-sort-alpha-up me-1'; } } if (this._sortField === 'size' && sizeBtn) { sizeBtn.classList.remove('btn-outline-secondary'); sizeBtn.classList.add('btn-secondary'); const icon = sizeBtn.querySelector('i'); if (icon) { icon.className = this._sortAsc ? 'fas fa-sort-amount-up me-1' : 'fas fa-sort-amount-down me-1'; } } } // ─── Private: Formatters ────────────────────────────────────────────────── /** * Format parameter count: 25600000 → "25.6M", 1200000000 → "1.2B" * @param {number} count * @returns {string} */ _formatParamCount(count) { if (count == null || isNaN(count) || count === 0) return '0'; const abs = Math.abs(count); if (abs >= 1e9) return (count / 1e9).toFixed(1).replace(/\.0$/, '') + 'B'; if (abs >= 1e6) return (count / 1e6).toFixed(1).replace(/\.0$/, '') + 'M'; if (abs >= 1e3) return (count / 1e3).toFixed(1).replace(/\.0$/, '') + 'K'; return String(count); } /** * Format byte size: 512000000 → "488.28 MB" * @param {number} bytes * @returns {string} */ _formatBytes(bytes) { if (bytes == null || isNaN(bytes) || bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.min(Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)), sizes.length - 1); return (bytes / Math.pow(k, i)).toFixed(2).replace(/\.00$/, '') + ' ' + sizes[i]; } /** * 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; } _clearDebounce() { if (this._debounceTimer) { clearTimeout(this._debounceTimer); this._debounceTimer = null; } } } window.TFLiteViewer = TFLiteViewer;