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

796 lines
20 KiB
JavaScript

let characters = {};
let models = {};
let styles = {};
let currentEditName = null;
let currentEditType = null;
let currentAddType = null;
let searchTerms = {
character: '',
model: '',
style: ''
};
let characterImages = {};
let styleImages = {};
let activeTab = 'characters';
let selectedCategory = 'all';
// 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) {
activeTab = tabName;
// Update tab buttons
document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(content => {
content.classList.remove('active');
});
document.getElementById(`${tabName}Tab`).classList.add('active');
}
async function loadData() {
try {
// 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');
}
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();
}
function renderCharacters() {
const grid = document.getElementById('characterGrid');
const emptyState = document.getElementById('emptyState');
grid.innerHTML = '';
const sortedNames = getSortedNames(characters);
const filteredNames = sortedNames.filter(name => {
const matchesSearch = name.toLowerCase().includes(
searchTerms.character.toLowerCase()
);
const matchesCategory = characterMatchesCategory(name, selectedCategory);
return matchesSearch && matchesCategory;
});
if (filteredNames.length === 0) {
grid.style.display = 'none';
emptyState.style.display = 'block';
return;
}
grid.style.display = 'grid';
emptyState.style.display = '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 = '';
const sortedNames = getSortedNames(models);
const filteredNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchTerms.model.toLowerCase())
);
if (filteredNames.length === 0) {
grid.style.display = 'none';
emptyState.style.display = 'block';
return;
}
grid.style.display = 'grid';
emptyState.style.display = 'none';
for (const name of filteredNames) {
const model = models[name];
const card = document.createElement('div');
card.className = 'preset-card';
card.onclick = () => showEditModal('model', name);
const preview = model.quality?.positive ||
model.embeddings?.positive || '';
card.innerHTML = `
<div class="preset-card-name">${name}</div>
<div class="preset-card-content">${preview}</div>
`;
grid.appendChild(card);
}
}
function renderStyles() {
const grid = document.getElementById('styleGrid');
const emptyState = document.getElementById('styleEmptyState');
grid.innerHTML = '';
const sortedNames = getSortedNames(styles);
const filteredNames = sortedNames.filter(name =>
name.toLowerCase().includes(searchTerms.style.toLowerCase())
);
if (filteredNames.length === 0) {
grid.style.display = 'none';
emptyState.style.display = 'block';
return;
}
grid.style.display = 'grid';
emptyState.style.display = '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 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];
document.getElementById('editCharName').textContent = 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 (characterImages[name]) {
previewImg.src = getImageUrl(name);
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
document.getElementById('editModal').classList.add('show');
} else if (type === 'model') {
const data = models[name];
document.getElementById('editModelName').textContent = name;
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 || '';
document.getElementById('modelEditModal').classList.add('show');
} else if (type === 'style') {
const data = styles[name];
document.getElementById('editStyleName').textContent = name;
document.getElementById('editStylePos').value =
data.positive || '';
document.getElementById('editStyleNeg').value =
data.negative || '';
const preview = document.getElementById('styleImagePreview');
const previewImg = document.getElementById('stylePreviewImg');
if (styleImages[name]) {
previewImg.src = getImageUrl(name, 'style');
preview.style.display = 'block';
} else {
preview.style.display = 'none';
}
document.getElementById('styleEditModal').classList.add('show');
}
}
function hideEditModal(type) {
if (type === 'character') {
document.getElementById('editModal').classList.remove('show');
} else if (type === 'model') {
document.getElementById('modelEditModal').classList.remove('show');
} else if (type === 'style') {
document.getElementById('styleEditModal').classList.remove('show');
}
currentEditName = null;
currentEditType = null;
}
async function saveItem(type) {
if (!currentEditName) return;
if (type === 'character') {
await saveCharacter();
} else if (type === 'model') {
models[currentEditName] = {
quality: {
positive: document.getElementById(
'editModelQualityPos').value,
negative: document.getElementById(
'editModelQualityNeg').value
},
embeddings: {
positive: document.getElementById(
'editModelEmbedPos').value,
negative: document.getElementById(
'editModelEmbedNeg').value
}
};
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') {
styles[currentEditName] = {
positive: document.getElementById('editStylePos').value,
negative: document.getElementById('editStyleNeg').value
};
const fileInput = document.getElementById('editStyleImage');
if (fileInput.files.length > 0) {
await processImage(fileInput.files[0], currentEditName, 'style');
}
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');
}
}
}
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');
}
}
}
// 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() {
if (!currentEditName) return;
characters[currentEditName] = {
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
};
const fileInput = document.getElementById('editImage');
if (fileInput.files.length > 0) {
await processImage(fileInput.files[0], currentEditName);
}
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(); // Changed from renderCharacters() to update categories
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`;
setTimeout(() => status.classList.remove('show'), 3000);
}
// Event listeners
document.getElementById('searchInput').addEventListener('input', (e) => {
searchTerms.character = e.target.value;
renderCharacters();
});
document.getElementById('modelSearchInput').addEventListener('input',
(e) => {
searchTerms.model = e.target.value;
renderModels();
}
);
document.getElementById('styleSearchInput').addEventListener('input',
(e) => {
searchTerms.style = e.target.value;
renderStyles();
}
);
document.getElementById('addModal').addEventListener('click', (e) => {
if (e.target.id === 'addModal') hideAddModal();
});
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');
}
);
loadData();