diff --git a/static/js/components/CheckpointCard.js b/static/js/components/CheckpointCard.js
index 66e2c18b..3a2292c9 100644
--- a/static/js/components/CheckpointCard.js
+++ b/static/js/components/CheckpointCard.js
@@ -1,334 +1,14 @@
-import { showToast, copyToClipboard, openExampleImagesFolder, openCivitai } from '../utils/uiHelpers.js';
-import { state } from '../state/index.js';
-import { showCheckpointModal } from './checkpointModal/index.js';
-import { NSFW_LEVELS } from '../utils/constants.js';
-import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata } from '../api/checkpointApi.js';
-import { showDeleteModal } from '../utils/modalUtils.js';
-
-// Add a global event delegation handler
-export function setupCheckpointCardEventDelegation() {
- const gridElement = document.getElementById('checkpointGrid');
- if (!gridElement) return;
-
- // Remove any existing event listener to prevent duplication
- gridElement.removeEventListener('click', handleCheckpointCardEvent);
-
- // Add the event delegation handler
- gridElement.addEventListener('click', handleCheckpointCardEvent);
-}
-
-// Event delegation handler for all checkpoint card events
-function handleCheckpointCardEvent(event) {
- // Find the closest card element
- const card = event.target.closest('.lora-card');
- if (!card) return;
-
- // Handle specific elements within the card
- if (event.target.closest('.toggle-blur-btn')) {
- event.stopPropagation();
- toggleBlurContent(card);
- return;
- }
-
- if (event.target.closest('.show-content-btn')) {
- event.stopPropagation();
- showBlurredContent(card);
- return;
- }
-
- if (event.target.closest('.fa-star')) {
- event.stopPropagation();
- toggleFavorite(card);
- return;
- }
-
- if (event.target.closest('.fa-globe')) {
- event.stopPropagation();
- if (card.dataset.from_civitai === 'true') {
- openCivitai(card.dataset.filepath);
- }
- return;
- }
-
- if (event.target.closest('.fa-copy')) {
- event.stopPropagation();
- copyCheckpointName(card);
- return;
- }
-
- if (event.target.closest('.fa-trash')) {
- event.stopPropagation();
- showDeleteModal(card.dataset.filepath);
- return;
- }
-
- if (event.target.closest('.fa-image')) {
- event.stopPropagation();
- replaceCheckpointPreview(card.dataset.filepath);
- return;
- }
-
- if (event.target.closest('.fa-folder-open')) {
- event.stopPropagation();
- openExampleImagesFolder(card.dataset.sha256);
- return;
- }
-
- // If no specific element was clicked, handle the card click (show modal)
- showCheckpointModalFromCard(card);
-}
-
-// Helper functions for event handling
-function toggleBlurContent(card) {
- const preview = card.querySelector('.card-preview');
- const isBlurred = preview.classList.toggle('blurred');
- const icon = card.querySelector('.toggle-blur-btn i');
-
- // Update the icon based on blur state
- if (isBlurred) {
- icon.className = 'fas fa-eye';
- } else {
- icon.className = 'fas fa-eye-slash';
- }
-
- // Toggle the overlay visibility
- const overlay = card.querySelector('.nsfw-overlay');
- if (overlay) {
- overlay.style.display = isBlurred ? 'flex' : 'none';
- }
-}
-
-function showBlurredContent(card) {
- const preview = card.querySelector('.card-preview');
- preview.classList.remove('blurred');
-
- // Update the toggle button icon
- const toggleBtn = card.querySelector('.toggle-blur-btn');
- if (toggleBtn) {
- toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
- }
-
- // Hide the overlay
- const overlay = card.querySelector('.nsfw-overlay');
- if (overlay) {
- overlay.style.display = 'none';
- }
-}
-
-async function toggleFavorite(card) {
- const starIcon = card.querySelector('.fa-star');
- const isFavorite = starIcon.classList.contains('fas');
- const newFavoriteState = !isFavorite;
-
- try {
- // Save the new favorite state to the server
- await saveModelMetadata(card.dataset.filepath, {
- favorite: newFavoriteState
- });
-
- if (newFavoriteState) {
- showToast('Added to favorites', 'success');
- } else {
- showToast('Removed from favorites', 'success');
- }
- } catch (error) {
- console.error('Failed to update favorite status:', error);
- showToast('Failed to update favorite status', 'error');
- }
-}
-
-async function copyCheckpointName(card) {
- const checkpointName = card.dataset.file_name;
-
- try {
- await copyToClipboard(checkpointName, 'Checkpoint name copied');
- } catch (err) {
- console.error('Copy failed:', err);
- showToast('Copy failed', 'error');
- }
-}
-
-function showCheckpointModalFromCard(card) {
- // Get the page-specific previewVersions map
- const previewVersions = state.pages.checkpoints.previewVersions || new Map();
- const version = previewVersions.get(card.dataset.filepath);
- const previewUrl = card.dataset.preview_url || '/loras_static/images/no-preview.png';
- const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
-
- // Show checkpoint details modal
- const checkpointMeta = {
- sha256: card.dataset.sha256,
- file_path: card.dataset.filepath,
- model_name: card.dataset.name,
- file_name: card.dataset.file_name,
- folder: card.dataset.folder,
- modified: card.dataset.modified,
- file_size: parseInt(card.dataset.file_size || '0'),
- from_civitai: card.dataset.from_civitai === 'true',
- base_model: card.dataset.base_model,
- notes: card.dataset.notes || '',
- preview_url: versionedPreviewUrl,
- // Parse civitai metadata from the card's dataset
- civitai: (() => {
- try {
- return JSON.parse(card.dataset.meta || '{}');
- } catch (e) {
- console.error('Failed to parse civitai metadata:', e);
- return {}; // Return empty object on error
- }
- })(),
- tags: (() => {
- try {
- return JSON.parse(card.dataset.tags || '[]');
- } catch (e) {
- console.error('Failed to parse tags:', e);
- return []; // Return empty array on error
- }
- })(),
- modelDescription: card.dataset.modelDescription || ''
- };
- showCheckpointModal(checkpointMeta);
-}
-
-function replaceCheckpointPreview(filePath) {
- if (window.replaceCheckpointPreview) {
- window.replaceCheckpointPreview(filePath);
- } else {
- apiReplaceCheckpointPreview(filePath);
- }
-}
+// Legacy CheckpointCard.js - now using shared ModelCard component
+import {
+ createModelCard,
+ setupModelCardEventDelegation
+} from './shared/ModelCard.js';
+// Re-export functions with original names for backwards compatibility
export function createCheckpointCard(checkpoint) {
- const card = document.createElement('div');
- card.className = 'lora-card'; // Reuse the same class for styling
- card.dataset.sha256 = checkpoint.sha256;
- card.dataset.filepath = checkpoint.file_path;
- card.dataset.name = checkpoint.model_name;
- card.dataset.file_name = checkpoint.file_name;
- card.dataset.folder = checkpoint.folder;
- card.dataset.modified = checkpoint.modified;
- card.dataset.file_size = checkpoint.file_size;
- card.dataset.from_civitai = checkpoint.from_civitai;
- card.dataset.notes = checkpoint.notes || '';
- card.dataset.base_model = checkpoint.base_model || 'Unknown';
- card.dataset.favorite = checkpoint.favorite ? 'true' : 'false';
+ return createModelCard(checkpoint, 'checkpoint');
+}
- // Store metadata if available
- if (checkpoint.civitai) {
- card.dataset.meta = JSON.stringify(checkpoint.civitai || {});
- }
-
- // Store tags if available
- if (checkpoint.tags && Array.isArray(checkpoint.tags)) {
- card.dataset.tags = JSON.stringify(checkpoint.tags);
- }
-
- if (checkpoint.modelDescription) {
- card.dataset.modelDescription = checkpoint.modelDescription;
- }
-
- // Store NSFW level if available
- const nsfwLevel = checkpoint.preview_nsfw_level !== undefined ? checkpoint.preview_nsfw_level : 0;
- card.dataset.nsfwLevel = nsfwLevel;
-
- // Determine if the preview should be blurred based on NSFW level and user settings
- const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
- if (shouldBlur) {
- card.classList.add('nsfw-content');
- }
-
- // Determine preview URL
- const previewUrl = checkpoint.preview_url || '/loras_static/images/no-preview.png';
-
- // Get the page-specific previewVersions map
- const previewVersions = state.pages.checkpoints.previewVersions || new Map();
- const version = previewVersions.get(checkpoint.file_path);
- const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
-
- // Determine NSFW warning text based on level
- let nsfwText = "Mature Content";
- if (nsfwLevel >= NSFW_LEVELS.XXX) {
- nsfwText = "XXX-rated Content";
- } else if (nsfwLevel >= NSFW_LEVELS.X) {
- nsfwText = "X-rated Content";
- } else if (nsfwLevel >= NSFW_LEVELS.R) {
- nsfwText = "R-rated Content";
- }
-
- // Check if autoplayOnHover is enabled for video previews
- const autoplayOnHover = state.global?.settings?.autoplayOnHover || false;
- const isVideo = previewUrl.endsWith('.mp4');
- const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
-
- // Get favorite status from checkpoint data
- const isFavorite = checkpoint.favorite === true;
-
- card.innerHTML = `
-
- ${isVideo ?
- `
` :
- `

`
- }
-
- ${shouldBlur ? `
-
- ` : ''}
-
-
- `;
-
- // Add video auto-play on hover functionality if needed
- const videoElement = card.querySelector('video');
- if (videoElement && autoplayOnHover) {
- const cardPreview = card.querySelector('.card-preview');
-
- // Remove autoplay attribute and pause initially
- videoElement.removeAttribute('autoplay');
- videoElement.pause();
-
- // Add mouse events to trigger play/pause using event attributes
- cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
- cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
- }
-
- return card;
+export function setupCheckpointCardEventDelegation() {
+ setupModelCardEventDelegation('checkpoint');
}
\ No newline at end of file
diff --git a/static/js/components/LoraCard.js b/static/js/components/LoraCard.js
index fa83e0c6..e200724f 100644
--- a/static/js/components/LoraCard.js
+++ b/static/js/components/LoraCard.js
@@ -1,508 +1,17 @@
-import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../utils/uiHelpers.js';
-import { state, getCurrentPageState } from '../state/index.js';
-import { showLoraModal } from './loraModal/index.js';
-import { bulkManager } from '../managers/BulkManager.js';
-import { NSFW_LEVELS } from '../utils/constants.js';
-import { replacePreview, saveModelMetadata } from '../api/loraApi.js'
-
-// Add a global event delegation handler
-export function setupLoraCardEventDelegation() {
- const gridElement = document.getElementById('loraGrid');
- if (!gridElement) return;
-
- // Remove any existing event listener to prevent duplication
- gridElement.removeEventListener('click', handleLoraCardEvent);
-
- // Add the event delegation handler
- gridElement.addEventListener('click', handleLoraCardEvent);
-}
-
-// Event delegation handler for all lora card events
-function handleLoraCardEvent(event) {
- // Find the closest card element
- const card = event.target.closest('.lora-card');
- if (!card) return;
-
- // Handle specific elements within the card
- if (event.target.closest('.toggle-blur-btn')) {
- event.stopPropagation();
- toggleBlurContent(card);
- return;
- }
-
- if (event.target.closest('.show-content-btn')) {
- event.stopPropagation();
- showBlurredContent(card);
- return;
- }
-
- if (event.target.closest('.fa-star')) {
- event.stopPropagation();
- toggleFavorite(card);
- return;
- }
-
- if (event.target.closest('.fa-globe')) {
- event.stopPropagation();
- if (card.dataset.from_civitai === 'true') {
- openCivitai(card.dataset.filepath);
- }
- return;
- }
-
- if (event.target.closest('.fa-paper-plane')) {
- event.stopPropagation();
- sendLoraToComfyUI(card, event.shiftKey);
- return;
- }
-
- if (event.target.closest('.fa-copy')) {
- event.stopPropagation();
- copyLoraSyntax(card);
- return;
- }
-
- if (event.target.closest('.fa-image')) {
- event.stopPropagation();
- replacePreview(card.dataset.filepath);
- return;
- }
-
- if (event.target.closest('.fa-folder-open')) {
- event.stopPropagation();
- handleExampleImagesAccess(card);
- return;
- }
-
- // If no specific element was clicked, handle the card click (show modal or toggle selection)
- const pageState = getCurrentPageState();
- if (state.bulkMode) {
- // Toggle selection using the bulk manager
- bulkManager.toggleCardSelection(card);
- } else if (pageState && pageState.duplicatesMode) {
- // In duplicates mode, don't open modal when clicking cards
- return;
- } else {
- // Normal behavior - show modal
- const loraMeta = {
- sha256: card.dataset.sha256,
- file_path: card.dataset.filepath,
- model_name: card.dataset.name,
- file_name: card.dataset.file_name,
- folder: card.dataset.folder,
- modified: card.dataset.modified,
- file_size: card.dataset.file_size,
- from_civitai: card.dataset.from_civitai === 'true',
- base_model: card.dataset.base_model,
- usage_tips: card.dataset.usage_tips,
- notes: card.dataset.notes,
- favorite: card.dataset.favorite === 'true',
- // Parse civitai metadata from the card's dataset
- civitai: (() => {
- try {
- // Attempt to parse the JSON string
- return JSON.parse(card.dataset.meta || '{}');
- } catch (e) {
- console.error('Failed to parse civitai metadata:', e);
- return {}; // Return empty object on error
- }
- })(),
- tags: JSON.parse(card.dataset.tags || '[]'),
- modelDescription: card.dataset.modelDescription || ''
- };
- showLoraModal(loraMeta);
- }
-}
-
-// Helper functions for event handling
-function toggleBlurContent(card) {
- const preview = card.querySelector('.card-preview');
- const isBlurred = preview.classList.toggle('blurred');
- const icon = card.querySelector('.toggle-blur-btn i');
-
- // Update the icon based on blur state
- if (isBlurred) {
- icon.className = 'fas fa-eye';
- } else {
- icon.className = 'fas fa-eye-slash';
- }
-
- // Toggle the overlay visibility
- const overlay = card.querySelector('.nsfw-overlay');
- if (overlay) {
- overlay.style.display = isBlurred ? 'flex' : 'none';
- }
-}
-
-function showBlurredContent(card) {
- const preview = card.querySelector('.card-preview');
- preview.classList.remove('blurred');
-
- // Update the toggle button icon
- const toggleBtn = card.querySelector('.toggle-blur-btn');
- if (toggleBtn) {
- toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
- }
-
- // Hide the overlay
- const overlay = card.querySelector('.nsfw-overlay');
- if (overlay) {
- overlay.style.display = 'none';
- }
-}
-
-async function toggleFavorite(card) {
- const starIcon = card.querySelector('.fa-star');
- const isFavorite = starIcon.classList.contains('fas');
- const newFavoriteState = !isFavorite;
-
- try {
- // Save the new favorite state to the server
- await saveModelMetadata(card.dataset.filepath, {
- favorite: newFavoriteState
- });
-
- if (newFavoriteState) {
- showToast('Added to favorites', 'success');
- } else {
- showToast('Removed from favorites', 'success');
- }
- } catch (error) {
- console.error('Failed to update favorite status:', error);
- showToast('Failed to update favorite status', 'error');
- }
-}
-
-// Function to send LoRA to ComfyUI workflow
-async function sendLoraToComfyUI(card, replaceMode) {
- const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
- const strength = usageTips.strength || 1;
- const loraSyntax = ``;
-
- sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
-}
-
-// Add function to copy lora syntax
-function copyLoraSyntax(card) {
- const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
- const strength = usageTips.strength || 1;
- const loraSyntax = ``;
-
- copyToClipboard(loraSyntax, 'LoRA syntax copied to clipboard');
-}
-
-// New function to handle example images access
-async function handleExampleImagesAccess(card) {
- const modelHash = card.dataset.sha256;
-
- try {
- // Check if example images exist
- const response = await fetch(`/api/has-example-images?model_hash=${modelHash}`);
- const data = await response.json();
-
- if (data.has_images) {
- // If images exist, open the folder directly (existing behavior)
- openExampleImagesFolder(modelHash);
- } else {
- // If no images exist, show the new modal
- showExampleAccessModal(card);
- }
- } catch (error) {
- console.error('Error checking for example images:', error);
- showToast('Error checking for example images', 'error');
- }
-}
-
-// Function to show the example access modal
-function showExampleAccessModal(card) {
- const modal = document.getElementById('exampleAccessModal');
- if (!modal) return;
-
- // Get download button and determine if download should be enabled
- const downloadBtn = modal.querySelector('#downloadExamplesBtn');
- let hasRemoteExamples = false;
-
- try {
- const metaData = JSON.parse(card.dataset.meta || '{}');
- hasRemoteExamples = metaData.images &&
- Array.isArray(metaData.images) &&
- metaData.images.length > 0 &&
- metaData.images[0].url;
- } catch (e) {
- console.error('Error parsing meta data:', e);
- }
-
- // Enable or disable download button
- if (downloadBtn) {
- if (hasRemoteExamples) {
- downloadBtn.classList.remove('disabled');
- downloadBtn.removeAttribute('title'); // Remove any previous tooltip
- downloadBtn.onclick = () => {
- modalManager.closeModal('exampleAccessModal');
- // Open settings modal and scroll to example images section
- const settingsModal = document.getElementById('settingsModal');
- if (settingsModal) {
- modalManager.showModal('settingsModal');
- // Scroll to example images section after modal is visible
- setTimeout(() => {
- const exampleSection = settingsModal.querySelector('.settings-section:nth-child(5)'); // Example Images section
- if (exampleSection) {
- exampleSection.scrollIntoView({ behavior: 'smooth' });
- }
- }, 300);
- }
- };
- } else {
- downloadBtn.classList.add('disabled');
- downloadBtn.setAttribute('title', 'No remote example images available for this model on Civitai');
- downloadBtn.onclick = null;
- }
- }
-
- // Set up import button
- const importBtn = modal.querySelector('#importExamplesBtn');
- if (importBtn) {
- importBtn.onclick = () => {
- modalManager.closeModal('exampleAccessModal');
-
- // Get the lora data from card dataset
- const loraMeta = {
- sha256: card.dataset.sha256,
- file_path: card.dataset.filepath,
- model_name: card.dataset.name,
- file_name: card.dataset.file_name,
- // Other properties needed for showLoraModal
- folder: card.dataset.folder,
- modified: card.dataset.modified,
- file_size: card.dataset.file_size,
- from_civitai: card.dataset.from_civitai === 'true',
- base_model: card.dataset.base_model,
- usage_tips: card.dataset.usage_tips,
- notes: card.dataset.notes,
- favorite: card.dataset.favorite === 'true',
- civitai: (() => {
- try {
- return JSON.parse(card.dataset.meta || '{}');
- } catch (e) {
- return {};
- }
- })(),
- tags: JSON.parse(card.dataset.tags || '[]'),
- modelDescription: card.dataset.modelDescription || ''
- };
-
- // Show the lora modal
- showLoraModal(loraMeta);
-
- // Scroll to import area after modal is visible
- setTimeout(() => {
- const importArea = document.querySelector('.example-import-area');
- if (importArea) {
- const showcaseTab = document.getElementById('showcase-tab');
- if (showcaseTab) {
- // First make sure showcase tab is visible
- const tabBtn = document.querySelector('.tab-btn[data-tab="showcase"]');
- if (tabBtn && !tabBtn.classList.contains('active')) {
- tabBtn.click();
- }
-
- // Then toggle showcase if collapsed
- const carousel = showcaseTab.querySelector('.carousel');
- if (carousel && carousel.classList.contains('collapsed')) {
- const scrollIndicator = showcaseTab.querySelector('.scroll-indicator');
- if (scrollIndicator) {
- scrollIndicator.click();
- }
- }
-
- // Finally scroll to the import area
- importArea.scrollIntoView({ behavior: 'smooth' });
- }
- }
- }, 500);
- };
- }
-
- // Show the modal
- modalManager.showModal('exampleAccessModal');
-}
+// Legacy LoraCard.js - now using shared ModelCard component
+import {
+ createModelCard,
+ setupModelCardEventDelegation,
+ updateCardsForBulkMode
+} from './shared/ModelCard.js';
+// Re-export functions with original names for backwards compatibility
export function createLoraCard(lora) {
- const card = document.createElement('div');
- card.className = 'lora-card';
- card.dataset.sha256 = lora.sha256;
- card.dataset.filepath = lora.file_path;
- card.dataset.name = lora.model_name;
- card.dataset.file_name = lora.file_name;
- card.dataset.folder = lora.folder;
- card.dataset.modified = lora.modified;
- card.dataset.file_size = lora.file_size;
- card.dataset.from_civitai = lora.from_civitai;
- card.dataset.base_model = lora.base_model;
- card.dataset.usage_tips = lora.usage_tips;
- card.dataset.notes = lora.notes;
- card.dataset.meta = JSON.stringify(lora.civitai || {});
- card.dataset.favorite = lora.favorite ? 'true' : 'false';
-
- // Store tags and model description
- if (lora.tags && Array.isArray(lora.tags)) {
- card.dataset.tags = JSON.stringify(lora.tags);
- }
- if (lora.modelDescription) {
- card.dataset.modelDescription = lora.modelDescription;
- }
-
- // Store NSFW level if available
- const nsfwLevel = lora.preview_nsfw_level !== undefined ? lora.preview_nsfw_level : 0;
- card.dataset.nsfwLevel = nsfwLevel;
-
- // Determine if the preview should be blurred based on NSFW level and user settings
- const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
- if (shouldBlur) {
- card.classList.add('nsfw-content');
- }
-
- // Apply selection state if in bulk mode and this card is in the selected set
- if (state.bulkMode && state.selectedLoras.has(lora.file_path)) {
- card.classList.add('selected');
- }
-
- // Get the page-specific previewVersions map
- const previewVersions = state.pages.loras.previewVersions || new Map();
- const version = previewVersions.get(lora.file_path);
- const previewUrl = lora.preview_url || '/loras_static/images/no-preview.png';
- const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
-
- // Determine NSFW warning text based on level
- let nsfwText = "Mature Content";
- if (nsfwLevel >= NSFW_LEVELS.XXX) {
- nsfwText = "XXX-rated Content";
- } else if (nsfwLevel >= NSFW_LEVELS.X) {
- nsfwText = "X-rated Content";
- } else if (nsfwLevel >= NSFW_LEVELS.R) {
- nsfwText = "R-rated Content";
- }
-
- // Check if autoplayOnHover is enabled for video previews
- const autoplayOnHover = state.global.settings.autoplayOnHover || false;
- const isVideo = previewUrl.endsWith('.mp4');
- const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
-
- // Get favorite status from the lora data
- const isFavorite = lora.favorite === true;
-
- card.innerHTML = `
-
- ${isVideo ?
- `
` :
- `

`
- }
-
- ${shouldBlur ? `
-
- ` : ''}
-
-
- `;
-
- // Add a special class for virtual scroll positioning if needed
- if (state.virtualScroller) {
- card.classList.add('virtual-scroll-item');
- }
-
- // Add video auto-play on hover functionality if needed
- const videoElement = card.querySelector('video');
- if (videoElement && autoplayOnHover) {
- const cardPreview = card.querySelector('.card-preview');
-
- // Remove autoplay attribute and pause initially
- videoElement.removeAttribute('autoplay');
- videoElement.pause();
-
- // Add mouse events to trigger play/pause using event attributes
- // This approach reduces the number of event listeners created
- cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
- cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
- }
-
- return card;
+ return createModelCard(lora, 'lora');
}
-// Add a method to update card appearance based on bulk mode
-export function updateCardsForBulkMode(isBulkMode) {
- // Update the state
- state.bulkMode = isBulkMode;
-
- document.body.classList.toggle('bulk-mode', isBulkMode);
-
- // Get all lora cards - this can now be from the DOM or through the virtual scroller
- const loraCards = document.querySelectorAll('.lora-card');
-
- loraCards.forEach(card => {
- // Get all action containers for this card
- const actions = card.querySelectorAll('.card-actions');
-
- // Handle display property based on mode
- if (isBulkMode) {
- // Hide actions when entering bulk mode
- actions.forEach(actionGroup => {
- actionGroup.style.display = 'none';
- });
- } else {
- // Ensure actions are visible when exiting bulk mode
- actions.forEach(actionGroup => {
- // We need to reset to default display style which is flex
- actionGroup.style.display = 'flex';
- });
- }
- });
-
- // If using virtual scroller, we need to rerender after toggling bulk mode
- if (state.virtualScroller && typeof state.virtualScroller.scheduleRender === 'function') {
- state.virtualScroller.scheduleRender();
- }
-
- // Apply selection state to cards if entering bulk mode
- if (isBulkMode) {
- bulkManager.applySelectionState();
- }
-}
\ No newline at end of file
+export function setupLoraCardEventDelegation() {
+ setupModelCardEventDelegation('lora');
+}
+
+export { updateCardsForBulkMode };
\ No newline at end of file
diff --git a/static/js/components/ModelDuplicatesManager.js b/static/js/components/ModelDuplicatesManager.js
index 04cfcc9f..13725a86 100644
--- a/static/js/components/ModelDuplicatesManager.js
+++ b/static/js/components/ModelDuplicatesManager.js
@@ -184,7 +184,7 @@ export class ModelDuplicatesManager {
document.body.classList.remove('duplicate-mode');
// Clear the model grid first
- const modelGrid = document.getElementById(this.modelType === 'loras' ? 'loraGrid' : 'checkpointGrid');
+ const modelGrid = document.getElementById('modelGrid');
if (modelGrid) {
modelGrid.innerHTML = '';
}
@@ -241,7 +241,7 @@ export class ModelDuplicatesManager {
}
renderDuplicateGroups() {
- const modelGrid = document.getElementById(this.modelType === 'loras' ? 'loraGrid' : 'checkpointGrid');
+ const modelGrid = document.getElementById('modelGrid');
if (!modelGrid) return;
// Clear existing content
diff --git a/static/js/components/checkpointModal/ModelMetadata.js b/static/js/components/checkpointModal/ModelMetadata.js
deleted file mode 100644
index 2db29a74..00000000
--- a/static/js/components/checkpointModal/ModelMetadata.js
+++ /dev/null
@@ -1,436 +0,0 @@
-/**
- * ModelMetadata.js
- * Handles checkpoint model metadata editing functionality
- */
-import { showToast } from '../../utils/uiHelpers.js';
-import { BASE_MODELS } from '../../utils/constants.js';
-import { state } from '../../state/index.js';
-import { saveModelMetadata, renameCheckpointFile } from '../../api/checkpointApi.js';
-
-/**
- * Set up model name editing functionality
- * @param {string} filePath - The full file path of the model.
- */
-export function setupModelNameEditing(filePath) {
- const modelNameContent = document.querySelector('.model-name-content');
- const editBtn = document.querySelector('.edit-model-name-btn');
-
- if (!modelNameContent || !editBtn) return;
-
- // Store the file path in a data attribute for later use
- modelNameContent.dataset.filePath = filePath;
-
- // Show edit button on hover
- const modelNameHeader = document.querySelector('.model-name-header');
- modelNameHeader.addEventListener('mouseenter', () => {
- editBtn.classList.add('visible');
- });
-
- modelNameHeader.addEventListener('mouseleave', () => {
- if (!modelNameHeader.classList.contains('editing')) {
- editBtn.classList.remove('visible');
- }
- });
-
- // Handle edit button click
- editBtn.addEventListener('click', () => {
- modelNameHeader.classList.add('editing');
- modelNameContent.setAttribute('contenteditable', 'true');
- // Store original value for comparison later
- modelNameContent.dataset.originalValue = modelNameContent.textContent.trim();
- modelNameContent.focus();
-
- // Place cursor at the end
- const range = document.createRange();
- const sel = window.getSelection();
- if (modelNameContent.childNodes.length > 0) {
- range.setStart(modelNameContent.childNodes[0], modelNameContent.textContent.length);
- range.collapse(true);
- sel.removeAllRanges();
- sel.addRange(range);
- }
-
- editBtn.classList.add('visible');
- });
-
- // Handle keyboard events in edit mode
- modelNameContent.addEventListener('keydown', function(e) {
- if (!this.getAttribute('contenteditable')) return;
-
- if (e.key === 'Enter') {
- e.preventDefault();
- this.blur(); // Trigger save on Enter
- } else if (e.key === 'Escape') {
- e.preventDefault();
- // Restore original value
- this.textContent = this.dataset.originalValue;
- exitEditMode();
- }
- });
-
- // Limit model name length
- modelNameContent.addEventListener('input', function() {
- if (!this.getAttribute('contenteditable')) return;
-
- if (this.textContent.length > 100) {
- this.textContent = this.textContent.substring(0, 100);
- // Place cursor at the end
- const range = document.createRange();
- const sel = window.getSelection();
- range.setStart(this.childNodes[0], 100);
- range.collapse(true);
- sel.removeAllRanges();
- sel.addRange(range);
-
- showToast('Model name is limited to 100 characters', 'warning');
- }
- });
-
- // Handle focus out - save changes
- modelNameContent.addEventListener('blur', async function() {
- if (!this.getAttribute('contenteditable')) return;
-
- const newModelName = this.textContent.trim();
- const originalValue = this.dataset.originalValue;
-
- // Basic validation
- if (!newModelName) {
- // Restore original value if empty
- this.textContent = originalValue;
- showToast('Model name cannot be empty', 'error');
- exitEditMode();
- return;
- }
-
- if (newModelName === originalValue) {
- // No changes, just exit edit mode
- exitEditMode();
- return;
- }
-
- try {
- // Get the file path from the dataset
- const filePath = this.dataset.filePath;
-
- await saveModelMetadata(filePath, { model_name: newModelName });
-
- showToast('Model name updated successfully', 'success');
- } catch (error) {
- console.error('Error updating model name:', error);
- this.textContent = originalValue; // Restore original model name
- showToast('Failed to update model name', 'error');
- } finally {
- exitEditMode();
- }
- });
-
- function exitEditMode() {
- modelNameContent.removeAttribute('contenteditable');
- modelNameHeader.classList.remove('editing');
- editBtn.classList.remove('visible');
- }
-}
-
-/**
- * Set up base model editing functionality
- * @param {string} filePath - The full file path of the model.
- */
-export function setupBaseModelEditing(filePath) {
- const baseModelContent = document.querySelector('.base-model-content');
- const editBtn = document.querySelector('.edit-base-model-btn');
-
- if (!baseModelContent || !editBtn) return;
-
- // Store the file path in a data attribute for later use
- baseModelContent.dataset.filePath = filePath;
-
- // Show edit button on hover
- const baseModelDisplay = document.querySelector('.base-model-display');
- baseModelDisplay.addEventListener('mouseenter', () => {
- editBtn.classList.add('visible');
- });
-
- baseModelDisplay.addEventListener('mouseleave', () => {
- if (!baseModelDisplay.classList.contains('editing')) {
- editBtn.classList.remove('visible');
- }
- });
-
- // Handle edit button click
- editBtn.addEventListener('click', () => {
- baseModelDisplay.classList.add('editing');
-
- // Store the original value to check for changes later
- const originalValue = baseModelContent.textContent.trim();
-
- // Create dropdown selector to replace the base model content
- const currentValue = originalValue;
- const dropdown = document.createElement('select');
- dropdown.className = 'base-model-selector';
-
- // Flag to track if a change was made
- let valueChanged = false;
-
- // Add options from BASE_MODELS constants
- const baseModelCategories = {
- 'Stable Diffusion 1.x': [BASE_MODELS.SD_1_4, BASE_MODELS.SD_1_5, BASE_MODELS.SD_1_5_LCM, BASE_MODELS.SD_1_5_HYPER],
- 'Stable Diffusion 2.x': [BASE_MODELS.SD_2_0, BASE_MODELS.SD_2_1],
- 'Stable Diffusion 3.x': [BASE_MODELS.SD_3, BASE_MODELS.SD_3_5, BASE_MODELS.SD_3_5_MEDIUM, BASE_MODELS.SD_3_5_LARGE, BASE_MODELS.SD_3_5_LARGE_TURBO],
- 'SDXL': [BASE_MODELS.SDXL, BASE_MODELS.SDXL_LIGHTNING, BASE_MODELS.SDXL_HYPER],
- 'Video Models': [BASE_MODELS.SVD, BASE_MODELS.LTXV, BASE_MODELS.WAN_VIDEO, BASE_MODELS.HUNYUAN_VIDEO],
- 'Other Models': [
- BASE_MODELS.FLUX_1_D, BASE_MODELS.FLUX_1_S, BASE_MODELS.FLUX_1_KONTEXT, BASE_MODELS.AURAFLOW,
- BASE_MODELS.PIXART_A, BASE_MODELS.PIXART_E, BASE_MODELS.HUNYUAN_1,
- BASE_MODELS.LUMINA, BASE_MODELS.KOLORS, BASE_MODELS.NOOBAI,
- BASE_MODELS.ILLUSTRIOUS, BASE_MODELS.PONY, BASE_MODELS.HIDREAM,
- BASE_MODELS.UNKNOWN
- ]
- };
-
- // Create option groups for better organization
- Object.entries(baseModelCategories).forEach(([category, models]) => {
- const group = document.createElement('optgroup');
- group.label = category;
-
- models.forEach(model => {
- const option = document.createElement('option');
- option.value = model;
- option.textContent = model;
- option.selected = model === currentValue;
- group.appendChild(option);
- });
-
- dropdown.appendChild(group);
- });
-
- // Replace content with dropdown
- baseModelContent.style.display = 'none';
- baseModelDisplay.insertBefore(dropdown, editBtn);
-
- // Hide edit button during editing
- editBtn.style.display = 'none';
-
- // Focus the dropdown
- dropdown.focus();
-
- // Handle dropdown change
- dropdown.addEventListener('change', function() {
- const selectedModel = this.value;
- baseModelContent.textContent = selectedModel;
-
- // Mark that a change was made if the value differs from original
- if (selectedModel !== originalValue) {
- valueChanged = true;
- } else {
- valueChanged = false;
- }
- });
-
- // Function to save changes and exit edit mode
- const saveAndExit = function() {
- // Check if dropdown still exists and remove it
- if (dropdown && dropdown.parentNode === baseModelDisplay) {
- baseModelDisplay.removeChild(dropdown);
- }
-
- // Show the content and edit button
- baseModelContent.style.display = '';
- editBtn.style.display = '';
-
- // Remove editing class
- baseModelDisplay.classList.remove('editing');
-
- // Only save if the value has actually changed
- if (valueChanged || baseModelContent.textContent.trim() !== originalValue) {
- // Use the passed filePath for saving
- saveBaseModel(filePath, originalValue);
- }
-
- // Remove this event listener
- document.removeEventListener('click', outsideClickHandler);
- };
-
- // Handle outside clicks to save and exit
- const outsideClickHandler = function(e) {
- // If click is outside the dropdown and base model display
- if (!baseModelDisplay.contains(e.target)) {
- saveAndExit();
- }
- };
-
- // Add delayed event listener for outside clicks
- setTimeout(() => {
- document.addEventListener('click', outsideClickHandler);
- }, 0);
-
- // Also handle dropdown blur event
- dropdown.addEventListener('blur', function(e) {
- // Only save if the related target is not the edit button or inside the baseModelDisplay
- if (!baseModelDisplay.contains(e.relatedTarget)) {
- saveAndExit();
- }
- });
- });
-}
-
-/**
- * Save base model
- * @param {string} filePath - File path
- * @param {string} originalValue - Original value (for comparison)
- */
-async function saveBaseModel(filePath, originalValue) {
- const baseModelElement = document.querySelector('.base-model-content');
- const newBaseModel = baseModelElement.textContent.trim();
-
- // Only save if the value has actually changed
- if (newBaseModel === originalValue) {
- return; // No change, no need to save
- }
-
- try {
- await saveModelMetadata(filePath, { base_model: newBaseModel });
-
- showToast('Base model updated successfully', 'success');
- } catch (error) {
- showToast('Failed to update base model', 'error');
- }
-}
-
-/**
- * Set up file name editing functionality
- * @param {string} filePath - The full file path of the model.
- */
-export function setupFileNameEditing(filePath) {
- const fileNameContent = document.querySelector('.file-name-content');
- const editBtn = document.querySelector('.edit-file-name-btn');
-
- if (!fileNameContent || !editBtn) return;
-
- // Store the original file path
- fileNameContent.dataset.filePath = filePath;
-
- // Show edit button on hover
- const fileNameWrapper = document.querySelector('.file-name-wrapper');
- fileNameWrapper.addEventListener('mouseenter', () => {
- editBtn.classList.add('visible');
- });
-
- fileNameWrapper.addEventListener('mouseleave', () => {
- if (!fileNameWrapper.classList.contains('editing')) {
- editBtn.classList.remove('visible');
- }
- });
-
- // Handle edit button click
- editBtn.addEventListener('click', () => {
- fileNameWrapper.classList.add('editing');
- fileNameContent.setAttribute('contenteditable', 'true');
- fileNameContent.focus();
-
- // Store original value for comparison later
- fileNameContent.dataset.originalValue = fileNameContent.textContent.trim();
-
- // Place cursor at the end
- const range = document.createRange();
- const sel = window.getSelection();
- range.selectNodeContents(fileNameContent);
- range.collapse(false);
- sel.removeAllRanges();
- sel.addRange(range);
-
- editBtn.classList.add('visible');
- });
-
- // Handle keyboard events in edit mode
- fileNameContent.addEventListener('keydown', function(e) {
- if (!this.getAttribute('contenteditable')) return;
-
- if (e.key === 'Enter') {
- e.preventDefault();
- this.blur(); // Trigger save on Enter
- } else if (e.key === 'Escape') {
- e.preventDefault();
- // Restore original value
- this.textContent = this.dataset.originalValue;
- exitEditMode();
- }
- });
-
- // Handle input validation
- fileNameContent.addEventListener('input', function() {
- if (!this.getAttribute('contenteditable')) return;
-
- // Replace invalid characters for filenames
- const invalidChars = /[\\/:*?"<>|]/g;
- if (invalidChars.test(this.textContent)) {
- const cursorPos = window.getSelection().getRangeAt(0).startOffset;
- this.textContent = this.textContent.replace(invalidChars, '');
-
- // Restore cursor position
- const range = document.createRange();
- const sel = window.getSelection();
- const newPos = Math.min(cursorPos, this.textContent.length);
-
- if (this.firstChild) {
- range.setStart(this.firstChild, newPos);
- range.collapse(true);
- sel.removeAllRanges();
- sel.addRange(range);
- }
-
- showToast('Invalid characters removed from filename', 'warning');
- }
- });
-
- // Handle focus out - save changes
- fileNameContent.addEventListener('blur', async function() {
- if (!this.getAttribute('contenteditable')) return;
-
- const newFileName = this.textContent.trim();
- const originalValue = this.dataset.originalValue;
-
- // Basic validation
- if (!newFileName) {
- // Restore original value if empty
- this.textContent = originalValue;
- showToast('File name cannot be empty', 'error');
- exitEditMode();
- return;
- }
-
- if (newFileName === originalValue) {
- // No changes, just exit edit mode
- exitEditMode();
- return;
- }
-
- try {
- // Use the passed filePath (which includes the original filename)
- // Call API to rename the file using the new function from checkpointApi.js
- const result = await renameCheckpointFile(filePath, newFileName);
-
- if (result.success) {
- showToast('File name updated successfully', 'success');
-
- const newFilePath = filePath.replace(originalValue, newFileName);
-
- state.virtualScroller.updateSingleItem(filePath, { file_name: newFileName, file_path: newFilePath });
- this.textContent = newFileName;
- } else {
- throw new Error(result.error || 'Unknown error');
- }
- } catch (error) {
- console.error('Error renaming file:', error);
- this.textContent = originalValue; // Restore original file name
- showToast(`Failed to rename file: ${error.message}`, 'error');
- } finally {
- exitEditMode();
- }
- });
-
- function exitEditMode() {
- fileNameContent.removeAttribute('contenteditable');
- fileNameWrapper.classList.remove('editing');
- editBtn.classList.remove('visible');
- }
-}
\ No newline at end of file
diff --git a/static/js/components/checkpointModal/ModelTags.js b/static/js/components/checkpointModal/ModelTags.js
deleted file mode 100644
index d3256608..00000000
--- a/static/js/components/checkpointModal/ModelTags.js
+++ /dev/null
@@ -1,471 +0,0 @@
-/**
- * ModelTags.js
- * Module for handling checkpoint model tag editing functionality
- */
-import { showToast } from '../../utils/uiHelpers.js';
-import { saveModelMetadata } from '../../api/checkpointApi.js';
-
-// Preset tag suggestions
-const PRESET_TAGS = [
- 'character', 'style', 'concept', 'clothing', 'base model',
- 'poses', 'background', 'vehicle', 'buildings',
- 'objects', 'animal'
-];
-
-// Create a named function so we can remove it later
-let saveTagsHandler = null;
-
-/**
- * Set up tag editing mode
- */
-export function setupTagEditMode() {
- const editBtn = document.querySelector('.edit-tags-btn');
- if (!editBtn) return;
-
- // Store original tags for restoring on cancel
- let originalTags = [];
-
- // Remove any previously attached click handler
- if (editBtn._hasClickHandler) {
- editBtn.removeEventListener('click', editBtn._clickHandler);
- }
-
- // Create new handler and store reference
- const editBtnClickHandler = function() {
- const tagsSection = document.querySelector('.model-tags-container');
- const isEditMode = tagsSection.classList.toggle('edit-mode');
- const filePath = this.dataset.filePath;
-
- // Toggle edit mode UI elements
- const compactTagsDisplay = tagsSection.querySelector('.model-tags-compact');
- const tagsEditContainer = tagsSection.querySelector('.metadata-edit-container');
-
- if (isEditMode) {
- // Enter edit mode
- this.innerHTML = ''; // Change to cancel icon
- this.title = "Cancel editing";
-
- // Get all tags from tooltip, not just the visible ones in compact display
- originalTags = Array.from(
- tagsSection.querySelectorAll('.tooltip-tag')
- ).map(tag => tag.textContent);
-
- // Hide compact display, show edit container
- compactTagsDisplay.style.display = 'none';
-
- // If edit container doesn't exist yet, create it
- if (!tagsEditContainer) {
- const editContainer = document.createElement('div');
- editContainer.className = 'metadata-edit-container';
-
- // Move the edit button inside the container header for better visibility
- const editBtnClone = editBtn.cloneNode(true);
- editBtnClone.classList.add('metadata-header-btn');
-
- // Create edit UI with edit button in the header
- editContainer.innerHTML = createTagEditUI(originalTags, editBtnClone.outerHTML);
- tagsSection.appendChild(editContainer);
-
- // Setup the tag input field behavior
- setupTagInput();
-
- // Create and add preset suggestions dropdown
- const tagForm = editContainer.querySelector('.metadata-add-form');
- const suggestionsDropdown = createSuggestionsDropdown(originalTags);
- tagForm.appendChild(suggestionsDropdown);
-
- // Setup delete buttons for existing tags
- setupDeleteButtons();
-
- // Transfer click event from original button to the cloned one
- const newEditBtn = editContainer.querySelector('.metadata-header-btn');
- if (newEditBtn) {
- newEditBtn.addEventListener('click', function() {
- editBtn.click();
- });
- }
-
- // Hide the original button when in edit mode
- editBtn.style.display = 'none';
- } else {
- // Just show the existing edit container
- tagsEditContainer.style.display = 'block';
- editBtn.style.display = 'none';
- }
- } else {
- // Exit edit mode
- this.innerHTML = ''; // Change back to edit icon
- this.title = "Edit tags";
- editBtn.style.display = 'block';
-
- // Show compact display, hide edit container
- compactTagsDisplay.style.display = 'flex';
- if (tagsEditContainer) tagsEditContainer.style.display = 'none';
-
- // Check if we're exiting edit mode due to "Save" or "Cancel"
- if (!this.dataset.skipRestore) {
- // If canceling, restore original tags
- restoreOriginalTags(tagsSection, originalTags);
- } else {
- // Reset the skip restore flag
- delete this.dataset.skipRestore;
- }
- }
- };
-
- // Store the handler reference on the button itself
- editBtn._clickHandler = editBtnClickHandler;
- editBtn._hasClickHandler = true;
- editBtn.addEventListener('click', editBtnClickHandler);
-
- // Clean up any previous document click handler
- if (saveTagsHandler) {
- document.removeEventListener('click', saveTagsHandler);
- }
-
- // Create new save handler and store reference
- saveTagsHandler = function(e) {
- if (e.target.classList.contains('save-tags-btn') ||
- e.target.closest('.save-tags-btn')) {
- saveTags();
- }
- };
-
- // Add the new handler
- document.addEventListener('click', saveTagsHandler);
-}
-
-/**
- * Create the tag editing UI
- * @param {Array} currentTags - Current tags
- * @param {string} editBtnHTML - HTML for the edit button to include in header
- * @returns {string} HTML markup for tag editing UI
- */
-function createTagEditUI(currentTags, editBtnHTML = '') {
- return `
-
-
-
-
-
-
-
-
-
-
- `;
-}
-
-/**
- * Create suggestions dropdown with preset tags
- * @param {Array} existingTags - Already added tags
- * @returns {HTMLElement} - Dropdown element
- */
-function createSuggestionsDropdown(existingTags = []) {
- const dropdown = document.createElement('div');
- dropdown.className = 'metadata-suggestions-dropdown';
-
- // Create header
- const header = document.createElement('div');
- header.className = 'metadata-suggestions-header';
- header.innerHTML = `
- Suggested Tags
- Click to add
- `;
- dropdown.appendChild(header);
-
- // Create tag container
- const container = document.createElement('div');
- container.className = 'metadata-suggestions-container';
-
- // Add each preset tag as a suggestion
- PRESET_TAGS.forEach(tag => {
- const isAdded = existingTags.includes(tag);
-
- const item = document.createElement('div');
- item.className = `metadata-suggestion-item ${isAdded ? 'already-added' : ''}`;
- item.title = tag;
- item.innerHTML = `
- ${tag}
- ${isAdded ? '' : ''}
- `;
-
- if (!isAdded) {
- item.addEventListener('click', () => {
- addNewTag(tag);
-
- // Also populate the input field for potential editing
- const input = document.querySelector('.metadata-input');
- if (input) input.value = tag;
-
- // Focus on the input
- if (input) input.focus();
-
- // Update dropdown without removing it
- updateSuggestionsDropdown();
- });
- }
-
- container.appendChild(item);
- });
-
- dropdown.appendChild(container);
- return dropdown;
-}
-
-/**
- * Set up tag input behavior
- */
-function setupTagInput() {
- const tagInput = document.querySelector('.metadata-input');
-
- if (tagInput) {
- tagInput.addEventListener('keydown', function(e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- addNewTag(this.value);
- this.value = ''; // Clear input after adding
- }
- });
- }
-}
-
-/**
- * Set up delete buttons for tags
- */
-function setupDeleteButtons() {
- document.querySelectorAll('.metadata-delete-btn').forEach(btn => {
- btn.addEventListener('click', function(e) {
- e.stopPropagation();
- const tag = this.closest('.metadata-item');
- tag.remove();
-
- // Update status of items in the suggestion dropdown
- updateSuggestionsDropdown();
- });
- });
-}
-
-/**
- * Add a new tag
- * @param {string} tag - Tag to add
- */
-function addNewTag(tag) {
- tag = tag.trim().toLowerCase();
- if (!tag) return;
-
- const tagsContainer = document.querySelector('.metadata-items');
- if (!tagsContainer) return;
-
- // Validation: Check length
- if (tag.length > 30) {
- showToast('Tag should not exceed 30 characters', 'error');
- return;
- }
-
- // Validation: Check total number
- const currentTags = tagsContainer.querySelectorAll('.metadata-item');
- if (currentTags.length >= 30) {
- showToast('Maximum 30 tags allowed', 'error');
- return;
- }
-
- // Validation: Check for duplicates
- const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
- if (existingTags.includes(tag)) {
- showToast('This tag already exists', 'error');
- return;
- }
-
- // Create new tag
- const newTag = document.createElement('div');
- newTag.className = 'metadata-item';
- newTag.dataset.tag = tag;
- newTag.innerHTML = `
- ${tag}
-
- `;
-
- // Add event listener to delete button
- const deleteBtn = newTag.querySelector('.metadata-delete-btn');
- deleteBtn.addEventListener('click', function(e) {
- e.stopPropagation();
- newTag.remove();
-
- // Update status of items in the suggestion dropdown
- updateSuggestionsDropdown();
- });
-
- tagsContainer.appendChild(newTag);
-
- // Update status of items in the suggestions dropdown
- updateSuggestionsDropdown();
-}
-
-/**
- * Update status of items in the suggestions dropdown
- */
-function updateSuggestionsDropdown() {
- const dropdown = document.querySelector('.metadata-suggestions-dropdown');
- if (!dropdown) return;
-
- // Get all current tags
- const currentTags = document.querySelectorAll('.metadata-item');
- const existingTags = Array.from(currentTags).map(tag => tag.dataset.tag);
-
- // Update status of each item in dropdown
- dropdown.querySelectorAll('.metadata-suggestion-item').forEach(item => {
- const tagText = item.querySelector('.metadata-suggestion-text').textContent;
- const isAdded = existingTags.includes(tagText);
-
- if (isAdded) {
- item.classList.add('already-added');
-
- // Add indicator if it doesn't exist
- let indicator = item.querySelector('.added-indicator');
- if (!indicator) {
- indicator = document.createElement('span');
- indicator.className = 'added-indicator';
- indicator.innerHTML = '';
- item.appendChild(indicator);
- }
-
- // Remove click event
- item.onclick = null;
- } else {
- // Re-enable items that are no longer in the list
- item.classList.remove('already-added');
-
- // Remove indicator if it exists
- const indicator = item.querySelector('.added-indicator');
- if (indicator) indicator.remove();
-
- // Restore click event if not already set
- if (!item.onclick) {
- item.onclick = () => {
- const tag = item.querySelector('.metadata-suggestion-text').textContent;
- addNewTag(tag);
-
- // Also populate the input field
- const input = document.querySelector('.metadata-input');
- if (input) input.value = tag;
-
- // Focus the input
- if (input) input.focus();
- };
- }
- }
- });
-}
-
-/**
- * Restore original tags when canceling edit
- * @param {HTMLElement} section - The tags section
- * @param {Array} originalTags - Original tags array
- */
-function restoreOriginalTags(section, originalTags) {
- // Nothing to do here as we're just hiding the edit UI
- // and showing the original compact tags which weren't modified
-}
-
-/**
- * Save tags
- */
-async function saveTags() {
- const editBtn = document.querySelector('.edit-tags-btn');
- if (!editBtn) return;
-
- const filePath = editBtn.dataset.filePath;
- const tagElements = document.querySelectorAll('.metadata-item');
- const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
-
- // Get original tags to compare
- const originalTagElements = document.querySelectorAll('.tooltip-tag');
- const originalTags = Array.from(originalTagElements).map(tag => tag.textContent);
-
- // Check if tags have actually changed
- const tagsChanged = JSON.stringify(tags) !== JSON.stringify(originalTags);
-
- if (!tagsChanged) {
- // No changes made, just exit edit mode without API call
- editBtn.dataset.skipRestore = "true";
- editBtn.click();
- return;
- }
-
- try {
- // Save tags metadata
- await saveModelMetadata(filePath, { tags: tags });
-
- // Set flag to skip restoring original tags when exiting edit mode
- editBtn.dataset.skipRestore = "true";
-
- // Update the compact tags display
- const compactTagsContainer = document.querySelector('.model-tags-container');
- if (compactTagsContainer) {
- // Generate new compact tags HTML
- const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
-
- if (compactTagsDisplay) {
- // Clear current tags
- compactTagsDisplay.innerHTML = '';
-
- // Add visible tags (up to 5)
- const visibleTags = tags.slice(0, 5);
- visibleTags.forEach(tag => {
- const span = document.createElement('span');
- span.className = 'model-tag-compact';
- span.textContent = tag;
- compactTagsDisplay.appendChild(span);
- });
-
- // Add more indicator if needed
- const remainingCount = Math.max(0, tags.length - 5);
- if (remainingCount > 0) {
- const more = document.createElement('span');
- more.className = 'model-tag-more';
- more.dataset.count = remainingCount;
- more.textContent = `+${remainingCount}`;
- compactTagsDisplay.appendChild(more);
- }
- }
-
- // Update tooltip content
- const tooltipContent = compactTagsContainer.querySelector('.tooltip-content');
- if (tooltipContent) {
- tooltipContent.innerHTML = '';
-
- tags.forEach(tag => {
- const span = document.createElement('span');
- span.className = 'tooltip-tag';
- span.textContent = tag;
- tooltipContent.appendChild(span);
- });
- }
- }
-
- // Exit edit mode
- editBtn.click();
-
- showToast('Tags updated successfully', 'success');
- } catch (error) {
- console.error('Error saving tags:', error);
- showToast('Failed to update tags', 'error');
- }
-}
diff --git a/static/js/components/checkpointModal/index.js b/static/js/components/checkpointModal/index.js
index 56c0f03e..8feaf8a2 100644
--- a/static/js/components/checkpointModal/index.js
+++ b/static/js/components/checkpointModal/index.js
@@ -1,239 +1,11 @@
/**
* CheckpointModal - Main entry point
*
- * Modularized checkpoint modal component that handles checkpoint model details display
+ * Legacy CheckpointModal - now using shared ModelModal component
*/
-import { showToast } from '../../utils/uiHelpers.js';
-import { modalManager } from '../../managers/ModalManager.js';
-import {
- toggleShowcase,
- setupShowcaseScroll,
- scrollToTop,
- loadExampleImages
-} from '../shared/showcase/ShowcaseView.js';
-import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
-import {
- setupModelNameEditing,
- setupBaseModelEditing,
- setupFileNameEditing
-} from './ModelMetadata.js';
-import { setupTagEditMode } from './ModelTags.js'; // Add import for tag editing
-import { saveModelMetadata } from '../../api/checkpointApi.js';
-import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
+import { showModelModal } from '../shared/ModelModal.js';
-/**
- * Display the checkpoint modal with the given checkpoint data
- * @param {Object} checkpoint - Checkpoint data object
- */
+// Re-export function with original name for backwards compatibility
export function showCheckpointModal(checkpoint) {
- const content = `
-
-
-
-
-
-
-
-
-
- ${checkpoint.civitai?.name || 'N/A'}
-
-
-
-
- ${checkpoint.file_name || 'N/A'}
-
-
-
-
-
-
- ${checkpoint.file_path.replace(/[^/]+$/, '')}
-
-
-
-
-
-
- ${checkpoint.base_model || 'Unknown'}
-
-
-
-
-
- ${formatFileSize(checkpoint.file_size)}
-
-
-
-
-
-
${checkpoint.notes || 'Add your notes here...'}
-
-
-
-
-
-
${checkpoint.civitai?.description || 'N/A'}
-
-
-
-
-
-
-
-
-
-
-
-
-
- Loading recipes...
-
-
-
-
-
-
- Loading model description...
-
-
- ${checkpoint.modelDescription || ''}
-
-
-
-
-
-
-
-
-
- `;
-
- modalManager.showModal('checkpointModal', content);
- setupEditableFields(checkpoint.file_path);
- setupShowcaseScroll('checkpointModal');
- setupTabSwitching();
- setupTagTooltip();
- setupTagEditMode(); // Initialize tag editing functionality
- setupModelNameEditing(checkpoint.file_path);
- setupBaseModelEditing(checkpoint.file_path);
- setupFileNameEditing(checkpoint.file_path);
-
- // If we have a model ID but no description, fetch it
- if (checkpoint.civitai?.modelId && !checkpoint.modelDescription) {
- loadModelDescription(checkpoint.civitai.modelId, checkpoint.file_path);
- }
-
- // Load example images asynchronously - merge regular and custom images
- const regularImages = checkpoint.civitai?.images || [];
- const customImages = checkpoint.civitai?.customImages || [];
- // Combine images - regular images first, then custom images
- const allImages = [...regularImages, ...customImages];
- loadExampleImages(allImages, checkpoint.sha256);
-}
-
-/**
- * Set up editable fields in the checkpoint modal
-* @param {string} filePath - The full file path of the model.
- */
-function setupEditableFields(filePath) {
- const editableFields = document.querySelectorAll('.editable-field [contenteditable]');
-
- editableFields.forEach(field => {
- field.addEventListener('focus', function() {
- if (this.textContent === 'Add your notes here...') {
- this.textContent = '';
- }
- });
-
- field.addEventListener('blur', function() {
- if (this.textContent.trim() === '') {
- if (this.classList.contains('notes-content')) {
- this.textContent = 'Add your notes here...';
- }
- }
- });
- });
-
- // Add keydown event listeners for notes
- const notesContent = document.querySelector('.notes-content');
- if (notesContent) {
- notesContent.addEventListener('keydown', async function(e) {
- if (e.key === 'Enter') {
- if (e.shiftKey) {
- // Allow shift+enter for new line
- return;
- }
- e.preventDefault();
- await saveNotes(filePath);
- }
- });
- }
-}
-
-/**
- * Save checkpoint notes
- * @param {string} filePath - Path to the checkpoint file
- */
-async function saveNotes(filePath) {
- const content = document.querySelector('.notes-content').textContent;
- try {
- await saveModelMetadata(filePath, { notes: content });
-
- showToast('Notes saved successfully', 'success');
- } catch (error) {
- showToast('Failed to save notes', 'error');
- }
-}
-
-// Export the checkpoint modal API
-const checkpointModal = {
- show: showCheckpointModal,
- toggleShowcase,
- scrollToTop
-};
-
-export { checkpointModal };
-
-// Define global functions for use in HTML
-window.toggleShowcase = function(element) {
- toggleShowcase(element);
-};
-
-window.scrollToTopCheckpoint = function(button) {
- scrollToTop(button);
-};
-
-window.saveCheckpointNotes = function(filePath) {
- saveNotes(filePath);
-};
\ No newline at end of file
+ return showModelModal(checkpoint, 'checkpoint');
+}
\ No newline at end of file
diff --git a/static/js/components/loraModal/ModelDescription.js b/static/js/components/loraModal/ModelDescription.js
deleted file mode 100644
index b4a0eb57..00000000
--- a/static/js/components/loraModal/ModelDescription.js
+++ /dev/null
@@ -1,102 +0,0 @@
-/**
- * ModelDescription.js
- * 处理LoRA模型描述相关的功能模块
- */
-import { showToast } from '../../utils/uiHelpers.js';
-
-/**
- * 设置标签页切换功能
- */
-export function setupTabSwitching() {
- const tabButtons = document.querySelectorAll('.showcase-tabs .tab-btn');
-
- tabButtons.forEach(button => {
- button.addEventListener('click', () => {
- // Remove active class from all tabs
- document.querySelectorAll('.showcase-tabs .tab-btn').forEach(btn =>
- btn.classList.remove('active')
- );
- document.querySelectorAll('.tab-content .tab-pane').forEach(tab =>
- tab.classList.remove('active')
- );
-
- // Add active class to clicked tab
- button.classList.add('active');
- const tabId = `${button.dataset.tab}-tab`;
- document.getElementById(tabId).classList.add('active');
-
- // If switching to description tab, make sure content is properly sized
- if (button.dataset.tab === 'description') {
- const descriptionContent = document.querySelector('.model-description-content');
- if (descriptionContent) {
- const hasContent = descriptionContent.innerHTML.trim() !== '';
- document.querySelector('.model-description-loading')?.classList.add('hidden');
-
- // If no content, show a message
- if (!hasContent) {
- descriptionContent.innerHTML = 'No model description available
';
- descriptionContent.classList.remove('hidden');
- }
- }
- }
- });
- });
-}
-
-/**
- * 加载模型描述
- * @param {string} modelId - 模型ID
- * @param {string} filePath - 文件路径
- */
-export async function loadModelDescription(modelId, filePath) {
- try {
- const descriptionContainer = document.querySelector('.model-description-content');
- const loadingElement = document.querySelector('.model-description-loading');
-
- if (!descriptionContainer || !loadingElement) return;
-
- // Show loading indicator
- loadingElement.classList.remove('hidden');
- descriptionContainer.classList.add('hidden');
-
- // Try to get model description from API
- const response = await fetch(`/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
-
- if (!response.ok) {
- throw new Error(`Failed to fetch model description: ${response.statusText}`);
- }
-
- const data = await response.json();
-
- if (data.success && data.description) {
- // Update the description content
- descriptionContainer.innerHTML = data.description;
-
- // Process any links in the description to open in new tab
- const links = descriptionContainer.querySelectorAll('a');
- links.forEach(link => {
- link.setAttribute('target', '_blank');
- link.setAttribute('rel', 'noopener noreferrer');
- });
-
- // Show the description and hide loading indicator
- descriptionContainer.classList.remove('hidden');
- loadingElement.classList.add('hidden');
- } else {
- throw new Error(data.error || 'No description available');
- }
- } catch (error) {
- console.error('Error loading model description:', error);
- const loadingElement = document.querySelector('.model-description-loading');
- if (loadingElement) {
- loadingElement.innerHTML = `Failed to load model description. ${error.message}
`;
- }
-
- // Show empty state message in the description container
- const descriptionContainer = document.querySelector('.model-description-content');
- if (descriptionContainer) {
- descriptionContainer.innerHTML = 'No model description available
';
- descriptionContainer.classList.remove('hidden');
- }
- }
-}
\ No newline at end of file
diff --git a/static/js/components/loraModal/index.js b/static/js/components/loraModal/index.js
index 1fe83ae1..5a2467de 100644
--- a/static/js/components/loraModal/index.js
+++ b/static/js/components/loraModal/index.js
@@ -1,338 +1,7 @@
-/**
- * LoraModal - 主入口点
- *
- * 将原始的LoraModal.js拆分成多个功能模块后的主入口文件
- */
-import { showToast } from '../../utils/uiHelpers.js';
-import { modalManager } from '../../managers/ModalManager.js';
-import {
- setupShowcaseScroll,
- scrollToTop,
- loadExampleImages
-} from '../shared/showcase/ShowcaseView.js';
-import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
-import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
-import { parsePresets, renderPresetTags } from './PresetTags.js';
-import { loadRecipesForLora } from './RecipeTab.js';
-import { setupTagEditMode } from './ModelTags.js'; // Add import for tag editing
-import {
- setupModelNameEditing,
- setupBaseModelEditing,
- setupFileNameEditing
-} from './ModelMetadata.js';
-import { saveModelMetadata } from '../../api/loraApi.js';
-import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
+// Legacy LoraModal - now using shared ModelModal component
+import { showModelModal } from '../shared/ModelModal.js';
-/**
- * 显示LoRA模型弹窗
- * @param {Object} lora - LoRA模型数据
- */
+// Re-export function with original name for backwards compatibility
export function showLoraModal(lora) {
- const escapedWords = lora.civitai?.trainedWords?.length ?
- lora.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
-
- const content = `
-
-
-
-
-
-
-
-
-
- ${lora.civitai.name || 'N/A'}
-
-
-
-
- ${lora.file_name || 'N/A'}
-
-
-
-
-
-
- ${lora.file_path.replace(/[^/]+$/, '') || 'N/A'}
-
-
-
-
-
-
- ${lora.base_model || 'N/A'}
-
-
-
-
-
- ${formatFileSize(lora.file_size)}
-
-
-
-
-
-
-
-
-
-
-
- ${renderPresetTags(parsePresets(lora.usage_tips))}
-
-
-
- ${renderTriggerWords(escapedWords, lora.file_path)}
-
-
-
-
${lora.notes || 'Add your notes here...'}
-
-
-
-
-
${lora.civitai?.description || 'N/A'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Loading example images...
-
-
-
-
-
-
- Loading model description...
-
-
- ${lora.modelDescription || ''}
-
-
-
-
-
-
- Loading recipes...
-
-
-
-
-
-
-
-
- `;
-
- modalManager.showModal('loraModal', content, null, function() {
- // Clean up all handlers when modal closes
- const modalElement = document.getElementById('loraModal');
- if (modalElement && modalElement._clickHandler) {
- modalElement.removeEventListener('click', modalElement._clickHandler);
- delete modalElement._clickHandler;
- }
- });
- setupEditableFields(lora.file_path);
- setupShowcaseScroll('loraModal');
- setupTabSwitching();
- setupTagTooltip();
- setupTriggerWordsEditMode();
- setupModelNameEditing(lora.file_path);
- setupBaseModelEditing(lora.file_path);
- setupFileNameEditing(lora.file_path);
- setupTagEditMode(); // Initialize tag editing functionality
- setupEventHandlers(lora.file_path);
-
- // If we have a model ID but no description, fetch it
- if (lora.civitai?.modelId && !lora.modelDescription) {
- loadModelDescription(lora.civitai.modelId, lora.file_path);
- }
-
- // Load recipes for this Lora
- loadRecipesForLora(lora.model_name, lora.sha256);
-
- // Load example images asynchronously - merge regular and custom images
- const regularImages = lora.civitai?.images || [];
- const customImages = lora.civitai?.customImages || [];
- // Combine images - regular images first, then custom images
- const allImages = [...regularImages, ...customImages];
- loadExampleImages(allImages, lora.sha256);
-}
-
-/**
- * Sets up event handlers using event delegation
- * @param {string} filePath - Path to the model file
- */
-function setupEventHandlers(filePath) {
- const modalElement = document.getElementById('loraModal');
-
- // Remove existing event listeners first
- modalElement.removeEventListener('click', handleModalClick);
-
- // Create and store the handler function
- function handleModalClick(event) {
- const target = event.target.closest('[data-action]');
- if (!target) return;
-
- const action = target.dataset.action;
-
- switch (action) {
- case 'close-modal':
- modalManager.closeModal('loraModal');
- break;
- case 'scroll-to-top':
- scrollToTop(target);
- break;
- }
- }
-
- // Add the event listener with the named function
- modalElement.addEventListener('click', handleModalClick);
-
- // Store reference to the handler on the element for potential cleanup
- modalElement._clickHandler = handleModalClick;
-}
-
-async function saveNotes(filePath) {
- const content = document.querySelector('.notes-content').textContent;
- try {
- await saveModelMetadata(filePath, { notes: content });
-
- showToast('Notes saved successfully', 'success');
- } catch (error) {
- showToast('Failed to save notes', 'error');
- }
-};
-
-function setupEditableFields(filePath) {
- const editableFields = document.querySelectorAll('.editable-field [contenteditable]');
-
- editableFields.forEach(field => {
- field.addEventListener('focus', function() {
- if (this.textContent === 'Add your notes here...') {
- this.textContent = '';
- }
- });
-
- field.addEventListener('blur', function() {
- if (this.textContent.trim() === '') {
- if (this.classList.contains('notes-content')) {
- this.textContent = 'Add your notes here...';
- }
- }
- });
- });
-
- const presetSelector = document.getElementById('preset-selector');
- const presetValue = document.getElementById('preset-value');
- const addPresetBtn = document.querySelector('.add-preset-btn');
- const presetTags = document.querySelector('.preset-tags');
-
- presetSelector.addEventListener('change', function() {
- const selected = this.value;
- if (selected) {
- presetValue.style.display = 'inline-block';
- presetValue.min = selected.includes('strength') ? -10 : 0;
- presetValue.max = selected.includes('strength') ? 10 : 10;
- presetValue.step = 0.5;
- if (selected === 'clip_skip') {
- presetValue.type = 'number';
- presetValue.step = 1;
- }
- // Add auto-focus
- setTimeout(() => presetValue.focus(), 0);
- } else {
- presetValue.style.display = 'none';
- }
- });
-
- addPresetBtn.addEventListener('click', async function() {
- const key = presetSelector.value;
- const value = presetValue.value;
-
- if (!key || !value) return;
-
- const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
- const currentPresets = parsePresets(loraCard.dataset.usage_tips);
-
- currentPresets[key] = parseFloat(value);
- const newPresetsJson = JSON.stringify(currentPresets);
-
- await saveModelMetadata(filePath, {
- usage_tips: newPresetsJson
- });
-
- presetTags.innerHTML = renderPresetTags(currentPresets);
-
- presetSelector.value = '';
- presetValue.value = '';
- presetValue.style.display = 'none';
- });
-
- // Add keydown event listeners for notes
- const notesContent = document.querySelector('.notes-content');
- if (notesContent) {
- notesContent.addEventListener('keydown', async function(e) {
- if (e.key === 'Enter') {
- if (e.shiftKey) {
- // Allow shift+enter for new line
- return;
- }
- e.preventDefault();
- await saveNotes(filePath);
- }
- });
- }
-
- // Add keydown event for preset value
- presetValue.addEventListener('keydown', function(e) {
- if (e.key === 'Enter') {
- e.preventDefault();
- addPresetBtn.click();
- }
- });
+ return showModelModal(lora, 'lora');
}
\ No newline at end of file
diff --git a/static/js/components/loraModal/utils.js b/static/js/components/loraModal/utils.js
deleted file mode 100644
index 909817b5..00000000
--- a/static/js/components/loraModal/utils.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/**
- * utils.js
- * LoraModal组件的辅助函数集合
- */
-import { showToast } from '../../utils/uiHelpers.js';
-
-/**
- * 格式化文件大小
- * @param {number} bytes - 字节数
- * @returns {string} 格式化后的文件大小
- */
-export function formatFileSize(bytes) {
- if (!bytes) return 'N/A';
- const units = ['B', 'KB', 'MB', 'GB'];
- let size = bytes;
- let unitIndex = 0;
-
- while (size >= 1024 && unitIndex < units.length - 1) {
- size /= 1024;
- unitIndex++;
- }
-
- return `${size.toFixed(1)} ${units[unitIndex]}`;
-}
-
-/**
- * 渲染紧凑标签
- * @param {Array} tags - 标签数组
- * @param {string} filePath - 文件路径,用于编辑按钮
- * @returns {string} HTML内容
- */
-export function renderCompactTags(tags, filePath = '') {
- // Remove the early return and always render the container
- const tagsList = tags || [];
-
- // Display up to 5 tags, with a tooltip indicator if there are more
- const visibleTags = tagsList.slice(0, 5);
- const remainingCount = Math.max(0, tagsList.length - 5);
-
- return `
-
- `;
-}
-
-/**
- * 设置标签提示功能
- */
-export function setupTagTooltip() {
- const tagsContainer = document.querySelector('.model-tags-container');
- const tooltip = document.querySelector('.model-tags-tooltip');
-
- if (tagsContainer && tooltip) {
- tagsContainer.addEventListener('mouseenter', () => {
- tooltip.classList.add('visible');
- });
-
- tagsContainer.addEventListener('mouseleave', () => {
- tooltip.classList.remove('visible');
- });
- }
-}
\ No newline at end of file
diff --git a/static/js/components/shared/ModelCard.js b/static/js/components/shared/ModelCard.js
new file mode 100644
index 00000000..d0f5a125
--- /dev/null
+++ b/static/js/components/shared/ModelCard.js
@@ -0,0 +1,570 @@
+import { showToast, openCivitai, copyToClipboard, sendLoraToWorkflow, openExampleImagesFolder } from '../../utils/uiHelpers.js';
+import { state, getCurrentPageState } from '../../state/index.js';
+import { showModelModal } from './ModelModal.js';
+import { bulkManager } from '../../managers/BulkManager.js';
+import { modalManager } from '../../managers/ModalManager.js';
+import { NSFW_LEVELS } from '../../utils/constants.js';
+import { replacePreview, saveModelMetadata as saveLoraMetadata } from '../../api/loraApi.js';
+import { replaceCheckpointPreview as apiReplaceCheckpointPreview, saveModelMetadata as saveCheckpointMetadata } from '../../api/checkpointApi.js';
+import { showDeleteModal } from '../../utils/modalUtils.js';
+
+// Add global event delegation handlers
+export function setupModelCardEventDelegation(modelType) {
+ const gridElement = document.getElementById('modelGrid');
+ if (!gridElement) return;
+
+ // Remove any existing event listener to prevent duplication
+ gridElement.removeEventListener('click', gridElement._handleModelCardEvent);
+
+ // Create event handler with modelType context
+ const handleModelCardEvent = (event) => handleModelCardEvent_internal(event, modelType);
+
+ // Add the event delegation handler
+ gridElement.addEventListener('click', handleModelCardEvent);
+
+ // Store reference to the handler for cleanup
+ gridElement._handleModelCardEvent = handleModelCardEvent;
+}
+
+// Event delegation handler for all model card events
+function handleModelCardEvent_internal(event, modelType) {
+ // Find the closest card element
+ const card = event.target.closest('.lora-card');
+ if (!card) return;
+
+ // Handle specific elements within the card
+ if (event.target.closest('.toggle-blur-btn')) {
+ event.stopPropagation();
+ toggleBlurContent(card);
+ return;
+ }
+
+ if (event.target.closest('.show-content-btn')) {
+ event.stopPropagation();
+ showBlurredContent(card);
+ return;
+ }
+
+ if (event.target.closest('.fa-star')) {
+ event.stopPropagation();
+ toggleFavorite(card, modelType);
+ return;
+ }
+
+ if (event.target.closest('.fa-globe')) {
+ event.stopPropagation();
+ if (card.dataset.from_civitai === 'true') {
+ openCivitai(card.dataset.filepath);
+ }
+ return;
+ }
+
+ if (event.target.closest('.fa-paper-plane')) {
+ event.stopPropagation();
+ handleSendToWorkflow(card, event.shiftKey, modelType);
+ return;
+ }
+
+ if (event.target.closest('.fa-copy')) {
+ event.stopPropagation();
+ handleCopyAction(card, modelType);
+ return;
+ }
+
+ if (event.target.closest('.fa-trash')) {
+ event.stopPropagation();
+ showDeleteModal(card.dataset.filepath, modelType);
+ return;
+ }
+
+ if (event.target.closest('.fa-image')) {
+ event.stopPropagation();
+ handleReplacePreview(card.dataset.filepath, modelType);
+ return;
+ }
+
+ if (event.target.closest('.fa-folder-open')) {
+ event.stopPropagation();
+ handleExampleImagesAccess(card, modelType);
+ return;
+ }
+
+ // If no specific element was clicked, handle the card click (show modal or toggle selection)
+ handleCardClick(card, modelType);
+}
+
+// Helper functions for event handling
+function toggleBlurContent(card) {
+ const preview = card.querySelector('.card-preview');
+ const isBlurred = preview.classList.toggle('blurred');
+ const icon = card.querySelector('.toggle-blur-btn i');
+
+ // Update the icon based on blur state
+ if (isBlurred) {
+ icon.className = 'fas fa-eye';
+ } else {
+ icon.className = 'fas fa-eye-slash';
+ }
+
+ // Toggle the overlay visibility
+ const overlay = card.querySelector('.nsfw-overlay');
+ if (overlay) {
+ overlay.style.display = isBlurred ? 'flex' : 'none';
+ }
+}
+
+function showBlurredContent(card) {
+ const preview = card.querySelector('.card-preview');
+ preview.classList.remove('blurred');
+
+ // Update the toggle button icon
+ const toggleBtn = card.querySelector('.toggle-blur-btn');
+ if (toggleBtn) {
+ toggleBtn.querySelector('i').className = 'fas fa-eye-slash';
+ }
+
+ // Hide the overlay
+ const overlay = card.querySelector('.nsfw-overlay');
+ if (overlay) {
+ overlay.style.display = 'none';
+ }
+}
+
+async function toggleFavorite(card, modelType) {
+ const starIcon = card.querySelector('.fa-star');
+ const isFavorite = starIcon.classList.contains('fas');
+ const newFavoriteState = !isFavorite;
+
+ try {
+ // Use the appropriate save function based on model type
+ const saveFunction = modelType === 'lora' ? saveLoraMetadata : saveCheckpointMetadata;
+ await saveFunction(card.dataset.filepath, {
+ favorite: newFavoriteState
+ });
+
+ if (newFavoriteState) {
+ showToast('Added to favorites', 'success');
+ } else {
+ showToast('Removed from favorites', 'success');
+ }
+ } catch (error) {
+ console.error('Failed to update favorite status:', error);
+ showToast('Failed to update favorite status', 'error');
+ }
+}
+
+function handleSendToWorkflow(card, replaceMode, modelType) {
+ if (modelType === 'lora') {
+ const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
+ const strength = usageTips.strength || 1;
+ const loraSyntax = ``;
+ sendLoraToWorkflow(loraSyntax, replaceMode, 'lora');
+ } else {
+ // Checkpoint send functionality - to be implemented
+ showToast('Send checkpoint to workflow - feature to be implemented', 'info');
+ }
+}
+
+function handleCopyAction(card, modelType) {
+ if (modelType === 'lora') {
+ const usageTips = JSON.parse(card.dataset.usage_tips || '{}');
+ const strength = usageTips.strength || 1;
+ const loraSyntax = ``;
+ copyToClipboard(loraSyntax, 'LoRA syntax copied to clipboard');
+ } else {
+ // Checkpoint copy functionality - copy checkpoint name
+ const checkpointName = card.dataset.file_name;
+ copyToClipboard(checkpointName, 'Checkpoint name copied');
+ }
+}
+
+function handleReplacePreview(filePath, modelType) {
+ if (modelType === 'lora') {
+ replacePreview(filePath);
+ } else {
+ if (window.replaceCheckpointPreview) {
+ window.replaceCheckpointPreview(filePath);
+ } else {
+ apiReplaceCheckpointPreview(filePath);
+ }
+ }
+}
+
+async function handleExampleImagesAccess(card, modelType) {
+ const modelHash = card.dataset.sha256;
+
+ try {
+ const response = await fetch(`/api/has-example-images?model_hash=${modelHash}`);
+ const data = await response.json();
+
+ if (data.has_images) {
+ openExampleImagesFolder(modelHash);
+ } else {
+ showExampleAccessModal(card, modelType);
+ }
+ } catch (error) {
+ console.error('Error checking for example images:', error);
+ showToast('Error checking for example images', 'error');
+ }
+}
+
+function handleCardClick(card, modelType) {
+ const pageState = getCurrentPageState();
+
+ if (state.bulkMode) {
+ // Toggle selection using the bulk manager
+ bulkManager.toggleCardSelection(card);
+ } else if (pageState && pageState.duplicatesMode) {
+ // In duplicates mode, don't open modal when clicking cards
+ return;
+ } else {
+ // Normal behavior - show modal
+ showModelModalFromCard(card, modelType);
+ }
+}
+
+function showModelModalFromCard(card, modelType) {
+ // Get the appropriate preview versions map
+ const previewVersionsKey = modelType === 'lora' ? 'loras' : 'checkpoints';
+ const previewVersions = state.pages[previewVersionsKey]?.previewVersions || new Map();
+ const version = previewVersions.get(card.dataset.filepath);
+ const previewUrl = card.dataset.preview_url || '/loras_static/images/no-preview.png';
+ const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
+
+ // Create model metadata object
+ const modelMeta = {
+ sha256: card.dataset.sha256,
+ file_path: card.dataset.filepath,
+ model_name: card.dataset.name,
+ file_name: card.dataset.file_name,
+ folder: card.dataset.folder,
+ modified: card.dataset.modified,
+ file_size: parseInt(card.dataset.file_size || '0'),
+ from_civitai: card.dataset.from_civitai === 'true',
+ base_model: card.dataset.base_model,
+ notes: card.dataset.notes || '',
+ preview_url: versionedPreviewUrl,
+ favorite: card.dataset.favorite === 'true',
+ // Parse civitai metadata from the card's dataset
+ civitai: JSON.parse(card.dataset.meta || '{}'),
+ tags: JSON.parse(card.dataset.tags || '[]'),
+ modelDescription: card.dataset.modelDescription || '',
+ // LoRA specific fields
+ ...(modelType === 'lora' && {
+ usage_tips: card.dataset.usage_tips,
+ })
+ };
+
+ showModelModal(modelMeta, modelType);
+}
+
+// Function to show the example access modal (generalized for lora and checkpoint)
+function showExampleAccessModal(card, modelType) {
+ const modal = document.getElementById('exampleAccessModal');
+ if (!modal) return;
+
+ // Get download button and determine if download should be enabled
+ const downloadBtn = modal.querySelector('#downloadExamplesBtn');
+ let hasRemoteExamples = false;
+
+ try {
+ const metaData = JSON.parse(card.dataset.meta || '{}');
+ hasRemoteExamples = metaData.images &&
+ Array.isArray(metaData.images) &&
+ metaData.images.length > 0 &&
+ metaData.images[0].url;
+ } catch (e) {
+ console.error('Error parsing meta data:', e);
+ }
+
+ // Enable or disable download button
+ if (downloadBtn) {
+ if (hasRemoteExamples) {
+ downloadBtn.classList.remove('disabled');
+ downloadBtn.removeAttribute('title');
+ downloadBtn.onclick = () => {
+ modalManager.closeModal('exampleAccessModal');
+ // Open settings modal and scroll to example images section
+ const settingsModal = document.getElementById('settingsModal');
+ if (settingsModal) {
+ modalManager.showModal('settingsModal');
+ setTimeout(() => {
+ const exampleSection = settingsModal.querySelector('.settings-section:nth-child(5)');
+ if (exampleSection) {
+ exampleSection.scrollIntoView({ behavior: 'smooth' });
+ }
+ }, 300);
+ }
+ };
+ } else {
+ downloadBtn.classList.add('disabled');
+ downloadBtn.setAttribute('title', 'No remote example images available for this model on Civitai');
+ downloadBtn.onclick = null;
+ }
+ }
+
+ // Set up import button
+ const importBtn = modal.querySelector('#importExamplesBtn');
+ if (importBtn) {
+ importBtn.onclick = () => {
+ modalManager.closeModal('exampleAccessModal');
+
+ // Get the model data from card dataset (works for both lora and checkpoint)
+ const modelMeta = {
+ sha256: card.dataset.sha256,
+ file_path: card.dataset.filepath,
+ model_name: card.dataset.name,
+ file_name: card.dataset.file_name,
+ folder: card.dataset.folder,
+ modified: card.dataset.modified,
+ file_size: card.dataset.file_size,
+ from_civitai: card.dataset.from_civitai === 'true',
+ base_model: card.dataset.base_model,
+ notes: card.dataset.notes,
+ favorite: card.dataset.favorite === 'true',
+ civitai: JSON.parse(card.dataset.meta || '{}'),
+ tags: JSON.parse(card.dataset.tags || '[]'),
+ modelDescription: card.dataset.modelDescription || ''
+ };
+
+ // Add usage_tips if present (for lora)
+ if (card.dataset.usage_tips) {
+ modelMeta.usage_tips = card.dataset.usage_tips;
+ }
+
+ // Show the model modal
+ showModelModal(modelMeta, modelType);
+
+ // Scroll to import area after modal is visible
+ setTimeout(() => {
+ const importArea = document.querySelector('.example-import-area');
+ if (importArea) {
+ const showcaseTab = document.getElementById('showcase-tab');
+ if (showcaseTab) {
+ // First make sure showcase tab is visible
+ const tabBtn = document.querySelector('.tab-btn[data-tab="showcase"]');
+ if (tabBtn && !tabBtn.classList.contains('active')) {
+ tabBtn.click();
+ }
+
+ // Then toggle showcase if collapsed
+ const carousel = showcaseTab.querySelector('.carousel');
+ if (carousel && carousel.classList.contains('collapsed')) {
+ const scrollIndicator = showcaseTab.querySelector('.scroll-indicator');
+ if (scrollIndicator) {
+ scrollIndicator.click();
+ }
+ }
+
+ // Finally scroll to the import area
+ importArea.scrollIntoView({ behavior: 'smooth' });
+ }
+ }
+ }, 500);
+ };
+ }
+
+ // Show the modal
+ modalManager.showModal('exampleAccessModal');
+}
+
+export function createModelCard(model, modelType) {
+ const card = document.createElement('div');
+ card.className = 'lora-card'; // Reuse the same class for styling
+ card.dataset.sha256 = model.sha256;
+ card.dataset.filepath = model.file_path;
+ card.dataset.name = model.model_name;
+ card.dataset.file_name = model.file_name;
+ card.dataset.folder = model.folder;
+ card.dataset.modified = model.modified;
+ card.dataset.file_size = model.file_size;
+ card.dataset.from_civitai = model.from_civitai;
+ card.dataset.notes = model.notes || '';
+ card.dataset.base_model = model.base_model || (modelType === 'checkpoint' ? 'Unknown' : '');
+ card.dataset.favorite = model.favorite ? 'true' : 'false';
+
+ // LoRA specific data
+ if (modelType === 'lora') {
+ card.dataset.usage_tips = model.usage_tips;
+ }
+
+ // Store metadata if available
+ if (model.civitai) {
+ card.dataset.meta = JSON.stringify(model.civitai || {});
+ }
+
+ // Store tags if available
+ if (model.tags && Array.isArray(model.tags)) {
+ card.dataset.tags = JSON.stringify(model.tags);
+ }
+
+ if (model.modelDescription) {
+ card.dataset.modelDescription = model.modelDescription;
+ }
+
+ // Store NSFW level if available
+ const nsfwLevel = model.preview_nsfw_level !== undefined ? model.preview_nsfw_level : 0;
+ card.dataset.nsfwLevel = nsfwLevel;
+
+ // Determine if the preview should be blurred based on NSFW level and user settings
+ const shouldBlur = state.settings.blurMatureContent && nsfwLevel > NSFW_LEVELS.PG13;
+ if (shouldBlur) {
+ card.classList.add('nsfw-content');
+ }
+
+ // Apply selection state if in bulk mode and this card is in the selected set (LoRA only)
+ if (modelType === 'lora' && state.bulkMode && state.selectedLoras.has(model.file_path)) {
+ card.classList.add('selected');
+ }
+
+ // Get the appropriate preview versions map
+ const previewVersionsKey = modelType === 'lora' ? 'loras' : 'checkpoints';
+ const previewVersions = state.pages[previewVersionsKey]?.previewVersions || new Map();
+ const version = previewVersions.get(model.file_path);
+ const previewUrl = model.preview_url || '/loras_static/images/no-preview.png';
+ const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl;
+
+ // Determine NSFW warning text based on level
+ let nsfwText = "Mature Content";
+ if (nsfwLevel >= NSFW_LEVELS.XXX) {
+ nsfwText = "XXX-rated Content";
+ } else if (nsfwLevel >= NSFW_LEVELS.X) {
+ nsfwText = "X-rated Content";
+ } else if (nsfwLevel >= NSFW_LEVELS.R) {
+ nsfwText = "R-rated Content";
+ }
+
+ // Check if autoplayOnHover is enabled for video previews
+ const autoplayOnHover = state.global?.settings?.autoplayOnHover || false;
+ const isVideo = previewUrl.endsWith('.mp4');
+ const videoAttrs = autoplayOnHover ? 'controls muted loop' : 'controls autoplay muted loop';
+
+ // Get favorite status from model data
+ const isFavorite = model.favorite === true;
+
+ // Generate action icons based on model type
+ const actionIcons = modelType === 'lora' ?
+ `
+
+
+
+
+
+
+ ` :
+ `
+
+
+
+
+
+
+ `;
+
+ card.innerHTML = `
+
+ ${isVideo ?
+ `
` :
+ `

`
+ }
+
+ ${shouldBlur ? `
+
+ ` : ''}
+
+
+ `;
+
+ // Add video auto-play on hover functionality if needed
+ const videoElement = card.querySelector('video');
+ if (videoElement && autoplayOnHover) {
+ const cardPreview = card.querySelector('.card-preview');
+
+ // Remove autoplay attribute and pause initially
+ videoElement.removeAttribute('autoplay');
+ videoElement.pause();
+
+ // Add mouse events to trigger play/pause using event attributes
+ cardPreview.setAttribute('onmouseenter', 'this.querySelector("video")?.play()');
+ cardPreview.setAttribute('onmouseleave', 'const v=this.querySelector("video"); if(v){v.pause();v.currentTime=0;}');
+ }
+
+ return card;
+}
+
+// Add a method to update card appearance based on bulk mode (LoRA only)
+export function updateCardsForBulkMode(isBulkMode) {
+ // Update the state
+ state.bulkMode = isBulkMode;
+
+ document.body.classList.toggle('bulk-mode', isBulkMode);
+
+ // Get all lora cards - this can now be from the DOM or through the virtual scroller
+ const loraCards = document.querySelectorAll('.lora-card');
+
+ loraCards.forEach(card => {
+ // Get all action containers for this card
+ const actions = card.querySelectorAll('.card-actions');
+
+ // Handle display property based on mode
+ if (isBulkMode) {
+ // Hide actions when entering bulk mode
+ actions.forEach(actionGroup => {
+ actionGroup.style.display = 'none';
+ });
+ } else {
+ // Ensure actions are visible when exiting bulk mode
+ actions.forEach(actionGroup => {
+ // We need to reset to default display style which is flex
+ actionGroup.style.display = 'flex';
+ });
+ }
+ });
+
+ // If using virtual scroller, we need to rerender after toggling bulk mode
+ if (state.virtualScroller && typeof state.virtualScroller.scheduleRender === 'function') {
+ state.virtualScroller.scheduleRender();
+ }
+
+ // Apply selection state to cards if entering bulk mode
+ if (isBulkMode) {
+ bulkManager.applySelectionState();
+ }
+}
\ No newline at end of file
diff --git a/static/js/components/checkpointModal/ModelDescription.js b/static/js/components/shared/ModelDescription.js
similarity index 81%
rename from static/js/components/checkpointModal/ModelDescription.js
rename to static/js/components/shared/ModelDescription.js
index 0f50fe8a..a7275c20 100644
--- a/static/js/components/checkpointModal/ModelDescription.js
+++ b/static/js/components/shared/ModelDescription.js
@@ -1,8 +1,7 @@
/**
* ModelDescription.js
- * Handles checkpoint model descriptions
+ * Handles model description related functionality - General version
*/
-import { showToast } from '../../utils/uiHelpers.js';
/**
* Set up tab switching functionality
@@ -25,7 +24,7 @@ export function setupTabSwitching() {
const tabId = `${button.dataset.tab}-tab`;
document.getElementById(tabId).classList.add('active');
- // If switching to description tab, make sure content is properly loaded and displayed
+ // If switching to description tab, make sure content is properly sized
if (button.dataset.tab === 'description') {
const descriptionContent = document.querySelector('.model-description-content');
if (descriptionContent) {
@@ -44,9 +43,9 @@ export function setupTabSwitching() {
}
/**
- * Load model description from API
- * @param {string} modelId - The Civitai model ID
- * @param {string} filePath - File path for the model
+ * Load model description - General version supports both LoRA and Checkpoint
+ * @param {string} modelId - Model ID
+ * @param {string} filePath - File path
*/
export async function loadModelDescription(modelId, filePath) {
try {
@@ -59,8 +58,17 @@ export async function loadModelDescription(modelId, filePath) {
loadingElement.classList.remove('hidden');
descriptionContainer.classList.add('hidden');
+ // Determine API endpoint based on file path or context
+ let apiEndpoint = `/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`;
+
+ // If this is a checkpoint (can be determined from file path or other context)
+ if (filePath.includes('.safetensors') || filePath.includes('.ckpt')) {
+ // For now, use the same endpoint - can be updated later if checkpoint-specific endpoint is needed
+ apiEndpoint = `/api/lora-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`;
+ }
+
// Try to get model description from API
- const response = await fetch(`/api/checkpoint-model-description?model_id=${modelId}&file_path=${encodeURIComponent(filePath)}`);
+ const response = await fetch(apiEndpoint);
if (!response.ok) {
throw new Error(`Failed to fetch model description: ${response.statusText}`);
diff --git a/static/js/components/loraModal/ModelMetadata.js b/static/js/components/shared/ModelMetadata.js
similarity index 85%
rename from static/js/components/loraModal/ModelMetadata.js
rename to static/js/components/shared/ModelMetadata.js
index 7ddc4489..273d7df3 100644
--- a/static/js/components/loraModal/ModelMetadata.js
+++ b/static/js/components/shared/ModelMetadata.js
@@ -1,15 +1,16 @@
/**
* ModelMetadata.js
- * 处理LoRA模型元数据编辑相关的功能模块
+ * Handles model metadata editing functionality - General version
*/
import { showToast } from '../../utils/uiHelpers.js';
import { BASE_MODELS } from '../../utils/constants.js';
import { state } from '../../state/index.js';
-import { saveModelMetadata, renameLoraFile } from '../../api/loraApi.js';
+import { saveModelMetadata as saveLoraMetadata, renameLoraFile } from '../../api/loraApi.js';
+import { saveModelMetadata as saveCheckpointMetadata, renameCheckpointFile } from '../../api/checkpointApi.js';
/**
- * 设置模型名称编辑功能
- * @param {string} filePath - 文件路径
+ * Set up model name editing functionality
+ * @param {string} filePath - File path
*/
export function setupModelNameEditing(filePath) {
const modelNameContent = document.querySelector('.model-name-content');
@@ -113,7 +114,11 @@ export function setupModelNameEditing(filePath) {
// Get the file path from the dataset
const filePath = this.dataset.filePath;
- await saveModelMetadata(filePath, { model_name: newModelName });
+ // Determine model type based on file extension
+ const isCheckpoint = filePath.includes('.safetensors') || filePath.includes('.ckpt');
+ const saveFunction = isCheckpoint ? saveCheckpointMetadata : saveLoraMetadata;
+
+ await saveFunction(filePath, { model_name: newModelName });
showToast('Model name updated successfully', 'success');
} catch (error) {
@@ -133,8 +138,8 @@ export function setupModelNameEditing(filePath) {
}
/**
- * 设置基础模型编辑功能
- * @param {string} filePath - 文件路径
+ * Set up base model editing functionality
+ * @param {string} filePath - File path
*/
export function setupBaseModelEditing(filePath) {
const baseModelContent = document.querySelector('.base-model-content');
@@ -278,9 +283,9 @@ export function setupBaseModelEditing(filePath) {
}
/**
- * 保存基础模型
- * @param {string} filePath - 文件路径
- * @param {string} originalValue - 原始值(用于比较)
+ * Save base model
+ * @param {string} filePath - File path
+ * @param {string} originalValue - Original value (for comparison)
*/
async function saveBaseModel(filePath, originalValue) {
const baseModelElement = document.querySelector('.base-model-content');
@@ -292,7 +297,11 @@ async function saveBaseModel(filePath, originalValue) {
}
try {
- await saveModelMetadata(filePath, { base_model: newBaseModel });
+ // Determine model type based on file extension
+ const isCheckpoint = filePath.includes('.safetensors') || filePath.includes('.ckpt');
+ const saveFunction = isCheckpoint ? saveCheckpointMetadata : saveLoraMetadata;
+
+ await saveFunction(filePath, { base_model: newBaseModel });
showToast('Base model updated successfully', 'success');
} catch (error) {
@@ -301,8 +310,8 @@ async function saveBaseModel(filePath, originalValue) {
}
/**
- * 设置文件名编辑功能
- * @param {string} filePath - 文件路径
+ * Set up file name editing functionality
+ * @param {string} filePath - File path
*/
export function setupFileNameEditing(filePath) {
const fileNameContent = document.querySelector('.file-name-content');
@@ -411,17 +420,36 @@ export function setupFileNameEditing(filePath) {
try {
// Get the file path from the dataset
const filePath = this.dataset.filePath;
-
- // Call API to rename the file using the new function from loraApi.js
- const result = await renameLoraFile(filePath, newFileName);
+
+ // Determine model type and use appropriate rename function
+ const isCheckpoint = filePath.includes('.safetensors') || filePath.includes('.ckpt');
+ let result;
+
+ if (isCheckpoint) {
+ // Use checkpoint rename function if it exists, otherwise fallback to generic approach
+ if (typeof renameCheckpointFile === 'function') {
+ result = await renameCheckpointFile(filePath, newFileName);
+ } else {
+ // Fallback: use checkpoint metadata save function
+ await saveCheckpointMetadata(filePath, { file_name: newFileName });
+ result = { success: true };
+ }
+ } else {
+ // Use LoRA rename function
+ result = await renameLoraFile(filePath, newFileName);
+ }
if (result.success) {
showToast('File name updated successfully', 'success');
- // Get the new file path and update the card
- const newFilePath = filePath.replace(originalValue, newFileName);
-;
- state.virtualScroller.updateSingleItem(filePath, { file_name: newFileName, file_path: newFilePath });
+ // Update virtual scroller if available (mainly for LoRAs)
+ if (state.virtualScroller && typeof state.virtualScroller.updateSingleItem === 'function') {
+ const newFilePath = filePath.replace(originalValue, newFileName);
+ state.virtualScroller.updateSingleItem(filePath, {
+ file_name: newFileName,
+ file_path: newFilePath
+ });
+ }
} else {
throw new Error(result.error || 'Unknown error');
}
diff --git a/static/js/components/shared/ModelModal.js b/static/js/components/shared/ModelModal.js
new file mode 100644
index 00000000..6951f281
--- /dev/null
+++ b/static/js/components/shared/ModelModal.js
@@ -0,0 +1,443 @@
+import { showToast } from '../../utils/uiHelpers.js';
+import { modalManager } from '../../managers/ModalManager.js';
+import {
+ toggleShowcase,
+ setupShowcaseScroll,
+ scrollToTop,
+ loadExampleImages
+} from './showcase/ShowcaseView.js';
+import { setupTabSwitching, loadModelDescription } from './ModelDescription.js';
+import {
+ setupModelNameEditing,
+ setupBaseModelEditing,
+ setupFileNameEditing
+} from './ModelMetadata.js';
+import { setupTagEditMode } from './ModelTags.js';
+import { saveModelMetadata as saveLoraMetadata } from '../../api/loraApi.js';
+import { saveModelMetadata as saveCheckpointMetadata } from '../../api/checkpointApi.js';
+import { renderCompactTags, setupTagTooltip, formatFileSize } from './utils.js';
+import { renderTriggerWords, setupTriggerWordsEditMode } from './TriggerWords.js';
+import { parsePresets, renderPresetTags } from './PresetTags.js';
+import { loadRecipesForLora } from './RecipeTab.js';
+
+/**
+ * Display the model modal with the given model data
+ * @param {Object} model - Model data object
+ * @param {string} modelType - Type of model ('lora' or 'checkpoint')
+ */
+export function showModelModal(model, modelType) {
+ const modalId = 'modelModal';
+ const modalTitle = model.model_name;
+
+ // Prepare LoRA specific data
+ const escapedWords = modelType === 'lora' && model.civitai?.trainedWords?.length ?
+ model.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : [];
+
+ // Generate model type specific content
+ const typeSpecificContent = modelType === 'lora' ? renderLoraSpecificContent(model, escapedWords) : '';
+
+ // Generate tabs based on model type
+ const tabsContent = modelType === 'lora' ?
+ `
+
+ ` :
+ `
+ `;
+
+ const tabPanesContent = modelType === 'lora' ?
+ `
+
+ Loading example images...
+
+
+
+
+
+
+ Loading model description...
+
+
+ ${model.modelDescription || ''}
+
+
+
+
+
+
+ Loading recipes...
+
+
` :
+ `
+
+ Loading examples...
+
+
+
+
+
+
+ Loading model description...
+
+
+ ${model.modelDescription || ''}
+
+
+
`;
+
+ const content = `
+
+
+
+
+
+
+
+
+
+ ${model.civitai?.name || 'N/A'}
+
+
+
+
+ ${model.file_name || 'N/A'}
+
+
+
+
+
+
+ ${model.file_path.replace(/[^/]+$/, '') || 'N/A'}
+
+
+
+
+
+
+ ${model.base_model || (modelType === 'checkpoint' ? 'Unknown' : 'N/A')}
+
+
+
+
+
+ ${formatFileSize(model.file_size)}
+
+
+ ${typeSpecificContent}
+
+
+
+
${model.notes || 'Add your notes here...'}
+ ${modelType === 'checkpoint' ? `
` : ''}
+
+
+
+
+
${model.civitai?.description || 'N/A'}
+
+
+
+
+
+
+ ${tabsContent}
+
+
+
+ ${tabPanesContent}
+
+
+
+
+
+
+ `;
+
+ const onCloseCallback = function() {
+ // Clean up all handlers when modal closes for LoRA
+ const modalElement = document.getElementById(modalId);
+ if (modalElement && modalElement._clickHandler) {
+ modalElement.removeEventListener('click', modalElement._clickHandler);
+ delete modalElement._clickHandler;
+ }
+ };
+
+ modalManager.showModal(modalId, content, null, onCloseCallback);
+ setupEditableFields(model.file_path, modelType);
+ setupShowcaseScroll(modalId);
+ setupTabSwitching();
+ setupTagTooltip();
+ setupTagEditMode();
+ setupModelNameEditing(model.file_path);
+ setupBaseModelEditing(model.file_path);
+ setupFileNameEditing(model.file_path);
+ setupEventHandlers(model.file_path);
+
+ // LoRA specific setup
+ if (modelType === 'lora') {
+ setupTriggerWordsEditMode();
+
+ // Load recipes for this LoRA
+ loadRecipesForLora(model.model_name, model.sha256);
+ }
+
+ // If we have a model ID but no description, fetch it
+ if (model.civitai?.modelId && !model.modelDescription) {
+ loadModelDescription(model.civitai.modelId, model.file_path);
+ }
+
+ // Load example images asynchronously - merge regular and custom images
+ const regularImages = model.civitai?.images || [];
+ const customImages = model.civitai?.customImages || [];
+ // Combine images - regular images first, then custom images
+ const allImages = [...regularImages, ...customImages];
+ loadExampleImages(allImages, model.sha256);
+}
+
+function renderLoraSpecificContent(lora, escapedWords) {
+ return `
+
+
+
+
+
+
+
+
+
+ ${renderPresetTags(parsePresets(lora.usage_tips))}
+
+
+
+ ${renderTriggerWords(escapedWords, lora.file_path)}
+ `;
+}
+
+/**
+ * Sets up event handlers using event delegation for LoRA modal
+ * @param {string} filePath - Path to the model file
+ */
+function setupEventHandlers(filePath) {
+ const modalElement = document.getElementById('modelModal');
+
+ // Remove existing event listeners first
+ modalElement.removeEventListener('click', handleModalClick);
+
+ // Create and store the handler function
+ function handleModalClick(event) {
+ const target = event.target.closest('[data-action]');
+ if (!target) return;
+
+ const action = target.dataset.action;
+
+ switch (action) {
+ case 'close-modal':
+ modalManager.closeModal('modelModal');
+ break;
+ case 'scroll-to-top':
+ scrollToTop(target);
+ break;
+ }
+ }
+
+ // Add the event listener with the named function
+ modalElement.addEventListener('click', handleModalClick);
+
+ // Store reference to the handler on the element for potential cleanup
+ modalElement._clickHandler = handleModalClick;
+}
+
+/**
+ * Set up editable fields in the model modal
+ * @param {string} filePath - The full file path of the model
+ * @param {string} modelType - Type of model ('lora' or 'checkpoint')
+ */
+function setupEditableFields(filePath, modelType) {
+ const editableFields = document.querySelectorAll('.editable-field [contenteditable]');
+
+ editableFields.forEach(field => {
+ field.addEventListener('focus', function() {
+ if (this.textContent === 'Add your notes here...') {
+ this.textContent = '';
+ }
+ });
+
+ field.addEventListener('blur', function() {
+ if (this.textContent.trim() === '') {
+ if (this.classList.contains('notes-content')) {
+ this.textContent = 'Add your notes here...';
+ }
+ }
+ });
+ });
+
+ // Add keydown event listeners for notes
+ const notesContent = document.querySelector('.notes-content');
+ if (notesContent) {
+ notesContent.addEventListener('keydown', async function(e) {
+ if (e.key === 'Enter') {
+ if (e.shiftKey) {
+ // Allow shift+enter for new line
+ return;
+ }
+ e.preventDefault();
+ await saveNotes(filePath, modelType);
+ }
+ });
+ }
+
+ // LoRA specific field setup
+ if (modelType === 'lora') {
+ setupLoraSpecificFields(filePath);
+ }
+}
+
+function setupLoraSpecificFields(filePath) {
+ const presetSelector = document.getElementById('preset-selector');
+ const presetValue = document.getElementById('preset-value');
+ const addPresetBtn = document.querySelector('.add-preset-btn');
+ const presetTags = document.querySelector('.preset-tags');
+
+ if (!presetSelector || !presetValue || !addPresetBtn || !presetTags) return;
+
+ presetSelector.addEventListener('change', function() {
+ const selected = this.value;
+ if (selected) {
+ presetValue.style.display = 'inline-block';
+ presetValue.min = selected.includes('strength') ? -10 : 0;
+ presetValue.max = selected.includes('strength') ? 10 : 10;
+ presetValue.step = 0.5;
+ if (selected === 'clip_skip') {
+ presetValue.type = 'number';
+ presetValue.step = 1;
+ }
+ // Add auto-focus
+ setTimeout(() => presetValue.focus(), 0);
+ } else {
+ presetValue.style.display = 'none';
+ }
+ });
+
+ addPresetBtn.addEventListener('click', async function() {
+ const key = presetSelector.value;
+ const value = presetValue.value;
+
+ if (!key || !value) return;
+
+ const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
+ const currentPresets = parsePresets(loraCard?.dataset.usage_tips);
+
+ currentPresets[key] = parseFloat(value);
+ const newPresetsJson = JSON.stringify(currentPresets);
+
+ await saveLoraMetadata(filePath, {
+ usage_tips: newPresetsJson
+ });
+
+ presetTags.innerHTML = renderPresetTags(currentPresets);
+
+ presetSelector.value = '';
+ presetValue.value = '';
+ presetValue.style.display = 'none';
+ });
+
+ // Add keydown event for preset value
+ presetValue.addEventListener('keydown', function(e) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ addPresetBtn.click();
+ }
+ });
+}
+
+/**
+ * Save model notes
+ * @param {string} filePath - Path to the model file
+ * @param {string} modelType - Type of model ('lora' or 'checkpoint')
+ */
+async function saveNotes(filePath, modelType) {
+ const content = document.querySelector('.notes-content').textContent;
+ try {
+ const saveFunction = modelType === 'lora' ? saveLoraMetadata : saveCheckpointMetadata;
+ await saveFunction(filePath, { notes: content });
+
+ showToast('Notes saved successfully', 'success');
+ } catch (error) {
+ showToast('Failed to save notes', 'error');
+ }
+}
+
+// Export the model modal API
+const modelModal = {
+ show: showModelModal,
+ toggleShowcase,
+ scrollToTop
+};
+
+export { modelModal };
+
+// Define global functions for use in HTML
+window.toggleShowcase = function(element) {
+ toggleShowcase(element);
+};
+
+window.scrollToTopModel = function(button) {
+ scrollToTop(button);
+};
+
+// Legacy global functions for backward compatibility
+window.scrollToTopLora = function(button) {
+ scrollToTop(button);
+};
+
+window.scrollToTopCheckpoint = function(button) {
+ scrollToTop(button);
+};
+
+window.saveModelNotes = function(filePath, modelType) {
+ saveNotes(filePath, modelType);
+};
+
+// Legacy functions
+window.saveLoraNotes = function(filePath) {
+ saveNotes(filePath, 'lora');
+};
+
+window.saveCheckpointNotes = function(filePath) {
+ saveNotes(filePath, 'checkpoint');
+};
\ No newline at end of file
diff --git a/static/js/components/loraModal/ModelTags.js b/static/js/components/shared/ModelTags.js
similarity index 96%
rename from static/js/components/loraModal/ModelTags.js
rename to static/js/components/shared/ModelTags.js
index 97ef62b7..22f47464 100644
--- a/static/js/components/loraModal/ModelTags.js
+++ b/static/js/components/shared/ModelTags.js
@@ -1,9 +1,10 @@
/**
* ModelTags.js
- * Module for handling model tag editing functionality
+ * Module for handling model tag editing functionality - 共享版本
*/
import { showToast } from '../../utils/uiHelpers.js';
-import { saveModelMetadata } from '../../api/loraApi.js';
+import { saveModelMetadata as saveLoraMetadata } from '../../api/loraApi.js';
+import { saveModelMetadata as saveCheckpointMetadata } from '../../api/checkpointApi.js';
// Preset tag suggestions
const PRESET_TAGS = [
@@ -135,6 +136,98 @@ export function setupTagEditMode() {
document.addEventListener('click', saveTagsHandler);
}
+// ...existing helper functions...
+
+/**
+ * Save tags - 支持LoRA和Checkpoint
+ */
+async function saveTags() {
+ const editBtn = document.querySelector('.edit-tags-btn');
+ if (!editBtn) return;
+
+ const filePath = editBtn.dataset.filePath;
+ const tagElements = document.querySelectorAll('.metadata-item');
+ const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
+
+ // Get original tags to compare
+ const originalTagElements = document.querySelectorAll('.tooltip-tag');
+ const originalTags = Array.from(originalTagElements).map(tag => tag.textContent);
+
+ // Check if tags have actually changed
+ const tagsChanged = JSON.stringify(tags) !== JSON.stringify(originalTags);
+
+ if (!tagsChanged) {
+ // No changes made, just exit edit mode without API call
+ editBtn.dataset.skipRestore = "true";
+ editBtn.click();
+ return;
+ }
+
+ try {
+ // Determine model type and use appropriate save function
+ const isCheckpoint = filePath.includes('.safetensors') || filePath.includes('.ckpt');
+ const saveFunction = isCheckpoint ? saveCheckpointMetadata : saveLoraMetadata;
+
+ // Save tags metadata
+ await saveFunction(filePath, { tags: tags });
+
+ // Set flag to skip restoring original tags when exiting edit mode
+ editBtn.dataset.skipRestore = "true";
+
+ // Update the compact tags display
+ const compactTagsContainer = document.querySelector('.model-tags-container');
+ if (compactTagsContainer) {
+ // Generate new compact tags HTML
+ const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
+
+ if (compactTagsDisplay) {
+ // Clear current tags
+ compactTagsDisplay.innerHTML = '';
+
+ // Add visible tags (up to 5)
+ const visibleTags = tags.slice(0, 5);
+ visibleTags.forEach(tag => {
+ const span = document.createElement('span');
+ span.className = 'model-tag-compact';
+ span.textContent = tag;
+ compactTagsDisplay.appendChild(span);
+ });
+
+ // Add more indicator if needed
+ const remainingCount = Math.max(0, tags.length - 5);
+ if (remainingCount > 0) {
+ const more = document.createElement('span');
+ more.className = 'model-tag-more';
+ more.dataset.count = remainingCount;
+ more.textContent = `+${remainingCount}`;
+ compactTagsDisplay.appendChild(more);
+ }
+ }
+
+ // Update tooltip content
+ const tooltipContent = compactTagsContainer.querySelector('.tooltip-content');
+ if (tooltipContent) {
+ tooltipContent.innerHTML = '';
+
+ tags.forEach(tag => {
+ const span = document.createElement('span');
+ span.className = 'tooltip-tag';
+ span.textContent = tag;
+ tooltipContent.appendChild(span);
+ });
+ }
+ }
+
+ // Exit edit mode
+ editBtn.click();
+
+ showToast('Tags updated successfully', 'success');
+ } catch (error) {
+ console.error('Error saving tags:', error);
+ showToast('Failed to update tags', 'error');
+ }
+}
+
/**
* Create the tag editing UI
* @param {Array} currentTags - Current tags
@@ -382,90 +475,4 @@ function updateSuggestionsDropdown() {
function restoreOriginalTags(section, originalTags) {
// Nothing to do here as we're just hiding the edit UI
// and showing the original compact tags which weren't modified
-}
-
-/**
- * Save tags
- */
-async function saveTags() {
- const editBtn = document.querySelector('.edit-tags-btn');
- if (!editBtn) return;
-
- const filePath = editBtn.dataset.filePath;
- const tagElements = document.querySelectorAll('.metadata-item');
- const tags = Array.from(tagElements).map(tag => tag.dataset.tag);
-
- // Get original tags to compare
- const originalTagElements = document.querySelectorAll('.tooltip-tag');
- const originalTags = Array.from(originalTagElements).map(tag => tag.textContent);
-
- // Check if tags have actually changed
- const tagsChanged = JSON.stringify(tags) !== JSON.stringify(originalTags);
-
- if (!tagsChanged) {
- // No changes made, just exit edit mode without API call
- editBtn.dataset.skipRestore = "true";
- editBtn.click();
- return;
- }
-
- try {
- // Save tags metadata
- await saveModelMetadata(filePath, { tags: tags });
-
- // Set flag to skip restoring original tags when exiting edit mode
- editBtn.dataset.skipRestore = "true";
-
- // Update the compact tags display
- const compactTagsContainer = document.querySelector('.model-tags-container');
- if (compactTagsContainer) {
- // Generate new compact tags HTML
- const compactTagsDisplay = compactTagsContainer.querySelector('.model-tags-compact');
-
- if (compactTagsDisplay) {
- // Clear current tags
- compactTagsDisplay.innerHTML = '';
-
- // Add visible tags (up to 5)
- const visibleTags = tags.slice(0, 5);
- visibleTags.forEach(tag => {
- const span = document.createElement('span');
- span.className = 'model-tag-compact';
- span.textContent = tag;
- compactTagsDisplay.appendChild(span);
- });
-
- // Add more indicator if needed
- const remainingCount = Math.max(0, tags.length - 5);
- if (remainingCount > 0) {
- const more = document.createElement('span');
- more.className = 'model-tag-more';
- more.dataset.count = remainingCount;
- more.textContent = `+${remainingCount}`;
- compactTagsDisplay.appendChild(more);
- }
- }
-
- // Update tooltip content
- const tooltipContent = compactTagsContainer.querySelector('.tooltip-content');
- if (tooltipContent) {
- tooltipContent.innerHTML = '';
-
- tags.forEach(tag => {
- const span = document.createElement('span');
- span.className = 'tooltip-tag';
- span.textContent = tag;
- tooltipContent.appendChild(span);
- });
- }
- }
-
- // Exit edit mode
- editBtn.click();
-
- showToast('Tags updated successfully', 'success');
- } catch (error) {
- console.error('Error saving tags:', error);
- showToast('Failed to update tags', 'error');
- }
-}
+}
\ No newline at end of file
diff --git a/static/js/components/loraModal/PresetTags.js b/static/js/components/shared/PresetTags.js
similarity index 67%
rename from static/js/components/loraModal/PresetTags.js
rename to static/js/components/shared/PresetTags.js
index dc08dd20..12d75c1f 100644
--- a/static/js/components/loraModal/PresetTags.js
+++ b/static/js/components/shared/PresetTags.js
@@ -1,13 +1,13 @@
/**
* PresetTags.js
- * 处理LoRA模型预设参数标签相关的功能模块
+ * Handles LoRA model preset parameter tags - Shared version
*/
import { saveModelMetadata } from '../../api/loraApi.js';
/**
- * 解析预设参数
- * @param {string} usageTips - 包含预设参数的JSON字符串
- * @returns {Object} 解析后的预设参数对象
+ * Parse preset parameters
+ * @param {string} usageTips - JSON string containing preset parameters
+ * @returns {Object} Parsed preset parameters object
*/
export function parsePresets(usageTips) {
if (!usageTips) return {};
@@ -19,9 +19,9 @@ export function parsePresets(usageTips) {
}
/**
- * 渲染预设标签
- * @param {Object} presets - 预设参数对象
- * @returns {string} HTML内容
+ * Render preset tags
+ * @param {Object} presets - Preset parameters object
+ * @returns {string} HTML content
*/
export function renderPresetTags(presets) {
return Object.entries(presets).map(([key, value]) => `
@@ -33,9 +33,9 @@ export function renderPresetTags(presets) {
}
/**
- * 格式化预设键名
- * @param {string} key - 预设键名
- * @returns {string} 格式化后的键名
+ * Format preset key name
+ * @param {string} key - Preset key name
+ * @returns {string} Formatted key name
*/
function formatPresetKey(key) {
return key.split('_').map(word =>
@@ -44,13 +44,13 @@ function formatPresetKey(key) {
}
/**
- * 移除预设参数
- * @param {string} key - 要移除的预设键名
+ * Remove preset parameter
+ * @param {string} key - Preset key name to remove
*/
window.removePreset = async function(key) {
- const filePath = document.querySelector('#loraModal .modal-content')
+ const filePath = document.querySelector('#modelModal .modal-content')
.querySelector('.file-path').textContent +
- document.querySelector('#loraModal .modal-content')
+ document.querySelector('#modelModal .modal-content')
.querySelector('#file-name').textContent + '.safetensors';
const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`);
const currentPresets = parsePresets(loraCard.dataset.usage_tips);
diff --git a/static/js/components/loraModal/RecipeTab.js b/static/js/components/shared/RecipeTab.js
similarity index 97%
rename from static/js/components/loraModal/RecipeTab.js
rename to static/js/components/shared/RecipeTab.js
index 1ccc4c8e..661d4169 100644
--- a/static/js/components/loraModal/RecipeTab.js
+++ b/static/js/components/shared/RecipeTab.js
@@ -1,5 +1,6 @@
/**
- * RecipeTab - Handles the recipes tab in the Lora Modal
+ * RecipeTab - Handles the recipes tab in model modals (LoRA specific functionality)
+ * Moved to shared directory for consistency
*/
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
import { setSessionItem, removeSessionItem } from '../../utils/storageHelpers.js';
@@ -189,7 +190,7 @@ function copyRecipeSyntax(recipeId) {
function navigateToRecipesPage(loraName, loraHash) {
// Close the current modal
if (window.modalManager) {
- modalManager.closeModal('loraModal');
+ modalManager.closeModal('modelModal');
}
// Clear any previous filters first
@@ -212,7 +213,7 @@ function navigateToRecipesPage(loraName, loraHash) {
function navigateToRecipeDetails(recipeId) {
// Close the current modal
if (window.modalManager) {
- modalManager.closeModal('loraModal');
+ modalManager.closeModal('modelModal');
}
// Clear any previous filters first
diff --git a/static/js/components/loraModal/TriggerWords.js b/static/js/components/shared/TriggerWords.js
similarity index 99%
rename from static/js/components/loraModal/TriggerWords.js
rename to static/js/components/shared/TriggerWords.js
index 3607c649..e011f700 100644
--- a/static/js/components/loraModal/TriggerWords.js
+++ b/static/js/components/shared/TriggerWords.js
@@ -1,6 +1,7 @@
/**
* TriggerWords.js
* Module that handles trigger word functionality for LoRA models
+ * Moved to shared directory for consistency
*/
import { showToast, copyToClipboard } from '../../utils/uiHelpers.js';
import { saveModelMetadata } from '../../api/loraApi.js';
diff --git a/static/js/components/checkpointModal/utils.js b/static/js/components/shared/utils.js
similarity index 89%
rename from static/js/components/checkpointModal/utils.js
rename to static/js/components/shared/utils.js
index fdf71ea3..0e5a5c1f 100644
--- a/static/js/components/checkpointModal/utils.js
+++ b/static/js/components/shared/utils.js
@@ -1,18 +1,16 @@
/**
* utils.js
- * CheckpointModal component utility functions
+ * Helper functions for the Model Modal component - General version
*/
-import { showToast } from '../../utils/uiHelpers.js';
/**
- * Format file size for display
- * @param {number} bytes - File size in bytes
- * @returns {string} - Formatted file size
+ * Format file size
+ * @param {number} bytes - Number of bytes
+ * @returns {string} Formatted file size
*/
export function formatFileSize(bytes) {
if (!bytes) return 'N/A';
-
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const units = ['B', 'KB', 'MB', 'GB'];
let size = bytes;
let unitIndex = 0;
diff --git a/static/js/core.js b/static/js/core.js
index c5415094..674b484a 100644
--- a/static/js/core.js
+++ b/static/js/core.js
@@ -10,7 +10,6 @@ import { helpManager } from './managers/HelpManager.js';
import { showToast, initTheme, initBackToTop } from './utils/uiHelpers.js';
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
import { migrateStorageItems } from './utils/storageHelpers.js';
-import { setupLoraCardEventDelegation } from './components/LoraCard.js';
// Core application class
export class AppCore {
@@ -68,11 +67,6 @@ export class AppCore {
initializePageFeatures() {
const pageType = this.getPageType();
- // Setup event delegation for lora cards if on the loras page
- if (pageType === 'loras') {
- setupLoraCardEventDelegation();
- }
-
// Initialize virtual scroll for pages that need it
if (['loras', 'recipes', 'checkpoints'].includes(pageType)) {
initializeInfiniteScroll(pageType);
diff --git a/static/js/loras.js b/static/js/loras.js
index 131abed8..c5e2f77a 100644
--- a/static/js/loras.js
+++ b/static/js/loras.js
@@ -1,6 +1,5 @@
import { appCore } from './core.js';
import { state } from './state/index.js';
-import { showLoraModal } from './components/loraModal/index.js';
import { loadMoreLoras } from './api/loraApi.js';
import { updateCardsForBulkMode } from './components/LoraCard.js';
import { bulkManager } from './managers/BulkManager.js';
@@ -36,7 +35,6 @@ class LoraPageManager {
// Only expose what's still needed globally
// Most functionality is now handled by the PageControls component
window.loadMoreLoras = loadMoreLoras;
- window.showLoraModal = showLoraModal;
window.confirmDelete = confirmDelete;
window.closeDeleteModal = closeDeleteModal;
window.confirmExclude = confirmExclude;
@@ -70,12 +68,6 @@ class LoraPageManager {
// Initialize common page features (virtual scroll)
appCore.initializePageFeatures();
-
- // Add virtual scroll class to grid for CSS adjustments
- const loraGrid = document.getElementById('loraGrid');
- if (loraGrid && state.virtualScroller) {
- loraGrid.classList.add('virtual-scroll');
- }
}
}
diff --git a/static/js/managers/ModalManager.js b/static/js/managers/ModalManager.js
index 356f1272..9f56c269 100644
--- a/static/js/managers/ModalManager.js
+++ b/static/js/managers/ModalManager.js
@@ -12,25 +12,12 @@ export class ModalManager {
this.boundHandleEscape = this.handleEscape.bind(this);
// Register all modals - only if they exist in the current page
- const loraModal = document.getElementById('loraModal');
- if (loraModal) {
- this.registerModal('loraModal', {
- element: loraModal,
+ const modelModal = document.getElementById('modelModal');
+ if (modelModal) {
+ this.registerModal('modelModal', {
+ element: modelModal,
onClose: () => {
- this.getModal('loraModal').element.style.display = 'none';
- document.body.classList.remove('modal-open');
- },
- closeOnOutsideClick: true
- });
- }
-
- // Add checkpointModal registration
- const checkpointModal = document.getElementById('checkpointModal');
- if (checkpointModal) {
- this.registerModal('checkpointModal', {
- element: checkpointModal,
- onClose: () => {
- this.getModal('checkpointModal').element.style.display = 'none';
+ this.getModal('modelModal').element.style.display = 'none';
document.body.classList.remove('modal-open');
},
closeOnOutsideClick: true
diff --git a/static/js/utils/infiniteScroll.js b/static/js/utils/infiniteScroll.js
index f92f5849..48876487 100644
--- a/static/js/utils/infiniteScroll.js
+++ b/static/js/utils/infiniteScroll.js
@@ -82,11 +82,9 @@ async function initializeVirtualScroll(pageType) {
gridId = 'recipeGrid';
break;
case 'checkpoints':
- gridId = 'checkpointGrid';
- break;
case 'loras':
default:
- gridId = 'loraGrid';
+ gridId = 'modelGrid';
break;
}
diff --git a/templates/checkpoints.html b/templates/checkpoints.html
index 115399d4..47f8295a 100644
--- a/templates/checkpoints.html
+++ b/templates/checkpoints.html
@@ -55,7 +55,7 @@
-
+
{% endblock %}
diff --git a/templates/components/checkpoint_modals.html b/templates/components/checkpoint_modals.html
index 603ba2b3..b2e2be49 100644
--- a/templates/components/checkpoint_modals.html
+++ b/templates/components/checkpoint_modals.html
@@ -1,7 +1,7 @@
-
+
diff --git a/templates/components/lora_modals.html b/templates/components/lora_modals.html
index 8eb8a8a9..15aca1d4 100644
--- a/templates/components/lora_modals.html
+++ b/templates/components/lora_modals.html
@@ -1,5 +1,5 @@
-
+
diff --git a/templates/loras.html b/templates/loras.html
index f2c0ab82..c4e45729 100644
--- a/templates/loras.html
+++ b/templates/loras.html
@@ -43,7 +43,7 @@
-