Add custom lora trigger word editor with modal UI and backend endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
a71b9ed315
commit
ee4451f3a2
+57
-1
@@ -1016,4 +1016,60 @@ async def refresh_models_endpoint(request):
|
||||
traceback.print_exc()
|
||||
return web.json_response({
|
||||
"error": str(e)
|
||||
}, status=500)
|
||||
}, status=500)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/configbuilder/get_lora_triggers")
|
||||
async def get_lora_triggers_endpoint(request):
|
||||
"""Get trigger words for a specific LoRA from loras_tags.json"""
|
||||
try:
|
||||
lora_name = request.query.get("lora_name", "")
|
||||
if not lora_name:
|
||||
return web.json_response({"error": "Missing lora_name"}, status=400)
|
||||
|
||||
json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json")
|
||||
triggers = []
|
||||
if os.path.exists(json_tags_path):
|
||||
from .lora_utils import load_json_from_file
|
||||
lora_tags = load_json_from_file(json_tags_path) or {}
|
||||
# Try exact match, normalized, and backslash variants
|
||||
normalized = lora_name.replace("\\", "/")
|
||||
backslash = lora_name.replace("/", "\\")
|
||||
triggers = lora_tags.get(lora_name, lora_tags.get(normalized, lora_tags.get(backslash, [])))
|
||||
|
||||
return web.json_response({"lora_name": lora_name, "triggers": triggers})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/configbuilder/save_lora_triggers")
|
||||
async def save_lora_triggers_endpoint(request):
|
||||
"""Save edited trigger words for a LoRA to loras_tags.json"""
|
||||
try:
|
||||
data = await request.json()
|
||||
lora_name = data.get("lora_name", "")
|
||||
triggers = data.get("triggers", [])
|
||||
|
||||
if not lora_name:
|
||||
return web.json_response({"error": "Missing lora_name"}, status=400)
|
||||
|
||||
json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json")
|
||||
from .lora_utils import load_json_from_file, save_dict_to_json
|
||||
lora_tags = {}
|
||||
if os.path.exists(json_tags_path):
|
||||
lora_tags = load_json_from_file(json_tags_path) or {}
|
||||
|
||||
# Normalize the name for consistent storage
|
||||
normalized = lora_name.replace("\\", "/")
|
||||
lora_tags[normalized] = triggers
|
||||
|
||||
save_dict_to_json(lora_tags, json_tags_path)
|
||||
|
||||
# Clear the trigger word LRU cache so changes take effect immediately
|
||||
from .trigger_words import clear_trigger_caches
|
||||
clear_trigger_caches()
|
||||
|
||||
print(f"[ConfigBuilder] ✏️ Saved {len(triggers)} trigger words for: {normalized}")
|
||||
return web.json_response({"status": "saved", "lora_name": normalized, "triggers": triggers})
|
||||
except Exception as e:
|
||||
return web.json_response({"error": str(e)}, status=500)
|
||||
@@ -1065,6 +1065,14 @@ export function createLoraElement(node, loraStr, arrayIdx, loraIdx, availableLor
|
||||
metadataBtn.onclick = async () => await showLoraMetadataModal(node, arrayIdx, parsed.name);
|
||||
moreOptionsContent.appendChild(metadataBtn);
|
||||
|
||||
// 4. Edit Trigger Words Button
|
||||
const editTriggersBtn = document.createElement("button");
|
||||
editTriggersBtn.className = "cb-button";
|
||||
editTriggersBtn.style.cssText = `width: 100%; background: linear-gradient(135deg, #336633, #446644); border-left: 4px solid #66cc66; margin-top: 4px;`;
|
||||
editTriggersBtn.textContent = "✏️ Edit Trigger Words";
|
||||
editTriggersBtn.onclick = async () => await showEditTriggersModal(node, arrayIdx, parsed.name);
|
||||
moreOptionsContent.appendChild(editTriggersBtn);
|
||||
|
||||
moreOptionsSection.appendChild(moreOptionsContent);
|
||||
contentDiv.appendChild(moreOptionsSection);
|
||||
}
|
||||
@@ -1590,6 +1598,177 @@ async function showModelMetadataModal(node, arrayIdx, modelName, modelType, forc
|
||||
modal.appendChild(closeBtn);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Trigger Word Editor Modal
|
||||
// ============================================================
|
||||
async function showEditTriggersModal(node, arrayIdx, loraName) {
|
||||
// Build modal overlay (same pattern as metadata modals)
|
||||
const overlay = document.createElement("div");
|
||||
overlay.style.cssText = "position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.85); z-index: 10000; display: flex; align-items: center; justify-content: center;";
|
||||
|
||||
const modal = document.createElement("div");
|
||||
modal.style.cssText = "background: #1a1a1a; border: 2px solid #66cc66; border-radius: 12px; padding: 25px; max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto; position: relative;";
|
||||
|
||||
const closeModal = () => { if (document.body.contains(overlay)) document.body.removeChild(overlay); };
|
||||
overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };
|
||||
document.addEventListener("keydown", function escHandler(e) {
|
||||
if (e.key === "Escape") { closeModal(); document.removeEventListener("keydown", escHandler); }
|
||||
});
|
||||
|
||||
// Close X button
|
||||
const closeX = document.createElement("button");
|
||||
closeX.textContent = "✕";
|
||||
closeX.style.cssText = "position: absolute; top: 10px; right: 15px; background: none; border: none; color: #ff4444; font-size: 20px; cursor: pointer;";
|
||||
closeX.onclick = closeModal;
|
||||
modal.appendChild(closeX);
|
||||
|
||||
// Title
|
||||
const title = document.createElement("h3");
|
||||
title.textContent = "✏️ Edit Trigger Words";
|
||||
title.style.cssText = "margin: 0 0 5px 0; color: #66cc66;";
|
||||
modal.appendChild(title);
|
||||
|
||||
const subtitle = document.createElement("div");
|
||||
subtitle.style.cssText = "font-size: 12px; color: #888; margin-bottom: 15px;";
|
||||
subtitle.textContent = loraName.split('/').pop();
|
||||
modal.appendChild(subtitle);
|
||||
|
||||
const status = document.createElement("div");
|
||||
status.textContent = "🔄 Loading trigger words...";
|
||||
status.style.cssText = "margin-bottom: 15px; color: #aaa; font-size: 12px;";
|
||||
modal.appendChild(status);
|
||||
|
||||
const chipsContainer = document.createElement("div");
|
||||
chipsContainer.style.cssText = "display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 15px; min-height: 30px; padding: 8px; background: #111; border: 1px solid #333; border-radius: 6px;";
|
||||
modal.appendChild(chipsContainer);
|
||||
|
||||
// Input row
|
||||
const inputRow = document.createElement("div");
|
||||
inputRow.style.cssText = "display: flex; gap: 6px; margin-bottom: 15px;";
|
||||
const input = document.createElement("input");
|
||||
input.className = "cb-input";
|
||||
input.type = "text";
|
||||
input.placeholder = "Add new trigger word...";
|
||||
input.style.cssText = "flex: 1; padding: 6px 10px; font-size: 12px;";
|
||||
const addWordBtn = document.createElement("button");
|
||||
addWordBtn.className = "cb-button primary";
|
||||
addWordBtn.textContent = "+ Add";
|
||||
addWordBtn.style.cssText = "padding: 6px 12px; font-size: 12px;";
|
||||
inputRow.appendChild(input);
|
||||
inputRow.appendChild(addWordBtn);
|
||||
modal.appendChild(inputRow);
|
||||
|
||||
// Button row
|
||||
const btnRow = document.createElement("div");
|
||||
btnRow.style.cssText = "display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px;";
|
||||
|
||||
const saveBtn = document.createElement("button");
|
||||
saveBtn.className = "cb-button";
|
||||
saveBtn.style.cssText = "background: #336633; border: 1px solid #66cc66; color: #88ff88; padding: 8px 20px;";
|
||||
saveBtn.textContent = "💾 Save";
|
||||
|
||||
const cancelBtn = document.createElement("button");
|
||||
cancelBtn.className = "cb-button";
|
||||
cancelBtn.style.cssText = "background: #333; color: #aaa; padding: 8px 20px;";
|
||||
cancelBtn.textContent = "Cancel";
|
||||
cancelBtn.onclick = closeModal;
|
||||
|
||||
btnRow.appendChild(cancelBtn);
|
||||
btnRow.appendChild(saveBtn);
|
||||
modal.appendChild(btnRow);
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// State
|
||||
let currentTriggers = [];
|
||||
|
||||
function renderTriggerChips() {
|
||||
chipsContainer.innerHTML = "";
|
||||
if (currentTriggers.length === 0) {
|
||||
const empty = document.createElement("span");
|
||||
empty.style.cssText = "color: #666; font-size: 11px; font-style: italic;";
|
||||
empty.textContent = "No trigger words";
|
||||
chipsContainer.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
currentTriggers.forEach((trigger, idx) => {
|
||||
const chip = document.createElement("span");
|
||||
chip.style.cssText = "background: #224422; border: 1px solid #448844; color: #aaffaa; padding: 3px 8px; border-radius: 12px; font-size: 11px; display: flex; align-items: center; gap: 4px;";
|
||||
chip.textContent = trigger;
|
||||
const removeBtn = document.createElement("span");
|
||||
removeBtn.textContent = "×";
|
||||
removeBtn.style.cssText = "cursor: pointer; color: #ff6666; font-weight: bold; margin-left: 2px;";
|
||||
removeBtn.onclick = () => {
|
||||
currentTriggers.splice(idx, 1);
|
||||
renderTriggerChips();
|
||||
};
|
||||
chip.appendChild(removeBtn);
|
||||
chipsContainer.appendChild(chip);
|
||||
});
|
||||
}
|
||||
|
||||
function addTriggerWord() {
|
||||
const word = input.value.trim();
|
||||
if (word && !currentTriggers.includes(word)) {
|
||||
currentTriggers.push(word);
|
||||
renderTriggerChips();
|
||||
input.value = "";
|
||||
}
|
||||
input.focus();
|
||||
}
|
||||
|
||||
addWordBtn.onclick = addTriggerWord;
|
||||
input.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); addTriggerWord(); } };
|
||||
|
||||
saveBtn.onclick = async () => {
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = "💾 Saving...";
|
||||
try {
|
||||
const resp = await fetch("/configbuilder/save_lora_triggers", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ lora_name: loraName, triggers: currentTriggers })
|
||||
});
|
||||
if (resp.ok) {
|
||||
status.textContent = "✅ Trigger words saved!";
|
||||
status.style.color = "#66ff66";
|
||||
setTimeout(closeModal, 800);
|
||||
} else {
|
||||
const err = await resp.json();
|
||||
status.textContent = "❌ Error: " + (err.error || "Save failed");
|
||||
status.style.color = "#ff6666";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = "💾 Save";
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = "❌ Error: " + e.message;
|
||||
status.style.color = "#ff6666";
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = "💾 Save";
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch current triggers
|
||||
try {
|
||||
const resp = await fetch(`/configbuilder/get_lora_triggers?lora_name=${encodeURIComponent(loraName)}`);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
currentTriggers = data.triggers || [];
|
||||
status.textContent = `Loaded ${currentTriggers.length} trigger word(s)`;
|
||||
status.style.color = "#88ff88";
|
||||
} else {
|
||||
status.textContent = "⚠️ No triggers found (you can add new ones)";
|
||||
status.style.color = "#ffaa00";
|
||||
}
|
||||
} catch (e) {
|
||||
status.textContent = "⚠️ Could not load triggers: " + e.message;
|
||||
status.style.color = "#ffaa00";
|
||||
}
|
||||
renderTriggerChips();
|
||||
input.focus();
|
||||
}
|
||||
|
||||
|
||||
// --- RENDER MODELS AND LORAS SECTIONS ---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user