feat: add local model hash calculation with manual-progress notifications

This commit is contained in:
Dariusz L
2026-07-02 00:41:40 +02:00
parent 1128234bbd
commit 88cf8580f7
3 changed files with 175 additions and 12 deletions
@@ -2189,8 +2189,12 @@ export const missingBrowserMethods = {
* Show a notification banner (similar to ComfyUI's "Reconnecting" banner)
*/
showNotification(message, type = 'success', options = {}) {
const duration = type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000);
showNotificationUtils(message, type, duration, false, options);
const duration = options?.duration || (type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000));
return showNotificationUtils(message, type, {
...options,
duration,
deduplicate: options?.deduplicate || false
});
},
refreshMissingListRow(missing, options = {}) {
+132 -6
View File
@@ -265,6 +265,124 @@ export const modelInfoMethods = {
};
},
getHashCompareFilename(model = {}) {
return model.filename
|| model.name
|| String(this.getLocalHashComparePath(model) || '').split(/[\/\\]/).pop()
|| 'Selected local model';
},
formatHashCompareProgressMessage(filename = '', percent = 0, message = '') {
const safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
const status = message || 'Calculating SHA256...';
return `Calculating hash for ${filename}...\n${Math.round(safePercent)}% - ${status}`;
},
updateHashCompareModelMetadata(model = {}, result = {}) {
const sha256 = this.normalizeSha256ForCompare(result?.sha256 || result?.hash || '');
if (!sha256 || !model || typeof model !== 'object') return;
model.sha256 = sha256;
model.hash = sha256;
if (!model.hashes || typeof model.hashes !== 'object' || Array.isArray(model.hashes)) {
model.hashes = {};
}
model.hashes.SHA256 = sha256;
if (result.metadata_path) {
model.metadata_path = result.metadata_path;
}
},
async calculateLocalHashCandidatesForCompare(model = {}) {
const path = this.getLocalHashComparePath(model);
if (!path) {
throw new Error('No local file path available');
}
const filename = this.getHashCompareFilename(model);
const notification = this.showNotification?.(
this.formatHashCompareProgressMessage(filename, 0, 'Preparing hash calculation...'),
'info',
{ manualProgress: true }
);
try {
const start = await this.fetchJson('/model_resolver/calculate-file-hash/start', {
method: 'POST',
silent: true,
body: JSON.stringify({
file_path: path,
metadata_path: model.metadata_path || model.metadataPath || ''
})
}, 'Start model hash calculation');
const progressId = start?.progress_id;
if (!progressId) {
throw new Error('No progress id returned');
}
let result = null;
for (;;) {
await new Promise(resolve => setTimeout(resolve, 250));
const progress = await this.fetchJson(
`/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
{ silent: true },
'Get model hash progress'
);
notification?.updateProgress?.(
progress?.percent || 0,
this.formatHashCompareProgressMessage(
filename,
progress?.percent || 0,
progress?.message || ''
)
);
if (progress?.status === 'done') {
result = progress;
break;
}
if (progress?.status === 'cancelled') {
throw new Error('Hash calculation cancelled');
}
if (progress?.status === 'error') {
throw new Error(progress.error || progress.message || 'Hash calculation failed');
}
}
const sha256 = this.normalizeSha256ForCompare(result?.sha256 || result?.hash || '');
if (!sha256) {
throw new Error('No hash returned');
}
this.updateHashCompareModelMetadata(model, result);
notification?.updateProgress?.(
100,
result?.metadata_updated
? `Hash calculated and saved for ${filename}.`
: `Hash calculated for ${filename}.`
);
window.setTimeout(() => notification?.close?.(), 900);
return {
data: result,
candidates: this.collectHashCandidatesForCompare(
{
...result,
sha256,
hash: sha256,
hashes: { SHA256: sha256 }
},
'calculated hash',
new Set()
)
};
} catch (error) {
notification?.close?.();
throw error;
}
},
markSearchResultHashBadgesForMissing(missing = {}, sha256 = '') {
const hash = this.normalizeSha256ForCompare(sha256);
if (!missing || !hash) return false;
@@ -331,16 +449,24 @@ export const modelInfoMethods = {
}
}
const filename = model.filename
|| model.name
|| String(this.getLocalHashComparePath(model) || '').split(/[\/\\]/).pop()
|| 'Selected local model';
if (!localHashes.length) {
try {
const calculated = await this.calculateLocalHashCandidatesForCompare(model);
metadataResult = calculated.data || metadataResult;
localHashes = calculated.candidates;
} catch (error) {
this.showNotification?.(`Hash calculation failed: ${error?.message || error}`, 'error');
return;
}
}
const filename = this.getHashCompareFilename(model);
if (!localHashes.length) {
const metadataPath = metadataResult?.metadata_path || model.metadata_path || '';
this.showNotification?.(
metadataPath
? 'Local metadata exists, but no completed SHA256 hash was found.'
: 'No local SHA256 metadata found for this model.',
? 'Hash calculation finished, but no completed SHA256 hash was found.'
: 'No local SHA256 hash found for this model.',
'warning'
);
return;
+37 -4
View File
@@ -1,7 +1,7 @@
/**
author: Azornes
title: AzToast
version: 1.0.0
version: 1.0.1
description: Notification Utilities (Toast System)
*/
@@ -66,6 +66,7 @@ const activeNotifications = new Map();
* 2. showNotification(message, backgroundColor = "#4a6cd4", duration = 3000, type = "info", deduplicate = false)
*
* @param message - The message to show
* @returns Notification controller with updateProgress/updateMessage/close helpers.
*/
export function showNotification(message, typeOrBgColor = "info", durationOrOptions = 3000, typeArg = "info", deduplicateArg = false) {
// Clean any prefix matching the project name (e.g. "[Model Resolver]")
@@ -104,6 +105,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
}
deduplicate = deduplicateArg;
}
const manualProgress = Boolean(options?.manualProgress);
// If deduplication is enabled, check if this message already exists
if (deduplicate) {
@@ -154,7 +156,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
});
}, duration);
existingNotification.timeout = newTimeout;
return; // Don't create a new notification
return existingNotification.controller || null; // Don't create a new notification
}
}
@@ -291,6 +293,10 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
// --- Progress Bar ---
const progressBar = document.createElement('div');
progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; transform: scaleX(1); transition: none; flex-shrink: 0;`;
if (manualProgress) {
progressBar.style.transform = 'scaleX(0)';
progressBar.style.transition = 'transform 0.18s ease-out';
}
// --- Assemble Notification ---
notification.appendChild(leftBar);
@@ -339,8 +345,11 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
let dismissTimeout = null;
let progressAnimationFrame = null;
let closed = false;
const closeNotification = () => {
if (closed) return;
closed = true;
if (dismissTimeout !== null) {
clearTimeout(dismissTimeout);
dismissTimeout = null;
@@ -364,6 +373,25 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
});
};
const controller = {
element: notification,
progressBar,
messageElement: msgSpan,
updateMessage(nextMessage = '') {
msgSpan.textContent = String(nextMessage || '');
},
updateProgress(percent = 0, nextMessage = undefined) {
const numericPercent = Number(percent);
const safePercent = Math.max(0, Math.min(100, Number.isFinite(numericPercent) ? numericPercent : 0));
progressBar.style.transition = 'transform 0.18s ease-out';
progressBar.style.transform = `scaleX(${safePercent / 100})`;
if (nextMessage !== undefined) {
msgSpan.textContent = String(nextMessage || '');
}
},
close: closeNotification
};
closeBtn.onclick = closeNotification;
const startDismissTimer = () => {
@@ -389,6 +417,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
};
const pauseAndRewindTimer = () => {
if (manualProgress) return;
if (dismissTimeout !== null)
clearTimeout(dismissTimeout);
dismissTimeout = null;
@@ -417,6 +446,7 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
});
notification.addEventListener('mouseleave', () => {
if (manualProgress) return;
startDismissTimer();
// Update stored timeout if deduplicate is enabled
if (deduplicate) {
@@ -428,14 +458,17 @@ export function showNotification(message, typeOrBgColor = "info", durationOrOpti
}
});
startDismissTimer();
if (!manualProgress) {
startDismissTimer();
}
// Store notification if deduplicate is enabled
if (deduplicate) {
activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame });
activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame, controller });
}
log.debug(`Notification shown: [${CONFIG.PROJECT_NAME}] ${message}`);
return controller;
}
/**