Files
waooplus/src/lib/novel-promotion/panel-ai-data-sync.ts

155 lines
5.0 KiB
TypeScript

export interface PanelCharacterRef {
name: string
appearance: string
slot?: string
}
type JsonRecord = Record<string, unknown>
function isJsonRecord(value: unknown): value is JsonRecord {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function assert(condition: boolean, message: string): asserts condition {
if (!condition) {
throw new Error(message)
}
}
function parseStructuredJsonFromString(raw: string, fieldName: string): unknown {
const trimmed = raw.trim()
if (!trimmed) return null
let parsed: unknown = trimmed
for (let depth = 0; depth < 2 && typeof parsed === 'string'; depth += 1) {
try {
parsed = JSON.parse(parsed)
} catch {
throw new Error(`${fieldName} must be valid JSON`)
}
}
if (typeof parsed === 'string') {
throw new Error(`${fieldName} must be JSON object/array, not a plain string`)
}
return parsed
}
function normalizeStructuredJsonInput(value: unknown, fieldName: string): unknown {
if (value === null || value === undefined || value === '') return null
if (typeof value === 'string') {
return parseStructuredJsonFromString(value, fieldName)
}
return value
}
function assertStructuredJsonValue(value: unknown, fieldName: string): asserts value is JsonRecord | unknown[] | null {
if (value === null) return
const isStructured = Array.isArray(value) || isJsonRecord(value)
assert(isStructured, `${fieldName} must be a JSON object or array`)
}
function assertNameRecord(value: unknown, fieldName: string): asserts value is JsonRecord & { name: string } {
assert(isJsonRecord(value), `${fieldName} item must be an object`)
assert(typeof value.name === 'string' && value.name.trim().length > 0, `${fieldName} item.name must be a non-empty string`)
}
function filterNamedRecordsBySet(
source: unknown[],
keepNames: ReadonlySet<string>,
fieldName: string,
): JsonRecord[] {
return source
.map((item) => {
assertNameRecord(item, fieldName)
return item
})
.filter((item) => keepNames.has(item.name))
}
function syncActingNotesJson(
actingNotesJson: string | null | undefined,
keepNames: ReadonlySet<string>,
): string | null | undefined {
if (actingNotesJson === undefined) return undefined
const parsed = normalizeStructuredJsonInput(actingNotesJson, 'actingNotes')
assertStructuredJsonValue(parsed, 'actingNotes')
if (parsed === null) return null
if (Array.isArray(parsed)) {
const filtered = filterNamedRecordsBySet(parsed, keepNames, 'actingNotes')
return JSON.stringify(filtered)
}
assert(isJsonRecord(parsed), 'actingNotes must be a JSON object or array')
const maybeCharacters = parsed.characters
if (maybeCharacters === undefined) {
return JSON.stringify(parsed)
}
assert(Array.isArray(maybeCharacters), 'actingNotes.characters must be an array')
const filtered = filterNamedRecordsBySet(maybeCharacters, keepNames, 'actingNotes.characters')
return JSON.stringify({
...parsed,
characters: filtered,
})
}
function syncPhotographyRulesJson(
photographyRulesJson: string | null | undefined,
keepNames: ReadonlySet<string>,
): string | null | undefined {
if (photographyRulesJson === undefined) return undefined
const parsed = normalizeStructuredJsonInput(photographyRulesJson, 'photographyRules')
assertStructuredJsonValue(parsed, 'photographyRules')
if (parsed === null) return null
assert(isJsonRecord(parsed), 'photographyRules must be a JSON object')
const maybeCharacters = parsed.characters
if (maybeCharacters === undefined) {
return JSON.stringify(parsed)
}
assert(Array.isArray(maybeCharacters), 'photographyRules.characters must be an array')
const filtered = filterNamedRecordsBySet(maybeCharacters, keepNames, 'photographyRules.characters')
return JSON.stringify({
...parsed,
characters: filtered,
})
}
export function serializeStructuredJsonField(value: unknown, fieldName: string): string | null {
const normalized = normalizeStructuredJsonInput(value, fieldName)
assertStructuredJsonValue(normalized, fieldName)
return normalized === null ? null : JSON.stringify(normalized)
}
export interface SyncPanelCharacterDependentJsonInput {
characters: PanelCharacterRef[]
removeIndex: number
actingNotesJson?: string | null
photographyRulesJson?: string | null
}
export interface SyncPanelCharacterDependentJsonResult {
characters: PanelCharacterRef[]
actingNotesJson?: string | null
photographyRulesJson?: string | null
}
export function syncPanelCharacterDependentJson({
characters,
removeIndex,
actingNotesJson,
photographyRulesJson,
}: SyncPanelCharacterDependentJsonInput): SyncPanelCharacterDependentJsonResult {
const nextCharacters = characters.filter((_, index) => index !== removeIndex)
const keepNames = new Set(nextCharacters.map((character) => character.name))
return {
characters: nextCharacters,
actingNotesJson: syncActingNotesJson(actingNotesJson, keepNames),
photographyRulesJson: syncPhotographyRulesJson(photographyRulesJson, keepNames),
}
}