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