Spaces:
Running
Running
| /** | |
| * 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<ParsedModel>} | |
| */ | |
| 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 = | |
| '<div class="alert alert-danger">' + | |
| '<i class="fas fa-times-circle me-2"></i>' + | |
| 'Application failed to initialize. Please refresh the page.' + | |
| '</div>'; | |
| } | |
| }); | |
| }); | |
| // 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, | |
| }; | |
| })(); | |