import { showToast } from '../utils/uiHelpers.js'; import { modalManager } from '../managers/ModalManager.js'; import { state } from '../state/index.js'; 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.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 || {}); const version = state.previewVersions.get(lora.file_path); const previewUrl = lora.preview_url || '/loras_static/images/no-preview.png'; const versionedPreviewUrl = version ? `${previewUrl}?t=${version}` : previewUrl; card.innerHTML = `
${previewUrl.endsWith('.mp4') ? `` : `${lora.model_name}` }
${lora.base_model}
`; // Main card click event card.addEventListener('click', () => { 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, from_civitai: card.dataset.from_civitai === 'true', base_model: card.dataset.base_model, usage_tips: card.dataset.usage_tips, notes: card.dataset.notes, civitai: JSON.parse(card.dataset.meta || '{}') }; showLoraModal(loraMeta); }); // Copy button click event card.querySelector('.fa-copy')?.addEventListener('click', e => { e.stopPropagation(); const usageTips = JSON.parse(card.dataset.usage_tips || '{}'); const strength = usageTips.strength || 1; const loraSyntax = ``; navigator.clipboard.writeText(loraSyntax) .then(() => showToast('LoRA syntax copied', 'success')) .catch(() => showToast('Copy failed', 'error')); }); // Civitai button click event if (lora.from_civitai) { card.querySelector('.fa-globe')?.addEventListener('click', e => { e.stopPropagation(); openCivitai(lora.model_name); }); } // Delete button click event card.querySelector('.fa-trash')?.addEventListener('click', e => { e.stopPropagation(); deleteModel(lora.file_path); }); // Replace preview button click event card.querySelector('.fa-image')?.addEventListener('click', e => { e.stopPropagation(); replacePreview(lora.file_path); }); return card; } export function showLoraModal(lora) { const escapedWords = lora.civitai?.trainedWords?.length ? lora.civitai.trainedWords.map(word => word.replace(/'/g, '\\\'')) : []; const content = ` `; modalManager.showModal('loraModal', content); setupEditableFields(); setupShowcaseScroll(); // Add this line } function setupEditableFields() { const editableFields = document.querySelectorAll('.editable-field [contenteditable]'); editableFields.forEach(field => { field.addEventListener('focus', function() { if (this.textContent === 'Add your notes here...' || this.textContent === 'Save usage tips here..') { this.textContent = ''; } }); field.addEventListener('blur', function() { if (this.textContent.trim() === '') { this.textContent = this.classList.contains('usage-tips-content') ? 'Save usage tips here..' : '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') ? 0 : 1; presetValue.max = selected.includes('strength') ? 1 : 12; presetValue.step = selected.includes('strength') ? 0.01 : 1; if (selected === 'clip_skip') { presetValue.type = 'number'; presetValue.step = 1; } } else { presetValue.style.display = 'none'; } }); addPresetBtn.addEventListener('click', async function() { const key = presetSelector.value; const value = presetValue.value; if (!key || !value) return; const filePath = document.querySelector('.modal-content') .querySelector('.file-path').textContent + document.querySelector('.modal-content') .querySelector('#file-name').textContent + '.safetensors'; 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 }); loraCard.dataset.usage_tips = newPresetsJson; presetTags.innerHTML = renderPresetTags(currentPresets); presetSelector.value = ''; presetValue.value = ''; presetValue.style.display = 'none'; }); } window.saveNotes = async function(filePath) { const content = document.querySelector('.notes-content').textContent; try { await saveModelMetadata(filePath, { notes: content }); // Update the corresponding lora card's dataset const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); if (loraCard) { loraCard.dataset.notes = content; } showToast('Notes saved successfully', 'success'); } catch (error) { showToast('Failed to save notes', 'error'); } }; async function saveModelMetadata(filePath, data) { const response = await fetch('/loras/api/save-metadata', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ file_path: filePath, ...data }) }); if (!response.ok) { throw new Error('Failed to save metadata'); } } function renderTriggerWords(words) { if (!words.length) return `
No trigger word needed
`; return `
${words.map(word => `
${word}
`).join('')}
`; } function renderShowcaseImages(images) { if (!images?.length) return ''; return `
Scroll or click to show ${images.length} examples
`; } // Add this to the window object for global access export function toggleShowcase(element) { const carousel = element.nextElementSibling; const isCollapsed = carousel.classList.contains('collapsed'); const indicator = element.querySelector('span'); const icon = element.querySelector('i'); carousel.classList.toggle('collapsed'); if (isCollapsed) { const count = carousel.querySelectorAll('.media-wrapper').length; indicator.textContent = `Scroll or click to hide examples`; icon.classList.replace('fa-chevron-down', 'fa-chevron-up'); initLazyLoading(carousel); } else { const count = carousel.querySelectorAll('.media-wrapper').length; indicator.textContent = `Scroll or click to show ${count} examples`; icon.classList.replace('fa-chevron-up', 'fa-chevron-down'); } }; // Add lazy loading initialization function initLazyLoading(container) { const lazyElements = container.querySelectorAll('.lazy'); const lazyLoad = (element) => { if (element.tagName.toLowerCase() === 'video') { element.src = element.dataset.src; element.querySelector('source').src = element.dataset.src; element.load(); } else { element.src = element.dataset.src; } element.classList.remove('lazy'); }; const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { lazyLoad(entry.target); observer.unobserve(entry.target); } }); }); lazyElements.forEach(element => observer.observe(element)); } export function setupShowcaseScroll() { // Change from modal-content to window/document level document.addEventListener('wheel', (event) => { const modalContent = document.querySelector('.modal-content'); if (!modalContent) return; const showcase = modalContent.querySelector('.showcase-section'); if (!showcase) return; const carousel = showcase.querySelector('.carousel'); const scrollIndicator = showcase.querySelector('.scroll-indicator'); if (carousel?.classList.contains('collapsed') && event.deltaY > 0) { const isNearBottom = modalContent.scrollHeight - modalContent.scrollTop - modalContent.clientHeight < 100; if (isNearBottom) { toggleShowcase(scrollIndicator); event.preventDefault(); } } }, { passive: false }); // Add passive: false option here // Keep the existing scroll tracking code const modalContent = document.querySelector('.modal-content'); if (modalContent) { modalContent.addEventListener('scroll', () => { const backToTopBtn = modalContent.querySelector('.back-to-top'); if (backToTopBtn) { if (modalContent.scrollTop > 300) { backToTopBtn.classList.add('visible'); } else { backToTopBtn.classList.remove('visible'); } } }); } } export function scrollToTop(button) { const modalContent = button.closest('.modal-content'); if (modalContent) { modalContent.scrollTo({ top: 0, behavior: 'smooth' }); } } function parsePresets(usageTips) { if (!usageTips || usageTips === 'Save usage tips here..') return {}; try { return JSON.parse(usageTips); } catch { return {}; } } function renderPresetTags(presets) { return Object.entries(presets).map(([key, value]) => `
${formatPresetKey(key)}: ${value}
`).join(''); } function formatPresetKey(key) { return key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1) ).join(' '); } window.removePreset = async function(key) { const filePath = document.querySelector('.modal-content') .querySelector('.file-path').textContent + document.querySelector('.modal-content') .querySelector('#file-name').textContent + '.safetensors'; const loraCard = document.querySelector(`.lora-card[data-filepath="${filePath}"]`); const currentPresets = parsePresets(loraCard.dataset.usage_tips); delete currentPresets[key]; const newPresetsJson = JSON.stringify(currentPresets); await saveModelMetadata(filePath, { usage_tips: newPresetsJson }); loraCard.dataset.usage_tips = newPresetsJson; document.querySelector('.preset-tags').innerHTML = renderPresetTags(currentPresets); };