Files
mudknight-comfyui-mudknight…/web/script.js
T

1630 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
let characters = {};
let models = {};
let styles = {};
let tags = {};
let currentEditName = null;
let currentEditType = null;
let currentOriginalName = null; // Add this
let currentAddType = null;
let searchTerms = {
character: '',
model: '',
style: '',
tag: '',
};
let characterImages = {};
let styleImages = {};
let activeTab = 'characters';
let selectedCategory = 'all';
let sidebarCollapsed = false;
let autocompleteTags = [];
let autocompleteState = {
activeElement: null,
selectedIndex: -1,
currentWord: '',
wordStart: 0,
filteredTags: []
};
let savedSearches = {
character: '',
model: '',
style: '',
tag: ''
};
// Load sidebar state from localStorage
function loadSidebarState() {
const saved = localStorage.getItem('sidebarCollapsed');
if (saved !== null) {
sidebarCollapsed = saved === 'true';
updateSidebarState();
}
}
// Toggle sidebar collapsed state
function toggleSidebar() {
sidebarCollapsed = !sidebarCollapsed;
localStorage.setItem('sidebarCollapsed', sidebarCollapsed);
updateSidebarState();
}
// Update sidebar UI based on collapsed state
function updateSidebarState() {
const sidebar = document.getElementById('categorySidebar');
const toggleBtn = document.getElementById('sidebarToggle');
if (sidebarCollapsed) {
sidebar.classList.add('collapsed');
toggleBtn.innerHTML = '›';
toggleBtn.setAttribute('aria-label', 'Expand sidebar');
} else {
sidebar.classList.remove('collapsed');
toggleBtn.innerHTML = '‹';
toggleBtn.setAttribute('aria-label', 'Collapse sidebar');
}
}
// Autocomplete
async function loadAutocompleteTags() {
try {
const response = await fetch('autocomplete.txt');
if (!response.ok) {
console.log('Autocomplete file not found');
return;
}
const text = await response.text();
const lines = text.split('\n');
autocompleteTags = lines
.map(line => {
const parts = line.trim().split(',');
if (parts.length >= 2) {
return {
tag: parts[0].trim(),
count: parseInt(parts[1]) || 0
};
}
return null;
})
.filter(Boolean)
.sort((a, b) => b.count - a.count); // Sort by usage count
console.log(`Loaded ${autocompleteTags.length} autocomplete tags`);
} catch (error) {
console.error('Error loading autocomplete tags:', error);
}
}
async function loadAutocompleteTags() {
try {
const response = await fetch('autocomplete.txt');
if (!response.ok) {
console.log('Autocomplete file not found');
return;
}
const text = await response.text();
const lines = text.split('\n');
autocompleteTags = lines
.map(line => {
const parts = line.trim().split(',');
if (parts.length >= 2) {
return {
tag: parts[0].trim(),
count: parseInt(parts[1]) || 0
};
}
return null;
})
.filter(Boolean)
.sort((a, b) => b.count - a.count); // Sort by usage count
console.log(`Loaded ${autocompleteTags.length} autocomplete tags`);
} catch (error) {
console.error('Error loading autocomplete tags:', error);
}
}
function getCurrentWord(input) {
const cursorPos = input.selectionStart;
const text = input.value;
// Find the start of the current word (after last comma or start)
let start = text.lastIndexOf(',', cursorPos - 1) + 1;
// Skip any whitespace after the comma to find actual word start
while (start < cursorPos && text[start] === ' ') {
start++;
}
// Find the end of the current word (next comma or end)
let end = text.indexOf(',', cursorPos);
if (end === -1) end = text.length;
const word = text.substring(start, end).trim();
return {
word: word,
start: start,
end: end
};
}
function showAutocomplete(input, word, startPos) {
if (!word || word.length < 2) {
hideAutocomplete();
return;
}
const searchWord = word.toLowerCase();
const filtered = autocompleteTags
.filter(item => item.tag.toLowerCase().includes(searchWord))
.slice(0, 10); // Limit to 10 suggestions
if (filtered.length === 0) {
hideAutocomplete();
return;
}
autocompleteState.filteredTags = filtered;
autocompleteState.selectedIndex = 0; // Changed from -1 to 0
autocompleteState.activeElement = input;
autocompleteState.currentWord = word;
autocompleteState.wordStart = startPos;
const dropdown = document.getElementById('autocompleteDropdown');
dropdown.innerHTML = '';
filtered.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'autocomplete-item';
if (index === 0) {
div.classList.add('selected'); // Add selected class to first item
}
div.innerHTML = `
<span class="autocomplete-tag">${item.tag}</span>
<span class="autocomplete-count">${item.count}</span>
`;
div.onclick = () => selectAutocomplete(index);
dropdown.appendChild(div);
});
// Position the dropdown
const rect = input.getBoundingClientRect();
dropdown.style.left = rect.left + 'px';
dropdown.style.top = (rect.bottom + 5) + 'px';
dropdown.style.width = rect.width + 'px';
dropdown.style.display = 'block';
}
function hideAutocomplete() {
const dropdown = document.getElementById('autocompleteDropdown');
dropdown.style.display = 'none';
autocompleteState.activeElement = null;
autocompleteState.selectedIndex = -1;
autocompleteState.filteredTags = [];
}
function selectAutocomplete(index) {
if (!autocompleteState.activeElement ||
index < 0 ||
index >= autocompleteState.filteredTags.length) {
return;
}
const input = autocompleteState.activeElement;
let selectedTag = autocompleteState.filteredTags[index].tag;
// Replace underscores with spaces
selectedTag = selectedTag.replace(/_/g, ' ');
// Escape parentheses
selectedTag = selectedTag.replace(/\(/g, '\\(').replace(/\)/g, '\\)');
const text = input.value;
// Find the current word boundaries
const info = getCurrentWord(input);
// Replace the current word with the selected tag
const before = text.substring(0, info.start);
const after = text.substring(info.end);
// Build new text - keep existing content before, just replace the word
const newText = before + selectedTag + ', ' + after.trimStart();
input.value = newText;
// Set cursor position after the inserted tag and comma
const newCursorPos = before.length + selectedTag.length + 2;
input.setSelectionRange(newCursorPos, newCursorPos);
hideAutocomplete();
input.focus();
}
function handleAutocompleteKeydown(e, input) {
const dropdown = document.getElementById('autocompleteDropdown');
if (dropdown.style.display !== 'block') {
return;
}
if (e.key === 'ArrowDown') {
e.preventDefault();
if (autocompleteState.selectedIndex < autocompleteState.filteredTags.length - 1) {
autocompleteState.selectedIndex++;
updateAutocompleteSelection();
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (autocompleteState.selectedIndex > 0) {
autocompleteState.selectedIndex--;
updateAutocompleteSelection();
}
} else if (e.key === 'Enter' || e.key === 'Tab') {
if (autocompleteState.selectedIndex >= 0) {
e.preventDefault();
selectAutocomplete(autocompleteState.selectedIndex);
}
} else if (e.key === 'Escape') {
e.preventDefault();
hideAutocomplete();
}
}
function updateAutocompleteSelection() {
const items = document.querySelectorAll('.autocomplete-item');
items.forEach((item, index) => {
if (index === autocompleteState.selectedIndex) {
item.classList.add('selected');
item.scrollIntoView({ block: 'nearest' });
} else {
item.classList.remove('selected');
}
});
}
function setupAutocomplete(input) {
input.addEventListener('input', (e) => {
const info = getCurrentWord(input);
showAutocomplete(input, info.word, info.start);
});
input.addEventListener('keydown', (e) => {
handleAutocompleteKeydown(e, input);
});
input.addEventListener('blur', (e) => {
// Delay hiding to allow click on dropdown
setTimeout(() => {
if (autocompleteState.activeElement === input) {
hideAutocomplete();
}
}, 200);
});
}
// Categories
function getAllCategories() {
const categorySet = new Set();
for (const name of Object.keys(characters)) {
const char = characters[name];
const categories = char.categories || '';
if (categories.trim()) {
const cats = categories.split(',').map(c => c.trim()).filter(Boolean);
cats.forEach(cat => categorySet.add(cat));
}
}
return Array.from(categorySet).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
}
function getCategoryCounts() {
const counts = { all: 0 };
for (const name of Object.keys(characters)) {
counts.all++;
const char = characters[name];
const categories = char.categories || '';
if (categories.trim()) {
const cats = categories.split(',').map(c => c.trim()).filter(Boolean);
cats.forEach(cat => {
counts[cat] = (counts[cat] || 0) + 1;
});
}
}
return counts;
}
function characterMatchesCategory(name, category) {
if (category === 'all') return true;
const char = characters[name];
const categories = char.categories || '';
if (!categories.trim()) return false;
const cats = categories.split(',').map(c => c.trim()).filter(Boolean);
return cats.includes(category);
}
function renderCategories() {
const categoryList = document.getElementById('categoryList');
const allCategories = getAllCategories();
const counts = getCategoryCounts();
// Clear existing (except "All")
categoryList.innerHTML = `
<div class="category-item ${selectedCategory === 'all' ? 'active' : ''}"
data-category="all" onclick="selectCategory('all')">
<span class="category-name">All Characters</span>
<span class="category-count">${counts.all}</span>
</div>
`;
// Add category items
for (const category of allCategories) {
const item = document.createElement('div');
item.className = `category-item ${selectedCategory === category ? 'active' : ''}`;
item.setAttribute('data-category', category);
item.onclick = () => selectCategory(category);
item.innerHTML = `
<span class="category-name">${category}</span>
<span class="category-count">${counts[category] || 0}</span>
`;
categoryList.appendChild(item);
}
}
function selectCategory(category) {
selectedCategory = category;
renderCategories();
renderCharacters();
}
function encodeName(name) {
return btoa(unescape(encodeURIComponent(name)));
}
function decodeName(b64) {
return decodeURIComponent(escape(atob(b64)));
}
function switchTab(tabName) {
// Map tab names to search term keys
const tabMap = {
characters: 'character',
models: 'model',
styles: 'style',
tags: 'tag',
};
// Save current search
const searchInput = document.getElementById('searchInput');
const currentKey = tabMap[activeTab];
savedSearches[currentKey] = searchInput.value;
searchTerms[currentKey] = searchInput.value;
activeTab = tabName;
// Restore saved search for new tab
const newKey = tabMap[tabName];
searchInput.value = savedSearches[newKey] || '';
searchTerms[newKey] = searchInput.value;
// Update placeholder
const placeholders = {
characters: 'Search characters...',
models: 'Search models...',
styles: 'Search styles...',
tags: 'Search tag presets...',
};
searchInput.placeholder = placeholders[tabName];
// Show/hide clear button
document.getElementById('clearSearch').style.display =
searchInput.value ? 'block' : 'none';
// Update tab buttons
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
if (btn.textContent.trim().toLowerCase() === tabName) {
btn.classList.add('active');
}
});
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}Tab`).classList.add('active');
// Render current tab
if (tabName === 'characters') renderCharacters();
else if (tabName === 'models') renderModels();
else if (tabName === 'styles') renderStyles();
else if (tabName === 'tags') renderTags();
}
function clearSearch() {
// Map tab names to search term keys
const tabMap = {
characters: 'character',
models: 'model',
styles: 'style'
};
const searchInput = document.getElementById('searchInput');
const currentKey = tabMap[activeTab];
searchInput.value = '';
searchTerms[currentKey] = '';
savedSearches[currentKey] = '';
document.getElementById('clearSearch').style.display = 'none';
if (activeTab === 'characters') renderCharacters();
else if (activeTab === 'models') renderModels();
else if (activeTab === 'styles') renderStyles();
searchInput.focus();
}
async function loadData() {
try {
// Load autocomplete tags first
await loadAutocompleteTags();
// Load characters
const charResponse = await fetch('/character_editor');
if (charResponse.ok) {
characters = await charResponse.json();
await checkImages('character');
}
// Load models
const modelResponse = await fetch('/model_editor');
if (modelResponse.ok) {
models = await modelResponse.json();
}
// Load styles
const styleResponse = await fetch('/style_editor');
if (styleResponse.ok) {
styles = await styleResponse.json();
await checkImages('style');
}
// Load tags
const tagResponse = await fetch('/tag_editor');
if (tagResponse.ok) {
tags = await tagResponse.json();
}
renderAll();
} catch (error) {
console.error('Load error:', error);
showStatus('Error loading data: ' + error.message, 'error');
}
}
async function checkImages(type) {
const dataMap = {
character: characters,
style: styles
};
const imageMap = {
character: characterImages,
style: styleImages
};
const endpoint = type === 'character' ?
'/character_editor/image/' : '/style_editor/image/';
for (const name of Object.keys(dataMap[type])) {
const response = await fetch(
`${endpoint}${encodeName(name)}`
);
imageMap[type][name] = response.ok;
}
}
function getImageUrl(name, type = 'character') {
const endpoint = type === 'character' ?
'/character_editor/image/' : '/style_editor/image/';
return `${endpoint}${encodeName(name)}?t=${Date.now()}`;
}
function getSortedNames(obj) {
return Object.keys(obj).sort((a, b) =>
a.toLowerCase().localeCompare(b.toLowerCase())
);
}
function renderAll() {
renderCategories();
renderCharacters();
renderModels();
renderStyles();
renderTags();
}
function renderCharacters() {
const grid = document.getElementById('characterGrid');
const emptyState = document.getElementById('emptyState');
grid.innerHTML = '';
// Add the "Add" card first
const addCard = document.createElement('div');
addCard.className = 'character-card add-card';
addCard.innerHTML = '+';
addCard.onclick = () => {
showEditModal('character', '');
};
grid.appendChild(addCard);
const sortedNames = getSortedNames(characters);
const filteredNames = sortedNames.filter(name => {
const char = characters[name];
const searchLower = searchTerms.character.toLowerCase();
const matchesName = name.toLowerCase().includes(searchLower);
const categories = char.categories || '';
const matchesCategorySearch = categories.toLowerCase().includes(searchLower);
const matchesSearch = matchesName || matchesCategorySearch;
const matchesCategoryFilter = characterMatchesCategory(name, selectedCategory);
return matchesSearch && matchesCategoryFilter;
});
emptyState.style.display = filteredNames.length === 0 ? 'block' : 'none';
for (const name of filteredNames) {
const card = document.createElement('div');
card.className = 'character-card';
const hasImage = characterImages[name];
if (hasImage) {
card.classList.add('has-image');
card.style.backgroundImage = `url(${getImageUrl(name)})`;
}
card.onclick = () => showEditModal('character', name);
card.innerHTML = `
${!hasImage ? '<div class="character-card-placeholder"></div>' : ''}
<div class="upload-hint">Drop image here</div>
<div class="character-card-name">${name}</div>
`;
setupDragAndDrop(card, name, 'character');
grid.appendChild(card);
}
}
function renderModels() {
const grid = document.getElementById('modelGrid');
const emptyState = document.getElementById('modelEmptyState');
grid.innerHTML = '';
// Add the "Add" card first
const addCard = document.createElement('div');
addCard.className = 'preset-card preset-add-card';
addCard.innerHTML = '+';
addCard.onclick = () => {
showEditModal('model', '');
};
grid.appendChild(addCard);
const sortedNames = getSortedNames(models);
const filteredNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchTerms.model.toLowerCase())
);
emptyState.style.display = filteredNames.length === 0 ? 'block' : 'none';
for (const name of filteredNames) {
const model = models[name];
const card = document.createElement('div');
card.className = 'preset-card';
card.onclick = () => showEditModal('model', name);
// Build preview with colored sections
let previewHTML = '';
// Collect all positive tags
const positiveParts = [];
if (model.quality?.positive) {
positiveParts.push(model.quality.positive);
}
if (model.embeddings?.positive) {
positiveParts.push(model.embeddings.positive);
}
// Collect all negative tags
const negativeParts = [];
if (model.quality?.negative) {
negativeParts.push(model.quality.negative);
}
if (model.embeddings?.negative) {
negativeParts.push(model.embeddings.negative);
}
// Build preview HTML
if (positiveParts.length > 0) {
previewHTML += `<span class="tag-positive">${positiveParts.join(', ')}</span>`;
}
if (negativeParts.length > 0) {
if (previewHTML) previewHTML += ' ';
previewHTML += `<span class="tag-negative">${negativeParts.join(', ')}</span>`;
}
if (!previewHTML) {
previewHTML = '<span class="tag-empty">(no tags defined)</span>';
}
card.innerHTML = `
<div class="preset-card-name">${name}</div>
<div class="preset-card-content tag-preview">${previewHTML}</div>
`;
grid.appendChild(card);
}
}
function renderStyles() {
const grid = document.getElementById('styleGrid');
const emptyState = document.getElementById('styleEmptyState');
grid.innerHTML = '';
// Add the "Add" card first
const addCard = document.createElement('div');
addCard.className = 'character-card add-card';
addCard.innerHTML = '+';
addCard.onclick = () => {
showEditModal('style', '');
};
grid.appendChild(addCard);
const sortedNames = getSortedNames(styles);
const filteredNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchTerms.style.toLowerCase())
);
emptyState.style.display = filteredNames.length === 0 ? 'block' : 'none';
for (const name of filteredNames) {
const card = document.createElement('div');
card.className = 'character-card';
const hasImage = styleImages[name];
if (hasImage) {
card.classList.add('has-image');
card.style.backgroundImage = `url(${getImageUrl(name, 'style')})`;
}
card.onclick = () => showEditModal('style', name);
card.innerHTML = `
${!hasImage ? '<div class="character-card-placeholder"></div>' : ''}
<div class="upload-hint">Drop image here</div>
<div class="character-card-name">${name}</div>
`;
setupDragAndDrop(card, name, 'style');
grid.appendChild(card);
}
}
function renderTags() {
const grid = document.getElementById('tagGrid');
const emptyState = document.getElementById('tagEmptyState');
grid.innerHTML = '';
// Add the "Add" card first
const addCard = document.createElement('div');
addCard.className = 'preset-card preset-add-card';
addCard.innerHTML = '+';
addCard.onclick = () => {
showEditModal('tag', '');
};
grid.appendChild(addCard);
const sortedNames = getSortedNames(tags);
const filteredNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchTerms.tag.toLowerCase())
);
emptyState.style.display = filteredNames.length === 0 ?
'block' : 'none';
for (const name of filteredNames) {
const tag = tags[name];
const card = document.createElement('div');
card.className = 'preset-card';
card.onclick = () => showEditModal('tag', name);
// Build preview with colored sections
let previewHTML = '';
if (tag.positive && tag.positive.trim()) {
previewHTML += `<span class="tag-positive">${tag.positive}</span>`;
}
if (tag.negative && tag.negative.trim()) {
if (previewHTML) previewHTML += ' ';
previewHTML += `<span class="tag-negative">${tag.negative}</span>`;
}
if (!previewHTML) {
previewHTML = '<span class="tag-empty">(no tags defined)</span>';
}
card.innerHTML = `
<div class="preset-card-name">${name}</div>
<div class="preset-card-content tag-preview">${previewHTML}</div>
`;
grid.appendChild(card);
}
}
function showAddModal(type) {
currentAddType = type;
const modal = document.getElementById('addModal');
const title = document.getElementById('addModalTitle');
const label = document.getElementById('addModalLabel');
const typeNames = {
character: 'Character',
model: 'Model',
style: 'Style'
};
title.textContent = `Add New ${typeNames[type]}`;
label.textContent = `${typeNames[type]} Name`;
document.getElementById('newItemName').value = '';
modal.classList.add('show');
}
function hideAddModal() {
document.getElementById('addModal').classList.remove('show');
currentAddType = null;
}
function addItem() {
const name = document.getElementById('newItemName').value.trim();
if (!name) {
alert('Please enter a name');
return;
}
const dataMap = {
character: characters,
model: models,
style: styles
};
if (dataMap[currentAddType][name]) {
alert('Item already exists');
return;
}
// Create default data based on type
if (currentAddType === 'character') {
characters[name] = {
character: '',
top: '',
bottom: '',
neg: ''
};
} else if (currentAddType === 'model') {
models[name] = {
quality: { positive: '', negative: '' },
embeddings: { positive: '', negative: '' }
};
} else if (currentAddType === 'style') {
styles[name] = {
positive: '',
negative: ''
};
}
renderAll();
hideAddModal();
showEditModal(currentAddType, name);
}
function showEditModal(type, name) {
currentEditName = name;
currentEditType = type;
if (type === 'character') {
const data = characters[name] || {
character: '',
top: '',
bottom: '',
neg: '',
categories: ''
};
currentOriginalName = name;
document.getElementById('editCharNameInput').value = name;
document.getElementById('editCharacter').value = data.character || '';
document.getElementById('editTop').value = data.top || '';
document.getElementById('editBottom').value = data.bottom || '';
document.getElementById('editNeg').value = data.neg || '';
document.getElementById('editCategories').value = data.categories || '';
const preview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
if (name && characterImages[name]) {
previewImg.src = getImageUrl(name);
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
// Setup drag and drop for modal
if (name) {
setupModalDragAndDrop('editModal', name, 'character');
}
// Setup autocomplete for tag fields
setupAutocomplete(document.getElementById('editCharacter'));
setupAutocomplete(document.getElementById('editTop'));
setupAutocomplete(document.getElementById('editBottom'));
setupAutocomplete(document.getElementById('editNeg'));
setupAutocomplete(document.getElementById('editCategories'));
setupAutocomplete(document.getElementById('editCharNameInput'));
document.getElementById('editModal').classList.add('show');
} else if (type === 'model') {
const data = models[name] || {
quality: { positive: '', negative: '' },
embeddings: { positive: '', negative: '' }
};
currentOriginalName = name; // Add this line
document.getElementById('editModelNameInput').value = name; // Changed from textContent
document.getElementById('editModelQualityPos').value =
data.quality?.positive || '';
document.getElementById('editModelQualityNeg').value =
data.quality?.negative || '';
document.getElementById('editModelEmbedPos').value =
data.embeddings?.positive || '';
document.getElementById('editModelEmbedNeg').value =
data.embeddings?.negative || '';
// Setup autocomplete for model fields
setupAutocomplete(document.getElementById('editModelQualityPos'));
setupAutocomplete(document.getElementById('editModelQualityNeg'));
setupAutocomplete(document.getElementById('editModelEmbedPos'));
setupAutocomplete(document.getElementById('editModelEmbedNeg'));
document.getElementById('modelEditModal').classList.add('show');
} else if (type === 'style') {
const data = styles[name] || {
positive: '',
negative: ''
};
currentOriginalName = name; // Add this line
document.getElementById('editStyleNameInput').value = name; // Changed from textContent
document.getElementById('editStylePos').value = data.positive || '';
document.getElementById('editStyleNeg').value = data.negative || '';
const preview = document.getElementById('styleImagePreview');
const previewImg = document.getElementById('stylePreviewImg');
if (name && styleImages[name]) {
previewImg.src = getImageUrl(name, 'style');
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
// Setup drag and drop for modal
if (name) {
setupModalDragAndDrop('styleEditModal', name, 'style');
}
// Setup autocomplete for style fields
setupAutocomplete(document.getElementById('editStylePos'));
setupAutocomplete(document.getElementById('editStyleNeg'));
document.getElementById('styleEditModal').classList.add('show');
} else if (type === 'tag') {
const data = tags[name] || {
positive: '',
negative: ''
};
currentOriginalName = name;
document.getElementById('editTagNameInput').value = name;
document.getElementById('editTagPos').value = data.positive || '';
document.getElementById('editTagNeg').value = data.negative || '';
// Setup autocomplete for tag fields
setupAutocomplete(document.getElementById('editTagPos'));
setupAutocomplete(document.getElementById('editTagNeg'));
setupAutocomplete(document.getElementById('editTagNameInput'));
document.getElementById('tagEditModal').classList.add('show');
}
}
function setupModalDragAndDrop(modalId, name, type = 'character') {
const modal = document.getElementById(modalId);
const modalContent = modal.querySelector('.modal-content');
// Remove any existing listeners
const newModalContent = modalContent.cloneNode(true);
modalContent.parentNode.replaceChild(newModalContent, modalContent);
newModalContent.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
newModalContent.classList.add('drag-over');
});
newModalContent.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
if (e.target === newModalContent) {
newModalContent.classList.remove('drag-over');
}
});
newModalContent.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
newModalContent.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) {
await processImage(files[0], name, type);
// Update preview
if (type === 'character') {
const preview = document.getElementById('imagePreview');
const previewImg = document.getElementById('previewImg');
previewImg.src = getImageUrl(name);
preview.style.display = 'block';
} else if (type === 'style') {
const preview = document.getElementById('styleImagePreview');
const previewImg = document.getElementById('stylePreviewImg');
previewImg.src = getImageUrl(name, 'style');
preview.style.display = 'block';
}
}
});
}
function hideEditModal(type) {
if (type === 'character' || !type) {
document.getElementById('editModal').classList.remove('show');
}
if (type === 'model') {
document.getElementById('modelEditModal').classList.remove('show');
}
if (type === 'style') {
document.getElementById('styleEditModal').classList.remove('show');
}
if (type === 'tag') {
document.getElementById('tagEditModal').classList.remove('show');
}
currentEditName = null;
currentEditType = null;
currentOriginalName = null; // Clear original name
}
async function saveItem(type) {
if (!currentEditName && currentEditName !== '') return;
if (type === 'character') {
await saveCharacter();
} else if (type === 'model') {
const newName = document.getElementById('editModelNameInput').value.trim();
if (!newName) {
alert('Model name cannot be empty');
return;
}
// Check if name changed and new name already exists
if (newName !== currentOriginalName && currentOriginalName && models[newName]) {
alert('A model with this name already exists');
return;
}
const modelData = {
quality: {
positive: document.getElementById('editModelQualityPos').value,
negative: document.getElementById('editModelQualityNeg').value
},
embeddings: {
positive: document.getElementById('editModelEmbedPos').value,
negative: document.getElementById('editModelEmbedNeg').value
}
};
// If this is a new model (no original name)
if (!currentOriginalName) {
if (models[newName]) {
alert('A model with this name already exists');
return;
}
models[newName] = modelData;
} else if (newName !== currentOriginalName) {
// Rename: delete old, add new
delete models[currentOriginalName];
models[newName] = modelData;
} else {
// Just update existing
models[currentOriginalName] = modelData;
}
try {
const response = await fetch('/model_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(models)
});
if (response.ok) {
showStatus('Model saved successfully!', 'success');
renderModels();
hideEditModal('model');
} else {
throw new Error('Failed to save');
}
} catch (error) {
showStatus('Error saving model: ' + error.message, 'error');
}
} else if (type === 'style') {
const newName = document.getElementById('editStyleNameInput').value.trim();
if (!newName) {
alert('Style name cannot be empty');
return;
}
// Check if name changed and new name already exists
if (newName !== currentOriginalName && currentOriginalName && styles[newName]) {
alert('A style with this name already exists');
return;
}
const styleData = {
positive: document.getElementById('editStylePos').value,
negative: document.getElementById('editStyleNeg').value
};
// Handle image upload first if there is one
const fileInput = document.getElementById('editStyleImage');
if (fileInput.files.length > 0) {
await processImage(fileInput.files[0], currentOriginalName || newName, 'style');
}
// If this is a new style (no original name)
if (!currentOriginalName) {
if (styles[newName]) {
alert('A style with this name already exists');
return;
}
styles[newName] = styleData;
} else if (newName !== currentOriginalName) {
// Rename: delete old, add new
delete styles[currentOriginalName];
styles[newName] = styleData;
// Update image tracking if renaming
if (styleImages[currentOriginalName]) {
styleImages[newName] = true;
delete styleImages[currentOriginalName];
}
} else {
// Just update existing
styles[currentOriginalName] = styleData;
}
try {
const response = await fetch('/style_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(styles)
});
if (response.ok) {
showStatus('Style saved successfully!', 'success');
renderStyles();
hideEditModal('style');
} else {
throw new Error('Failed to save');
}
} catch (error) {
showStatus('Error saving style: ' + error.message, 'error');
}
} else if (type === 'tag') {
const newName = document.getElementById(
'editTagNameInput'
).value.trim();
if (!newName) {
alert('Tag name cannot be empty');
return;
}
// Check if name changed and new name already exists
if (newName !== currentOriginalName &&
currentOriginalName &&
tags[newName]) {
alert('A tag preset with this name already exists');
return;
}
const tagData = {
positive: document.getElementById('editTagPos').value,
negative: document.getElementById('editTagNeg').value
};
// If this is a new tag (no original name)
if (!currentOriginalName) {
if (tags[newName]) {
alert('A tag preset with this name already exists');
return;
}
tags[newName] = tagData;
} else if (newName !== currentOriginalName) {
// Rename: delete old, add new
delete tags[currentOriginalName];
tags[newName] = tagData;
} else {
// Just update existing
tags[currentOriginalName] = tagData;
}
try {
const response = await fetch('/tag_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tags)
});
if (response.ok) {
showStatus('Tag preset saved successfully!', 'success');
renderTags();
hideEditModal('tag');
} else {
throw new Error('Failed to save');
}
} catch (error) {
showStatus(
'Error saving tag preset: ' + error.message,
'error'
);
}
}
}
async function deleteCurrentItem(type) {
if (!currentEditName) return;
const typeNames = {
character: 'character',
model: 'model',
style: 'style'
};
if (!confirm(`Delete ${typeNames[type]} "${currentEditName}"?`)) return;
if (type === 'character') {
await deleteCurrentCharacter();
} else if (type === 'model') {
delete models[currentEditName];
try {
const response = await fetch('/model_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(models)
});
if (response.ok) {
showStatus('Model deleted successfully!', 'success');
renderModels();
hideEditModal('model');
} else {
throw new Error('Failed to delete');
}
} catch (error) {
showStatus('Error deleting model: ' + error.message, 'error');
}
} else if (type === 'style') {
delete styles[currentEditName];
try {
const response = await fetch('/style_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(styles)
});
if (response.ok) {
showStatus('Style deleted successfully!', 'success');
renderStyles();
hideEditModal('style');
} else {
throw new Error('Failed to delete');
}
} catch (error) {
showStatus('Error deleting style: ' + error.message, 'error');
}
} else if (type === 'tag') {
delete tags[currentEditName];
try {
const response = await fetch('/tag_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(tags)
});
if (response.ok) {
showStatus('Tag preset deleted successfully!', 'success');
renderTags();
hideEditModal('tag');
} else {
throw new Error('Failed to delete');
}
} catch (error) {
showStatus(
'Error deleting tag preset: ' + error.message,
'error'
);
}
}
}
// Keep existing character-specific functions
function setupDragAndDrop(card, name, type = 'character') {
card.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
card.classList.add('drag-over');
});
card.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
card.classList.remove('drag-over');
});
card.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
card.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0 && files[0].type.startsWith('image/')) {
await processImage(files[0], name, type);
}
});
}
async function processImage(file, name, type = 'character') {
return new Promise((resolve) => {
const reader = new FileReader();
reader.onload = async (e) => {
try {
const endpoint = type === 'character' ?
'/character_editor/image/' : '/style_editor/image/';
const response = await fetch(
`${endpoint}${encodeName(name)}`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
image: e.target.result
})
}
);
if (response.ok) {
if (type === 'character') {
characterImages[name] = true;
renderCharacters();
} else {
styleImages[name] = true;
renderStyles();
}
showStatus('Image updated!', 'success');
} else {
throw new Error('Failed to upload image');
}
resolve();
} catch (error) {
showStatus('Error uploading image: ' + error.message,
'error');
resolve();
}
};
reader.readAsDataURL(file);
});
}
async function removeImage(type = 'character') {
if (!currentEditName) return;
const typeLabel = type === 'character' ? 'character' : 'style';
if (!confirm(`Remove image from this ${typeLabel}?`)) return;
try {
const endpoint = type === 'character' ?
'/character_editor/image/' : '/style_editor/image/';
const response = await fetch(
`${endpoint}${encodeName(currentEditName)}`,
{
method: 'DELETE'
}
);
if (response.ok || response.status === 404) {
if (type === 'character') {
characterImages[currentEditName] = false;
document.getElementById('imagePreview').style.display =
'none';
renderCharacters();
} else {
styleImages[currentEditName] = false;
document.getElementById('styleImagePreview').style.display =
'none';
renderStyles();
}
showStatus('Image removed!', 'success');
} else {
throw new Error('Failed to delete image');
}
} catch (error) {
showStatus('Error removing image: ' + error.message, 'error');
}
}
async function saveCharacter() {
const newName = document.getElementById('editCharNameInput').value.trim();
if (!newName) {
alert('Character name cannot be empty');
return;
}
// Check if name changed and new name already exists
if (newName !== currentOriginalName && currentOriginalName && characters[newName]) {
alert('A character with this name already exists');
return;
}
const characterData = {
character: document.getElementById('editCharacter').value,
top: document.getElementById('editTop').value,
bottom: document.getElementById('editBottom').value,
neg: document.getElementById('editNeg').value,
categories: document.getElementById('editCategories').value
};
// Handle image upload first if there is one
const fileInput = document.getElementById('editImage');
if (fileInput.files.length > 0) {
await processImage(fileInput.files[0], currentOriginalName || newName);
}
// If this is a new character (no original name)
if (!currentOriginalName) {
if (characters[newName]) {
alert('A character with this name already exists');
return;
}
characters[newName] = characterData;
try {
const response = await fetch('/character_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(characters)
});
if (response.ok) {
showStatus('Character created successfully!', 'success');
renderAll();
hideEditModal('character');
} else {
throw new Error('Failed to save');
}
} catch (error) {
showStatus('Error creating character: ' + error.message, 'error');
// Remove the character if save failed
delete characters[newName];
}
return;
}
// If name changed, use rename endpoint
if (newName !== currentOriginalName) {
try {
const response = await fetch('/character_editor/rename', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
oldName: currentOriginalName,
newName: newName,
data: characterData
})
});
if (response.ok) {
// Update local data
delete characters[currentOriginalName];
characters[newName] = characterData;
// Update image tracking
if (characterImages[currentOriginalName]) {
characterImages[newName] = true;
delete characterImages[currentOriginalName];
}
showStatus('Character renamed successfully!', 'success');
renderAll();
hideEditModal('character');
} else {
const error = await response.json();
throw new Error(error.error || 'Failed to rename');
}
} catch (error) {
showStatus('Error renaming character: ' + error.message, 'error');
}
} else {
// Just update the data
characters[currentOriginalName] = characterData;
try {
const response = await fetch('/character_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(characters)
});
if (response.ok) {
showStatus('Character saved successfully!', 'success');
renderAll();
hideEditModal('character');
} else {
throw new Error('Failed to save');
}
} catch (error) {
showStatus('Error saving character: ' + error.message, 'error');
}
}
}
async function deleteCurrentCharacter() {
if (!currentEditName) return;
if (!confirm(`Delete character "${currentEditName}"?`)) return;
delete characters[currentEditName];
try {
const response = await fetch('/character_editor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(characters)
});
if (response.ok) {
showStatus('Character deleted successfully!', 'success');
renderAll(); // Changed from renderCharacters() to update categories
hideEditModal('character');
} else {
throw new Error('Failed to delete');
}
} catch (error) {
showStatus('Error deleting character: ' + error.message, 'error');
}
}
function showStatus(message, type) {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type} show`;
// Clear any existing timeout
if (status.hideTimeout) {
clearTimeout(status.hideTimeout);
}
// Set timeout to start slide-out animation
status.hideTimeout = setTimeout(() => {
status.classList.add('hiding');
// Remove after animation completes
setTimeout(() => {
status.classList.remove('show', 'hiding');
}, 300); // Match animation duration
}, 3000);
}
// Event listeners
document.getElementById('searchInput').addEventListener('input', (e) => {
// Map tab names to search term keys
const tabMap = {
characters: 'character',
models: 'model',
styles: 'style'
};
const value = e.target.value;
const currentKey = tabMap[activeTab];
searchTerms[currentKey] = value;
savedSearches[currentKey] = value;
// Show/hide clear button
document.getElementById('clearSearch').style.display =
value ? 'block' : 'none';
if (activeTab === 'characters') renderCharacters();
else if (activeTab === 'models') renderModels();
else if (activeTab === 'styles') renderStyles();
});
document.getElementById('editModal').addEventListener('click', (e) => {
if (e.target.id === 'editModal') hideEditModal('character');
});
document.getElementById('modelEditModal').addEventListener('click',
(e) => {
if (e.target.id === 'modelEditModal') hideEditModal('model');
}
);
document.getElementById('styleEditModal').addEventListener('click',
(e) => {
if (e.target.id === 'styleEditModal') hideEditModal('style');
}
);
document.getElementById('tagEditModal').addEventListener('click',
(e) => {
if (e.target.id === 'tagEditModal') hideEditModal('tag');
}
);
document.addEventListener('click', (e) => {
const dropdown = document.getElementById('autocompleteDropdown');
if (dropdown &&
dropdown.style.display === 'block' &&
!dropdown.contains(e.target) &&
e.target !== autocompleteState.activeElement) {
hideAutocomplete();
}
});
loadData();