Make keep-prompt snippets clickable and allow renaming the start still.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,25 +2,31 @@
|
|||||||
<div ref="root" class="relative shrink-0">
|
<div ref="root" class="relative shrink-0">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="flex h-7 w-7 items-center justify-center rounded-full border border-white/15 text-lg leading-none text-amber-200 hover:border-amber-300/50 hover:text-amber-100 disabled:cursor-not-allowed disabled:opacity-40"
|
class="rounded-full border px-3 py-1 text-xs font-medium disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
:class="open || items.length
|
||||||
|
? 'border-amber-300/40 text-amber-100 hover:bg-amber-400/10'
|
||||||
|
: 'border-white/15 text-zinc-400 hover:border-amber-300/40 hover:text-amber-100'"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
:title="items.length ? 'Add a saved prompt' : 'Add snippets in Settings'"
|
:title="items.length ? 'Add a saved snippet to the prompt' : 'Add snippets in Settings → Keep prompts'"
|
||||||
:aria-expanded="open"
|
:aria-expanded="open"
|
||||||
aria-haspopup="menu"
|
aria-haspopup="menu"
|
||||||
aria-label="Add saved prompt"
|
aria-label="Add saved prompt snippet"
|
||||||
@click.stop="open = !open"
|
@click.stop="toggle"
|
||||||
>
|
>
|
||||||
+
|
Snippets
|
||||||
</button>
|
</button>
|
||||||
|
<Teleport to="body">
|
||||||
<div
|
<div
|
||||||
v-if="open"
|
v-if="open"
|
||||||
class="absolute top-full z-30 mt-1 w-72 max-w-[calc(100vw-2rem)] overflow-hidden rounded-2xl border border-white/10 bg-zinc-950 shadow-2xl"
|
ref="menu"
|
||||||
:class="align === 'left' ? 'left-0' : 'right-0'"
|
class="fixed z-[80] w-72 max-w-[calc(100vw-1rem)] overflow-hidden rounded-2xl border border-white/10 bg-zinc-950 shadow-2xl"
|
||||||
|
:style="menuStyle"
|
||||||
role="menu"
|
role="menu"
|
||||||
|
@click.stop
|
||||||
>
|
>
|
||||||
<p class="px-3 pt-2 text-[10px] uppercase tracking-wide text-zinc-500">{{ heading }}</p>
|
<p class="px-3 pt-2 text-[10px] uppercase tracking-wide text-zinc-500">{{ heading }}</p>
|
||||||
<p v-if="!items.length" class="px-3 py-3 text-xs text-zinc-500">
|
<p v-if="!items.length" class="px-3 py-3 text-xs text-zinc-500">
|
||||||
No saved snippets yet. Add video and image lists in Settings.
|
No snippets in this list yet. Add them in Settings → Keep prompts.
|
||||||
</p>
|
</p>
|
||||||
<ul v-else class="max-h-64 overflow-y-auto p-1">
|
<ul v-else class="max-h-64 overflow-y-auto p-1">
|
||||||
<li v-for="item in items" :key="item.id">
|
<li v-for="item in items" :key="item.id">
|
||||||
@@ -36,6 +42,7 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -50,7 +57,7 @@ const props = withDefaults(defineProps<{
|
|||||||
}>(), {
|
}>(), {
|
||||||
heading: 'Saved prompts',
|
heading: 'Saved prompts',
|
||||||
disabled: false,
|
disabled: false,
|
||||||
align: 'right'
|
align: 'left'
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -58,7 +65,27 @@ const emit = defineEmits<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const root = ref<HTMLElement | null>(null)
|
const root = ref<HTMLElement | null>(null)
|
||||||
|
const menu = ref<HTMLElement | null>(null)
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
|
const menuStyle = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
function place() {
|
||||||
|
const el = root.value
|
||||||
|
if (!el) return
|
||||||
|
const rect = el.getBoundingClientRect()
|
||||||
|
const width = Math.min(288, window.innerWidth - 16)
|
||||||
|
let left = props.align === 'right' ? rect.right - width : rect.left
|
||||||
|
left = Math.min(Math.max(8, left), window.innerWidth - width - 8)
|
||||||
|
menuStyle.value = {
|
||||||
|
top: `${Math.round(rect.bottom + 4)}px`,
|
||||||
|
left: `${Math.round(left)}px`,
|
||||||
|
width: `${width}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
open.value = !open.value
|
||||||
|
}
|
||||||
|
|
||||||
function pick(item: KeepPrompt) {
|
function pick(item: KeepPrompt) {
|
||||||
emit('insert', item)
|
emit('insert', item)
|
||||||
@@ -68,6 +95,7 @@ function pick(item: KeepPrompt) {
|
|||||||
function onDocClick(event: MouseEvent) {
|
function onDocClick(event: MouseEvent) {
|
||||||
const target = event.target as Node | null
|
const target = event.target as Node | null
|
||||||
if (root.value && target && root.value.contains(target)) return
|
if (root.value && target && root.value.contains(target)) return
|
||||||
|
if (menu.value && target && menu.value.contains(target)) return
|
||||||
open.value = false
|
open.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,13 +107,29 @@ watch(() => props.disabled, (value) => {
|
|||||||
if (value) open.value = false
|
if (value) open.value = false
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
watch(open, (value) => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
|
if (value) {
|
||||||
|
nextTick(() => {
|
||||||
|
place()
|
||||||
window.addEventListener('click', onDocClick)
|
window.addEventListener('click', onDocClick)
|
||||||
window.addEventListener('keydown', onKey)
|
window.addEventListener('keydown', onKey)
|
||||||
|
window.addEventListener('resize', place)
|
||||||
|
window.addEventListener('scroll', place, true)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.removeEventListener('click', onDocClick)
|
||||||
|
window.removeEventListener('keydown', onKey)
|
||||||
|
window.removeEventListener('resize', place)
|
||||||
|
window.removeEventListener('scroll', place, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
if (typeof window === 'undefined') return
|
||||||
window.removeEventListener('click', onDocClick)
|
window.removeEventListener('click', onDocClick)
|
||||||
window.removeEventListener('keydown', onKey)
|
window.removeEventListener('keydown', onKey)
|
||||||
|
window.removeEventListener('resize', place)
|
||||||
|
window.removeEventListener('scroll', place, true)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -34,6 +34,18 @@
|
|||||||
v-else
|
v-else
|
||||||
class="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-zinc-900 px-4 py-3 text-base leading-relaxed text-zinc-200 [overflow-wrap:anywhere]"
|
class="min-h-0 flex-1 overflow-y-auto whitespace-pre-wrap rounded-2xl border border-white/10 bg-zinc-900 px-4 py-3 text-base leading-relaxed text-zinc-200 [overflow-wrap:anywhere]"
|
||||||
>{{ text }}</div>
|
>{{ text }}</div>
|
||||||
|
<div v-if="editable && keepItems.length" class="mt-3 flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
v-for="item in keepItems"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="max-w-full truncate rounded-full border border-white/10 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100"
|
||||||
|
:title="item.text"
|
||||||
|
@click="onKeepInsert(item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+94
-4
@@ -88,6 +88,30 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="file || inputStillId" class="mt-2 flex min-w-0 items-center gap-2">
|
||||||
|
<input
|
||||||
|
v-if="renamingInputStill"
|
||||||
|
ref="inputStillRenameEl"
|
||||||
|
v-model="inputStillNameDraft"
|
||||||
|
class="min-w-0 flex-1 rounded-xl border border-amber-300/40 bg-zinc-950 px-3 py-1.5 text-sm outline-none ring-amber-300/40 focus:ring-2"
|
||||||
|
maxlength="80"
|
||||||
|
aria-label="Input still name"
|
||||||
|
@keydown.enter.prevent="saveInputStillName"
|
||||||
|
@keydown.escape.prevent="cancelInputStillRename"
|
||||||
|
@click.stop
|
||||||
|
@blur="saveInputStillName"
|
||||||
|
>
|
||||||
|
<p v-else class="min-w-0 flex-1 truncate text-sm text-zinc-300">{{ inputStillLabel }}</p>
|
||||||
|
<button
|
||||||
|
v-if="!renamingInputStill"
|
||||||
|
type="button"
|
||||||
|
class="shrink-0 text-xs text-amber-300 hover:text-amber-100"
|
||||||
|
@click.stop="startInputStillRename"
|
||||||
|
>
|
||||||
|
Rename
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="studioMode === 'editv2'" class="flex flex-wrap gap-2">
|
<div v-if="studioMode === 'editv2'" class="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -188,16 +212,14 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
<div class="mb-2 flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'generate' ? 'Generate prompt' : studioMode === 'editv2' && v2Mode === 'refine' ? 'Refine prompt' : studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
|
<label class="text-sm font-medium text-zinc-300">{{ studioMode === 'editv2' && v2Mode === 'generate' ? 'Generate prompt' : studioMode === 'editv2' && v2Mode === 'refine' ? 'Refine prompt' : studioMode === 'edit' || studioMode === 'editv2' ? 'Edit prompt' : 'Motion & scene prompt' }}</label>
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<KeepPromptMenu
|
<KeepPromptMenu
|
||||||
:items="activeKeepPrompts"
|
:items="activeKeepPrompts"
|
||||||
:heading="keepPromptHeading"
|
:heading="keepPromptHeading"
|
||||||
align="left"
|
align="right"
|
||||||
@insert="insertKeepPrompt"
|
@insert="insertKeepPrompt"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
|
||||||
<button
|
<button
|
||||||
v-if="identityPrompting"
|
v-if="identityPrompting"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -291,6 +313,18 @@
|
|||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="activeKeepPrompts.length" class="mt-2 flex flex-wrap gap-1.5">
|
||||||
|
<button
|
||||||
|
v-for="item in activeKeepPrompts"
|
||||||
|
:key="item.id"
|
||||||
|
type="button"
|
||||||
|
class="max-w-full truncate rounded-full border border-white/10 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100"
|
||||||
|
:title="item.text"
|
||||||
|
@click="insertKeepPrompt(item)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p v-if="studioMode === 'editv2' && v2Mode === 'generate'" class="mt-3 text-xs text-zinc-500">No reference image. Prompt only.</p>
|
<p v-if="studioMode === 'editv2' && v2Mode === 'generate'" class="mt-3 text-xs text-zinc-500">No reference image. Prompt only.</p>
|
||||||
<div v-if="studioMode === 'editv2' && v2Mode === 'refine'" class="mt-3 space-y-2">
|
<div v-if="studioMode === 'editv2' && v2Mode === 'refine'" class="mt-3 space-y-2">
|
||||||
<p class="text-xs text-zinc-500">Prompt only what should change in the painted area.</p>
|
<p class="text-xs text-zinc-500">Prompt only what should change in the painted area.</p>
|
||||||
@@ -2361,6 +2395,9 @@ const editLightbox = ref<HTMLElement | null>(null)
|
|||||||
const editLightboxUrl = ref('')
|
const editLightboxUrl = ref('')
|
||||||
const file = ref<File | null>(null)
|
const file = ref<File | null>(null)
|
||||||
const inputStillId = ref('')
|
const inputStillId = ref('')
|
||||||
|
const renamingInputStill = ref(false)
|
||||||
|
const inputStillNameDraft = ref('')
|
||||||
|
const inputStillRenameEl = ref<HTMLInputElement | null>(null)
|
||||||
const preview = ref('')
|
const preview = ref('')
|
||||||
const dragging = ref(false)
|
const dragging = ref(false)
|
||||||
const prompt = ref('')
|
const prompt = ref('')
|
||||||
@@ -4591,6 +4628,7 @@ async function copyPrompt(text: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetImage() {
|
function resetImage() {
|
||||||
|
cancelInputStillRename()
|
||||||
if (preview.value) URL.revokeObjectURL(preview.value)
|
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||||
file.value = null
|
file.value = null
|
||||||
inputStillId.value = ''
|
inputStillId.value = ''
|
||||||
@@ -4623,7 +4661,59 @@ function stillIdFromLibraryUrl(url: string) {
|
|||||||
return match ? decodeURIComponent(match[1]) : ''
|
return match ? decodeURIComponent(match[1]) : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const inputStill = computed(() => stills.value.find(item => item.id === inputStillId.value) || null)
|
||||||
|
|
||||||
|
const inputStillLabel = computed(() => {
|
||||||
|
if (inputStill.value) return stillTitle(inputStill.value)
|
||||||
|
const name = file.value?.name || ''
|
||||||
|
return name.replace(/\.[a-z0-9]+$/i, '') || 'Untitled still'
|
||||||
|
})
|
||||||
|
|
||||||
|
function startInputStillRename() {
|
||||||
|
renamingInputStill.value = true
|
||||||
|
inputStillNameDraft.value = inputStillLabel.value
|
||||||
|
nextTick(() => {
|
||||||
|
inputStillRenameEl.value?.focus()
|
||||||
|
inputStillRenameEl.value?.select()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelInputStillRename() {
|
||||||
|
renamingInputStill.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveInputStillName() {
|
||||||
|
if (!renamingInputStill.value) return
|
||||||
|
const name = inputStillNameDraft.value.trim().slice(0, 80)
|
||||||
|
if (!name) {
|
||||||
|
toast('A name is required')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (name === inputStillLabel.value) {
|
||||||
|
cancelInputStillRename()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (file.value) {
|
||||||
|
const ext = (file.value.name.match(/\.[a-z0-9]+$/i) || ['.png'])[0]
|
||||||
|
file.value = new File([file.value], `${clipFileName(name)}${ext}`, { type: file.value.type || 'image/png' })
|
||||||
|
}
|
||||||
|
const id = inputStillId.value
|
||||||
|
if (id) {
|
||||||
|
try {
|
||||||
|
applyLibrary(await $fetch(`/api/library/stills/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: { name }
|
||||||
|
}))
|
||||||
|
} catch (error: any) {
|
||||||
|
toast(error?.data?.statusMessage || 'Could not rename still')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelInputStillRename()
|
||||||
|
}
|
||||||
|
|
||||||
function readFile(next: File, opts?: { keepThumbnailPref?: boolean; stillId?: string }) {
|
function readFile(next: File, opts?: { keepThumbnailPref?: boolean; stillId?: string }) {
|
||||||
|
cancelInputStillRename()
|
||||||
if (preview.value) URL.revokeObjectURL(preview.value)
|
if (preview.value) URL.revokeObjectURL(preview.value)
|
||||||
file.value = next
|
file.value = next
|
||||||
inputStillId.value = opts?.stillId || ''
|
inputStillId.value = opts?.stillId || ''
|
||||||
|
|||||||
Reference in New Issue
Block a user