Add LoRA activation words and hide LoRAs that do not match the current engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -32,6 +32,12 @@
|
||||
<span class="block leading-none" aria-hidden="true">⋮⋮</span>
|
||||
</button>
|
||||
<span class="min-w-0 truncate text-zinc-200">{{ loraDisplayName(item.name) }}</span>
|
||||
<LoraTriggerTip
|
||||
:text="triggerFor(item.name)"
|
||||
:aria-label="'Activation for ' + loraDisplayName(item.name)"
|
||||
@insert="emit('insert-trigger', $event)"
|
||||
@save="saveTrigger(item.name, $event)"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -99,6 +105,7 @@ import {
|
||||
clampLoraStrength,
|
||||
loraDisplayName,
|
||||
loraIdentityKey,
|
||||
loraTriggerFor,
|
||||
makeLoraStackItem,
|
||||
type LoraStackItem
|
||||
} from '~/utils/loras'
|
||||
@@ -112,6 +119,7 @@ const props = withDefaults(defineProps<{
|
||||
compact?: boolean
|
||||
disabled?: boolean
|
||||
clipStrength?: boolean
|
||||
triggers?: Record<string, string>
|
||||
}>(), {
|
||||
label: 'LoRA',
|
||||
emptyLabel: 'None',
|
||||
@@ -119,9 +127,15 @@ const props = withDefaults(defineProps<{
|
||||
missingLabel: 'Wake GPU to load LoRAs',
|
||||
compact: false,
|
||||
disabled: false,
|
||||
clipStrength: true
|
||||
clipStrength: true,
|
||||
triggers: () => ({})
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'insert-trigger': [text: string]
|
||||
'update-trigger': [name: string, trigger: string]
|
||||
}>()
|
||||
|
||||
const model = defineModel<LoraStackItem[]>({ default: () => [] })
|
||||
|
||||
const draggingIndex = ref<number | null>(null)
|
||||
@@ -201,4 +215,12 @@ function setStrength(index: number, key: 'strengthModel' | 'strengthClip', value
|
||||
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 saveTrigger(name: string, trigger: string) {
|
||||
emit('update-trigger', name, trigger)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<template>
|
||||
<div class="group relative shrink-0" @wheel.prevent="onIconWheel">
|
||||
<button
|
||||
type="button"
|
||||
class="relative z-10 flex h-6 w-6 items-center justify-center rounded-full"
|
||||
:class="text ? 'text-amber-200/80 hover:text-amber-100' : 'text-zinc-600 hover:text-zinc-400'"
|
||||
:aria-label="ariaLabel"
|
||||
:title="text ? 'Insert activation word' : 'Set activation word'"
|
||||
@click.stop="onIconClick"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-4 w-4">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4M12 8h.01" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="invisible absolute left-0 top-full z-30 -mt-1 pt-2 group-hover:visible group-focus-within:visible">
|
||||
<div
|
||||
ref="panel"
|
||||
class="pointer-events-auto w-64 max-w-[calc(100vw-2rem)] overflow-hidden rounded-xl border border-white/10 bg-zinc-950 p-3 text-xs leading-relaxed text-zinc-300 shadow-2xl"
|
||||
@click.stop
|
||||
>
|
||||
<p class="text-[10px] uppercase tracking-wide text-zinc-500">Activation</p>
|
||||
<p v-if="text" class="mt-1 max-h-24 overflow-y-auto whitespace-pre-wrap break-words">{{ text }}</p>
|
||||
<p v-else class="mt-1 text-zinc-500">No word yet. Type one, then insert it into the prompt.</p>
|
||||
<input
|
||||
ref="input"
|
||||
v-model="draft"
|
||||
class="mt-2 w-full rounded-lg border border-white/10 bg-zinc-900 px-2 py-1 text-xs outline-none ring-amber-300/40 focus:ring-2"
|
||||
maxlength="240"
|
||||
placeholder="ohwx woman"
|
||||
@keydown.enter.prevent="save"
|
||||
>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="text-[11px] text-amber-200 hover:text-amber-100 disabled:text-zinc-600"
|
||||
:disabled="!text"
|
||||
@click="emit('insert', text)"
|
||||
>
|
||||
Insert into prompt
|
||||
</button>
|
||||
<button type="button" class="text-[11px] text-zinc-400 hover:text-white" @click="save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
text: string
|
||||
ariaLabel: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
insert: [text: string]
|
||||
save: [text: string]
|
||||
}>()
|
||||
|
||||
const panel = ref<HTMLElement | null>(null)
|
||||
const input = ref<HTMLInputElement | null>(null)
|
||||
const draft = ref(props.text)
|
||||
|
||||
watch(() => props.text, (value) => {
|
||||
draft.value = value
|
||||
})
|
||||
|
||||
function onIconClick() {
|
||||
if (props.text) emit('insert', props.text)
|
||||
else input.value?.focus()
|
||||
}
|
||||
|
||||
function save() {
|
||||
emit('save', draft.value)
|
||||
}
|
||||
|
||||
function onIconWheel(event: WheelEvent) {
|
||||
const el = panel.value
|
||||
if (!el) return
|
||||
el.scrollTop += event.deltaY
|
||||
}
|
||||
</script>
|
||||
@@ -159,6 +159,7 @@
|
||||
<p class="mt-1 text-sm text-zinc-400">
|
||||
Short names used in auto-generated output titles. Leave blank to use the file stem
|
||||
(<span class="text-zinc-300">XAIgen-cline</span> becomes <span class="text-zinc-300">cline</span>).
|
||||
Activation is the word or phrase the <span class="text-zinc-300">i</span> on a LoRA slider inserts into the prompt.
|
||||
</p>
|
||||
</div>
|
||||
<p v-if="loraRowsLoading" class="text-sm text-zinc-500">Loading LoRAs…</p>
|
||||
@@ -172,13 +173,22 @@
|
||||
<p class="truncate text-sm text-zinc-200">{{ row.file }}</p>
|
||||
<p class="text-[10px] uppercase tracking-wide text-zinc-600">{{ row.kind === 'both' ? 'Image · Video' : row.kind }}</p>
|
||||
</div>
|
||||
<input
|
||||
v-model="row.draft"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm sm:w-44"
|
||||
:placeholder="row.placeholder"
|
||||
maxlength="24"
|
||||
@change="saveLoraAlias(row)"
|
||||
>
|
||||
<div class="flex w-full flex-col gap-2 sm:w-56">
|
||||
<input
|
||||
v-model="row.draft"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
|
||||
:placeholder="row.placeholder"
|
||||
maxlength="24"
|
||||
@change="saveLoraAlias(row)"
|
||||
>
|
||||
<input
|
||||
v-model="row.trigger"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
|
||||
placeholder="Activation word"
|
||||
maxlength="240"
|
||||
@change="saveLoraTrigger(row)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -279,7 +289,7 @@ const newPresetName = ref('')
|
||||
const newPresetPrompt = ref('')
|
||||
const promptPresets = ref<Array<{ id: string; name: string; description?: string; builtin?: boolean; systemPrompt?: string }>>([])
|
||||
const loraRowsLoading = ref(true)
|
||||
const loraRows = ref<Array<{ key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; placeholder: string }>>([])
|
||||
const loraRows = ref<Array<{ key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; trigger: string; placeholder: string }>>([])
|
||||
const loginUser = ref(props.username || '')
|
||||
const loginCurrent = ref('')
|
||||
const loginNext = ref('')
|
||||
@@ -462,9 +472,10 @@ async function loadLoraKeys() {
|
||||
loraRowsLoading.value = true
|
||||
try {
|
||||
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
|
||||
const library = await $fetch<{ loraAliases?: Record<string, string> }>('/api/library?light=1').catch(() => ({ loraAliases: {} }))
|
||||
const library = await $fetch<{ loraAliases?: Record<string, string>; loraTriggers?: Record<string, string> }>('/api/library?light=1').catch(() => ({ loraAliases: {}, loraTriggers: {} }))
|
||||
const aliases = library.loraAliases || {}
|
||||
const map = new Map<string, { key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; placeholder: string }>()
|
||||
const triggers = library.loraTriggers || {}
|
||||
const map = new Map<string, { key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; trigger: string; placeholder: string }>()
|
||||
const add = (name: string, kind: 'image' | 'video') => {
|
||||
if (isFilenameSystemLora(name)) return
|
||||
const key = loraIdentityKey(name)
|
||||
@@ -479,6 +490,7 @@ async function loadLoraKeys() {
|
||||
file: loraDisplayName(name),
|
||||
kind,
|
||||
draft: aliases[key] || '',
|
||||
trigger: triggers[key] || '',
|
||||
placeholder: defaultLoraFilenameKey(name)
|
||||
})
|
||||
}
|
||||
@@ -501,6 +513,17 @@ async function saveLoraAlias(row: { key: string; draft: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveLoraTrigger(row: { key: string; trigger: string }) {
|
||||
try {
|
||||
emit('updated', await $fetch('/api/library/lora-triggers', {
|
||||
method: 'PUT',
|
||||
body: { key: row.key, trigger: row.trigger }
|
||||
}))
|
||||
} catch (error: any) {
|
||||
emit('error', error?.data?.statusMessage || 'Could not save LoRA activation word')
|
||||
}
|
||||
}
|
||||
|
||||
async function addPreset() {
|
||||
const name = newPresetName.value.trim()
|
||||
const systemPrompt = newPresetPrompt.value.trim()
|
||||
|
||||
Reference in New Issue
Block a user