refactor: move No LoRA feature from LoRA Pool to Lora Cycler widget

Move the 'empty/no LoRA' cycling functionality from the LoRA Pool node
to the Lora Cycler widget for cleaner architecture:

Frontend changes:
- Add include_no_lora field to CyclerConfig interface
- Add includeNoLora state and logic to useLoraCyclerState composable
- Add toggle UI in LoraCyclerSettingsView with special styling
- Show 'No LoRA' entry in LoraListModal when enabled
- Update LoraCyclerWidget to integrate new logic

Backend changes:
- lora_cycler.py reads include_no_lora from config
- Calculate effective_total_count (actual count + 1 when enabled)
- Return empty lora_stack when on No LoRA position
- Return actual LoRA count in total_count (not effective count)

Reverted files to pre-PR state:
- lora_loader.py, lora_pool.py, lora_randomizer.py, lora_stacker.py
- lora_routes.py, lora_service.py
- LoraPoolWidget.vue and related files

Related to PR #861

Co-authored-by: dogatech <dogatech@dogatech.home>
This commit is contained in:
Will Miao
2026-03-19 14:19:49 +08:00
parent 8dd849892d
commit 1ae1b0d607
22 changed files with 459 additions and 316 deletions
@@ -13,7 +13,9 @@
@click="handleOpenSelector"
>
<span class="progress-label">{{ isWorkflowExecuting ? 'Using LoRA:' : 'Next LoRA:' }}</span>
<span class="progress-name clickable" :class="{ disabled: isPauseDisabled }" :title="currentLoraFilename">
<span class="progress-name clickable"
:class="{ disabled: isPauseDisabled, 'no-lora': isNoLora }"
:title="currentLoraFilename">
{{ currentLoraName || 'None' }}
<svg class="selector-icon" viewBox="0 0 24 24" fill="currentColor">
<path d="M7 10l5 5 5-5z"/>
@@ -160,6 +162,27 @@
/>
</div>
</div>
<!-- Include No LoRA Toggle -->
<div class="setting-section">
<div class="section-header-with-toggle">
<label class="setting-label">
Add "No LoRA" step
</label>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': includeNoLora }"
@click="$emit('update:includeNoLora', !includeNoLora)"
role="switch"
:aria-checked="includeNoLora"
title="Add an iteration without LoRA for comparison"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</div>
</div>
</div>
</template>
@@ -182,6 +205,8 @@ const props = defineProps<{
isPauseDisabled: boolean
isWorkflowExecuting: boolean
executingRepeatStep: number
includeNoLora: boolean
isNoLora?: boolean
}>()
const emit = defineEmits<{
@@ -190,6 +215,7 @@ const emit = defineEmits<{
'update:clipStrength': [value: number]
'update:useCustomClipRange': [value: boolean]
'update:repeatCount': [value: number]
'update:includeNoLora': [value: boolean]
'toggle-pause': []
'reset-index': []
'open-lora-selector': []
@@ -346,6 +372,16 @@ const onRepeatBlur = (event: Event) => {
color: rgba(191, 219, 254, 1);
}
.progress-name.no-lora {
font-style: italic;
color: rgba(226, 232, 240, 0.6);
}
.progress-name.clickable.no-lora:hover:not(.disabled) {
background: rgba(160, 174, 192, 0.2);
color: rgba(226, 232, 240, 0.8);
}
.progress-name.clickable.disabled {
cursor: not-allowed;
opacity: 0.5;
@@ -35,7 +35,10 @@
v-for="item in filteredList"
:key="item.index"
class="lora-item"
:class="{ active: currentIndex === item.index }"
:class="{
active: currentIndex === item.index,
'no-lora-item': item.lora.file_name === 'No LoRA'
}"
@mouseenter="showPreview(item.lora.file_name, $event)"
@mouseleave="hidePreview"
@click="selectLora(item.index)"
@@ -65,6 +68,7 @@ const props = defineProps<{
visible: boolean
loraList: LoraItem[]
currentIndex: number
includeNoLora?: boolean
}>()
const emit = defineEmits<{
@@ -79,7 +83,8 @@ const searchInputRef = ref<HTMLInputElement | null>(null)
let previewTooltip: any = null
const subtitleText = computed(() => {
const total = props.loraList.length
const baseTotal = props.loraList.length
const total = props.includeNoLora ? baseTotal + 1 : baseTotal
const filtered = filteredList.value.length
if (filtered === total) {
return `Total: ${total} LoRA${total !== 1 ? 's' : ''}`
@@ -88,11 +93,19 @@ const subtitleText = computed(() => {
})
const filteredList = computed<LoraListItem[]>(() => {
const list = props.loraList.map((lora, idx) => ({
const list: LoraListItem[] = props.loraList.map((lora, idx) => ({
index: idx + 1,
lora
}))
// Add "No LoRA" option at the end if includeNoLora is enabled
if (props.includeNoLora) {
list.push({
index: list.length + 1,
lora: { file_name: 'No LoRA' } as LoraItem
})
}
if (!searchQuery.value.trim()) {
return list
}
@@ -303,6 +316,15 @@ onUnmounted(() => {
font-weight: 500;
}
.lora-item.no-lora-item .lora-name {
font-style: italic;
color: rgba(226, 232, 240, 0.6);
}
.lora-item.no-lora-item:hover .lora-name {
color: rgba(226, 232, 240, 0.8);
}
.no-results {
padding: 32px 20px;
text-align: center;