Add Prompt Builder sequence section order.
Copied scene text and the scene editor follow a persisted sectionOrder, with legacy saves keeping the default style/settings/set/characters/prompt order. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+100
-7
@@ -44,6 +44,45 @@
|
|||||||
<p class="mt-1 text-[11px] text-zinc-500">Save key only — never copied into the prompt text.</p>
|
<p class="mt-1 text-[11px] text-zinc-500">Save key only — never copied into the prompt text.</p>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-medium text-zinc-300">Prompt order</span>
|
||||||
|
<p class="text-[11px] text-zinc-500">Drag to reorder, or use the arrows. Copied text and the scene editor follow this order.</p>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
v-for="(key, index) in sectionOrder"
|
||||||
|
:key="key"
|
||||||
|
class="flex items-center gap-2 rounded-2xl border border-white/10 bg-zinc-950/40 px-3 py-2"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="onSectionDragStart(index, $event)"
|
||||||
|
@dragover.prevent="onSectionDragOver($event)"
|
||||||
|
@drop.prevent="onSectionDrop(index)"
|
||||||
|
>
|
||||||
|
<span class="cursor-grab select-none text-zinc-500 active:cursor-grabbing" title="Drag to reorder">⠿</span>
|
||||||
|
<span class="min-w-0 flex-1 text-sm text-zinc-200">{{ sectionLabel(key) }}</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
title="Move up"
|
||||||
|
:disabled="index === 0"
|
||||||
|
@click="moveSection(index, -1)"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10 disabled:cursor-not-allowed disabled:opacity-40"
|
||||||
|
title="Move down"
|
||||||
|
:disabled="index >= sectionOrder.length - 1"
|
||||||
|
@click="moveSection(index, 1)"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
@@ -271,7 +310,8 @@
|
|||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div class="space-y-1">␍
|
<template v-for="sectionKey in sectionOrder" :key="'scene-section-' + sectionKey">
|
||||||
|
<div v-if="sectionKey === 'style'" class="space-y-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-sm font-medium text-zinc-300">Style</span>
|
<span class="text-sm font-medium text-zinc-300">Style</span>
|
||||||
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.style = ''">Clear</button>
|
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.style = ''">Clear</button>
|
||||||
@@ -284,7 +324,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">␍
|
<div v-else-if="sectionKey === 'settings'" class="space-y-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-sm font-medium text-zinc-300">Settings</span>
|
<span class="text-sm font-medium text-zinc-300">Settings</span>
|
||||||
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.settings = ''">Clear</button>
|
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.settings = ''">Clear</button>
|
||||||
@@ -297,7 +337,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="sets.length" class="space-y-3">␍
|
<div v-else-if="sectionKey === 'set' && sets.length" class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-medium text-zinc-300">Sets in this scene</span>
|
<span class="text-sm font-medium text-zinc-300">Sets in this scene</span>
|
||||||
<p class="text-[11px] text-zinc-500">Toggle which set blocks appear. Pick a framing for each on set.</p>
|
<p class="text-[11px] text-zinc-500">Toggle which set blocks appear. Pick a framing for each on set.</p>
|
||||||
@@ -332,7 +372,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-3">␍
|
<div v-else-if="sectionKey === 'characters'" class="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-sm font-medium text-zinc-300">In this scene</span>
|
<span class="text-sm font-medium text-zinc-300">In this scene</span>
|
||||||
<p class="text-[11px] text-zinc-500">Toggle who appears. Pick an outfit for each on character.</p>
|
<p class="text-[11px] text-zinc-500">Toggle who appears. Pick an outfit for each on character.</p>
|
||||||
@@ -367,7 +407,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-1">␍
|
<div v-else-if="sectionKey === 'prompt'" class="space-y-1">
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="text-sm font-medium text-zinc-300">Prompt</span>
|
<span class="text-sm font-medium text-zinc-300">Prompt</span>
|
||||||
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.prompt = ''">Clear</button>
|
<button type="button" class="rounded-full border border-amber-300/70 px-3 min-h-11 py-1.5 text-sm font-semibold text-amber-100 hover:border-amber-300 hover:bg-amber-400/10" @click="activeScene.prompt = ''">Clear</button>
|
||||||
@@ -379,6 +419,7 @@
|
|||||||
placeholder="What happens in this image"
|
placeholder="What happens in this image"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section class="rounded-2xl border border-white/10 bg-zinc-950/30">
|
<section class="rounded-2xl border border-white/10 bg-zinc-950/30">
|
||||||
@@ -467,9 +508,11 @@ import type {
|
|||||||
PromptSequence,
|
PromptSequence,
|
||||||
PromptSequenceCharacter,
|
PromptSequenceCharacter,
|
||||||
PromptSequenceScene,
|
PromptSequenceScene,
|
||||||
|
PromptSequenceSectionKey,
|
||||||
PromptSequenceSet
|
PromptSequenceSet
|
||||||
} from '~/utils/promptSequences'
|
} from '~/utils/promptSequences'
|
||||||
import {
|
import {
|
||||||
|
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||||
PROMPT_SEQUENCE_FRAMING_SUGGESTIONS,
|
PROMPT_SEQUENCE_FRAMING_SUGGESTIONS,
|
||||||
assembleSceneText,
|
assembleSceneText,
|
||||||
assembleSequenceText,
|
assembleSequenceText,
|
||||||
@@ -481,14 +524,24 @@ import {
|
|||||||
emptySet,
|
emptySet,
|
||||||
ensureSceneStatesForCharacters,
|
ensureSceneStatesForCharacters,
|
||||||
ensureSceneStatesForSets,
|
ensureSceneStatesForSets,
|
||||||
normalizePromptSequence␍
|
normalizePromptSequence,
|
||||||
|
normalizeSectionOrder
|
||||||
} from '~/utils/promptSequences'
|
} from '~/utils/promptSequences'
|
||||||
|
|
||||||
useHead({ title: 'Prompt Builder' })
|
useHead({ title: 'Prompt Builder' })
|
||||||
|
|
||||||
|
const SECTION_LABELS: Record<PromptSequenceSectionKey, string> = {
|
||||||
|
style: 'Style',
|
||||||
|
settings: 'Settings',
|
||||||
|
set: 'Set',
|
||||||
|
characters: 'Characters',
|
||||||
|
prompt: 'Prompt'
|
||||||
|
}
|
||||||
|
|
||||||
const sequences = ref<PromptSequence[]>([])
|
const sequences = ref<PromptSequence[]>([])
|
||||||
const selectedId = ref('')
|
const selectedId = ref('')
|
||||||
const name = ref('')
|
const name = ref('')
|
||||||
|
const sectionOrder = ref<PromptSequenceSectionKey[]>([...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER])
|
||||||
const characters = ref<PromptSequenceCharacter[]>([emptyCharacter()])
|
const characters = ref<PromptSequenceCharacter[]>([emptyCharacter()])
|
||||||
const sets = ref<PromptSequenceSet[]>([])
|
const sets = ref<PromptSequenceSet[]>([])
|
||||||
const scenes = ref<PromptSequenceScene[]>([emptyScene(1, characters.value, sets.value)])
|
const scenes = ref<PromptSequenceScene[]>([emptyScene(1, characters.value, sets.value)])
|
||||||
@@ -501,18 +554,55 @@ const copied = ref(false)
|
|||||||
const copiedScene = ref(false)
|
const copiedScene = ref(false)
|
||||||
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
let copyTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
let copySceneTimer: ReturnType<typeof setTimeout> | null = null
|
let copySceneTimer: ReturnType<typeof setTimeout> | null = null
|
||||||
|
let dragSectionIndex: number | null = null
|
||||||
|
|
||||||
const activeScene = computed(() => scenes.value[activeIndex.value] || null)
|
const activeScene = computed(() => scenes.value[activeIndex.value] || null)
|
||||||
const previewText = computed(() => assembleSequenceText({
|
const previewText = computed(() => assembleSequenceText({
|
||||||
|
sectionOrder: sectionOrder.value,
|
||||||
characters: characters.value,
|
characters: characters.value,
|
||||||
sets: sets.value,
|
sets: sets.value,
|
||||||
scenes: scenes.value
|
scenes: scenes.value
|
||||||
}))
|
}))
|
||||||
const activeSceneText = computed(() => {
|
const activeSceneText = computed(() => {
|
||||||
if (!activeScene.value) return ''
|
if (!activeScene.value) return ''
|
||||||
return assembleSceneText(activeScene.value, characters.value, sets.value)␍
|
return assembleSceneText(activeScene.value, characters.value, sets.value, sectionOrder.value)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function sectionLabel(key: PromptSequenceSectionKey) {
|
||||||
|
return SECTION_LABELS[key] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveSection(index: number, delta: number) {
|
||||||
|
const next = index + delta
|
||||||
|
if (next < 0 || next >= sectionOrder.value.length) return
|
||||||
|
const copy = [...sectionOrder.value]
|
||||||
|
const [item] = copy.splice(index, 1)
|
||||||
|
copy.splice(next, 0, item)
|
||||||
|
sectionOrder.value = copy
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSectionDragStart(index: number, event: DragEvent) {
|
||||||
|
dragSectionIndex = index
|
||||||
|
event.dataTransfer?.setData('text/plain', String(index))
|
||||||
|
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSectionDragOver(event: DragEvent) {
|
||||||
|
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSectionDrop(index: number) {
|
||||||
|
if (dragSectionIndex == null || dragSectionIndex === index) {
|
||||||
|
dragSectionIndex = null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const copy = [...sectionOrder.value]
|
||||||
|
const [item] = copy.splice(dragSectionIndex, 1)
|
||||||
|
copy.splice(index, 0, item)
|
||||||
|
sectionOrder.value = copy
|
||||||
|
dragSectionIndex = null
|
||||||
|
}
|
||||||
|
|
||||||
function framingLabelPlaceholder(index: number) {
|
function framingLabelPlaceholder(index: number) {
|
||||||
const suggestion = PROMPT_SEQUENCE_FRAMING_SUGGESTIONS[index % PROMPT_SEQUENCE_FRAMING_SUGGESTIONS.length]
|
const suggestion = PROMPT_SEQUENCE_FRAMING_SUGGESTIONS[index % PROMPT_SEQUENCE_FRAMING_SUGGESTIONS.length]
|
||||||
return suggestion || 'wide / medium / narrow'
|
return suggestion || 'wide / medium / narrow'
|
||||||
@@ -521,6 +611,7 @@ function framingLabelPlaceholder(index: number) {
|
|||||||
function startNew() {
|
function startNew() {
|
||||||
selectedId.value = ''
|
selectedId.value = ''
|
||||||
name.value = ''
|
name.value = ''
|
||||||
|
sectionOrder.value = [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER]
|
||||||
characters.value = [emptyCharacter()]
|
characters.value = [emptyCharacter()]
|
||||||
sets.value = []
|
sets.value = []
|
||||||
scenes.value = [emptyScene(1, characters.value, sets.value)]
|
scenes.value = [emptyScene(1, characters.value, sets.value)]
|
||||||
@@ -533,6 +624,7 @@ function select(sequence: PromptSequence) {
|
|||||||
const normalized = normalizePromptSequence(sequence) || sequence
|
const normalized = normalizePromptSequence(sequence) || sequence
|
||||||
selectedId.value = normalized.id
|
selectedId.value = normalized.id
|
||||||
name.value = normalized.name
|
name.value = normalized.name
|
||||||
|
sectionOrder.value = normalizeSectionOrder(normalized.sectionOrder)
|
||||||
characters.value = (normalized.characters?.length ? normalized.characters : [emptyCharacter()]).map(character => ({
|
characters.value = (normalized.characters?.length ? normalized.characters : [emptyCharacter()]).map(character => ({
|
||||||
...emptyCharacter(),
|
...emptyCharacter(),
|
||||||
...character,
|
...character,
|
||||||
@@ -781,6 +873,7 @@ async function save() {
|
|||||||
notice.value = ''
|
notice.value = ''
|
||||||
const payload = {
|
const payload = {
|
||||||
name: nextName,
|
name: nextName,
|
||||||
|
sectionOrder: sectionOrder.value,
|
||||||
characters: characters.value,
|
characters: characters.value,
|
||||||
sets: sets.value,
|
sets: sets.value,
|
||||||
scenes: scenes.value
|
scenes: scenes.value
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const body = await readBody<{
|
const body = await readBody<{
|
||||||
name?: string
|
name?: string
|
||||||
|
sectionOrder?: unknown
|
||||||
characters?: unknown
|
characters?: unknown
|
||||||
sets?: unknown
|
sets?: unknown
|
||||||
scenes?: unknown
|
scenes?: unknown
|
||||||
@@ -11,6 +12,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
}>(event).catch(() => ({}))
|
}>(event).catch(() => ({}))
|
||||||
const sequence = await savePromptSequence(owner, {
|
const sequence = await savePromptSequence(owner, {
|
||||||
name: body?.name || '',
|
name: body?.name || '',
|
||||||
|
sectionOrder: body?.sectionOrder,
|
||||||
characters: body?.characters,
|
characters: body?.characters,
|
||||||
sets: body?.sets,
|
sets: body?.sets,
|
||||||
scenes: body?.scenes,
|
scenes: body?.scenes,
|
||||||
|
|||||||
@@ -3,9 +3,16 @@ import { listPromptSequences, updatePromptSequence } from '~/server/utils/prompt
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const { owner } = assertLibraryOwner(event)
|
const { owner } = assertLibraryOwner(event)
|
||||||
const id = String(getRouterParam(event, 'id') || '')
|
const id = String(getRouterParam(event, 'id') || '')
|
||||||
const body = await readBody<{ name?: string; characters?: unknown; sets?: unknown; scenes?: unknown }>(event).catch(() => ({}))
|
const body = await readBody<{
|
||||||
|
name?: string
|
||||||
|
sectionOrder?: unknown
|
||||||
|
characters?: unknown
|
||||||
|
sets?: unknown
|
||||||
|
scenes?: unknown
|
||||||
|
}>(event).catch(() => ({}))
|
||||||
const sequence = await updatePromptSequence(owner, id, {
|
const sequence = await updatePromptSequence(owner, id, {
|
||||||
name: body?.name,
|
name: body?.name,
|
||||||
|
sectionOrder: body?.sectionOrder,
|
||||||
characters: body?.characters,
|
characters: body?.characters,
|
||||||
sets: body?.sets,
|
sets: body?.sets,
|
||||||
scenes: body?.scenes
|
scenes: body?.scenes
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import {
|
|||||||
normalizeCharacter,
|
normalizeCharacter,
|
||||||
normalizeSet,
|
normalizeSet,
|
||||||
normalizeScene,
|
normalizeScene,
|
||||||
|
normalizeSectionOrder,
|
||||||
emptyCharacter,
|
emptyCharacter,
|
||||||
emptyScene,
|
emptyScene,
|
||||||
|
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||||
type PromptSequence,
|
type PromptSequence,
|
||||||
type PromptSequenceCharacter,
|
type PromptSequenceCharacter,
|
||||||
|
type PromptSequenceSectionKey,
|
||||||
type PromptSequenceSet,
|
type PromptSequenceSet,
|
||||||
type PromptSequenceScene
|
type PromptSequenceScene
|
||||||
} from '~/utils/promptSequences'
|
} from '~/utils/promptSequences'
|
||||||
@@ -91,6 +94,7 @@ export function getPromptSequence(owner: string, id: string) {
|
|||||||
|
|
||||||
export async function savePromptSequence(owner: string, input: {
|
export async function savePromptSequence(owner: string, input: {
|
||||||
name: string
|
name: string
|
||||||
|
sectionOrder?: unknown
|
||||||
characters?: unknown
|
characters?: unknown
|
||||||
sets?: unknown
|
sets?: unknown
|
||||||
scenes?: unknown
|
scenes?: unknown
|
||||||
@@ -101,6 +105,9 @@ export async function savePromptSequence(owner: string, input: {
|
|||||||
const characters = normalizeCharactersInput(input.characters)
|
const characters = normalizeCharactersInput(input.characters)
|
||||||
const sets = normalizeSetsInput(input.sets)
|
const sets = normalizeSetsInput(input.sets)
|
||||||
const scenes = normalizeScenesInput(input.scenes, characters, sets)
|
const scenes = normalizeScenesInput(input.scenes, characters, sets)
|
||||||
|
const sectionOrder = input.sectionOrder !== undefined
|
||||||
|
? normalizeSectionOrder(input.sectionOrder)
|
||||||
|
: [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER]
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
return mutate(owner, (sequences) => {
|
return mutate(owner, (sequences) => {
|
||||||
const existing = sequences.find(item => item.name.toLowerCase() === name.toLowerCase())
|
const existing = sequences.find(item => item.name.toLowerCase() === name.toLowerCase())
|
||||||
@@ -108,6 +115,7 @@ export async function savePromptSequence(owner: string, input: {
|
|||||||
throw createError({ statusCode: 409, statusMessage: `Overwrite sequence “${existing.name}”?` })
|
throw createError({ statusCode: 409, statusMessage: `Overwrite sequence “${existing.name}”?` })
|
||||||
}
|
}
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
existing.sectionOrder = sectionOrder
|
||||||
existing.characters = characters
|
existing.characters = characters
|
||||||
existing.sets = sets
|
existing.sets = sets
|
||||||
existing.scenes = scenes
|
existing.scenes = scenes
|
||||||
@@ -117,6 +125,7 @@ export async function savePromptSequence(owner: string, input: {
|
|||||||
const created: PromptSequence = {
|
const created: PromptSequence = {
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name,
|
name,
|
||||||
|
sectionOrder,
|
||||||
characters,
|
characters,
|
||||||
sets,
|
sets,
|
||||||
scenes,
|
scenes,
|
||||||
@@ -130,6 +139,7 @@ export async function savePromptSequence(owner: string, input: {
|
|||||||
|
|
||||||
export async function updatePromptSequence(owner: string, id: string, input: {
|
export async function updatePromptSequence(owner: string, id: string, input: {
|
||||||
name?: string
|
name?: string
|
||||||
|
sectionOrder?: unknown
|
||||||
characters?: unknown
|
characters?: unknown
|
||||||
sets?: unknown
|
sets?: unknown
|
||||||
scenes?: unknown
|
scenes?: unknown
|
||||||
@@ -146,6 +156,11 @@ export async function updatePromptSequence(owner: string, id: string, input: {
|
|||||||
if (clash) throw createError({ statusCode: 409, statusMessage: `A sequence named “${clash.name}” already exists` })
|
if (clash) throw createError({ statusCode: 409, statusMessage: `A sequence named “${clash.name}” already exists` })
|
||||||
existing.name = name
|
existing.name = name
|
||||||
}
|
}
|
||||||
|
if (input.sectionOrder !== undefined) {
|
||||||
|
existing.sectionOrder = normalizeSectionOrder(input.sectionOrder)
|
||||||
|
} else if (!existing.sectionOrder?.length) {
|
||||||
|
existing.sectionOrder = [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER] as PromptSequenceSectionKey[]
|
||||||
|
}
|
||||||
if (input.characters !== undefined || input.sets !== undefined || input.scenes !== undefined) {
|
if (input.characters !== undefined || input.sets !== undefined || input.scenes !== undefined) {
|
||||||
const characters = input.characters !== undefined
|
const characters = input.characters !== undefined
|
||||||
? normalizeCharactersInput(input.characters)
|
? normalizeCharactersInput(input.characters)
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const {
|
|||||||
emptyFraming,
|
emptyFraming,
|
||||||
cloneSceneForNext,
|
cloneSceneForNext,
|
||||||
normalizePromptSequence,
|
normalizePromptSequence,
|
||||||
|
normalizeSectionOrder,
|
||||||
|
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||||
ensureSceneStatesForCharacters
|
ensureSceneStatesForCharacters
|
||||||
} = await import(pathToFileURL(tmp).href)
|
} = await import(pathToFileURL(tmp).href)
|
||||||
unlinkSync(tmp)
|
unlinkSync(tmp)
|
||||||
@@ -204,3 +206,83 @@ test('ensureSceneStatesForCharacters turns new character on only for current sce
|
|||||||
assert.equal(scenes[0].characterStates.find(s => s.characterId === b.id).on, false)
|
assert.equal(scenes[0].characterStates.find(s => s.characterId === b.id).on, false)
|
||||||
assert.equal(scenes[1].characterStates.find(s => s.characterId === b.id).on, true)
|
assert.equal(scenes[1].characterStates.find(s => s.characterId === b.id).on, true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('sectionOrder settings then prompt then characters; empty style and set omitted', () => {
|
||||||
|
const ada = {
|
||||||
|
...emptyCharacter(),
|
||||||
|
id: 'c1',
|
||||||
|
name: 'Ada',
|
||||||
|
description: 'tall',
|
||||||
|
outfits: []
|
||||||
|
}
|
||||||
|
const set = {
|
||||||
|
...emptySet(),
|
||||||
|
id: 's1',
|
||||||
|
name: 'Courtyard',
|
||||||
|
framings: [{ ...emptyFraming('wide'), id: 'f1', text: 'open yard' }]
|
||||||
|
}
|
||||||
|
const scene = {
|
||||||
|
style: '',
|
||||||
|
settings: 'soft light',
|
||||||
|
setStates: [{ setId: 's1', on: true, framingId: null }],
|
||||||
|
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
|
||||||
|
prompt: 'looks up'
|
||||||
|
}
|
||||||
|
const order = ['settings', 'prompt', 'characters']
|
||||||
|
const text = assembleSceneText(scene, [ada], [set], order)
|
||||||
|
assert.equal(text, 'soft light\n\nlooks up\n\nAda: tall')
|
||||||
|
assert.ok(!text.includes('open yard'))
|
||||||
|
assert.ok(!text.includes('Courtyard'))
|
||||||
|
assert.ok(!text.includes('wide'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('legacy sequence without sectionOrder keeps default order and \\n\\n\\n join', () => {
|
||||||
|
const legacy = normalizePromptSequence({
|
||||||
|
id: 'seq1',
|
||||||
|
name: 'Old order',
|
||||||
|
characters: [{ id: 'c1', name: 'Ada', description: 'tall', outfits: [] }],
|
||||||
|
sets: [{
|
||||||
|
id: 's1',
|
||||||
|
name: 'Hall',
|
||||||
|
framings: [{ id: 'f1', label: 'wide', text: 'stone hall' }]
|
||||||
|
}],
|
||||||
|
scenes: [
|
||||||
|
{
|
||||||
|
id: 'sc1',
|
||||||
|
title: 'One',
|
||||||
|
style: 'oil',
|
||||||
|
settings: 'day',
|
||||||
|
setStates: [{ setId: 's1', on: true, framingId: 'f1' }],
|
||||||
|
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
|
||||||
|
prompt: 'walk'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sc2',
|
||||||
|
title: 'Two',
|
||||||
|
style: 'ink',
|
||||||
|
settings: '',
|
||||||
|
setStates: [{ setId: 's1', on: false, framingId: 'f1' }],
|
||||||
|
characterStates: [{ characterId: 'c1', on: true, outfitId: null }],
|
||||||
|
prompt: 'stop'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
createdAt: 1,
|
||||||
|
updatedAt: 1
|
||||||
|
})
|
||||||
|
assert.deepEqual(legacy.sectionOrder, [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER])
|
||||||
|
assert.deepEqual(normalizeSectionOrder(undefined), [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER])
|
||||||
|
assert.deepEqual(normalizeSectionOrder(['prompt', 'prompt', 'bogus', 'settings']), [
|
||||||
|
'prompt',
|
||||||
|
'settings',
|
||||||
|
'style',
|
||||||
|
'set',
|
||||||
|
'characters'
|
||||||
|
])
|
||||||
|
const full = assembleSequenceText(legacy)
|
||||||
|
assert.equal(
|
||||||
|
full,
|
||||||
|
'oil\n\nday\n\nstone hall\n\nAda: tall\n\nwalk\n\n\nink\n\nAda: tall\n\nstop'
|
||||||
|
)
|
||||||
|
assert.ok(!full.includes('Hall'))
|
||||||
|
assert.ok(!full.includes('wide'))
|
||||||
|
})
|
||||||
|
|||||||
@@ -50,9 +50,18 @@ export type PromptSequenceScene = {
|
|||||||
prompt: string
|
prompt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Scene prompt sections. Default copy order. */
|
||||||
|
export const PROMPT_SEQUENCE_SECTION_KEYS = ['style', 'settings', 'set', 'characters', 'prompt'] as const
|
||||||
|
export type PromptSequenceSectionKey = (typeof PROMPT_SEQUENCE_SECTION_KEYS)[number]
|
||||||
|
export const DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER: PromptSequenceSectionKey[] = [
|
||||||
|
...PROMPT_SEQUENCE_SECTION_KEYS
|
||||||
|
]
|
||||||
|
|
||||||
export type PromptSequence = {
|
export type PromptSequence = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
|
/** Order of scene sections when assembling / in the editor. */
|
||||||
|
sectionOrder: PromptSequenceSectionKey[]
|
||||||
characters: PromptSequenceCharacter[]
|
characters: PromptSequenceCharacter[]
|
||||||
sets: PromptSequenceSet[]
|
sets: PromptSequenceSet[]
|
||||||
scenes: PromptSequenceScene[]
|
scenes: PromptSequenceScene[]
|
||||||
@@ -60,6 +69,26 @@ export type PromptSequence = {
|
|||||||
updatedAt: number
|
updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Valid unique keys in given order; missing keys append in default order. */
|
||||||
|
export function normalizeSectionOrder(raw: unknown): PromptSequenceSectionKey[] {
|
||||||
|
const allowed = new Set<string>(PROMPT_SEQUENCE_SECTION_KEYS)
|
||||||
|
const seen = new Set<PromptSequenceSectionKey>()
|
||||||
|
const ordered: PromptSequenceSectionKey[] = []
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
for (const item of raw) {
|
||||||
|
const key = String(item || '').trim()
|
||||||
|
if (!allowed.has(key) || seen.has(key as PromptSequenceSectionKey)) continue
|
||||||
|
const typed = key as PromptSequenceSectionKey
|
||||||
|
seen.add(typed)
|
||||||
|
ordered.push(typed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const key of DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER) {
|
||||||
|
if (!seen.has(key)) ordered.push(key)
|
||||||
|
}
|
||||||
|
return ordered
|
||||||
|
}
|
||||||
|
|
||||||
export const PROMPT_SEQUENCE_NAME_MAX = 80
|
export const PROMPT_SEQUENCE_NAME_MAX = 80
|
||||||
export const PROMPT_SEQUENCE_FIELD_MAX = 50_000
|
export const PROMPT_SEQUENCE_FIELD_MAX = 50_000
|
||||||
export const PROMPT_SEQUENCE_TITLE_MAX = 60
|
export const PROMPT_SEQUENCE_TITLE_MAX = 60
|
||||||
@@ -206,7 +235,8 @@ export function assembleSceneText(
|
|||||||
characters?: Array<{ name?: string; description?: string }>
|
characters?: Array<{ name?: string; description?: string }>
|
||||||
},
|
},
|
||||||
sequenceCharacters?: PromptSequenceCharacter[],
|
sequenceCharacters?: PromptSequenceCharacter[],
|
||||||
sequenceSets?: PromptSequenceSet[]
|
sequenceSets?: PromptSequenceSet[],
|
||||||
|
sectionOrder?: PromptSequenceSectionKey[] | unknown
|
||||||
) {
|
) {
|
||||||
const setBlock = sequenceSets?.length
|
const setBlock = sequenceSets?.length
|
||||||
? assembleSetBlock(scene, sequenceSets)
|
? assembleSetBlock(scene, sequenceSets)
|
||||||
@@ -217,13 +247,15 @@ export function assembleSceneText(
|
|||||||
.map(character => formatCharacterLine(character))
|
.map(character => formatCharacterLine(character))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n')
|
.join('\n')
|
||||||
const parts = [
|
const blocks: Record<PromptSequenceSectionKey, string> = {
|
||||||
String(scene.style || '').trim(),
|
style: String(scene.style || '').trim(),
|
||||||
String(scene.settings || '').trim(),
|
settings: String(scene.settings || '').trim(),
|
||||||
setBlock,
|
set: setBlock,
|
||||||
characterBlock,
|
characters: characterBlock,
|
||||||
String(scene.prompt || '').trim()
|
prompt: String(scene.prompt || '').trim()
|
||||||
].filter(Boolean)
|
}
|
||||||
|
const order = normalizeSectionOrder(sectionOrder)
|
||||||
|
const parts = order.map(key => blocks[key]).filter(Boolean)
|
||||||
return parts.join('\n\n')
|
return parts.join('\n\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,6 +265,7 @@ export function assembleSceneText(
|
|||||||
* Sequence name is never included.
|
* Sequence name is never included.
|
||||||
*/
|
*/
|
||||||
export function assembleSequenceText(sequence: {
|
export function assembleSequenceText(sequence: {
|
||||||
|
sectionOrder?: PromptSequenceSectionKey[] | unknown
|
||||||
characters?: PromptSequenceCharacter[]
|
characters?: PromptSequenceCharacter[]
|
||||||
sets?: PromptSequenceSet[]
|
sets?: PromptSequenceSet[]
|
||||||
scenes?: Array<{
|
scenes?: Array<{
|
||||||
@@ -246,11 +279,13 @@ export function assembleSequenceText(sequence: {
|
|||||||
}) {
|
}) {
|
||||||
const characters = sequence.characters || []
|
const characters = sequence.characters || []
|
||||||
const sets = sequence.sets || []
|
const sets = sequence.sets || []
|
||||||
|
const sectionOrder = normalizeSectionOrder(sequence.sectionOrder)
|
||||||
return (sequence.scenes || [])
|
return (sequence.scenes || [])
|
||||||
.map(scene => assembleSceneText(
|
.map(scene => assembleSceneText(
|
||||||
scene,
|
scene,
|
||||||
characters.length ? characters : undefined,
|
characters.length ? characters : undefined,
|
||||||
sets.length ? sets : undefined
|
sets.length ? sets : undefined,
|
||||||
|
sectionOrder
|
||||||
))
|
))
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join('\n\n\n')
|
.join('\n\n\n')
|
||||||
@@ -478,6 +513,7 @@ export function normalizePromptSequence(raw: unknown): PromptSequence | null {
|
|||||||
return {
|
return {
|
||||||
id: String(rec.id || '').trim() || newId(),
|
id: String(rec.id || '').trim() || newId(),
|
||||||
name,
|
name,
|
||||||
|
sectionOrder: normalizeSectionOrder(rec.sectionOrder),
|
||||||
characters,
|
characters,
|
||||||
sets,
|
sets,
|
||||||
scenes,
|
scenes,
|
||||||
|
|||||||
Reference in New Issue
Block a user