feat: improve model scan cache override, combo UX, and filename handling
- Add force_rescan end-to-end so callers can bypass the short-lived local model scan cache: backend entry in __init__.py and core/resolver.py, lifecycle_graph_methods forwards it through the workflow analysis body, and download_target_methods passes ?force=1 from the refresh flow. - Rewrite combo dropdown lifecycle in missing_browser_methods with show/hide helpers, pointer-capture tracking, and a capture-phase outside-click listener so the list stays open during drag/hover and closes reliably otherwise. - Animate refresh buttons (CSS keyframes + JS Web Animations) with reduced-motion fallback. - Encode selected model path into the selected-bar data-model attribute and wire contextmenu so users can open the containing folder from the selection. - Fix XSS risk in selected-bar label by escaping label HTML; change queue/selected Remove buttons to danger style. - Increase max missing-browser model column width clamp from 430 to 820 and stop truncating missing-filename display; show full filename with tooltip instead. - Update huggingface-author-index.json metadata timestamp/counts (1 file changed, 1 line).
This commit is contained in:
@@ -218,7 +218,7 @@ export const missingBrowserMethods = {
|
||||
}
|
||||
|
||||
return {
|
||||
modelPx: clamp(modelPx, 180, 430),
|
||||
modelPx: clamp(modelPx, 180, 820),
|
||||
typePx: clamp(typePx, 66, 120)
|
||||
};
|
||||
},
|
||||
@@ -255,7 +255,7 @@ export const missingBrowserMethods = {
|
||||
<span class="mr-missing-stat mr-missing-stat-none">${stats.none} no match</span>
|
||||
</div>
|
||||
<button id="mr-refresh-missing-analysis" type="button" class="mr-btn mr-btn-secondary mr-btn-sm mr-missing-refresh-btn" data-tooltip="Re-analyze workflow and refresh local matches">
|
||||
${getSvgIcon('refreshCw')} Refresh
|
||||
<span class="mr-refresh-spin-target">${getSvgIcon('refreshCw')}</span> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,7 +278,6 @@ export const missingBrowserMethods = {
|
||||
const isSelected = key === selectedKey;
|
||||
const isBatchSelected = this.batchSelectedMissingKeys?.has(key);
|
||||
const filename = this.getMissingFilename(missing);
|
||||
const formattedFilename = this.formatFilename(filename, 46);
|
||||
const bestMatch = this.getBestLocalMatch(missing, 70);
|
||||
const confidence = bestMatch ? Number(bestMatch.confidence || 0) : 0;
|
||||
const matchName = bestMatch?.model?.relative_path || bestMatch?.filename || bestMatch?.path || '';
|
||||
@@ -302,7 +301,7 @@ export const missingBrowserMethods = {
|
||||
</span>
|
||||
<span class="mr-missing-row-index">${index + 1}</span>
|
||||
<span class="mr-missing-row-model">
|
||||
<span class="mr-missing-row-name" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(formattedFilename.display)}</span>
|
||||
<span class="mr-missing-row-name" data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>
|
||||
${rowNodeHtml}
|
||||
</span>
|
||||
<span class="mr-missing-row-type ${typeColorClass}">${this.escapeHtml(typeLabel)}</span>
|
||||
@@ -598,17 +597,62 @@ export const missingBrowserMethods = {
|
||||
const comboInput = container.querySelector(`#combo-input-${comboId}`);
|
||||
const comboList = container.querySelector(`#combo-list-${comboId}`);
|
||||
const comboRefresh = container.querySelector(`#combo-refresh-${comboId}`);
|
||||
const comboSection = comboInput?.closest('.mr-combo-section')
|
||||
|| comboList?.closest('.mr-combo-section')
|
||||
|| comboRefresh?.closest('.mr-combo-section');
|
||||
|
||||
if (comboList) {
|
||||
this.enableWheelScrollChaining(comboList);
|
||||
}
|
||||
|
||||
const allModels = Array.isArray(this.allModels) ? this.allModels : [];
|
||||
const getAllModels = () => Array.isArray(this.allModels) ? this.allModels : [];
|
||||
const buildLabel = (m) => `${m.category ? m.category + ': ' : ''}${m.relative_path || m.filename || ''}`;
|
||||
const getFolder = (m) => m.path || m.base_directory || '';
|
||||
let comboListPointerActive = false;
|
||||
let outsideComboPointerHandler = null;
|
||||
const isInsideCombo = (target) => (
|
||||
Boolean(target) && (
|
||||
Boolean(comboSection?.contains(target))
|
||||
|| Boolean(comboInput?.contains(target))
|
||||
|| Boolean(comboList?.contains(target))
|
||||
|| Boolean(comboRefresh?.contains(target))
|
||||
)
|
||||
);
|
||||
const stopOutsideComboPointerListener = () => {
|
||||
if (!outsideComboPointerHandler) return;
|
||||
window.removeEventListener('pointerdown', outsideComboPointerHandler, true);
|
||||
outsideComboPointerHandler = null;
|
||||
};
|
||||
const startOutsideComboPointerListener = () => {
|
||||
if (outsideComboPointerHandler) return;
|
||||
outsideComboPointerHandler = (event) => {
|
||||
if (!comboList?.isConnected) {
|
||||
stopOutsideComboPointerListener();
|
||||
return;
|
||||
}
|
||||
if (isInsideCombo(event.target)) {
|
||||
return;
|
||||
}
|
||||
hideComboList();
|
||||
};
|
||||
window.addEventListener('pointerdown', outsideComboPointerHandler, true);
|
||||
};
|
||||
const showComboList = () => {
|
||||
if (!comboList) return;
|
||||
comboList.classList.remove('mr-is-hidden');
|
||||
comboList.classList.add('mr-is-visible');
|
||||
startOutsideComboPointerListener();
|
||||
};
|
||||
const hideComboList = () => {
|
||||
if (!comboList) return;
|
||||
comboList.classList.remove('mr-is-visible');
|
||||
comboList.classList.add('mr-is-hidden');
|
||||
stopOutsideComboPointerListener();
|
||||
};
|
||||
|
||||
const populateComboOptions = (filterText, highlightIdx = -1) => {
|
||||
if (!comboList) return;
|
||||
const allModels = getAllModels();
|
||||
const f = (filterText || '').toLowerCase();
|
||||
const filtered = f
|
||||
? allModels.filter(m => buildLabel(m).toLowerCase().includes(f))
|
||||
@@ -639,6 +683,7 @@ export const missingBrowserMethods = {
|
||||
const chosenModel = allModels[idx];
|
||||
if (chosenModel) {
|
||||
this.queueResolution(missing, chosenModel);
|
||||
hideComboList();
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -646,6 +691,16 @@ export const missingBrowserMethods = {
|
||||
};
|
||||
|
||||
if (comboList) {
|
||||
comboList.addEventListener('pointerdown', () => {
|
||||
comboListPointerActive = true;
|
||||
const releaseComboPointer = () => {
|
||||
setTimeout(() => {
|
||||
comboListPointerActive = false;
|
||||
}, 0);
|
||||
};
|
||||
window.addEventListener('pointerup', releaseComboPointer, { once: true, capture: true });
|
||||
window.addEventListener('pointercancel', releaseComboPointer, { once: true, capture: true });
|
||||
}, { capture: true });
|
||||
populateComboOptions('');
|
||||
}
|
||||
|
||||
@@ -655,27 +710,40 @@ export const missingBrowserMethods = {
|
||||
}, 200);
|
||||
comboInput.addEventListener('input', debouncedFilter);
|
||||
comboInput.addEventListener('focus', () => {
|
||||
if (comboList) {
|
||||
comboList.classList.remove('mr-is-hidden');
|
||||
comboList.classList.add('mr-is-visible');
|
||||
}
|
||||
showComboList();
|
||||
populateComboOptions(comboInput.value);
|
||||
});
|
||||
comboInput.addEventListener('blur', () => {
|
||||
setTimeout(() => {
|
||||
if (comboList) {
|
||||
comboList.classList.remove('mr-is-visible');
|
||||
comboList.classList.add('mr-is-hidden');
|
||||
if (!comboList) return;
|
||||
if (comboListPointerActive || comboList.matches(':hover')) {
|
||||
return;
|
||||
}
|
||||
hideComboList();
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
if (comboRefresh) {
|
||||
comboRefresh.addEventListener('click', async () => {
|
||||
this.allModels = null;
|
||||
await this.ensureAllModelsLoaded();
|
||||
populateComboOptions(comboInput?.value || '');
|
||||
const minRefreshFeedback = new Promise(resolve => setTimeout(resolve, 420));
|
||||
const refreshAnimation = this.startRefreshButtonAnimation(comboRefresh);
|
||||
try {
|
||||
comboRefresh.disabled = true;
|
||||
comboRefresh.classList.add('mr-btn-is-disabled', 'mr-is-refreshing');
|
||||
this.allModels = null;
|
||||
await this.ensureAllModelsLoaded({ force: true });
|
||||
await minRefreshFeedback;
|
||||
populateComboOptions(comboInput?.value || '');
|
||||
} catch (error) {
|
||||
await minRefreshFeedback;
|
||||
console.warn('Model Resolver: could not refresh local model list', error);
|
||||
this.showNotification('Failed to refresh local model list', 'error');
|
||||
} finally {
|
||||
refreshAnimation?.cancel();
|
||||
comboRefresh.disabled = false;
|
||||
comboRefresh.classList.remove('mr-btn-is-disabled', 'mr-is-refreshing');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -690,6 +758,29 @@ export const missingBrowserMethods = {
|
||||
this.restoreDownloadProgressForMissing?.(missing);
|
||||
},
|
||||
|
||||
startRefreshButtonAnimation(button) {
|
||||
if (!button) return null;
|
||||
|
||||
const target = button.querySelector('.mr-refresh-spin-target') || button.querySelector('svg');
|
||||
if (!target || typeof target.animate !== 'function') return null;
|
||||
|
||||
const existing = target.getAnimations?.() || [];
|
||||
existing.forEach(animation => animation.cancel());
|
||||
target.style.transformOrigin = 'center';
|
||||
|
||||
return target.animate(
|
||||
[
|
||||
{ transform: 'rotate(0deg)' },
|
||||
{ transform: 'rotate(360deg)' }
|
||||
],
|
||||
{
|
||||
duration: 620,
|
||||
easing: 'linear',
|
||||
iterations: Infinity
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Display missing models in the dialog
|
||||
*/
|
||||
@@ -803,23 +894,27 @@ export const missingBrowserMethods = {
|
||||
async refreshMissingAnalysis(button = null) {
|
||||
if (button?.disabled) return;
|
||||
|
||||
const minRefreshFeedback = new Promise(resolve => setTimeout(resolve, 420));
|
||||
const refreshAnimation = this.startRefreshButtonAnimation(button);
|
||||
try {
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.classList.add('mr-btn-is-disabled');
|
||||
button.classList.add('mr-btn-is-disabled', 'mr-is-refreshing');
|
||||
}
|
||||
|
||||
this.showNotification('Refreshing missing models and local matches...', 'info');
|
||||
this.allModels = null;
|
||||
this.invalidateLoadedModelsCacheForActiveWorkflow?.();
|
||||
await minRefreshFeedback;
|
||||
await this.loadWorkflowData(null, { force: true });
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: missing analysis refresh failed:', error);
|
||||
this.showNotification('Refresh failed: ' + error.message, 'error');
|
||||
} finally {
|
||||
if (button) {
|
||||
refreshAnimation?.cancel();
|
||||
button.disabled = false;
|
||||
button.classList.remove('mr-btn-is-disabled');
|
||||
button.classList.remove('mr-btn-is-disabled', 'mr-is-refreshing');
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -835,8 +930,7 @@ export const missingBrowserMethods = {
|
||||
const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
|
||||
const otherMatches = filteredMatches.filter(m => m.confidence < 100 && m.confidence >= 70);
|
||||
|
||||
// Format the missing filename for display
|
||||
const missingFilename = this.formatFilename(missing.original_path, 60);
|
||||
const missingFilename = this.getMissingFilename(missing);
|
||||
|
||||
// Determine node info for the chip
|
||||
const nodeDisplay = this.getMissingNodeDisplay(missing);
|
||||
@@ -850,7 +944,7 @@ export const missingBrowserMethods = {
|
||||
html += `<div class="mr-card-title-wrap">`;
|
||||
|
||||
const titleMetaParts = [];
|
||||
let titlePrimaryHtml = `<span class="mr-card-title-primary" data-tooltip="${this.escapeHtml(missingFilename.full)}">${missingFilename.display}</span>`;
|
||||
let titlePrimaryHtml = `<span class="mr-card-title-primary" data-tooltip="${this.escapeHtml(missingFilename)}">${this.escapeHtml(missingFilename)}</span>`;
|
||||
let titleSecondaryHtml = '';
|
||||
|
||||
const modelId = missing.urn_model_id || missing.urn?.model_id;
|
||||
@@ -859,7 +953,7 @@ export const missingBrowserMethods = {
|
||||
const urnLoadingId = `urn-loading-${missing.node_id}-${missing.widget_index}`;
|
||||
|
||||
if (missing.is_urn) {
|
||||
titleMetaParts.push(`<span class="mr-card-title-eyebrow" data-tooltip="${this.escapeHtml(missingFilename.full)}">${missingFilename.display}</span>`);
|
||||
titleMetaParts.push(`<span class="mr-card-title-eyebrow" data-tooltip="${this.escapeHtml(missingFilename)}">${this.escapeHtml(missingFilename)}</span>`);
|
||||
}
|
||||
|
||||
if (missing.is_urn && !missing.civitai_info) {
|
||||
@@ -923,7 +1017,7 @@ export const missingBrowserMethods = {
|
||||
html += `<div class="mr-combo-row">`;
|
||||
html += `<label class="mr-combo-label">Model</label>`;
|
||||
html += `<input id="combo-input-${comboId}" class="mr-combo-input" type="text" placeholder="Type to filter local models...">`;
|
||||
html += `<button id="combo-refresh-${comboId}" type="button" aria-label="Reload local model list" data-tooltip="Reload local model list" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-combo-refresh-btn">${getSvgIcon('refreshCw', 'currentColor', 'mr-combo-refresh-icon')}</button>`;
|
||||
html += `<button id="combo-refresh-${comboId}" type="button" aria-label="Reload local model list" data-tooltip="Reload local model list" class="mr-btn mr-btn-secondary mr-btn-sm mr-btn-icon-only mr-combo-refresh-btn"><span class="mr-refresh-spin-target">${getSvgIcon('refreshCw', 'currentColor', 'mr-combo-refresh-icon')}</span></button>`;
|
||||
html += `</div>`;
|
||||
html += `<div id="combo-list-${comboId}" class="mr-combo-list"></div>`;
|
||||
html += `</div>`;
|
||||
|
||||
Reference in New Issue
Block a user