Add per-LoRA variant trigger file selection in loader settings.
Support optional companion txt files named lora.Variant.txt alongside the default lora.txt, with toggles in the Grid and List LoRA loader settings dialog to merge character/concept tags into the active trigger list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+40
-68
@@ -1,6 +1,16 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { bindLoraContextMenu, fetchLoraTriggers } from "./lora_info.js";
|
||||
import { bindLoraContextMenu, fetchLoraTriggerVariants } from "./lora_info.js";
|
||||
import { bindPreviewImage, replaceWithPreviewPlaceholder } from "./lora_preview_cache.js";
|
||||
import {
|
||||
createEmptyTriggerEntry,
|
||||
entryTriggerStates,
|
||||
entryVariantStates,
|
||||
formatEnabledTriggers,
|
||||
initEntryTriggers,
|
||||
loadEntryTriggerData,
|
||||
LoraEntrySettingsDialog,
|
||||
restoreTriggerEntryFromWidget,
|
||||
} from "./lora_loader_settings.js";
|
||||
|
||||
const NODE_NAME = "GridLoraLoader";
|
||||
const API_BASE = "/crashutils/loras";
|
||||
@@ -530,28 +540,6 @@ function trimNsfwBrowsePath(path) {
|
||||
return parts.slice(0, nsfwIndex).join("/");
|
||||
}
|
||||
|
||||
function initEntryTriggers(entry, triggers) {
|
||||
entry.triggers = [...(triggers || [])];
|
||||
entry.triggerOn = entry.triggerOn || {};
|
||||
for (const word of entry.triggers) {
|
||||
if (entry.triggerOn[word] === undefined) {
|
||||
entry.triggerOn[word] = true;
|
||||
}
|
||||
}
|
||||
for (const word of Object.keys(entry.triggerOn)) {
|
||||
if (!entry.triggers.includes(word)) {
|
||||
delete entry.triggerOn[word];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function entryTriggerStates(entry) {
|
||||
return (entry.triggers || []).map((word) => ({
|
||||
word,
|
||||
on: entry.triggerOn?.[word] !== false,
|
||||
}));
|
||||
}
|
||||
|
||||
function isTagEnabledForEntry(entry, word) {
|
||||
return entry.triggerOn?.[word] !== false;
|
||||
}
|
||||
@@ -1064,7 +1052,8 @@ class GridLoraLoaderUI {
|
||||
if (!item?.path || !el || (item.triggers && item.triggers.length)) {
|
||||
return;
|
||||
}
|
||||
const triggers = await fetchLoraTriggers(item.path);
|
||||
const data = await fetchLoraTriggerVariants(item.path);
|
||||
const triggers = data.default || [];
|
||||
if (!triggers.length) {
|
||||
return;
|
||||
}
|
||||
@@ -1082,25 +1071,22 @@ class GridLoraLoaderUI {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected.set(loraPath, {
|
||||
const entry = createEmptyTriggerEntry({
|
||||
on: true,
|
||||
lora: loraPath,
|
||||
name: loraName,
|
||||
strength: 1.0,
|
||||
triggers: item.triggers || [],
|
||||
triggerOn: {},
|
||||
});
|
||||
initEntryTriggers(this.selected.get(loraPath), item.triggers || []);
|
||||
if (!item.triggers?.length) {
|
||||
fetchLoraTriggers(loraPath).then((triggers) => {
|
||||
const entry = this.selected.get(loraPath);
|
||||
if (!entry || !triggers.length) return;
|
||||
initEntryTriggers(entry, triggers);
|
||||
this.renderSelected();
|
||||
this.renderTags();
|
||||
this.syncWidgets();
|
||||
});
|
||||
}
|
||||
initEntryTriggers(entry, item.triggers || []);
|
||||
entry.defaultTriggers = item.triggers || [];
|
||||
this.selected.set(loraPath, entry);
|
||||
|
||||
loadEntryTriggerData(entry).then(() => {
|
||||
if (!this.selected.has(loraPath)) return;
|
||||
this.renderSelected();
|
||||
this.renderTags();
|
||||
this.syncWidgets();
|
||||
});
|
||||
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
@@ -1143,15 +1129,15 @@ class GridLoraLoaderUI {
|
||||
el.innerHTML = `
|
||||
<img alt="" loading="lazy" />
|
||||
<span class="strength-badge" title="LoRA strength">${formatStrength(entry.strength)}</span>
|
||||
<button class="cog-btn" title="Set strength">⚙</button>
|
||||
<div class="item-label" title="${formatTriggers(entry.triggers) || entry.lora}">${entry.name || entry.lora.split("/").pop()}</div>
|
||||
<button class="cog-btn" title="LoRA settings">⚙</button>
|
||||
<div class="item-label" title="${formatTriggers(formatEnabledTriggers(entry)) || entry.lora}">${entry.name || entry.lora.split("/").pop()}</div>
|
||||
`;
|
||||
|
||||
bindPreviewImage(el.querySelector("img"), entry.lora, replaceWithPreviewPlaceholder);
|
||||
|
||||
el.querySelector(".cog-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.promptStrength(entry, e);
|
||||
this.openSettings(entry);
|
||||
});
|
||||
el.querySelector(".cog-btn").addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
@@ -1276,21 +1262,15 @@ class GridLoraLoaderUI {
|
||||
}
|
||||
}
|
||||
|
||||
promptStrength(entry, event) {
|
||||
app.canvas.prompt(
|
||||
"LoRA Strength",
|
||||
entry.strength,
|
||||
(value) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num)) {
|
||||
entry.strength = num;
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
}
|
||||
openSettings(entry) {
|
||||
new LoraEntrySettingsDialog(entry, {
|
||||
onApply: () => {
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
this.renderTags();
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
},
|
||||
event
|
||||
);
|
||||
}).open();
|
||||
}
|
||||
|
||||
syncWidgets() {
|
||||
@@ -1312,6 +1292,7 @@ class GridLoraLoaderUI {
|
||||
lora: entry.lora,
|
||||
strength: entry.strength,
|
||||
triggerStates: entryTriggerStates(entry),
|
||||
variantStates: entryVariantStates(entry),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -1321,7 +1302,7 @@ class GridLoraLoaderUI {
|
||||
|
||||
async refreshSelectedTriggers() {
|
||||
const tasks = [...this.selected.values()].map(async (entry) => {
|
||||
initEntryTriggers(entry, await fetchLoraTriggers(entry.lora));
|
||||
await loadEntryTriggerData(entry);
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
this.renderSelected();
|
||||
@@ -1334,22 +1315,13 @@ class GridLoraLoaderUI {
|
||||
for (const value of widgetValues || []) {
|
||||
if (value && typeof value === "object" && value.lora) {
|
||||
const name = value.lora.split("/").pop().replace(/\.[^.]+$/, "");
|
||||
const entry = {
|
||||
const entry = createEmptyTriggerEntry({
|
||||
on: value.on !== false,
|
||||
lora: value.lora,
|
||||
name,
|
||||
strength: value.strength ?? 1.0,
|
||||
triggers: [],
|
||||
triggerOn: {},
|
||||
};
|
||||
if (Array.isArray(value.triggerStates)) {
|
||||
entry.triggers = value.triggerStates.map((item) => item.word).filter(Boolean);
|
||||
entry.triggerOn = Object.fromEntries(
|
||||
value.triggerStates
|
||||
.filter((item) => item?.word)
|
||||
.map((item) => [item.word, item.on !== false])
|
||||
);
|
||||
}
|
||||
});
|
||||
restoreTriggerEntryFromWidget(entry, value);
|
||||
this.selected.set(value.lora, entry);
|
||||
}
|
||||
}
|
||||
|
||||
+40
-32
@@ -1,6 +1,16 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { bindLoraContextMenu, fetchLoraTriggers } from "./lora_info.js";
|
||||
import { bindLoraContextMenu, fetchLoraTriggerVariants } from "./lora_info.js";
|
||||
import { bindPreviewImage, replaceWithPreviewPlaceholder } from "./lora_preview_cache.js";
|
||||
import {
|
||||
createEmptyTriggerEntry,
|
||||
entryTriggerStates,
|
||||
entryVariantStates,
|
||||
formatEnabledTriggers,
|
||||
initEntryTriggers,
|
||||
loadEntryTriggerData,
|
||||
LoraEntrySettingsDialog,
|
||||
restoreTriggerEntryFromWidget,
|
||||
} from "./lora_loader_settings.js";
|
||||
|
||||
const NODE_NAME = "ListLoraLoader";
|
||||
const API_BASE = "/crashutils/loras";
|
||||
@@ -787,7 +797,8 @@ class ListLoraLoaderUI {
|
||||
if (!item?.path || !tagsEl || (item.triggers && item.triggers.length)) {
|
||||
return;
|
||||
}
|
||||
const triggers = await fetchLoraTriggers(item.path);
|
||||
const data = await fetchLoraTriggerVariants(item.path);
|
||||
const triggers = data.default || [];
|
||||
if (!triggers.length) {
|
||||
return;
|
||||
}
|
||||
@@ -804,21 +815,21 @@ class ListLoraLoaderUI {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selected.set(loraPath, {
|
||||
const entry = createEmptyTriggerEntry({
|
||||
on: true,
|
||||
lora: loraPath,
|
||||
name: item.name,
|
||||
strength: 1.0,
|
||||
triggers: item.triggers || [],
|
||||
});
|
||||
if (!item.triggers?.length) {
|
||||
fetchLoraTriggers(loraPath).then((triggers) => {
|
||||
const entry = this.selected.get(loraPath);
|
||||
if (!entry || !triggers.length) return;
|
||||
entry.triggers = triggers;
|
||||
this.renderSelected();
|
||||
});
|
||||
}
|
||||
initEntryTriggers(entry, item.triggers || []);
|
||||
entry.defaultTriggers = item.triggers || [];
|
||||
this.selected.set(loraPath, entry);
|
||||
|
||||
loadEntryTriggerData(entry).then(() => {
|
||||
if (!this.selected.has(loraPath)) return;
|
||||
this.renderSelected();
|
||||
this.syncWidgets();
|
||||
});
|
||||
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
@@ -856,7 +867,7 @@ class ListLoraLoaderUI {
|
||||
el.classList.add("disabled");
|
||||
}
|
||||
|
||||
const tags = formatTriggers(entry.triggers);
|
||||
const tags = formatTriggers(formatEnabledTriggers(entry));
|
||||
const tagsHtml = tags
|
||||
? `<div class="row-tags" title="${escapeHtml(tags)}">${escapeHtml(tags)}</div>`
|
||||
: `<div class="row-tags empty"></div>`;
|
||||
@@ -870,12 +881,12 @@ class ListLoraLoaderUI {
|
||||
<div class="row-name" title="${escapeHtml(entry.lora)}">${escapeHtml(entry.name || entry.lora.split("/").pop())}</div>
|
||||
${tagsHtml}
|
||||
</div>
|
||||
<button class="cog-btn" title="Set strength">⚙</button>
|
||||
<button class="cog-btn" title="LoRA settings">⚙</button>
|
||||
`;
|
||||
|
||||
el.querySelector(".cog-btn").addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.promptStrength(entry, e);
|
||||
this.openSettings(entry);
|
||||
});
|
||||
el.querySelector(".cog-btn").addEventListener("pointerdown", (e) => e.stopPropagation());
|
||||
|
||||
@@ -925,21 +936,14 @@ class ListLoraLoaderUI {
|
||||
return el;
|
||||
}
|
||||
|
||||
promptStrength(entry, event) {
|
||||
app.canvas.prompt(
|
||||
"LoRA Strength",
|
||||
entry.strength,
|
||||
(value) => {
|
||||
const num = Number(value);
|
||||
if (!Number.isNaN(num)) {
|
||||
entry.strength = num;
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
}
|
||||
openSettings(entry) {
|
||||
new LoraEntrySettingsDialog(entry, {
|
||||
onApply: () => {
|
||||
this.syncWidgets();
|
||||
this.renderSelected();
|
||||
this.node.setDirtyCanvas(true, true);
|
||||
},
|
||||
event
|
||||
);
|
||||
}).open();
|
||||
}
|
||||
|
||||
syncWidgets() {
|
||||
@@ -958,6 +962,8 @@ class ListLoraLoaderUI {
|
||||
on: entry.on,
|
||||
lora: entry.lora,
|
||||
strength: entry.strength,
|
||||
triggerStates: entryTriggerStates(entry),
|
||||
variantStates: entryVariantStates(entry),
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -967,10 +973,11 @@ class ListLoraLoaderUI {
|
||||
|
||||
async refreshSelectedTriggers() {
|
||||
const tasks = [...this.selected.values()].map(async (entry) => {
|
||||
entry.triggers = await fetchLoraTriggers(entry.lora);
|
||||
await loadEntryTriggerData(entry);
|
||||
});
|
||||
await Promise.all(tasks);
|
||||
this.renderSelected();
|
||||
this.syncWidgets();
|
||||
}
|
||||
|
||||
loadFromWidgetValues(widgetValues, properties) {
|
||||
@@ -978,13 +985,14 @@ class ListLoraLoaderUI {
|
||||
for (const value of widgetValues || []) {
|
||||
if (value && typeof value === "object" && value.lora) {
|
||||
const name = value.lora.split("/").pop().replace(/\.[^.]+$/, "");
|
||||
this.selected.set(value.lora, {
|
||||
const entry = createEmptyTriggerEntry({
|
||||
on: value.on !== false,
|
||||
lora: value.lora,
|
||||
name,
|
||||
strength: value.strength ?? 1.0,
|
||||
triggers: [],
|
||||
});
|
||||
restoreTriggerEntryFromWidget(entry, value);
|
||||
this.selected.set(value.lora, entry);
|
||||
}
|
||||
}
|
||||
this.loadFromProperties(properties ?? this.node.properties);
|
||||
|
||||
@@ -320,6 +320,20 @@ export async function fetchLoraTriggers(loraPath) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLoraTriggerVariants(loraPath) {
|
||||
if (!loraPath) return { default: [], variants: [] };
|
||||
try {
|
||||
const res = await fetch(`${INFO_API}/trigger-variants?path=${encodeURIComponent(loraPath)}`);
|
||||
const data = await res.json();
|
||||
return {
|
||||
default: data.default || [],
|
||||
variants: data.variants || [],
|
||||
};
|
||||
} catch {
|
||||
return { default: [], variants: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export function showLoraInfoDialog(loraPath) {
|
||||
if (!loraPath) return;
|
||||
new LoraInfoDialog(loraPath).open();
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
import { fetchLoraTriggerVariants } from "./lora_info.js";
|
||||
|
||||
let stylesInjected = false;
|
||||
|
||||
const SETTINGS_STYLES = `
|
||||
.crash-lora-settings-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.55);
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.crash-lora-settings-dialog {
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #666;
|
||||
border-radius: 8px;
|
||||
width: min(480px, 96vw);
|
||||
max-height: 86vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: #ddd;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 12px 40px rgba(0,0,0,0.45);
|
||||
}
|
||||
.crash-lora-settings-dialog * { box-sizing: border-box; }
|
||||
.crash-lora-settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid #555;
|
||||
background: #333;
|
||||
}
|
||||
.crash-lora-settings-header h2 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.crash-lora-settings-close {
|
||||
border: none;
|
||||
background: #444;
|
||||
color: #ddd;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.crash-lora-settings-close:hover { background: #555; color: #fff; }
|
||||
.crash-lora-settings-body {
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
.crash-lora-settings-field label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #ccc;
|
||||
}
|
||||
.crash-lora-settings-field input[type="number"] {
|
||||
width: 80px;
|
||||
padding: 4px 6px;
|
||||
border: 1px solid #555;
|
||||
border-radius: 4px;
|
||||
background: #1a1a1a;
|
||||
color: #eee;
|
||||
}
|
||||
.crash-lora-settings-section h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #aaa;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.crash-lora-settings-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
.crash-lora-settings-chip {
|
||||
border: 1px solid #555;
|
||||
border-radius: 999px;
|
||||
background: #1f1f1f;
|
||||
color: #aaa;
|
||||
font-size: 10px;
|
||||
line-height: 1.2;
|
||||
padding: 3px 8px;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
transition: border-color 0.15s, background 0.15s, color 0.15s, opacity 0.15s;
|
||||
}
|
||||
.crash-lora-settings-chip.on {
|
||||
border-color: #6c6;
|
||||
background: #243024;
|
||||
color: #cfc;
|
||||
}
|
||||
.crash-lora-settings-chip.off {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.crash-lora-settings-chip:hover {
|
||||
border-color: #8af;
|
||||
}
|
||||
.crash-lora-settings-empty {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
}
|
||||
.crash-lora-settings-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-top: 1px solid #555;
|
||||
background: #333;
|
||||
}
|
||||
.crash-lora-settings-btn {
|
||||
border: 1px solid #666;
|
||||
background: #3a3a3a;
|
||||
color: #eee;
|
||||
border-radius: 4px;
|
||||
padding: 5px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
.crash-lora-settings-btn.primary {
|
||||
border-color: #6c6;
|
||||
background: #243024;
|
||||
}
|
||||
.crash-lora-settings-btn:hover {
|
||||
background: #4a4a4a;
|
||||
border-color: #8af;
|
||||
}
|
||||
`;
|
||||
|
||||
function injectSettingsStyles() {
|
||||
if (stylesInjected) return;
|
||||
stylesInjected = true;
|
||||
const style = document.createElement("style");
|
||||
style.textContent = SETTINGS_STYLES;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
export function initEntryTriggers(entry, triggers) {
|
||||
entry.triggers = [...(triggers || [])];
|
||||
entry.triggerOn = entry.triggerOn || {};
|
||||
for (const word of entry.triggers) {
|
||||
if (entry.triggerOn[word] === undefined) {
|
||||
entry.triggerOn[word] = true;
|
||||
}
|
||||
}
|
||||
for (const word of Object.keys(entry.triggerOn)) {
|
||||
if (!entry.triggers.includes(word)) {
|
||||
delete entry.triggerOn[word];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function recomputeEntryTriggers(entry) {
|
||||
const merged = [];
|
||||
const seen = new Set();
|
||||
const addTerms = (list) => {
|
||||
for (const word of list || []) {
|
||||
const key = word.toLowerCase();
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
merged.push(word);
|
||||
}
|
||||
};
|
||||
|
||||
addTerms(entry.defaultTriggers);
|
||||
for (const variant of entry.variantFiles || []) {
|
||||
if (entry.variantOn?.[variant.id] === true) {
|
||||
addTerms(variant.triggers);
|
||||
}
|
||||
}
|
||||
initEntryTriggers(entry, merged);
|
||||
}
|
||||
|
||||
export function entryTriggerStates(entry) {
|
||||
return (entry.triggers || []).map((word) => ({
|
||||
word,
|
||||
on: entry.triggerOn?.[word] !== false,
|
||||
}));
|
||||
}
|
||||
|
||||
export function entryVariantStates(entry) {
|
||||
return (entry.variantFiles || []).map((variant) => ({
|
||||
id: variant.id,
|
||||
on: entry.variantOn?.[variant.id] === true,
|
||||
}));
|
||||
}
|
||||
|
||||
export function formatEnabledTriggers(entry) {
|
||||
return (entry.triggers || []).filter((word) => entry.triggerOn?.[word] !== false);
|
||||
}
|
||||
|
||||
export async function loadEntryTriggerData(entry) {
|
||||
const data = await fetchLoraTriggerVariants(entry.lora);
|
||||
entry.defaultTriggers = data.default || [];
|
||||
entry.variantFiles = data.variants || [];
|
||||
entry.variantOn = entry.variantOn || {};
|
||||
recomputeEntryTriggers(entry);
|
||||
}
|
||||
|
||||
export function createEmptyTriggerEntry(base = {}) {
|
||||
return {
|
||||
defaultTriggers: [],
|
||||
variantFiles: [],
|
||||
variantOn: {},
|
||||
triggers: [],
|
||||
triggerOn: {},
|
||||
...base,
|
||||
};
|
||||
}
|
||||
|
||||
export function restoreTriggerEntryFromWidget(entry, value) {
|
||||
if (Array.isArray(value.variantStates)) {
|
||||
entry.variantOn = Object.fromEntries(
|
||||
value.variantStates.filter((item) => item?.id).map((item) => [item.id, item.on === true])
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value.triggerStates)) {
|
||||
entry.triggers = value.triggerStates.map((item) => item.word).filter(Boolean);
|
||||
entry.triggerOn = Object.fromEntries(
|
||||
value.triggerStates.filter((item) => item?.word).map((item) => [item.word, item.on !== false])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class LoraEntrySettingsDialog {
|
||||
constructor(entry, { onApply } = {}) {
|
||||
injectSettingsStyles();
|
||||
this.entry = entry;
|
||||
this.onApply = onApply;
|
||||
this.closed = false;
|
||||
|
||||
this.overlay = document.createElement("div");
|
||||
this.overlay.className = "crash-lora-settings-overlay";
|
||||
this.overlay.innerHTML = `
|
||||
<div class="crash-lora-settings-dialog" role="dialog" aria-modal="true">
|
||||
<div class="crash-lora-settings-header">
|
||||
<h2>LoRA Settings</h2>
|
||||
<button type="button" class="crash-lora-settings-close" title="Close">✕</button>
|
||||
</div>
|
||||
<div class="crash-lora-settings-body">
|
||||
<div class="crash-lora-settings-field">
|
||||
<label>
|
||||
Strength
|
||||
<input type="number" class="strength-input" step="0.05" />
|
||||
</label>
|
||||
</div>
|
||||
<div class="crash-lora-settings-section variant-section">
|
||||
<h3>Character / Concept Tags</h3>
|
||||
<div class="crash-lora-settings-chips variant-chips"></div>
|
||||
</div>
|
||||
<div class="crash-lora-settings-section tag-section">
|
||||
<h3>Trigger Tags</h3>
|
||||
<div class="crash-lora-settings-chips tag-chips"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="crash-lora-settings-footer">
|
||||
<button type="button" class="crash-lora-settings-btn cancel-btn">Cancel</button>
|
||||
<button type="button" class="crash-lora-settings-btn primary apply-btn">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.titleEl = this.overlay.querySelector("h2");
|
||||
this.strengthInput = this.overlay.querySelector(".strength-input");
|
||||
this.variantSection = this.overlay.querySelector(".variant-section");
|
||||
this.variantChipsEl = this.overlay.querySelector(".variant-chips");
|
||||
this.tagChipsEl = this.overlay.querySelector(".tag-chips");
|
||||
|
||||
this.overlay.querySelector(".crash-lora-settings-close").addEventListener("click", () => this.close(false));
|
||||
this.overlay.querySelector(".cancel-btn").addEventListener("click", () => this.close(false));
|
||||
this.overlay.querySelector(".apply-btn").addEventListener("click", () => this.close(true));
|
||||
this.overlay.addEventListener("click", (e) => {
|
||||
if (e.target === this.overlay) this.close(false);
|
||||
});
|
||||
|
||||
this.strengthInput.value = String(this.entry.strength ?? 1.0);
|
||||
this.titleEl.textContent = this.entry.name || this.entry.lora.split("/").pop() || "LoRA Settings";
|
||||
this.renderVariantChips();
|
||||
this.renderTagChips();
|
||||
}
|
||||
|
||||
renderVariantChips() {
|
||||
const variants = this.entry.variantFiles || [];
|
||||
this.variantSection.style.display = variants.length ? "" : "none";
|
||||
this.variantChipsEl.innerHTML = "";
|
||||
|
||||
if (variants.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const variant of variants) {
|
||||
const chip = document.createElement("button");
|
||||
chip.type = "button";
|
||||
chip.className = `crash-lora-settings-chip${this.entry.variantOn?.[variant.id] === true ? " on" : " off"}`;
|
||||
chip.textContent = variant.label || variant.id;
|
||||
chip.title = variant.label || variant.id;
|
||||
chip.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
const enabled = this.entry.variantOn?.[variant.id] === true;
|
||||
this.entry.variantOn[variant.id] = !enabled;
|
||||
recomputeEntryTriggers(this.entry);
|
||||
this.renderVariantChips();
|
||||
this.renderTagChips();
|
||||
});
|
||||
this.variantChipsEl.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
renderTagChips() {
|
||||
this.tagChipsEl.innerHTML = "";
|
||||
const tags = this.entry.triggers || [];
|
||||
|
||||
if (tags.length === 0) {
|
||||
this.tagChipsEl.innerHTML = `<div class="crash-lora-settings-empty">No trigger tags available</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const word of tags) {
|
||||
const chip = document.createElement("button");
|
||||
chip.type = "button";
|
||||
chip.className = `crash-lora-settings-chip${this.entry.triggerOn?.[word] !== false ? " on" : " off"}`;
|
||||
chip.textContent = word;
|
||||
chip.title = word;
|
||||
chip.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
this.entry.triggerOn[word] = this.entry.triggerOn?.[word] === false;
|
||||
this.renderTagChips();
|
||||
});
|
||||
this.tagChipsEl.appendChild(chip);
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
document.body.appendChild(this.overlay);
|
||||
this.strengthInput.focus();
|
||||
this.strengthInput.select();
|
||||
}
|
||||
|
||||
close(apply) {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
|
||||
if (apply) {
|
||||
const num = Number(this.strengthInput.value);
|
||||
if (!Number.isNaN(num)) {
|
||||
this.entry.strength = num;
|
||||
}
|
||||
this.onApply?.();
|
||||
}
|
||||
|
||||
this.overlay.remove();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user