Images: preserve restored LoRAs through model load and never send hidden LoRAs

- The LoRA effect cleared the selection on every load->capable transition, which
  wiped adapters restored from a gallery recipe before the model finished loading.
  Track the previously-loaded family in a ref and clear only on a real family swap;
  keep the selection on the initial load and on unload.
- Gate the generate payload's loras on loraCapable so a restored selection that is
  hidden (loaded model does not support LoRA) is never sent to the backend.
This commit is contained in:
Daniel Han 2026-07-02 01:13:39 +00:00
commit e05c9cc947

View file

@ -985,18 +985,30 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
}, [images, hasMore, selectedId, quant]);
// Refresh the LoRA picker's suggestions when the loaded model (family) changes. A LoRA is
// trained for a specific base family, so a model swap invalidates the current selection --
// clear it (the user re-adds a suggestion or types a Hub repo id for the new family). We do
// trained for a specific base family, so a real model SWAP invalidates the current selection
// -- clear it then (the user re-adds a suggestion or types a Hub repo id for the new family).
// But do NOT clear on the first load or an unload: a user can restore a saved recipe (which
// sets loras) BEFORE the model finishes loading, and clearing on that load->capable
// transition would silently drop the restored adapters. We track the previously-loaded
// family in a ref and clear only when it changes to a different loaded family. We also do
// NOT filter the selection against the discovered catalog: a valid pick can be a free-text
// Hugging Face repo id that is not in the (often empty) curated list.
const loraCapable = Boolean(status?.loaded && status?.supports_lora);
const prevLoraFamilyRef = useRef<string | null | undefined>(undefined);
useEffect(() => {
if (!loraCapable) {
// Options are gone with the model, but keep the selection: it may have just been
// restored while the model is (re)loading. It is only SENT when loraCapable, and a
// real family swap below clears it.
setAvailableLoras([]);
setLoras([]);
return;
}
setLoras([]);
const fam = status?.family ?? null;
const prev = prevLoraFamilyRef.current;
if (prev != null && prev !== fam) {
setLoras([]);
}
prevLoraFamilyRef.current = fam;
let cancelled = false;
listDiffusionLoras(status?.family ?? undefined)
.then((list) => {
@ -1564,7 +1576,11 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
reference_images: condRefImages,
// Drop empty (no id typed yet) and zero-weight rows, and trim hand-typed repo ids,
// so the recipe records only adapters that actually applied. Empty -> omit entirely.
// Gate on loraCapable: a restore can leave adapters in state while the loaded model
// does not support LoRA (picker hidden), and sending them would fail generation with
// no visible row to remove.
loras: (() => {
if (!loraCapable) return undefined;
const active = loras
.map((l) => ({ id: l.id.trim(), weight: l.weight }))
.filter((l) => l.id && l.weight > 0);