diff --git a/web/css/linker-main.css b/web/css/linker-main.css
index fd8098f..77d4636 100644
--- a/web/css/linker-main.css
+++ b/web/css/linker-main.css
@@ -19,24 +19,6 @@
color: var(--ml-text-muted);
}
-.ml-status-card-vertical {
- flex-direction: column;
- align-items: flex-start;
-}
-
-.ml-action-row {
- margin-top: var(--ml-spacing-8);
- display: flex;
- gap: var(--ml-spacing-8);
-}
-
-.ml-search-results-stack {
- margin-top: var(--ml-spacing-8);
- display: flex;
- flex-direction: column;
- gap: var(--ml-spacing-8);
-}
-
.ml-stack-md {
display: flex;
flex-direction: column;
@@ -764,6 +746,118 @@
margin-top: 8px;
display: none;
}
+.ml-search-results-table-wrap {
+ margin-top: 8px;
+ overflow-x: auto;
+ background: rgba(255,255,255,0.025);
+ border: 1px solid var(--ml-border);
+ border-radius: 8px;
+}
+.ml-search-results-table {
+ width: 100%;
+ min-width: 620px;
+ border-collapse: collapse;
+ table-layout: fixed;
+ font-size: 11px;
+}
+.ml-search-results-table th,
+.ml-search-results-table td {
+ padding: 7px 10px;
+ border-bottom: 1px solid rgba(255,255,255,0.065);
+ vertical-align: middle;
+}
+.ml-search-results-table th {
+ color: var(--ml-text-muted);
+ font-size: 10px;
+ font-weight: 700;
+ text-align: left;
+ background: rgba(255,255,255,0.035);
+}
+.ml-search-results-table tr:last-child td {
+ border-bottom: 0;
+}
+.ml-search-col-source {
+ width: 124px;
+}
+.ml-search-col-match {
+ width: 78px;
+}
+.ml-search-col-size {
+ width: 92px;
+}
+.ml-search-col-actions {
+ width: 86px;
+}
+.ml-search-source-pill {
+ display: inline-flex;
+ max-width: 100%;
+ min-height: 18px;
+ align-items: center;
+ padding: 2px 7px;
+ overflow: hidden;
+ color: var(--ml-text);
+ background: rgba(255,255,255,0.1);
+ border: 1px solid rgba(255,255,255,0.08);
+ border-radius: 999px;
+ font-size: 9px;
+ font-weight: 700;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.ml-search-result-model {
+ display: grid;
+ min-width: 0;
+ gap: 2px;
+}
+.ml-search-result-model span,
+.ml-search-result-model small {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.ml-search-result-model span {
+ color: var(--ml-text);
+ font-weight: 650;
+}
+.ml-search-result-model small,
+.ml-search-size,
+.ml-search-result-empty {
+ color: var(--ml-text-muted);
+ font-size: 10px;
+}
+.ml-search-match {
+ font-weight: 750;
+}
+.ml-search-match-strong {
+ color: #72d38a;
+}
+.ml-search-match-medium {
+ color: #f1cf64;
+}
+.ml-search-match-weak {
+ color: #8fd0ff;
+}
+.ml-search-match-neutral {
+ color: var(--ml-text-muted);
+}
+.ml-search-result-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 6px;
+}
+.ml-search-result-action-btn {
+ width: 24px;
+ min-width: 24px;
+ min-height: 24px;
+ padding: 0;
+ border-radius: 7px;
+}
+.ml-search-result-action-btn svg {
+ width: 13px;
+ height: 13px;
+ display: block;
+}
.ml-search-progress-list {
display: grid;
gap: 6px;
diff --git a/web/linker.js b/web/linker.js
index 9d94b4a..8edd365 100644
--- a/web/linker.js
+++ b/web/linker.js
@@ -363,53 +363,119 @@ class LinkerManagerDialog extends ComfyDialog {
return html;
}
- formatSearchResultMeta(result = {}, options = {}) {
- const filename = options.filename || result.filename || '';
- const secondaryText = options.secondaryText || '';
- const confidenceValue = result.confidence;
- const confidenceDisplay = typeof confidenceValue === 'number'
- ? `${Math.round(confidenceValue)}% match`
- : '';
- const sizeDisplay = result.size
- ? (typeof result.size === 'number' ? this.formatBytes(result.size) : result.size)
- : '';
-
- let html = `
`;
+ const rows = [];
+ let statusHtml = '';
if (!hasActiveProgress && state?.lastAttemptError) {
- html += this.renderStatusMessage(state.lastAttemptError, 'error');
+ statusHtml += this.renderStatusMessage(state.lastAttemptError, 'error');
} else if (!hasActiveProgress && state?.lastAttemptFound === false) {
const searchedLabel = (state.lastAttemptSources || []).map(source => this.getSearchSourceLabel(source)).join(', ');
- html += this.renderStatusMessage(`No new matches found in ${searchedLabel}. Existing results are kept below.`, 'warning');
+ statusHtml += this.renderStatusMessage(`No new matches found in ${searchedLabel}. Existing results are kept below.`, 'warning');
}
- // Popular models result (highest priority)
if (popular) {
const popularFilename = popular.filename || missing.original_path?.split('/').pop()?.split('\\').pop() || '';
- const actionHtml = `
`;
- html += this.renderOnlineSearchResultCard({
- statusClass: 'ml-status-success',
- title: 'Found in Popular Models',
- result: popular,
+ rows.push({
+ sourceKey: 'popular',
+ sourceLabel: 'Popular',
+ model: popular.name || popularFilename,
filename: popularFilename,
- secondaryText: popular.name || '',
- actionHtml
+ secondary: popular.name && popular.name !== popularFilename ? popularFilename : '',
+ match: this.getSearchResultMatchDisplay(popular, 'Known', 'strong'),
+ size: this.formatSearchResultSize(popular),
+ downloadUrl: popular.url,
+ downloadFilename: popularFilename,
+ category: popular.directory || missing.category,
+ openUrl: this.getModelCardUrl(popular.url)
});
}
- // Model list result (ComfyUI Manager database with fuzzy matching)
if (modelListResult && modelListResult.url) {
- const matchType = modelListResult.match_type === 'exact' ? 'Exact match' : 'Similar model';
- const statusClass = modelListResult.match_type === 'exact' ? 'ml-status-success' : 'ml-status-info';
- const actionHtml = `
`;
- html += this.renderOnlineSearchResultCard({
- statusClass,
- title: `${matchType} in Model Database`,
- result: modelListResult,
+ rows.push({
+ sourceKey: 'model-list',
+ sourceLabel: 'Local Database',
+ model: modelListResult.name || modelListResult.filename,
filename: modelListResult.filename,
- secondaryText: modelListResult.name || '',
- actionHtml
+ secondary: modelListResult.name && modelListResult.name !== modelListResult.filename ? modelListResult.filename : '',
+ match: this.getSearchResultMatchDisplay(modelListResult),
+ size: this.formatSearchResultSize(modelListResult),
+ downloadUrl: modelListResult.url,
+ downloadFilename: modelListResult.filename,
+ category: modelListResult.directory || missing.category,
+ openUrl: this.getModelCardUrl(modelListResult.url)
});
}
- // HuggingFace result
if (hfResult && hfResult.url) {
const hfRepo = hfResult.repo_id || hfResult.repo || '';
const hfModelUrl = hfRepo ? `https://huggingface.co/${hfRepo}` : this.getModelCardUrl(hfResult.url);
- let actionHtml = `
`;
- if (hfModelUrl) {
- actionHtml += `
Open on HuggingFace`;
- }
- html += this.renderOnlineSearchResultCard({
- statusClass: 'ml-status-info',
- title: 'Found on HuggingFace',
- result: hfResult,
+ rows.push({
+ sourceKey: 'huggingface',
+ sourceLabel: 'HuggingFace',
+ model: hfRepo || hfResult.filename,
filename: hfResult.filename,
- secondaryText: hfRepo,
- actionHtml
+ secondary: hfResult.path && hfResult.path !== hfResult.filename ? hfResult.path : '',
+ match: this.getSearchResultMatchDisplay(hfResult),
+ size: this.formatSearchResultSize(hfResult),
+ downloadUrl: hfResult.url,
+ downloadFilename: hfResult.filename,
+ category: missing.category,
+ openUrl: hfModelUrl
});
}
- // LoRA Manager archive result
if (loraManagerArchiveResult && loraManagerArchiveResult.url) {
const archiveFilename = loraManagerArchiveResult.filename || missing.original_path?.split('/').pop()?.split('\\').pop() || '';
- const actionHtml = loraManagerArchiveResult.download_url
- ? `
`
- : '';
- html += this.renderOnlineSearchResultCard({
- statusClass: 'ml-status-info',
- title: 'Found in LoRA Manager Archive',
- result: loraManagerArchiveResult,
+ rows.push({
+ sourceKey: 'lora-archive',
+ sourceLabel: 'LoRA Archive',
+ model: loraManagerArchiveResult.name || loraManagerArchiveResult.version_name || archiveFilename,
filename: archiveFilename,
- secondaryText: loraManagerArchiveResult.version_name || loraManagerArchiveResult.name || '',
- actionHtml
+ secondary: loraManagerArchiveResult.version_name || '',
+ match: this.getSearchResultMatchDisplay(loraManagerArchiveResult),
+ size: this.formatSearchResultSize(loraManagerArchiveResult),
+ downloadUrl: loraManagerArchiveResult.download_url || '',
+ downloadFilename: archiveFilename,
+ category: missing.category,
+ openUrl: loraManagerArchiveResult.url
});
}
- // CivitAI result
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}` : ''}` : '');
- // Use expected_filename from URN resolution, or fallback to civitaiResult filename
const downloadFilename = missing.civitai_info?.expected_filename || civitaiResult.filename || civitaiResult.name;
- // Build display name with version if available
- const modelName = missing.civitai_info?.version_name ? `${missing.civitai_info.model_name} v${missing.civitai_info.version_name}` : (civitaiResult.name || 'Model');
- const topLineHtml = `
${civitaiResult.type || ''}
`;
- const actionHtml = `
`;
- html += this.renderOnlineSearchResultCard({
- statusClass: 'ml-status-warning',
- title: 'Found on CivitAI',
- result: civitaiResult,
+ const modelName = missing.civitai_info?.version_name ? `${missing.civitai_info.model_name} v${missing.civitai_info.version_name}` : (civitaiResult.name || downloadFilename || 'Model');
+ rows.push({
+ sourceKey: 'civitai',
+ sourceLabel: 'CivitAI',
+ model: modelName,
filename: downloadFilename,
- secondaryText: '',
- topLineHtml,
- actionHtml
+ secondary: civitaiResult.type || civitaiResult.base_model || '',
+ match: this.getSearchResultMatchDisplay(civitaiResult),
+ size: this.formatSearchResultSize(civitaiResult),
+ downloadUrl: civitaiResult.download_url,
+ downloadFilename,
+ category: missing.category,
+ openUrl: modelUrl
});
}
- html += '
';
+ const html = `${progressHtml}${statusHtml}${this.renderSearchResultsTable(rows)}`;
container.innerHTML = html;
// Attach download listeners
diff --git a/web/utils/icon_utils.js b/web/utils/icon_utils.js
index c74095f..17985c6 100644
--- a/web/utils/icon_utils.js
+++ b/web/utils/icon_utils.js
@@ -4,6 +4,8 @@ import { app } from "../../../scripts/app.js";
const SVG_ICONS = {
search: `