Add LoRA stacks with per-item strengths and named video/image generation presets so setups survive reload without mixing Klein onto MiniMax. Keep LTX Pink Cherry and xAIGen LoRAs on xAIGen, and run Recommend on Beast.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-28 15:24:44 -05:00
co-authored by Cursor
parent 0af3cddb14
commit f8db0d626a
36 changed files with 1767 additions and 246 deletions
+140
View File
@@ -0,0 +1,140 @@
<template>
<div class="block min-w-0" :class="compact ? 'text-xs' : 'text-sm'">
<span v-if="label" class="mb-1 block text-zinc-400">{{ label }}</span>
<ul v-if="model.length" class="space-y-2">
<li
v-for="(item, index) in model"
:key="item.name"
class="rounded-xl border border-white/10 bg-zinc-950/80"
:class="compact ? 'px-2 py-1.5' : 'px-3 py-2'"
>
<div class="flex items-center justify-between gap-2">
<span class="min-w-0 truncate text-zinc-200">{{ loraDisplayName(item.name) }}</span>
<button
type="button"
class="shrink-0 text-[11px] text-zinc-500 hover:text-red-300 disabled:opacity-50"
:disabled="disabled"
@click="removeAt(index)"
>
Remove
</button>
</div>
<div class="mt-1.5 grid grid-cols-2 gap-2">
<label class="block min-w-0">
<span class="mb-0.5 flex justify-between text-[10px] uppercase tracking-wider text-zinc-500">
<span>Model</span>
<span class="tabular-nums normal-case text-zinc-400">{{ formatLoraStrength(item.strengthModel) }}</span>
</span>
<input
type="range"
class="aigen-slider w-full"
:min="LORA_STRENGTH_MIN"
:max="LORA_STRENGTH_MAX"
:step="LORA_STRENGTH_STEP"
:value="item.strengthModel"
:disabled="disabled"
@input="setStrength(index, 'strengthModel', $event)"
>
</label>
<label class="block min-w-0" :class="{ 'opacity-50': !clipStrength }">
<span class="mb-0.5 flex justify-between text-[10px] uppercase tracking-wider text-zinc-500">
<span>CLIP</span>
<span class="tabular-nums normal-case text-zinc-400">{{ formatLoraStrength(item.strengthClip) }}</span>
</span>
<input
type="range"
class="aigen-slider w-full"
:min="LORA_STRENGTH_MIN"
:max="LORA_STRENGTH_MAX"
:step="LORA_STRENGTH_STEP"
:value="item.strengthClip"
:disabled="disabled || !clipStrength"
@input="setStrength(index, 'strengthClip', $event)"
>
</label>
</div>
</li>
</ul>
<p v-if="!clipStrength && model.length" class="mt-1 text-[11px] text-zinc-500">CLIP unused on this graph — model strength only.</p>
<select
v-if="availableNames.length && model.length < LORA_STACK_MAX"
class="w-full rounded-xl border border-white/10 bg-zinc-950 outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-50"
:class="[
compact ? 'px-2 py-1 text-xs' : 'px-3 py-2 text-sm',
model.length ? 'mt-2' : ''
]"
:disabled="disabled"
:value="''"
@change="onAdd"
>
<option value="">{{ model.length ? addLabel : emptyLabel }}</option>
<option
v-for="name in availableNames"
:key="name"
:value="name"
>{{ loraDisplayName(name) }}</option>
</select>
<p v-else-if="!model.length" class="text-[11px] text-zinc-500">{{ emptyLabel }}</p>
</div>
</template>
<script setup lang="ts">
import {
LORA_STACK_MAX,
LORA_STRENGTH_MAX,
LORA_STRENGTH_MIN,
LORA_STRENGTH_STEP,
clampLoraStrength,
formatLoraStrength,
loraDisplayName,
makeLoraStackItem,
type LoraStackItem
} from '~/utils/loras'
const props = withDefaults(defineProps<{
names: string[]
label?: string
emptyLabel?: string
addLabel?: string
compact?: boolean
disabled?: boolean
clipStrength?: boolean
}>(), {
label: 'LoRA',
emptyLabel: 'None',
addLabel: 'Add LoRA',
compact: false,
disabled: false,
clipStrength: true
})
const model = defineModel<LoraStackItem[]>({ default: () => [] })
const availableNames = computed(() => {
const taken = new Set(model.value.map(item => item.name.replace(/\\/g, '/').toLowerCase()))
return props.names.filter(name => !taken.has(name.replace(/\\/g, '/').toLowerCase()))
})
function onAdd(event: Event) {
const name = (event.target as HTMLSelectElement).value
;(event.target as HTMLSelectElement).value = ''
addLora(name)
}
function addLora(name: string) {
const item = makeLoraStackItem(name)
if (!item || model.value.length >= LORA_STACK_MAX) return
const taken = new Set(model.value.map(entry => entry.name.replace(/\\/g, '/').toLowerCase()))
if (taken.has(item.name.replace(/\\/g, '/').toLowerCase())) return
model.value = [...model.value, item]
}
function removeAt(index: number) {
model.value = model.value.filter((_, i) => i !== index)
}
function setStrength(index: number, key: 'strengthModel' | 'strengthClip', event: Event) {
const value = clampLoraStrength((event.target as HTMLInputElement).value)
model.value = model.value.map((item, i) => i === index ? { ...item, [key]: value } : item)
}
</script>