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:
+961
-868
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
const body = await readBody<{
|
||||
name?: string
|
||||
sectionOrder?: unknown
|
||||
characters?: unknown
|
||||
sets?: unknown
|
||||
scenes?: unknown
|
||||
@@ -11,6 +12,7 @@ export default defineEventHandler(async (event) => {
|
||||
}>(event).catch(() => ({}))
|
||||
const sequence = await savePromptSequence(owner, {
|
||||
name: body?.name || '',
|
||||
sectionOrder: body?.sectionOrder,
|
||||
characters: body?.characters,
|
||||
sets: body?.sets,
|
||||
scenes: body?.scenes,
|
||||
|
||||
@@ -3,9 +3,16 @@ import { listPromptSequences, updatePromptSequence } from '~/server/utils/prompt
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { owner } = assertLibraryOwner(event)
|
||||
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, {
|
||||
name: body?.name,
|
||||
sectionOrder: body?.sectionOrder,
|
||||
characters: body?.characters,
|
||||
sets: body?.sets,
|
||||
scenes: body?.scenes
|
||||
|
||||
@@ -7,10 +7,13 @@ import {
|
||||
normalizeCharacter,
|
||||
normalizeSet,
|
||||
normalizeScene,
|
||||
normalizeSectionOrder,
|
||||
emptyCharacter,
|
||||
emptyScene,
|
||||
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||
type PromptSequence,
|
||||
type PromptSequenceCharacter,
|
||||
type PromptSequenceSectionKey,
|
||||
type PromptSequenceSet,
|
||||
type PromptSequenceScene
|
||||
} from '~/utils/promptSequences'
|
||||
@@ -91,6 +94,7 @@ export function getPromptSequence(owner: string, id: string) {
|
||||
|
||||
export async function savePromptSequence(owner: string, input: {
|
||||
name: string
|
||||
sectionOrder?: unknown
|
||||
characters?: unknown
|
||||
sets?: unknown
|
||||
scenes?: unknown
|
||||
@@ -101,6 +105,9 @@ export async function savePromptSequence(owner: string, input: {
|
||||
const characters = normalizeCharactersInput(input.characters)
|
||||
const sets = normalizeSetsInput(input.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()
|
||||
return mutate(owner, (sequences) => {
|
||||
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}”?` })
|
||||
}
|
||||
if (existing) {
|
||||
existing.sectionOrder = sectionOrder
|
||||
existing.characters = characters
|
||||
existing.sets = sets
|
||||
existing.scenes = scenes
|
||||
@@ -117,6 +125,7 @@ export async function savePromptSequence(owner: string, input: {
|
||||
const created: PromptSequence = {
|
||||
id: crypto.randomUUID(),
|
||||
name,
|
||||
sectionOrder,
|
||||
characters,
|
||||
sets,
|
||||
scenes,
|
||||
@@ -130,6 +139,7 @@ export async function savePromptSequence(owner: string, input: {
|
||||
|
||||
export async function updatePromptSequence(owner: string, id: string, input: {
|
||||
name?: string
|
||||
sectionOrder?: unknown
|
||||
characters?: unknown
|
||||
sets?: 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` })
|
||||
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) {
|
||||
const characters = input.characters !== undefined
|
||||
? normalizeCharactersInput(input.characters)
|
||||
|
||||
@@ -26,6 +26,8 @@ const {
|
||||
emptyFraming,
|
||||
cloneSceneForNext,
|
||||
normalizePromptSequence,
|
||||
normalizeSectionOrder,
|
||||
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||
ensureSceneStatesForCharacters
|
||||
} = await import(pathToFileURL(tmp).href)
|
||||
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[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
|
||||
}
|
||||
|
||||
/** 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 = {
|
||||
id: string
|
||||
name: string
|
||||
/** Order of scene sections when assembling / in the editor. */
|
||||
sectionOrder: PromptSequenceSectionKey[]
|
||||
characters: PromptSequenceCharacter[]
|
||||
sets: PromptSequenceSet[]
|
||||
scenes: PromptSequenceScene[]
|
||||
@@ -60,6 +69,26 @@ export type PromptSequence = {
|
||||
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_FIELD_MAX = 50_000
|
||||
export const PROMPT_SEQUENCE_TITLE_MAX = 60
|
||||
@@ -206,7 +235,8 @@ export function assembleSceneText(
|
||||
characters?: Array<{ name?: string; description?: string }>
|
||||
},
|
||||
sequenceCharacters?: PromptSequenceCharacter[],
|
||||
sequenceSets?: PromptSequenceSet[]
|
||||
sequenceSets?: PromptSequenceSet[],
|
||||
sectionOrder?: PromptSequenceSectionKey[] | unknown
|
||||
) {
|
||||
const setBlock = sequenceSets?.length
|
||||
? assembleSetBlock(scene, sequenceSets)
|
||||
@@ -217,13 +247,15 @@ export function assembleSceneText(
|
||||
.map(character => formatCharacterLine(character))
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
const parts = [
|
||||
String(scene.style || '').trim(),
|
||||
String(scene.settings || '').trim(),
|
||||
setBlock,
|
||||
characterBlock,
|
||||
String(scene.prompt || '').trim()
|
||||
].filter(Boolean)
|
||||
const blocks: Record<PromptSequenceSectionKey, string> = {
|
||||
style: String(scene.style || '').trim(),
|
||||
settings: String(scene.settings || '').trim(),
|
||||
set: setBlock,
|
||||
characters: characterBlock,
|
||||
prompt: String(scene.prompt || '').trim()
|
||||
}
|
||||
const order = normalizeSectionOrder(sectionOrder)
|
||||
const parts = order.map(key => blocks[key]).filter(Boolean)
|
||||
return parts.join('\n\n')
|
||||
}
|
||||
|
||||
@@ -233,6 +265,7 @@ export function assembleSceneText(
|
||||
* Sequence name is never included.
|
||||
*/
|
||||
export function assembleSequenceText(sequence: {
|
||||
sectionOrder?: PromptSequenceSectionKey[] | unknown
|
||||
characters?: PromptSequenceCharacter[]
|
||||
sets?: PromptSequenceSet[]
|
||||
scenes?: Array<{
|
||||
@@ -246,11 +279,13 @@ export function assembleSequenceText(sequence: {
|
||||
}) {
|
||||
const characters = sequence.characters || []
|
||||
const sets = sequence.sets || []
|
||||
const sectionOrder = normalizeSectionOrder(sequence.sectionOrder)
|
||||
return (sequence.scenes || [])
|
||||
.map(scene => assembleSceneText(
|
||||
scene,
|
||||
characters.length ? characters : undefined,
|
||||
sets.length ? sets : undefined
|
||||
sets.length ? sets : undefined,
|
||||
sectionOrder
|
||||
))
|
||||
.filter(Boolean)
|
||||
.join('\n\n\n')
|
||||
@@ -478,6 +513,7 @@ export function normalizePromptSequence(raw: unknown): PromptSequence | null {
|
||||
return {
|
||||
id: String(rec.id || '').trim() || newId(),
|
||||
name,
|
||||
sectionOrder: normalizeSectionOrder(rec.sectionOrder),
|
||||
characters,
|
||||
sets,
|
||||
scenes,
|
||||
|
||||
Reference in New Issue
Block a user