model-explorer / js /ui /recentModels.js
mr4's picture
Upload 71 files
9bd422a verified
/**
* 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 = '<p class="text-muted small">No recently uploaded models.</p>';
return;
}
let html = '<ul class="list-group list-group-flush mb-2" role="list" aria-label="Recent models">';
entries.forEach((entry) => {
const name = this._escapeHtml(entry.fileName || 'Unknown');
const size = this._formatSize(entry.fileSize);
const time = this._formatTime(entry.uploadTime);
html += `
<li class="list-group-item list-group-item-action recent-model-item p-2"
role="button"
tabindex="0"
data-file-name="${this._escapeHtml(entry.fileName || '')}"
title="Click to see re-upload instructions"
aria-label="${name} β€” ${size}, uploaded ${time}">
<div class="fw-semibold small text-truncate">${name}</div>
<div class="text-muted" style="font-size: 0.75rem;">
<span>${size}</span>
<span class="mx-1">Β·</span>
<span>${time}</span>
</div>
</li>`;
});
html += '</ul>';
html += `
<button class="btn btn-sm btn-outline-danger recent-models-clear-btn w-100"
type="button"
aria-label="Clear all recent model history">
<i class="fas fa-trash-alt me-1"></i>Clear History
</button>`;
this._container.innerHTML = html;
this._attachHandlers();
}
/**
* Clear all history from localStorage and re-render.
*/
clearHistory() {
try {
localStorage.removeItem(this._storageKey);
} catch (err) {
console.error('[RecentModels] Failed to clear history:', err);
}
this.render();
}
/**
* Get the current history entries.
* @returns {Array<{fileName: string, fileSize: number, uploadTime: number}>}
*/
getHistory() {
return this._loadHistory();
}
}
window.RecentModels = RecentModels;