Files
aigen/components/PromptPartsEditor.vue
T
TowstyandCursor f788fd4bb4 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>
2026-09-01 18:39:02 -05:00

209 lines
7.4 KiB
Vue

<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>