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>
|
||||
</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="flex flex-wrap items-center justify-between gap-2">
|
||||
<div>
|
||||
@@ -271,7 +310,8 @@
|
||||
>
|
||||
</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">
|
||||
<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>
|
||||
@@ -284,7 +324,7 @@
|
||||
/>
|
||||
</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">
|
||||
<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>
|
||||
@@ -297,7 +337,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="sets.length" class="space-y-3">␍
|
||||
<div v-else-if="sectionKey === 'set' && sets.length" class="space-y-3">
|
||||
<div>
|
||||
<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>
|
||||
@@ -332,7 +372,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">␍
|
||||
<div v-else-if="sectionKey === 'characters'" class="space-y-3">
|
||||
<div>
|
||||
<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>
|
||||
@@ -367,7 +407,7 @@
|
||||
</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">
|
||||
<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>
|
||||
@@ -379,6 +419,7 @@
|
||||
placeholder="What happens in this image"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<section class="rounded-2xl border border-white/10 bg-zinc-950/30">
|
||||
@@ -467,9 +508,11 @@ import type {
|
||||
PromptSequence,
|
||||
PromptSequenceCharacter,
|
||||
PromptSequenceScene,
|
||||
PromptSequenceSectionKey,
|
||||
PromptSequenceSet
|
||||
} from '~/utils/promptSequences'
|
||||
import {
|
||||
DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER,
|
||||
PROMPT_SEQUENCE_FRAMING_SUGGESTIONS,
|
||||
assembleSceneText,
|
||||
assembleSequenceText,
|
||||
@@ -481,14 +524,24 @@ import {
|
||||
emptySet,
|
||||
ensureSceneStatesForCharacters,
|
||||
ensureSceneStatesForSets,
|
||||
normalizePromptSequence␍
|
||||
normalizePromptSequence,
|
||||
normalizeSectionOrder
|
||||
} from '~/utils/promptSequences'
|
||||
|
||||
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 selectedId = ref('')
|
||||
const name = ref('')
|
||||
const sectionOrder = ref<PromptSequenceSectionKey[]>([...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER])
|
||||
const characters = ref<PromptSequenceCharacter[]>([emptyCharacter()])
|
||||
const sets = ref<PromptSequenceSet[]>([])
|
||||
const scenes = ref<PromptSequenceScene[]>([emptyScene(1, characters.value, sets.value)])
|
||||
@@ -501,18 +554,55 @@ const copied = ref(false)
|
||||
const copiedScene = ref(false)
|
||||
let copyTimer: 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 previewText = computed(() => assembleSequenceText({
|
||||
sectionOrder: sectionOrder.value,
|
||||
characters: characters.value,
|
||||
sets: sets.value,
|
||||
scenes: scenes.value
|
||||
}))
|
||||
const activeSceneText = computed(() => {
|
||||
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) {
|
||||
const suggestion = PROMPT_SEQUENCE_FRAMING_SUGGESTIONS[index % PROMPT_SEQUENCE_FRAMING_SUGGESTIONS.length]
|
||||
return suggestion || 'wide / medium / narrow'
|
||||
@@ -521,6 +611,7 @@ function framingLabelPlaceholder(index: number) {
|
||||
function startNew() {
|
||||
selectedId.value = ''
|
||||
name.value = ''
|
||||
sectionOrder.value = [...DEFAULT_PROMPT_SEQUENCE_SECTION_ORDER]
|
||||
characters.value = [emptyCharacter()]
|
||||
sets.value = []
|
||||
scenes.value = [emptyScene(1, characters.value, sets.value)]
|
||||
@@ -533,6 +624,7 @@ function select(sequence: PromptSequence) {
|
||||
const normalized = normalizePromptSequence(sequence) || sequence
|
||||
selectedId.value = normalized.id
|
||||
name.value = normalized.name
|
||||
sectionOrder.value = normalizeSectionOrder(normalized.sectionOrder)
|
||||
characters.value = (normalized.characters?.length ? normalized.characters : [emptyCharacter()]).map(character => ({
|
||||
...emptyCharacter(),
|
||||
...character,
|
||||
@@ -781,6 +873,7 @@ async function save() {
|
||||
notice.value = ''
|
||||
const payload = {
|
||||
name: nextName,
|
||||
sectionOrder: sectionOrder.value,
|
||||
characters: characters.value,
|
||||
sets: sets.value,
|
||||
scenes: scenes.value
|
||||
|
||||
@@ -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