From 497818d1ee76ed3108b33174d7e1ff4601a8a87d Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 26 May 2026 17:02:36 +0200 Subject: [PATCH] Render and normalize model version names in Linker UI Add helper methods to normalize and split model and version names, render versioned model names in search results, cards, loading states and CivitAI links, include version metadata on rows, and add CSS styling for model version text. Improves display clarity when models include version names and avoids duplicated filenames. --- web/css/linker-main.css | 5 +++ web/linker.js | 82 +++++++++++++++++++++++++++++++---------- 2 files changed, 67 insertions(+), 20 deletions(-) diff --git a/web/css/linker-main.css b/web/css/linker-main.css index cd41eeb..e94822a 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -784,6 +784,11 @@ color: var(--ml-text); font-weight: 650; } +.ml-model-version { + color: var(--ml-text-secondary); + font-style: italic; + font-weight: 500; +} .ml-search-result-model small, .ml-search-size, .ml-search-result-empty { diff --git a/web/linker.js b/web/linker.js index b09037b..a270a9b 100644 --- a/web/linker.js +++ b/web/linker.js @@ -402,6 +402,37 @@ class LinkerManagerDialog extends ComfyDialog { this.syncSearchSourceUi(missing, container); } + normalizeVersionName(versionName) { + return String(versionName || '').trim().replace(/^v{2,}(?=\d)/i, 'v'); + } + + getModelVersionParts(modelName, versionName) { + const name = String(modelName || '').trim(); + const version = this.normalizeVersionName(versionName); + if (!version || version === name) { + return { name: name || version, version: '' }; + } + if (name && name.toLowerCase().includes(version.toLowerCase())) { + return { name, version: '' }; + } + return { name, version }; + } + + getVersionedModelName(modelName, versionName) { + const parts = this.getModelVersionParts(modelName, versionName); + if (!parts.version) return parts.name; + return parts.name ? `${parts.name} ${parts.version}` : parts.version; + } + + renderVersionedModelNameHtml(modelName, versionName) { + const parts = this.getModelVersionParts(modelName, versionName); + const nameHtml = parts.name ? this.escapeHtml(parts.name) : ''; + const versionHtml = parts.version + ? `${this.escapeHtml(parts.version)}` + : ''; + return [nameHtml, versionHtml].filter(Boolean).join(' '); + } + formatSearchResultSize(result = {}) { if (result.size === 0) return '0 B'; if (!result.size) return ''; @@ -455,13 +486,21 @@ class LinkerManagerDialog extends ComfyDialog { const modelUrl = downloadSource.model_url || downloadSource.workflow_model_url || this.getModelCardUrl(downloadSource.url); + const versionName = downloadSource.version_name || missing.civitai_info?.version_name || ''; + const modelParts = this.getModelVersionParts( + downloadSource.name || missing.civitai_info?.model_name || '', + versionName + ); + const modelName = modelParts.name || downloadFilename; + const fullModelName = this.getVersionedModelName(modelName, modelParts.version); return { sourceKey, sourceLabel, - model: downloadSource.name || downloadFilename, + model: modelName, + version: modelParts.version, filename: downloadFilename, - secondary: sourceSecondary || (downloadSource.name && downloadSource.name !== downloadFilename ? downloadFilename : ''), + secondary: sourceSecondary || (fullModelName && fullModelName !== downloadFilename ? downloadFilename : ''), match: isFromWorkflow ? { label: 'Provided', className: 'strong' } : this.getSearchResultMatchDisplay(downloadSource, 'Known', 'strong'), @@ -517,7 +556,11 @@ class LinkerManagerDialog extends ComfyDialog { for (const row of rows) { const sourceKey = String(row.sourceKey || '').replace(/[^a-z0-9_-]/gi, ''); const sourceLabel = this.escapeHtml(row.sourceLabel || row.sourceKey || 'Source'); - const model = this.escapeHtml(row.model || row.filename || 'Model'); + const rawModel = row.model || row.filename || 'Model'; + const rawVersion = row.version || ''; + const model = this.escapeHtml(rawModel); + const modelTitle = this.escapeHtml(this.getVersionedModelName(rawModel, rawVersion) || rawModel); + const modelHtml = this.renderVersionedModelNameHtml(rawModel, rawVersion) || model; const secondary = row.secondary ? this.escapeHtml(row.secondary) : ''; const filename = row.filename && row.filename !== row.model ? this.escapeHtml(row.filename) : ''; const match = row.match || { label: 'Match', className: 'neutral' }; @@ -557,8 +600,8 @@ class LinkerManagerDialog extends ComfyDialog { ${sourceLabel} -
- ${model} +
+ ${modelHtml} ${secondary || filename ? `${secondary || filename}` : ''}
@@ -1358,7 +1401,7 @@ class LinkerManagerDialog extends ComfyDialog { if (titleEl) { const modelName = data.model_name || data.modelName || 'Unknown Model'; const versionName = data.version_name || data.versionName || ''; - titleEl.textContent = versionName ? `${modelName} - ${versionName}` : modelName; + titleEl.innerHTML = this.renderVersionedModelNameHtml(modelName, versionName) || this.escapeHtml(modelName); } // Update type tag @@ -3990,15 +4033,9 @@ class LinkerManagerDialog extends ComfyDialog { } else if (missing.is_urn && missing.civitai_info) { // URN with resolved info - show model name/version const civitaiInfo = missing.civitai_info; - let civitaiLabel = ''; - if (civitaiInfo.model_name) { - civitaiLabel += civitaiInfo.model_name; - } - if (civitaiInfo.version_name && civitaiInfo.version_name !== civitaiInfo.model_name) { - civitaiLabel += ` v${civitaiInfo.version_name}`; - } - if (civitaiLabel) { - const linkHtml = modelUrl ? `${civitaiLabel}` : `${civitaiLabel}`; + const civitaiLabelHtml = this.renderVersionedModelNameHtml(civitaiInfo.model_name, civitaiInfo.version_name); + if (civitaiLabelHtml) { + const linkHtml = modelUrl ? `${civitaiLabelHtml}` : `${civitaiLabelHtml}`; titlePrimaryHtml = `${linkHtml}`; } if (civitaiInfo.expected_filename) { @@ -4978,9 +5015,10 @@ class LinkerManagerDialog extends ComfyDialog { const loadingEl = document.getElementById(loadingElementId); if (loadingEl && data.civitai) { const civitai = data.civitai; - const label = civitai.name || civitai.filename || 'Model'; + const labelHtml = this.renderVersionedModelNameHtml(civitai.name, civitai.version_name) + || this.escapeHtml(civitai.filename || 'Model'); const url = modelUrl || `https://civitai.com/models/${modelId}?modelVersionId=${versionId}`; - loadingEl.innerHTML = `${label}`; + loadingEl.innerHTML = `${labelHtml}`; } else if (loadingEl) { loadingEl.textContent = 'Not found'; loadingEl.style.color = 'var(--ml-text-muted)'; @@ -5005,6 +5043,7 @@ class LinkerManagerDialog extends ComfyDialog { url: data.civitai.download_url, filename: data.civitai.filename, name: data.civitai.name, + version_name: data.civitai.version_name, type: data.civitai.type, directory: missing.category || 'checkpoints', match_type: 'exact', @@ -5202,12 +5241,14 @@ class LinkerManagerDialog extends ComfyDialog { if (loraManagerArchiveResult && loraManagerArchiveResult.url) { const archiveFilename = loraManagerArchiveResult.filename || missing.original_path?.split('/').pop()?.split('\\').pop() || ''; + const archiveName = loraManagerArchiveResult.name || archiveFilename; addRow({ sourceKey: 'lora-archive', sourceLabel: 'LoRA Archive', - model: loraManagerArchiveResult.name || loraManagerArchiveResult.version_name || archiveFilename, + model: archiveName, + version: loraManagerArchiveResult.version_name || '', filename: archiveFilename, - secondary: loraManagerArchiveResult.version_name || '', + secondary: archiveName && archiveName !== archiveFilename ? archiveFilename : '', match: this.getSearchResultMatchDisplay(loraManagerArchiveResult), size: this.formatSearchResultSize(loraManagerArchiveResult), downloadUrl: loraManagerArchiveResult.download_url || '', @@ -5220,11 +5261,12 @@ class LinkerManagerDialog extends ComfyDialog { if (civitaiResult && civitaiResult.download_url) { const modelUrl = civitaiResult.url || (civitaiResult.model_id ? `https://civitai.com/models/${civitaiResult.model_id}${civitaiResult.version_id ? `?modelVersionId=${civitaiResult.version_id}` : ''}` : ''); const downloadFilename = missing.civitai_info?.expected_filename || civitaiResult.filename || civitaiResult.name; - const modelName = missing.civitai_info?.version_name ? `${missing.civitai_info.model_name} v${missing.civitai_info.version_name}` : (civitaiResult.name || downloadFilename || 'Model'); + const modelName = missing.civitai_info?.model_name || civitaiResult.name || downloadFilename || 'Model'; addRow({ sourceKey: 'civitai', sourceLabel: 'CivitAI', model: modelName, + version: missing.civitai_info?.version_name || civitaiResult.version_name || '', filename: downloadFilename, secondary: civitaiResult.type || civitaiResult.base_model || '', match: this.getSearchResultMatchDisplay(civitaiResult),