Move nav, Poke, Sleep, and Sign out into a top-left menu.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-30 19:29:29 -05:00
co-authored by Cursor
parent 64179dd516
commit 8df265cbf3
5 changed files with 322 additions and 224 deletions
+297
View File
@@ -0,0 +1,297 @@
<template>
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl">
<div
class="mx-auto flex items-center justify-between px-4 py-4 sm:px-6"
:class="wide ? 'max-w-[90rem]' : 'max-w-7xl'"
>
<div class="relative flex items-center gap-3">
<NuxtLink
to="/"
class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 font-display font-extrabold text-zinc-950"
>A</NuxtLink>
<div>
<button
type="button"
class="flex items-center gap-1.5 text-left"
:aria-expanded="menuOpen"
aria-haspopup="menu"
@click.stop="toggleMenu"
>
<span class="font-display text-xl font-bold leading-none">{{ instanceName }}</span>
<svg class="h-4 w-4 text-zinc-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 10.94l3.71-3.71a.75.75 0 1 1 1.06 1.06l-4.24 4.24a.75.75 0 0 1-1.06 0L5.21 8.29a.75.75 0 0 1 .02-1.08Z"
clip-rule="evenodd"
/>
</svg>
</button>
<p class="hidden text-[11px] uppercase tracking-[0.22em] text-zinc-500 sm:block">{{ subtitle }}</p>
</div>
<div
v-if="menuOpen"
class="absolute left-0 top-full z-30 mt-2 w-52 rounded-2xl border border-white/10 bg-zinc-950/95 p-1 shadow-xl shadow-black/40 backdrop-blur-xl"
role="menu"
@click.stop
>
<NuxtLink
v-if="current !== 'studio'"
to="/"
class="block rounded-xl px-3 py-2 text-sm text-zinc-200 hover:bg-white/5 hover:text-white"
role="menuitem"
@click="menuOpen = false"
>Studio</NuxtLink>
<NuxtLink
to="/library"
class="block rounded-xl px-3 py-2 text-sm hover:bg-white/5 hover:text-white"
:class="current === 'library' ? 'text-amber-100' : 'text-zinc-200'"
role="menuitem"
@click="menuOpen = false"
>Library</NuxtLink>
<NuxtLink
to="/director"
class="block rounded-xl px-3 py-2 text-sm hover:bg-white/5 hover:text-white"
:class="current === 'director' ? 'text-amber-100' : 'text-zinc-200'"
role="menuitem"
@click="menuOpen = false"
>Director</NuxtLink>
<button
type="button"
class="block w-full rounded-xl px-3 py-2 text-left text-sm hover:bg-white/5 disabled:cursor-not-allowed disabled:opacity-50"
:class="gpuWaking ? 'text-amber-100' : 'text-zinc-200 hover:text-white'"
:disabled="gpuWaking || gpuSleeping"
:title="gpuError || 'Start Comfy on the GPU'"
role="menuitem"
@click="pokeGpu"
>{{ gpuWaking ? 'Poking…' : 'Poke' }}</button>
<button
type="button"
class="block w-full rounded-xl px-3 py-2 text-left text-sm hover:bg-white/5 disabled:cursor-not-allowed disabled:opacity-50"
:class="gpuSleeping ? 'text-amber-100' : 'text-zinc-200 hover:text-white'"
:disabled="gpuSleeping || gpuWaking || gpuBusy"
:title="gpuError || (gpuBusy ? 'Cancel the running job first' : 'Stop Comfy on the GPU to free VRAM. Does not shut the PC down.')"
role="menuitem"
@click="sleepGpu"
>{{ gpuSleeping ? 'Sleeping…' : 'Sleep' }}</button>
<div v-if="authEnabled || showSettings" class="my-1 border-t border-white/10" />
<button
v-if="showSettings"
type="button"
class="block w-full rounded-xl px-3 py-2 text-left text-sm text-zinc-200 hover:bg-white/5 hover:text-white"
role="menuitem"
@click="openSettings"
>Settings</button>
<a
v-if="authEnabled"
href="/api/auth/logout"
class="block rounded-xl px-3 py-2 text-sm text-zinc-400 hover:bg-white/5 hover:text-white"
role="menuitem"
>Sign out</a>
<p v-if="gpuError" class="px-3 py-1.5 text-[11px] text-red-300">{{ gpuError }}</p>
</div>
</div>
<div class="flex items-center gap-3 text-sm">
<span
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
:class="rigBadge.className"
:title="rigBadge.title"
>
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
{{ rigBadge.label }}
</span>
<NuxtLink
to="/queue"
class="rounded-full border px-3 py-1 hover:border-amber-300/50 hover:text-white"
:class="current === 'queue' || queueCount
? 'border-amber-300/40 text-amber-100'
: 'border-white/10 text-zinc-300'"
>
Queue<span v-if="queueCount" class="ml-1.5 text-amber-200">{{ queueCount }}</span>
</NuxtLink>
</div>
</div>
</header>
</template>
<script setup lang="ts">
withDefaults(defineProps<{
subtitle: string
current?: 'studio' | 'library' | 'director' | 'queue'
wide?: boolean
showSettings?: boolean
queueCount?: number
}>(), {
current: 'studio',
wide: false,
showSettings: false,
queueCount: 0
})
const emit = defineEmits<{
settings: []
gpu: []
}>()
const runtimeConfig = useRuntimeConfig()
const instanceName = ref(String(runtimeConfig.public.instanceName || 'AIGen'))
const authEnabled = ref(false)
const menuOpen = ref(false)
const gpuWaking = ref(false)
const gpuSleeping = ref(false)
const gpuError = ref('')
const comfyOk = ref(false)
const imageOk = ref(false)
const comfyBusy = ref(false)
const imageBusy = ref(false)
const comfyAsleep = ref(false)
let healthTimer: ReturnType<typeof setInterval> | null = null
const gpuBusy = computed(() => comfyBusy.value || imageBusy.value)
const rigBadge = computed(() => {
if (gpuSleeping.value) {
return {
label: 'Stopping…',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Stopping Comfy on the GPU'
}
}
if (gpuWaking.value) {
return {
label: 'Starting…',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Starting Comfy Desktop on the GPU'
}
}
if (!comfyOk.value && !imageOk.value) {
return {
label: 'Offline',
className: 'border-red-400/30 text-red-300',
dot: 'bg-red-400',
title: comfyAsleep.value
? 'Desktop GPU is asleep. Open the menu and click Poke, or generate to start Comfy.'
: 'Desktop GPU is unreachable'
}
}
if (gpuBusy.value) {
return {
label: 'Live',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Desktop GPU is rendering'
}
}
return {
label: 'Ready',
className: 'border-emerald-400/30 text-emerald-300',
dot: 'bg-emerald-400',
title: 'Desktop GPU is free'
}
})
function toggleMenu() {
menuOpen.value = !menuOpen.value
}
function closeMenu() {
menuOpen.value = false
}
function openSettings() {
menuOpen.value = false
emit('settings')
}
function applyHealth(health: {
comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean }
imageComfy?: { ok?: boolean; busy?: boolean }
} | null | undefined) {
const nextComfy = Boolean(health?.comfy?.ok)
const nextImage = Boolean(health?.imageComfy?.ok)
comfyOk.value = nextComfy
imageOk.value = nextImage
comfyBusy.value = Boolean(nextComfy && health?.comfy?.busy)
imageBusy.value = Boolean(nextImage && health?.imageComfy?.busy)
comfyAsleep.value = Boolean(health?.comfy?.asleep) && !nextComfy && !nextImage
}
async function pollHealth() {
const health = await $fetch<{
comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean }
imageComfy?: { ok?: boolean; busy?: boolean }
}>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } }))
applyHealth(health)
}
async function pokeGpu() {
if (gpuWaking.value || gpuSleeping.value) return
gpuWaking.value = true
gpuError.value = ''
try {
await $fetch('/api/comfy/wake', { method: 'POST' })
const deadline = Date.now() + 180_000
while (Date.now() < deadline) {
await pollHealth()
emit('gpu')
if (comfyOk.value || imageOk.value) return
await new Promise(resolve => setTimeout(resolve, 3000))
}
gpuError.value = 'Started Comfy but it is not answering yet. If the instance picker is showing, open ComfyUI (1).'
} catch (error: any) {
gpuError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not wake the GPU.'
} finally {
gpuWaking.value = false
emit('gpu')
}
}
async function sleepGpu() {
if (gpuSleeping.value || gpuWaking.value || gpuBusy.value) return
gpuSleeping.value = true
gpuError.value = ''
try {
await $fetch('/api/comfy/sleep', { method: 'POST' })
comfyOk.value = false
imageOk.value = false
comfyAsleep.value = true
await pollHealth()
} catch (error: any) {
gpuError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not stop Comfy.'
} finally {
gpuSleeping.value = false
emit('gpu')
}
}
function onWindowClick() {
closeMenu()
}
function onKey(event: KeyboardEvent) {
if (event.key === 'Escape') closeMenu()
}
onMounted(async () => {
window.addEventListener('click', onWindowClick)
window.addEventListener('keydown', onKey)
const [health, me] = await Promise.all([
$fetch<{ comfy?: { ok?: boolean; busy?: boolean; asleep?: boolean }; imageComfy?: { ok?: boolean; busy?: boolean } }>('/api/health').catch(() => ({ comfy: { ok: false, busy: false } })),
$fetch<{ authEnabled?: boolean; instanceName?: string }>('/api/auth/me').catch(() => ({}))
])
applyHealth(health)
authEnabled.value = Boolean(me.authEnabled)
if (me.instanceName) instanceName.value = me.instanceName
healthTimer = setInterval(() => {
if (typeof document !== 'undefined' && document.hidden) return
void pollHealth()
}, 5000)
})
onBeforeUnmount(() => {
window.removeEventListener('click', onWindowClick)
window.removeEventListener('keydown', onKey)
if (healthTimer) clearInterval(healthTimer)
healthTimer = null
})
</script>
+1 -34
View File
@@ -1,34 +1,6 @@
<template> <template>
<div class="min-h-dvh min-w-0"> <div class="min-h-dvh min-w-0">
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl"> <AppHeader subtitle="Director" current="director" />
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6">
<div class="flex items-center gap-3">
<NuxtLink to="/" class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 text-zinc-950 font-display font-extrabold">A</NuxtLink>
<div>
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1>
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">Director</p>
</div>
</div>
<div class="flex items-center gap-3 text-sm">
<NuxtLink to="/" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Studio
</NuxtLink>
<NuxtLink to="/library" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Library
</NuxtLink>
<NuxtLink
to="/director"
class="rounded-full border border-amber-300/70 bg-amber-400/10 px-3 py-1 text-amber-100"
>
Director
</NuxtLink>
<NuxtLink to="/queue" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Queue
</NuxtLink>
<a v-if="authEnabled" href="/api/auth/logout" class="text-zinc-400 hover:text-white">Sign out</a>
</div>
</div>
</header>
<main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]"> <main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(18rem,22rem)]">
<section class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5"> <section class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5">
@@ -187,8 +159,6 @@ The next beat. Same clothes as now.
[AUDIO CUES] [AUDIO CUES]
Ambient only.` Ambient only.`
const instanceName = ref(String(useRuntimeConfig().public.instanceName || 'AIGen'))
const authEnabled = ref(false)
const scripts = ref<DirectorScript[]>([]) const scripts = ref<DirectorScript[]>([])
const selectedId = ref('') const selectedId = ref('')
const name = ref('') const name = ref('')
@@ -299,9 +269,6 @@ function dirty() {
} }
onMounted(async () => { onMounted(async () => {
const me = await $fetch<{ authEnabled?: boolean; instanceName?: string }>('/api/auth/me').catch(() => ({}))
authEnabled.value = Boolean(me.authEnabled)
instanceName.value = me.instanceName || instanceName.value
try { try {
await loadScripts() await loadScripts()
} catch (err: any) { } catch (err: any) {
+12 -141
View File
@@ -1,74 +1,13 @@
<template> <template>
<div class="min-h-dvh min-w-0"> <div class="min-h-dvh min-w-0">
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl"> <AppHeader
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6"> :subtitle="ltxEnabled ? 'MiniMax H3 · LTX-2.3 · Flux.2 Klein' : 'MiniMax H3 · Flux.2 Klein'"
<div class="flex items-center gap-3"> current="studio"
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 text-zinc-950 font-display font-extrabold">A</div> show-settings
<div> :queue-count="queueBadgeCount"
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1> @settings="settingsOpen = true"
<p class="hidden text-[11px] uppercase tracking-[0.22em] text-zinc-500 sm:block">{{ ltxEnabled ? 'MiniMax H3 · LTX-2.3 · Flux.2 Klein' : 'MiniMax H3 · Flux.2 Klein' }}</p> @gpu="pollComfyHealth"
</div> />
</div>
<div class="flex items-center gap-3 text-sm">
<div class="flex items-center gap-2">
<span
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
:class="rigBadge.className"
:title="rigBadge.title"
>
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
{{ rigBadge.label }}
</span>
<button
v-if="showWakeGpu"
type="button"
class="rounded-full border px-3 py-1 hover:border-amber-300/50 hover:text-white disabled:cursor-wait disabled:opacity-80"
:class="gpuWaking ? 'border-amber-300/40 text-amber-100' : 'border-amber-400/40 text-amber-200'"
:disabled="gpuWaking"
:title="gpuWakeError || (gpuWaking ? 'Starting Comfy Desktop on the GPU' : 'Poke / Wake GPU — start Comfy before generating')"
@click="pokeGpu"
>
{{ gpuWaking ? 'Starting GPU…' : 'Wake GPU' }}
</button>
<button
v-if="showSleepGpu"
type="button"
class="rounded-full border px-3 py-1 hover:border-amber-300/50 hover:text-white disabled:cursor-not-allowed disabled:opacity-80"
:class="gpuSleeping ? 'border-amber-300/40 text-amber-100' : 'border-white/15 text-zinc-300'"
:disabled="gpuSleeping || rigBusy"
:title="gpuWakeError || (rigBusy ? 'Cancel the running job first' : (gpuSleeping ? 'Stopping Comfy on the GPU' : 'Stop Comfy on the GPU to free VRAM. Does not shut the PC down.'))"
@click="sleepGpu"
>
{{ gpuSleeping ? 'Stopping GPU…' : 'Sleep GPU' }}
</button>
</div>
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">
Settings
</button>
<NuxtLink
to="/library"
class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white"
>
Library
</NuxtLink>
<NuxtLink
to="/director"
class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white"
>
Director
</NuxtLink>
<NuxtLink
to="/queue"
class="rounded-full border px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white"
:class="queueBadgeCount ? 'border-amber-300/40 text-amber-100' : 'border-white/10'"
>
Queue<span v-if="queueBadgeCount" class="ml-1.5 text-amber-200">{{ queueBadgeCount }}</span>
</NuxtLink>
<a v-if="authEnabled" href="/api/auth/logout" class="text-zinc-400 hover:text-white">Sign out</a>
</div>
</div>
</header>
<main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"> <main class="mx-auto grid min-w-0 max-w-7xl gap-6 px-4 py-6 sm:px-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<section id="studio-input" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5"> <section id="studio-input" class="glass min-w-0 max-w-full rounded-3xl p-5 sm:p-6 space-y-5">
@@ -2464,10 +2403,8 @@ const comfyOk = ref(false)
const comfyBusy = ref(false) const comfyBusy = ref(false)
const comfyAsleep = ref(false) const comfyAsleep = ref(false)
const gpuWaking = ref(false) const gpuWaking = ref(false)
const gpuSleeping = ref(false)
const gpuWakeError = ref('') const gpuWakeError = ref('')
const userName = ref('') const userName = ref('')
const authEnabled = ref(false)
const authMode = ref('') const authMode = ref('')
const instanceName = ref(String(runtimeConfig.public.instanceName || 'AIGen')) const instanceName = ref(String(runtimeConfig.public.instanceName || 'AIGen'))
const ltxEnabled = computed(() => ltxWorkflowEnabled({ const ltxEnabled = computed(() => ltxWorkflowEnabled({
@@ -2735,53 +2672,7 @@ const liveMaxStep = computed(() => (
: (maxStep.value || defaultVideoSteps(turbo.value, videoWorkflow.value)) : (maxStep.value || defaultVideoSteps(turbo.value, videoWorkflow.value))
)) ))
const rigBusy = computed(() => ( const showWakeGpu = computed(() => gpuWaking.value || (!comfyOk.value && !imageComfyOk.value))
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
))
const showWakeGpu = computed(() => gpuWaking.value || (!gpuSleeping.value && !comfyOk.value && !imageComfyOk.value))
const showSleepGpu = computed(() => gpuSleeping.value || ((comfyOk.value || imageComfyOk.value) && !gpuWaking.value))
const rigBadge = computed(() => {
if (gpuSleeping.value) {
return {
label: 'Stopping…',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Stopping Comfy on the GPU'
}
}
if (gpuWaking.value) {
return {
label: 'Starting…',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Starting Comfy Desktop on the GPU'
}
}
if (!comfyOk.value && !imageComfyOk.value) {
return {
label: 'Offline',
className: 'border-red-400/30 text-red-300',
dot: 'bg-red-400',
title: comfyAsleep.value
? 'Desktop GPU is asleep. Click Wake GPU or generate to start Comfy.'
: 'Desktop GPU is unreachable'
}
}
if (rigBusy.value) {
return {
label: 'Live',
className: 'border-amber-400/40 text-amber-200',
dot: 'bg-amber-400 animate-pulse',
title: 'Desktop GPU is rendering'
}
}
return {
label: 'Ready',
className: 'border-emerald-400/30 text-emerald-300',
dot: 'bg-emerald-400',
title: 'Desktop GPU is free'
}
})
const canClearPreview = computed(() => Boolean( const canClearPreview = computed(() => Boolean(
videoUrl.value || editResultUrl.value || extendOpen.value || lockedSave.value || editLockedSave.value videoUrl.value || editResultUrl.value || extendOpen.value || lockedSave.value || editLockedSave.value
@@ -2878,11 +2769,10 @@ const recommendHint = computed(() => {
return 'Send this still and draft to Beast Qwen VL.' return 'Send this still and draft to Beast Qwen VL.'
}) })
const gpuSleepHint = computed(() => { const gpuSleepHint = computed(() => {
if (gpuSleeping.value) return 'Stopping Comfy on the GPU to free VRAM. The PC stays on.'
if (comfyOk.value || imageComfyOk.value) return '' if (comfyOk.value || imageComfyOk.value) return ''
if (gpuWaking.value) return 'Starting Comfy on the GPU. Generate will wait in the job queue until it answers.' if (gpuWaking.value) return 'Starting Comfy on the GPU. Generate will wait in the job queue until it answers.'
if (comfyAsleep.value) return 'GPU is asleep. Generate will wake it — the first job can take a few minutes. Or click Wake GPU to pre-warm.' if (comfyAsleep.value) return 'GPU is asleep. Generate will wake it — the first job can take a few minutes. Or open the menu and click Poke.'
return 'GPU is offline. Generate will try to start Comfy. Or click Wake GPU.' return 'GPU is offline. Generate will try to start Comfy. Or open the menu and click Poke.'
}) })
const editBlocked = computed(() => { const editBlocked = computed(() => {
if (!file.value || !folderId.value) return true if (!file.value || !folderId.value) return true
@@ -3550,7 +3440,7 @@ async function pollComfyHealth() {
} }
async function pokeGpu() { async function pokeGpu() {
if (gpuWaking.value || gpuSleeping.value) return if (gpuWaking.value) return
gpuWaking.value = true gpuWaking.value = true
gpuWakeError.value = '' gpuWakeError.value = ''
try { try {
@@ -3572,24 +3462,6 @@ async function pokeGpu() {
} }
} }
async function sleepGpu() {
if (gpuSleeping.value || gpuWaking.value || rigBusy.value) return
gpuSleeping.value = true
gpuWakeError.value = ''
try {
await $fetch('/api/comfy/sleep', { method: 'POST' })
comfyOk.value = false
imageComfyOk.value = false
imageBeastOk.value = false
comfyAsleep.value = true
await pollComfyHealth()
} catch (error: any) {
gpuWakeError.value = error?.data?.statusMessage || error?.statusMessage || error?.message || 'Could not stop Comfy.'
} finally {
gpuSleeping.value = false
}
}
onMounted(async () => { onMounted(async () => {
try { try {
withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false' withSound.value = localStorage.getItem('aigen-generate-sound') !== 'false'
@@ -3641,7 +3513,6 @@ onMounted(async () => {
void loadGenerationPresets() void loadGenerationPresets()
await loadDirectorScripts().catch(() => null) await loadDirectorScripts().catch(() => null)
userName.value = me.user?.name || me.user?.email || '' userName.value = me.user?.name || me.user?.email || ''
authEnabled.value = Boolean(me.authEnabled)
authMode.value = me.authMode || '' authMode.value = me.authMode || ''
instanceName.value = me.instanceName || instanceName.value || 'AIGen' instanceName.value = me.instanceName || instanceName.value || 'AIGen'
applyInstancePrivacy() applyInstancePrivacy()
+7 -21
View File
@@ -1,24 +1,12 @@
<template> <template>
<div class="flex h-dvh min-w-0 flex-col overflow-hidden"> <div class="flex h-dvh min-w-0 flex-col overflow-hidden">
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl"> <AppHeader
<div class="mx-auto flex max-w-[90rem] items-center justify-between px-4 py-4 sm:px-6"> subtitle="Library"
<div class="flex items-center gap-3"> current="library"
<NuxtLink to="/" class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 text-zinc-950 font-display font-extrabold">A</NuxtLink> wide
<div> show-settings
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1> @settings="settingsOpen = true"
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">Library</p> />
</div>
</div>
<div class="flex items-center gap-3 text-sm">
<NuxtLink to="/" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">Studio</NuxtLink>
<NuxtLink to="/library" class="rounded-full border border-amber-300/70 bg-amber-400/10 px-3 py-1 text-amber-100">Library</NuxtLink>
<NuxtLink to="/director" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">Director</NuxtLink>
<NuxtLink to="/queue" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">Queue</NuxtLink>
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white" @click="settingsOpen = true">Settings</button>
<a v-if="authEnabled" href="/api/auth/logout" class="text-zinc-400 hover:text-white">Sign out</a>
</div>
</div>
</header>
<main class="mx-auto flex min-h-0 w-full max-w-[90rem] flex-1 flex-col gap-4 px-4 py-4 sm:px-6 lg:flex-row"> <main class="mx-auto flex min-h-0 w-full max-w-[90rem] flex-1 flex-col gap-4 px-4 py-4 sm:px-6 lg:flex-row">
<section class="glass flex min-h-0 min-w-0 flex-1 flex-col rounded-3xl p-4 sm:p-5"> <section class="glass flex min-h-0 min-w-0 flex-1 flex-col rounded-3xl p-4 sm:p-5">
@@ -369,7 +357,6 @@ const showPrivacyToggles = computed(() => isXaigenStudio({
studio: runtimeConfig.public.studio, studio: runtimeConfig.public.studio,
instanceName: runtimeConfig.public.instanceName || instanceName.value instanceName: runtimeConfig.public.instanceName || instanceName.value
})) }))
const authEnabled = ref(false)
const authMode = ref('') const authMode = ref('')
const userName = ref('') const userName = ref('')
const settingsOpen = ref(false) const settingsOpen = ref(false)
@@ -869,7 +856,6 @@ onMounted(async () => {
loadLibrary().catch(() => applyLibrary({ folders: [], clips: [], stills: [] })) loadLibrary().catch(() => applyLibrary({ folders: [], clips: [], stills: [] }))
]) ])
userName.value = me.user?.name || me.user?.email || '' userName.value = me.user?.name || me.user?.email || ''
authEnabled.value = Boolean(me.authEnabled)
authMode.value = me.authMode || '' authMode.value = me.authMode || ''
instanceName.value = me.instanceName || instanceName.value instanceName.value = me.instanceName || instanceName.value
await nextTick() await nextTick()
+5 -28
View File
@@ -1,28 +1,10 @@
<template> <template>
<div class="min-h-dvh min-w-0"> <div class="min-h-dvh min-w-0">
<header class="sticky top-0 z-20 border-b border-white/5 bg-zinc-950/70 backdrop-blur-xl"> <AppHeader
<div class="mx-auto flex max-w-7xl items-center justify-between px-4 py-4 sm:px-6"> subtitle="Queue"
<div class="flex items-center gap-3"> current="queue"
<NuxtLink to="/" class="flex h-10 w-10 items-center justify-center rounded-xl bg-amber-400 text-zinc-950 font-display font-extrabold">A</NuxtLink> :queue-count="waitingCount || pendingCount"
<div> />
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1>
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">Queue</p>
</div>
</div>
<div class="flex items-center gap-3 text-sm">
<NuxtLink to="/" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Studio
</NuxtLink>
<NuxtLink to="/library" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Library
</NuxtLink>
<NuxtLink to="/director" class="rounded-full border border-white/10 px-3 py-1 text-zinc-300 hover:border-amber-300/50 hover:text-white">
Director
</NuxtLink>
<a v-if="authEnabled" href="/api/auth/logout" class="text-zinc-400 hover:text-white">Sign out</a>
</div>
</div>
</header>
<main class="mx-auto min-w-0 max-w-7xl space-y-6 px-4 py-6 sm:px-6"> <main class="mx-auto min-w-0 max-w-7xl space-y-6 px-4 py-6 sm:px-6">
<div class="flex flex-wrap items-end justify-between gap-3"> <div class="flex flex-wrap items-end justify-between gap-3">
@@ -475,9 +457,7 @@ type PendingRemove = {
queue?: Queue queue?: Queue
} }
const instanceName = ref(String(useRuntimeConfig().public.instanceName || 'AIGen'))
const videoLoras = ref<string[]>([]) const videoLoras = ref<string[]>([])
const authEnabled = ref(false)
const jobs = ref<StudioJobRow[]>([]) const jobs = ref<StudioJobRow[]>([])
const orphans = ref<Queue[]>([]) const orphans = ref<Queue[]>([])
const waitingCount = ref(0) const waitingCount = ref(0)
@@ -1033,9 +1013,6 @@ function saveSegment(queue: Queue, segment: Segment) {
} }
onMounted(async () => { onMounted(async () => {
const me = await $fetch<{ user?: { name?: string }; authEnabled?: boolean; instanceName?: string }>('/api/auth/me').catch(() => ({ user: null }))
authEnabled.value = Boolean(me.authEnabled)
instanceName.value = me.instanceName || instanceName.value
void $fetch<{ video?: string[] }>('/api/loras').then((listed) => { void $fetch<{ video?: string[] }>('/api/loras').then((listed) => {
videoLoras.value = listed.video || [] videoLoras.value = listed.video || []
}).catch(() => null) }).catch(() => null)