Spaces:
Running
Running
File size: 6,042 Bytes
9bd422a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 | /**
* 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;
|