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>
|
<template>
|
||||||
<div class="lg:grid lg:grid-cols-[13rem_minmax(0,1fr)] lg:items-start lg:gap-10">
|
<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">
|
<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"
|
v-for="item in navItems"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
:href="`#${item.id}`"
|
type="button"
|
||||||
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"
|
class="rounded-full border px-3 py-1.5 text-left text-xs lg:rounded-xl lg:text-sm"
|
||||||
>{{ item.label }}</a>
|
: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>
|
</nav>
|
||||||
|
|
||||||
<div class="space-y-10">
|
<div class="min-w-0 space-y-4">
|
||||||
<p
|
<p
|
||||||
v-if="notice"
|
v-if="notice"
|
||||||
class="rounded-2xl border px-4 py-3 text-sm"
|
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'"
|
: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>
|
>{{ notice }}</p>
|
||||||
|
|
||||||
<section id="settings-gpu" class="scroll-mt-24">
|
<section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6">
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">GPU</h3>
|
<h2 class="font-display text-xl font-bold">GPU</h2>
|
||||||
<div class="mt-2 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
<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">
|
<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.
|
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>
|
</p>
|
||||||
@@ -46,9 +50,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="settings-storage" class="scroll-mt-24">
|
<section v-if="activePanel === 'storage'" class="glass rounded-3xl p-5 sm:p-6">
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Storage</h3>
|
<h2 class="font-display text-xl font-bold">Storage</h2>
|
||||||
<div class="mt-2 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
|
<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-if="storageLoading" class="text-sm text-zinc-500">Measuring…</p>
|
||||||
<p v-else-if="storageError" class="text-sm text-zinc-500">{{ storageError }}</p>
|
<p v-else-if="storageError" class="text-sm text-zinc-500">{{ storageError }}</p>
|
||||||
<template v-else-if="storage">
|
<template v-else-if="storage">
|
||||||
@@ -59,8 +63,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-if="authMode === 'password'" id="settings-login" class="scroll-mt-24 space-y-3">
|
<section v-if="activePanel === 'login' && authMode === 'password'" class="glass space-y-3 rounded-3xl p-5 sm:p-6">
|
||||||
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Site login</h3>
|
<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>
|
<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">
|
<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">
|
<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>
|
</form>
|
||||||
</section>
|
</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">
|
<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>
|
<span class="text-xs text-zinc-500">{{ folders.length }} total</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -159,19 +163,41 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</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>
|
<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">
|
<p class="mt-1 text-sm text-zinc-400">
|
||||||
Short names used in auto-generated output titles. Leave blank to use the file stem
|
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>).
|
(<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.
|
Activation is the word or phrase the <span class="text-zinc-300">i</span> on a LoRA slider inserts into the prompt.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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-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-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
|
<div
|
||||||
v-for="row in loraRows"
|
v-for="row in filteredLoraRows"
|
||||||
:key="row.key"
|
: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"
|
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>
|
</div>
|
||||||
</section>
|
</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>
|
<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">
|
<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.
|
Saved snippets for the + button next to the studio prompt. Video and image lists are separate.
|
||||||
</p>
|
</p>
|
||||||
@@ -255,9 +281,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
<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">
|
<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.
|
Instruction sets sent to Beast Qwen VL with your still and draft. Use these later for multi-shot video scripts.
|
||||||
</p>
|
</p>
|
||||||
@@ -342,6 +368,12 @@ export interface SettingsFolder {
|
|||||||
unlocked: boolean
|
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 folders = ref<SettingsFolder[]>([])
|
||||||
const authMode = ref('')
|
const authMode = ref('')
|
||||||
const userName = ref('')
|
const userName = ref('')
|
||||||
@@ -349,17 +381,36 @@ const notice = ref('')
|
|||||||
const noticeError = ref(false)
|
const noticeError = ref(false)
|
||||||
let noticeTimer: ReturnType<typeof setTimeout> | null = null
|
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 navItems = computed(() => {
|
||||||
const items = [
|
const items: Array<{ id: SettingsPanel; label: string }> = [
|
||||||
{ id: 'settings-gpu', label: 'GPU' },
|
{ id: 'gpu', label: 'GPU' },
|
||||||
{ id: 'settings-storage', label: 'Storage' }
|
{ 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(
|
items.push(
|
||||||
{ id: 'settings-folders', label: 'Folders' },
|
{ id: 'folders', label: 'Folders' },
|
||||||
{ id: 'settings-loras', label: 'LoRA keys' },
|
{ id: 'loras', label: 'LoRA keys' },
|
||||||
{ id: 'settings-keeps', label: 'Keep prompts' },
|
{ id: 'keeps', label: 'Keep prompts' },
|
||||||
{ id: 'settings-prompts', label: 'Prompt helper' }
|
{ id: 'prompts', label: 'Prompt helper' }
|
||||||
)
|
)
|
||||||
return items
|
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 promptPresets = ref<Array<{ id: string; name: string; description?: string; builtin?: boolean; systemPrompt?: string }>>([])
|
||||||
const loraRowsLoading = ref(true)
|
const loraRowsLoading = ref(true)
|
||||||
const loraRows = ref<Array<{ key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; trigger: string; placeholder: string }>>([])
|
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 loginUser = ref('')
|
||||||
const loginCurrent = ref('')
|
const loginCurrent = ref('')
|
||||||
const loginNext = ref('')
|
const loginNext = ref('')
|
||||||
@@ -468,6 +536,11 @@ onMounted(() => {
|
|||||||
void loadLoraKeys()
|
void loadLoraKeys()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(activePanel, () => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
window.scrollTo({ top: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (noticeTimer) clearTimeout(noticeTimer)
|
if (noticeTimer) clearTimeout(noticeTimer)
|
||||||
})
|
})
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@
|
|||||||
<div class="mb-8">
|
<div class="mb-8">
|
||||||
<h1 class="font-display text-2xl font-bold">Settings</h1>
|
<h1 class="font-display text-2xl font-bold">Settings</h1>
|
||||||
<p class="mt-1 max-w-2xl text-sm text-zinc-400">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<SettingsForm />
|
<SettingsForm />
|
||||||
|
|||||||
Reference in New Issue
Block a user