Spaces:
Running
Running
| /** | |
| * ModelListDisplay - Displays the list of available ONNX models | |
| * Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6 | |
| */ | |
| class ModelListDisplay { | |
| /** | |
| * @param {string} containerId - ID of the container element | |
| */ | |
| constructor(containerId) { | |
| this._containerId = containerId; | |
| this._container = document.getElementById(containerId); | |
| this._selectedModelId = null; | |
| this._unsubscribe = null; | |
| if (!this._container) { | |
| console.warn(`[ModelListDisplay] Container #${containerId} not found`); | |
| return; | |
| } | |
| this._bindStateSubscriptions(); | |
| } | |
| // βββ Private ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _bindStateSubscriptions() { | |
| // Restore persisted selection | |
| const persistedId = window.StateManager | |
| ? window.StateManager.getPersistedSelectedModelId() | |
| : null; | |
| if (persistedId) { | |
| this._selectedModelId = persistedId; | |
| } | |
| // Subscribe to currentModel changes to keep selection in sync | |
| if (window.StateManager) { | |
| this._unsubscribe = window.StateManager.subscribe('currentModel', (model) => { | |
| const id = model && model.metadata ? model.metadata.fileName : null; | |
| if (id !== this._selectedModelId) { | |
| this._selectedModelId = id; | |
| this._updateSelectedState(); | |
| } | |
| }); | |
| } | |
| } | |
| /** | |
| * Format a file path to a human-readable name (show only the file name). | |
| * @param {string} filePath | |
| * @returns {string} | |
| */ | |
| _formatFileName(filePath) { | |
| if (!filePath) return 'Unknown'; | |
| // Use helper if available | |
| if (typeof formatFileName === 'function') { | |
| return formatFileName(filePath); | |
| } | |
| // Fallback: strip path separators | |
| const parts = filePath.replace(/\\/g, '/').split('/'); | |
| return parts[parts.length - 1] || filePath; | |
| } | |
| /** | |
| * Build a single list item element for a model. | |
| * @param {ModelInfo} model | |
| * @returns {HTMLElement} | |
| */ | |
| _buildItem(model) { | |
| const item = document.createElement('a'); | |
| item.href = '#'; | |
| item.className = 'list-group-item list-group-item-action model-list-item'; | |
| item.dataset.modelId = model.id || model.path; | |
| item.setAttribute('role', 'button'); | |
| item.setAttribute('aria-label', `Select model ${this._formatFileName(model.name || model.path)}`); | |
| const displayName = this._formatFileName(model.name || model.path); | |
| item.innerHTML = ` | |
| <div class="d-flex align-items-center"> | |
| <i class="fas fa-cube me-2 text-primary"></i> | |
| <div class="overflow-hidden"> | |
| <div class="text-truncate fw-medium">${this._escapeHtml(displayName)}</div> | |
| ${model.description ? `<small class="text-muted text-truncate d-block">${this._escapeHtml(model.description)}</small>` : ''} | |
| </div> | |
| </div> | |
| `; | |
| // Mark as selected if this is the current model | |
| if (this._selectedModelId && (model.id === this._selectedModelId || model.path === this._selectedModelId)) { | |
| item.classList.add('active'); | |
| item.setAttribute('aria-current', 'true'); | |
| } | |
| item.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| this._onModelClick(model, item); | |
| }); | |
| return item; | |
| } | |
| /** | |
| * Handle model item click. | |
| * @param {ModelInfo} model | |
| * @param {HTMLElement} item | |
| */ | |
| _onModelClick(model, item) { | |
| // Update visual selection | |
| this._selectedModelId = model.id || model.path; | |
| this._updateSelectedState(); | |
| // Emit event via EventBus | |
| if (window.EventBus) { | |
| window.EventBus.emit(CONFIG.EVENTS.MODEL_SELECTED, model); | |
| } | |
| // Update StateManager if available (without triggering re-render loop) | |
| if (window.StateManager) { | |
| // Persist selection | |
| try { | |
| localStorage.setItem(CONFIG.STORAGE.SELECTED_MODEL, this._selectedModelId); | |
| } catch (_) { /* ignore */ } | |
| } | |
| } | |
| /** | |
| * Update the active/selected CSS class on all items. | |
| */ | |
| _updateSelectedState() { | |
| if (!this._container) return; | |
| const items = this._container.querySelectorAll('.model-list-item'); | |
| items.forEach((item) => { | |
| const isSelected = item.dataset.modelId === this._selectedModelId; | |
| item.classList.toggle('active', isSelected); | |
| if (isSelected) { | |
| item.setAttribute('aria-current', 'true'); | |
| } else { | |
| item.removeAttribute('aria-current'); | |
| } | |
| }); | |
| } | |
| /** | |
| * Escape HTML special characters to prevent XSS. | |
| * @param {string} str | |
| * @returns {string} | |
| */ | |
| _escapeHtml(str) { | |
| const div = document.createElement('div'); | |
| div.appendChild(document.createTextNode(str)); | |
| return div.innerHTML; | |
| } | |
| // βββ Public API βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /** | |
| * Render the model list. | |
| * @param {Array<ModelInfo>} models | |
| */ | |
| render(models) { | |
| if (!this._container) return; | |
| this._container.innerHTML = ''; | |
| if (!models || models.length === 0) { | |
| const empty = document.createElement('div'); | |
| empty.className = 'list-group-item text-muted text-center py-3'; | |
| empty.setAttribute('role', 'status'); | |
| empty.innerHTML = `<i class="fas fa-inbox me-2"></i>${CONFIG.ERRORS.NO_MODELS_AVAILABLE}`; | |
| this._container.appendChild(empty); | |
| return; | |
| } | |
| const fragment = document.createDocumentFragment(); | |
| models.forEach((model) => { | |
| fragment.appendChild(this._buildItem(model)); | |
| }); | |
| this._container.appendChild(fragment); | |
| } | |
| /** | |
| * Clear the model list display. | |
| */ | |
| clear() { | |
| if (!this._container) return; | |
| this._container.innerHTML = ''; | |
| } | |
| /** | |
| * Destroy the component and clean up subscriptions. | |
| */ | |
| destroy() { | |
| if (this._unsubscribe) { | |
| this._unsubscribe(); | |
| this._unsubscribe = null; | |
| } | |
| } | |
| } | |
| window.ModelListDisplay = ModelListDisplay; | |