Show one settings panel at a time and add a LoRA filter.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-31 21:31:04 -05:00
co-authored by Cursor
parent 1dba62bf54
commit 85dc378712
2 changed files with 104 additions and 31 deletions
+103 -30
View File
@@ -1,24 +1,28 @@
<template>
<div class="lg:grid lg:grid-cols-[13rem_minmax(0,1fr)] lg:items-start lg:gap-10">
<nav class="mb-6 flex flex-wrap gap-2 lg:sticky lg:top-24 lg:mb-0 lg:flex-col lg:gap-1">
<a
<button
v-for="item in navItems"
:key="item.id"
:href="`#${item.id}`"
class="rounded-full border border-white/10 px-3 py-1.5 text-xs text-zinc-400 hover:border-amber-300/40 hover:text-amber-100 lg:rounded-xl lg:text-sm"
>{{ item.label }}</a>
type="button"
class="rounded-full border px-3 py-1.5 text-left text-xs lg:rounded-xl lg:text-sm"
:class="activePanel === item.id
? 'border-amber-300/50 bg-amber-400/10 text-amber-100'
: 'border-white/10 text-zinc-400 hover:border-amber-300/40 hover:text-amber-100'"
@click="activePanel = item.id"
>{{ item.label }}</button>
</nav>
<div class="space-y-10">
<div class="min-w-0 space-y-4">
<p
v-if="notice"
class="rounded-2xl border px-4 py-3 text-sm"
:class="noticeError ? 'border-red-400/30 bg-red-950/40 text-red-100' : 'border-emerald-400/20 bg-emerald-950/30 text-emerald-100'"
>{{ notice }}</p>
<section id="settings-gpu" class="scroll-mt-24">
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">GPU</h3>
<div class="mt-2 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
<section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6">
<h2 class="font-display text-xl font-bold">GPU</h2>
<div class="mt-4 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
<p class="text-sm text-zinc-400">
Stops Comfy on the desktop GPU so it drops out of VRAM. The PC stays on. Generate or Wake GPU will start it again.
</p>
@@ -46,9 +50,9 @@
</div>
</section>
<section id="settings-storage" class="scroll-mt-24">
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Storage</h3>
<div class="mt-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
<section v-if="activePanel === 'storage'" class="glass rounded-3xl p-5 sm:p-6">
<h2 class="font-display text-xl font-bold">Storage</h2>
<div class="mt-4 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
<p v-if="storageLoading" class="text-sm text-zinc-500">Measuring…</p>
<p v-else-if="storageError" class="text-sm text-zinc-500">{{ storageError }}</p>
<template v-else-if="storage">
@@ -59,8 +63,8 @@
</div>
</section>
<section v-if="authMode === 'password'" id="settings-login" class="scroll-mt-24 space-y-3">
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Site login</h3>
<section v-if="activePanel === 'login' && authMode === 'password'" class="glass space-y-3 rounded-3xl p-5 sm:p-6">
<h2 class="font-display text-xl font-bold">Site login</h2>
<p class="text-sm text-zinc-400">Username and password are stored encrypted on this server. Changing them does not move the library.</p>
<form class="grid gap-2 sm:grid-cols-2" @submit.prevent="saveLogin">
<input v-model="loginUser" class="rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm" placeholder="Username" autocomplete="username">
@@ -70,9 +74,9 @@
</form>
</section>
<section id="settings-folders" class="scroll-mt-24 space-y-3">
<section v-if="activePanel === 'folders'" class="glass space-y-3 rounded-3xl p-5 sm:p-6">
<div class="flex items-center justify-between gap-3">
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Folders</h3>
<h2 class="font-display text-xl font-bold">Folders</h2>
<span class="text-xs text-zinc-500">{{ folders.length }} total</span>
</div>
@@ -159,19 +163,41 @@
</form>
</section>
<section id="settings-loras" class="scroll-mt-24 space-y-3">
<section v-if="activePanel === 'loras'" class="glass space-y-4 rounded-3xl p-5 sm:p-6">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">LoRA filename keys</h3>
<h2 class="font-display text-xl font-bold">LoRA filename keys</h2>
<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>
<div class="flex flex-col gap-2 sm:flex-row sm:items-center">
<input
v-model="loraQuery"
type="search"
class="min-w-0 flex-1 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
placeholder="Filter by file, key, or activation…"
>
<div class="inline-flex shrink-0 rounded-full border border-white/10 p-0.5">
<button
v-for="option in loraKindOptions"
:key="option.id"
type="button"
class="rounded-full px-2.5 py-1.5 text-[11px]"
:class="loraKind === option.id ? 'bg-amber-400/15 text-amber-100' : 'text-zinc-500 hover:text-zinc-300'"
@click="loraKind = option.id"
>{{ option.label }}</button>
</div>
</div>
<p v-if="loraRowsLoading" class="text-sm text-zinc-500">Loading LoRAs…</p>
<p v-else-if="!loraRows.length" class="text-sm text-zinc-500">No LoRA files found on Comfy yet.</p>
<p v-else class="text-xs text-zinc-500">
Showing {{ filteredLoraRows.length }} of {{ loraRows.length }}
</p>
<p v-if="!loraRowsLoading && loraRows.length && !filteredLoraRows.length" class="text-sm text-zinc-500">No LoRAs match that filter.</p>
<div
v-for="row in loraRows"
v-for="row in filteredLoraRows"
:key="row.key"
class="flex flex-col gap-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3 sm:flex-row sm:items-center"
>
@@ -198,9 +224,9 @@
</div>
</section>
<section id="settings-keeps" class="scroll-mt-24 space-y-4">
<section v-if="activePanel === 'keeps'" class="glass space-y-4 rounded-3xl p-5 sm:p-6">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Keep prompts</h3>
<h2 class="font-display text-xl font-bold">Keep prompts</h2>
<p class="mt-1 text-sm text-zinc-400">
Saved snippets for the + button next to the studio prompt. Video and image lists are separate.
</p>
@@ -255,9 +281,9 @@
</div>
</section>
<section id="settings-prompts" class="scroll-mt-24 space-y-3">
<section v-if="activePanel === 'prompts'" class="glass space-y-3 rounded-3xl p-5 sm:p-6">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Prompt helper</h3>
<h2 class="font-display text-xl font-bold">Prompt helper</h2>
<p class="mt-1 text-sm text-zinc-400">
Instruction sets sent to Beast Qwen VL with your still and draft. Use these later for multi-shot video scripts.
</p>
@@ -342,6 +368,12 @@ export interface SettingsFolder {
unlocked: boolean
}
type SettingsPanel = 'gpu' | 'storage' | 'login' | 'folders' | 'loras' | 'keeps' | 'prompts'
type LoraKindFilter = 'all' | 'image' | 'video'
const route = useRoute()
const router = useRouter()
const folders = ref<SettingsFolder[]>([])
const authMode = ref('')
const userName = ref('')
@@ -349,17 +381,36 @@ const notice = ref('')
const noticeError = ref(false)
let noticeTimer: ReturnType<typeof setTimeout> | null = null
const panelIds: SettingsPanel[] = ['gpu', 'storage', 'login', 'folders', 'loras', 'keeps', 'prompts']
function parsePanel(raw: unknown): SettingsPanel {
const value = String(raw || '')
return panelIds.includes(value as SettingsPanel) ? value as SettingsPanel : 'gpu'
}
const activePanel = computed({
get() {
const id = parsePanel(route.query.panel)
if (id === 'login' && authMode.value && authMode.value !== 'password') return 'gpu'
return id
},
set(id: SettingsPanel) {
const query = { ...route.query, panel: id }
void router.replace({ query })
}
})
const navItems = computed(() => {
const items = [
{ id: 'settings-gpu', label: 'GPU' },
{ id: 'settings-storage', label: 'Storage' }
const items: Array<{ id: SettingsPanel; label: string }> = [
{ id: 'gpu', label: 'GPU' },
{ id: 'storage', label: 'Storage' }
]
if (authMode.value === 'password') items.push({ id: 'settings-login', label: 'Site login' })
if (authMode.value === 'password') items.push({ id: 'login', label: 'Site login' })
items.push(
{ id: 'settings-folders', label: 'Folders' },
{ id: 'settings-loras', label: 'LoRA keys' },
{ id: 'settings-keeps', label: 'Keep prompts' },
{ id: 'settings-prompts', label: 'Prompt helper' }
{ id: 'folders', label: 'Folders' },
{ id: 'loras', label: 'LoRA keys' },
{ id: 'keeps', label: 'Keep prompts' },
{ id: 'prompts', label: 'Prompt helper' }
)
return items
})
@@ -379,6 +430,23 @@ const newKeep = reactive<Record<KeepPromptKind, { label: string; text: string }>
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; trigger: string; placeholder: string }>>([])
const loraQuery = ref('')
const loraKind = ref<LoraKindFilter>('all')
const loraKindOptions: Array<{ id: LoraKindFilter; label: string }> = [
{ id: 'all', label: 'All' },
{ id: 'image', label: 'Image' },
{ id: 'video', label: 'Video' }
]
const filteredLoraRows = computed(() => {
const q = loraQuery.value.trim().toLowerCase()
return loraRows.value.filter((row) => {
if (loraKind.value === 'image' && row.kind === 'video') return false
if (loraKind.value === 'video' && row.kind === 'image') return false
if (!q) return true
const hay = `${row.file} ${row.draft} ${row.trigger} ${row.key} ${row.placeholder}`.toLowerCase()
return hay.includes(q)
})
})
const loginUser = ref('')
const loginCurrent = ref('')
const loginNext = ref('')
@@ -468,6 +536,11 @@ onMounted(() => {
void loadLoraKeys()
})
watch(activePanel, () => {
if (typeof window === 'undefined') return
window.scrollTo({ top: 0 })
})
onBeforeUnmount(() => {
if (noticeTimer) clearTimeout(noticeTimer)
})
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="mb-8">
<h1 class="font-display text-2xl font-bold">Settings</h1>
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
GPU, storage, folders, LoRA keys, keep-prompt snippets, and prompt-helper instruction sets for this account.
Pick a panel on the left. LoRA keys can be filtered by name, alias, or activation word.
</p>
</div>
<SettingsForm />