/** Pure prompt-sequence helpers (shared by page + tests). */ export function newId() { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID() } return `ps-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}` } export function emptyCharacter() { return { id: newId(), name: '', description: '' } } export function emptyScene(index = 1) { return { id: newId(), title: `Scene ${Math.max(1, index)}`, style: '', settings: '', characters: [emptyCharacter()], prompt: '' } } export function cloneSceneForNext(previous, nextIndex) { return { id: newId(), title: `Scene ${nextIndex}`, style: previous.style, settings: previous.settings, characters: (previous.characters || []).map(character => ({ id: newId(), name: character.name, description: character.description })), prompt: previous.prompt } } export function formatCharacterLine(character) { const name = String(character?.name || '').trim() const description = String(character?.description || '').trim() if (!name && !description) return '' if (name && description) return `${name}: ${description}` return name || description } export function assembleSceneText(scene) { const characterBlock = (scene?.characters || []) .map(formatCharacterLine) .filter(Boolean) .join('\n') const parts = [ String(scene?.style || '').trim(), String(scene?.settings || '').trim(), characterBlock, String(scene?.prompt || '').trim() ].filter(Boolean) return parts.join('\n\n') } /** Scenes join with \\n\\n\\n (desktop Batch double blank). Name is never included. */ export function assembleSequenceText(sequence) { return (sequence?.scenes || []) .map(assembleSceneText) .filter(Boolean) .join('\n\n\n') }