/** * ONNX Model Explorer - Application Integration Hub * Wires all components together and manages the startup flow. * Requirements: 1.1, 2.1, 3.1, 4.1, 5.1, 6.1, 8.1–8.6, 16–25 */ (function () { 'use strict'; // ─── Component References ───────────────────────────────────────────────── let modelLoader = null; let onnxParser = null; let graphProcessor = null; let modelListDisplay = null; let metadataDisplay = null; let inputOutputDisplay = null; let initializerDisplay = null; let graphVisualizer = null; let fileUploadHandler = null; let searchFilter = null; let exportHandler = null; let nodeDetailPanel = null; let graphMinimap = null; let graphPathHighlighter = null; let layerStats = null; let modelComplexity = null; let tensorShapeInspector = null; let opsetChecker = null; let recentModels = null; let graphExport = null; let shareableURL = null; let fullscreenManager = null; let sidebarToggle = null; let helpTooltip = null; let guidedTour = null; let graphSearch = null; let graphLayoutSwitcher = null; let nodeGrouping = null; let graphAnnotation = null; let flopsEstimator = null; let languageSwitcher = null; let safeTensorsParser = null; let safeTensorsViewer = null; let tfliteParser = null; let tfliteViewer = null; let printHandler = null; // ─── Performance: In-Memory Parsed Model Cache ──────────────────────────── // Keyed by model path; avoids re-parsing the same model on repeated selection. const _parsedModelCache = new Map(); const _CACHE_MAX_SIZE = (typeof CONFIG !== 'undefined' && CONFIG.PERFORMANCE) ? CONFIG.PERFORMANCE.CACHE_SIZE : 10; // ─── Startup Flow ───────────────────────────────────────────────────────── /** * Main entry point – runs after DOMContentLoaded. */ async function init() { console.log('[App] Initializing ONNX Model Explorer…'); // 1. Initialize core services _initCoreServices(); // 2. Initialize UI components _initUIComponents(); // 3. Wire EventBus → component handlers _wireEvents(); // 4. Load model list and render it await _loadAndRenderModelList(); // 5. Restore user preferences (zoom level, etc.) _restoreUserPreferences(); // 6. Restore previously selected model (if any) _restoreSelection(); console.log('[App] Ready.'); } // ─── Initialization Helpers ─────────────────────────────────────────────── /** * Instantiate core business-logic services. */ function _initCoreServices() { // ModelLoader if (window.ModelLoader) { modelLoader = new window.ModelLoader(); } else { console.warn('[App] ModelLoader not available'); } // ONNXParser if (window.ONNXParser) { onnxParser = new window.ONNXParser(); } else { console.warn('[App] ONNXParser not available'); } // GraphProcessor if (window.GraphProcessor) { graphProcessor = new window.GraphProcessor(); } else { console.warn('[App] GraphProcessor not available'); } // GraphPathHighlighter if (window.GraphPathHighlighter) { graphPathHighlighter = new window.GraphPathHighlighter(); } // ShareableURL if (window.ShareableURL) { shareableURL = new window.ShareableURL(); } // SafeTensorsParser if (window.SafeTensorsParser) { safeTensorsParser = new window.SafeTensorsParser(); } else { console.warn('[App] SafeTensorsParser not available'); } // TFLiteParser if (window.TFLiteParser) { tfliteParser = new window.TFLiteParser(); } else { console.warn('[App] TFLiteParser not available'); } // Warn if protobuf.js is missing (graceful degradation) if (typeof protobuf === 'undefined') { console.warn('[App] protobuf.js not loaded – model parsing will be unavailable.'); if (window.ErrorHandler) { window.ErrorHandler.handleWarning( 'protobuf.js library could not be loaded. Model parsing is unavailable.', 'App' ); } } // Warn if Cytoscape is missing if (typeof cytoscape === 'undefined') { console.warn('[App] Cytoscape.js not loaded – graph visualization will be unavailable.'); } } /** * Instantiate and configure all UI components. */ function _initUIComponents() { // ErrorDisplay subscribes to StateManager.error automatically (see errorDisplay.js) // ModelListDisplay if (window.ModelListDisplay) { modelListDisplay = new window.ModelListDisplay('modelListContainer'); } // MetadataDisplay if (window.MetadataDisplay) { metadataDisplay = new window.MetadataDisplay('metadataContainer'); } // InputOutputDisplay if (window.InputOutputDisplay) { inputOutputDisplay = new window.InputOutputDisplay('inputOutputContainer'); } // InitializerDisplay if (window.InitializerDisplay) { initializerDisplay = new window.InitializerDisplay('initializerContainer'); } // GraphVisualizer if (window.GraphVisualizer) { graphVisualizer = new window.GraphVisualizer(); } // FileUploadHandler if (window.FileUploadHandler) { fileUploadHandler = new window.FileUploadHandler('uploadBtn', 'fileInput', 'app'); } // SearchFilter if (window.SearchFilter) { searchFilter = new window.SearchFilter('searchInput'); } // ExportHandler if (window.ExportHandler) { exportHandler = new window.ExportHandler('exportBtn'); } // NodeDetailPanel if (window.NodeDetailPanel) { nodeDetailPanel = new window.NodeDetailPanel('nodeDetailContainer'); } // GraphMinimap if (window.GraphMinimap) { graphMinimap = new window.GraphMinimap('graphContainer'); } // LayerStats if (window.LayerStats) { layerStats = new window.LayerStats('layerStatsContainer'); } // ModelComplexity if (window.ModelComplexity) { modelComplexity = new window.ModelComplexity('modelComplexityContainer'); } // TensorShapeInspector if (window.TensorShapeInspector) { tensorShapeInspector = new window.TensorShapeInspector(); } // OpsetChecker if (window.OpsetChecker) { opsetChecker = new window.OpsetChecker('opsetCheckerContainer'); } // RecentModels if (window.RecentModels) { recentModels = new window.RecentModels('recentModelsContainer'); } // GraphExport if (window.GraphExport) { graphExport = new window.GraphExport('graphExportContainer'); } // FullscreenManager if (window.FullscreenManager) { fullscreenManager = new window.FullscreenManager('#graphContainer'); fullscreenManager.init(); } // SidebarToggle if (window.SidebarToggle) { sidebarToggle = new window.SidebarToggle('.col-lg-3.col-md-4', '.col-lg-9.col-md-8'); sidebarToggle.init(); } // HelpTooltip if (window.HelpTooltip) { helpTooltip = new window.HelpTooltip(); helpTooltip.init(); } // LanguageSwitcher (before GuidedTour so tour button picks up correct lang) if (window.LanguageSwitcher) { languageSwitcher = new window.LanguageSwitcher(); languageSwitcher.init(); } // GuidedTour if (window.GuidedTour) { guidedTour = new window.GuidedTour(); guidedTour.init(); } // GraphSearch if (window.GraphSearch) { graphSearch = new window.GraphSearch('#graphContainer'); } // GraphLayoutSwitcher if (window.GraphLayoutSwitcher) { graphLayoutSwitcher = new window.GraphLayoutSwitcher(); } // NodeGrouping if (window.NodeGrouping) { nodeGrouping = new window.NodeGrouping(); } // GraphAnnotation if (window.GraphAnnotation) { graphAnnotation = new window.GraphAnnotation('#graphContainer'); graphAnnotation.init(); } // FlopsEstimator if (window.FlopsEstimator) { flopsEstimator = new window.FlopsEstimator('flopsEstimatorContainer'); } // SafeTensorsViewer if (window.SafeTensorsViewer) { safeTensorsViewer = new window.SafeTensorsViewer('modelDetailsContainer'); } // TFLiteViewer if (window.TFLiteViewer) { tfliteViewer = new window.TFLiteViewer('modelDetailsContainer'); } // PrintHandler if (window.PrintHandler) { printHandler = new window.PrintHandler('printBtn', 'print-header'); } // Subscribe ModelListDisplay to filteredModelList changes if (modelListDisplay && window.StateManager) { window.StateManager.subscribe('filteredModelList', function (filteredList) { modelListDisplay.render(filteredList); }); } } // ─── Event Wiring ───────────────────────────────────────────────────────── /** * Connect EventBus events to component handlers. */ function _wireEvents() { if (!window.EventBus) { console.warn('[App] EventBus not available – events will not be wired'); return; } // model:selected → load + parse + update all displays window.EventBus.on(CONFIG.EVENTS.MODEL_SELECTED, _onModelSelected); // file:uploaded → parse + update all displays window.EventBus.on(CONFIG.EVENTS.FILE_UPLOADED, _onFileUploaded); // node:selected → highlight in graph window.EventBus.on(CONFIG.EVENTS.NODE_SELECTED, _onNodeSelected); // search:updated → re-render model list (SearchFilter already updates // StateManager.filteredModelList; the subscription above handles re-render) // layerstats:highlight → highlight all nodes of that opType in the graph window.EventBus.on('layerstats:highlight', _onLayerStatsHighlight); // Copy Link button → ShareableURL var copyLinkBtn = document.getElementById('copyLinkBtn'); if (copyLinkBtn && shareableURL) { copyLinkBtn.addEventListener('click', function() { shareableURL.copyToClipboard().then(function(ok) { if (ok && window.ErrorDisplay) { window.ErrorDisplay.show('Link copied to clipboard!', 'info'); } }); }); } } // ─── Event Handlers ─────────────────────────────────────────────────────── /** * Handle model:selected event from ModelListDisplay. * Flow: check cache → load file → parse → cache → update StateManager → update all UI components * @param {ModelInfo} model */ async function _onModelSelected(model) { if (!model || !model.path) { console.warn('[App] model:selected fired with invalid model', model); return; } // Hide SafeTensors viewer when selecting an ONNX model from the list if (safeTensorsViewer) { safeTensorsViewer.hide(); } // Hide TFLite viewer when selecting an ONNX model from the list if (tfliteViewer) { tfliteViewer.hide(); } // Re-enable Export button if (exportHandler && exportHandler._exportBtn) { exportHandler._exportBtn.disabled = false; exportHandler._exportBtn.title = ''; } _showLoading(CONFIG.INFO.LOADING_MODEL); try { // 1. Check in-memory cache first (lazy loading: only parse when needed) const cacheKey = model.path; let parsedModel = _parsedModelCache.get(cacheKey); if (parsedModel) { console.log('[App] Cache hit for model:', cacheKey); } else { // 2. Load the model file if (!modelLoader) throw new Error('ModelLoader is not initialized'); const buffer = await modelLoader.loadModelFile(model.path); // 3. Parse the model _showLoading(CONFIG.INFO.PARSING_MODEL); parsedModel = await _parseModel(buffer, { fileName: model.name || model.path, fileSize: model.size || buffer.byteLength }); // 4. Store in cache (evict oldest if over limit) if (_parsedModelCache.size >= _CACHE_MAX_SIZE) { const firstKey = _parsedModelCache.keys().next().value; _parsedModelCache.delete(firstKey); } _parsedModelCache.set(cacheKey, parsedModel); } // 5. Update StateManager window.StateManager.setCurrentModel(parsedModel); window.StateManager.setLoading(false); // 6. Persist last selected model path to localStorage _saveUserPreference(CONFIG.STORAGE.SELECTED_MODEL, cacheKey); // 7. Update all UI components _updateAllDisplays(parsedModel); // 8. Emit model:loaded window.EventBus.emit(CONFIG.EVENTS.MODEL_LOADED, { model: parsedModel }); // 9. Load annotations for the model if (graphAnnotation && parsedModel.metadata && parsedModel.metadata.fileName) { graphAnnotation.loadAnnotations(parsedModel.metadata.fileName); } _clearLoading(); } catch (err) { window.StateManager.setLoading(false); _handleError(err, 'model:selected'); } } /** * Handle file:uploaded event from FileUploadHandler. * Flow: parse → update StateManager → update all UI components * @param {{ file: File, data: ArrayBuffer, fileName: string }} payload */ async function _onFileUploaded(payload) { if (!payload || !payload.data) { console.warn('[App] file:uploaded fired with invalid payload', payload); return; } var fileName = payload.fileName || (payload.file && payload.file.name) || 'uploaded.onnx'; var isSafeTensors = fileName.toLowerCase().endsWith('.safetensors'); var isTFLite = fileName.toLowerCase().endsWith('.tflite'); if (isTFLite) { // ── TFLite pipeline ── _showLoading(CONFIG.INFO.PARSING_MODEL); try { if (!tfliteParser) throw new Error('TFLiteParser is not initialized'); var result = tfliteParser.parse(payload.data); if (!result.success) { throw new Error(result.error || 'Failed to parse TFLite file'); } // Hide SafeTensors viewer if visible if (safeTensorsViewer) { safeTensorsViewer.hide(); } // Render TFLite viewer (also hides ONNX panels internally) if (tfliteViewer) { tfliteViewer.render(result.data, fileName); } // Disable Export button (Req 6.4) if (exportHandler && exportHandler._exportBtn) { exportHandler._exportBtn.disabled = true; exportHandler._exportBtn.title = 'Export is not available for TFLite files'; } // Update RecentModels (Req 6.3) if (recentModels && typeof recentModels.addEntry === 'function') { recentModels.addEntry( fileName, payload.file ? payload.file.size : payload.data.byteLength ); } _clearLoading(); } catch (err) { _clearLoading(); _handleError(err, 'file:uploaded (tflite)'); } } else if (isSafeTensors) { // ── SafeTensors pipeline ── _showLoading(CONFIG.INFO.PARSING_MODEL); try { if (!safeTensorsParser) throw new Error('SafeTensorsParser is not initialized'); var result = safeTensorsParser.parse(payload.data); if (!result.success) { throw new Error(result.error || 'Failed to parse SafeTensors file'); } // Render SafeTensors viewer (also hides ONNX panels internally) if (safeTensorsViewer) { safeTensorsViewer.render(result.data, fileName); } // Disable Export button (Req 43.6) if (exportHandler && exportHandler._exportBtn) { exportHandler._exportBtn.disabled = true; exportHandler._exportBtn.title = 'Export is not available for SafeTensors files'; } // Update RecentModels if (recentModels && typeof recentModels.addEntry === 'function') { recentModels.addEntry( fileName, payload.file ? payload.file.size : payload.data.byteLength ); } _clearLoading(); } catch (err) { _clearLoading(); _handleError(err, 'file:uploaded (safetensors)'); } } else { // ── ONNX pipeline (existing) ── // Hide SafeTensors viewer if visible if (safeTensorsViewer) { safeTensorsViewer.hide(); } // Hide TFLite viewer if visible if (tfliteViewer) { tfliteViewer.hide(); } // Re-enable Export button if (exportHandler && exportHandler._exportBtn) { exportHandler._exportBtn.disabled = false; exportHandler._exportBtn.title = ''; } _showLoading(CONFIG.INFO.PARSING_MODEL); try { const parsedModel = await _parseModel(payload.data, { fileName: fileName, fileSize: payload.file ? payload.file.size : payload.data.byteLength }); window.StateManager.setCurrentModel(parsedModel); window.StateManager.setLoading(false); _updateAllDisplays(parsedModel); window.EventBus.emit(CONFIG.EVENTS.MODEL_LOADED, { model: parsedModel }); // Load annotations for the uploaded model if (graphAnnotation && parsedModel.metadata && parsedModel.metadata.fileName) { graphAnnotation.loadAnnotations(parsedModel.metadata.fileName); } _clearLoading(); } catch (err) { window.StateManager.setLoading(false); _handleError(err, 'file:uploaded'); } } } /** * Handle node:selected event – highlight the node in the graph. * @param {{ nodeId: string, source?: string }} payload */ function _onNodeSelected(payload) { if (!payload || !payload.nodeId) return; if (graphVisualizer) { graphVisualizer.highlightNode(payload.nodeId); } } /** * Handle layerstats:highlight event – highlight all nodes of a given opType. * @param {{ opType: string }} payload */ function _onLayerStatsHighlight(payload) { if (!payload || !payload.opType || !graphVisualizer || !graphVisualizer._cy) return; var cy = graphVisualizer._cy; cy.elements().removeClass('highlighted'); cy.nodes().forEach(function (node) { if (node.data('opType') === payload.opType) { node.addClass('highlighted'); } }); } // ─── Model Processing ───────────────────────────────────────────────────── /** * Parse an ONNX model buffer using ONNXParser. * @param {ArrayBuffer} buffer * @param {{ fileName: string, fileSize: number }} options * @returns {Promise} */ async function _parseModel(buffer, options) { if (!onnxParser) throw new Error('ONNXParser is not initialized'); return onnxParser.parseModel(buffer, options); } /** * Update all UI display components with a parsed model. * Also processes the graph and initializes the GraphVisualizer. * @param {ParsedModel} parsedModel */ function _updateAllDisplays(parsedModel) { // Metadata if (metadataDisplay) { metadataDisplay.render(parsedModel); } // Inputs / Outputs if (inputOutputDisplay) { inputOutputDisplay.render(parsedModel); } // Initializers if (initializerDisplay) { initializerDisplay.render(parsedModel); } // Node Detail Panel – reset to guidance on new model load if (nodeDetailPanel) { nodeDetailPanel.clear(); } // Render new analysis panels directly if (layerStats) { var stats = layerStats.compute(parsedModel); layerStats.render(stats); } if (modelComplexity) { var metrics = modelComplexity.compute(parsedModel); modelComplexity.render(metrics); } if (opsetChecker) { var opsetData = opsetChecker.compute(parsedModel); opsetChecker.render(opsetData); } // FlopsEstimator if (flopsEstimator) { flopsEstimator.compute(parsedModel); flopsEstimator.render(); } // Update TensorShapeInspector with new model data if (tensorShapeInspector) { tensorShapeInspector.updateModel(parsedModel); } // Graph if (graphProcessor && graphVisualizer) { _showLoading(CONFIG.INFO.RENDERING_GRAPH); const { elements } = graphProcessor.processGraph(parsedModel); const container = document.getElementById('graphContainer'); if (container) { // Clear placeholder text container.innerHTML = ''; graphVisualizer.initialize(container, elements); // Attach graph-dependent handlers var cy = graphVisualizer._cy; if (cy) { if (graphPathHighlighter) { graphPathHighlighter.attachCytoscapeHandlers(cy); } if (tensorShapeInspector) { tensorShapeInspector.attachToCytoscape(cy); } } // Emit graph:rendered event window.EventBus.emit(CONFIG.EVENTS.GRAPH_RENDERED, { cy: cy }); // Initialize graph-dependent UI components if (graphSearch) { graphSearch.init(); } if (graphLayoutSwitcher) { graphLayoutSwitcher.init(); } if (nodeGrouping) { nodeGrouping.init(); } // Refresh minimap if (graphMinimap) { setTimeout(function() { graphMinimap.refresh(); }, 300); } // Restore pending zoom preference if any if (window._onnxApp && window._onnxApp._pendingZoom) { requestAnimationFrame(function () { graphVisualizer.zoom(window._onnxApp._pendingZoom); window._onnxApp._pendingZoom = null; }); } } _clearLoading(); } } // ─── Model List Loading ─────────────────────────────────────────────────── /** * Load models.json and populate StateManager + ModelListDisplay. */ async function _loadAndRenderModelList() { try { if (!modelLoader) { console.warn('[App] ModelLoader not available – skipping model list load'); return; } const models = await modelLoader.loadModelList(); window.StateManager.setModelList(models); window.StateManager.setFilteredModelList(models.slice()); // Initial render (subscription will handle subsequent updates) if (modelListDisplay) { modelListDisplay.render(models); } console.log(`[App] Loaded ${models.length} model(s)`); // Initialize ShareableURL with the model list if (shareableURL) { shareableURL.init(models); } } catch (err) { _handleError(err, 'loadModelList'); } } // ─── Selection Restore ──────────────────────────────────────────────────── /** * Restore the previously selected model from URL hash or localStorage (if any). */ function _restoreSelection() { // 1. Check ShareableURL hash first (takes priority) if (shareableURL) { var hashResult = shareableURL.readHash(); if (hashResult.error && window.ErrorDisplay) { window.ErrorDisplay.show(hashResult.error, 'warning'); } if (hashResult.model && window.EventBus) { console.log('[App] Restoring model from URL hash:', hashResult.model.id); window.EventBus.emit(CONFIG.EVENTS.MODEL_SELECTED, hashResult.model); return; } } // 2. Fall back to localStorage persisted selection const persistedId = window.StateManager ? window.StateManager.getPersistedSelectedModelId() : null; if (!persistedId) return; const modelList = window.StateManager.getModelList(); const model = modelList.find( (m) => m.id === persistedId || m.path === persistedId || m.name === persistedId ); if (model && window.EventBus) { console.log('[App] Restoring previously selected model:', persistedId); window.EventBus.emit(CONFIG.EVENTS.MODEL_SELECTED, model); } } // ─── UI Helpers ─────────────────────────────────────────────────────────── /** * Show a loading indicator in the error container. * @param {string} message */ function _showLoading(message) { window.StateManager.setLoading(true); if (window.ErrorDisplay) { window.ErrorDisplay.show(message, 'info', false); } } /** * Clear the loading indicator. */ function _clearLoading() { window.StateManager.setLoading(false); if (window.ErrorDisplay) { window.ErrorDisplay.hide(); } } /** * Handle an error using ErrorHandler and display it. * @param {Error|string} err * @param {string} context */ function _handleError(err, context) { if (window.ErrorHandler) { window.ErrorHandler.handleError(err, context); } else { console.error(`[App][${context}]`, err); if (window.ErrorDisplay) { window.ErrorDisplay.showError( (err && err.message) ? err.message : String(err) ); } } } // ─── User Preferences (localStorage) ───────────────────────────────────── /** * Save a user preference to localStorage. * @param {string} key * @param {*} value */ function _saveUserPreference(key, value) { try { localStorage.setItem(key, typeof value === 'string' ? value : JSON.stringify(value)); } catch (_) { /* ignore quota/security errors */ } } /** * Load a user preference from localStorage. * @param {string} key * @param {*} [defaultValue=null] * @returns {*} */ function _loadUserPreference(key, defaultValue) { try { const raw = localStorage.getItem(key); if (raw === null) return defaultValue !== undefined ? defaultValue : null; try { return JSON.parse(raw); } catch (_) { return raw; } } catch (_) { return defaultValue !== undefined ? defaultValue : null; } } /** * Restore user preferences (zoom level) from localStorage. */ function _restoreUserPreferences() { const prefs = _loadUserPreference(CONFIG.STORAGE.USER_PREFERENCES, {}); if (prefs && typeof prefs.zoomLevel === 'number' && graphVisualizer) { // Zoom will be applied after graph is initialized; store for later use window._onnxApp._pendingZoom = prefs.zoomLevel; } } // ─── Bootstrap ──────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', function () { init().catch(function (err) { console.error('[App] Fatal initialization error:', err); const container = document.getElementById('errorContainer'); if (container) { container.innerHTML = '
' + '' + 'Application failed to initialize. Please refresh the page.' + '
'; } }); }); // Expose for debugging window._onnxApp = { getModelLoader: () => modelLoader, getOnnxParser: () => onnxParser, getGraphProcessor: () => graphProcessor, getModelListDisplay: () => modelListDisplay, getMetadataDisplay: () => metadataDisplay, getInputOutputDisplay:() => inputOutputDisplay, getInitializerDisplay:() => initializerDisplay, getGraphVisualizer: () => graphVisualizer, getFileUploadHandler: () => fileUploadHandler, getSearchFilter: () => searchFilter, getExportHandler: () => exportHandler, getNodeDetailPanel: () => nodeDetailPanel, getGraphMinimap: () => graphMinimap, getGraphPathHighlighter: () => graphPathHighlighter, getLayerStats: () => layerStats, getModelComplexity: () => modelComplexity, getTensorShapeInspector: () => tensorShapeInspector, getOpsetChecker: () => opsetChecker, getRecentModels: () => recentModels, getGraphExport: () => graphExport, getShareableURL: () => shareableURL, getFullscreenManager: () => fullscreenManager, getSidebarToggle: () => sidebarToggle, getHelpTooltip: () => helpTooltip, getGuidedTour: () => guidedTour, getGraphSearch: () => graphSearch, getGraphLayoutSwitcher: () => graphLayoutSwitcher, getNodeGrouping: () => nodeGrouping, getGraphAnnotation: () => graphAnnotation, getFlopsEstimator: () => flopsEstimator, getLanguageSwitcher: () => languageSwitcher, getSafeTensorsParser: () => safeTensorsParser, getSafeTensorsViewer: () => safeTensorsViewer, getTFLiteParser: () => tfliteParser, getTFLiteViewer: () => tfliteViewer, getPrintHandler: () => printHandler, getParsedModelCache: () => _parsedModelCache, clearParsedModelCache:() => _parsedModelCache.clear(), _pendingZoom: null, }; })();