diff options
| -rw-r--r-- | src/engine/aiGenerator.ts | 5 | ||||
| -rw-r--r-- | src/lib/sanitize.ts | 37 |
2 files changed, 31 insertions, 11 deletions
diff --git a/src/engine/aiGenerator.ts b/src/engine/aiGenerator.ts index 5648ec9..bd66549 100644 --- a/src/engine/aiGenerator.ts +++ b/src/engine/aiGenerator.ts @@ -75,7 +75,10 @@ function buildUserPrompt(inputs: BrandInputs): string { function validate(raw: unknown): BrandOutputs { const result = sanitizeOutputs(raw); - if (!result) throw new Error('Response is not a valid brand output object'); + if (!result) { + const preview = JSON.stringify(raw)?.slice(0, 300) ?? '(unparseable)'; + throw new Error(`Model returned an unexpected structure. Try a larger model.\n\nGot: ${preview}`); + } return result; } diff --git a/src/lib/sanitize.ts b/src/lib/sanitize.ts index d63eb19..b77893e 100644 --- a/src/lib/sanitize.ts +++ b/src/lib/sanitize.ts @@ -48,30 +48,47 @@ function normalizeHex(hex: string): string { * Recursively coerce every field of a brand output object to its expected type. * Returns `null` if the input is clearly not a valid brand output at all. */ +const DEFAULT_SWATCHES = [ + { id: 's0', name: 'Background', hex: '#ffffff', role: 'background' }, + { id: 's1', name: 'Surface', hex: '#f5f5f5', role: 'neutral' }, + { id: 's2', name: 'Primary', hex: '#2563eb', role: 'primary' }, + { id: 's3', name: 'Accent', hex: '#7c3aed', role: 'accent' }, + { id: 's4', name: 'Text', hex: '#111111', role: 'text' }, +]; + export function sanitizeOutputs(raw: unknown): BrandOutputs | null { if (!raw || typeof raw !== 'object') return null; - const r = raw as Record<string, unknown>; + let r = raw as Record<string, unknown>; + + // Unwrap single-key envelopes: {"brand_package": {...}} → {...} + const keys = Object.keys(r); + if (keys.length === 1 && r[keys[0]] && typeof r[keys[0]] === 'object') { + r = r[keys[0]] as Record<string, unknown>; + } - // Must have at least the core keys to be worth keeping - if (!r.overview && !r.positioning && !r.tone) return null; + // Must have at least one recognisable content field + if (!r.overview && !r.positioning && !r.tone && !r.titles && !r.palette) return null; const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>; const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>; const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>; - const swatches = Array.isArray(palette.swatches) - ? palette.swatches.map((s: unknown, i: number) => { + // Accept swatches under palette.swatches or palette.colors + const rawSwatches = Array.isArray(palette.swatches) ? palette.swatches + : Array.isArray(palette.colors) ? palette.colors + : []; + + const swatches = rawSwatches.length + ? rawSwatches.map((s: unknown, i: number) => { const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>; return { id: str(sw.id, `s${i}`), name: str(sw.name, 'Color'), - hex: normalizeHex(str(sw.hex, '#888888')), - role: str(sw.role, 'accent'), + hex: normalizeHex(str(sw.hex ?? sw.color ?? sw.value, '#888888')), + role: str(sw.role ?? sw.type, 'accent'), }; }) - : []; - - if (!swatches.length) return null; + : DEFAULT_SWATCHES; const visualDirections = Array.isArray(r.visualDirections) ? r.visualDirections.map((v: unknown, i: number) => { |
