/**
* 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 = `
${totalOperators}
Operators
${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' : ''}
`;
}
wrapper.innerHTML = `
`;
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 = `
`;
// 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;