Skip to content

Commit ee4451f

Browse files
JasonHokuclaude
andcommitted
Add custom lora trigger word editor with modal UI and backend endpoints
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a71b9ed commit ee4451f

2 files changed

Lines changed: 236 additions & 1 deletion

File tree

config_builder_node.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1016,4 +1016,60 @@ async def refresh_models_endpoint(request):
10161016
traceback.print_exc()
10171017
return web.json_response({
10181018
"error": str(e)
1019-
}, status=500)
1019+
}, status=500)
1020+
1021+
1022+
@server.PromptServer.instance.routes.get("/configbuilder/get_lora_triggers")
1023+
async def get_lora_triggers_endpoint(request):
1024+
"""Get trigger words for a specific LoRA from loras_tags.json"""
1025+
try:
1026+
lora_name = request.query.get("lora_name", "")
1027+
if not lora_name:
1028+
return web.json_response({"error": "Missing lora_name"}, status=400)
1029+
1030+
json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json")
1031+
triggers = []
1032+
if os.path.exists(json_tags_path):
1033+
from .lora_utils import load_json_from_file
1034+
lora_tags = load_json_from_file(json_tags_path) or {}
1035+
# Try exact match, normalized, and backslash variants
1036+
normalized = lora_name.replace("\\", "/")
1037+
backslash = lora_name.replace("/", "\\")
1038+
triggers = lora_tags.get(lora_name, lora_tags.get(normalized, lora_tags.get(backslash, [])))
1039+
1040+
return web.json_response({"lora_name": lora_name, "triggers": triggers})
1041+
except Exception as e:
1042+
return web.json_response({"error": str(e)}, status=500)
1043+
1044+
1045+
@server.PromptServer.instance.routes.post("/configbuilder/save_lora_triggers")
1046+
async def save_lora_triggers_endpoint(request):
1047+
"""Save edited trigger words for a LoRA to loras_tags.json"""
1048+
try:
1049+
data = await request.json()
1050+
lora_name = data.get("lora_name", "")
1051+
triggers = data.get("triggers", [])
1052+
1053+
if not lora_name:
1054+
return web.json_response({"error": "Missing lora_name"}, status=400)
1055+
1056+
json_tags_path = os.path.join(folder_paths.get_output_directory(), "benchmarks/loras_tags.json")
1057+
from .lora_utils import load_json_from_file, save_dict_to_json
1058+
lora_tags = {}
1059+
if os.path.exists(json_tags_path):
1060+
lora_tags = load_json_from_file(json_tags_path) or {}
1061+
1062+
# Normalize the name for consistent storage
1063+
normalized = lora_name.replace("\\", "/")
1064+
lora_tags[normalized] = triggers
1065+
1066+
save_dict_to_json(lora_tags, json_tags_path)
1067+
1068+
# Clear the trigger word LRU cache so changes take effect immediately
1069+
from .trigger_words import clear_trigger_caches
1070+
clear_trigger_caches()
1071+
1072+
print(f"[ConfigBuilder] ✏️ Saved {len(triggers)} trigger words for: {normalized}")
1073+
return web.json_response({"status": "saved", "lora_name": normalized, "triggers": triggers})
1074+
except Exception as e:
1075+
return web.json_response({"error": str(e)}, status=500)

web/conf_builder/conf-builder-config-management.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,14 @@ export function createLoraElement(node, loraStr, arrayIdx, loraIdx, availableLor
10651065
metadataBtn.onclick = async () => await showLoraMetadataModal(node, arrayIdx, parsed.name);
10661066
moreOptionsContent.appendChild(metadataBtn);
10671067

1068+
// 4. Edit Trigger Words Button
1069+
const editTriggersBtn = document.createElement("button");
1070+
editTriggersBtn.className = "cb-button";
1071+
editTriggersBtn.style.cssText = `width: 100%; background: linear-gradient(135deg, #336633, #446644); border-left: 4px solid #66cc66; margin-top: 4px;`;
1072+
editTriggersBtn.textContent = "✏️ Edit Trigger Words";
1073+
editTriggersBtn.onclick = async () => await showEditTriggersModal(node, arrayIdx, parsed.name);
1074+
moreOptionsContent.appendChild(editTriggersBtn);
1075+
10681076
moreOptionsSection.appendChild(moreOptionsContent);
10691077
contentDiv.appendChild(moreOptionsSection);
10701078
}
@@ -1590,6 +1598,177 @@ async function showModelMetadataModal(node, arrayIdx, modelName, modelType, forc
15901598
modal.appendChild(closeBtn);
15911599
}
15921600

1601+
// ============================================================
1602+
// Trigger Word Editor Modal
1603+
// ============================================================
1604+
async function showEditTriggersModal(node, arrayIdx, loraName) {
1605+
// Build modal overlay (same pattern as metadata modals)
1606+
const overlay = document.createElement("div");
1607+
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;";
1608+
1609+
const modal = document.createElement("div");
1610+
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;";
1611+
1612+
const closeModal = () => { if (document.body.contains(overlay)) document.body.removeChild(overlay); };
1613+
overlay.onclick = (e) => { if (e.target === overlay) closeModal(); };
1614+
document.addEventListener("keydown", function escHandler(e) {
1615+
if (e.key === "Escape") { closeModal(); document.removeEventListener("keydown", escHandler); }
1616+
});
1617+
1618+
// Close X button
1619+
const closeX = document.createElement("button");
1620+
closeX.textContent = "✕";
1621+
closeX.style.cssText = "position: absolute; top: 10px; right: 15px; background: none; border: none; color: #ff4444; font-size: 20px; cursor: pointer;";
1622+
closeX.onclick = closeModal;
1623+
modal.appendChild(closeX);
1624+
1625+
// Title
1626+
const title = document.createElement("h3");
1627+
title.textContent = "✏️ Edit Trigger Words";
1628+
title.style.cssText = "margin: 0 0 5px 0; color: #66cc66;";
1629+
modal.appendChild(title);
1630+
1631+
const subtitle = document.createElement("div");
1632+
subtitle.style.cssText = "font-size: 12px; color: #888; margin-bottom: 15px;";
1633+
subtitle.textContent = loraName.split('/').pop();
1634+
modal.appendChild(subtitle);
1635+
1636+
const status = document.createElement("div");
1637+
status.textContent = "🔄 Loading trigger words...";
1638+
status.style.cssText = "margin-bottom: 15px; color: #aaa; font-size: 12px;";
1639+
modal.appendChild(status);
1640+
1641+
const chipsContainer = document.createElement("div");
1642+
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;";
1643+
modal.appendChild(chipsContainer);
1644+
1645+
// Input row
1646+
const inputRow = document.createElement("div");
1647+
inputRow.style.cssText = "display: flex; gap: 6px; margin-bottom: 15px;";
1648+
const input = document.createElement("input");
1649+
input.className = "cb-input";
1650+
input.type = "text";
1651+
input.placeholder = "Add new trigger word...";
1652+
input.style.cssText = "flex: 1; padding: 6px 10px; font-size: 12px;";
1653+
const addWordBtn = document.createElement("button");
1654+
addWordBtn.className = "cb-button primary";
1655+
addWordBtn.textContent = "+ Add";
1656+
addWordBtn.style.cssText = "padding: 6px 12px; font-size: 12px;";
1657+
inputRow.appendChild(input);
1658+
inputRow.appendChild(addWordBtn);
1659+
modal.appendChild(inputRow);
1660+
1661+
// Button row
1662+
const btnRow = document.createElement("div");
1663+
btnRow.style.cssText = "display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px;";
1664+
1665+
const saveBtn = document.createElement("button");
1666+
saveBtn.className = "cb-button";
1667+
saveBtn.style.cssText = "background: #336633; border: 1px solid #66cc66; color: #88ff88; padding: 8px 20px;";
1668+
saveBtn.textContent = "💾 Save";
1669+
1670+
const cancelBtn = document.createElement("button");
1671+
cancelBtn.className = "cb-button";
1672+
cancelBtn.style.cssText = "background: #333; color: #aaa; padding: 8px 20px;";
1673+
cancelBtn.textContent = "Cancel";
1674+
cancelBtn.onclick = closeModal;
1675+
1676+
btnRow.appendChild(cancelBtn);
1677+
btnRow.appendChild(saveBtn);
1678+
modal.appendChild(btnRow);
1679+
1680+
overlay.appendChild(modal);
1681+
document.body.appendChild(overlay);
1682+
1683+
// State
1684+
let currentTriggers = [];
1685+
1686+
function renderTriggerChips() {
1687+
chipsContainer.innerHTML = "";
1688+
if (currentTriggers.length === 0) {
1689+
const empty = document.createElement("span");
1690+
empty.style.cssText = "color: #666; font-size: 11px; font-style: italic;";
1691+
empty.textContent = "No trigger words";
1692+
chipsContainer.appendChild(empty);
1693+
return;
1694+
}
1695+
currentTriggers.forEach((trigger, idx) => {
1696+
const chip = document.createElement("span");
1697+
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;";
1698+
chip.textContent = trigger;
1699+
const removeBtn = document.createElement("span");
1700+
removeBtn.textContent = "×";
1701+
removeBtn.style.cssText = "cursor: pointer; color: #ff6666; font-weight: bold; margin-left: 2px;";
1702+
removeBtn.onclick = () => {
1703+
currentTriggers.splice(idx, 1);
1704+
renderTriggerChips();
1705+
};
1706+
chip.appendChild(removeBtn);
1707+
chipsContainer.appendChild(chip);
1708+
});
1709+
}
1710+
1711+
function addTriggerWord() {
1712+
const word = input.value.trim();
1713+
if (word && !currentTriggers.includes(word)) {
1714+
currentTriggers.push(word);
1715+
renderTriggerChips();
1716+
input.value = "";
1717+
}
1718+
input.focus();
1719+
}
1720+
1721+
addWordBtn.onclick = addTriggerWord;
1722+
input.onkeydown = (e) => { if (e.key === "Enter") { e.preventDefault(); addTriggerWord(); } };
1723+
1724+
saveBtn.onclick = async () => {
1725+
saveBtn.disabled = true;
1726+
saveBtn.textContent = "💾 Saving...";
1727+
try {
1728+
const resp = await fetch("/configbuilder/save_lora_triggers", {
1729+
method: "POST",
1730+
headers: { "Content-Type": "application/json" },
1731+
body: JSON.stringify({ lora_name: loraName, triggers: currentTriggers })
1732+
});
1733+
if (resp.ok) {
1734+
status.textContent = "✅ Trigger words saved!";
1735+
status.style.color = "#66ff66";
1736+
setTimeout(closeModal, 800);
1737+
} else {
1738+
const err = await resp.json();
1739+
status.textContent = "❌ Error: " + (err.error || "Save failed");
1740+
status.style.color = "#ff6666";
1741+
saveBtn.disabled = false;
1742+
saveBtn.textContent = "💾 Save";
1743+
}
1744+
} catch (e) {
1745+
status.textContent = "❌ Error: " + e.message;
1746+
status.style.color = "#ff6666";
1747+
saveBtn.disabled = false;
1748+
saveBtn.textContent = "💾 Save";
1749+
}
1750+
};
1751+
1752+
// Fetch current triggers
1753+
try {
1754+
const resp = await fetch(`/configbuilder/get_lora_triggers?lora_name=${encodeURIComponent(loraName)}`);
1755+
if (resp.ok) {
1756+
const data = await resp.json();
1757+
currentTriggers = data.triggers || [];
1758+
status.textContent = `Loaded ${currentTriggers.length} trigger word(s)`;
1759+
status.style.color = "#88ff88";
1760+
} else {
1761+
status.textContent = "⚠️ No triggers found (you can add new ones)";
1762+
status.style.color = "#ffaa00";
1763+
}
1764+
} catch (e) {
1765+
status.textContent = "⚠️ Could not load triggers: " + e.message;
1766+
status.style.color = "#ffaa00";
1767+
}
1768+
renderTriggerChips();
1769+
input.focus();
1770+
}
1771+
15931772

15941773
// --- RENDER MODELS AND LORAS SECTIONS ---
15951774

0 commit comments

Comments
 (0)