feat: enhance model version download with progress tracking

- Set refresh to true when fetching model update versions to ensure latest data
- Refactor handleDownloadVersion to be async and accept button parameter
- Add progress tracking and WebSocket integration for download operations
- Implement button state management during download process
- Add error handling and cleanup for download operations
- Update download action to await async download handler
This commit is contained in:
Will Miao
2025-10-26 09:39:42 +08:00
parent 51098f2829
commit 994fa4bd43
3 changed files with 176 additions and 86 deletions

View File

@@ -391,7 +391,7 @@ export function initVersionsTab({
try { try {
const client = ensureClient(); const client = ensureClient();
const response = await client.fetchModelUpdateVersions(modelId, { const response = await client.fetchModelUpdateVersions(modelId, {
refresh: false, refresh: true,
}); });
if (!response?.success) { if (!response?.success) {
throw new Error(response?.error || 'Request failed'); throw new Error(response?.error || 'Request failed');
@@ -509,11 +509,34 @@ export function initVersionsTab({
} }
} }
function handleDownloadVersion(versionId) { async function handleDownloadVersion(button, versionId) {
if (!controller.record) { if (!controller.record) {
return; return;
} }
downloadManager.openForModelVersion(modelType, modelId, versionId);
const version = controller.record.versions.find(item => item.versionId === versionId);
if (!version) {
console.warn('Target version missing from record for download:', versionId);
return;
}
button.disabled = true;
try {
const success = await downloadManager.downloadVersionWithDefaults(modelType, modelId, versionId, {
versionName: version.name || `#${version.versionId}`,
});
if (success) {
await refresh();
}
} catch (error) {
console.error('Failed to start direct download for version:', error);
} finally {
if (document.body.contains(button)) {
button.disabled = false;
}
}
} }
container.addEventListener('click', async event => { container.addEventListener('click', async event => {
@@ -541,7 +564,7 @@ export function initVersionsTab({
switch (action) { switch (action) {
case 'download': case 'download':
event.preventDefault(); event.preventDefault();
handleDownloadVersion(versionId); await handleDownloadVersion(actionButton, versionId);
break; break;
case 'delete': case 'delete':
event.preventDefault(); event.preventDefault();

View File

@@ -428,6 +428,125 @@ export class DownloadManager {
this.updateTargetPath(); this.updateTargetPath();
} }
async executeDownloadWithProgress({
modelId,
versionId,
versionName = '',
modelRoot = '',
targetFolder = '',
useDefaultPaths = false,
source = null,
closeModal = false,
}) {
const config = this.apiClient?.apiConfig?.config;
if (!this.apiClient || !config) {
throw new Error('Download manager is not initialized with an API client');
}
const displayName = versionName || `#${versionId}`;
let ws = null;
let updateProgress = () => {};
try {
this.loadingManager.restoreProgressBar();
updateProgress = this.loadingManager.showDownloadProgress(1);
updateProgress(0, 0, displayName);
const downloadId = Date.now().toString();
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
ws.onmessage = event => {
const data = JSON.parse(event.data);
if (data.type === 'download_id') {
console.log(`Connected to download progress with ID: ${data.download_id}`);
return;
}
if (data.status === 'progress' && data.download_id === downloadId) {
const metrics = {
bytesDownloaded: data.bytes_downloaded,
totalBytes: data.total_bytes,
bytesPerSecond: data.bytes_per_second,
};
updateProgress(data.progress, 0, displayName, metrics);
if (data.progress < 3) {
this.loadingManager.setStatus(translate('modals.download.status.preparing'));
} else if (data.progress === 3) {
this.loadingManager.setStatus(translate('modals.download.status.downloadedPreview'));
} else if (data.progress > 3 && data.progress < 100) {
this.loadingManager.setStatus(
translate('modals.download.status.downloadingFile', { type: config.singularName })
);
} else {
this.loadingManager.setStatus(translate('modals.download.status.finalizing'));
}
}
};
ws.onerror = error => {
console.error('WebSocket error:', error);
};
await this.apiClient.downloadModel(
modelId,
versionId,
modelRoot,
targetFolder,
useDefaultPaths,
downloadId,
source
);
showToast('toast.loras.downloadCompleted', {}, 'success');
if (closeModal) {
modalManager.closeModal('downloadModal');
}
if (ws && ws.readyState === WebSocket.OPEN) {
ws.close();
ws = null;
}
const pageState = this.apiClient.getPageState();
if (!useDefaultPaths && targetFolder) {
pageState.activeFolder = targetFolder;
setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder);
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
const isActive = tag.dataset.folder === targetFolder;
tag.classList.toggle('active', isActive);
if (isActive && !tag.parentNode.classList.contains('collapsed')) {
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
}
await resetAndReload(true);
return true;
} catch (error) {
console.error('Failed to download model version:', error);
showToast('toast.downloads.downloadError', { message: error?.message }, 'error');
return false;
} finally {
try {
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
ws.close();
}
} catch (closeError) {
console.debug('Failed to close download progress socket:', closeError);
}
this.loadingManager.hide();
}
}
updatePathSelectionUI() { updatePathSelectionUI() {
const manualSelection = document.getElementById('manualPathSelection'); const manualSelection = document.getElementById('manualPathSelection');
@@ -489,92 +608,38 @@ export class DownloadManager {
} else { } else {
targetFolder = this.folderTreeManager.getSelectedPath(); targetFolder = this.folderTreeManager.getSelectedPath();
} }
return this.executeDownloadWithProgress({
modelId: this.modelId,
versionId: this.currentVersion.id,
versionName: this.currentVersion.name,
modelRoot,
targetFolder,
useDefaultPaths,
source: this.source,
closeModal: true,
});
}
async downloadVersionWithDefaults(modelType, modelId, versionId, { versionName = '', source = null } = {}) {
try { try {
const updateProgress = this.loadingManager.showDownloadProgress(1); this.apiClient = getModelApiClient(modelType);
updateProgress(0, 0, this.currentVersion.name);
const downloadId = Date.now().toString();
// Setup WebSocket for progress updates
const wsProtocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const ws = new WebSocket(`${wsProtocol}${window.location.host}/ws/download-progress?id=${downloadId}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'download_id') {
console.log(`Connected to download progress with ID: ${data.download_id}`);
return;
}
if (data.status === 'progress' && data.download_id === downloadId) {
const metrics = {
bytesDownloaded: data.bytes_downloaded,
totalBytes: data.total_bytes,
bytesPerSecond: data.bytes_per_second
};
updateProgress(data.progress, 0, this.currentVersion.name, metrics);
if (data.progress < 3) {
this.loadingManager.setStatus(translate('modals.download.status.preparing'));
} else if (data.progress === 3) {
this.loadingManager.setStatus(translate('modals.download.status.downloadedPreview'));
} else if (data.progress > 3 && data.progress < 100) {
this.loadingManager.setStatus(translate('modals.download.status.downloadingFile', { type: config.singularName }));
} else {
this.loadingManager.setStatus(translate('modals.download.status.finalizing'));
}
}
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
// Start download with use_default_paths parameter
await this.apiClient.downloadModel(
this.modelId,
this.currentVersion.id,
modelRoot,
targetFolder,
useDefaultPaths,
downloadId,
this.source
);
showToast('toast.loras.downloadCompleted', {}, 'success');
modalManager.closeModal('downloadModal');
ws.close();
// Update state and trigger reload
const pageState = this.apiClient.getPageState();
if (!useDefaultPaths) {
pageState.activeFolder = targetFolder;
// Save the active folder preference
setStorageItem(`${this.apiClient.modelType}_activeFolder`, targetFolder);
// Update UI folder selection
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
const isActive = tag.dataset.folder === targetFolder;
tag.classList.toggle('active', isActive);
if (isActive && !tag.parentNode.classList.contains('collapsed')) {
tag.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
});
}
await resetAndReload(true);
} catch (error) { } catch (error) {
showToast('toast.downloads.downloadError', { message: error.message }, 'error'); this.apiClient = getModelApiClient();
} finally {
this.loadingManager.hide();
} }
this.modelId = modelId ? modelId.toString() : null;
this.source = source;
return this.executeDownloadWithProgress({
modelId,
versionId,
versionName,
modelRoot: '',
targetFolder: '',
useDefaultPaths: true,
source,
closeModal: false,
});
} }
async initializeFolderTree() { async initializeFolderTree() {

View File

@@ -38,6 +38,7 @@ export class LoadingManager {
this.setProgress(0); this.setProgress(0);
this.setStatus(''); this.setStatus('');
this.removeDetailsContainer(); this.removeDetailsContainer();
this.progressBar.style.display = 'block';
} }
// Create a details container for enhanced progress display // Create a details container for enhanced progress display
@@ -69,6 +70,7 @@ export class LoadingManager {
// Show enhanced progress for downloads // Show enhanced progress for downloads
showDownloadProgress(totalItems = 1) { showDownloadProgress(totalItems = 1) {
this.show(translate('modals.download.status.preparing', {}, 'Preparing download...'), 0); this.show(translate('modals.download.status.preparing', {}, 'Preparing download...'), 0);
this.progressBar.style.display = 'none';
// Create details container // Create details container
const detailsContainer = this.createDetailsContainer(); const detailsContainer = this.createDetailsContainer();