/** * ExportHandler - Handles exporting model information as JSON * Requirements: 12.1, 12.2, 12.3, 12.4 */ const ExportHandler = (function () { class ExportHandler { /** * @param {string} [exportBtnId='exportBtn'] - ID of the export button element */ constructor(exportBtnId = 'exportBtn') { this._exportBtn = document.getElementById(exportBtnId); this._unsubscribe = null; this._init(); } // ─── Private ──────────────────────────────────────────────────────────── _init() { if (!this._exportBtn) { console.warn('[ExportHandler] Export button not found:', this._exportBtn); return; } // Bind click handler this._exportBtn.addEventListener('click', () => this._handleExport()); // Subscribe to currentModel changes to enable/disable button this._unsubscribe = StateManager.subscribe('currentModel', (model) => { this._setButtonEnabled(!!model); }); // Set initial state this._setButtonEnabled(!!StateManager.getCurrentModel()); } /** * Enable or disable the export button. * @param {boolean} enabled */ _setButtonEnabled(enabled) { if (!this._exportBtn) return; this._exportBtn.disabled = !enabled; } /** * Build the export payload from the current model. * @param {object} model - ParsedModel from StateManager * @returns {object} */ _buildExportData(model) { const data = {}; // Metadata data.metadata = model.metadata ? Object.assign({}, model.metadata) : {}; // Inputs data.inputs = Array.isArray(model.inputs) ? model.inputs.map((i) => Object.assign({}, i)) : []; // Outputs data.outputs = Array.isArray(model.outputs) ? model.outputs.map((o) => Object.assign({}, o)) : []; // Graph (nodes + edges) if (model.graph) { data.graph = { name: model.graph.name || '', nodes: Array.isArray(model.graph.nodes) ? model.graph.nodes.map((n) => Object.assign({}, n)) : [], edges: Array.isArray(model.graph.edges) ? model.graph.edges.map((e) => Object.assign({}, e)) : [], }; } else { data.graph = { name: '', nodes: [], edges: [] }; } // Initializers data.initializers = Array.isArray(model.initializers) ? model.initializers.map((init) => Object.assign({}, init)) : []; return data; } /** * Derive the download file name from the model's fileName metadata. * e.g. "model.onnx" → "model_info.json" * @param {object} metadata * @returns {string} */ _buildFileName(metadata) { const fileName = (metadata && metadata.fileName) || 'model'; // Strip directory path, keep base name const base = fileName.split('/').pop().split('\\').pop(); // Remove extension const withoutExt = base.replace(/\.[^.]+$/, ''); return `${withoutExt}_info.json`; } /** * Trigger a browser download of the given JSON string. * @param {string} jsonStr * @param {string} fileName */ _triggerDownload(jsonStr, fileName) { const blob = new Blob([jsonStr], { type: 'application/json' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = fileName; anchor.style.display = 'none'; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); // Release the object URL after a short delay setTimeout(() => URL.revokeObjectURL(url), 1000); } /** * Show a brief success or error notification. * Falls back to console if no ErrorDisplay is available. * @param {string} message * @param {'success'|'error'} type */ _showNotification(message, type) { if (window.ErrorDisplay && typeof window.ErrorDisplay.show === 'function') { window.ErrorDisplay.show(message, type === 'error' ? 'error' : 'info'); } else { if (type === 'error') { console.error('[ExportHandler]', message); } else { console.info('[ExportHandler]', message); } } } // ─── Export Handler ────────────────────────────────────────────────────── _handleExport() { try { const model = StateManager.getCurrentModel(); if (!model) { this._showNotification( CONFIG.ERRORS.EXPORT_ERROR + ' No model is currently loaded.', 'error' ); return; } const exportData = this._buildExportData(model); const jsonStr = JSON.stringify(exportData, null, 2); const fileName = this._buildFileName(model.metadata); this._triggerDownload(jsonStr, fileName); this._showNotification(CONFIG.SUCCESS.MODEL_EXPORTED, 'success'); } catch (err) { console.error('[ExportHandler] Export failed:', err); this._showNotification(CONFIG.ERRORS.EXPORT_ERROR, 'error'); } } // ─── Public API ────────────────────────────────────────────────────────── /** * Programmatically trigger an export (useful for testing). */ export() { this._handleExport(); } /** * Clean up subscriptions. */ destroy() { if (typeof this._unsubscribe === 'function') { this._unsubscribe(); this._unsubscribe = null; } } } return ExportHandler; })(); // Export for global access in vanilla JS context window.ExportHandler = ExportHandler;