Files
ComfyUI-Lora-Manager/vue-widgets/src/components/lora-randomizer/LoraRandomizerSettingsView.vue
T
Will Miao d1c65a6186 fix(dual-range-slider): allow equal min/max values in Lora Randomizer (#775)
Add allowEqualValues prop to DualRangeSlider component (default: false for backward compatibility).
When enabled, removes the step offset constraint that prevented min and max handles from being set to the same value.

Applied to all range sliders in LoraRandomizerSettingsView:
- LoRA Count range slider
- Model Strength Range slider
- Recommended Strength Scale slider
- Clip Strength Range slider

Backend already handles equal values correctly via rng.uniform().
2026-01-22 16:47:39 +08:00

512 lines
14 KiB
Vue

<template>
<div class="randomizer-settings">
<div class="settings-header">
<h3 class="settings-title">RANDOMIZER SETTINGS</h3>
</div>
<!-- LoRA Count -->
<div class="setting-section">
<label class="setting-label">LoRA Count</label>
<div class="count-mode-tabs">
<label class="count-mode-tab" :class="{ active: countMode === 'fixed' }">
<input
type="radio"
name="count-mode"
value="fixed"
:checked="countMode === 'fixed'"
@change="$emit('update:countMode', 'fixed')"
/>
<span class="count-mode-tab-label">Fixed</span>
</label>
<label class="count-mode-tab" :class="{ active: countMode === 'range' }">
<input
type="radio"
name="count-mode"
value="range"
:checked="countMode === 'range'"
@change="$emit('update:countMode', 'range')"
/>
<span class="count-mode-tab-label">Range</span>
</label>
</div>
<div class="slider-container">
<SingleSlider
v-if="countMode === 'fixed'"
:min="1"
:max="10"
:value="countFixed"
:step="1"
:default-range="{ min: 1, max: 5 }"
@update:value="$emit('update:countFixed', $event)"
/>
<DualRangeSlider
v-else
:min="1"
:max="10"
:value-min="countMin"
:value-max="countMax"
:step="1"
:default-range="{ min: 1, max: 5 }"
:allow-equal-values="true"
@update:value-min="$emit('update:countMin', $event)"
@update:value-max="$emit('update:countMax', $event)"
/>
</div>
</div>
<!-- Model Strength Range -->
<div class="setting-section">
<label class="setting-label">Model Strength Range</label>
<div class="slider-container">
<DualRangeSlider
:min="-10"
:max="10"
:value-min="modelStrengthMin"
:value-max="modelStrengthMax"
:step="0.1"
:default-range="{ min: -2, max: 3 }"
:scale-mode="'segmented'"
:segments="strengthSegments"
:allow-equal-values="true"
@update:value-min="$emit('update:modelStrengthMin', $event)"
@update:value-max="$emit('update:modelStrengthMax', $event)"
/>
</div>
</div>
<!-- Recommended Strength -->
<div class="setting-section">
<div class="section-header-with-toggle">
<label class="setting-label">
Preset Strength Scale
</label>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': useRecommendedStrength }"
@click="$emit('update:useRecommendedStrength', !useRecommendedStrength)"
role="switch"
:aria-checked="useRecommendedStrength"
title="Use scaled preset strength when enabled"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</div>
<div class="slider-container" :class="{ 'slider-container--disabled': !useRecommendedStrength }">
<DualRangeSlider
:min="0"
:max="2"
:value-min="recommendedStrengthScaleMin"
:value-max="recommendedStrengthScaleMax"
:step="0.1"
:default-range="{ min: 0.5, max: 1.0 }"
:disabled="!useRecommendedStrength"
:allow-equal-values="true"
@update:value-min="$emit('update:recommendedStrengthScaleMin', $event)"
@update:value-max="$emit('update:recommendedStrengthScaleMax', $event)"
/>
</div>
</div>
<!-- Clip Strength Range -->
<div class="setting-section">
<div class="section-header-with-toggle">
<label class="setting-label">
Clip Strength Range - {{ useCustomClipRange ? 'Custom Range' : 'Use Model Strength' }}
</label>
<button
type="button"
class="toggle-switch"
:class="{ 'toggle-switch--active': useCustomClipRange }"
@click="$emit('update:useCustomClipRange', !useCustomClipRange)"
role="switch"
:aria-checked="useCustomClipRange"
title="Use custom clip strength range when enabled, otherwise use model strength"
>
<span class="toggle-switch__track"></span>
<span class="toggle-switch__thumb"></span>
</button>
</div>
<div class="slider-container" :class="{ 'slider-container--disabled': isClipStrengthDisabled }">
<DualRangeSlider
:min="-10"
:max="10"
:value-min="clipStrengthMin"
:value-max="clipStrengthMax"
:step="0.1"
:default-range="{ min: -1, max: 2 }"
:scale-mode="'segmented'"
:segments="strengthSegments"
:disabled="isClipStrengthDisabled"
:allow-equal-values="true"
@update:value-min="$emit('update:clipStrengthMin', $event)"
@update:value-max="$emit('update:clipStrengthMax', $event)"
/>
</div>
</div>
<!-- Roll Mode - New 3-button design -->
<div class="setting-section">
<label class="setting-label">Roll Mode</label>
<div class="roll-buttons-with-tooltip">
<div class="roll-buttons">
<button
class="roll-button"
:class="{ selected: rollMode === 'fixed' }"
:disabled="isRolling"
@click="$emit('generate-fixed')"
>
<svg class="roll-button__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="20" rx="5"/>
<circle cx="12" cy="12" r="3"/>
<circle cx="6" cy="8" r="1.5"/>
<circle cx="18" cy="16" r="1.5"/>
</svg>
<span class="roll-button__text">Generate Fixed</span>
</button>
<button
class="roll-button"
:class="{ selected: rollMode === 'always' }"
:disabled="isRolling"
@click="$emit('always-randomize')"
>
<svg class="roll-button__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
<path d="M21 3v5h-5"/>
<circle cx="12" cy="12" r="3"/>
<circle cx="6" cy="8" r="1.5"/>
<circle cx="18" cy="16" r="1.5"/>
</svg>
<span class="roll-button__text">Always Randomize</span>
</button>
<button
class="roll-button"
:class="{ selected: rollMode === 'fixed' && canReuseLast && areLorasEqual(currentLoras, lastUsed) }"
:disabled="!canReuseLast"
@mouseenter="showTooltip = true"
@mouseleave="showTooltip = false"
@click="$emit('reuse-last')"
>
<svg class="roll-button__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 14 4 9l5-5"/>
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5v0a5.5 5.5 0 0 1-5.5 5.5H11"/>
</svg>
<span class="roll-button__text">Reuse Last</span>
</button>
</div>
<!-- Last Used Preview Tooltip -->
<Transition name="tooltip">
<LastUsedPreview
v-if="showTooltip && lastUsed && lastUsed.length > 0"
:loras="lastUsed"
/>
</Transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import LastUsedPreview from './LastUsedPreview.vue'
import SingleSlider from '../shared/SingleSlider.vue'
import DualRangeSlider from '../shared/DualRangeSlider.vue'
import type { LoraEntry } from '../../composables/types'
const strengthSegments = [
{ min: -10, max: -2, widthPercent: 20 },
{ min: -2, max: 2, widthPercent: 60, wheelStepMultiplier: 0.5 },
{ min: 2, max: 10, widthPercent: 20 }
]
defineProps<{
countMode: 'fixed' | 'range'
countFixed: number
countMin: number
countMax: number
modelStrengthMin: number
modelStrengthMax: number
useCustomClipRange: boolean
clipStrengthMin: number
clipStrengthMax: number
rollMode: 'fixed' | 'always'
isRolling: boolean
isClipStrengthDisabled: boolean
lastUsed: LoraEntry[] | null
currentLoras: LoraEntry[]
canReuseLast: boolean
useRecommendedStrength: boolean
recommendedStrengthScaleMin: number
recommendedStrengthScaleMax: number
}>()
defineEmits<{
'update:countMode': [value: 'fixed' | 'range']
'update:countFixed': [value: number]
'update:countMin': [value: number]
'update:countMax': [value: number]
'update:modelStrengthMin': [value: number]
'update:modelStrengthMax': [value: number]
'update:useCustomClipRange': [value: boolean]
'update:clipStrengthMin': [value: number]
'update:clipStrengthMax': [value: number]
'update:rollMode': [value: 'fixed' | 'always']
'update:useRecommendedStrength': [value: boolean]
'update:recommendedStrengthScaleMin': [value: number]
'update:recommendedStrengthScaleMax': [value: number]
'generate-fixed': []
'always-randomize': []
'reuse-last': []
}>()
const showTooltip = ref(false)
const areLorasEqual = (a: LoraEntry[] | null, b: LoraEntry[] | null): boolean => {
if (!a || !b) return false
if (a.length !== b.length) return false
const sortedA = [...a].sort((x, y) => x.name.localeCompare(y.name))
const sortedB = [...b].sort((x, y) => x.name.localeCompare(y.name))
return sortedA.every((lora, i) =>
lora.name === sortedB[i].name &&
lora.strength === sortedB[i].strength &&
lora.clipStrength === sortedB[i].clipStrength
)
}
</script>
<style scoped>
.randomizer-settings {
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
color: #e4e4e7;
}
.settings-header {
margin-bottom: 8px;
}
.settings-title {
font-size: 10px;
font-weight: 600;
letter-spacing: 0.05em;
color: var(--fg-color, #fff);
opacity: 0.6;
margin: 0;
text-transform: uppercase;
}
.setting-section {
margin-bottom: 6px;
}
.setting-label {
font-size: 13px;
font-weight: 500;
color: rgba(226, 232, 240, 0.8);
display: block;
margin-bottom: 8px;
}
.section-header-with-toggle {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
}
.section-header-with-toggle .setting-label {
margin-bottom: 4px;
}
/* Count Mode Tabs */
.count-mode-tabs {
display: flex;
background: rgba(26, 32, 44, 0.9);
border: 1px solid rgba(226, 232, 240, 0.2);
border-radius: 6px;
overflow: hidden;
margin-bottom: 8px;
}
.count-mode-tab {
flex: 1;
position: relative;
padding: 8px 12px;
text-align: center;
cursor: pointer;
transition: all 0.2s ease;
}
.count-mode-tab input[type="radio"] {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.count-mode-tab-label {
font-size: 13px;
font-weight: 500;
color: rgba(226, 232, 240, 0.7);
transition: all 0.2s ease;
}
.count-mode-tab:hover .count-mode-tab-label {
color: rgba(226, 232, 240, 0.9);
}
.count-mode-tab.active .count-mode-tab-label {
color: rgba(191, 219, 254, 1);
font-weight: 600;
}
.count-mode-tab.active {
background: rgba(66, 153, 225, 0.2);
}
.count-mode-tab.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
background: rgba(66, 153, 225, 0.9);
}
.slider-container {
background: rgba(26, 32, 44, 0.9);
border: 1px solid rgba(226, 232, 240, 0.2);
border-radius: 6px;
padding: 6px;
}
.slider-container--disabled {
opacity: 0.5;
pointer-events: none;
}
/* Toggle Switch (same style as LicenseSection) */
.toggle-switch {
position: relative;
width: 36px;
height: 20px;
padding: 0;
background: transparent;
border: none;
cursor: pointer;
}
.toggle-switch__track {
position: absolute;
inset: 0;
background: var(--comfy-input-bg, #333);
border: 1px solid var(--border-color, #444);
border-radius: 10px;
transition: all 0.2s;
}
.toggle-switch--active .toggle-switch__track {
background: rgba(66, 153, 225, 0.3);
border-color: rgba(66, 153, 225, 0.6);
}
.toggle-switch__thumb {
position: absolute;
top: 3px;
left: 2px;
width: 14px;
height: 14px;
background: var(--fg-color, #fff);
border-radius: 50%;
transition: all 0.2s;
opacity: 0.6;
}
.toggle-switch--active .toggle-switch__thumb {
transform: translateX(16px);
background: #4299e1;
opacity: 1;
}
.toggle-switch:hover .toggle-switch__thumb {
opacity: 1;
}
/* Roll buttons with tooltip container */
.roll-buttons-with-tooltip {
position: relative;
}
/* Roll buttons container */
.roll-buttons {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 6px;
}
.roll-button {
padding: 6px 8px;
background: rgba(30, 30, 36, 0.6);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6px;
color: #e4e4e7;
font-size: 12px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.roll-button:hover:not(:disabled) {
background: rgba(66, 153, 225, 0.2);
border-color: rgba(66, 153, 225, 0.4);
color: #bfdbfe;
}
.roll-button.selected {
background: rgba(66, 153, 225, 0.3);
border-color: rgba(66, 153, 225, 0.6);
color: #e4e4e7;
box-shadow: 0 0 0 1px rgba(66, 153, 225, 0.3);
}
.roll-button:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.roll-button__icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.roll-button__text {
font-size: 12px;
text-align: center;
line-height: 1.2;
}
/* Tooltip transitions */
.tooltip-enter-active,
.tooltip-leave-active {
transition: opacity 0.15s ease, transform 0.15s ease;
}
.tooltip-enter-from,
.tooltip-leave-to {
opacity: 0;
transform: translateY(4px);
}
</style>