Fix library thumbnails and let clips keep a display name.
Extract a poster frame when a still-thumb is missing, and show the clip name in the library with the prompt on the info icon. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+51
-5
@@ -87,6 +87,15 @@
|
||||
{{ folder.name }}{{ folder.protected ? (folder.unlocked ? ' · unlocked' : ' · locked') : '' }}
|
||||
</option>
|
||||
</select>
|
||||
<label class="block text-sm">
|
||||
<span class="mb-1 block text-zinc-400">Name</span>
|
||||
<input
|
||||
v-model="clipName"
|
||||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
|
||||
placeholder="Shown in the library and used for the download filename"
|
||||
maxlength="80"
|
||||
>
|
||||
</label>
|
||||
<form v-if="currentFolder?.protected && !currentFolder.unlocked" class="space-y-2" @submit.prevent="unlockFolder(folderId)">
|
||||
<p class="text-sm text-zinc-400">Unlock this folder to generate into it.</p>
|
||||
<div class="flex gap-2">
|
||||
@@ -200,7 +209,7 @@
|
||||
<a
|
||||
v-if="videoUrl && !awaitingReveal"
|
||||
:href="videoUrl"
|
||||
download="aigen.mp4"
|
||||
:download="`${outputDownloadName}.mp4`"
|
||||
class="mt-4 inline-flex rounded-2xl border border-white/15 px-4 py-2 text-sm hover:border-amber-300/50"
|
||||
>
|
||||
Download MP4
|
||||
@@ -247,11 +256,36 @@
|
||||
<li v-for="item in visibleClips" :key="item.id" class="flex gap-3 rounded-2xl border border-white/5 bg-zinc-950/40 p-3">
|
||||
<button type="button" class="relative h-16 w-16 shrink-0 overflow-hidden rounded-xl bg-zinc-900" @click="openClip(item)">
|
||||
<span v-if="item.hideThumbnail" class="sensitive-glow block h-full w-full" />
|
||||
<img v-else :src="`/api/library/clips/${item.id}/thumb`" alt="" class="h-full w-full object-cover">
|
||||
<img v-else-if="item.hasThumbnail" :src="`/api/library/clips/${item.id}/thumb`" alt="" class="h-full w-full object-cover">
|
||||
<video
|
||||
v-else
|
||||
:src="`/api/library/clips/${item.id}/video#t=0.1`"
|
||||
class="h-full w-full object-cover"
|
||||
muted
|
||||
playsinline
|
||||
preload="metadata"
|
||||
/>
|
||||
</button>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm">{{ item.prompt }}</p>
|
||||
<p class="text-xs text-zinc-500">{{ item.width }}×{{ item.height }} · {{ item.steps }} steps · seed {{ item.seed }}</p>
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="min-w-0 flex-1 truncate text-sm font-medium">{{ item.name || item.prompt }}</p>
|
||||
<div class="group relative shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-6 w-6 items-center justify-center rounded-full text-zinc-500 hover:text-amber-200"
|
||||
:aria-label="'Prompt for ' + (item.name || 'clip')"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="h-4 w-4">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4M12 8h.01" stroke-linecap="round" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="pointer-events-none invisible absolute right-0 top-7 z-20 w-64 whitespace-pre-wrap rounded-xl border border-white/10 bg-zinc-950 p-3 text-xs leading-relaxed text-zinc-300 shadow-2xl group-hover:visible group-focus-within:visible">
|
||||
{{ item.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-zinc-500">{{ item.width && item.height ? `${item.width}×${item.height}` : 'size unknown' }} · {{ item.steps || '—' }} steps · seed {{ item.seed || '—' }}</p>
|
||||
<div class="mt-2 flex gap-3">
|
||||
<button type="button" class="text-xs text-amber-300" @click="openClip(item)">Open</button>
|
||||
<button type="button" class="text-xs text-amber-300" @click="rerun(item)">Rerun</button>
|
||||
@@ -320,6 +354,7 @@ interface LibraryStill {
|
||||
interface LibraryClip {
|
||||
id: string
|
||||
folderId: string
|
||||
name?: string
|
||||
prompt: string
|
||||
aspect: string
|
||||
width: number
|
||||
@@ -328,6 +363,7 @@ interface LibraryClip {
|
||||
turbo: boolean
|
||||
seed: number
|
||||
hideThumbnail: boolean
|
||||
hasThumbnail?: boolean
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
@@ -349,6 +385,8 @@ const file = ref<File | null>(null)
|
||||
const preview = ref('')
|
||||
const dragging = ref(false)
|
||||
const prompt = ref('')
|
||||
const clipName = ref('')
|
||||
const outputDownloadName = ref('aigen')
|
||||
const aspect = ref<AspectId>('auto')
|
||||
const imageWidth = ref(0)
|
||||
const imageHeight = ref(0)
|
||||
@@ -543,8 +581,13 @@ function stopTimer() {
|
||||
timer = null
|
||||
}
|
||||
|
||||
function clipFileName(name: string) {
|
||||
return name.replace(/[<>:"/\\|?*'`]/g, '-').replace(/\s+/g, ' ').trim().slice(0, 80) || 'aigen'
|
||||
}
|
||||
|
||||
function rerun(item: LibraryClip) {
|
||||
prompt.value = item.prompt
|
||||
clipName.value = item.name || ''
|
||||
turbo.value = item.turbo
|
||||
seedInput.value = String(item.seed)
|
||||
aspect.value = (item.aspect as AspectId) || 'auto'
|
||||
@@ -574,8 +617,9 @@ async function lockFolder(id: string) {
|
||||
function openClip(item: LibraryClip) {
|
||||
videoUrl.value = `/api/library/clips/${item.id}/video`
|
||||
awaitingReveal.value = item.hideThumbnail
|
||||
playerSize.value = { width: item.width, height: item.height }
|
||||
playerSize.value = { width: item.width || 1344, height: item.height || 768 }
|
||||
browseFolderId.value = item.folderId
|
||||
outputDownloadName.value = clipFileName(item.name || item.prompt.split(/[.!?\n]/)[0] || 'aigen')
|
||||
statusMessage.value = item.hideThumbnail ? 'Hidden clip ready' : 'Loaded from library'
|
||||
progress.value = 100
|
||||
}
|
||||
@@ -631,6 +675,8 @@ async function generate() {
|
||||
body.append('seed', seedInput.value || 'random')
|
||||
body.append('folderId', folderId.value)
|
||||
body.append('hideThumbnail', String(hideThumbnail.value))
|
||||
body.append('name', clipName.value.trim())
|
||||
outputDownloadName.value = clipFileName(clipName.value.trim() || prompt.value.trim().split(/[.!?\n]/)[0] || 'aigen')
|
||||
const started = await $fetch<{ jobId: string; seed: number; width: number; height: number; steps: number; turbo: boolean; hideThumbnail?: boolean }>('/api/generate', {
|
||||
method: 'POST',
|
||||
body
|
||||
|
||||
Reference in New Issue
Block a user