Files
aigen/components/LoraSelect.vue
T

236 lines
7.4 KiB
Vue

<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>
<p v-if="model.length > 1" class="mb-1 text-[11px] text-zinc-500">Drag to set chain order. Top applies first.</p>
<ul v-if="model.length" class="space-y-2">
<li
v-for="(item, index) in model"
:key="item.name"
class="rounded-xl border bg-zinc-950/80"
:class="[
compact ? 'px-2 py-1.5' : 'px-3 py-2',
dragOverIndex === index ? 'border-amber-300/50' : 'border-white/10',
draggingIndex === index ? 'opacity-60' : ''
]"
@dragover.prevent="onDragOver(index)"
@drop.prevent="onDrop(index)"
@dragleave="onDragLeave(index)"
>
<div class="flex items-center justify-between gap-2">
<div class="flex min-w-0 items-center gap-2">
<button
v-if="model.length > 1"
type="button"
class="shrink-0 cursor-grab touch-none text-zinc-500 hover:text-zinc-300 active:cursor-grabbing disabled:cursor-not-allowed disabled:opacity-40"
:disabled="disabled"
draggable="true"
title="Drag to reorder"
aria-label="Drag to reorder"
@dragstart="onDragStart(index, $event)"
@dragend="onDragEnd"
>
<span class="block leading-none" aria-hidden="true">⋮⋮</span>
</button>
<span class="min-w-0 truncate text-zinc-200">{{ labelFor(item.name) }}</span>
<LoraTriggerTip
:text="triggerFor(item.name)"
:aria-label="'Activation for ' + labelFor(item.name)"
@insert="emit('insert-trigger', $event)"
@save="saveTrigger(item.name, $event)"
/>
</div>
<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 flex flex-wrap gap-3">
<Thumbwheel
:model-value="item.strengthModel"
label="Model"
:lock-key="`lora-model:${item.name}`"
:min="LORA_STRENGTH_MIN"
:max="LORA_STRENGTH_MAX"
:step="LORA_STRENGTH_STEP"
compact
:disabled="disabled"
@update:model-value="setStrength(index, 'strengthModel', $event)"
/>
<Thumbwheel
:model-value="item.strengthClip"
label="CLIP"
:lock-key="`lora-clip:${item.name}`"
:min="LORA_STRENGTH_MIN"
:max="LORA_STRENGTH_MAX"
:step="LORA_STRENGTH_STEP"
compact
:disabled="disabled || !clipStrength"
@update:model-value="setStrength(index, 'strengthClip', $event)"
/>
</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="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 || !availableNames.length"
:value="''"
@change="onAdd"
>
<option value="">{{ selectPlaceholder }}</option>
<option
v-for="name in availableNames"
:key="name"
:value="name"
>{{ labelFor(name) }}</option>
</select>
</div>
</template>
<script setup lang="ts">
import {
LORA_STACK_MAX,
LORA_STRENGTH_MAX,
LORA_STRENGTH_MIN,
LORA_STRENGTH_STEP,
clampLoraStrength,
loraFriendlyName,
loraIdentityKey,
loraTriggerFor,
makeLoraStackItem,
type LoraStackItem
} from '~/utils/loras'
const props = withDefaults(defineProps<{
names: string[]
label?: string
emptyLabel?: string
addLabel?: string
missingLabel?: string
compact?: boolean
disabled?: boolean
clipStrength?: boolean
triggers?: Record<string, string>
aliases?: Record<string, string>
}>(), {
label: 'LoRA',
emptyLabel: 'None',
addLabel: 'Add LoRA',
missingLabel: 'Wake GPU to load LoRAs',
compact: false,
disabled: false,
clipStrength: true,
triggers: () => ({}),
aliases: () => ({})
})
const emit = defineEmits<{
'insert-trigger': [text: string]
'update-trigger': [name: string, trigger: string]
}>()
const model = defineModel<LoraStackItem[]>({ default: () => [] })
const draggingIndex = ref<number | null>(null)
const dragOverIndex = ref<number | null>(null)
const availableNames = computed(() => {
const taken = new Set(model.value.map(item => loraIdentityKey(item.name)))
return props.names
.filter(name => !taken.has(loraIdentityKey(name)))
.slice()
.sort((a, b) => labelFor(a).localeCompare(labelFor(b), undefined, { sensitivity: 'base' }))
})
const selectPlaceholder = computed(() => {
if (!availableNames.value.length) {
return props.names.length ? 'All listed LoRAs are in the chain' : props.missingLabel
}
return model.value.length ? props.addLabel : props.emptyLabel
})
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 => loraIdentityKey(entry.name)))
if (taken.has(loraIdentityKey(item.name))) return
model.value = [...model.value, item]
}
function removeAt(index: number) {
model.value = model.value.filter((_, i) => i !== index)
}
function moveItem(from: number, to: number) {
if (from === to || from < 0 || to < 0 || to >= model.value.length) return
const next = [...model.value]
const [item] = next.splice(from, 1)
next.splice(to, 0, item)
model.value = next
}
function onDragStart(index: number, event: DragEvent) {
if (props.disabled) {
event.preventDefault()
return
}
draggingIndex.value = index
dragOverIndex.value = index
event.dataTransfer?.setData('text/plain', String(index))
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
}
function onDragOver(index: number) {
if (draggingIndex.value === null || props.disabled) return
dragOverIndex.value = index
}
function onDragLeave(index: number) {
if (dragOverIndex.value === index) dragOverIndex.value = null
}
function onDrop(index: number) {
if (draggingIndex.value === null || props.disabled) return
moveItem(draggingIndex.value, index)
draggingIndex.value = null
dragOverIndex.value = null
}
function onDragEnd() {
draggingIndex.value = null
dragOverIndex.value = null
}
function setStrength(index: number, key: 'strengthModel' | 'strengthClip', value: number) {
const next = clampLoraStrength(value)
model.value = model.value.map((item, i) => i === index ? { ...item, [key]: next } : item)
}
function triggerFor(name: string) {
return loraTriggerFor(name, props.triggers)
}
function labelFor(name: string) {
return loraFriendlyName(name, props.aliases)
}
function saveTrigger(name: string, trigger: string) {
emit('update-trigger', name, trigger)
}
</script>