Files
huchukato-ComfyUI-HuggingFace/web/js/ui/UI.js
T
huchukato 43861995ab Complete HuggingFace rebranding for UI
- Replaced all remaining 'Civicomfy' references with 'HuggingFace'
- Updated JavaScript extension name and button text
- Fixed all UI component references
- Updated console logs and error messages
- Changed extension registration name
- Updated CSS and image paths

This should fix the missing button issue in ComfyUI
2026-02-21 02:03:08 +01:00

466 lines
22 KiB
JavaScript

import { Feedback } from "./feedback.js";
import { setupEventListeners } from "./handlers/eventListeners.js";
import { handleDownloadSubmit, fetchAndDisplayDownloadPreview, debounceFetchDownloadPreview } from "./handlers/downloadHandler.js";
import { handleSearchSubmit } from "./handlers/searchHandler.js";
import {
handleSettingsSave,
loadAndApplySettings,
loadSettingsFromCookie,
saveSettingsToCookie,
applySettings,
getDefaultSettings,
loadGlobalRootSetting,
handleSetGlobalRoot,
handleClearGlobalRoot,
} from "./handlers/settingsHandler.js";
import { startStatusUpdates, stopStatusUpdates, updateStatus, handleCancelDownload, handleRetryDownload, handleOpenPath, handleClearHistory } from "./handlers/statusHandler.js";
import { renderSearchResults } from "./searchRenderer.js";
import { renderDownloadList } from "./statusRenderer.js";
import { renderDownloadPreview } from "./previewRenderer.js";
import { modalTemplate } from "./templates.js";
import { HuggingFaceDownloaderAPI } from "../api/huggingface.js";
export class HuggingFaceDownloaderUI {
constructor() {
this.modal = null;
this.tabs = {};
this.tabContents = {};
this.activeTab = 'download';
this.modelTypes = {};
this.statusInterval = null;
this.statusData = { queue: [], active: [], history: [] };
this.baseModels = [];
this.searchPagination = { currentPage: 1, totalPages: 1, limit: 20 };
this.settings = this.getDefaultSettings();
this.toastTimeout = null;
this.modelPreviewDebounceTimeout = null;
this.updateStatus();
this.buildModalHTML();
this.cacheDOMElements();
this.setupEventListeners();
this.feedback = new Feedback(this.modal.querySelector('#huggingface-toast'));
// Ensure icon stylesheet is loaded so buttons render icons immediately
this.ensureFontAwesome();
}
// --- Core UI Methods ---
buildModalHTML() {
this.modal = document.createElement('div');
this.modal.className = 'huggingface-downloader-modal';
this.modal.id = 'huggingface-downloader-modal';
this.modal.innerHTML = modalTemplate(this.settings);
}
cacheDOMElements() {
this.closeButton = this.modal.querySelector('#huggingface-close-modal');
this.tabContainer = this.modal.querySelector('.huggingface-downloader-tabs');
// Download Tab
this.downloadForm = this.modal.querySelector('#huggingface-download-form');
this.downloadPreviewArea = this.modal.querySelector('#huggingface-download-preview-area');
this.modelUrlInput = this.modal.querySelector('#huggingface-model-url');
this.modelVersionIdInput = this.modal.querySelector('#huggingface-model-version-id');
this.downloadModelTypeSelect = this.modal.querySelector('#huggingface-model-type');
this.createModelTypeButton = this.modal.querySelector('#huggingface-create-model-type');
this.customFilenameInput = this.modal.querySelector('#huggingface-custom-filename');
this.subdirSelect = this.modal.querySelector('#huggingface-subdir-select');
this.createSubdirButton = this.modal.querySelector('#huggingface-create-subdir');
this.saveBasePathHint = this.modal.querySelector('#huggingface-save-base-path');
this.downloadConnectionsInput = this.modal.querySelector('#huggingface-connections');
this.forceRedownloadCheckbox = this.modal.querySelector('#huggingface-force-redownload');
this.downloadSubmitButton = this.modal.querySelector('#huggingface-download-submit');
// Search Tab
this.searchForm = this.modal.querySelector('#huggingface-search-form');
this.searchQueryInput = this.modal.querySelector('#huggingface-search-query');
this.searchTypeSelect = this.modal.querySelector('#huggingface-search-type');
this.searchBaseModelSelect = this.modal.querySelector('#huggingface-search-base-model');
this.searchSortSelect = this.modal.querySelector('#huggingface-search-sort');
this.searchPeriodSelect = this.modal.querySelector('#huggingface-search-period');
this.searchSubmitButton = this.modal.querySelector('#huggingface-search-submit');
this.searchResultsContainer = this.modal.querySelector('#huggingface-search-results');
this.searchPaginationContainer = this.modal.querySelector('#huggingface-search-pagination');
// Status Tab
this.statusContent = this.modal.querySelector('#huggingface-status-content');
this.activeListContainer = this.modal.querySelector('#huggingface-active-list');
this.queuedListContainer = this.modal.querySelector('#huggingface-queued-list');
this.historyListContainer = this.modal.querySelector('#huggingface-history-list');
this.statusIndicator = this.modal.querySelector('#huggingface-status-indicator');
this.activeCountSpan = this.modal.querySelector('#huggingface-active-count');
this.clearHistoryButton = this.modal.querySelector('#huggingface-clear-history-button');
this.confirmClearModal = this.modal.querySelector('#huggingface-confirm-clear-modal');
this.confirmClearYesButton = this.modal.querySelector('#huggingface-confirm-clear-yes');
this.confirmClearNoButton = this.modal.querySelector('#huggingface-confirm-clear-no');
// Settings Tab
this.settingsForm = this.modal.querySelector('#huggingface-settings-form');
this.settingsApiKeyInput = this.modal.querySelector('#huggingface-settings-api-key');
this.settingsGlobalRootInput = this.modal.querySelector('#huggingface-settings-global-root');
this.settingsSetGlobalRootButton = this.modal.querySelector('#huggingface-settings-set-global-root');
this.settingsClearGlobalRootButton = this.modal.querySelector('#huggingface-settings-clear-global-root');
this.settingsConnectionsInput = this.modal.querySelector('#huggingface-settings-connections');
this.settingsDefaultTypeSelect = this.modal.querySelector('#huggingface-settings-default-type');
this.settingsAutoOpenCheckbox = this.modal.querySelector('#huggingface-settings-auto-open-status');
this.settingsHideMatureCheckbox = this.modal.querySelector('#huggingface-settings-hide-mature');
this.settingsNsfwThresholdInput = this.modal.querySelector('#huggingface-settings-nsfw-threshold');
this.settingsSaveButton = this.modal.querySelector('#huggingface-settings-save');
// Toast Notification
this.toastElement = this.modal.querySelector('#huggingface-toast');
// Collect tabs and contents
this.tabs = {};
this.modal.querySelectorAll('.huggingface-downloader-tab').forEach(tab => {
this.tabs[tab.dataset.tab] = tab;
});
this.tabContents = {};
this.modal.querySelectorAll('.huggingface-downloader-tab-content').forEach(content => {
const tabName = content.id.replace('huggingface-tab-', '');
if (tabName) this.tabContents[tabName] = content;
});
}
async initializeUI() {
console.info("[HuggingFace] Initializing UI components...");
await this.populateModelTypes();
await this.populateBaseModels();
this.loadAndApplySettings();
await this.loadGlobalRootSetting();
if (this.downloadModelTypeSelect) {
await this.loadAndPopulateSubdirs(this.downloadModelTypeSelect.value);
}
}
async populateModelTypes() {
console.log("[HuggingFace] Populating model types...");
try {
const types = await HuggingFaceDownloaderAPI.getModelTypes();
if (!types || typeof types !== 'object' || Object.keys(types).length === 0) {
throw new Error("Received invalid model types data format.");
}
this.modelTypes = types;
const sortedTypes = Object.entries(this.modelTypes).sort((a, b) => a[1].localeCompare(b[1]));
this.downloadModelTypeSelect.innerHTML = '';
this.searchTypeSelect.innerHTML = '<option value="any">Any Type</option>';
this.settingsDefaultTypeSelect.innerHTML = '';
sortedTypes.forEach(([key, displayName]) => {
const option = document.createElement('option');
option.value = key;
option.textContent = displayName;
this.downloadModelTypeSelect.appendChild(option.cloneNode(true));
this.settingsDefaultTypeSelect.appendChild(option.cloneNode(true));
this.searchTypeSelect.appendChild(option.cloneNode(true));
});
// After types are populated, load subdirs for the current selection
await this.loadAndPopulateSubdirs(this.downloadModelTypeSelect.value);
} catch (error) {
console.error("[HuggingFace] Failed to get or populate model types:", error);
this.showToast('Failed to load model types', 'error');
this.downloadModelTypeSelect.innerHTML = '<option value="checkpoints">Checkpoints (Default)</option>';
this.modelTypes = { "checkpoints": "Checkpoints (Default)" };
}
}
async loadAndPopulateSubdirs(modelType) {
try {
const res = await HuggingFaceDownloaderAPI.getModelDirs(modelType);
const select = this.subdirSelect;
if (!select) return;
const current = select.value;
select.innerHTML = '';
const optRoot = document.createElement('option');
optRoot.value = '';
optRoot.textContent = '(root)';
select.appendChild(optRoot);
if (res && Array.isArray(res.subdirs)) {
// res.subdirs contains '' for root; skip empty since we added (root)
res.subdirs.filter(p => p && typeof p === 'string').forEach(rel => {
const opt = document.createElement('option');
opt.value = rel;
opt.textContent = rel;
select.appendChild(opt);
});
}
// Restore selection if still present
if (Array.from(select.options).some(o => o.value === current)) {
select.value = current;
}
if (this.saveBasePathHint) {
const basePath = (res && typeof res.base_dir === 'string') ? res.base_dir : '';
this.saveBasePathHint.textContent = basePath ? `Base path: ${basePath}` : '';
}
} catch (e) {
console.error('[HuggingFace] Failed to load subdirectories:', e);
if (this.subdirSelect) {
this.subdirSelect.innerHTML = '<option value="">(root)</option>';
}
if (this.saveBasePathHint) {
this.saveBasePathHint.textContent = '';
}
}
}
// (loadAndPopulateRoots removed; dynamic types already reflect models/ subfolders)
async populateBaseModels() {
console.log("[HuggingFace] Populating base models...");
try {
const result = await HuggingFaceDownloaderAPI.getBaseModels();
if (!result || !Array.isArray(result.base_models)) {
throw new Error("Invalid base models data format received.");
}
this.baseModels = result.base_models.sort();
const existingOptions = Array.from(this.searchBaseModelSelect.options);
existingOptions.slice(1).forEach(opt => opt.remove());
this.baseModels.forEach(baseModelName => {
const option = document.createElement('option');
option.value = baseModelName;
option.textContent = baseModelName;
this.searchBaseModelSelect.appendChild(option);
});
} catch (error) {
console.error("[HuggingFace] Failed to get or populate base models:", error);
this.showToast('Failed to load base models list', 'error');
}
}
switchTab(tabId) {
if (this.activeTab === tabId || !this.tabs[tabId] || !this.tabContents[tabId]) return;
this.tabs[this.activeTab]?.classList.remove('active');
this.tabContents[this.activeTab]?.classList.remove('active');
this.tabs[tabId].classList.add('active');
this.tabContents[tabId].classList.add('active');
this.tabContents[tabId].scrollTop = 0;
this.activeTab = tabId;
if (tabId === 'status') this.updateStatus();
else if (tabId === 'settings') this.applySettings();
else if(tabId === 'download') {
this.downloadConnectionsInput.value = this.settings.numConnections;
if (Object.keys(this.modelTypes).length > 0) {
this.downloadModelTypeSelect.value = this.settings.defaultModelType;
}
}
}
// --- Modal Control ---
openModal() {
this.modal?.classList.add('open');
document.body.style.setProperty('overflow', 'hidden', 'important');
this.startStatusUpdates();
if (this.activeTab === 'status') this.updateStatus();
if (!this.settings.apiKey) this.switchTab('settings');
}
closeModal() {
this.modal?.classList.remove('open');
document.body.style.removeProperty('overflow');
this.stopStatusUpdates();
}
// --- Utility Methods ---
formatBytes(bytes, decimals = 2) {
if (bytes === null || bytes === undefined || isNaN(bytes)) return 'N/A';
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
formatSpeed(bytesPerSecond) {
if (!isFinite(bytesPerSecond) || bytesPerSecond <= 0) return '';
return this.formatBytes(bytesPerSecond) + '/s';
}
formatDuration(isoStart, isoEnd) {
try {
const diffSeconds = Math.round((new Date(isoEnd) - new Date(isoStart)) / 1000);
if (isNaN(diffSeconds) || diffSeconds < 0) return 'N/A';
if (diffSeconds < 60) return `${diffSeconds}s`;
const diffMinutes = Math.floor(diffSeconds / 60);
const remainingSeconds = diffSeconds % 60;
return `${diffMinutes}m ${remainingSeconds}s`;
} catch (e) {
return 'N/A';
}
}
showToast(message, type = 'info', duration = 3000) {
this.feedback?.show(message, type, duration);
}
ensureFontAwesome() {
this.feedback?.ensureFontAwesome();
}
// --- Rendering (delegated to external renderers) ---
renderDownloadList = (items, container, emptyMessage) => renderDownloadList(this, items, container, emptyMessage);
renderSearchResults = (items) => renderSearchResults(this, items);
renderDownloadPreview = (data) => renderDownloadPreview(this, data);
// --- Auto-select model type based on HuggingFace model type ---
inferFolderFromHuggingFaceType(huggingfaceType) {
if (!huggingfaceType || typeof huggingfaceType !== 'string') return null;
const t = huggingfaceType.trim().toLowerCase();
const keys = Object.keys(this.modelTypes || {});
if (keys.length === 0) return null;
const exists = (k) => keys.includes(k);
const findBy = (pred) => keys.find(pred);
// Direct matches first
if (exists(t)) return t;
if (exists(`${t}s`)) return `${t}s`;
// Common mappings from HuggingFace types to ComfyUI folders
const candidates = [];
const addIfExists = (k) => { if (exists(k)) candidates.push(k); };
switch (t) {
case 'checkpoint':
addIfExists('checkpoints');
addIfExists('models');
break;
case 'lora': case 'locon': case 'lycoris':
addIfExists('loras');
break;
case 'vae':
addIfExists('vae');
break;
case 'textualinversion': case 'embedding': case 'embeddings':
addIfExists('embeddings');
break;
case 'hypernetwork':
addIfExists('hypernetworks');
break;
case 'controlnet':
addIfExists('controlnet');
break;
case 'unet': case 'unet2':
addIfExists('unet');
break;
case 'diffusers': case 'diffusionmodels': case 'diffusion_models': case 'diffusion':
addIfExists('diffusers');
addIfExists('diffusion_models');
break;
case 'upscaler': case 'upscalers':
addIfExists('upscale_models');
addIfExists('upscalers');
break;
case 'motionmodule':
addIfExists('motion_models');
break;
case 'poses':
addIfExists('poses');
break;
case 'wildcards':
addIfExists('wildcards');
break;
case 'onnx':
addIfExists('onnx');
break;
}
if (candidates.length > 0) return candidates[0];
// Relaxed match: name contains type
const contains = findBy(k => k.toLowerCase().includes(t));
if (contains) return contains;
return null;
}
async autoSelectModelTypeFromHuggingFace(huggingfaceType) {
try {
const folder = this.inferFolderFromHuggingFaceType(huggingfaceType);
if (!folder) return;
if (this.downloadModelTypeSelect && this.downloadModelTypeSelect.value !== folder) {
this.downloadModelTypeSelect.value = folder;
await this.loadAndPopulateSubdirs(folder);
// Reset subdir to root after auto-switch
if (this.subdirSelect) this.subdirSelect.value = '';
}
} catch (e) {
console.warn('[HuggingFace] Auto-select model type failed:', e);
}
}
renderSearchPagination(metadata) {
if (!this.searchPaginationContainer) return;
if (!metadata || metadata.totalPages <= 1) {
this.searchPaginationContainer.innerHTML = '';
this.searchPagination = { ...this.searchPagination, ...metadata };
return;
}
this.searchPagination = { ...this.searchPagination, ...metadata };
const { currentPage, totalPages, totalItems } = this.searchPagination;
const createButton = (text, page, isDisabled = false, isCurrent = false) => {
const button = document.createElement('button');
button.className = `huggingface-button small huggingface-page-button ${isCurrent ? 'primary active' : ''}`;
button.dataset.page = page;
button.disabled = isDisabled;
button.innerHTML = text;
button.type = 'button';
return button;
};
const fragment = document.createDocumentFragment();
fragment.appendChild(createButton('&laquo; Prev', currentPage - 1, currentPage === 1));
let startPage = Math.max(1, currentPage - 2);
let endPage = Math.min(totalPages, currentPage + 2);
if (startPage > 1) fragment.appendChild(createButton('1', 1));
if (startPage > 2) fragment.appendChild(document.createElement('span')).textContent = '...';
for (let i = startPage; i <= endPage; i++) {
fragment.appendChild(createButton(i, i, false, i === currentPage));
}
if (endPage < totalPages - 1) fragment.appendChild(document.createElement('span')).textContent = '...';
if (endPage < totalPages) fragment.appendChild(createButton(totalPages, totalPages));
fragment.appendChild(createButton('Next &raquo;', currentPage + 1, currentPage === totalPages));
const info = document.createElement('div');
info.className = 'huggingface-pagination-info';
info.textContent = `Page ${currentPage} of ${totalPages} (${totalItems.toLocaleString()} models)`;
fragment.appendChild(info);
this.searchPaginationContainer.innerHTML = '';
this.searchPaginationContainer.appendChild(fragment);
}
// --- Event Handlers and State Management (delegated to handlers) ---
setupEventListeners = () => setupEventListeners(this);
getDefaultSettings = () => getDefaultSettings();
loadAndApplySettings = () => loadAndApplySettings(this);
loadSettingsFromCookie = () => loadSettingsFromCookie(this);
saveSettingsToCookie = () => saveSettingsToCookie(this);
applySettings = () => applySettings(this);
handleSettingsSave = () => handleSettingsSave(this);
loadGlobalRootSetting = () => loadGlobalRootSetting(this);
handleSetGlobalRoot = () => handleSetGlobalRoot(this);
handleClearGlobalRoot = () => handleClearGlobalRoot(this);
handleDownloadSubmit = () => handleDownloadSubmit(this);
handleSearchSubmit = () => handleSearchSubmit(this);
fetchAndDisplayDownloadPreview = () => fetchAndDisplayDownloadPreview(this);
debounceFetchDownloadPreview = (delay) => debounceFetchDownloadPreview(this, delay);
startStatusUpdates = () => startStatusUpdates(this);
stopStatusUpdates = () => stopStatusUpdates(this);
updateStatus = () => updateStatus(this);
handleCancelDownload = (downloadId) => handleCancelDownload(this, downloadId);
handleRetryDownload = (downloadId, button) => handleRetryDownload(this, downloadId, button);
handleOpenPath = (downloadId, button) => handleOpenPath(this, downloadId, button);
handleClearHistory = () => handleClearHistory(this);
}