mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-25 23:25:43 -03:00
feat(lora-cycler): add sequential LoRA cycling through filtered pool
Add Lora Cycler node that cycles through LoRAs sequentially from a filtered pool. Supports configurable sort order, strength settings, and persists cycle progress across workflow save/load. Backend: - New LoraCyclerNode with cycle() method - New /api/lm/loras/cycler-list endpoint - LoraService.get_cycler_list() for filtered/sorted list Frontend: - LoraCyclerWidget with Vue.js component - useLoraCyclerState composable - LoraCyclerSettingsView for UI display
This commit is contained in:
219
vue-widgets/src/components/LoraCyclerWidget.vue
Normal file
219
vue-widgets/src/components/LoraCyclerWidget.vue
Normal file
@@ -0,0 +1,219 @@
|
||||
<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"
|
||||
:sort-by="state.sortBy.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"
|
||||
@update:sort-by="handleSortByChange"
|
||||
@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)
|
||||
|
||||
// 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 = state.sortBy.value === 'filename'
|
||||
? currentLora.file_name
|
||||
: (currentLora.model_name || currentLora.file_name)
|
||||
state.currentLoraFilename.value = currentLora.file_name
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerWidget] Error updating index:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle sort by change
|
||||
const handleSortByChange = async (newSortBy: 'filename' | 'model_name') => {
|
||||
state.sortBy.value = newSortBy
|
||||
|
||||
// Refresh list with new sort order
|
||||
try {
|
||||
const poolConfig = getPoolConfig()
|
||||
await state.refreshList(poolConfig)
|
||||
} catch (error) {
|
||||
console.error('[LoraCyclerWidget] Error changing sort:', 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
if (output?.sort_by !== undefined) {
|
||||
const val = Array.isArray(output.sort_by) ? output.sort_by[0] : output.sort_by
|
||||
state.sortBy.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>
|
||||
Reference in New Issue
Block a user