Split prompts into pre/prompt/post and tidy the AIGen video form.

Keep wrappers off the library mid-prompt so Use as input and Extend restore the same split, send stills to xAIGen as hidden inputs, and stop the two-column library from growing with the list.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-01 18:39:02 -05:00
co-authored by Cursor
parent ed04dcfee3
commit f788fd4bb4
22 changed files with 753 additions and 199 deletions
+39 -24
View File
@@ -1,5 +1,5 @@
<template>
<details class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
<details v-if="!embedded" class="rounded-2xl border border-white/10 bg-zinc-950/40 px-4 py-3">
<summary class="cursor-pointer text-sm font-medium text-zinc-200">
Global locks
<span class="ml-2 font-normal text-xs text-zinc-500">Every shot inherits these. Each clip adds one beat.</span>
@@ -7,31 +7,45 @@
<p class="mt-2 text-xs text-zinc-500">
Shared standards for last-frame I2V. Later shots get a compact <span class="text-zinc-300">[INHERITS]</span> line, not this whole block again.
</p>
<textarea
:value="modelValue"
rows="10"
class="mt-3 max-h-64 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2"
:placeholder="placeholder"
@input="emit('update:modelValue', ($event.target as HTMLTextAreaElement).value)"
/>
<div class="mt-2 flex flex-wrap gap-2">
<button
type="button"
class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100"
@click="insertTemplate"
>
Insert template
</button>
<button
v-if="modelValue.trim()"
type="button"
class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-400 hover:text-white"
@click="emit('update:modelValue', '')"
>
Clear
</button>
<div class="mt-3">
<PromptField
:model-value="modelValue"
:rows="10"
title="Global locks"
write-label="Write locks"
edit-label="View / edit locks"
textarea-class="max-h-64"
:placeholder="placeholder"
@update:model-value="emit('update:modelValue', $event)"
/>
<div class="mt-2 flex flex-wrap gap-2">
<button type="button" class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100" @click="insertTemplate">Insert template</button>
<button v-if="modelValue.trim()" type="button" class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-400 hover:text-white" @click="emit('update:modelValue', '')">Clear</button>
</div>
</div>
</details>
<div v-else>
<p class="text-sm font-medium text-zinc-200">Global locks</p>
<p class="mt-0.5 text-xs text-zinc-500">
Shared standards for last-frame I2V. Later shots get a compact <span class="text-zinc-300">[INHERITS]</span> line, not this whole block again.
</p>
<div class="mt-3">
<PromptField
:model-value="modelValue"
:rows="10"
title="Global locks"
write-label="Write locks"
edit-label="View / edit locks"
textarea-class="max-h-64"
:placeholder="placeholder"
@update:model-value="emit('update:modelValue', $event)"
/>
<div class="mt-2 flex flex-wrap gap-2">
<button type="button" class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-300 hover:border-amber-300/40 hover:text-amber-100" @click="insertTemplate">Insert template</button>
<button v-if="modelValue.trim()" type="button" class="rounded-lg border border-white/15 px-2.5 py-1 text-xs text-zinc-400 hover:text-white" @click="emit('update:modelValue', '')">Clear</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
@@ -39,6 +53,7 @@ import { DEFAULT_GLOBAL_LOCKS } from '~/utils/globalLocks'
defineProps<{
modelValue: string
embedded?: boolean
}>()
const emit = defineEmits<{
+71
View File
@@ -0,0 +1,71 @@
<template>
<div>
<textarea
:value="modelValue"
:rows="rows"
class="hidden w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
:class="textareaClass"
:placeholder="placeholder"
:disabled="disabled"
@input="onInput"
/>
<button
type="button"
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-left md:hidden"
:disabled="disabled"
@click="drawerOpen = true"
>
<p v-if="modelValue.trim()" class="line-clamp-4 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ modelValue }}</p>
<p v-else class="text-sm text-zinc-600">{{ placeholder }}</p>
<span class="mt-1 inline-block text-xs text-amber-200">{{ modelValue.trim() ? editLabel : writeLabel }}</span>
</button>
<PromptDrawer
:open="drawerOpen"
:title="title"
:text="modelValue"
:placeholder="placeholder"
:keep-items="keepItems"
:keep-heading="keepHeading"
@update:open="drawerOpen = $event"
@update:text="emit('update:modelValue', $event)"
/>
</div>
</template>
<script setup lang="ts">
import type { KeepPrompt } from '~/utils/keepPrompts'
const props = withDefaults(defineProps<{
modelValue?: string
rows?: number
placeholder?: string
title?: string
editLabel?: string
writeLabel?: string
textareaClass?: string
disabled?: boolean
keepItems?: KeepPrompt[]
keepHeading?: string
}>(), {
modelValue: '',
rows: 3,
placeholder: '',
title: 'Prompt',
editLabel: 'View / edit',
writeLabel: 'Write',
textareaClass: 'max-h-32 md:max-h-none',
disabled: false,
keepItems: () => [],
keepHeading: 'Saved prompts'
})
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
const drawerOpen = ref(false)
function onInput(event: Event) {
emit('update:modelValue', (event.target as HTMLTextAreaElement).value)
}
</script>
+208
View File
@@ -0,0 +1,208 @@
<template>
<div class="space-y-3">
<div class="flex flex-wrap items-center gap-2">
<KeepPromptMenu
:items="keepItems"
:heading="keepHeading"
:disabled="disabled"
@insert="insertSnippet"
/>
<div class="inline-flex rounded-full border border-white/10 p-0.5">
<button
v-for="option in targets"
:key="option.id"
type="button"
class="rounded-full px-2.5 py-1 text-[11px]"
:class="snippetTarget === option.id ? 'bg-amber-400/15 text-amber-100' : 'text-zinc-500 hover:text-zinc-300'"
:disabled="disabled"
@click="snippetTarget = option.id"
>
{{ option.label }}
</button>
</div>
<span class="text-[11px] text-zinc-500">Snippets go into {{ targetLabel }}.</span>
</div>
<label class="block text-sm">
<span class="mb-1 block text-xs font-medium text-zinc-400">Pre-text</span>
<textarea
:value="pre"
rows="2"
class="hidden max-h-40 w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-2.5 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
:placeholder="prePlaceholder"
:disabled="disabled"
@focus="snippetTarget = 'pre'"
@input="onInput('pre', $event)"
/>
<button
type="button"
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-2.5 text-left md:hidden"
@click="openDrawer('pre')"
>
<p v-if="pre.trim()" class="line-clamp-3 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ pre }}</p>
<p v-else class="text-sm text-zinc-600">{{ prePlaceholder }}</p>
<span class="mt-1 inline-block text-xs text-amber-200">{{ pre.trim() ? 'View / edit pre-text' : 'Write pre-text' }}</span>
</button>
</label>
<label class="block text-sm">
<span class="mb-1 block text-xs font-medium text-zinc-400">Prompt</span>
<textarea
:value="prompt"
:rows="promptRows"
class="hidden max-h-[min(28rem,70vh)] w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
:placeholder="promptPlaceholder"
:disabled="disabled"
@focus="snippetTarget = 'prompt'"
@input="onInput('prompt', $event)"
/>
<button
type="button"
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-3 text-left md:hidden"
@click="openDrawer('prompt')"
>
<p v-if="prompt.trim()" class="line-clamp-4 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ prompt }}</p>
<p v-else class="text-sm text-zinc-600">{{ promptPlaceholder }}</p>
<span class="mt-2 inline-block text-xs text-amber-200">{{ prompt.trim() ? 'View / edit prompt' : 'Write prompt' }}</span>
</button>
</label>
<label class="block text-sm">
<span class="mb-1 block text-xs font-medium text-zinc-400">Post-text</span>
<textarea
:value="post"
rows="2"
class="hidden max-h-40 w-full overflow-y-auto rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-2.5 text-sm outline-none ring-amber-300/40 placeholder:text-zinc-600 [overflow-wrap:anywhere] focus:ring-2 md:block"
:placeholder="postPlaceholder"
:disabled="disabled"
@focus="snippetTarget = 'post'"
@input="onInput('post', $event)"
/>
<button
type="button"
class="w-full rounded-2xl border border-white/10 bg-zinc-950/70 px-4 py-2.5 text-left md:hidden"
@click="openDrawer('post')"
>
<p v-if="post.trim()" class="line-clamp-3 whitespace-pre-wrap text-sm text-zinc-200 [overflow-wrap:anywhere]">{{ post }}</p>
<p v-else class="text-sm text-zinc-600">{{ postPlaceholder }}</p>
<span class="mt-1 inline-block text-xs text-amber-200">{{ post.trim() ? 'View / edit post-text' : 'Write post-text' }}</span>
</button>
</label>
<div v-if="keepItems.length" class="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"
:disabled="disabled"
@click="insertSnippet(item)"
>
{{ item.label }}
</button>
</div>
<PromptDrawer
:open="drawerOpen"
:title="drawerTitle"
:text="drawerText"
:placeholder="drawerPlaceholder"
:keep-items="keepItems"
:keep-heading="keepHeading"
@update:open="drawerOpen = $event"
@update:text="onDrawerText"
/>
</div>
</template>
<script setup lang="ts">
import { insertKeepSnippet, type KeepPrompt } from '~/utils/keepPrompts'
const targets = [
{ id: 'pre' as const, label: 'Pre' },
{ id: 'prompt' as const, label: 'Prompt' },
{ id: 'post' as const, label: 'Post' }
]
const props = withDefaults(defineProps<{
pre?: string
prompt?: string
post?: string
keepItems?: KeepPrompt[]
keepHeading?: string
promptPlaceholder?: string
prePlaceholder?: string
postPlaceholder?: string
promptRows?: number
disabled?: boolean
}>(), {
pre: '',
prompt: '',
post: '',
keepItems: () => [],
keepHeading: 'Saved prompts',
promptPlaceholder: '',
prePlaceholder: 'Optional text before the prompt…',
postPlaceholder: 'Optional text after the prompt…',
promptRows: 6,
disabled: false
})
const emit = defineEmits<{
'update:pre': [value: string]
'update:prompt': [value: string]
'update:post': [value: string]
}>()
const snippetTarget = ref<'pre' | 'prompt' | 'post'>('prompt')
const drawerField = ref<'pre' | 'prompt' | 'post'>('prompt')
const drawerOpen = ref(false)
const targetLabel = computed(() => targets.find(item => item.id === snippetTarget.value)?.label.toLowerCase() || 'prompt')
const drawerTitle = computed(() => {
if (drawerField.value === 'pre') return 'Pre-text'
if (drawerField.value === 'post') return 'Post-text'
return 'Prompt'
})
const drawerPlaceholder = computed(() => {
if (drawerField.value === 'pre') return props.prePlaceholder
if (drawerField.value === 'post') return props.postPlaceholder
return props.promptPlaceholder
})
const drawerText = computed(() => {
if (drawerField.value === 'pre') return props.pre
if (drawerField.value === 'post') return props.post
return props.prompt
})
function currentValue(target: 'pre' | 'prompt' | 'post') {
if (target === 'pre') return props.pre
if (target === 'post') return props.post
return props.prompt
}
function emitField(target: 'pre' | 'prompt' | 'post', value: string) {
if (target === 'pre') emit('update:pre', value)
else if (target === 'post') emit('update:post', value)
else emit('update:prompt', value)
}
function onInput(target: 'pre' | 'prompt' | 'post', event: Event) {
emitField(target, (event.target as HTMLTextAreaElement).value)
}
function insertSnippet(item: KeepPrompt) {
emitField(snippetTarget.value, insertKeepSnippet(currentValue(snippetTarget.value), item.text))
}
function openDrawer(target: 'pre' | 'prompt' | 'post') {
snippetTarget.value = target
drawerField.value = target
drawerOpen.value = true
}
function onDrawerText(value: string) {
emitField(drawerField.value, value)
}
</script>