Commit b4f395d373
Unsigned
Layout: unified · split
src/engine/aiGenerator.ts +4 −1
| @@ -75,7 +75,10 @@ function buildUserPrompt(inputs: BrandInputs): string { | ||
| 75 | 75 | |
| 76 | 76 | function validate(raw: unknown): BrandOutputs { |
| 77 | 77 | const result = sanitizeOutputs(raw); |
| 78 | if (!result) throw new Error('Response is not a valid brand output object'); | |
| 78 | if (!result) { | |
| 79 | const preview = JSON.stringify(raw)?.slice(0, 300) ?? '(unparseable)'; | |
| 80 | throw new Error(`Model returned an unexpected structure. Try a larger model.\n\nGot: ${preview}`); | |
| 81 | } | |
| 79 | 82 | return result; |
| 80 | 83 | } |
| 81 | 84 | |
src/lib/sanitize.ts +27 −10
| @@ -48,30 +48,47 @@ function normalizeHex(hex: string): string { | ||
| 48 | 48 | * Recursively coerce every field of a brand output object to its expected type. |
| 49 | 49 | * Returns `null` if the input is clearly not a valid brand output at all. |
| 50 | 50 | */ |
| 51 | const DEFAULT_SWATCHES = [ | |
| 52 | { id: 's0', name: 'Background', hex: '#ffffff', role: 'background' }, | |
| 53 | { id: 's1', name: 'Surface', hex: '#f5f5f5', role: 'neutral' }, | |
| 54 | { id: 's2', name: 'Primary', hex: '#2563eb', role: 'primary' }, | |
| 55 | { id: 's3', name: 'Accent', hex: '#7c3aed', role: 'accent' }, | |
| 56 | { id: 's4', name: 'Text', hex: '#111111', role: 'text' }, | |
| 57 | ]; | |
| 58 | ||
| 51 | 59 | export function sanitizeOutputs(raw: unknown): BrandOutputs | null { |
| 52 | 60 | if (!raw || typeof raw !== 'object') return null; |
| 53 | const r = raw as Record<string, unknown>; | |
| 61 | let r = raw as Record<string, unknown>; | |
| 62 | ||
| 63 | // Unwrap single-key envelopes: {"brand_package": {...}} → {...} | |
| 64 | const keys = Object.keys(r); | |
| 65 | if (keys.length === 1 && r[keys[0]] && typeof r[keys[0]] === 'object') { | |
| 66 | r = r[keys[0]] as Record<string, unknown>; | |
| 67 | } | |
| 54 | 68 | |
| 55 | // Must have at least the core keys to be worth keeping | |
| 56 | if (!r.overview && !r.positioning && !r.tone) return null; | |
| 69 | // Must have at least one recognisable content field | |
| 70 | if (!r.overview && !r.positioning && !r.tone && !r.titles && !r.palette) return null; | |
| 57 | 71 | |
| 58 | 72 | const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>; |
| 59 | 73 | const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>; |
| 60 | 74 | const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>; |
| 61 | 75 | |
| 62 | const swatches = Array.isArray(palette.swatches) | |
| 63 | ? palette.swatches.map((s: unknown, i: number) => { | |
| 76 | // Accept swatches under palette.swatches or palette.colors | |
| 77 | const rawSwatches = Array.isArray(palette.swatches) ? palette.swatches | |
| 78 | : Array.isArray(palette.colors) ? palette.colors | |
| 79 | : []; | |
| 80 | ||
| 81 | const swatches = rawSwatches.length | |
| 82 | ? rawSwatches.map((s: unknown, i: number) => { | |
| 64 | 83 | const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>; |
| 65 | 84 | return { |
| 66 | 85 | id: str(sw.id, `s${i}`), |
| 67 | 86 | name: str(sw.name, 'Color'), |
| 68 | hex: normalizeHex(str(sw.hex, '#888888')), | |
| 69 | role: str(sw.role, 'accent'), | |
| 87 | hex: normalizeHex(str(sw.hex ?? sw.color ?? sw.value, '#888888')), | |
| 88 | role: str(sw.role ?? sw.type, 'accent'), | |
| 70 | 89 | }; |
| 71 | 90 | }) |
| 72 | : []; | |
| 73 | ||
| 74 | if (!swatches.length) return null; | |
| 91 | : DEFAULT_SWATCHES; | |
| 75 | 92 | |
| 76 | 93 | const visualDirections = Array.isArray(r.visualDirections) |
| 77 | 94 | ? r.visualDirections.map((v: unknown, i: number) => { |