feat(testing): enhance test configuration and add Vue component tests

- Update package.json test script to run both JS and Vue tests
- Simplify LoraCyclerLM output by removing redundant lora name fallback
- Extend Vitest config to include TypeScript test files
- Add Vue testing dependencies and setup for component testing
- Implement comprehensive test suite for BatchQueueSimulator component
- Add test setup file with global mocks for ComfyUI modules
This commit is contained in:
Will Miao
2026-02-01 00:59:50 +08:00
parent ffcfe5ea3e
commit e17d6c8ebf
20 changed files with 4931 additions and 159 deletions

View File

@@ -10,10 +10,18 @@
:use-custom-clip-range="state.useCustomClipRange.value"
:is-clip-strength-disabled="state.isClipStrengthDisabled.value"
:is-loading="state.isLoading.value"
:repeat-count="state.repeatCount.value"
:repeat-used="state.displayRepeatUsed.value"
:is-paused="state.isPaused.value"
:is-workflow-executing="state.isWorkflowExecuting.value"
:executing-repeat-step="state.executingRepeatStep.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:repeat-count="handleRepeatCountChange"
@toggle-pause="handleTogglePause"
@reset-index="handleResetIndex"
@refresh="handleRefresh"
/>
</div>
@@ -31,6 +39,7 @@ type CyclerWidget = ComponentWidget<CyclerConfig>
const props = defineProps<{
widget: CyclerWidget
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
api?: any // ComfyUI API for execution events
}>()
// State management
@@ -39,6 +48,35 @@ const state = useLoraCyclerState(props.widget)
// Symbol to track if the widget has been executed at least once
const HAS_EXECUTED = Symbol('HAS_EXECUTED')
// Execution context queue for batch queue synchronization
// In batch queue mode, all beforeQueued calls happen BEFORE any onExecuted calls,
// so we need to snapshot the state at queue time and replay it during execution
interface ExecutionContext {
isPaused: boolean
repeatUsed: number
repeatCount: number
shouldAdvanceDisplay: boolean
displayRepeatUsed: number // Value to show in UI after completion
}
const executionQueue: ExecutionContext[] = []
// Track pending executions for batch queue support (deferred UI updates)
// Uses FIFO order since executions are processed in the order they were queued
interface PendingExecution {
repeatUsed: number
repeatCount: number
shouldAdvanceDisplay: boolean
displayRepeatUsed: number // Value to show in UI after completion
output?: {
nextIndex: number
nextLoraName: string
nextLoraFilename: string
currentLoraName: string
currentLoraFilename: string
}
}
const pendingExecutions: PendingExecution[] = []
// Track last known pool config hash
const lastPoolConfigHash = ref('')
@@ -62,6 +100,9 @@ const handleIndexUpdate = async (newIndex: number) => {
state.executionIndex.value = null
state.nextIndex.value = null
// Clear execution queue since user is manually changing state
executionQueue.length = 0
state.setIndex(newIndex)
// Refresh list to update current LoRA display
@@ -100,6 +141,79 @@ const handleRefresh = async () => {
}
}
// Handle repeat count change
const handleRepeatCountChange = (newValue: number) => {
state.repeatCount.value = newValue
// Reset repeatUsed when changing repeat count
state.repeatUsed.value = 0
state.displayRepeatUsed.value = 0
}
// Clear all pending items from server queue
const clearPendingQueue = async () => {
try {
// Clear local execution queue
executionQueue.length = 0
// Clear server queue (pending items only)
await fetch('/queue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clear: true })
})
console.log('[LoraCyclerWidget] Cleared pending queue on pause')
} catch (error) {
console.error('[LoraCyclerWidget] Error clearing queue:', error)
}
}
// Handle pause toggle
const handleTogglePause = async () => {
const wasPaused = state.isPaused.value
state.togglePause()
// When transitioning to paused state, clear pending queue
if (!wasPaused && state.isPaused.value) {
// Reset execution state so subsequent manual queues start fresh
;(props.widget as any)[HAS_EXECUTED] = false
state.executionIndex.value = null
state.nextIndex.value = null
await clearPendingQueue()
}
}
// Handle reset index
const handleResetIndex = async () => {
// Reset execution state
;(props.widget as any)[HAS_EXECUTED] = false
state.executionIndex.value = null
state.nextIndex.value = null
// Clear execution queue since user is resetting state
executionQueue.length = 0
// Reset index and repeat state
state.resetIndex()
// Refresh list to update current LoRA display
try {
const poolConfig = getPoolConfig()
const loraList = await state.fetchCyclerList(poolConfig)
if (loraList.length > 0) {
const currentLora = loraList[0]
if (currentLora) {
state.currentLoraName.value = currentLora.file_name
state.currentLoraFilename.value = currentLora.file_name
}
}
} catch (error) {
console.error('[LoraCyclerWidget] Error resetting index:', error)
}
}
// Check for pool config changes
const checkPoolConfigChanges = async () => {
if (!isMounted.value) return
@@ -135,17 +249,66 @@ onMounted(async () => {
// Add beforeQueued hook to handle index shifting for batch queue synchronization
// This ensures each execution uses a different LoRA in the cycle
// Now with support for repeat count and pause features
//
// IMPORTANT: In batch queue mode, ALL beforeQueued calls happen BEFORE any execution.
// We push an "execution context" snapshot to a queue so that onExecuted can use the
// correct state values that were captured at queue time (not the live state).
;(props.widget as any).beforeQueued = () => {
if (state.isPaused.value) {
// When paused: use current index, don't advance, don't count toward repeat limit
// Push context indicating this execution should NOT advance display
executionQueue.push({
isPaused: true,
repeatUsed: state.repeatUsed.value,
repeatCount: state.repeatCount.value,
shouldAdvanceDisplay: false,
displayRepeatUsed: state.displayRepeatUsed.value // Keep current display value when paused
})
// CRITICAL: Clear execution_index when paused to force backend to use current_index
// This ensures paused executions use the same LoRA regardless of any
// execution_index set by previous non-paused beforeQueued calls
const pausedConfig = state.buildConfig()
pausedConfig.execution_index = null
props.widget.value = pausedConfig
return
}
if ((props.widget as any)[HAS_EXECUTED]) {
// After first execution: shift indices (previous next_index becomes execution_index)
state.generateNextIndex()
// After first execution: check repeat logic
if (state.repeatUsed.value < state.repeatCount.value) {
// Still repeating: increment repeatUsed, use same index
state.repeatUsed.value++
} else {
// Repeat complete: reset repeatUsed to 1, advance to next index
state.repeatUsed.value = 1
state.generateNextIndex()
}
} else {
// First execution: just initialize next_index (execution_index stays null)
// This means first execution uses current_index from widget
// First execution: initialize
state.repeatUsed.value = 1
state.initializeNextIndex()
;(props.widget as any)[HAS_EXECUTED] = true
}
// Determine if this execution should advance the display
// (only when repeat cycle is complete for this queued item)
const shouldAdvanceDisplay = state.repeatUsed.value >= state.repeatCount.value
// Calculate the display value to show after this execution completes
// When advancing to a new LoRA: reset to 0 (fresh start for new LoRA)
// When repeating same LoRA: show current repeat step
const displayRepeatUsed = shouldAdvanceDisplay ? 0 : state.repeatUsed.value
// Push execution context snapshot to queue
executionQueue.push({
isPaused: false,
repeatUsed: state.repeatUsed.value,
repeatCount: state.repeatCount.value,
shouldAdvanceDisplay,
displayRepeatUsed
})
// Update the widget value so the indices are included in the serialized config
props.widget.value = state.buildConfig()
}
@@ -163,35 +326,62 @@ onMounted(async () => {
}
// Override onExecuted to handle backend UI updates
// This defers the UI update until workflow completes (via API events)
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
}
// Pop execution context from queue (FIFO order)
const context = executionQueue.shift()
// Determine if we should advance the display index
const shouldAdvanceDisplay = context
? context.shouldAdvanceDisplay
: (!state.isPaused.value && state.repeatUsed.value >= state.repeatCount.value)
// Extract output values
const nextIndex = output?.next_index !== undefined
? (Array.isArray(output.next_index) ? output.next_index[0] : output.next_index)
: state.currentIndex.value
const nextLoraName = output?.next_lora_name !== undefined
? (Array.isArray(output.next_lora_name) ? output.next_lora_name[0] : output.next_lora_name)
: ''
const nextLoraFilename = output?.next_lora_filename !== undefined
? (Array.isArray(output.next_lora_filename) ? output.next_lora_filename[0] : output.next_lora_filename)
: ''
const currentLoraName = output?.current_lora_name !== undefined
? (Array.isArray(output.current_lora_name) ? output.current_lora_name[0] : output.current_lora_name)
: ''
const currentLoraFilename = output?.current_lora_filename !== undefined
? (Array.isArray(output.current_lora_filename) ? output.current_lora_filename[0] : output.current_lora_filename)
: ''
// Update total count immediately (doesn't need to wait for workflow completion)
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
// Store pending update (will be applied on workflow completion)
if (context) {
pendingExecutions.push({
repeatUsed: context.repeatUsed,
repeatCount: context.repeatCount,
shouldAdvanceDisplay,
displayRepeatUsed: context.displayRepeatUsed,
output: {
nextIndex,
nextLoraName,
nextLoraFilename,
currentLoraName,
currentLoraFilename
}
})
// Update visual feedback state (don't update displayRepeatUsed yet - wait for workflow completion)
state.executingRepeatStep.value = context.repeatUsed
state.isWorkflowExecuting.value = true
}
// Call original onExecuted if it exists
@@ -200,11 +390,69 @@ onMounted(async () => {
}
}
// Set up execution tracking via API events
if (props.api) {
// Handle workflow completion events using FIFO order
// Note: The 'executing' event doesn't contain prompt_id (only node ID as string),
// so we use FIFO order instead of prompt_id matching since executions are processed
// in the order they were queued
const handleExecutionComplete = () => {
// Process the first pending execution (FIFO order)
if (pendingExecutions.length === 0) {
return
}
const pending = pendingExecutions.shift()!
// Apply UI update now that workflow is complete
// Update repeat display (deferred like index updates)
state.displayRepeatUsed.value = pending.displayRepeatUsed
if (pending.output) {
if (pending.shouldAdvanceDisplay) {
state.currentIndex.value = pending.output.nextIndex
state.currentLoraName.value = pending.output.nextLoraName
state.currentLoraFilename.value = pending.output.nextLoraFilename
} else {
// When not advancing, show current LoRA info
state.currentLoraName.value = pending.output.currentLoraName
state.currentLoraFilename.value = pending.output.currentLoraFilename
}
}
// Reset visual feedback if no more pending
if (pendingExecutions.length === 0) {
state.isWorkflowExecuting.value = false
state.executingRepeatStep.value = 0
}
}
props.api.addEventListener('execution_success', handleExecutionComplete)
props.api.addEventListener('execution_error', handleExecutionComplete)
props.api.addEventListener('execution_interrupted', handleExecutionComplete)
// Store cleanup function for API listeners
const apiCleanup = () => {
props.api.removeEventListener('execution_success', handleExecutionComplete)
props.api.removeEventListener('execution_error', handleExecutionComplete)
props.api.removeEventListener('execution_interrupted', handleExecutionComplete)
}
// Extend existing cleanup
const existingCleanup = (props.widget as any).onRemoveCleanup
;(props.widget as any).onRemoveCleanup = () => {
existingCleanup?.()
apiCleanup()
}
}
// 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)
const existingCleanupForInterval = (props.widget as any).onRemoveCleanup
;(props.widget as any).onRemoveCleanup = () => {
existingCleanupForInterval?.()
clearInterval(checkInterval)
}
})

View File

@@ -6,15 +6,22 @@
<!-- Progress Display -->
<div class="setting-section progress-section">
<div class="progress-display">
<div class="progress-display" :class="{ executing: isWorkflowExecuting }">
<div class="progress-info">
<span class="progress-label">Next LoRA:</span>
<span class="progress-label">{{ isWorkflowExecuting ? 'Using LoRA:' : 'Next LoRA:' }}</span>
<span class="progress-name" :title="currentLoraFilename">{{ currentLoraName || 'None' }}</span>
</div>
<div class="progress-counter">
<span class="progress-index">{{ currentIndex }}</span>
<span class="progress-separator">/</span>
<span class="progress-total">{{ totalCount }}</span>
<!-- Repeat indicator (only shown when repeatCount > 1) -->
<div v-if="repeatCount > 1" class="repeat-badge">
<span class="repeat-badge-label">Rep</span>
<span class="repeat-badge-value">{{ repeatUsed }}/{{ repeatCount }}</span>
</div>
<button
class="refresh-button"
:disabled="isLoading"
@@ -39,10 +46,11 @@
</div>
</div>
<!-- Starting Index -->
<!-- Starting Index with Advanced Controls -->
<div class="setting-section">
<label class="setting-label">Starting Index</label>
<div class="index-input-container">
<div class="index-controls-row">
<!-- Index input -->
<input
type="number"
class="index-input"
@@ -57,6 +65,47 @@
@pointerup.stop
/>
<span class="index-hint">1 - {{ totalCount || 1 }}</span>
<!-- Repeat control -->
<span class="repeat-label">x</span>
<input
type="number"
class="repeat-input"
min="1"
max="99"
:value="repeatCount"
@input="onRepeatInput"
@blur="onRepeatBlur"
@pointerdown.stop
@pointermove.stop
@pointerup.stop
title="Repeat each LoRA this many times"
/>
<span class="repeat-hint">times</span>
<!-- Control buttons -->
<button
class="control-btn"
:class="{ active: isPaused }"
@click="$emit('toggle-pause')"
:title="isPaused ? 'Continue iteration' : 'Pause iteration'"
>
<svg v-if="isPaused" viewBox="0 0 24 24" fill="currentColor" class="control-icon">
<path d="M8 5v14l11-7z"/>
</svg>
<svg v-else viewBox="0 0 24 24" fill="currentColor" class="control-icon">
<path d="M6 4h4v16H6zm8 0h4v16h-4z"/>
</svg>
</button>
<button
class="control-btn"
@click="$emit('reset-index')"
title="Reset to index 1"
>
<svg viewBox="0 0 24 24" fill="currentColor" class="control-icon">
<path d="M12 5V1L7 6l5 5V7c3.31 0 6 2.69 6 6s-2.69 6-6 6-6-2.69-6-6H4c0 4.42 3.58 8 8 8s8-3.58 8-8-3.58-8-8-8z"/>
</svg>
</button>
</div>
</div>
@@ -123,6 +172,11 @@ const props = defineProps<{
useCustomClipRange: boolean
isClipStrengthDisabled: boolean
isLoading: boolean
repeatCount: number
repeatUsed: number
isPaused: boolean
isWorkflowExecuting: boolean
executingRepeatStep: number
}>()
const emit = defineEmits<{
@@ -130,11 +184,15 @@ const emit = defineEmits<{
'update:modelStrength': [value: number]
'update:clipStrength': [value: number]
'update:useCustomClipRange': [value: boolean]
'update:repeatCount': [value: number]
'toggle-pause': []
'reset-index': []
'refresh': []
}>()
// Temporary value for input while typing
const tempIndex = ref<string>('')
const tempRepeat = ref<string>('')
const onIndexInput = (event: Event) => {
const input = event.target as HTMLInputElement
@@ -154,6 +212,25 @@ const onIndexBlur = (event: Event) => {
}
tempIndex.value = ''
}
const onRepeatInput = (event: Event) => {
const input = event.target as HTMLInputElement
tempRepeat.value = input.value
}
const onRepeatBlur = (event: Event) => {
const input = event.target as HTMLInputElement
const value = parseInt(input.value, 10)
if (!isNaN(value)) {
const clampedValue = Math.max(1, Math.min(value, 99))
emit('update:repeatCount', clampedValue)
input.value = clampedValue.toString()
} else {
input.value = props.repeatCount.toString()
}
tempRepeat.value = ''
}
</script>
<style scoped>
@@ -203,6 +280,17 @@ const onIndexBlur = (event: Event) => {
display: flex;
justify-content: space-between;
align-items: center;
transition: border-color 0.3s ease;
}
.progress-display.executing {
border-color: rgba(66, 153, 225, 0.5);
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { border-color: rgba(66, 153, 225, 0.3); }
50% { border-color: rgba(66, 153, 225, 0.7); }
}
.progress-info {
@@ -243,6 +331,9 @@ const onIndexBlur = (event: Event) => {
font-weight: 600;
color: rgba(66, 153, 225, 1);
font-family: 'SF Mono', 'Roboto Mono', monospace;
min-width: 4ch;
text-align: right;
font-variant-numeric: tabular-nums;
}
.progress-separator {
@@ -256,6 +347,9 @@ const onIndexBlur = (event: Event) => {
font-weight: 500;
color: rgba(226, 232, 240, 0.6);
font-family: 'SF Mono', 'Roboto Mono', monospace;
min-width: 4ch;
text-align: left;
font-variant-numeric: tabular-nums;
}
.refresh-button {
@@ -303,16 +397,43 @@ const onIndexBlur = (event: Event) => {
}
}
/* Index Input */
.index-input-container {
/* Repeat Badge */
.repeat-badge {
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
padding: 2px 6px;
background: rgba(245, 158, 11, 0.15);
border: 1px solid rgba(245, 158, 11, 0.3);
border-radius: 4px;
}
.repeat-badge-label {
font-size: 10px;
color: rgba(253, 230, 138, 0.7);
text-transform: uppercase;
}
.repeat-badge-value {
font-size: 12px;
font-family: 'SF Mono', 'Roboto Mono', monospace;
color: rgba(253, 230, 138, 1);
min-width: 3ch;
font-variant-numeric: tabular-nums;
}
/* Index Controls Row */
.index-controls-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.index-input {
width: 80px;
padding: 6px 10px;
width: 60px;
padding: 6px 8px;
background: rgba(26, 32, 44, 0.9);
border: 1px solid rgba(226, 232, 240, 0.2);
border-radius: 6px;
@@ -334,6 +455,75 @@ const onIndexBlur = (event: Event) => {
.index-hint {
font-size: 11px;
color: rgba(226, 232, 240, 0.4);
min-width: 7ch;
font-variant-numeric: tabular-nums;
}
/* Repeat Controls */
.repeat-label {
font-size: 13px;
color: rgba(226, 232, 240, 0.6);
margin-left: 4px;
}
.repeat-input {
width: 44px;
padding: 6px 6px;
background: rgba(26, 32, 44, 0.9);
border: 1px solid rgba(226, 232, 240, 0.2);
border-radius: 6px;
color: #e4e4e7;
font-size: 13px;
font-family: 'SF Mono', 'Roboto Mono', monospace;
text-align: center;
}
.repeat-input:focus {
outline: none;
border-color: rgba(66, 153, 225, 0.6);
}
.repeat-hint {
font-size: 11px;
color: rgba(226, 232, 240, 0.4);
}
/* Control Buttons */
.control-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
padding: 0;
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
color: rgba(226, 232, 240, 0.6);
cursor: pointer;
transition: all 0.2s;
}
.control-btn:hover {
background: rgba(66, 153, 225, 0.2);
border-color: rgba(66, 153, 225, 0.4);
color: rgba(191, 219, 254, 1);
}
.control-btn.active {
background: rgba(245, 158, 11, 0.2);
border-color: rgba(245, 158, 11, 0.5);
color: rgba(253, 230, 138, 1);
}
.control-btn.active:hover {
background: rgba(245, 158, 11, 0.3);
border-color: rgba(245, 158, 11, 0.6);
}
.control-icon {
width: 14px;
height: 14px;
}
/* Slider Container */

View File

@@ -80,6 +80,10 @@ export interface CyclerConfig {
// Dual-index mechanism for batch queue synchronization
execution_index?: number | null // Index to use for current execution
next_index?: number | null // Index for display after execution
// Advanced index control features
repeat_count: number // How many times each LoRA should repeat (default: 1)
repeat_used: number // How many times current index has been used
is_paused: boolean // Whether iteration is paused
}
// Widget config union type

View File

@@ -29,6 +29,16 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
const executionIndex = ref<number | null>(null)
const nextIndex = ref<number | null>(null)
// Advanced index control features
const repeatCount = ref(1) // How many times each LoRA should repeat
const repeatUsed = ref(0) // How many times current index has been used (internal tracking)
const displayRepeatUsed = ref(0) // For UI display, deferred updates like currentIndex
const isPaused = ref(false) // Whether iteration is paused
// Execution progress tracking (visual feedback)
const isWorkflowExecuting = ref(false) // Workflow is currently running
const executingRepeatStep = ref(0) // Which repeat step (1-based, 0 = not executing)
// Build config object from current state
const buildConfig = (): CyclerConfig => {
// Skip updating widget.value during restoration to prevent infinite loops
@@ -45,6 +55,9 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
current_lora_filename: currentLoraFilename.value,
execution_index: executionIndex.value,
next_index: nextIndex.value,
repeat_count: repeatCount.value,
repeat_used: repeatUsed.value,
is_paused: isPaused.value,
}
}
return {
@@ -59,6 +72,9 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
current_lora_filename: currentLoraFilename.value,
execution_index: executionIndex.value,
next_index: nextIndex.value,
repeat_count: repeatCount.value,
repeat_used: repeatUsed.value,
is_paused: isPaused.value,
}
}
@@ -77,6 +93,10 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
sortBy.value = config.sort_by || 'filename'
currentLoraName.value = config.current_lora_name || ''
currentLoraFilename.value = config.current_lora_filename || ''
// Advanced index control features
repeatCount.value = config.repeat_count ?? 1
repeatUsed.value = config.repeat_used ?? 0
isPaused.value = config.is_paused ?? false
// Note: execution_index and next_index are not restored from config
// as they are transient values used only during batch execution
} finally {
@@ -215,6 +235,19 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
}
}
// Reset index to 1 and clear repeat state
const resetIndex = () => {
currentIndex.value = 1
repeatUsed.value = 0
displayRepeatUsed.value = 0
// Note: isPaused is intentionally not reset - user may want to stay paused after reset
}
// Toggle pause state
const togglePause = () => {
isPaused.value = !isPaused.value
}
// Computed property to check if clip strength is disabled
const isClipStrengthDisabled = computed(() => !useCustomClipRange.value)
@@ -236,6 +269,9 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
sortBy,
currentLoraName,
currentLoraFilename,
repeatCount,
repeatUsed,
isPaused,
], () => {
widget.value = buildConfig()
}, { deep: true })
@@ -254,6 +290,12 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
isLoading,
executionIndex,
nextIndex,
repeatCount,
repeatUsed,
displayRepeatUsed,
isPaused,
isWorkflowExecuting,
executingRepeatStep,
// Computed
isClipStrengthDisabled,
@@ -267,5 +309,7 @@ export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
setIndex,
generateNextIndex,
initializeNextIndex,
resetIndex,
togglePause,
}
}

View File

@@ -27,6 +27,8 @@ const AUTOCOMPLETE_TEXT_WIDGET_MAX_HEIGHT = 100
// @ts-ignore - ComfyUI external module
import { app } from '../../../scripts/app.js'
// @ts-ignore - ComfyUI external module
import { api } from '../../../scripts/api.js'
// @ts-ignore
import { getPoolConfigFromConnectedNode, getActiveLorasFromNode, updateConnectedTriggerWords, updateDownstreamLoaders } from '../../web/comfyui/utils.js'
@@ -255,7 +257,8 @@ function createLoraCyclerWidget(node) {
const vueApp = createApp(LoraCyclerWidget, {
widget,
node
node,
api
})
vueApp.use(PrimeVue, {