mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-24 14:42:11 -03:00
- Add execution_index and next_index fields to CyclerConfig interface - Introduce beforeQueued hook in widget to handle index shifting for batch executions - Use execution_index when provided, fall back to current_index for single executions - Track execution state with Symbol to differentiate first vs subsequent executions - Update state management to handle dual-index logic for proper LoRA cycling in batch queues
224 lines
7.3 KiB
Vue
224 lines
7.3 KiB
Vue
<template>
|
|
<div class="lora-cycler-widget">
|
|
<LoraCyclerSettingsView
|
|
:current-index="state.currentIndex.value"
|
|
:total-count="state.totalCount.value"
|
|
:current-lora-name="state.currentLoraName.value"
|
|
:current-lora-filename="state.currentLoraFilename.value"
|
|
:model-strength="state.modelStrength.value"
|
|
:clip-strength="state.clipStrength.value"
|
|
:use-custom-clip-range="state.useCustomClipRange.value"
|
|
:is-clip-strength-disabled="state.isClipStrengthDisabled.value"
|
|
:is-loading="state.isLoading.value"
|
|
@update:current-index="handleIndexUpdate"
|
|
@update:model-strength="state.modelStrength.value = $event"
|
|
@update:clip-strength="state.clipStrength.value = $event"
|
|
@update:use-custom-clip-range="handleUseCustomClipRangeChange"
|
|
@refresh="handleRefresh"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { onMounted, ref } from 'vue'
|
|
import LoraCyclerSettingsView from './lora-cycler/LoraCyclerSettingsView.vue'
|
|
import { useLoraCyclerState } from '../composables/useLoraCyclerState'
|
|
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from '../composables/types'
|
|
|
|
// Props
|
|
const props = defineProps<{
|
|
widget: ComponentWidget
|
|
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
|
}>()
|
|
|
|
// State management
|
|
const state = useLoraCyclerState(props.widget)
|
|
|
|
// Symbol to track if the widget has been executed at least once
|
|
const HAS_EXECUTED = Symbol('HAS_EXECUTED')
|
|
|
|
// Track last known pool config hash
|
|
const lastPoolConfigHash = ref('')
|
|
|
|
// Track if component is mounted
|
|
const isMounted = ref(false)
|
|
|
|
// Get pool config from connected node
|
|
const getPoolConfig = (): LoraPoolConfig | null => {
|
|
// Check if getPoolConfig method exists on node (added by main.ts)
|
|
if ((props.node as any).getPoolConfig) {
|
|
return (props.node as any).getPoolConfig()
|
|
}
|
|
return null
|
|
}
|
|
|
|
// Handle index update from user
|
|
const handleIndexUpdate = async (newIndex: number) => {
|
|
state.setIndex(newIndex)
|
|
|
|
// Refresh list to update current LoRA display
|
|
try {
|
|
const poolConfig = getPoolConfig()
|
|
const loraList = await state.fetchCyclerList(poolConfig)
|
|
|
|
if (loraList.length > 0 && newIndex > 0 && newIndex <= loraList.length) {
|
|
const currentLora = loraList[newIndex - 1]
|
|
if (currentLora) {
|
|
state.currentLoraName.value = currentLora.file_name
|
|
state.currentLoraFilename.value = currentLora.file_name
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[LoraCyclerWidget] Error updating index:', error)
|
|
}
|
|
}
|
|
|
|
// Handle use custom clip range toggle
|
|
const handleUseCustomClipRangeChange = (newValue: boolean) => {
|
|
state.useCustomClipRange.value = newValue
|
|
// When toggling off, sync clip strength to model strength
|
|
if (!newValue) {
|
|
state.clipStrength.value = state.modelStrength.value
|
|
}
|
|
}
|
|
|
|
// Handle refresh button click
|
|
const handleRefresh = async () => {
|
|
try {
|
|
const poolConfig = getPoolConfig()
|
|
await state.refreshList(poolConfig)
|
|
} catch (error) {
|
|
console.error('[LoraCyclerWidget] Error refreshing:', error)
|
|
}
|
|
}
|
|
|
|
// Check for pool config changes
|
|
const checkPoolConfigChanges = async () => {
|
|
if (!isMounted.value) return
|
|
|
|
const poolConfig = getPoolConfig()
|
|
const newHash = state.hashPoolConfig(poolConfig)
|
|
|
|
if (newHash !== lastPoolConfigHash.value) {
|
|
console.log('[LoraCyclerWidget] Pool config changed, refreshing list')
|
|
lastPoolConfigHash.value = newHash
|
|
try {
|
|
await state.refreshList(poolConfig)
|
|
} catch (error) {
|
|
console.error('[LoraCyclerWidget] Error on pool config change:', error)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lifecycle
|
|
onMounted(async () => {
|
|
// Setup serialization
|
|
props.widget.serializeValue = async () => {
|
|
return state.buildConfig()
|
|
}
|
|
|
|
// Handle external value updates (e.g., loading workflow, paste)
|
|
props.widget.onSetValue = (v) => {
|
|
state.restoreFromConfig(v as CyclerConfig)
|
|
}
|
|
|
|
// Restore from saved value
|
|
if (props.widget.value) {
|
|
state.restoreFromConfig(props.widget.value as CyclerConfig)
|
|
}
|
|
|
|
// Add beforeQueued hook to handle index shifting for batch queue synchronization
|
|
// This ensures each execution uses a different LoRA in the cycle
|
|
;(props.widget as any).beforeQueued = () => {
|
|
if ((props.widget as any)[HAS_EXECUTED]) {
|
|
// After first execution: shift indices (previous next_index becomes execution_index)
|
|
state.generateNextIndex()
|
|
} else {
|
|
// First execution: just initialize next_index (execution_index stays null)
|
|
// This means first execution uses current_index from widget
|
|
state.initializeNextIndex()
|
|
;(props.widget as any)[HAS_EXECUTED] = true
|
|
}
|
|
|
|
// Update the widget value so the indices are included in the serialized config
|
|
const config = state.buildConfig()
|
|
if ((props.widget as any).updateConfig) {
|
|
;(props.widget as any).updateConfig(config)
|
|
} else {
|
|
props.widget.value = config
|
|
}
|
|
}
|
|
|
|
// Mark component as mounted
|
|
isMounted.value = true
|
|
|
|
// Initial load
|
|
try {
|
|
const poolConfig = getPoolConfig()
|
|
lastPoolConfigHash.value = state.hashPoolConfig(poolConfig)
|
|
await state.refreshList(poolConfig)
|
|
} catch (error) {
|
|
console.error('[LoraCyclerWidget] Error on initial load:', error)
|
|
}
|
|
|
|
// Override onExecuted to handle backend UI updates
|
|
const originalOnExecuted = (props.node as any).onExecuted?.bind(props.node)
|
|
|
|
;(props.node as any).onExecuted = function(output: any) {
|
|
console.log("[LoraCyclerWidget] Node executed with output:", output)
|
|
|
|
// Update state from backend response (values are wrapped in arrays)
|
|
if (output?.next_index !== undefined) {
|
|
const val = Array.isArray(output.next_index) ? output.next_index[0] : output.next_index
|
|
state.currentIndex.value = val
|
|
}
|
|
if (output?.total_count !== undefined) {
|
|
const val = Array.isArray(output.total_count) ? output.total_count[0] : output.total_count
|
|
state.totalCount.value = val
|
|
}
|
|
if (output?.current_lora_name !== undefined) {
|
|
const val = Array.isArray(output.current_lora_name) ? output.current_lora_name[0] : output.current_lora_name
|
|
state.currentLoraName.value = val
|
|
}
|
|
if (output?.current_lora_filename !== undefined) {
|
|
const val = Array.isArray(output.current_lora_filename) ? output.current_lora_filename[0] : output.current_lora_filename
|
|
state.currentLoraFilename.value = val
|
|
}
|
|
if (output?.next_lora_name !== undefined) {
|
|
const val = Array.isArray(output.next_lora_name) ? output.next_lora_name[0] : output.next_lora_name
|
|
state.currentLoraName.value = val
|
|
}
|
|
if (output?.next_lora_filename !== undefined) {
|
|
const val = Array.isArray(output.next_lora_filename) ? output.next_lora_filename[0] : output.next_lora_filename
|
|
state.currentLoraFilename.value = val
|
|
}
|
|
|
|
// Call original onExecuted if it exists
|
|
if (originalOnExecuted) {
|
|
return originalOnExecuted(output)
|
|
}
|
|
}
|
|
|
|
// Watch for connection changes by polling (since ComfyUI doesn't provide connection events)
|
|
const checkInterval = setInterval(checkPoolConfigChanges, 1000)
|
|
|
|
// Cleanup on unmount (handled by Vue's effect scope)
|
|
;(props.widget as any).onRemoveCleanup = () => {
|
|
clearInterval(checkInterval)
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.lora-cycler-widget {
|
|
padding: 6px;
|
|
background: rgba(40, 44, 52, 0.6);
|
|
border-radius: 6px;
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
box-sizing: border-box;
|
|
}
|
|
</style>
|