Show one settings panel at a time and add a LoRA filter.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+103
-30
@@ -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
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user