/** * RecentModels - Manages and displays recently uploaded model history * Saves file name, size, and upload timestamp to localStorage on successful upload. * Displays up to 5 most recent models in a sidebar list. * Requirements: 23.1, 23.2, 23.3, 23.4, 23.5 */ class RecentModels { /** * @param {string} containerId - ID of the container element */ constructor(containerId) { this._containerId = containerId; this._container = document.getElementById(containerId); this._maxItems = 5; this._storageKey = (typeof CONFIG !== 'undefined' && CONFIG.STORAGE && CONFIG.STORAGE.RECENT_MODELS) ? CONFIG.STORAGE.RECENT_MODELS : 'onnx_explorer_recent_models'; if (!this._container) { console.warn(`[RecentModels] Container #${containerId} not found`); } this._setupEventListeners(); this.render(); } // ─── Private ────────────────────────────────────────────────────────────── /** * Listen for file:uploaded events to auto-save history. */ _setupEventListeners() { if (window.EventBus && typeof CONFIG !== 'undefined' && CONFIG.EVENTS) { window.EventBus.on(CONFIG.EVENTS.FILE_UPLOADED, (data) => { if (data && data.fileName) { const fileSize = (data.file && data.file.size) ? data.file.size : 0; this.addEntry(data.fileName, fileSize); } }); } } /** * Load history entries from localStorage. * @returns {Array<{fileName: string, fileSize: number, uploadTime: number}>} */ _loadHistory() { try { const raw = localStorage.getItem(this._storageKey); if (!raw) return []; const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : []; } catch (err) { console.error('[RecentModels] Failed to load history:', err); return []; } } /** * Save history entries to localStorage. * @param {Array} entries */ _saveHistory(entries) { try { localStorage.setItem(this._storageKey, JSON.stringify(entries)); } catch (err) { console.error('[RecentModels] Failed to save history:', err); } } /** * Format a timestamp into a readable date/time string. * @param {number} timestamp * @returns {string} */ _formatTime(timestamp) { if (!timestamp) return 'Unknown'; try { const date = new Date(timestamp); return date.toLocaleString(); } catch (e) { return 'Unknown'; } } /** * Format file size using Formatters utility if available, otherwise fallback. * @param {number} bytes * @returns {string} */ _formatSize(bytes) { if (window.Formatters && window.Formatters.formatBytes) { return window.Formatters.formatBytes(bytes); } if (bytes == null || isNaN(bytes) || bytes === 0) return 'Unknown size'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; 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) + ' ' + 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; } /** * Show a re-upload guidance message when a history item is clicked. * @param {string} fileName */ _showReuploadMessage(fileName) { const escapedName = this._escapeHtml(fileName); const message = `To view "${escapedName}" again, please re-upload the file using the Upload button. Browsers cannot store file content in history.`; if (window.EventBus && typeof CONFIG !== 'undefined' && CONFIG.EVENTS) { window.EventBus.emit(CONFIG.EVENTS.ERROR_OCCURRED, { message: message, type: 'info' }); } if (window.ErrorDisplay && window.ErrorDisplay.show) { window.ErrorDisplay.show(message, 'info'); } } /** * Attach click handlers to history items and the clear button. */ _attachHandlers() { if (!this._container) return; // History item click handlers const items = this._container.querySelectorAll('.recent-model-item'); items.forEach((item) => { const handler = () => { const fileName = item.dataset.fileName; if (fileName) this._showReuploadMessage(fileName); }; item.addEventListener('click', handler); item.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handler(); } }); }); // Clear history button const clearBtn = this._container.querySelector('.recent-models-clear-btn'); if (clearBtn) { clearBtn.addEventListener('click', () => this.clearHistory()); clearBtn.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.clearHistory(); } }); } } // ─── Public API ─────────────────────────────────────────────────────────── /** * Add a new entry to the recent models history. * @param {string} fileName - Name of the uploaded file * @param {number} fileSize - Size in bytes */ addEntry(fileName, fileSize) { const entries = this._loadHistory(); const newEntry = { fileName: fileName, fileSize: fileSize || 0, uploadTime: Date.now() }; // Remove duplicate if same fileName already exists const filtered = entries.filter((e) => e.fileName !== fileName); // Add new entry at the beginning filtered.unshift(newEntry); // Keep only the most recent entries const trimmed = filtered.slice(0, this._maxItems); this._saveHistory(trimmed); this.render(); } /** * Render the recent models list into the container. */ render() { if (!this._container) return; const entries = this._loadHistory(); if (entries.length === 0) { this._container.innerHTML = '
No recently uploaded models.
'; return; } let html = '