import { showToast, openCivitai } 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 { getModelApiClient } from '../../api/baseModelApi.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 = ` `; 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; case 'view-civitai': openCivitai(target.dataset.filepath); break; case 'view-creator': const username = target.dataset.username; if (username) { window.open(`https://civitai.com/user/${username}`, '_blank'); } 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 (notes and usage tips) 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 getModelApiClient().saveModelMetadata(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 { await getModelApiClient().saveModelMetadata(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'); };