Merge pull request #82 from ComfyAssets/feature/add-prompt
Feature/add prompt
This commit is contained in:
@@ -824,6 +824,14 @@ MIT License - see LICENSE file for details.
|
||||
|
||||
## Changelog
|
||||
|
||||
### v3.0.24 (Manual Prompt Creation)
|
||||
|
||||
- **➕ Add Prompt Button**: New "Add Prompt" button in the admin dashboard Bulk Actions bar
|
||||
- **📝 Direct Prompt Creation**: Create prompts directly from the UI without generating images
|
||||
- **🛡️ Protection from Pruning**: New prompts are automatically protected from orphan cleanup by default
|
||||
- **🏷️ Full Metadata Support**: Set category, tags, rating, and notes when creating prompts
|
||||
- **🔒 Orphan Protection Toggle**: Option to protect or unprotect prompts from maintenance cleanup
|
||||
|
||||
### v3.0.23 (AI AutoTag Release)
|
||||
|
||||
- **🏷️ AI AutoTag**: Automatically tag your entire image collection using JoyCaption vision models
|
||||
|
||||
@@ -3205,15 +3205,19 @@ class PromptManagerAPI:
|
||||
}
|
||||
|
||||
# Prune orphaned prompts (prompts with no linked images)
|
||||
# Note: Prompts with the __protected__ tag are excluded from orphan cleanup
|
||||
# This allows users to add prompts manually without images that won't be deleted
|
||||
if 'prune_orphaned_prompts' in operations:
|
||||
try:
|
||||
with self.db.model.get_connection() as conn:
|
||||
# Find prompts that have no images linked to them
|
||||
# Exclude prompts with the __protected__ tag
|
||||
cursor = conn.execute("""
|
||||
SELECT p.id
|
||||
FROM prompts p
|
||||
LEFT JOIN generated_images gi ON p.id = gi.prompt_id
|
||||
SELECT p.id
|
||||
FROM prompts p
|
||||
LEFT JOIN generated_images gi ON p.id = gi.prompt_id
|
||||
WHERE gi.prompt_id IS NULL
|
||||
AND (p.tags IS NULL OR p.tags NOT LIKE '%"__protected__"%')
|
||||
""")
|
||||
orphaned_prompts = cursor.fetchall()
|
||||
orphaned_count = len(orphaned_prompts)
|
||||
@@ -3231,7 +3235,7 @@ class PromptManagerAPI:
|
||||
results['prune_orphaned_prompts'] = {
|
||||
'success': True,
|
||||
'removed_count': removed_count,
|
||||
'message': f'Removed {removed_count} orphaned prompts (prompts with no linked images)'
|
||||
'message': f'Removed {removed_count} orphaned prompts (prompts with no linked images, excluding protected prompts)'
|
||||
}
|
||||
except Exception as e:
|
||||
results['prune_orphaned_prompts'] = {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "promptmanager"
|
||||
description = "A powerful ComfyUI custom node that extends the standard text encoder with persistent prompt storage, advanced search capabilities, and an automatic image gallery system using SQLite."
|
||||
version = "3.0.23"
|
||||
version = "3.0.24"
|
||||
license = {file = "LICENSE"}
|
||||
dependencies = ["# Core dependencies for PromptManager", "# Note: Most dependencies are already included with ComfyUI", "# Already included with Python standard library:", "# - sqlite3", "# - hashlib", "# - json", "# - datetime", "# - os", "# - typing", "# - threading", "# - uuid", "# Required for gallery functionality:", "watchdog>=2.1.0 # For file system monitoring", "Pillow>=8.0.0 # For image metadata extraction (usually included with ComfyUI)", "# Optional dependencies for enhanced search functionality:", "# fuzzywuzzy[speedup]>=0.18.0 # For fuzzy string matching (optional)", "# sqlalchemy>=1.4.0 # For advanced ORM features (optional)", "# Development dependencies (optional):", "# pytest>=6.0.0 # For running tests", "# black>=22.0.0 # For code formatting", "# flake8>=4.0.0 # For linting", "# mypy>=0.910 # For type checking"]
|
||||
|
||||
|
||||
+294
-2
@@ -204,11 +204,15 @@
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors">
|
||||
🏷️ Add Tags
|
||||
</button>
|
||||
<button id="bulkCategoryBtn" disabled
|
||||
<button id="bulkCategoryBtn" disabled
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white text-sm font-medium rounded-lg transition-colors">
|
||||
📁 Set Category
|
||||
</button>
|
||||
<button id="statsBtn"
|
||||
<button id="addPromptBtn"
|
||||
class="px-4 py-2 bg-emerald-600 hover:bg-emerald-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
➕ Add Prompt
|
||||
</button>
|
||||
<button id="statsBtn"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
📊 Stats
|
||||
</button>
|
||||
@@ -823,6 +827,101 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Prompt Modal -->
|
||||
<div id="addPromptModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-2xl w-full mx-4 border border-gray-700 max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-gray-100">➕ Add New Prompt</h3>
|
||||
<button onclick="window.admin.hideModal('addPromptModal')" class="p-2 rounded-lg hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Text -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Prompt Text <span class="text-red-400">*</span></label>
|
||||
<textarea id="addPromptText" rows="4"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm resize-none custom-scrollbar"
|
||||
placeholder="Enter your prompt text here..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Category and Rating Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Category</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="addPromptCategory" list="addPromptCategoryList"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm"
|
||||
placeholder="Select or type category...">
|
||||
<datalist id="addPromptCategoryList"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Rating</label>
|
||||
<div class="flex items-center space-x-1" id="addPromptRating">
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="1">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="2">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="3">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="4">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="5">☆</button>
|
||||
<button type="button" class="ml-2 text-xs text-gray-500 hover:text-gray-300" id="clearAddPromptRatingBtn">Clear</button>
|
||||
</div>
|
||||
<input type="hidden" id="addPromptRatingValue" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Tags</label>
|
||||
<div class="relative">
|
||||
<div id="addPromptTagsContainer" class="flex flex-wrap gap-2 p-3 bg-gray-700 border border-gray-600 rounded-lg min-h-[44px]">
|
||||
<input type="text" id="addPromptTagInput"
|
||||
class="flex-1 min-w-[150px] bg-transparent border-none outline-none text-gray-100 text-sm placeholder-gray-500"
|
||||
placeholder="Type to add tags...">
|
||||
</div>
|
||||
<div id="addPromptTagSuggestions" class="absolute left-0 right-0 top-full mt-1 bg-gray-700 border border-gray-600 rounded-lg shadow-lg hidden z-10 max-h-48 overflow-y-auto custom-scrollbar"></div>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-1">Press Enter or comma to add a tag. Click a tag to remove it.</p>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Notes</label>
|
||||
<textarea id="addPromptNotes" rows="2"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm resize-none custom-scrollbar"
|
||||
placeholder="Optional notes about this prompt..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Protection Option -->
|
||||
<div class="mb-6">
|
||||
<label class="flex items-start p-4 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-650 border border-gray-600">
|
||||
<input type="checkbox" id="addPromptProtected" checked class="w-4 h-4 mt-0.5 text-green-600 bg-gray-700 border-gray-600 rounded focus:ring-green-500">
|
||||
<div class="ml-3">
|
||||
<span class="text-gray-100 font-medium">Protect from orphan cleanup</span>
|
||||
<p class="text-gray-400 text-sm mt-1">Keep this prompt even if it has no linked images. Uncheck if you want this prompt to be automatically removed during maintenance when it has no images.</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="window.admin.hideModal('addPromptModal')"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="saveNewPromptBtn"
|
||||
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
➕ Add Prompt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Auto Tag Modal -->
|
||||
<div id="autoTagModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-lg w-full mx-4 border border-gray-700">
|
||||
@@ -1193,6 +1292,7 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
document.getElementById("restoreBtn").addEventListener("click", () => this.showRestoreModal());
|
||||
document.getElementById("scanBtn").addEventListener("click", () => this.showScanModal());
|
||||
document.getElementById("autoTagBtn").addEventListener("click", () => this.showAutoTagModal());
|
||||
document.getElementById("addPromptBtn").addEventListener("click", () => this.showAddPromptModal());
|
||||
document.getElementById("logsBtn").addEventListener("click", () => this.showLogsModal());
|
||||
document.getElementById("metadataBtn").addEventListener("click", () => this.openMetadataViewer());
|
||||
document.getElementById("galleryBtn").addEventListener("click", () => this.openGallery());
|
||||
@@ -1273,6 +1373,10 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
// Tags accordion toggle
|
||||
document.getElementById("reviewTagsToggle").addEventListener("click", () => this.toggleReviewTags());
|
||||
|
||||
// Add Prompt modal
|
||||
document.getElementById("saveNewPromptBtn").addEventListener("click", () => this.saveNewPrompt());
|
||||
this.setupAddPromptModal();
|
||||
|
||||
// Close modals on backdrop click
|
||||
document.querySelectorAll("[id$='Modal']").forEach((modal) => {
|
||||
modal.addEventListener("click", (e) => {
|
||||
@@ -4277,6 +4381,194 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
tagsExpanded: false // Track accordion state
|
||||
};
|
||||
|
||||
// ==================== Add Prompt Modal ====================
|
||||
|
||||
showAddPromptModal() {
|
||||
// Clear form
|
||||
document.getElementById('addPromptText').value = '';
|
||||
document.getElementById('addPromptCategory').value = '';
|
||||
document.getElementById('addPromptRatingValue').value = '';
|
||||
document.getElementById('addPromptNotes').value = '';
|
||||
document.getElementById('addPromptProtected').checked = true;
|
||||
|
||||
// Clear tags
|
||||
this.addPromptTags = [];
|
||||
this.renderAddPromptTags();
|
||||
|
||||
// Reset rating stars
|
||||
this.updateAddPromptRatingStars(0);
|
||||
|
||||
// Populate categories datalist
|
||||
this.populateAddPromptCategories();
|
||||
|
||||
this.showModal('addPromptModal');
|
||||
}
|
||||
|
||||
setupAddPromptModal() {
|
||||
this.addPromptTags = [];
|
||||
|
||||
// Rating stars
|
||||
const ratingContainer = document.getElementById('addPromptRating');
|
||||
const ratingStars = ratingContainer.querySelectorAll('.rating-star');
|
||||
ratingStars.forEach(star => {
|
||||
star.addEventListener('click', () => {
|
||||
const rating = parseInt(star.dataset.rating);
|
||||
document.getElementById('addPromptRatingValue').value = rating;
|
||||
this.updateAddPromptRatingStars(rating);
|
||||
});
|
||||
});
|
||||
|
||||
// Clear rating button
|
||||
document.getElementById('clearAddPromptRatingBtn').addEventListener('click', () => {
|
||||
document.getElementById('addPromptRatingValue').value = '';
|
||||
this.updateAddPromptRatingStars(0);
|
||||
});
|
||||
|
||||
// Tag input
|
||||
const tagInput = document.getElementById('addPromptTagInput');
|
||||
tagInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
const tag = tagInput.value.trim().replace(/,/g, '');
|
||||
if (tag && !this.addPromptTags.includes(tag)) {
|
||||
this.addPromptTags.push(tag);
|
||||
this.renderAddPromptTags();
|
||||
}
|
||||
tagInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Tag suggestions
|
||||
tagInput.addEventListener('input', () => {
|
||||
this.showAddPromptTagSuggestions(tagInput.value);
|
||||
});
|
||||
}
|
||||
|
||||
updateAddPromptRatingStars(rating) {
|
||||
const stars = document.querySelectorAll('#addPromptRating .rating-star');
|
||||
stars.forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.textContent = '★';
|
||||
star.classList.add('text-yellow-400');
|
||||
star.classList.remove('text-gray-500');
|
||||
} else {
|
||||
star.textContent = '☆';
|
||||
star.classList.remove('text-yellow-400');
|
||||
star.classList.add('text-gray-500');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
renderAddPromptTags() {
|
||||
const container = document.getElementById('addPromptTagsContainer');
|
||||
const input = document.getElementById('addPromptTagInput');
|
||||
|
||||
// Remove existing tag chips
|
||||
container.querySelectorAll('.tag-chip').forEach(chip => chip.remove());
|
||||
|
||||
// Add tag chips
|
||||
this.addPromptTags.forEach(tag => {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip inline-flex items-center px-2 py-1 bg-blue-600 text-white text-xs rounded cursor-pointer hover:bg-blue-700';
|
||||
chip.innerHTML = `${tag} <span class="ml-1">×</span>`;
|
||||
chip.addEventListener('click', () => {
|
||||
this.addPromptTags = this.addPromptTags.filter(t => t !== tag);
|
||||
this.renderAddPromptTags();
|
||||
});
|
||||
container.insertBefore(chip, input);
|
||||
});
|
||||
}
|
||||
|
||||
showAddPromptTagSuggestions(query) {
|
||||
const suggestionsContainer = document.getElementById('addPromptTagSuggestions');
|
||||
if (!query) {
|
||||
suggestionsContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
const matchingTags = this.tags.filter(tag =>
|
||||
tag.toLowerCase().includes(query.toLowerCase()) &&
|
||||
!this.addPromptTags.includes(tag)
|
||||
).slice(0, 10);
|
||||
|
||||
if (matchingTags.length === 0) {
|
||||
suggestionsContainer.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
suggestionsContainer.innerHTML = matchingTags.map(tag => `
|
||||
<div class="px-3 py-2 hover:bg-gray-600 cursor-pointer text-sm text-gray-200" data-tag="${tag}">
|
||||
${tag}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
suggestionsContainer.querySelectorAll('[data-tag]').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const tag = el.dataset.tag;
|
||||
if (!this.addPromptTags.includes(tag)) {
|
||||
this.addPromptTags.push(tag);
|
||||
this.renderAddPromptTags();
|
||||
}
|
||||
document.getElementById('addPromptTagInput').value = '';
|
||||
suggestionsContainer.classList.add('hidden');
|
||||
});
|
||||
});
|
||||
|
||||
suggestionsContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
populateAddPromptCategories() {
|
||||
const datalist = document.getElementById('addPromptCategoryList');
|
||||
datalist.innerHTML = this.categories.map(cat => `<option value="${cat}">`).join('');
|
||||
}
|
||||
|
||||
async saveNewPrompt() {
|
||||
const text = document.getElementById('addPromptText').value.trim();
|
||||
const category = document.getElementById('addPromptCategory').value.trim();
|
||||
const rating = document.getElementById('addPromptRatingValue').value;
|
||||
const notes = document.getElementById('addPromptNotes').value.trim();
|
||||
const isProtected = document.getElementById('addPromptProtected').checked;
|
||||
|
||||
if (!text) {
|
||||
this.showNotification('Prompt text is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/prompt_manager/prompts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
category: category || null,
|
||||
rating: rating ? parseInt(rating) : null,
|
||||
tags: this.addPromptTags,
|
||||
notes: notes || null,
|
||||
is_protected: isProtected
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
this.showNotification('Prompt added successfully!', 'success');
|
||||
this.hideModal('addPromptModal');
|
||||
// Refresh the prompts list
|
||||
this.search();
|
||||
// Reload categories and tags in case new ones were added
|
||||
this.loadCategories();
|
||||
this.loadTags();
|
||||
} else {
|
||||
this.showNotification(data.error || 'Failed to add prompt', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding prompt:', error);
|
||||
this.showNotification('Failed to add prompt', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== End Add Prompt Modal ====================
|
||||
|
||||
async showAutoTagModal() {
|
||||
this.showModal("autoTagModal");
|
||||
await this.checkAutoTagModels();
|
||||
|
||||
+391
-2
@@ -191,6 +191,10 @@
|
||||
class="px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
🏷️ Auto Tag
|
||||
</button>
|
||||
<button id="addPromptBtn"
|
||||
class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm font-medium rounded-lg transition-colors">
|
||||
➕ Add Prompt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -780,6 +784,101 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Prompt Modal -->
|
||||
<div id="addPromptModal" class="fixed inset-0 bg-black bg-opacity-50 hidden items-center justify-center z-50">
|
||||
<div class="bg-gray-800 rounded-xl p-6 max-w-2xl w-full mx-4 border border-gray-700 max-h-[90vh] overflow-y-auto custom-scrollbar">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-xl font-semibold text-gray-100">➕ Add New Prompt</h3>
|
||||
<button onclick="window.gallery.hideAddPromptModal()" class="p-2 rounded-lg hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Prompt Text -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Prompt Text <span class="text-red-400">*</span></label>
|
||||
<textarea id="addPromptText" rows="4"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm resize-none custom-scrollbar"
|
||||
placeholder="Enter your prompt text here..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Category and Rating Row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
||||
<!-- Category -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Category</label>
|
||||
<div class="relative">
|
||||
<input type="text" id="addPromptCategory" list="categoryList"
|
||||
class="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm"
|
||||
placeholder="Select or type category...">
|
||||
<datalist id="categoryList"></datalist>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rating -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Rating</label>
|
||||
<div class="flex items-center space-x-1" id="addPromptRating">
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="1">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="2">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="3">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="4">☆</button>
|
||||
<button type="button" class="rating-star text-2xl text-gray-500 hover:text-yellow-400 transition-colors" data-rating="5">☆</button>
|
||||
<button type="button" class="ml-2 text-xs text-gray-500 hover:text-gray-300" id="clearRatingBtn">Clear</button>
|
||||
</div>
|
||||
<input type="hidden" id="addPromptRatingValue" value="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tags -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Tags</label>
|
||||
<div class="relative">
|
||||
<div id="addPromptTagsContainer" class="flex flex-wrap gap-2 p-3 bg-gray-700 border border-gray-600 rounded-lg min-h-[44px]">
|
||||
<input type="text" id="addPromptTagInput"
|
||||
class="flex-1 min-w-[150px] bg-transparent border-none outline-none text-gray-100 text-sm placeholder-gray-500"
|
||||
placeholder="Type to add tags...">
|
||||
</div>
|
||||
<div id="tagSuggestions" class="absolute left-0 right-0 top-full mt-1 bg-gray-700 border border-gray-600 rounded-lg shadow-lg hidden z-10 max-h-48 overflow-y-auto custom-scrollbar"></div>
|
||||
</div>
|
||||
<p class="text-gray-500 text-xs mt-1">Press Enter or comma to add a tag. Click a tag to remove it.</p>
|
||||
</div>
|
||||
|
||||
<!-- Notes -->
|
||||
<div class="mb-5">
|
||||
<label class="block text-sm font-medium text-gray-400 mb-2">Notes</label>
|
||||
<textarea id="addPromptNotes" rows="2"
|
||||
class="w-full px-4 py-3 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-green-500 focus:border-green-500 text-gray-100 text-sm resize-none custom-scrollbar"
|
||||
placeholder="Optional notes about this prompt..."></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Protection Option -->
|
||||
<div class="mb-6">
|
||||
<label class="flex items-start p-4 bg-gray-700 rounded-lg cursor-pointer hover:bg-gray-650 border border-gray-600">
|
||||
<input type="checkbox" id="addPromptProtected" checked class="w-4 h-4 mt-0.5 text-green-600 bg-gray-700 border-gray-600 rounded focus:ring-green-500">
|
||||
<div class="ml-3">
|
||||
<span class="text-gray-100 font-medium">Protect from orphan cleanup</span>
|
||||
<p class="text-gray-400 text-sm mt-1">Keep this prompt even if it has no linked images. Uncheck if you want this prompt to be automatically removed during maintenance when it has no images.</p>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button onclick="window.gallery.hideAddPromptModal()"
|
||||
class="px-6 py-2 bg-gray-600 hover:bg-gray-700 text-white font-medium rounded-lg transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button id="saveNewPromptBtn"
|
||||
class="px-6 py-2 bg-green-600 hover:bg-green-700 text-white font-medium rounded-lg transition-colors">
|
||||
➕ Add Prompt
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Tag chip styles for Auto Tag feature */
|
||||
.tag-chip {
|
||||
@@ -837,6 +936,7 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
document.getElementById('listViewBtn').addEventListener('click', () => this.setViewMode('list'));
|
||||
document.getElementById('settingsBtn').addEventListener('click', () => this.showSettings());
|
||||
document.getElementById('autoTagBtn').addEventListener('click', () => this.showAutoTagModal());
|
||||
document.getElementById('addPromptBtn').addEventListener('click', () => this.showAddPromptModal());
|
||||
document.getElementById('retryBtn').addEventListener('click', () => this.loadImages());
|
||||
document.getElementById('prevPageBtn').addEventListener('click', () => this.previousPage());
|
||||
document.getElementById('nextPageBtn').addEventListener('click', () => this.nextPage());
|
||||
@@ -883,8 +983,19 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
document.getElementById('cancelDownloadBtn').addEventListener('click', () => this.cancelDownload());
|
||||
document.getElementById('unloadModelBtn').addEventListener('click', () => this.unloadModel());
|
||||
|
||||
// Add Prompt modal event listeners
|
||||
document.getElementById('saveNewPromptBtn').addEventListener('click', () => this.saveNewPrompt());
|
||||
document.getElementById('addPromptTagInput').addEventListener('input', (e) => this.handleTagInput(e));
|
||||
document.getElementById('addPromptTagInput').addEventListener('keydown', (e) => this.handleTagKeydown(e));
|
||||
document.getElementById('clearRatingBtn').addEventListener('click', () => this.clearRating());
|
||||
|
||||
// Rating stars click handlers
|
||||
document.querySelectorAll('#addPromptRating .rating-star').forEach(star => {
|
||||
star.addEventListener('click', (e) => this.setRating(parseInt(e.target.dataset.rating)));
|
||||
});
|
||||
|
||||
// Close AutoTag modals on outside click
|
||||
['autoTagModal', 'autoTagReviewModal', 'autoTagProgressModal', 'autoTagDownloadModal'].forEach(modalId => {
|
||||
['autoTagModal', 'autoTagReviewModal', 'autoTagProgressModal', 'autoTagDownloadModal', 'addPromptModal'].forEach(modalId => {
|
||||
document.getElementById(modalId).addEventListener('click', (e) => {
|
||||
if (e.target.id === modalId) {
|
||||
this.hideModal(modalId);
|
||||
@@ -1198,7 +1309,7 @@ No sentences, no commentary, no captions. Only tags. Keep length short but descr
|
||||
📋 COPY ALL
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Scrollable Content -->
|
||||
<div id="metadata-content" class="flex-1 overflow-y-auto p-4 space-y-6">
|
||||
<!-- Loading state -->
|
||||
@@ -3563,6 +3674,284 @@ Seed: ${this.currentMetadata.seed || 'Unknown'}`;
|
||||
});
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Add Prompt Feature Methods
|
||||
// ==========================================
|
||||
|
||||
// State for Add Prompt feature
|
||||
addPromptState = {
|
||||
allTags: [],
|
||||
allCategories: [],
|
||||
selectedTags: [],
|
||||
selectedRating: null
|
||||
};
|
||||
|
||||
async showAddPromptModal() {
|
||||
// Reset form
|
||||
this.resetAddPromptForm();
|
||||
|
||||
// Load tags and categories for autocomplete
|
||||
await this.loadTagsAndCategories();
|
||||
|
||||
this.showModal('addPromptModal');
|
||||
document.getElementById('addPromptText').focus();
|
||||
}
|
||||
|
||||
hideAddPromptModal() {
|
||||
this.hideModal('addPromptModal');
|
||||
this.hideSuggestions();
|
||||
}
|
||||
|
||||
resetAddPromptForm() {
|
||||
document.getElementById('addPromptText').value = '';
|
||||
document.getElementById('addPromptCategory').value = '';
|
||||
document.getElementById('addPromptNotes').value = '';
|
||||
document.getElementById('addPromptTagInput').value = '';
|
||||
document.getElementById('addPromptProtected').checked = true;
|
||||
document.getElementById('addPromptRatingValue').value = '';
|
||||
this.addPromptState.selectedTags = [];
|
||||
this.addPromptState.selectedRating = null;
|
||||
this.renderSelectedTags();
|
||||
this.updateRatingStars(0);
|
||||
}
|
||||
|
||||
async loadTagsAndCategories() {
|
||||
try {
|
||||
// Load tags
|
||||
const tagsResponse = await fetch('/prompt_manager/tags');
|
||||
const tagsData = await tagsResponse.json();
|
||||
if (tagsData.success) {
|
||||
// Filter out the __protected__ tag from suggestions
|
||||
this.addPromptState.allTags = tagsData.tags.filter(tag => tag !== '__protected__');
|
||||
}
|
||||
|
||||
// Load categories
|
||||
const categoriesResponse = await fetch('/prompt_manager/categories');
|
||||
const categoriesData = await categoriesResponse.json();
|
||||
if (categoriesData.success) {
|
||||
this.addPromptState.allCategories = categoriesData.categories;
|
||||
this.populateCategoryDatalist();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading tags/categories:', error);
|
||||
}
|
||||
}
|
||||
|
||||
populateCategoryDatalist() {
|
||||
const datalist = document.getElementById('categoryList');
|
||||
datalist.innerHTML = this.addPromptState.allCategories
|
||||
.map(cat => `<option value="${this.escapeHtml(cat)}">`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
handleTagInput(e) {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
|
||||
if (query.length < 1) {
|
||||
this.hideSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter tags that match query and aren't already selected
|
||||
const suggestions = this.addPromptState.allTags
|
||||
.filter(tag =>
|
||||
tag.toLowerCase().includes(query) &&
|
||||
!this.addPromptState.selectedTags.includes(tag)
|
||||
)
|
||||
.slice(0, 10);
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
this.showSuggestions(suggestions);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}
|
||||
|
||||
handleTagKeydown(e) {
|
||||
if (e.key === 'Enter' || e.key === ',') {
|
||||
e.preventDefault();
|
||||
const input = e.target;
|
||||
const value = input.value.replace(',', '').trim();
|
||||
|
||||
if (value && !this.addPromptState.selectedTags.includes(value)) {
|
||||
this.addTag(value);
|
||||
input.value = '';
|
||||
this.hideSuggestions();
|
||||
}
|
||||
} else if (e.key === 'Backspace' && e.target.value === '') {
|
||||
// Remove last tag on backspace with empty input
|
||||
if (this.addPromptState.selectedTags.length > 0) {
|
||||
this.removeTag(this.addPromptState.selectedTags.length - 1);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
this.hideSuggestions();
|
||||
} else if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
|
||||
this.navigateSuggestions(e.key === 'ArrowDown' ? 1 : -1);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
showSuggestions(suggestions) {
|
||||
const container = document.getElementById('tagSuggestions');
|
||||
container.innerHTML = suggestions
|
||||
.map((tag, index) => `
|
||||
<div class="px-3 py-2 hover:bg-gray-600 cursor-pointer text-gray-200 text-sm suggestion-item"
|
||||
data-index="${index}"
|
||||
onclick="window.gallery.selectSuggestion('${this.escapeHtml(tag)}')">
|
||||
${this.escapeHtml(tag)}
|
||||
</div>
|
||||
`)
|
||||
.join('');
|
||||
container.classList.remove('hidden');
|
||||
}
|
||||
|
||||
hideSuggestions() {
|
||||
document.getElementById('tagSuggestions').classList.add('hidden');
|
||||
}
|
||||
|
||||
navigateSuggestions(direction) {
|
||||
const container = document.getElementById('tagSuggestions');
|
||||
const items = container.querySelectorAll('.suggestion-item');
|
||||
if (items.length === 0) return;
|
||||
|
||||
const current = container.querySelector('.bg-gray-600');
|
||||
let nextIndex = 0;
|
||||
|
||||
if (current) {
|
||||
current.classList.remove('bg-gray-600');
|
||||
nextIndex = parseInt(current.dataset.index) + direction;
|
||||
if (nextIndex < 0) nextIndex = items.length - 1;
|
||||
if (nextIndex >= items.length) nextIndex = 0;
|
||||
} else if (direction === -1) {
|
||||
nextIndex = items.length - 1;
|
||||
}
|
||||
|
||||
items[nextIndex].classList.add('bg-gray-600');
|
||||
}
|
||||
|
||||
selectSuggestion(tag) {
|
||||
this.addTag(tag);
|
||||
document.getElementById('addPromptTagInput').value = '';
|
||||
this.hideSuggestions();
|
||||
document.getElementById('addPromptTagInput').focus();
|
||||
}
|
||||
|
||||
addTag(tag) {
|
||||
if (!this.addPromptState.selectedTags.includes(tag)) {
|
||||
this.addPromptState.selectedTags.push(tag);
|
||||
this.renderSelectedTags();
|
||||
}
|
||||
}
|
||||
|
||||
removeTag(index) {
|
||||
this.addPromptState.selectedTags.splice(index, 1);
|
||||
this.renderSelectedTags();
|
||||
}
|
||||
|
||||
renderSelectedTags() {
|
||||
const container = document.getElementById('addPromptTagsContainer');
|
||||
const input = document.getElementById('addPromptTagInput');
|
||||
|
||||
// Remove existing tag chips (keep only the input)
|
||||
container.querySelectorAll('.tag-chip').forEach(el => el.remove());
|
||||
|
||||
// Add tag chips before the input
|
||||
this.addPromptState.selectedTags.forEach((tag, index) => {
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.innerHTML = `
|
||||
${this.escapeHtml(tag)}
|
||||
<span class="tag-remove" onclick="window.gallery.removeTag(${index})">×</span>
|
||||
`;
|
||||
container.insertBefore(chip, input);
|
||||
});
|
||||
}
|
||||
|
||||
setRating(rating) {
|
||||
this.addPromptState.selectedRating = rating;
|
||||
document.getElementById('addPromptRatingValue').value = rating;
|
||||
this.updateRatingStars(rating);
|
||||
}
|
||||
|
||||
clearRating() {
|
||||
this.addPromptState.selectedRating = null;
|
||||
document.getElementById('addPromptRatingValue').value = '';
|
||||
this.updateRatingStars(0);
|
||||
}
|
||||
|
||||
updateRatingStars(rating) {
|
||||
const stars = document.querySelectorAll('#addPromptRating .rating-star');
|
||||
stars.forEach((star, index) => {
|
||||
if (index < rating) {
|
||||
star.textContent = '★';
|
||||
star.classList.remove('text-gray-500');
|
||||
star.classList.add('text-yellow-400');
|
||||
} else {
|
||||
star.textContent = '☆';
|
||||
star.classList.remove('text-yellow-400');
|
||||
star.classList.add('text-gray-500');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async saveNewPrompt() {
|
||||
const text = document.getElementById('addPromptText').value.trim();
|
||||
const category = document.getElementById('addPromptCategory').value.trim();
|
||||
const notes = document.getElementById('addPromptNotes').value.trim();
|
||||
const isProtected = document.getElementById('addPromptProtected').checked;
|
||||
const rating = this.addPromptState.selectedRating;
|
||||
|
||||
// Validation
|
||||
if (!text) {
|
||||
this.showNotification('Prompt text is required', 'error');
|
||||
document.getElementById('addPromptText').focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Build tags array - add __protected__ if checkbox is checked
|
||||
let tags = [...this.addPromptState.selectedTags];
|
||||
if (isProtected && !tags.includes('__protected__')) {
|
||||
tags.push('__protected__');
|
||||
}
|
||||
|
||||
// Disable button during save
|
||||
const saveBtn = document.getElementById('saveNewPromptBtn');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/prompt_manager/save', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
text: text,
|
||||
category: category || null,
|
||||
tags: tags,
|
||||
rating: rating,
|
||||
notes: notes || null
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
const message = data.is_duplicate
|
||||
? 'Prompt already exists (updated metadata)'
|
||||
: 'Prompt added successfully!';
|
||||
this.showNotification(message, 'success');
|
||||
this.hideAddPromptModal();
|
||||
} else {
|
||||
this.showNotification(`Failed to save prompt: ${data.error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving prompt:', error);
|
||||
this.showNotification('Failed to save prompt', 'error');
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '➕ Add Prompt';
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// Auto Tag Feature Methods
|
||||
// ==========================================
|
||||
|
||||
Reference in New Issue
Block a user