diff --git a/shared/prompt-sequences.mjs b/shared/prompt-sequences.mjs deleted file mode 100644 index 2610397..0000000 --- a/shared/prompt-sequences.mjs +++ /dev/null @@ -1,68 +0,0 @@ -/** 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') -} diff --git a/tests/prompt-sequences.test.mjs b/tests/prompt-sequences.test.mjs index 86d7567..ee318c5 100644 --- a/tests/prompt-sequences.test.mjs +++ b/tests/prompt-sequences.test.mjs @@ -1,12 +1,28 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { +import { readFileSync, writeFileSync, unlinkSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { pathToFileURL } from 'node:url' +import ts from 'typescript' + +const src = readFileSync(new URL('../utils/promptSequences.ts', import.meta.url), 'utf8') +const js = ts.transpileModule(src, { + compilerOptions: { + target: ts.ScriptTarget.ES2022, + module: ts.ModuleKind.ESNext + } +}).outputText +const tmp = join(tmpdir(), `prompt-sequences-${process.pid}.mjs`) +writeFileSync(tmp, js) +const { assembleSceneText, assembleSequenceText, formatCharacterLine, emptyScene, cloneSceneForNext -} from '../shared/prompt-sequences.mjs' +} = await import(pathToFileURL(tmp).href) +unlinkSync(tmp) test('formatCharacterLine covers name/description cases', () => { assert.equal(formatCharacterLine({ name: 'Ada', description: 'tall' }), 'Ada: tall') diff --git a/utils/promptSequences.ts b/utils/promptSequences.ts index a44e297..502aa6c 100644 --- a/utils/promptSequences.ts +++ b/utils/promptSequences.ts @@ -25,21 +25,89 @@ export const PROMPT_SEQUENCE_NAME_MAX = 80 export const PROMPT_SEQUENCE_FIELD_MAX = 50_000 export const PROMPT_SEQUENCE_TITLE_MAX = 60 -export { - newId, - emptyCharacter, - emptyScene, - cloneSceneForNext, - formatCharacterLine, - assembleSceneText, - assembleSequenceText -} from '../shared/prompt-sequences.mjs' +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)}` +} -import { - newId, - emptyCharacter, - emptyScene -} from '../shared/prompt-sequences.mjs' +export function emptyCharacter(): PromptSequenceCharacter { + return { id: newId(), name: '', description: '' } +} + +export function emptyScene(index = 1): PromptSequenceScene { + return { + id: newId(), + title: `Scene ${Math.max(1, index)}`, + style: '', + settings: '', + characters: [emptyCharacter()], + prompt: '' + } +} + +export function cloneSceneForNext(previous: PromptSequenceScene, nextIndex: number): PromptSequenceScene { + 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 + } +} + +/** Character line: "Name: description", or name-only, or description-only. */ +export function formatCharacterLine(character: { name?: string; description?: string }) { + 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: { + style?: string + settings?: string + characters?: Array<{ name?: string; description?: string }> + prompt?: string +}) { + 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') +} + +/** + * Whole sequence as one copyable block. + * Scenes join with a double blank line (\n\n\n) — same separator desktop Batch uses. + * Sequence name is never included. + */ +export function assembleSequenceText(sequence: { + scenes?: Array<{ + style?: string + settings?: string + characters?: Array<{ name?: string; description?: string }> + prompt?: string + }> +}) { + return (sequence.scenes || []) + .map(assembleSceneText) + .filter(Boolean) + .join('\n\n\n') +} export function normalizePromptSequenceName(raw: unknown) { return String(raw || '').trim().replace(/\s+/g, ' ').slice(0, PROMPT_SEQUENCE_NAME_MAX)