Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eba701a82c | ||
|
|
bdc1ab6c2d | ||
|
|
d9743dd8c9 | ||
|
|
1b29452455 | ||
|
|
292d53a20e |
@@ -0,0 +1,118 @@
|
|||||||
|
/* Studio 2 display skins — Classic | Studio. Layout tokens only; palette matches main.css. */
|
||||||
|
|
||||||
|
:root,
|
||||||
|
[data-display='classic'],
|
||||||
|
[data-display='studio'] {
|
||||||
|
--display-bg: #09090b;
|
||||||
|
--display-fg: #e4e4e7;
|
||||||
|
--display-muted: #a1a1aa;
|
||||||
|
--display-border: rgba(255, 255, 255, 0.1);
|
||||||
|
--display-amber: #fbbf24;
|
||||||
|
--display-amber-fg: #09090b;
|
||||||
|
--display-radius: 8px;
|
||||||
|
--display-focus: rgba(252, 211, 77, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='classic'] {
|
||||||
|
--chrome-gap: 0.5rem;
|
||||||
|
--rail-h: 2.25rem;
|
||||||
|
--card-pad: 0.75rem;
|
||||||
|
--display-type-title: 1.75rem;
|
||||||
|
--display-type-body: 0.875rem;
|
||||||
|
--display-viewer-radius: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='studio'] {
|
||||||
|
--chrome-gap: 1rem;
|
||||||
|
--rail-h: 3rem;
|
||||||
|
--card-pad: 1rem;
|
||||||
|
--display-type-title: 1.875rem;
|
||||||
|
--display-type-body: 0.9375rem;
|
||||||
|
--display-viewer-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-title {
|
||||||
|
font-size: var(--display-type-title);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-body {
|
||||||
|
font-size: var(--display-type-body);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-stack {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--chrome-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-card {
|
||||||
|
border-radius: var(--display-radius);
|
||||||
|
border: 1px solid var(--display-border);
|
||||||
|
padding: var(--card-pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-viewer {
|
||||||
|
border-radius: var(--display-viewer-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-rail {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
min-height: var(--rail-h);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-rail__seg {
|
||||||
|
min-height: var(--rail-h);
|
||||||
|
border-radius: var(--display-radius);
|
||||||
|
border: 1px solid var(--display-border);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
font-size: var(--display-type-body);
|
||||||
|
color: var(--display-fg);
|
||||||
|
background: transparent;
|
||||||
|
outline: none;
|
||||||
|
transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-rail__seg:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px var(--display-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-rail__seg[aria-pressed='true'],
|
||||||
|
[data-display] .display-rail__seg[data-active='true'] {
|
||||||
|
border-color: var(--display-amber);
|
||||||
|
background: var(--display-amber);
|
||||||
|
color: var(--display-amber-fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-rail__seg:disabled,
|
||||||
|
[data-display] .display-rail__seg[aria-disabled='true'] {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='studio'] .display-rail__seg {
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0.625rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display='classic'] .display-rail__seg {
|
||||||
|
min-height: 2.25rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-cta {
|
||||||
|
border-radius: var(--display-radius);
|
||||||
|
background: var(--display-amber);
|
||||||
|
color: var(--display-amber-fg);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-cta:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px var(--display-focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-display] .display-control {
|
||||||
|
border-radius: var(--display-radius);
|
||||||
|
}
|
||||||
@@ -20,6 +20,37 @@
|
|||||||
: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 v-if="activePanel === 'display'" class="glass rounded-3xl p-5 sm:p-6">
|
||||||
|
<h2 class="font-display text-xl font-bold">Display</h2>
|
||||||
|
<p class="mt-1 text-sm text-zinc-400">Layout only. Jobs, engines, and library IDs stay the same.</p>
|
||||||
|
<div class="mt-4 grid gap-3 sm:grid-cols-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-2xl border p-4 text-left outline-none ring-amber-300/40 focus-visible:ring-2"
|
||||||
|
:class="storedDisplayMode === 'classic'
|
||||||
|
? 'border-amber-300/60 bg-amber-400/10'
|
||||||
|
: 'border-white/10 bg-zinc-950/50 hover:border-white/25'"
|
||||||
|
:aria-pressed="storedDisplayMode === 'classic'"
|
||||||
|
@click="setDisplayMode('classic')"
|
||||||
|
>
|
||||||
|
<span class="block font-display text-lg font-semibold text-zinc-50">Classic</span>
|
||||||
|
<span class="mt-1 block text-xs text-zinc-400">Packed header, compact selects, tight gaps.</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-2xl border p-4 text-left outline-none ring-amber-300/40 focus-visible:ring-2"
|
||||||
|
:class="storedDisplayMode === 'studio'
|
||||||
|
? 'border-amber-300/60 bg-amber-400/10'
|
||||||
|
: 'border-white/10 bg-zinc-950/50 hover:border-white/25'"
|
||||||
|
:aria-pressed="storedDisplayMode === 'studio'"
|
||||||
|
@click="setDisplayMode('studio')"
|
||||||
|
>
|
||||||
|
<span class="block font-display text-lg font-semibold text-zinc-50">Studio</span>
|
||||||
|
<span class="mt-1 block text-xs text-zinc-400">Task and model rails, locks by slots, eye toggles.</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6">
|
<section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6">
|
||||||
<h2 class="font-display text-xl font-bold">GPU & defaults</h2>
|
<h2 class="font-display text-xl font-bold">GPU & defaults</h2>
|
||||||
<div class="mt-4 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">
|
||||||
@@ -422,11 +453,12 @@ export interface SettingsFolder {
|
|||||||
unlocked: boolean
|
unlocked: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingsPanel = 'gpu' | 'storage' | 'login' | 'folders' | 'loras' | 'keeps' | 'prompts'
|
type SettingsPanel = 'display' | 'gpu' | 'storage' | 'login' | 'folders' | 'loras' | 'keeps' | 'prompts'
|
||||||
type LoraKindFilter = 'all' | 'image' | 'video'
|
type LoraKindFilter = 'all' | 'image' | 'video'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { storedDisplayMode, setDisplayMode } = useDisplayMode()
|
||||||
|
|
||||||
const folders = ref<SettingsFolder[]>([])
|
const folders = ref<SettingsFolder[]>([])
|
||||||
const authMode = ref('')
|
const authMode = ref('')
|
||||||
@@ -435,7 +467,7 @@ 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']
|
const panelIds: SettingsPanel[] = ['display', 'gpu', 'storage', 'login', 'folders', 'loras', 'keeps', 'prompts']
|
||||||
|
|
||||||
function parsePanel(raw: unknown): SettingsPanel {
|
function parsePanel(raw: unknown): SettingsPanel {
|
||||||
const value = String(raw || '')
|
const value = String(raw || '')
|
||||||
@@ -456,6 +488,7 @@ const activePanel = computed({
|
|||||||
|
|
||||||
const navItems = computed(() => {
|
const navItems = computed(() => {
|
||||||
const items: Array<{ id: SettingsPanel; label: string }> = [
|
const items: Array<{ id: SettingsPanel; label: string }> = [
|
||||||
|
{ id: 'display', label: 'Display' },
|
||||||
{ id: 'gpu', label: 'GPU & defaults' },
|
{ id: 'gpu', label: 'GPU & defaults' },
|
||||||
{ id: 'storage', label: 'Storage' }
|
{ id: 'storage', label: 'Storage' }
|
||||||
]
|
]
|
||||||
|
|||||||
+108
-38
@@ -1,52 +1,94 @@
|
|||||||
<template>
|
<template>
|
||||||
<main class="w-full min-w-0 space-y-5 p-4 md:p-6 xl:p-8">
|
<main class="display-stack w-full min-w-0 p-4 md:p-6 xl:p-8" :data-display="displayMode">
|
||||||
<header class="flex flex-wrap items-end justify-between gap-4">
|
<header class="flex flex-wrap items-end justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-display text-3xl font-bold tracking-tight text-zinc-50">Studio 2</h1>
|
<h1 class="display-title font-display font-bold tracking-tight text-zinc-50">Studio 2</h1>
|
||||||
<p class="mt-1 text-sm text-zinc-400">One hero. Every shot.</p>
|
<p class="display-body mt-1 text-zinc-400">One hero. Every shot.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<label class="text-sm text-zinc-300">Folder
|
<label class="display-body text-zinc-300">Folder
|
||||||
<select v-model="form.folderId" class="ml-2 rounded-lg border border-white/10 bg-zinc-900 px-3 py-2 outline-none ring-amber-300/40 focus:ring-2">
|
<select v-model="form.folderId" class="display-control ml-2 border border-white/10 bg-zinc-900 px-3 py-2 outline-none ring-amber-300/40 focus:ring-2">
|
||||||
<option v-for="f in library.folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
<option v-for="f in library.folders" :key="f.id" :value="f.id">{{ f.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="isClassic && form.mode!=='music'" class="flex flex-wrap items-center gap-3 text-sm text-zinc-300" aria-label="Identity locks">
|
||||||
|
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockFace" type="checkbox" class="accent-amber-400">Lock face</label>
|
||||||
|
<label class="inline-flex items-center gap-1.5"><input v-model="form.lockOutfit" type="checkbox" class="accent-amber-400">Lock outfit</label>
|
||||||
|
</div>
|
||||||
|
<div v-if="isClassic && xaigen" class="flex flex-wrap items-center gap-3 text-sm text-zinc-300" aria-label="Preview hide">
|
||||||
|
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideInput" type="checkbox" class="accent-amber-400">Hide input</label>
|
||||||
|
<label class="inline-flex items-center gap-1.5"><input v-model="classicHideOutput" type="checkbox" class="accent-amber-400">Hide output</label>
|
||||||
|
</div>
|
||||||
<nav class="flex flex-wrap items-center gap-3 text-sm" aria-label="Studio links">
|
<nav class="flex flex-wrap items-center gap-3 text-sm" aria-label="Studio links">
|
||||||
<NuxtLink to="/library" class="text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2">Library</NuxtLink>
|
<Studio2GpuStatus :status="gpu" />
|
||||||
<NuxtLink to="/queue" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Queue</NuxtLink>
|
<NuxtLink to="/queue" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Queue</NuxtLink>
|
||||||
|
<NuxtLink to="/library" class="text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2">Library</NuxtLink>
|
||||||
<NuxtLink to="/settings" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Settings</NuxtLink>
|
<NuxtLink to="/settings" class="text-zinc-300 outline-none ring-amber-300/40 focus-visible:ring-2">Settings</NuxtLink>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-white/10 px-4 py-2 text-sm">
|
<div class="display-stack min-w-0">
|
||||||
<Studio2GpuStatus :status="gpu" />
|
<label v-if="isClassic" class="display-body block text-zinc-300">Task
|
||||||
</div>
|
<select :value="form.mode" class="display-control mt-1 w-full max-w-xs border border-white/10 bg-zinc-900 px-3 py-2 capitalize outline-none ring-amber-300/40 focus:ring-2" @change="onTaskSelect">
|
||||||
|
<option v-for="task in tasks" :key="task" :value="task" class="capitalize">{{ task }}</option>
|
||||||
<nav class="flex flex-wrap gap-2" aria-label="Tasks">
|
</select>
|
||||||
|
</label>
|
||||||
|
<nav v-else class="display-rail" aria-label="Tasks">
|
||||||
<button
|
<button
|
||||||
v-for="task in tasks"
|
v-for="task in tasks"
|
||||||
:key="task"
|
:key="task"
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-4 py-2 capitalize outline-none ring-amber-300/50 focus-visible:ring-2"
|
class="display-rail__seg capitalize"
|
||||||
:class="form.mode===task ? 'border-amber-300 bg-amber-300 text-black' : 'border-white/15 text-zinc-200 hover:border-white/30'"
|
|
||||||
:aria-pressed="form.mode===task"
|
:aria-pressed="form.mode===task"
|
||||||
@click="chooseTask(task)"
|
@click="chooseTask(task)"
|
||||||
>{{ task }}</button>
|
>{{ task }}</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div v-if="form.mode!=='music'" class="min-w-0">
|
||||||
|
<label v-if="isClassic" class="display-body block text-zinc-300">Model
|
||||||
|
<select v-model="form.engine" class="display-control mt-1 w-full max-w-md border border-white/10 bg-zinc-900 px-3 py-2 outline-none ring-amber-300/40 focus:ring-2">
|
||||||
|
<option v-for="opt in engineOptions" :key="opt.value" :value="opt.value" :disabled="opt.disabled">{{ opt.label }}{{ opt.hint ? ' — '+opt.hint : '' }}</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<div v-else class="display-rail" role="group" aria-label="Model">
|
||||||
|
<button
|
||||||
|
v-for="opt in engineOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
type="button"
|
||||||
|
class="display-rail__seg"
|
||||||
|
:aria-pressed="form.engine===opt.value"
|
||||||
|
:aria-disabled="opt.disabled || undefined"
|
||||||
|
:disabled="opt.disabled"
|
||||||
|
:title="opt.hint || undefined"
|
||||||
|
@click="!opt.disabled && (form.engine=opt.value)"
|
||||||
|
>
|
||||||
|
<span class="block">{{ opt.label }}</span>
|
||||||
|
<span v-if="opt.hint" class="mt-0.5 block text-[11px] font-normal opacity-80">{{ opt.hint }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="display-body text-zinc-400">Model · YuE2</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,3fr)_minmax(24rem,2fr)] 2xl:gap-8">
|
<div class="grid min-w-0 gap-6 xl:grid-cols-[minmax(0,3fr)_minmax(24rem,2fr)] 2xl:gap-8">
|
||||||
<section class="min-w-0 space-y-4">
|
<section class="min-w-0 space-y-4">
|
||||||
<div class="relative flex min-h-80 items-center justify-center overflow-hidden rounded-3xl border border-white/10 bg-black">
|
<div class="display-viewer relative flex min-h-80 items-center justify-center overflow-hidden border border-white/10 bg-black">
|
||||||
<button
|
<button
|
||||||
v-if="selected && xaigen"
|
v-if="selected && xaigen && isStudio"
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute right-3 top-3 z-10 rounded-full border border-white/20 bg-zinc-950/80 p-2 text-zinc-200 outline-none ring-amber-300/50 hover:border-amber-300/50 focus-visible:ring-2"
|
class="absolute right-3 top-3 z-10 rounded-full border border-white/20 bg-zinc-950/80 p-2 text-zinc-200 outline-none ring-amber-300/50 hover:border-amber-300/50 focus-visible:ring-2"
|
||||||
:aria-pressed="!isHidden(selected)"
|
:aria-pressed="!isHidden(selected)"
|
||||||
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
:aria-label="isHidden(selected) ? 'Show preview' : 'Hide preview'"
|
||||||
@click.stop="toggleReveal(selected.id)"
|
@click.stop="toggleReveal(selected.id)"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">{{ isHidden(selected) ? 'Show' : 'Hide' }}</span>
|
<svg class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||||
|
<path v-if="isHidden(selected)" d="M3 3l18 18M10.5 10.6a2.5 2.5 0 0 0 3 3M9.4 5.1A10.4 10.4 0 0 1 12 4.5c5 0 9.3 3.1 11 7.5a11.6 11.6 0 0 1-4.2 5.1M6.2 6.2A11.5 11.5 0 0 0 1 12c1.2 2.7 3.3 4.9 5.9 6.3A10.5 10.5 0 0 0 12 19.5c1.2 0 2.4-.2 3.5-.6" stroke-linecap="round"/>
|
||||||
|
<template v-else>
|
||||||
|
<path d="M2 12s3.5-7.5 10-7.5S22 12 22 12s-3.5 7.5-10 7.5S2 12 2 12Z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</template>
|
||||||
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<p v-if="selected && isHidden(selected)" class="text-zinc-400">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
<p v-if="selected && isHidden(selected)" class="text-zinc-400">{{ selected.kind==='track' ? 'Track loaded · preview hidden' : selected.role==='input' ? 'Still loaded · preview hidden' : 'Output hidden' }}</p>
|
||||||
<div v-else-if="selected?.kind==='track'" class="w-full max-w-xl px-6 py-4">
|
<div v-else-if="selected?.kind==='track'" class="w-full max-w-xl px-6 py-4">
|
||||||
@@ -79,22 +121,30 @@
|
|||||||
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" class="h-20 w-24 rounded object-cover" alt="Family output">
|
<img v-else :src="o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : stillUrl(o.id)" class="h-20 w-24 rounded object-cover" alt="Family output">
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="xaigen"
|
v-if="xaigen && isStudio"
|
||||||
type="button"
|
type="button"
|
||||||
class="absolute right-1 top-1 rounded-md border border-white/20 bg-zinc-950/80 px-1.5 py-0.5 text-[10px] text-zinc-200 outline-none ring-amber-300/50 focus-visible:ring-2"
|
class="absolute right-1 top-1 rounded-md border border-white/20 bg-zinc-950/80 p-1 text-zinc-200 outline-none ring-amber-300/50 focus-visible:ring-2"
|
||||||
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
:aria-label="isHidden(o) ? 'Show tile' : 'Hide tile'"
|
||||||
:aria-pressed="!isHidden(o)"
|
:aria-pressed="!isHidden(o)"
|
||||||
@click.stop="toggleReveal(o.id)"
|
@click.stop="toggleReveal(o.id)"
|
||||||
>{{ isHidden(o) ? 'Show' : 'Hide' }}</button>
|
>
|
||||||
|
<svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" aria-hidden="true">
|
||||||
|
<path v-if="isHidden(o)" d="M3 3l18 18M10.5 10.6a2.5 2.5 0 0 0 3 3M9.4 5.1A10.4 10.4 0 0 1 12 4.5c5 0 9.3 3.1 11 7.5a11.6 11.6 0 0 1-4.2 5.1M6.2 6.2A11.5 11.5 0 0 0 1 12c1.2 2.7 3.3 4.9 5.9 6.3A10.5 10.5 0 0 0 12 19.5c1.2 0 2.4-.2 3.5-.6" stroke-linecap="round"/>
|
||||||
|
<template v-else>
|
||||||
|
<path d="M2 12s3.5-7.5 10-7.5S22 12 22 12s-3.5 7.5-10 7.5S2 12 2 12Z"/>
|
||||||
|
<circle cx="12" cy="12" r="3"/>
|
||||||
|
</template>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-if="form.mode!=='music'" aria-label="Source" class="rounded-2xl border border-white/10 p-3">
|
<section v-if="form.mode!=='music'" aria-label="Source" class="display-card">
|
||||||
<div class="mb-3 flex flex-wrap items-start justify-between gap-3">
|
<div class="mb-3 flex flex-wrap items-start justify-between gap-3">
|
||||||
<h2 class="text-xs uppercase tracking-wider text-zinc-400">Source</h2>
|
<h2 class="text-xs uppercase tracking-wider text-zinc-400">Source</h2>
|
||||||
<div class="flex flex-wrap gap-4 text-sm text-zinc-300" aria-label="Identity locks">
|
<div v-if="isStudio" class="flex flex-wrap gap-4 text-sm text-zinc-300" aria-label="Identity locks">
|
||||||
<label class="flex items-start gap-2"><input v-model="form.lockFace" type="checkbox" class="mt-1 accent-amber-400"><span>Lock face<span class="block text-xs text-zinc-500">Face, hair, glasses, body</span></span></label>
|
<label class="flex items-start gap-2"><input v-model="form.lockFace" type="checkbox" class="mt-1 accent-amber-400"><span>Lock face<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||||
<label class="flex items-start gap-2"><input v-model="form.lockOutfit" type="checkbox" class="mt-1 accent-amber-400"><span>Lock outfit<span class="block text-xs text-zinc-500">Wardrobe from hero</span></span></label>
|
<label class="flex items-start gap-2"><input v-model="form.lockOutfit" type="checkbox" class="mt-1 accent-amber-400"><span>Lock outfit<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-4">
|
<div class="flex flex-wrap gap-4">
|
||||||
@@ -160,7 +210,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<p v-if="error" role="alert" class="text-sm text-red-300">{{ error }}</p>
|
<p v-if="error" role="alert" class="text-sm text-red-300">{{ error }}</p>
|
||||||
<button type="button" :disabled="submitting" class="w-full rounded-2xl bg-amber-300 p-4 font-semibold text-black outline-none ring-amber-300/60 focus-visible:ring-2 disabled:opacity-50" @click="queue">{{ submitting?'Queuing…':'Queue' }}</button>
|
<button type="button" :disabled="submitting" class="display-cta w-full p-4 outline-none disabled:opacity-50" @click="queue">{{ submitting?'Queuing…':'Queue' }}</button>
|
||||||
|
|
||||||
<h2 class="text-sm font-semibold text-zinc-300">Job queue</h2>
|
<h2 class="text-sm font-semibold text-zinc-300">Job queue</h2>
|
||||||
<div v-for="job in upscales.filter(j=>j.folderId===form.folderId && ['queued','running'].includes(j.status))" :key="job.id" class="space-y-2 rounded-xl border border-amber-300/25 bg-zinc-900 p-4 text-sm" role="status">
|
<div v-for="job in upscales.filter(j=>j.folderId===form.folderId && ['queued','running'].includes(j.status))" :key="job.id" class="space-y-2 rounded-xl border border-amber-300/25 bg-zinc-900 p-4 text-sm" role="status">
|
||||||
@@ -189,18 +239,6 @@
|
|||||||
<button type="button" class="float-right outline-none ring-amber-300/40 focus-visible:ring-2" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
<button type="button" class="float-right outline-none ring-amber-300/40 focus-visible:ring-2" @click="settingsOpen=false;detailsOpen=false">Close</button>
|
||||||
<template v-if="settingsOpen">
|
<template v-if="settingsOpen">
|
||||||
<h2 class="mb-6 text-xl font-semibold">Generation settings</h2>
|
<h2 class="mb-6 text-xl font-semibold">Generation settings</h2>
|
||||||
<label class="mb-4 block">Engine
|
|
||||||
<select v-model="form.engine" class="mt-1 w-full rounded bg-zinc-800 p-2 outline-none ring-amber-300/40 focus:ring-2">
|
|
||||||
<template v-if="video">
|
|
||||||
<option value="minimax">MiniMax H3</option>
|
|
||||||
<option v-if="xaigen" value="ltx">LTX Pink Cherry</option>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<option value="flux">Flux Klein 9B Base</option>
|
|
||||||
<option value="krea" :disabled="['edit','compose','iterate'].includes(form.mode)">Krea 2 (Generate / Refine)</option>
|
|
||||||
</template>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<AspectRatioPick v-model="form.settings.aspect" heading="Aspect" :options="aspectOptions" />
|
<AspectRatioPick v-model="form.settings.aspect" heading="Aspect" :options="aspectOptions" />
|
||||||
<p class="my-3 text-sm text-zinc-400">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding, never stretch</p>
|
<p class="my-3 text-sm text-zinc-400">{{ resolvedSize.width }} × {{ resolvedSize.height }} · fit with padding, never stretch</p>
|
||||||
<label v-for="key in ['steps','cfg',...(video?['duration','fps']:[])]" :key="key" class="mb-4 block capitalize">{{ key }}
|
<label v-for="key in ['steps','cfg',...(video?['duration','fps']:[])]" :key="key" class="mb-4 block capitalize">{{ key }}
|
||||||
@@ -255,6 +293,9 @@ import {randomSeed} from '~/shared/studio2/seed.mjs'
|
|||||||
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
|
||||||
|
|
||||||
const xaigen=useRuntimeConfig().public.studio==='xaigen'
|
const xaigen=useRuntimeConfig().public.studio==='xaigen'
|
||||||
|
const {displayMode,isStudio,isClassic}=useDisplayMode()
|
||||||
|
const classicHideInput=ref(true)
|
||||||
|
const classicHideOutput=ref(true)
|
||||||
const picker=ref('')
|
const picker=ref('')
|
||||||
const uploading=ref(false),uploadMessage=ref('')
|
const uploading=ref(false),uploadMessage=ref('')
|
||||||
const revealed=ref<Set<string>>(new Set())
|
const revealed=ref<Set<string>>(new Set())
|
||||||
@@ -274,6 +315,23 @@ const route=useRoute()
|
|||||||
const tasks=['generate','edit','compose','refine','video','extend','music']
|
const tasks=['generate','edit','compose','refine','video','extend','music']
|
||||||
const settingsOpen=ref(false),detailsOpen=ref(false),submitting=ref(false),error=ref(''),batchText=ref('')
|
const settingsOpen=ref(false),detailsOpen=ref(false),submitting=ref(false),error=ref(''),batchText=ref('')
|
||||||
const video=computed(()=>['video','extend'].includes(form.mode))
|
const video=computed(()=>['video','extend'].includes(form.mode))
|
||||||
|
const kreaSticky=computed(()=>['edit','compose','iterate'].includes(form.mode))
|
||||||
|
const engineOptions=computed(()=>{
|
||||||
|
if(video.value){
|
||||||
|
return [
|
||||||
|
{value:'minimax',label:'MiniMax H3',disabled:false,hint:''},
|
||||||
|
{value:'ltx',label:'LTX',disabled:!xaigen,hint:xaigen?'':'xAIGen only'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{value:'flux',label:'Klein',disabled:false,hint:''},
|
||||||
|
{value:'krea',label:'Krea 2',disabled:kreaSticky.value,hint:kreaSticky.value?'sticky hero = Klein':''}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
watch(engineOptions,opts=>{
|
||||||
|
const current=opts.find(o=>o.value===form.engine)
|
||||||
|
if(!current || current.disabled)form.engine=opts.find(o=>!o.disabled)?.value || form.engine
|
||||||
|
})
|
||||||
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
|
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
|
||||||
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
|
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
|
||||||
const compiled=computed(()=>compilePrompt(form.promptSections,video.value,form))
|
const compiled=computed(()=>compilePrompt(form.promptSections,video.value,form))
|
||||||
@@ -292,8 +350,19 @@ const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id==
|
|||||||
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value))
|
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value))
|
||||||
|
|
||||||
function isHidden(asset:any){return previewHidden(asset,revealed.value,xaigen)}
|
function isHidden(asset:any){
|
||||||
function slotHidden(id:string){return xaigen && !!id && !revealed.value.has(id)}
|
if(!xaigen || !asset?.id)return false
|
||||||
|
if(isClassic.value){
|
||||||
|
const input=asset.role==='input'
|
||||||
|
return input?classicHideInput.value:classicHideOutput.value
|
||||||
|
}
|
||||||
|
return previewHidden(asset,revealed.value,xaigen)
|
||||||
|
}
|
||||||
|
function slotHidden(id:string){
|
||||||
|
if(!xaigen || !id)return false
|
||||||
|
if(isClassic.value)return classicHideInput.value
|
||||||
|
return !revealed.value.has(id)
|
||||||
|
}
|
||||||
function toggleReveal(id:string){
|
function toggleReveal(id:string){
|
||||||
if(!id)return
|
if(!id)return
|
||||||
const next=new Set(revealed.value)
|
const next=new Set(revealed.value)
|
||||||
@@ -323,6 +392,7 @@ function chooseTask(task:string){
|
|||||||
}
|
}
|
||||||
const was=video.value;form.mode=task;form.startClipId='';if(video.value!==was){form.engine=video.value?(xaigen && form.engine==='ltx'?'ltx':'minimax'):'flux';form.settings.steps=video.value?8:24;form.settings.cfg=video.value?1.5:4}
|
const was=video.value;form.mode=task;form.startClipId='';if(video.value!==was){form.engine=video.value?(xaigen && form.engine==='ltx'?'ltx':'minimax'):'flux';form.settings.steps=video.value?8:24;form.settings.cfg=video.value?1.5:4}
|
||||||
}
|
}
|
||||||
|
function onTaskSelect(event:Event){chooseTask((event.target as HTMLSelectElement).value)}
|
||||||
function chooseClip(id:string){if(!id){form.startClipId='';return}const clip=clips.value.find((c:any)=>c.id===id);if(clip){try{Object.assign(form,bindClip(form,clip,xaigen,loraMemory))}catch(e:any){error.value=e?.message||'Could not copy clip settings.';form.startClipId=clip.id;form.mode='extend';form.engine=xaigen && form.engine==='ltx'?'ltx':'minimax'}}}
|
function chooseClip(id:string){if(!id){form.startClipId='';return}const clip=clips.value.find((c:any)=>c.id===id);if(clip){try{Object.assign(form,bindClip(form,clip,xaigen,loraMemory))}catch(e:any){error.value=e?.message||'Could not copy clip settings.';form.startClipId=clip.id;form.mode='extend';form.engine=xaigen && form.engine==='ltx'?'ltx':'minimax'}}}
|
||||||
function selectOutput(item:any){selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)}
|
function selectOutput(item:any){selected.value=item;if(form.mode==='extend' && item?.kind==='clip')chooseClip(item.id)}
|
||||||
function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''}
|
function restore(item:any){Object.assign(form,restoreAll(form,item,xaigen,loraMemory));form.guides=form.guides||[];batchText.value=''}
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
export type DisplayMode = 'classic' | 'studio'
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'aigen.displayMode'
|
||||||
|
|
||||||
|
function parseMode(raw: unknown): DisplayMode | null {
|
||||||
|
const value = String(raw || '').trim().toLowerCase()
|
||||||
|
return value === 'classic' || value === 'studio' ? value : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStored(): DisplayMode {
|
||||||
|
if (!import.meta.client) return 'studio'
|
||||||
|
try {
|
||||||
|
return parseMode(localStorage.getItem(STORAGE_KEY)) || 'studio'
|
||||||
|
} catch {
|
||||||
|
return 'studio'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeStored(mode: DisplayMode) {
|
||||||
|
if (!import.meta.client) return
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, mode)
|
||||||
|
} catch { /* ignore quota / private mode */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Classic | Studio chrome skin. One Bench tree; layout only. */
|
||||||
|
export function useDisplayMode() {
|
||||||
|
const route = useRoute()
|
||||||
|
const displayMode = useState<DisplayMode>('aigen.displayMode', () => 'studio')
|
||||||
|
const sessionOverride = useState<DisplayMode | null>('aigen.displayMode.session', () => null)
|
||||||
|
const hydrated = useState('aigen.displayMode.hydrated', () => false)
|
||||||
|
|
||||||
|
const resolved = computed<DisplayMode>(() => {
|
||||||
|
const fromQuery = parseMode(route.query.display)
|
||||||
|
if (fromQuery) return fromQuery
|
||||||
|
if (sessionOverride.value) return sessionOverride.value
|
||||||
|
return displayMode.value
|
||||||
|
})
|
||||||
|
|
||||||
|
function setDisplayMode(mode: DisplayMode, opts?: { persist?: boolean; session?: boolean }) {
|
||||||
|
const persist = opts?.persist !== false
|
||||||
|
if (opts?.session) sessionOverride.value = mode
|
||||||
|
else sessionOverride.value = null
|
||||||
|
displayMode.value = mode
|
||||||
|
if (persist) writeStored(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
function hydrate() {
|
||||||
|
if (hydrated.value) return
|
||||||
|
hydrated.value = true
|
||||||
|
const fromQuery = parseMode(route.query.display)
|
||||||
|
if (fromQuery) {
|
||||||
|
sessionOverride.value = fromQuery
|
||||||
|
return
|
||||||
|
}
|
||||||
|
displayMode.value = readStored()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.client) {
|
||||||
|
onMounted(hydrate)
|
||||||
|
watch(
|
||||||
|
() => route.query.display,
|
||||||
|
(raw) => {
|
||||||
|
const fromQuery = parseMode(raw)
|
||||||
|
if (fromQuery) sessionOverride.value = fromQuery
|
||||||
|
else if (sessionOverride.value && !parseMode(route.query.display)) {
|
||||||
|
// Drop session override when query cleared; keep stored preference.
|
||||||
|
sessionOverride.value = null
|
||||||
|
displayMode.value = readStored()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
displayMode: resolved,
|
||||||
|
storedDisplayMode: displayMode,
|
||||||
|
setDisplayMode,
|
||||||
|
isStudio: computed(() => resolved.value === 'studio'),
|
||||||
|
isClassic: computed(() => resolved.value === 'classic')
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -4,7 +4,7 @@ const xaigenStudio = /^xaigen$/i.test(String(process.env.NUXT_PUBLIC_STUDIO || '
|
|||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2025-08-01',
|
compatibilityDate: '2025-08-01',
|
||||||
modules: ['@nuxtjs/tailwindcss'],
|
modules: ['@nuxtjs/tailwindcss'],
|
||||||
css: ['~/assets/css/main.css'],
|
css: ['~/assets/css/main.css', '~/assets/css/display.css'],
|
||||||
app: {
|
app: {
|
||||||
head: {
|
head: {
|
||||||
title: 'AIGen — MiniMax H3',
|
title: 'AIGen — MiniMax H3',
|
||||||
|
|||||||
Reference in New Issue
Block a user