From ffd7d548f2d619a4f114e7d1f545d500e63e79bd Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 26 May 2026 16:19:44 +0200 Subject: [PATCH] web(linker): add estimated search progress timers and UI Add estimated progress timers and helpers to show smoother search progress across sources. Track per-run timers, compute eased percentages, update progress state on completion/error, and adjust CSS for running state and transitions. --- web/css/linker-main.css | 15 +---- web/linker.js | 120 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 118 insertions(+), 17 deletions(-) diff --git a/web/css/linker-main.css b/web/css/linker-main.css index 77d4636..66b4731 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -903,11 +903,10 @@ height: 100%; background: var(--ml-accent); border-radius: inherit; - transition: width 0.2s ease, background 0.2s ease; + transition: width 0.42s ease, background 0.2s ease; } -.ml-search-progress-fill-running { - width: 42% !important; - animation: ml-search-progress-slide 1.05s ease-in-out infinite; +.ml-search-progress-running .ml-search-progress-fill { + background: linear-gradient(90deg, rgba(114,211,138,0.62), var(--ml-accent)); } .ml-search-progress-found .ml-search-progress-fill { background: #72d38a; @@ -918,14 +917,6 @@ .ml-search-progress-error .ml-search-progress-fill { background: #ff8a80; } -@keyframes ml-search-progress-slide { - 0% { - transform: translateX(-120%); - } - 100% { - transform: translateX(240%); - } -} .ml-combo-section { position: relative; margin-top: 8px; diff --git a/web/linker.js b/web/linker.js index 8edd365..80b1bc0 100644 --- a/web/linker.js +++ b/web/linker.js @@ -25,6 +25,7 @@ class LinkerManagerDialog extends ComfyDialog { this.pendingIndex = new Map(); // key -> index in pendingResolutions this.activeDownloads = {}; // Track active downloads this.searchResultCache = new Map(); + this.searchProgressTimers = new Map(); this.cachedAnalysisData = null; this.cachedWorkflowSignature = null; this.boundHandleOutsideClick = this.handleOutsideClick.bind(this); @@ -248,6 +249,92 @@ class LinkerManagerDialog extends ComfyDialog { }; } + getSearchSourceEstimateMs(source, isUrn = false) { + if (isUrn && source === 'civitai') return 5000; + + const estimates = { + local: 1400, + lora_manager_archive: 3200, + huggingface: 18000, + civitai: 22000 + }; + return estimates[source] || 8000; + } + + getEstimatedSearchProgressPercent(elapsedMs, estimateMs) { + const safeEstimate = Math.max(1000, Number(estimateMs) || 8000); + const elapsed = Math.max(0, Number(elapsedMs) || 0); + + if (elapsed <= safeEstimate) { + const normalized = Math.min(1, elapsed / safeEstimate); + const eased = 1 - Math.pow(1 - normalized, 2.1); + return 6 + eased * 78; + } + + const overtime = elapsed - safeEstimate; + const slowTail = 1 - Math.exp(-overtime / (safeEstimate * 1.6)); + return Math.min(94, 84 + slowTail * 10); + } + + getSearchProgressTimerKey(runId, source) { + return `${runId || 'search'}:${source}`; + } + + clearSearchProgressTimer(runId, source) { + const key = this.getSearchProgressTimerKey(runId, source); + const timer = this.searchProgressTimers.get(key); + if (timer) { + clearInterval(timer); + this.searchProgressTimers.delete(key); + } + } + + clearSearchProgressTimers(runId) { + if (!runId) return; + const prefix = `${runId}:`; + for (const [key, timer] of this.searchProgressTimers.entries()) { + if (key.startsWith(prefix)) { + clearInterval(timer); + this.searchProgressTimers.delete(key); + } + } + } + + startEstimatedSearchProgress(state, missing, container, source, runId) { + this.clearSearchProgressTimer(runId, source); + + const tick = () => { + if (state.activeSearchRunId !== runId) { + this.clearSearchProgressTimer(runId, source); + return; + } + + const progress = state.sourceProgress?.[source]; + if (!progress || progress.status !== 'running') { + this.clearSearchProgressTimer(runId, source); + return; + } + + const elapsedMs = Date.now() - (progress.startedAt || Date.now()); + const percent = this.getEstimatedSearchProgressPercent( + elapsedMs, + progress.estimateMs + ); + + if (percent > (Number(progress.percent) || 0) + 0.2) { + this.setSourceProgress(state, source, { percent }); + this.displaySearchResults(missing, state, container); + } + }; + + tick(); + const timer = setInterval(tick, 450); + this.searchProgressTimers.set( + this.getSearchProgressTimerKey(runId, source), + timer + ); + } + hasActiveSearchProgress(state = {}) { return Object.values(state.sourceProgress || {}).some(progress => ( progress?.status === 'pending' || progress?.status === 'running' @@ -270,9 +357,13 @@ class LinkerManagerDialog extends ComfyDialog { for (const [source, progress] of progressEntries) { const status = progress?.status || 'pending'; const label = this.getSearchSourceLabel(source); - const statusLabel = progress?.message || statusLabels[status] || status; - const percent = status === 'pending' ? 0 : (status === 'running' ? 55 : 100); - const safePercent = Math.max(0, Math.min(100, percent)); + const percent = status === 'pending' + ? 0 + : (status === 'running' ? progress?.percent : 100); + const safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); + const statusLabel = status === 'running' + ? `Searching... ${Math.round(safePercent)}%` + : (progress?.message || statusLabels[status] || status); html += `
@@ -280,7 +371,7 @@ class LinkerManagerDialog extends ComfyDialog { ${this.escapeHtml(statusLabel)}
-
+
`; @@ -4699,6 +4790,7 @@ class LinkerManagerDialog extends ComfyDialog { throw new Error(`${selectedSourceLabel} is not available in this install`); } + this.clearSearchProgressTimers(state.activeSearchRunId); searchRunId = `${Date.now()}-${Math.random().toString(36).slice(2)}`; state.activeSearchRunId = searchRunId; state.lastAttemptSources = sourceIds; @@ -4708,7 +4800,9 @@ class LinkerManagerDialog extends ComfyDialog { for (const source of sourceIds) { this.setSourceProgress(state, source, { status: 'running', - message: 'Searching...' + percent: 6, + startedAt: Date.now(), + estimateMs: this.getSearchSourceEstimateMs(source, isUrn) }); } @@ -4720,6 +4814,15 @@ class LinkerManagerDialog extends ComfyDialog { resultsDiv.style.display = 'block'; this.displaySearchResults(missing, state, resultsDiv); } + for (const source of sourceIds) { + this.startEstimatedSearchProgress( + state, + missing, + resultsDiv, + source, + searchRunId + ); + } // For URNs, include model_id and version_id for direct download const tokens = this.getStoredTokens(); @@ -4782,8 +4885,10 @@ class LinkerManagerDialog extends ComfyDialog { state.results = this.mergeSearchResults(state.results, data); state.lastAttemptSources = Array.from(attemptedSources); state.lastAttemptFound = anyFound; + this.clearSearchProgressTimer(searchRunId, source); this.setSourceProgress(state, source, { status: found ? 'found' : 'none', + percent: 100, message: found ? 'Found' : 'No match' }); @@ -4811,8 +4916,10 @@ class LinkerManagerDialog extends ComfyDialog { attemptedSources.add(source); state.lastAttemptSources = Array.from(attemptedSources); state.lastAttemptFound = anyFound; + this.clearSearchProgressTimer(searchRunId, source); this.setSourceProgress(state, source, { status: 'error', + percent: 100, message: error.message || 'Error' }); this.displaySearchResults(missing, state, resultsDiv); @@ -4821,6 +4928,7 @@ class LinkerManagerDialog extends ComfyDialog { }); await Promise.all(searchPromises); + this.clearSearchProgressTimers(searchRunId); if (state.activeSearchRunId === searchRunId) { state.activeSearchRunId = null; @@ -4834,12 +4942,14 @@ class LinkerManagerDialog extends ComfyDialog { } catch (error) { console.error('Model Linker: Search error:', error); + this.clearSearchProgressTimers(searchRunId); state.lastAttemptError = error.message; if (resultsDiv) { resultsDiv.innerHTML = this.renderStatusMessage(`Search failed: ${error.message}`, 'error'); } } finally { if (!searchRunId || state.activeSearchRunId === searchRunId) { + this.clearSearchProgressTimers(searchRunId); state.activeSearchRunId = null; } if (searchBtn) {