Show a single Live GPU status, keep job progress visible across input tabs, and let the library move or delete in batches without switching folders.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+333
-131
@@ -6,24 +6,17 @@
|
|||||||
<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>
|
<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>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1>
|
<h1 class="font-display text-xl font-bold leading-none">{{ instanceName }}</h1>
|
||||||
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">MiniMax H3 · Flux.2 Klein · ComfyUI</p>
|
<p class="text-[11px] uppercase tracking-[0.22em] text-zinc-500">MiniMax H3 · Flux.2 Klein</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3 text-sm">
|
<div class="flex items-center gap-3 text-sm">
|
||||||
<span
|
<span
|
||||||
class="items-center gap-2 rounded-full border px-3 py-1"
|
class="hidden items-center gap-2 rounded-full border px-3 py-1 sm:inline-flex"
|
||||||
:class="[studioMode === 'video' ? 'hidden sm:inline-flex' : 'hidden xl:inline-flex opacity-60', comfyBadge.className]"
|
:class="rigBadge.className"
|
||||||
|
:title="rigBadge.title"
|
||||||
>
|
>
|
||||||
<span class="h-2 w-2 rounded-full" :class="comfyBadge.dot" />
|
<span class="h-2 w-2 rounded-full" :class="rigBadge.dot" />
|
||||||
{{ comfyBadge.label }}
|
{{ rigBadge.label }}
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="items-center gap-2 rounded-full border px-3 py-1"
|
|
||||||
:class="[studioMode === 'edit' ? 'hidden sm:inline-flex' : 'hidden xl:inline-flex opacity-60', imageComfyBadge.className]"
|
|
||||||
:title="imageComfyHost || 'Image edit on Beast'"
|
|
||||||
>
|
|
||||||
<span class="h-2 w-2 rounded-full" :class="imageComfyBadge.dot" />
|
|
||||||
{{ imageComfyBadge.label }}
|
|
||||||
</span>
|
</span>
|
||||||
<span class="hidden md:block text-zinc-400">{{ userName }}</span>
|
<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">
|
<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">
|
||||||
@@ -42,7 +35,7 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
:class="studioMode === 'video' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="studioMode === 'video' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
@click="studioMode = 'video'"
|
@click="studioMode = 'video'; outputFocus = 'video'"
|
||||||
>
|
>
|
||||||
Video
|
Video
|
||||||
</button>
|
</button>
|
||||||
@@ -50,15 +43,15 @@
|
|||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs font-medium"
|
class="rounded-full border px-3 py-1 text-xs font-medium"
|
||||||
:class="studioMode === 'edit' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
:class="studioMode === 'edit' ? 'border-amber-300/70 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400 hover:text-white'"
|
||||||
@click="studioMode = 'edit'"
|
@click="studioMode = 'edit'; outputFocus = 'edit'"
|
||||||
>
|
>
|
||||||
Image edit
|
Image edit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="font-display text-2xl font-bold">Input</h2>
|
<h2 class="font-display text-2xl font-bold">Input</h2>
|
||||||
<p class="text-sm text-zinc-400">{{ studioMode === 'edit'
|
<p class="text-sm text-zinc-400">{{ studioMode === 'edit'
|
||||||
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Both run on Beast; files are removed from the desktop after save.'
|
? 'Drop the still to edit. An optional second still applies image 2 onto image 1. Files leave the desktop after save.'
|
||||||
: 'Drop a still, describe the motion, then relay the job to the local desktop.' }}</p>
|
: 'Drop a still, describe the motion, then send the job to the desktop GPU.' }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -502,11 +495,7 @@
|
|||||||
<summary class="cursor-pointer text-sm font-medium">Image edit settings</summary>
|
<summary class="cursor-pointer text-sm font-medium">Image edit settings</summary>
|
||||||
<div class="mt-4 space-y-4">
|
<div class="mt-4 space-y-4">
|
||||||
<p class="text-xs text-zinc-500">
|
<p class="text-xs text-zinc-500">
|
||||||
Flux.2 Klein 9B on Beast. Sidecar is off.
|
Flux.2 Klein 9B. Shares the desktop GPU with video.
|
||||||
<span class="text-zinc-200">{{ imageComfyBadge.label }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-[11px] text-zinc-500">
|
|
||||||
Beast {{ imageBeastHost || imageComfyHost || 'unset' }}{{ comfyOk ? (comfyBusy ? ' · busy' : ' · free') : ' · offline' }}
|
|
||||||
</p>
|
</p>
|
||||||
<label class="block text-sm">
|
<label class="block text-sm">
|
||||||
<span class="mb-1 block text-zinc-400">Negative prompt</span>
|
<span class="mb-1 block text-zinc-400">Negative prompt</span>
|
||||||
@@ -540,7 +529,7 @@
|
|||||||
{{ studioMode === 'edit' ? (editBusy ? 'Editing…' : editLabel) : generateLabel }}
|
{{ studioMode === 'edit' ? (editBusy ? 'Editing…' : editLabel) : generateLabel }}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="studioMode === 'edit' ? editBusy : videoBusy"
|
v-if="videoBusy || editBusy"
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-2xl border border-red-400/40 px-4 py-3 text-red-300"
|
class="rounded-2xl border border-red-400/40 px-4 py-3 text-red-300"
|
||||||
@click="cancelJob"
|
@click="cancelJob"
|
||||||
@@ -572,32 +561,45 @@
|
|||||||
<section class="space-y-5">
|
<section class="space-y-5">
|
||||||
<div ref="outputPanel" class="glass rounded-3xl p-5 sm:p-6">
|
<div ref="outputPanel" class="glass 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">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
<h2 class="font-display text-2xl font-bold">Output</h2>
|
<h2 class="font-display text-2xl font-bold">Output</h2>
|
||||||
<p v-if="studioMode === 'edit'" class="text-xs text-zinc-500">
|
<button
|
||||||
{{ editElapsedLabel }}
|
v-if="canClearPreview"
|
||||||
<template v-if="editChainTotal > 1"> · chain {{ editChainStep }}/{{ editChainTotal }}</template>
|
type="button"
|
||||||
· step {{ editStep }}/{{ editMaxStep }}
|
class="rounded-full border border-white/15 px-2.5 py-0.5 text-xs text-zinc-300 hover:border-amber-300/50 hover:text-white"
|
||||||
</p>
|
@click="clearPreview"
|
||||||
<p v-else class="text-xs text-zinc-500">
|
>
|
||||||
{{ elapsedLabel }}
|
Clear preview
|
||||||
<template v-if="chainTotal > 1"> · chain {{ chainStep }}/{{ chainTotal }}</template>
|
</button>
|
||||||
· step {{ step }}/{{ maxStep || defaultVideoSteps(turbo, videoWorkflow) }}
|
</div>
|
||||||
|
<p class="text-xs text-zinc-500">
|
||||||
|
{{ liveElapsedLabel }}
|
||||||
|
<template v-if="outputProgressKind === 'edit' && editChainTotal > 1"> · chain {{ editChainStep }}/{{ editChainTotal }}</template>
|
||||||
|
<template v-else-if="outputProgressKind === 'video' && chainTotal > 1"> · chain {{ chainStep }}/{{ chainTotal }}</template>
|
||||||
|
· step {{ liveStep }}/{{ liveMaxStep }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="studioMode === 'edit' && videoBusy" class="mt-2 text-xs text-zinc-500">Video is using Beast. This edit waits in the Comfy queue.</p>
|
|
||||||
<p v-else-if="studioMode === 'video' && editBusy" class="mt-2 text-xs text-zinc-500">Image edit is using Beast. Video waits until that finishes.</p>
|
|
||||||
|
|
||||||
<div v-show="studioMode === 'video'">
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<div class="mb-2 flex items-center justify-between text-sm">
|
<div class="mb-2 flex items-center justify-between gap-3 text-sm">
|
||||||
<span :class="statusBusy ? 'text-amber-200' : 'text-zinc-300'">{{ statusMessage }}</span>
|
<span class="min-w-0 truncate" :class="liveStatusBusy ? 'text-amber-200' : 'text-zinc-300'">
|
||||||
<span class="text-amber-200">{{ progress }}%</span>
|
<span
|
||||||
|
v-if="outputProgressKind !== studioMode"
|
||||||
|
class="mr-2 inline-flex rounded-full border border-amber-300/30 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-amber-200"
|
||||||
|
>{{ outputProgressKind === 'edit' ? 'Edit' : 'Video' }}</span>
|
||||||
|
{{ liveStatusMessage }}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-amber-200">{{ liveProgressPct }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="chainTotal > 1" class="mb-2 text-xs text-amber-200/80">
|
<p v-if="outputProgressKind === 'video' && chainTotal > 1" class="mb-2 text-xs text-amber-200/80">
|
||||||
Step {{ chainStep }}/{{ chainTotal }}{{ chainLabel ? `: ${chainLabel}` : '' }}
|
Step {{ chainStep }}/{{ chainTotal }}{{ chainLabel ? `: ${chainLabel}` : '' }}
|
||||||
<template v-if="overallProgress"> · {{ overallProgress }}% overall</template>
|
<template v-if="overallProgress"> · {{ overallProgress }}% overall</template>
|
||||||
</p>
|
</p>
|
||||||
<ol v-if="videoBusy && activeChainPlan.length > 1" class="mb-3 space-y-1">
|
<p v-else-if="outputProgressKind === 'edit' && editChainTotal > 1" class="mb-2 text-xs text-amber-200/80">
|
||||||
|
Step {{ editChainStep }}/{{ editChainTotal }}{{ editChainLabel ? `: ${editChainLabel}` : '' }}
|
||||||
|
<template v-if="editOverallProgress"> · {{ editOverallProgress }}% overall</template>
|
||||||
|
</p>
|
||||||
|
<ol v-if="outputProgressKind === 'video' && videoBusy && activeChainPlan.length > 1" class="mb-3 space-y-1">
|
||||||
<li
|
<li
|
||||||
v-for="(part, index) in activeChainPlan"
|
v-for="(part, index) in activeChainPlan"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -610,10 +612,24 @@
|
|||||||
<span class="shrink-0 text-zinc-500">{{ formatClipDuration(part.duration) }}</span>
|
<span class="shrink-0 text-zinc-500">{{ formatClipDuration(part.duration) }}</span>
|
||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
<ol v-else-if="outputProgressKind === 'edit' && editBusy && editActiveChainPlan.length > 1" class="mb-3 space-y-1">
|
||||||
|
<li
|
||||||
|
v-for="(part, index) in editActiveChainPlan"
|
||||||
|
:key="index"
|
||||||
|
class="flex items-center gap-2 rounded-lg px-2 py-1 text-xs"
|
||||||
|
:class="editChainPartClass(index)"
|
||||||
|
>
|
||||||
|
<span class="h-2 w-2 shrink-0 rounded-full" :class="editChainPartDot(index)" />
|
||||||
|
<span class="shrink-0 font-medium">{{ part.label }}</span>
|
||||||
|
<span class="min-w-0 truncate text-zinc-400">{{ part.prompt }}</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
<div class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
<div class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
||||||
<div class="h-full rounded-full bg-amber-400 progress-glow transition-all duration-300" :style="{ width: progress + '%' }" />
|
<div class="h-full rounded-full bg-amber-400 progress-glow transition-all duration-300" :style="{ width: liveProgressPct + '%' }" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-show="outputPreviewKind === 'video'">
|
||||||
<div
|
<div
|
||||||
class="relative mt-5 overflow-hidden rounded-2xl bg-black/50"
|
class="relative mt-5 overflow-hidden rounded-2xl bg-black/50"
|
||||||
:class="{ 'sensitive-glow': awaitingReveal || lockedSave }"
|
:class="{ 'sensitive-glow': awaitingReveal || lockedSave }"
|
||||||
@@ -711,32 +727,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="studioMode === 'edit'">
|
<div v-show="outputPreviewKind === 'edit'">
|
||||||
<div class="mt-4">
|
|
||||||
<div class="mb-2 flex items-center justify-between text-sm">
|
|
||||||
<span :class="editStatusBusy ? 'text-amber-200' : 'text-zinc-300'">{{ editStatusMessage }}</span>
|
|
||||||
<span class="text-amber-200">{{ editProgress }}%</span>
|
|
||||||
</div>
|
|
||||||
<p v-if="editChainTotal > 1" class="mb-2 text-xs text-amber-200/80">
|
|
||||||
Step {{ editChainStep }}/{{ editChainTotal }}{{ editChainLabel ? `: ${editChainLabel}` : '' }}
|
|
||||||
<template v-if="editOverallProgress"> · {{ editOverallProgress }}% overall</template>
|
|
||||||
</p>
|
|
||||||
<ol v-if="editBusy && editActiveChainPlan.length > 1" class="mb-3 space-y-1">
|
|
||||||
<li
|
|
||||||
v-for="(part, index) in editActiveChainPlan"
|
|
||||||
:key="index"
|
|
||||||
class="flex items-center gap-2 rounded-lg px-2 py-1 text-xs"
|
|
||||||
:class="editChainPartClass(index)"
|
|
||||||
>
|
|
||||||
<span class="h-2 w-2 shrink-0 rounded-full" :class="editChainPartDot(index)" />
|
|
||||||
<span class="shrink-0 font-medium">{{ part.label }}</span>
|
|
||||||
<span class="min-w-0 truncate text-zinc-400">{{ part.prompt }}</span>
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
<div class="h-2 overflow-hidden rounded-full bg-zinc-800">
|
|
||||||
<div class="h-full rounded-full bg-amber-400 progress-glow transition-all duration-300" :style="{ width: (editOverallProgress || editProgress) + '%' }" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="relative mt-5 overflow-hidden rounded-2xl bg-black/50"
|
class="relative mt-5 overflow-hidden rounded-2xl bg-black/50"
|
||||||
:class="{ 'sensitive-glow': editAwaitingReveal || editLockedSave }"
|
:class="{ 'sensitive-glow': editAwaitingReveal || editLockedSave }"
|
||||||
@@ -793,10 +784,16 @@
|
|||||||
|
|
||||||
<div class="glass rounded-3xl p-5 sm:p-6">
|
<div class="glass 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">
|
||||||
<div>
|
|
||||||
<h2 class="font-display text-xl font-bold">Library</h2>
|
<h2 class="font-display text-xl font-bold">Library</h2>
|
||||||
<p class="mt-1 text-xs text-zinc-500">Generated outputs only. Uploaded stills live in the image picker.</p>
|
<div class="flex items-center gap-3">
|
||||||
</div>
|
<button
|
||||||
|
v-if="libraryEntries.length"
|
||||||
|
type="button"
|
||||||
|
class="text-xs text-zinc-400 hover:text-white"
|
||||||
|
@click="selectedKeys.length === libraryEntries.length ? clearSelection() : selectAllLibrary()"
|
||||||
|
>
|
||||||
|
{{ selectedKeys.length && selectedKeys.length === libraryEntries.length ? 'Clear selection' : 'Select all' }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="browseFolder?.protected && browseFolder.unlocked"
|
v-if="browseFolder?.protected && browseFolder.unlocked"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -806,6 +803,7 @@
|
|||||||
Lock folder
|
Lock folder
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form v-if="browseFolder?.protected && !browseFolder.unlocked" class="mt-4 space-y-3" @submit.prevent="unlockFolder(browseFolderId)">
|
<form v-if="browseFolder?.protected && !browseFolder.unlocked" class="mt-4 space-y-3" @submit.prevent="unlockFolder(browseFolderId)">
|
||||||
<p class="text-sm text-zinc-400">This folder is locked. Unlock to view {{ folderOutputCount(browseFolder) }} output{{ folderOutputCount(browseFolder) === 1 ? '' : 's' }}.</p>
|
<p class="text-sm text-zinc-400">This folder is locked. Unlock to view {{ folderOutputCount(browseFolder) }} output{{ folderOutputCount(browseFolder) === 1 ? '' : 's' }}.</p>
|
||||||
@@ -814,43 +812,76 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mt-4 flex flex-wrap gap-2">
|
<div class="mt-4 flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<div class="flex min-w-0 flex-wrap gap-1.5">
|
||||||
<button
|
<button
|
||||||
v-for="folder in folders"
|
v-for="folder in folders"
|
||||||
:key="folder.id"
|
:key="folder.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs"
|
class="rounded-full border px-2.5 py-1 text-[11px]"
|
||||||
:class="browseFolderId === folder.id ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400'"
|
:class="browseFolderId === folder.id ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400'"
|
||||||
@click="browseFolderId = folder.id"
|
@click="browseFolderId = folder.id"
|
||||||
>
|
>
|
||||||
{{ folder.name }}
|
{{ folder.name }}
|
||||||
<span class="text-zinc-500">({{ folderOutputCount(folder) }})</span>
|
<span class="text-zinc-500">{{ folderOutputCount(folder) }}</span>
|
||||||
<span v-if="folder.protected">{{ folder.unlocked ? '' : ' · lock' }}</span>
|
<span v-if="folder.protected && !folder.unlocked"> · lock</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="inline-flex rounded-full border border-white/10 p-0.5">
|
||||||
<div class="mt-3 flex flex-wrap gap-2">
|
|
||||||
<button
|
<button
|
||||||
v-for="option in [{ id: 'all', label: 'All' }, { id: 'images', label: 'Images' }, { id: 'videos', label: 'Videos' }] as const"
|
v-for="option in [{ id: 'all', label: 'All' }, { id: 'images', label: 'Images' }, { id: 'videos', label: 'Videos' }] as const"
|
||||||
:key="option.id"
|
:key="option.id"
|
||||||
type="button"
|
type="button"
|
||||||
class="rounded-full border px-3 py-1 text-xs"
|
class="rounded-full px-2.5 py-1 text-[11px]"
|
||||||
:class="libraryFilter === option.id ? 'border-amber-300 bg-amber-400/10 text-amber-100' : 'border-white/10 text-zinc-400'"
|
:class="libraryFilter === option.id ? 'bg-amber-400/15 text-amber-100' : 'text-zinc-500 hover:text-zinc-300'"
|
||||||
@click="libraryFilter = option.id"
|
@click="libraryFilter = option.id"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<p v-if="!libraryEntries.length" class="mt-4 text-sm text-zinc-500">{{ libraryEmptyLabel }}</p>
|
<p v-if="!libraryEntries.length" class="mt-4 text-sm text-zinc-500">{{ libraryEmptyLabel }}</p>
|
||||||
|
<div
|
||||||
|
v-if="selectedKeys.length"
|
||||||
|
class="mt-3 flex flex-wrap items-center gap-2 rounded-xl border border-amber-300/20 bg-amber-400/5 px-3 py-2 text-xs"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-amber-100">{{ selectedKeys.length }} selected</span>
|
||||||
|
<select
|
||||||
|
v-if="folders.length > 1"
|
||||||
|
class="max-w-[9.5rem] rounded-lg border border-white/10 bg-zinc-950 px-1.5 py-0.5 text-amber-300"
|
||||||
|
:value="''"
|
||||||
|
@change="onMoveSelected($event)"
|
||||||
|
>
|
||||||
|
<option disabled value="">Move to…</option>
|
||||||
|
<option
|
||||||
|
v-for="folder in folders.filter(folder => folder.id !== browseFolderId)"
|
||||||
|
:key="folder.id"
|
||||||
|
:value="folder.id"
|
||||||
|
>
|
||||||
|
{{ folder.name }}{{ folder.protected && !folder.unlocked ? ' · locked' : '' }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="text-red-300 hover:text-red-200" @click="removeSelected">Delete</button>
|
||||||
|
<button type="button" class="text-zinc-400 hover:text-white" @click="clearSelection">Clear</button>
|
||||||
|
</div>
|
||||||
<ul class="mt-4 space-y-3">
|
<ul class="mt-4 space-y-3">
|
||||||
<li
|
<li
|
||||||
v-for="entry in libraryEntries"
|
v-for="entry in libraryEntries"
|
||||||
:key="entry.key"
|
:key="entry.key"
|
||||||
class="rounded-2xl border border-white/5 bg-zinc-950/40 p-3"
|
class="rounded-2xl border p-3"
|
||||||
|
:class="selectedKeys.includes(entry.key) ? 'border-amber-300/30 bg-amber-400/5' : 'border-white/5 bg-zinc-950/40'"
|
||||||
>
|
>
|
||||||
<template v-if="entry.kind === 'clip'">
|
<template v-if="entry.kind === 'clip'">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
|
<label class="flex shrink-0 items-start pt-1" @click.stop>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
|
||||||
|
:checked="selectedKeys.includes(entry.key)"
|
||||||
|
@click.prevent="onSelectClick(entry.key, $event)"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openClip(entry.group.head)">
|
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openClip(entry.group.head)">
|
||||||
<span v-if="entry.group.head.hideThumbnail" class="sensitive-glow block h-full w-full" />
|
<span v-if="entry.group.head.hideThumbnail" class="sensitive-glow block h-full w-full" />
|
||||||
<img v-else-if="entry.group.head.hasThumbnail" :src="`/api/library/clips/${entry.group.head.id}/thumb`" alt="" class="h-full w-full object-cover">
|
<img v-else-if="entry.group.head.hasThumbnail" :src="`/api/library/clips/${entry.group.head.id}/thumb`" alt="" class="h-full w-full object-cover">
|
||||||
@@ -982,6 +1013,14 @@
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
<template v-else-if="entry.kind === 'still' && entry.stillGroup">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
|
<label class="flex shrink-0 items-start pt-1" @click.stop>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="mt-0.5 h-4 w-4 rounded border-white/20 bg-zinc-950 accent-amber-400"
|
||||||
|
:checked="selectedKeys.includes(entry.key)"
|
||||||
|
@click.prevent="onSelectClick(entry.key, $event)"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openStill(entry.stillGroup.head)">
|
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openStill(entry.stillGroup.head)">
|
||||||
<span v-if="entry.stillGroup.head.hideInput" class="sensitive-glow block h-full w-full" />
|
<span v-if="entry.stillGroup.head.hideInput" class="sensitive-glow block h-full w-full" />
|
||||||
<img
|
<img
|
||||||
@@ -1293,6 +1332,7 @@ const EDIT_JOB_STORE = 'aigen-active-edit-job'
|
|||||||
const videoBusy = ref(false)
|
const videoBusy = ref(false)
|
||||||
const editBusy = ref(false)
|
const editBusy = ref(false)
|
||||||
const studioMode = ref('video')
|
const studioMode = ref('video')
|
||||||
|
const outputFocus = ref<'video' | 'edit'>('video')
|
||||||
const imageComfyOk = ref(false)
|
const imageComfyOk = ref(false)
|
||||||
const imageComfyBusy = ref(false)
|
const imageComfyBusy = ref(false)
|
||||||
const imageComfyConfigured = ref(false)
|
const imageComfyConfigured = ref(false)
|
||||||
@@ -1377,6 +1417,8 @@ const settingsOpen = ref(false)
|
|||||||
const pickerOpen = ref(false)
|
const pickerOpen = ref(false)
|
||||||
const pickerSlot = ref<'main' | 'editRef' | number>('main')
|
const pickerSlot = ref<'main' | 'editRef' | number>('main')
|
||||||
const libraryFilter = ref<'all' | 'images' | 'videos'>('all')
|
const libraryFilter = ref<'all' | 'images' | 'videos'>('all')
|
||||||
|
const selectedKeys = ref<string[]>([])
|
||||||
|
const lastSelectedKey = ref('')
|
||||||
const videoWorkflow = ref('v1')
|
const videoWorkflow = ref('v1')
|
||||||
const useIdentityRefs = ref(false)
|
const useIdentityRefs = ref(false)
|
||||||
const identityRefs = ref<(File | null)[]>([null, null, null, null])
|
const identityRefs = ref<(File | null)[]>([null, null, null, null])
|
||||||
@@ -1409,56 +1451,75 @@ function formatElapsed(ms: number) {
|
|||||||
const elapsedLabel = computed(() => formatElapsed(elapsedMs.value))
|
const elapsedLabel = computed(() => formatElapsed(elapsedMs.value))
|
||||||
const editElapsedLabel = computed(() => formatElapsed(editElapsedMs.value))
|
const editElapsedLabel = computed(() => formatElapsed(editElapsedMs.value))
|
||||||
|
|
||||||
const comfyBadge = computed(() => {
|
const outputProgressKind = computed<'video' | 'edit'>(() => {
|
||||||
if (!comfyOk.value) {
|
if (editBusy.value && !videoBusy.value) return 'edit'
|
||||||
|
if (videoBusy.value && !editBusy.value) return 'video'
|
||||||
|
return outputFocus.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const outputPreviewKind = computed<'video' | 'edit'>(() => {
|
||||||
|
if (outputFocus.value === 'video') {
|
||||||
|
if (editBusy.value && !videoBusy.value && !videoUrl.value) return 'edit'
|
||||||
|
return 'video'
|
||||||
|
}
|
||||||
|
if (videoBusy.value && !editBusy.value && !editResultUrl.value) return 'video'
|
||||||
|
return 'edit'
|
||||||
|
})
|
||||||
|
|
||||||
|
const liveElapsedLabel = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit' ? editElapsedLabel.value : elapsedLabel.value
|
||||||
|
))
|
||||||
|
const liveStatusMessage = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit' ? editStatusMessage.value : statusMessage.value
|
||||||
|
))
|
||||||
|
const liveStatusBusy = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit' ? editStatusBusy.value : statusBusy.value
|
||||||
|
))
|
||||||
|
const liveProgressPct = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit'
|
||||||
|
? (editOverallProgress.value || editProgress.value)
|
||||||
|
: progress.value
|
||||||
|
))
|
||||||
|
const liveStep = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit' ? editStep.value : step.value
|
||||||
|
))
|
||||||
|
const liveMaxStep = computed(() => (
|
||||||
|
outputProgressKind.value === 'edit'
|
||||||
|
? editMaxStep.value
|
||||||
|
: (maxStep.value || defaultVideoSteps(turbo.value, videoWorkflow.value))
|
||||||
|
))
|
||||||
|
|
||||||
|
const rigBusy = computed(() => (
|
||||||
|
comfyBusy.value || imageComfyBusy.value || videoBusy.value || editBusy.value
|
||||||
|
))
|
||||||
|
const rigBadge = computed(() => {
|
||||||
|
if (!comfyOk.value && !imageComfyOk.value) {
|
||||||
return {
|
return {
|
||||||
label: 'ComfyUI offline',
|
label: 'Offline',
|
||||||
className: 'border-red-400/30 text-red-300',
|
className: 'border-red-400/30 text-red-300',
|
||||||
dot: 'bg-red-400'
|
dot: 'bg-red-400',
|
||||||
|
title: 'Desktop GPU is unreachable'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (comfyBusy.value) {
|
if (rigBusy.value) {
|
||||||
return {
|
return {
|
||||||
label: 'ComfyUI busy',
|
label: 'Live',
|
||||||
className: 'border-amber-400/40 text-amber-200',
|
className: 'border-amber-400/40 text-amber-200',
|
||||||
dot: 'bg-amber-400'
|
dot: 'bg-amber-400 animate-pulse',
|
||||||
|
title: 'Desktop GPU is rendering'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
label: 'ComfyUI online',
|
label: 'Ready',
|
||||||
className: 'border-emerald-400/30 text-emerald-300',
|
className: 'border-emerald-400/30 text-emerald-300',
|
||||||
dot: 'bg-emerald-400'
|
dot: 'bg-emerald-400',
|
||||||
|
title: 'Desktop GPU is free'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageComfyBadge = computed(() => {
|
const canClearPreview = computed(() => Boolean(
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) {
|
videoUrl.value || editResultUrl.value || extendOpen.value || lockedSave.value || editLockedSave.value
|
||||||
return {
|
))
|
||||||
label: 'Edit unset',
|
|
||||||
className: 'border-white/10 text-zinc-500',
|
|
||||||
dot: 'bg-zinc-500'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (comfyOk.value && !comfyBusy.value) {
|
|
||||||
return {
|
|
||||||
label: 'Beast · Klein ready',
|
|
||||||
className: 'border-emerald-400/30 text-emerald-300',
|
|
||||||
dot: 'bg-emerald-400'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (comfyBusy.value) {
|
|
||||||
return {
|
|
||||||
label: 'Beast · busy',
|
|
||||||
className: 'border-amber-400/40 text-amber-200',
|
|
||||||
dot: 'bg-amber-400'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
label: 'Beast offline',
|
|
||||||
className: 'border-red-400/30 text-red-300',
|
|
||||||
dot: 'bg-red-400'
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function snap32(value: number) {
|
function snap32(value: number) {
|
||||||
return Math.max(32, Math.round(value / 32) * 32)
|
return Math.max(32, Math.round(value / 32) * 32)
|
||||||
@@ -1523,7 +1584,7 @@ const editBlocked = computed(() => {
|
|||||||
})
|
})
|
||||||
const editBlockReason = computed(() => {
|
const editBlockReason = computed(() => {
|
||||||
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
if (!imageComfyConfigured.value && !comfyOk.value) return 'Image edit is not configured. Set COMFY_HOST.'
|
||||||
if (!comfyOk.value && !imageComfyOk.value) return 'Beast is offline.'
|
if (!comfyOk.value && !imageComfyOk.value) return 'Desktop GPU is offline.'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
const generateLabel = computed(() => {
|
const generateLabel = computed(() => {
|
||||||
@@ -1903,6 +1964,19 @@ watch(folderId, () => {
|
|||||||
applyThumbnailDefault()
|
applyThumbnailDefault()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch([browseFolderId, libraryFilter], () => {
|
||||||
|
selectedKeys.value = []
|
||||||
|
lastSelectedKey.value = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(editBusy, (busy) => {
|
||||||
|
if (busy) outputFocus.value = 'edit'
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(videoBusy, (busy) => {
|
||||||
|
if (busy) outputFocus.value = 'video'
|
||||||
|
})
|
||||||
|
|
||||||
watch(hideInputPreview, (value) => {
|
watch(hideInputPreview, (value) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('aigen-hide-input-preview', String(value))
|
localStorage.setItem('aigen-hide-input-preview', String(value))
|
||||||
@@ -2303,6 +2377,7 @@ async function loadRerunImage(item: LibraryClip, parts: LibraryClip[]) {
|
|||||||
|
|
||||||
async function rerun(item: LibraryClip, collection = false) {
|
async function rerun(item: LibraryClip, collection = false) {
|
||||||
studioMode.value = 'video'
|
studioMode.value = 'video'
|
||||||
|
outputFocus.value = 'video'
|
||||||
closeExtend()
|
closeExtend()
|
||||||
const parts = familyPartsFor(item)
|
const parts = familyPartsFor(item)
|
||||||
const target = collection ? parts[0] || item : item
|
const target = collection ? parts[0] || item : item
|
||||||
@@ -2518,6 +2593,7 @@ async function generateExtension() {
|
|||||||
|
|
||||||
function openClip(item: LibraryClip) {
|
function openClip(item: LibraryClip) {
|
||||||
studioMode.value = 'video'
|
studioMode.value = 'video'
|
||||||
|
outputFocus.value = 'video'
|
||||||
closeExtend()
|
closeExtend()
|
||||||
watchingLibrary.value = true
|
watchingLibrary.value = true
|
||||||
currentClipId.value = item.id
|
currentClipId.value = item.id
|
||||||
@@ -2536,6 +2612,7 @@ function openClip(item: LibraryClip) {
|
|||||||
|
|
||||||
function openStill(still: LibraryStill) {
|
function openStill(still: LibraryStill) {
|
||||||
studioMode.value = 'edit'
|
studioMode.value = 'edit'
|
||||||
|
outputFocus.value = 'edit'
|
||||||
browseFolderId.value = still.folderId
|
browseFolderId.value = still.folderId
|
||||||
editResultUrl.value = `/api/library/stills/${still.id}`
|
editResultUrl.value = `/api/library/stills/${still.id}`
|
||||||
concealEditOutput.value = still.hideInput === true
|
concealEditOutput.value = still.hideInput === true
|
||||||
@@ -2594,10 +2671,7 @@ async function onMoveStill(still: LibraryStill, event: Event, family = false) {
|
|||||||
body: { folderId: destId, family: family || undefined }
|
body: { folderId: destId, family: family || undefined }
|
||||||
}))
|
}))
|
||||||
const dest = folders.value.find(folder => folder.id === destId)
|
const dest = folders.value.find(folder => folder.id === destId)
|
||||||
if (dest?.unlocked) browseFolderId.value = destId
|
toast(`Moved ${family ? 'edit chain' : 'still'} to ${dest?.name || 'folder'}${dest?.unlocked ? '' : ' (locked)'}`)
|
||||||
editStatusMessage.value = dest?.unlocked
|
|
||||||
? `Moved ${family ? 'edit chain' : 'still'} to ${dest.name}`
|
|
||||||
: `Moved ${family ? 'edit chain' : 'still'} to ${dest?.name || 'folder'} (locked)`
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast(error?.data?.statusMessage || 'Could not move image')
|
toast(error?.data?.statusMessage || 'Could not move image')
|
||||||
}
|
}
|
||||||
@@ -2647,6 +2721,133 @@ function stillFamilyPartsFor(still: LibraryStill) {
|
|||||||
return chronologicalStills(group?.parts || [still])
|
return chronologicalStills(group?.parts || [still])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearPreview() {
|
||||||
|
closeExtend()
|
||||||
|
videoUrl.value = ''
|
||||||
|
awaitingReveal.value = false
|
||||||
|
concealOutput.value = false
|
||||||
|
lockedSave.value = false
|
||||||
|
currentClipId.value = ''
|
||||||
|
watchingLibrary.value = false
|
||||||
|
editResultUrl.value = ''
|
||||||
|
editAwaitingReveal.value = false
|
||||||
|
concealEditOutput.value = false
|
||||||
|
editLockedSave.value = false
|
||||||
|
if (!videoBusy.value) {
|
||||||
|
statusMessage.value = 'Waiting for a still.'
|
||||||
|
progress.value = 0
|
||||||
|
}
|
||||||
|
if (!editBusy.value) {
|
||||||
|
editStatusMessage.value = 'Waiting for a still.'
|
||||||
|
editProgress.value = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectedLibraryEntries() {
|
||||||
|
const chosen = new Set(selectedKeys.value)
|
||||||
|
return libraryEntries.value.filter(entry => chosen.has(entry.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearSelection() {
|
||||||
|
selectedKeys.value = []
|
||||||
|
lastSelectedKey.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAllLibrary() {
|
||||||
|
selectedKeys.value = libraryEntries.value.map(entry => entry.key)
|
||||||
|
lastSelectedKey.value = selectedKeys.value.at(-1) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectClick(key: string, event: MouseEvent) {
|
||||||
|
const keys = libraryEntries.value.map(entry => entry.key)
|
||||||
|
if (event.shiftKey && lastSelectedKey.value) {
|
||||||
|
const start = keys.indexOf(lastSelectedKey.value)
|
||||||
|
const end = keys.indexOf(key)
|
||||||
|
if (start >= 0 && end >= 0) {
|
||||||
|
const [from, to] = start < end ? [start, end] : [end, start]
|
||||||
|
const range = keys.slice(from, to + 1)
|
||||||
|
selectedKeys.value = [...new Set([...selectedKeys.value, ...range])]
|
||||||
|
lastSelectedKey.value = key
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedKeys.value.includes(key)) {
|
||||||
|
selectedKeys.value = selectedKeys.value.filter(item => item !== key)
|
||||||
|
} else {
|
||||||
|
selectedKeys.value = [...selectedKeys.value, key]
|
||||||
|
}
|
||||||
|
lastSelectedKey.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onMoveSelected(event: Event) {
|
||||||
|
const select = event.target as HTMLSelectElement
|
||||||
|
const destId = select.value
|
||||||
|
select.value = ''
|
||||||
|
if (!destId) return
|
||||||
|
const items = selectedLibraryEntries()
|
||||||
|
if (!items.length) return
|
||||||
|
const dest = folders.value.find(folder => folder.id === destId)
|
||||||
|
const results = await Promise.allSettled(items.map(entry => {
|
||||||
|
if (entry.kind === 'clip') {
|
||||||
|
return $fetch(`/api/library/clips/${entry.group.head.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { folderId: destId, family: true }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return $fetch(`/api/library/stills/${entry.stillGroup!.head.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { folderId: destId, family: true }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
await loadLibrary()
|
||||||
|
const failed = results.filter(result => result.status === 'rejected').length
|
||||||
|
const moved = items.length - failed
|
||||||
|
clearSelection()
|
||||||
|
if (failed) toast(`${moved} moved to ${dest?.name || 'folder'}. ${failed} could not be moved.`)
|
||||||
|
else toast(`Moved ${moved} to ${dest?.name || 'folder'}${dest?.unlocked ? '' : ' (locked)'}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSelected() {
|
||||||
|
const items = selectedLibraryEntries()
|
||||||
|
if (!items.length) return
|
||||||
|
const label = items.length === 1 ? 'this item' : `these ${items.length} items`
|
||||||
|
if (!confirm(`Remove ${label} from this folder?`)) return
|
||||||
|
const clipIds = new Set<string>()
|
||||||
|
const stillIds = new Set<string>()
|
||||||
|
const results = await Promise.allSettled(items.map(entry => {
|
||||||
|
if (entry.kind === 'clip') {
|
||||||
|
for (const part of entry.group.parts) clipIds.add(part.id)
|
||||||
|
return $fetch(`/api/library/clips/${entry.group.head.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
query: { family: '1' }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for (const part of entry.stillGroup!.parts) stillIds.add(part.id)
|
||||||
|
return $fetch(`/api/library/stills/${entry.stillGroup!.head.id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
query: { family: '1' }
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
if ([...clipIds].some(id => videoUrl.value.includes(id) || currentClipId.value === id)) {
|
||||||
|
videoUrl.value = ''
|
||||||
|
awaitingReveal.value = false
|
||||||
|
concealOutput.value = false
|
||||||
|
lockedSave.value = false
|
||||||
|
currentClipId.value = ''
|
||||||
|
closeExtend()
|
||||||
|
}
|
||||||
|
if ([...stillIds].some(id => editResultUrl.value.includes(id))) {
|
||||||
|
editResultUrl.value = ''
|
||||||
|
editAwaitingReveal.value = false
|
||||||
|
concealEditOutput.value = false
|
||||||
|
editLockedSave.value = false
|
||||||
|
}
|
||||||
|
await loadLibrary()
|
||||||
|
const failed = results.filter(result => result.status === 'rejected').length
|
||||||
|
clearSelection()
|
||||||
|
if (failed) toast(`${items.length - failed} removed. ${failed} could not be deleted.`)
|
||||||
|
}
|
||||||
|
|
||||||
async function removeClip(item: LibraryClip, family = false) {
|
async function removeClip(item: LibraryClip, family = false) {
|
||||||
const watched = family ? familyPartsFor(item) : [item]
|
const watched = family ? familyPartsFor(item) : [item]
|
||||||
try {
|
try {
|
||||||
@@ -2680,10 +2881,7 @@ async function onMoveClip(item: LibraryClip, event: Event, family = false) {
|
|||||||
body: { folderId: destId, family: family || undefined }
|
body: { folderId: destId, family: family || undefined }
|
||||||
}))
|
}))
|
||||||
const dest = folders.value.find(folder => folder.id === destId)
|
const dest = folders.value.find(folder => folder.id === destId)
|
||||||
if (dest?.unlocked) browseFolderId.value = destId
|
toast(`Moved ${family ? 'collection' : 'clip'} to ${dest?.name || 'folder'}${dest?.unlocked ? '' : ' (locked)'}`)
|
||||||
statusMessage.value = dest?.unlocked
|
|
||||||
? `Moved ${family ? 'collection' : 'clip'} to ${dest.name}`
|
|
||||||
: `Moved ${family ? 'collection' : 'clip'} to ${dest?.name || 'folder'} (locked)`
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast(error?.data?.statusMessage || 'Could not move clip')
|
toast(error?.data?.statusMessage || 'Could not move clip')
|
||||||
}
|
}
|
||||||
@@ -3260,16 +3458,20 @@ function listen(kind: 'video' | 'edit', id: string, hidden: boolean, folderLocke
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function cancelJob() {
|
async function cancelJob() {
|
||||||
const kind = studioMode.value === 'edit' ? 'edit' : 'video'
|
const kind = outputProgressKind.value === 'edit' && editBusy.value
|
||||||
|
? 'edit'
|
||||||
|
: videoBusy.value
|
||||||
|
? 'video'
|
||||||
|
: studioMode.value === 'edit' ? 'edit' : 'video'
|
||||||
const id = kind === 'edit' ? editJobId.value : videoJobId.value
|
const id = kind === 'edit' ? editJobId.value : videoJobId.value
|
||||||
try {
|
try {
|
||||||
await $fetch('/api/interrupt', { method: 'POST', body: { jobId: id, kind } })
|
await $fetch('/api/interrupt', { method: 'POST', body: { jobId: id, kind } })
|
||||||
if (kind === 'edit') editStatusMessage.value = 'Edit cancelled. Sidecar is idle — edit again.'
|
if (kind === 'edit') editStatusMessage.value = 'Edit cancelled. GPU is idle — edit again.'
|
||||||
else statusMessage.value = 'Job cancelled. ComfyUI is idle — generate again.'
|
else statusMessage.value = 'Job cancelled. GPU is idle — generate again.'
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast(error?.statusMessage || error?.data?.statusMessage || 'Failed to interrupt ComfyUI')
|
toast(error?.statusMessage || error?.data?.statusMessage || 'Failed to interrupt the job')
|
||||||
if (kind === 'edit') editStatusMessage.value = 'Cancelled locally. Edit again when a box is free.'
|
if (kind === 'edit') editStatusMessage.value = 'Cancelled locally. Edit again when the GPU is free.'
|
||||||
else statusMessage.value = 'Cancelled locally. Generate again when ComfyUI is free.'
|
else statusMessage.value = 'Cancelled locally. Generate again when the GPU is free.'
|
||||||
}
|
}
|
||||||
if (kind === 'edit') {
|
if (kind === 'edit') {
|
||||||
editSettledUi = true
|
editSettledUi = true
|
||||||
|
|||||||
Reference in New Issue
Block a user