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:
@@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="flex min-w-0 flex-wrap items-end gap-2">
|
||||
<label class="min-w-[9rem] flex-1 text-xs">
|
||||
<span class="mb-1 block text-zinc-500">Preset</span>
|
||||
<select
|
||||
:value="selectedId"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-2 py-1 text-xs outline-none ring-amber-300/40 focus:ring-2"
|
||||
:disabled="disabled"
|
||||
@change="onPick"
|
||||
>
|
||||
<option value="">Load…</option>
|
||||
<option
|
||||
v-for="preset in presets"
|
||||
:key="preset.id"
|
||||
:value="preset.id"
|
||||
>{{ preset.name }}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="w-32 text-xs">
|
||||
<span class="mb-1 block text-zinc-500">Name</span>
|
||||
<input
|
||||
:value="name"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-2 py-1 text-xs outline-none ring-amber-300/40 placeholder:text-zinc-600 focus:ring-2"
|
||||
placeholder="Save as…"
|
||||
maxlength="80"
|
||||
:disabled="disabled"
|
||||
@input="emit('update:name', ($event.target as HTMLInputElement).value)"
|
||||
@keydown.enter.prevent="emit('save')"
|
||||
>
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-xl border border-white/10 px-2 py-1 text-xs text-zinc-300 hover:border-amber-300/50 hover:text-white disabled:opacity-50"
|
||||
:disabled="disabled || !name.trim()"
|
||||
@click="emit('save')"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
v-if="selectedId"
|
||||
type="button"
|
||||
class="rounded-xl border border-white/10 px-2 py-1 text-xs text-zinc-500 hover:border-red-400/40 hover:text-red-300 disabled:opacity-50"
|
||||
:disabled="disabled"
|
||||
@click="emit('remove')"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GenerationPreset } from '~/utils/generationPresets'
|
||||
|
||||
defineProps<{
|
||||
presets: GenerationPreset[]
|
||||
selectedId: string
|
||||
name: string
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
load: [id: string]
|
||||
save: []
|
||||
remove: []
|
||||
'update:name': [value: string]
|
||||
'update:selectedId': [value: string]
|
||||
}>()
|
||||
|
||||
function onPick(event: Event) {
|
||||
const id = (event.target as HTMLSelectElement).value
|
||||
emit('update:selectedId', id)
|
||||
if (id) emit('load', id)
|
||||
}
|
||||
</script>
|
||||
@@ -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>
|
||||
@@ -127,7 +127,7 @@
|
||||
<div>
|
||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Prompt helper</h3>
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Instruction sets sent to Sidecar Qwen VL with your still and draft. Use these later for multi-shot video scripts.
|
||||
Instruction sets sent to Beast Qwen VL with your still and draft. Use these later for multi-shot video scripts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user