Add clickable keep-prompt snippets for video and image.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-08-31 20:39:08 -05:00
co-authored by Cursor
parent 529598cef2
commit a3380870ad
7 changed files with 407 additions and 12 deletions
+122 -1
View File
@@ -192,6 +192,63 @@
</div>
</section>
<section class="mt-8 space-y-4">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Keep prompts</h3>
<p class="mt-1 text-sm text-zinc-400">
Saved snippets for the + button next to the studio prompt. Video and image lists are separate.
</p>
</div>
<div v-for="kind in keepKinds" :key="kind.id" class="space-y-2">
<p class="text-xs font-medium uppercase tracking-wide text-zinc-500">{{ kind.label }}</p>
<div
v-for="item in keepPrompts[kind.id]"
:key="item.id"
class="rounded-2xl border border-white/10 bg-zinc-950/50 p-3 space-y-2"
>
<div class="flex items-center gap-2">
<input
v-model="item.label"
class="min-w-0 flex-1 rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
placeholder="Short name"
maxlength="40"
@change="saveKeepKind(kind.id)"
>
<button type="button" class="shrink-0 text-xs text-red-300 hover:text-red-200" @click="removeKeepItem(kind.id, item.id)">
Delete
</button>
</div>
<textarea
v-model="item.text"
rows="3"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
placeholder="Text added to the prompt…"
maxlength="500"
@change="saveKeepKind(kind.id)"
/>
</div>
<p v-if="!keepPrompts[kind.id].length" class="text-xs text-zinc-600">None yet.</p>
<form class="space-y-2 rounded-2xl border border-dashed border-white/15 p-3" @submit.prevent="addKeepItem(kind.id)">
<input
v-model="newKeep[kind.id].label"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
:placeholder="kind.id === 'video' ? 'e.g. Locked camera' : 'e.g. Soft daylight'"
maxlength="40"
>
<textarea
v-model="newKeep[kind.id].text"
rows="2"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm"
placeholder="Snippet…"
maxlength="500"
/>
<button type="submit" class="rounded-xl bg-white/10 px-3 py-2 text-xs font-semibold text-white hover:bg-white/15">
Add {{ kind.label.toLowerCase() }} snippet
</button>
</form>
</div>
</section>
<section class="mt-8 space-y-3">
<div>
<h3 class="text-sm font-semibold uppercase tracking-wider text-zinc-500">Prompt helper</h3>
@@ -261,6 +318,15 @@
<script setup lang="ts">
import { defaultLoraFilenameKey, isFilenameSystemLora } from '~/utils/outputNames'
import { loraDisplayName, loraIdentityKey } from '~/utils/loras'
import {
KEEP_PROMPT_MAX,
emptyKeepPrompts,
normalizeKeepPrompts,
sanitizeKeepPromptLabel,
sanitizeKeepPromptText,
type KeepPromptKind,
type KeepPromptLists
} from '~/utils/keepPrompts'
export interface SettingsFolder {
id: string
@@ -287,6 +353,15 @@ const emit = defineEmits<{
const newName = ref('')
const newPresetName = ref('')
const newPresetPrompt = ref('')
const keepKinds: Array<{ id: KeepPromptKind; label: string }> = [
{ id: 'video', label: 'Video' },
{ id: 'image', label: 'Image' }
]
const keepPrompts = ref<KeepPromptLists>(emptyKeepPrompts())
const newKeep = reactive<Record<KeepPromptKind, { label: string; text: string }>>({
video: { label: '', text: '' },
image: { label: '', text: '' }
})
const promptPresets = ref<Array<{ id: string; name: string; description?: string; builtin?: boolean; systemPrompt?: string }>>([])
const loraRowsLoading = ref(true)
const loraRows = ref<Array<{ key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; trigger: string; placeholder: string }>>([])
@@ -472,7 +547,12 @@ async function loadLoraKeys() {
loraRowsLoading.value = true
try {
const listed = await $fetch<{ image?: string[]; video?: string[] }>('/api/loras').catch(() => ({ image: [], video: [] }))
const library = await $fetch<{ loraAliases?: Record<string, string>; loraTriggers?: Record<string, string> }>('/api/library?light=1').catch(() => ({ loraAliases: {}, loraTriggers: {} }))
const library = await $fetch<{
loraAliases?: Record<string, string>
loraTriggers?: Record<string, string>
keepPrompts?: KeepPromptLists
}>('/api/library?light=1').catch(() => ({ loraAliases: {}, loraTriggers: {}, keepPrompts: emptyKeepPrompts() }))
keepPrompts.value = normalizeKeepPrompts(library.keepPrompts)
const aliases = library.loraAliases || {}
const triggers = library.loraTriggers || {}
const map = new Map<string, { key: string; file: string; kind: 'image' | 'video' | 'both'; draft: string; trigger: string; placeholder: string }>()
@@ -502,6 +582,47 @@ async function loadLoraKeys() {
}
}
async function saveKeepKind(kind: KeepPromptKind) {
try {
const data = await $fetch<{ keepPrompts?: KeepPromptLists }>('/api/library/keep-prompts', {
method: 'PUT',
body: { kind, items: keepPrompts.value[kind] }
})
keepPrompts.value = normalizeKeepPrompts(data.keepPrompts)
emit('updated', data)
} catch (error: any) {
emit('error', error?.data?.statusMessage || 'Could not save keep prompts')
}
}
function addKeepItem(kind: KeepPromptKind) {
const text = sanitizeKeepPromptText(newKeep[kind].text)
if (!text) {
emit('error', 'Snippet text is required')
return
}
if (keepPrompts.value[kind].length >= KEEP_PROMPT_MAX) {
emit('error', `At most ${KEEP_PROMPT_MAX} snippets per list`)
return
}
keepPrompts.value[kind] = [
...keepPrompts.value[kind],
{
id: crypto.randomUUID(),
label: sanitizeKeepPromptLabel(newKeep[kind].label, text),
text
}
]
newKeep[kind].label = ''
newKeep[kind].text = ''
void saveKeepKind(kind)
}
function removeKeepItem(kind: KeepPromptKind, id: string) {
keepPrompts.value[kind] = keepPrompts.value[kind].filter(item => item.id !== id)
void saveKeepKind(kind)
}
async function saveLoraAlias(row: { key: string; draft: string }) {
try {
emit('updated', await $fetch('/api/library/lora-aliases', {