Refactor search results to table layout with helper methods and new styles; add download and externalLink SVG icons.

This commit is contained in:
Dariusz L
2026-05-26 16:13:55 +02:00
parent bebf5b6e59
commit bab7cd266f
3 changed files with 284 additions and 117 deletions
+112 -18
View File
@@ -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;
+170 -99
View File
@@ -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 = `<div class="ml-meta-line">`;
if (filename) {
html += `<span class="ml-chip">${filename}</span>`;
}
if (confidenceDisplay) {
html += ` <span class="ml-chip">${confidenceDisplay}</span>`;
}
if (sizeDisplay) {
html += ` <span class="ml-download-size">[${sizeDisplay}]</span>`;
}
if (secondaryText) {
html += ` <span class="ml-meta-secondary">${secondaryText}</span>`;
}
html += `</div>`;
return html;
formatSearchResultSize(result = {}) {
if (result.size === 0) return '0 B';
if (!result.size) return '';
return typeof result.size === 'number' ? this.formatBytes(result.size) : String(result.size);
}
renderOnlineSearchResultCard({
statusClass,
title,
result,
filename,
secondaryText = '',
actionHtml = '',
topLineHtml = ''
}) {
let html = `<div class="ml-status ${statusClass} ml-status-card-vertical">`;
html += `<strong>${title}</strong>`;
if (topLineHtml) {
html += topLineHtml;
getSearchResultMatchDisplay(result = {}, fallbackLabel = 'Match', fallbackClass = 'neutral') {
const matchType = String(result.match_type || '').toLowerCase();
if (matchType === 'exact') {
return { label: 'Exact', className: 'strong' };
}
html += this.formatSearchResultMeta(result, { filename, secondaryText });
if (actionHtml) {
html += `<div class="ml-action-row">${actionHtml}</div>`;
const confidence = Number(result.confidence);
if (Number.isFinite(confidence) && confidence > 0) {
const className = confidence >= 95 ? 'strong' : (confidence >= 70 ? 'medium' : 'weak');
return { label: `${Math.round(confidence)}%`, className };
}
html += `</div>`;
if (matchType === 'partial') {
return { label: 'Partial', className: 'medium' };
}
if (matchType === 'fuzzy' || matchType === 'similar') {
return { label: matchType === 'fuzzy' ? 'Fuzzy' : 'Similar', className: 'medium' };
}
return { label: fallbackLabel, className: fallbackClass };
}
renderSearchResultsTable(rows = []) {
if (!rows.length) return '';
let html = `
<div class="ml-search-results-table-wrap">
<table class="ml-search-results-table">
<colgroup>
<col class="ml-search-col-source">
<col class="ml-search-col-model">
<col class="ml-search-col-match">
<col class="ml-search-col-size">
<col class="ml-search-col-actions">
</colgroup>
<thead>
<tr>
<th>Source</th>
<th>Model</th>
<th>Match</th>
<th>Size</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
`;
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 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' };
const matchClass = String(match.className || 'neutral').replace(/[^a-z0-9_-]/gi, '');
const size = this.escapeHtml(row.size || '-');
const downloadUrl = row.downloadUrl || '';
const openUrl = row.openUrl || '';
const downloadFilename = row.downloadFilename || row.filename || row.model || 'model';
const category = row.category || '';
let actions = '';
if (downloadUrl) {
actions += `
<button class="search-download-btn ml-btn ml-btn-secondary ml-btn-sm ml-search-result-action-btn"
title="Download"
aria-label="Download ${this.escapeHtml(downloadFilename)}"
data-url="${this.escapeHtml(downloadUrl)}"
data-filename="${this.escapeHtml(downloadFilename)}"
data-category="${this.escapeHtml(category)}">${getSvgIcon('download')}</button>
`;
}
if (openUrl) {
actions += `
<a href="${this.escapeHtml(openUrl)}"
target="_blank"
rel="noopener noreferrer"
class="ml-btn ml-btn-secondary ml-btn-sm ml-search-result-action-btn"
title="Open model page"
aria-label="Open model page">${getSvgIcon('externalLink')}</a>
`;
}
if (!actions) {
actions = '<span class="ml-search-result-empty">-</span>';
}
html += `
<tr>
<td><span class="ml-search-source-pill ml-search-source-${sourceKey}">${sourceLabel}</span></td>
<td>
<div class="ml-search-result-model" title="${model}">
<span>${model}</span>
${secondary || filename ? `<small>${secondary || filename}</small>` : ''}
</div>
</td>
<td><span class="ml-search-match ml-search-match-${matchClass}">${this.escapeHtml(match.label)}</span></td>
<td class="ml-search-size">${size}</td>
<td><div class="ml-search-result-actions">${actions}</div></td>
</tr>
`;
}
html += `
</tbody>
</table>
</div>
`;
return html;
}
@@ -4934,99 +5000,104 @@ class LinkerManagerDialog extends ComfyDialog {
return;
}
let html = `${progressHtml}<div class="ml-search-results-stack">`;
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 = `<button class="search-download-btn ml-btn ml-btn-primary ml-btn-sm" data-url="${popular.url}" data-filename="${popularFilename}" data-category="${popular.directory || missing.category}"><span class="ml-btn-icon">☁</span> Download</button>`;
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 = `<button class="search-download-btn ml-btn ml-btn-primary ml-btn-sm" data-url="${modelListResult.url}" data-filename="${modelListResult.filename}" data-category="${modelListResult.directory || missing.category}"><span class="ml-btn-icon">☁</span> Download</button>`;
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 = `<button class="search-download-btn ml-btn ml-btn-link ml-btn-sm" data-url="${hfResult.url}" data-filename="${hfResult.filename}" data-category="${missing.category}"><span class="ml-btn-icon">☁</span> Download</button>`;
if (hfModelUrl) {
actionHtml += `<a href="${hfModelUrl}" target="_blank" rel="noopener noreferrer" class="ml-btn ml-btn-secondary ml-btn-sm">Open on HuggingFace</a>`;
}
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
? `<button class="search-download-btn ml-btn ml-btn-secondary ml-btn-sm" data-url="${loraManagerArchiveResult.download_url}" data-filename="${archiveFilename}" data-category="${missing.category}"><span class="ml-btn-icon">☁</span> Download</button>`
: '';
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 = `<div class="ml-meta-line"><button class="ml-btn ml-btn-sm ml-civitai-open-btn" onclick="window.open('${modelUrl}', '_blank')">${modelName}</button> <span class="ml-meta-secondary">${civitaiResult.type || ''}</span></div>`;
const actionHtml = `<button class="search-download-btn ml-btn ml-btn-sm ml-civitai-download-btn" data-url="${civitaiResult.download_url}" data-filename="${downloadFilename}" data-category="${missing.category}"><span class="ml-btn-icon">☁</span> Download</button>`;
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 += '</div>';
const html = `${progressHtml}${statusHtml}${this.renderSearchResultsTable(rows)}`;
container.innerHTML = html;
// Attach download listeners
+2
View File
@@ -4,6 +4,8 @@ import { app } from "../../../scripts/app.js";
const SVG_ICONS = {
search: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="6.5"></circle><path d="M16 16l5 5"></path></svg>`,
locate: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12h3"></path><path d="M19 12h3"></path><path d="M12 2v3"></path><path d="M12 19v3"></path><circle cx="12" cy="12" r="7"></circle><circle cx="12" cy="12" r="2.5"></circle></svg>`,
download: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg>`,
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 3h6v6"></path><path d="M10 14 21 3"></path><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path></svg>`,
eye: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"></path><circle cx="12" cy="12" r="3"></circle></svg>`,
eyeOff: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M17.94 17.94A10.94 10.94 0 0 1 12 19C5 19 1 12 1 12a21.77 21.77 0 0 1 5.06-5.94"></path><path d="M9.9 4.24A10.93 10.93 0 0 1 12 4c7 0 11 8 11 8a21.72 21.72 0 0 1-4.31 5.18"></path><path d="M14.12 14.12a3 3 0 1 1-4.24-4.24"></path><path d="M1 1l22 22"></path></svg>`,
civitai: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 178 178" aria-hidden="true"><defs><linearGradient id="ml-civitai-gradient" gradientUnits="userSpaceOnUse" x1="89.3" y1="-665.5" x2="89.3" y2="-841.1" gradientTransform="matrix(1 0 0 -1 0 -664)"><stop offset="0" stop-color="#1284F7"></stop><stop offset="1" stop-color="#0A20C9"></stop></linearGradient></defs><path fill="#000" d="M13.3,45.4v87.7l76,43.9l76-43.9V45.4l-76-43.9L13.3,45.4z"></path><path fill="url(#ml-civitai-gradient)" d="M89.3,29.2l52,30v60l-52,30l-52-30v-60L89.3,29.2 M89.3,1.5l-76,43.9v87.8l76,43.9l76-43.9V45.4L89.3,1.5z"></path><path fill="#FFF" d="M104.1,97.2l-14.9,8.5l-14.9-8.5v-17l14.9-8.5l14.9,8.5h18.2V69.7l-33-19l-33,19v38.1l33,19l33-19V97.2H104.1z"></path></svg>`