diff --git a/web/css/resolver-shell.css b/web/css/resolver-shell.css index 79c9a62..a712ddc 100644 --- a/web/css/resolver-shell.css +++ b/web/css/resolver-shell.css @@ -393,6 +393,16 @@ font-size: 13px; } +.mr-downloads-panel { + display: grid; + gap: 0; + min-height: 0; +} + +.mr-downloads-subtabs { + margin: 0 0 -1px; +} + .mr-queue-items { display: flex; flex-direction: column; @@ -495,6 +505,29 @@ border-radius: 999px; } +.mr-download-history-toolbar { + display: flex; + justify-content: flex-end; + padding: 0 8px 4px; +} + +.mr-download-history-status { + flex: 0 0 auto; + max-width: 130px; + padding: 2px 6px; + overflow: hidden; + color: #d7d7d7; + font-size: 10px; + font-weight: 800; + line-height: 1.2; + text-overflow: ellipsis; + text-transform: uppercase; + white-space: nowrap; + background: rgba(255, 255, 255, 0.07); + border: 1px solid rgba(255, 255, 255, 0.105); + border-radius: 999px; +} + .mr-download-queue-progress { display: grid; gap: 4px; diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index 2be59e1..f4845f7 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -70,6 +70,8 @@ export const queueMethods = { createQueuePanel() { this.queuePanelActiveTab = this.queuePanelActiveTab || 'queued'; + this.queueDownloadsActiveTab = this.queueDownloadsActiveTab || 'active'; + this.loadDownloadHistory?.(); this.queueClearButton = $el("button", { id: "queue-clear", className: "mr-btn mr-btn-secondary mr-btn-sm", @@ -117,10 +119,18 @@ export const queueMethods = { this.updateQueuePanel(); }, + setQueueDownloadsTab(tab) { + const nextTab = tab === 'history' ? 'history' : 'active'; + if (this.queueDownloadsActiveTab === nextTab) return; + this.queueDownloadsActiveTab = nextTab; + this.updateQueuePanel(); + }, + updateQueuePanel() { if (!this.queueList || !this.queueHeader) return; const list = Array.isArray(this.pendingResolutions) ? this.pendingResolutions : []; const downloads = this.getActiveQueuePanelDownloads(); + const history = this.getDownloadHistory(); const activeTab = this.queuePanelActiveTab === 'downloads' ? 'downloads' : 'queued'; const clearBtn = this.queueClearButton || this.queueHeader.querySelector('#queue-clear'); const showActions = activeTab === 'queued'; @@ -129,7 +139,7 @@ export const queueMethods = { this.updateQueuePanelTabs(list.length, downloads.length, activeTab); if (activeTab === 'downloads') { - this.renderQueueDownloads(downloads); + this.renderDownloadsPanel(downloads, history); return; } @@ -743,10 +753,191 @@ export const queueMethods = { return this.getActiveQueuePanelDownloads().map(({ downloadId }) => downloadId); }, + loadDownloadHistory() { + if (this._downloadHistoryLoaded) return this.downloadHistory || []; + this._downloadHistoryLoaded = true; + try { + const raw = localStorage.getItem(this.downloadHistoryStorageKey || 'model_resolver_download_history'); + const parsed = raw ? JSON.parse(raw) : []; + this.downloadHistory = Array.isArray(parsed) + ? parsed.filter(item => item && typeof item === 'object') + : []; + } catch (error) { + console.warn('Model Resolver: failed to load download history', error); + this.downloadHistory = []; + } + return this.downloadHistory; + }, + + getDownloadHistory() { + if (!this._downloadHistoryLoaded) { + this.loadDownloadHistory(); + } + return Array.isArray(this.downloadHistory) ? this.downloadHistory : []; + }, + + saveDownloadHistory() { + const history = this.getDownloadHistory().slice(0, this.downloadHistoryLimit || 200); + this.downloadHistory = history; + try { + localStorage.setItem( + this.downloadHistoryStorageKey || 'model_resolver_download_history', + JSON.stringify(history) + ); + } catch (error) { + console.warn('Model Resolver: failed to save download history', error); + } + }, + + getDownloadHistoryIdentity(entry = {}) { + return [ + entry.path || '', + entry.filename || '', + entry.category || '', + entry.sourceUrl || '' + ].map(value => String(value || '').trim().toLowerCase()).join('::'); + }, + + addDownloadHistoryEntry(entry = {}) { + if (!entry || !entry.filename) return null; + + const history = this.getDownloadHistory(); + const identity = this.getDownloadHistoryIdentity(entry); + const filtered = identity + ? history.filter(item => this.getDownloadHistoryIdentity(item) !== identity) + : history; + const nextEntry = { + ...entry, + id: entry.id || `${Date.now()}-${Math.random().toString(36).slice(2)}`, + completedAt: entry.completedAt || new Date().toISOString() + }; + this.downloadHistory = [nextEntry, ...filtered].slice(0, this.downloadHistoryLimit || 200); + this.saveDownloadHistory(); + this.updateQueuePanel?.(); + return nextEntry; + }, + + rememberCompletedDownloadHistory(downloadId, info = {}, progress = {}) { + const missing = info?.missing || {}; + const filename = progress.filename + || info.filename + || missing.download_source?.filename + || missing.original_path?.split(/[\/\\]/).pop() + || ''; + if (!filename) return null; + + const directory = progress.directory || info.downloadDirectory || ''; + const path = progress.path || info.downloadPath || ''; + const category = info.category || missing.category || progress.category || ''; + const workflowLabel = this.getDownloadWorkflowLabel?.(info) || info.workflowLabel || ''; + const nodeLabel = missing.subgraph_name || missing.node_type || (missing.subgraph_id ? 'Subgraph' : 'Node'); + const status = progress.already_exists ? 'already_exists' : 'completed'; + return this.addDownloadHistoryEntry({ + downloadId, + filename, + category, + categoryLabel: this.getCategoryDisplayName?.(category) || category, + nodeLabel, + nodeId: missing.node_id ?? '', + widgetIndex: missing.widget_index ?? '', + workflowLabel, + workflowRouteKey: info.workflowRouteKey || '', + workflowTabId: info.workflowTabId || '', + workflowTabName: info.workflowTabName || '', + path, + directory, + sourceUrl: info.sourceUrl || missing.download_source?.url || '', + totalSize: progress.total_size || progress.size || 0, + status, + statusLabel: status === 'already_exists' ? 'Already downloaded' : 'Downloaded', + message: progress.message || '', + completedAt: new Date().toISOString() + }); + }, + + formatDownloadHistoryTime(value = '') { + if (!value) return ''; + try { + const date = new Date(value); + if (Number.isNaN(date.getTime())) return ''; + return date.toLocaleString(undefined, { + month: 'short', + day: '2-digit', + hour: '2-digit', + minute: '2-digit' + }); + } catch (error) { + return ''; + } + }, + + getDownloadHistoryFolderContext(entry = {}) { + const targetPath = entry.directory || entry.path || ''; + if (!targetPath) return null; + return { + context_scope: 'download_folder', + open_folder_label: 'Open Download Folder', + name: entry.filename || 'Download', + path: targetPath, + resolved_path: targetPath, + folder_path: entry.directory || targetPath, + download_directory: entry.directory || '', + download_path: entry.path || '', + category: entry.category || '' + }; + }, + + renderDownloadsPanel(downloads = [], history = this.getDownloadHistory()) { + const activeSubTab = this.queueDownloadsActiveTab === 'history' ? 'history' : 'active'; + const activeSelected = activeSubTab === 'active'; + let html = '