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 += `