Files
ComfyUI-Lora-Manager/vue-widgets/src/components/LoraCyclerWidget.vue
Will Miao 2121054cb9 feat(lora-cycler): implement batch queue synchronization with dual-index mechanism
- 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
2026-01-22 21:22:52 +08:00

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>