mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-04-02 02:38:52 -03:00
fix(randomizer): defer UI updates until workflow completion (fixes #824)
This commit is contained in:
@@ -51,6 +51,7 @@ type RandomizerWidget = ComponentWidget<RandomizerConfig>
|
|||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
widget: RandomizerWidget
|
widget: RandomizerWidget
|
||||||
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
||||||
|
api?: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// State management
|
// State management
|
||||||
@@ -65,6 +66,13 @@ const currentLoras = ref<LoraEntry[]>([])
|
|||||||
// Track if component is mounted to avoid early watch triggers
|
// Track if component is mounted to avoid early watch triggers
|
||||||
const isMounted = ref(false)
|
const isMounted = ref(false)
|
||||||
|
|
||||||
|
interface PendingExecution {
|
||||||
|
loras?: LoraEntry[]
|
||||||
|
lastUsed?: LoraEntry[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const pendingExecutions: PendingExecution[] = []
|
||||||
|
|
||||||
// Computed property to check if we can reuse last
|
// Computed property to check if we can reuse last
|
||||||
const canReuseLast = computed(() => {
|
const canReuseLast = computed(() => {
|
||||||
const lastUsed = state.lastUsed.value
|
const lastUsed = state.lastUsed.value
|
||||||
@@ -265,18 +273,20 @@ onMounted(async () => {
|
|||||||
;(props.node as any).onExecuted = function(output: any) {
|
;(props.node as any).onExecuted = function(output: any) {
|
||||||
console.log("[LoraRandomizerWidget] Node executed with output:", output)
|
console.log("[LoraRandomizerWidget] Node executed with output:", output)
|
||||||
|
|
||||||
// Update last_used from backend
|
const pendingUpdate: PendingExecution = {}
|
||||||
|
|
||||||
if (output?.last_used !== undefined) {
|
if (output?.last_used !== undefined) {
|
||||||
state.lastUsed.value = output.last_used
|
pendingUpdate.lastUsed = output.last_used
|
||||||
console.log(`[LoraRandomizerWidget] Updated last_used: ${output.last_used ? output.last_used.length : 0} LoRAs`)
|
console.log(`[LoraRandomizerWidget] Queued last_used update: ${output.last_used ? output.last_used.length : 0} LoRAs`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update loras widget if backend provided new loras
|
if (output?.loras && Array.isArray(output.loras)) {
|
||||||
const lorasWidget = props.node.widgets?.find((w: any) => w.name === 'loras')
|
pendingUpdate.loras = output.loras
|
||||||
if (lorasWidget && output?.loras && Array.isArray(output.loras)) {
|
console.log("[LoraRandomizerWidget] Queued loras data from backend:", output.loras)
|
||||||
console.log("[LoraRandomizerWidget] Received loras data from backend:", output.loras)
|
}
|
||||||
lorasWidget.value = output.loras
|
|
||||||
currentLoras.value = output.loras
|
if (pendingUpdate.lastUsed !== undefined || pendingUpdate.loras !== undefined) {
|
||||||
|
pendingExecutions.push(pendingUpdate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call original onExecuted if it exists
|
// Call original onExecuted if it exists
|
||||||
@@ -284,6 +294,44 @@ onMounted(async () => {
|
|||||||
return originalOnExecuted(output)
|
return originalOnExecuted(output)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (props.api) {
|
||||||
|
const handleExecutionComplete = () => {
|
||||||
|
if (pendingExecutions.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const pending = pendingExecutions.shift()!
|
||||||
|
|
||||||
|
if (pending.lastUsed !== undefined) {
|
||||||
|
state.lastUsed.value = pending.lastUsed
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.loras !== undefined) {
|
||||||
|
const lorasWidget = props.node.widgets?.find((w: any) => w.name === 'loras')
|
||||||
|
if (lorasWidget) {
|
||||||
|
lorasWidget.value = pending.loras
|
||||||
|
}
|
||||||
|
currentLoras.value = pending.loras
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
props.api.addEventListener('execution_success', handleExecutionComplete)
|
||||||
|
props.api.addEventListener('execution_error', handleExecutionComplete)
|
||||||
|
props.api.addEventListener('execution_interrupted', handleExecutionComplete)
|
||||||
|
|
||||||
|
const apiCleanup = () => {
|
||||||
|
props.api.removeEventListener('execution_success', handleExecutionComplete)
|
||||||
|
props.api.removeEventListener('execution_error', handleExecutionComplete)
|
||||||
|
props.api.removeEventListener('execution_interrupted', handleExecutionComplete)
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingCleanup = (props.widget as any).onRemoveCleanup
|
||||||
|
;(props.widget as any).onRemoveCleanup = () => {
|
||||||
|
existingCleanup?.()
|
||||||
|
apiCleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -201,7 +201,8 @@ function createLoraRandomizerWidget(node) {
|
|||||||
|
|
||||||
const vueApp = createApp(LoraRandomizerWidget, {
|
const vueApp = createApp(LoraRandomizerWidget, {
|
||||||
widget,
|
widget,
|
||||||
node
|
node,
|
||||||
|
api
|
||||||
})
|
})
|
||||||
|
|
||||||
vueApp.use(PrimeVue, {
|
vueApp.use(PrimeVue, {
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { nextTick } from 'vue'
|
||||||
|
import { shallowMount } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import LoraRandomizerWidget from '@/components/LoraRandomizerWidget.vue'
|
||||||
|
import type { LoraEntry, RandomizerConfig } from '@/composables/types'
|
||||||
|
|
||||||
|
function createApiMock() {
|
||||||
|
const target = new EventTarget()
|
||||||
|
return {
|
||||||
|
addEventListener: target.addEventListener.bind(target),
|
||||||
|
removeEventListener: target.removeEventListener.bind(target),
|
||||||
|
dispatchEvent: target.dispatchEvent.bind(target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDefaultConfig(): RandomizerConfig {
|
||||||
|
return {
|
||||||
|
count_mode: 'range',
|
||||||
|
count_fixed: 3,
|
||||||
|
count_min: 2,
|
||||||
|
count_max: 5,
|
||||||
|
model_strength_min: 0,
|
||||||
|
model_strength_max: 1,
|
||||||
|
use_same_clip_strength: true,
|
||||||
|
clip_strength_min: 0,
|
||||||
|
clip_strength_max: 1,
|
||||||
|
roll_mode: 'always',
|
||||||
|
use_recommended_strength: false,
|
||||||
|
recommended_strength_scale_min: 0.5,
|
||||||
|
recommended_strength_scale_max: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('LoraRandomizerWidget deferred execution updates', () => {
|
||||||
|
it('applies backend loras and last_used only after workflow completion', async () => {
|
||||||
|
const initialLoras: LoraEntry[] = [
|
||||||
|
{
|
||||||
|
name: 'initial.safetensors',
|
||||||
|
strength: 0.8,
|
||||||
|
clipStrength: 0.8,
|
||||||
|
active: true,
|
||||||
|
expanded: false,
|
||||||
|
locked: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const deferredLoras: LoraEntry[] = [
|
||||||
|
{
|
||||||
|
name: 'deferred.safetensors',
|
||||||
|
strength: 1,
|
||||||
|
clipStrength: 1,
|
||||||
|
active: true,
|
||||||
|
expanded: false,
|
||||||
|
locked: false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
const lorasWidget = { name: 'loras', value: initialLoras }
|
||||||
|
const node = {
|
||||||
|
id: 101,
|
||||||
|
widgets: [lorasWidget],
|
||||||
|
onExecuted: vi.fn()
|
||||||
|
}
|
||||||
|
const widget = {
|
||||||
|
value: createDefaultConfig()
|
||||||
|
}
|
||||||
|
const api = createApiMock()
|
||||||
|
|
||||||
|
const wrapper = shallowMount(LoraRandomizerWidget, {
|
||||||
|
props: {
|
||||||
|
widget,
|
||||||
|
node,
|
||||||
|
api
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const settingsView = wrapper.findComponent({ name: 'LoraRandomizerSettingsView' })
|
||||||
|
expect(settingsView.exists()).toBe(true)
|
||||||
|
expect(settingsView.props('lastUsed')).toBeNull()
|
||||||
|
|
||||||
|
;(node as any).onExecuted({
|
||||||
|
loras: deferredLoras,
|
||||||
|
last_used: deferredLoras
|
||||||
|
})
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(lorasWidget.value).toEqual(initialLoras)
|
||||||
|
expect(settingsView.props('lastUsed')).toBeNull()
|
||||||
|
|
||||||
|
api.dispatchEvent(new Event('execution_success'))
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(lorasWidget.value).toEqual(deferredLoras)
|
||||||
|
expect(settingsView.props('lastUsed')).toEqual(deferredLoras)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1577,7 +1577,7 @@ to { transform: rotate(360deg);
|
|||||||
transform: translateY(4px);
|
transform: translateY(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.lora-randomizer-widget[data-v-94d3fca2] {
|
.lora-randomizer-widget[data-v-ca6e8cec] {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
background: rgba(40, 44, 52, 0.6);
|
background: rgba(40, 44, 52, 0.6);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
@@ -13240,7 +13240,8 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
|||||||
__name: "LoraRandomizerWidget",
|
__name: "LoraRandomizerWidget",
|
||||||
props: {
|
props: {
|
||||||
widget: {},
|
widget: {},
|
||||||
node: {}
|
node: {},
|
||||||
|
api: {}
|
||||||
},
|
},
|
||||||
setup(__props) {
|
setup(__props) {
|
||||||
const props = __props;
|
const props = __props;
|
||||||
@@ -13248,6 +13249,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
|||||||
const HAS_EXECUTED = Symbol("HAS_EXECUTED");
|
const HAS_EXECUTED = Symbol("HAS_EXECUTED");
|
||||||
const currentLoras = ref([]);
|
const currentLoras = ref([]);
|
||||||
const isMounted = ref(false);
|
const isMounted = ref(false);
|
||||||
|
const pendingExecutions = [];
|
||||||
const canReuseLast = computed(() => {
|
const canReuseLast = computed(() => {
|
||||||
const lastUsed = state.lastUsed.value;
|
const lastUsed = state.lastUsed.value;
|
||||||
if (!lastUsed || lastUsed.length === 0) return false;
|
if (!lastUsed || lastUsed.length === 0) return false;
|
||||||
@@ -13377,22 +13379,55 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
const originalOnExecuted = (_b = props.node.onExecuted) == null ? void 0 : _b.bind(props.node);
|
const originalOnExecuted = (_b = props.node.onExecuted) == null ? void 0 : _b.bind(props.node);
|
||||||
props.node.onExecuted = function(output) {
|
props.node.onExecuted = function(output) {
|
||||||
var _a3;
|
|
||||||
console.log("[LoraRandomizerWidget] Node executed with output:", output);
|
console.log("[LoraRandomizerWidget] Node executed with output:", output);
|
||||||
|
const pendingUpdate = {};
|
||||||
if ((output == null ? void 0 : output.last_used) !== void 0) {
|
if ((output == null ? void 0 : output.last_used) !== void 0) {
|
||||||
state.lastUsed.value = output.last_used;
|
pendingUpdate.lastUsed = output.last_used;
|
||||||
console.log(`[LoraRandomizerWidget] Updated last_used: ${output.last_used ? output.last_used.length : 0} LoRAs`);
|
console.log(`[LoraRandomizerWidget] Queued last_used update: ${output.last_used ? output.last_used.length : 0} LoRAs`);
|
||||||
}
|
}
|
||||||
const lorasWidget2 = (_a3 = props.node.widgets) == null ? void 0 : _a3.find((w2) => w2.name === "loras");
|
if ((output == null ? void 0 : output.loras) && Array.isArray(output.loras)) {
|
||||||
if (lorasWidget2 && (output == null ? void 0 : output.loras) && Array.isArray(output.loras)) {
|
pendingUpdate.loras = output.loras;
|
||||||
console.log("[LoraRandomizerWidget] Received loras data from backend:", output.loras);
|
console.log("[LoraRandomizerWidget] Queued loras data from backend:", output.loras);
|
||||||
lorasWidget2.value = output.loras;
|
}
|
||||||
currentLoras.value = output.loras;
|
if (pendingUpdate.lastUsed !== void 0 || pendingUpdate.loras !== void 0) {
|
||||||
|
pendingExecutions.push(pendingUpdate);
|
||||||
}
|
}
|
||||||
if (originalOnExecuted) {
|
if (originalOnExecuted) {
|
||||||
return originalOnExecuted(output);
|
return originalOnExecuted(output);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (props.api) {
|
||||||
|
const handleExecutionComplete = () => {
|
||||||
|
var _a3;
|
||||||
|
if (pendingExecutions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const pending = pendingExecutions.shift();
|
||||||
|
if (pending.lastUsed !== void 0) {
|
||||||
|
state.lastUsed.value = pending.lastUsed;
|
||||||
|
}
|
||||||
|
if (pending.loras !== void 0) {
|
||||||
|
const lorasWidget2 = (_a3 = props.node.widgets) == null ? void 0 : _a3.find((w2) => w2.name === "loras");
|
||||||
|
if (lorasWidget2) {
|
||||||
|
lorasWidget2.value = pending.loras;
|
||||||
|
}
|
||||||
|
currentLoras.value = pending.loras;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
props.api.addEventListener("execution_success", handleExecutionComplete);
|
||||||
|
props.api.addEventListener("execution_error", handleExecutionComplete);
|
||||||
|
props.api.addEventListener("execution_interrupted", handleExecutionComplete);
|
||||||
|
const apiCleanup = () => {
|
||||||
|
props.api.removeEventListener("execution_success", handleExecutionComplete);
|
||||||
|
props.api.removeEventListener("execution_error", handleExecutionComplete);
|
||||||
|
props.api.removeEventListener("execution_interrupted", handleExecutionComplete);
|
||||||
|
};
|
||||||
|
const existingCleanup = props.widget.onRemoveCleanup;
|
||||||
|
props.widget.onRemoveCleanup = () => {
|
||||||
|
existingCleanup == null ? void 0 : existingCleanup();
|
||||||
|
apiCleanup();
|
||||||
|
};
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return (_ctx, _cache) => {
|
return (_ctx, _cache) => {
|
||||||
return openBlock(), createElementBlock("div", {
|
return openBlock(), createElementBlock("div", {
|
||||||
@@ -13439,7 +13474,7 @@ const _sfc_main$5 = /* @__PURE__ */ defineComponent({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-94d3fca2"]]);
|
const LoraRandomizerWidget = /* @__PURE__ */ _export_sfc(_sfc_main$5, [["__scopeId", "data-v-ca6e8cec"]]);
|
||||||
const _hoisted_1$3 = { class: "cycler-settings" };
|
const _hoisted_1$3 = { class: "cycler-settings" };
|
||||||
const _hoisted_2$3 = { class: "setting-section progress-section" };
|
const _hoisted_2$3 = { class: "setting-section progress-section" };
|
||||||
const _hoisted_3$3 = { class: "progress-label" };
|
const _hoisted_3$3 = { class: "progress-label" };
|
||||||
@@ -15262,7 +15297,8 @@ function createLoraRandomizerWidget(node) {
|
|||||||
};
|
};
|
||||||
const vueApp = createApp(LoraRandomizerWidget, {
|
const vueApp = createApp(LoraRandomizerWidget, {
|
||||||
widget,
|
widget,
|
||||||
node
|
node,
|
||||||
|
api
|
||||||
});
|
});
|
||||||
vueApp.use(PrimeVue, {
|
vueApp.use(PrimeVue, {
|
||||||
unstyled: true,
|
unstyled: true,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user