Add file pickers for checkpoint, LoRA, and load existing

- Checkpoint field in preset editor now opens a file picker filtered to .safetensors
- LoRA field in flake editor now opens a file picker filtered to .safetensors
- Load Existing now opens a file picker filtered to .yaml/.yml
- Each picker attempts to map the selected file name back to the backend's full path
- Manual text entry is still available via a small '...' button next to each picker

Note: Browsers do not allow setting the default directory of a file picker for security reasons.

Closes #18, closes #20, closes #22
This commit is contained in:
Mehdi Chaid
2026-04-28 11:30:23 -04:00
parent ce7620656b
commit aba0f00ff5
+137 -48
View File
@@ -119,6 +119,32 @@ async function fetchFlakeMeta(name) {
return META_CACHE[name];
}
// ---------- File picker helper ----------
function createFilePicker({ accept, onSelect, text = "Browse files..." }) {
const wrap = document.createElement("div");
css(wrap, "position:relative;width:100%;");
const box = document.createElement("div");
css(box, "background:#1a1a1a;color:#ddd;border:1px solid #333;padding:6px 8px;border-radius:6px;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;");
box.textContent = text;
const input = document.createElement("input");
input.type = "file";
if (accept) input.accept = accept;
input.style.display = "none";
input.addEventListener("change", () => {
const file = input.files?.[0];
if (file && onSelect) onSelect(file);
input.value = "";
});
wrap.appendChild(box);
wrap.appendChild(input);
return { container: wrap, box, input, trigger: () => input.click() };
}
// ---------- Default-flake helpers ----------
function makeDefaultEntry() {
@@ -704,14 +730,7 @@ function openEditModal({ mode, name, data, dirs }) {
const loraPathCol = document.createElement("div");
css(loraPathCol, "flex:3;min-width:0;display:flex;gap:4px;align-items:center;");
const loraBox = document.createElement("div");
css(loraBox, "flex:1;background:#1a1a1a;color:#ddd;border:1px solid #333;padding:6px 8px;border-radius:6px;font-size:13px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;");
const loraPath = fieldState.lora?.path || "";
const loraName = loraPath ? loraPath.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "No LoRA selected";
loraBox.textContent = loraName;
loraBox.title = loraPath || "";
const loraWrap = makeSearchableDropdown([], loraPath, "Select LoRA...");
const loraWrap = makeSearchableDropdown([], fieldState.lora?.path || "", "Select LoRA...");
loraWrap.container.style.display = "none";
(async () => {
try {
@@ -720,8 +739,34 @@ function openEditModal({ mode, name, data, dirs }) {
} catch { /* ignore */ }
})();
loraBox.addEventListener("click", () => {
loraBox.style.display = "none";
const loraPicker = createFilePicker({
accept: ".safetensors",
onSelect: async (file) => {
try {
const loras = await fetchLoras();
const name = file.name;
const match = loras.find(l => l === name || l.endsWith("/" + name) || l.endsWith("\\" + name));
const val = match || name;
if (!fieldState.lora) fieldState.lora = { strength: 1.0 };
fieldState.lora.path = val;
loraWrap.element.value = val;
loraWrap.element.dispatchEvent(new Event("change"));
} catch { /* ignore */ }
},
});
const loraBox = loraPicker.box;
const loraPath = fieldState.lora?.path || "";
loraBox.textContent = loraPath ? loraPath.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "No LoRA selected";
loraBox.title = loraPath || "";
const loraEditBtn = makeSmallButton("...");
loraEditBtn.title = "Type manually";
loraBox.addEventListener("click", () => loraPicker.trigger());
loraEditBtn.addEventListener("click", (e) => {
e.stopPropagation();
loraPicker.container.style.display = "none";
loraEditBtn.style.display = "none";
loraWrap.container.style.display = "block";
loraWrap.element.focus();
});
@@ -729,18 +774,27 @@ function openEditModal({ mode, name, data, dirs }) {
const val = loraWrap.element.value;
if (!fieldState.lora) fieldState.lora = { strength: 1.0 };
fieldState.lora.path = val;
const newName = val ? val.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "No LoRA selected";
loraBox.textContent = newName;
loraBox.textContent = val ? val.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "No LoRA selected";
loraBox.title = val;
loraPicker.container.style.display = "block";
loraEditBtn.style.display = "inline-block";
loraWrap.container.style.display = "none";
});
loraWrap.element.addEventListener("blur", () => {
setTimeout(() => {
const val = loraWrap.element.value;
if (!fieldState.lora) fieldState.lora = { strength: 1.0 };
fieldState.lora.path = val;
loraBox.textContent = val ? val.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "No LoRA selected";
loraBox.title = val;
loraPicker.container.style.display = "block";
loraEditBtn.style.display = "inline-block";
loraWrap.container.style.display = "none";
loraBox.style.display = "block";
}, 200);
});
loraPathCol.appendChild(loraBox);
loraPathCol.appendChild(loraPicker.container);
loraPathCol.appendChild(loraEditBtn);
loraPathCol.appendChild(loraWrap.container);
if (loraPath) {
@@ -1209,40 +1263,27 @@ function openEditModal({ mode, name, data, dirs }) {
// ---------- Picker (Load existing) ----------
function openPicker(available) {
function openFileLoadPicker(available) {
return new Promise((resolve) => {
const { content, footer, close, handlers } = openOverlay();
handlers.onClose = (v) => resolve(v ?? null);
css(content.parentElement, content.parentElement.style.cssText + "min-width:320px;");
const title = document.createElement("h3");
css(title, "margin:0 0 8px;font-size:16px;color:#fff;font-weight:500;");
title.textContent = "Load existing flake";
content.appendChild(title);
if (available.length === 0) {
const empty = document.createElement("div");
empty.textContent = "No saved flakes available.";
css(empty, "opacity:0.5;font-style:italic;padding:12px;text-align:center;");
content.appendChild(empty);
} else {
const listBox = document.createElement("div");
css(listBox, "display:flex;flex-direction:column;gap:4px;");
for (const n of available) {
const item = document.createElement("button");
item.textContent = n;
css(item, "text-align:left;padding:6px 10px;background:#2a2a2a;color:#ddd;border:1px solid #444;border-radius:3px;cursor:pointer;font-size:12px;");
item.addEventListener("mouseenter", () => { item.style.background = "#333"; });
item.addEventListener("mouseleave", () => { item.style.background = "#2a2a2a"; });
item.addEventListener("click", () => close({ name: n }));
listBox.appendChild(item);
const input = document.createElement("input");
input.type = "file";
input.accept = ".yaml,.yml";
input.style.display = "none";
input.addEventListener("change", () => {
const file = input.files?.[0];
if (!file) { resolve(null); return; }
const stem = file.name.replace(/\.ya?ml$/i, "");
const match = available.find(n => n === stem || n.endsWith("/" + stem) || n.endsWith("\\" + stem));
if (match) {
resolve({ name: match });
} else {
window.alert(`'${stem}' not found in the flakes library. Make sure the file is inside the models/flakes/ folder.`);
resolve(null);
}
content.appendChild(listBox);
}
const cancelBtn = makeButton("Cancel");
cancelBtn.addEventListener("click", () => close(undefined));
footer.appendChild(cancelBtn);
});
document.body.appendChild(input);
input.click();
setTimeout(() => { input.remove(); }, 60000);
});
}
@@ -1703,7 +1744,7 @@ function setupFlakeWidget(node) {
const { flakes } = await fetchList();
const used = new Set(readEntries().filter(e => e.name).map(e => e.name));
const available = flakes.filter(n => !used.has(n));
const result = await openPicker(available);
const result = await openFileLoadPicker(available);
if (!result || !result.name) return;
const arr = readEntries();
let has_lora = false;
@@ -1868,7 +1909,30 @@ function openPresetEditModal({ mode, name, data }) {
content.appendChild(makeComfyLabel("Checkpoint"));
const ckptWrap = makeSearchableDropdown([], data.checkpoint || "", "Select checkpoint...");
const ckptPicker = createFilePicker({
accept: ".safetensors",
onSelect: async (file) => {
try {
const ckpts = await fetchCheckpoints();
const name = file.name;
const match = ckpts.find(c => c === name || c.endsWith("/" + name) || c.endsWith("\\" + name));
ckptWrap.element.value = match || name;
ckptWrap.element.dispatchEvent(new Event("change"));
} catch { /* ignore */ }
},
});
const ckptBox = ckptPicker.box;
ckptBox.textContent = data.checkpoint ? data.checkpoint.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "Select Checkpoint";
const ckptRow = document.createElement("div");
css(ckptRow, "display:flex;gap:4px;align-items:center;");
ckptRow.appendChild(ckptPicker.container);
const ckptEditBtn = makeSmallButton("...");
ckptEditBtn.title = "Type manually";
ckptRow.appendChild(ckptEditBtn);
content.appendChild(ckptRow);
content.appendChild(ckptWrap.container);
ckptWrap.container.style.display = "none";
(async () => {
try {
const ckpts = await fetchCheckpoints();
@@ -1876,6 +1940,31 @@ function openPresetEditModal({ mode, name, data }) {
} catch { /* ignore */ }
})();
ckptBox.addEventListener("click", () => ckptPicker.trigger());
ckptEditBtn.addEventListener("click", (e) => {
e.stopPropagation();
ckptPicker.container.style.display = "none";
ckptEditBtn.style.display = "none";
ckptWrap.container.style.display = "block";
ckptWrap.element.focus();
});
ckptWrap.element.addEventListener("change", () => {
const val = ckptWrap.element.value;
ckptBox.textContent = val ? val.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "Select Checkpoint";
ckptPicker.container.style.display = "block";
ckptEditBtn.style.display = "inline-block";
ckptWrap.container.style.display = "none";
});
ckptWrap.element.addEventListener("blur", () => {
setTimeout(() => {
const val = ckptWrap.element.value;
ckptBox.textContent = val ? val.replace(/\.safetensors?$/i, "").split(/[\\/]/).pop() : "Select Checkpoint";
ckptPicker.container.style.display = "block";
ckptEditBtn.style.display = "inline-block";
ckptWrap.container.style.display = "none";
}, 200);
});
const sliderRow = document.createElement("div");
css(sliderRow, "display:flex;gap:8px;align-items:flex-start;");
const csWrap = document.createElement("div");
@@ -2212,7 +2301,7 @@ function setupFlakeComboWidget(node) {
const { flakes } = await fetchList();
const used = new Set(readAllFlakes().filter(e => e.name).map(e => e.name));
const available = flakes.filter(n => !used.has(n));
const result = await openPicker(available);
const result = await openFileLoadPicker(available);
if (!result || !result.name) return;
const arr = readAllFlakes();
let has_lora = false;