mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 06:32:12 -03:00
checkpoint
This commit is contained in:
230
static/js/components/RecipeCard.js
Normal file
230
static/js/components/RecipeCard.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// Recipe Card Component
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
class RecipeCard {
|
||||
constructor(recipe, clickHandler) {
|
||||
this.recipe = recipe;
|
||||
this.clickHandler = clickHandler;
|
||||
this.element = this.createCardElement();
|
||||
}
|
||||
|
||||
createCardElement() {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'recipe-card';
|
||||
card.dataset.filePath = this.recipe.file_path;
|
||||
card.dataset.title = this.recipe.title;
|
||||
card.dataset.created = this.recipe.created_date;
|
||||
card.dataset.id = this.recipe.id || '';
|
||||
|
||||
// Get base model
|
||||
const baseModel = this.recipe.base_model || '';
|
||||
|
||||
// Ensure loras array exists
|
||||
const loras = this.recipe.loras || [];
|
||||
const lorasCount = loras.length;
|
||||
|
||||
// Check if all LoRAs are available in the library
|
||||
const missingLorasCount = loras.filter(lora => !lora.inLibrary).length;
|
||||
const allLorasAvailable = missingLorasCount === 0 && lorasCount > 0;
|
||||
|
||||
// Ensure file_url exists, fallback to file_path if needed
|
||||
const imageUrl = this.recipe.file_url ||
|
||||
(this.recipe.file_path ? `/loras_static/root1/preview/${this.recipe.file_path.split('/').pop()}` :
|
||||
'/loras_static/images/no-preview.png');
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="recipe-indicator" title="Recipe">R</div>
|
||||
<div class="card-preview">
|
||||
<img src="${imageUrl}" alt="${this.recipe.title}">
|
||||
<div class="card-header">
|
||||
<div class="base-model-wrapper">
|
||||
${baseModel ? `<span class="base-model-label" title="${baseModel}">${baseModel}</span>` : ''}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
||||
<i class="fas fa-copy" title="Copy Recipe Syntax"></i>
|
||||
<i class="fas fa-trash" title="Delete Recipe"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="model-info">
|
||||
<span class="model-name">${this.recipe.title}</span>
|
||||
</div>
|
||||
<div class="lora-count ${allLorasAvailable ? 'ready' : (lorasCount > 0 ? 'missing' : '')}"
|
||||
title="${this.getLoraStatusTitle(lorasCount, missingLorasCount)}">
|
||||
<i class="fas fa-layer-group"></i> ${lorasCount}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.attachEventListeners(card);
|
||||
return card;
|
||||
}
|
||||
|
||||
getLoraStatusTitle(totalCount, missingCount) {
|
||||
if (totalCount === 0) return "No LoRAs in this recipe";
|
||||
if (missingCount === 0) return "All LoRAs available - Ready to use";
|
||||
return `${missingCount} of ${totalCount} LoRAs missing`;
|
||||
}
|
||||
|
||||
attachEventListeners(card) {
|
||||
// Recipe card click event
|
||||
card.addEventListener('click', () => {
|
||||
this.clickHandler(this.recipe);
|
||||
});
|
||||
|
||||
// Share button click event - prevent propagation to card
|
||||
card.querySelector('.fa-share-alt')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
// TODO: Implement share functionality
|
||||
showToast('Share functionality will be implemented later', 'info');
|
||||
});
|
||||
|
||||
// Copy button click event - prevent propagation to card
|
||||
card.querySelector('.fa-copy')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.copyRecipeSyntax();
|
||||
});
|
||||
|
||||
// Delete button click event - prevent propagation to card
|
||||
card.querySelector('.fa-trash')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
this.showDeleteConfirmation();
|
||||
});
|
||||
}
|
||||
|
||||
copyRecipeSyntax() {
|
||||
try {
|
||||
// Generate recipe syntax in the format <lora:file_name:strength> separated by spaces
|
||||
const loras = this.recipe.loras || [];
|
||||
if (loras.length === 0) {
|
||||
showToast('No LoRAs in this recipe to copy', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const syntax = loras.map(lora => {
|
||||
// Use file_name if available, otherwise use empty placeholder
|
||||
const fileName = lora.file_name || '[missing-lora]';
|
||||
const strength = lora.strength || 1.0;
|
||||
return `<lora:${fileName}:${strength}>`;
|
||||
}).join(' ');
|
||||
|
||||
// Copy to clipboard
|
||||
navigator.clipboard.writeText(syntax)
|
||||
.then(() => {
|
||||
showToast('Recipe syntax copied to clipboard', 'success');
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
showToast('Failed to copy recipe syntax', 'error');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error copying recipe syntax:', error);
|
||||
showToast('Error copying recipe syntax', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
showDeleteConfirmation() {
|
||||
try {
|
||||
// Get recipe ID
|
||||
const recipeId = this.recipe.id;
|
||||
if (!recipeId) {
|
||||
showToast('Cannot delete recipe: Missing recipe ID', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up delete modal content
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
const deleteMessage = deleteModal.querySelector('.delete-message');
|
||||
const deleteModelInfo = deleteModal.querySelector('.delete-model-info');
|
||||
|
||||
// Update modal content
|
||||
deleteMessage.textContent = 'Are you sure you want to delete this recipe?';
|
||||
deleteModelInfo.innerHTML = `
|
||||
<div class="delete-preview">
|
||||
<img src="${this.recipe.file_url || '/loras_static/images/no-preview.png'}" alt="${this.recipe.title}">
|
||||
</div>
|
||||
<div class="delete-info">
|
||||
<h3>${this.recipe.title}</h3>
|
||||
<p>This action cannot be undone.</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Store recipe ID in the modal for the delete confirmation handler
|
||||
deleteModal.dataset.recipeId = recipeId;
|
||||
|
||||
// Update the confirm delete button to use recipe delete handler
|
||||
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
||||
deleteBtn.onclick = () => this.confirmDeleteRecipe();
|
||||
|
||||
// Show the modal
|
||||
deleteModal.style.display = 'flex';
|
||||
} catch (error) {
|
||||
console.error('Error showing delete confirmation:', error);
|
||||
showToast('Error showing delete confirmation', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
confirmDeleteRecipe() {
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
const recipeId = deleteModal.dataset.recipeId;
|
||||
|
||||
if (!recipeId) {
|
||||
showToast('Cannot delete recipe: Missing recipe ID', 'error');
|
||||
closeDeleteModal();
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
||||
const originalText = deleteBtn.textContent;
|
||||
deleteBtn.textContent = 'Deleting...';
|
||||
deleteBtn.disabled = true;
|
||||
|
||||
// Call API to delete the recipe
|
||||
fetch(`/api/recipe/${recipeId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to delete recipe');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
showToast('Recipe deleted successfully', 'success');
|
||||
|
||||
// Refresh the recipe list if we're on the recipes page
|
||||
if (window.recipeManager && typeof window.recipeManager.loadRecipes === 'function') {
|
||||
window.recipeManager.loadRecipes();
|
||||
}
|
||||
|
||||
closeDeleteModal();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error deleting recipe:', error);
|
||||
showToast('Error deleting recipe: ' + error.message, 'error');
|
||||
|
||||
// Reset button state
|
||||
deleteBtn.textContent = originalText;
|
||||
deleteBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
closeDeleteModal() {
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
deleteModal.style.display = 'none';
|
||||
|
||||
// Reset the delete button handler
|
||||
const deleteBtn = deleteModal.querySelector('.delete-btn');
|
||||
deleteBtn.textContent = 'Delete';
|
||||
deleteBtn.disabled = false;
|
||||
deleteBtn.onclick = null;
|
||||
}
|
||||
}
|
||||
|
||||
export { RecipeCard };
|
||||
205
static/js/components/RecipeModal.js
Normal file
205
static/js/components/RecipeModal.js
Normal file
@@ -0,0 +1,205 @@
|
||||
// Recipe Modal Component
|
||||
import { showToast } from '../utils/uiHelpers.js';
|
||||
|
||||
class RecipeModal {
|
||||
constructor() {
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.setupCopyButtons();
|
||||
}
|
||||
|
||||
showRecipeDetails(recipe) {
|
||||
console.log(recipe);
|
||||
// Set modal title
|
||||
const modalTitle = document.getElementById('recipeModalTitle');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = recipe.title || 'Recipe Details';
|
||||
}
|
||||
|
||||
// Set recipe image
|
||||
const modalImage = document.getElementById('recipeModalImage');
|
||||
if (modalImage) {
|
||||
// Ensure file_url exists, fallback to file_path if needed
|
||||
const imageUrl = recipe.file_url ||
|
||||
(recipe.file_path ? `/loras_static/root1/preview/${recipe.file_path.split('/').pop()}` :
|
||||
'/loras_static/images/no-preview.png');
|
||||
modalImage.src = imageUrl;
|
||||
modalImage.alt = recipe.title || 'Recipe Preview';
|
||||
}
|
||||
|
||||
// Set generation parameters
|
||||
const promptElement = document.getElementById('recipePrompt');
|
||||
const negativePromptElement = document.getElementById('recipeNegativePrompt');
|
||||
const otherParamsElement = document.getElementById('recipeOtherParams');
|
||||
|
||||
if (recipe.gen_params) {
|
||||
// Set prompt
|
||||
if (promptElement && recipe.gen_params.prompt) {
|
||||
promptElement.textContent = recipe.gen_params.prompt;
|
||||
} else if (promptElement) {
|
||||
promptElement.textContent = 'No prompt information available';
|
||||
}
|
||||
|
||||
// Set negative prompt
|
||||
if (negativePromptElement && recipe.gen_params.negative_prompt) {
|
||||
negativePromptElement.textContent = recipe.gen_params.negative_prompt;
|
||||
} else if (negativePromptElement) {
|
||||
negativePromptElement.textContent = 'No negative prompt information available';
|
||||
}
|
||||
|
||||
// Set other parameters
|
||||
if (otherParamsElement) {
|
||||
// Clear previous params
|
||||
otherParamsElement.innerHTML = '';
|
||||
|
||||
// Add all other parameters except prompt and negative_prompt
|
||||
const excludedParams = ['prompt', 'negative_prompt'];
|
||||
|
||||
for (const [key, value] of Object.entries(recipe.gen_params)) {
|
||||
if (!excludedParams.includes(key) && value !== undefined && value !== null) {
|
||||
const paramTag = document.createElement('div');
|
||||
paramTag.className = 'param-tag';
|
||||
paramTag.innerHTML = `
|
||||
<span class="param-name">${key}:</span>
|
||||
<span class="param-value">${value}</span>
|
||||
`;
|
||||
otherParamsElement.appendChild(paramTag);
|
||||
}
|
||||
}
|
||||
|
||||
// If no other params, show a message
|
||||
if (otherParamsElement.children.length === 0) {
|
||||
otherParamsElement.innerHTML = '<div class="no-params">No additional parameters available</div>';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No generation parameters available
|
||||
if (promptElement) promptElement.textContent = 'No prompt information available';
|
||||
if (negativePromptElement) negativePromptElement.textContent = 'No negative prompt information available';
|
||||
if (otherParamsElement) otherParamsElement.innerHTML = '<div class="no-params">No parameters available</div>';
|
||||
}
|
||||
|
||||
// Set LoRAs list and count
|
||||
const lorasListElement = document.getElementById('recipeLorasList');
|
||||
const lorasCountElement = document.getElementById('recipeLorasCount');
|
||||
|
||||
// 检查所有 LoRAs 是否都在库中
|
||||
let allLorasAvailable = true;
|
||||
let missingLorasCount = 0;
|
||||
|
||||
if (recipe.loras && recipe.loras.length > 0) {
|
||||
recipe.loras.forEach(lora => {
|
||||
if (!lora.inLibrary) {
|
||||
allLorasAvailable = false;
|
||||
missingLorasCount++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置 LoRAs 计数和状态
|
||||
if (lorasCountElement && recipe.loras) {
|
||||
const totalCount = recipe.loras.length;
|
||||
|
||||
// 创建状态指示器
|
||||
let statusHTML = '';
|
||||
if (totalCount > 0) {
|
||||
if (allLorasAvailable) {
|
||||
statusHTML = `<div class="recipe-status ready"><i class="fas fa-check-circle"></i> Ready to use</div>`;
|
||||
} else {
|
||||
statusHTML = `<div class="recipe-status missing"><i class="fas fa-exclamation-triangle"></i> ${missingLorasCount} missing</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
lorasCountElement.innerHTML = `<i class="fas fa-layer-group"></i> ${totalCount} LoRAs ${statusHTML}`;
|
||||
}
|
||||
|
||||
if (lorasListElement && recipe.loras && recipe.loras.length > 0) {
|
||||
lorasListElement.innerHTML = recipe.loras.map(lora => {
|
||||
const existsLocally = lora.inLibrary;
|
||||
const localPath = lora.localPath || '';
|
||||
|
||||
// Create local status badge
|
||||
const localStatus = existsLocally ?
|
||||
`<div class="local-badge">
|
||||
<i class="fas fa-check"></i> In Library
|
||||
<div class="local-path">${localPath}</div>
|
||||
</div>` :
|
||||
`<div class="missing-badge">
|
||||
<i class="fas fa-exclamation-triangle"></i> Not in Library
|
||||
</div>`;
|
||||
|
||||
return `
|
||||
<div class="recipe-lora-item ${existsLocally ? 'exists-locally' : 'missing-locally'}">
|
||||
<div class="recipe-lora-thumbnail">
|
||||
<img src="${lora.preview_url || '/loras_static/images/no-preview.png'}" alt="LoRA preview">
|
||||
</div>
|
||||
<div class="recipe-lora-content">
|
||||
<div class="recipe-lora-header">
|
||||
<h4>${lora.modelName}</h4>
|
||||
${localStatus}
|
||||
</div>
|
||||
${lora.modelVersionName ? `<div class="recipe-lora-version">${lora.modelVersionName}</div>` : ''}
|
||||
<div class="recipe-lora-info">
|
||||
${lora.baseModel ? `<div class="base-model">${lora.baseModel}</div>` : ''}
|
||||
<div class="recipe-lora-weight">Weight: ${lora.strength || 1.0}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Generate recipe syntax for copy button
|
||||
this.recipeLorasSyntax = recipe.loras.map(lora =>
|
||||
`<lora:${lora.file_name}:${lora.strength || 1.0}>`
|
||||
).join(' ');
|
||||
|
||||
} else if (lorasListElement) {
|
||||
lorasListElement.innerHTML = '<div class="no-loras">No LoRAs associated with this recipe</div>';
|
||||
this.recipeLorasSyntax = '';
|
||||
}
|
||||
|
||||
// Show the modal
|
||||
modalManager.showModal('recipeModal');
|
||||
}
|
||||
|
||||
// Setup copy buttons for prompts and recipe syntax
|
||||
setupCopyButtons() {
|
||||
const copyPromptBtn = document.getElementById('copyPromptBtn');
|
||||
const copyNegativePromptBtn = document.getElementById('copyNegativePromptBtn');
|
||||
const copyRecipeSyntaxBtn = document.getElementById('copyRecipeSyntaxBtn');
|
||||
|
||||
if (copyPromptBtn) {
|
||||
copyPromptBtn.addEventListener('click', () => {
|
||||
const promptText = document.getElementById('recipePrompt').textContent;
|
||||
this.copyToClipboard(promptText, 'Prompt copied to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
if (copyNegativePromptBtn) {
|
||||
copyNegativePromptBtn.addEventListener('click', () => {
|
||||
const negativePromptText = document.getElementById('recipeNegativePrompt').textContent;
|
||||
this.copyToClipboard(negativePromptText, 'Negative prompt copied to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
if (copyRecipeSyntaxBtn) {
|
||||
copyRecipeSyntaxBtn.addEventListener('click', () => {
|
||||
this.copyToClipboard(this.recipeLorasSyntax, 'Recipe syntax copied to clipboard');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to copy text to clipboard
|
||||
copyToClipboard(text, successMessage) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
showToast(successMessage, 'success');
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy text: ', err);
|
||||
showToast('Failed to copy text', 'error');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { RecipeModal };
|
||||
Reference in New Issue
Block a user