summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Cleberg <[email protected]>2026-04-18 23:23:25 -0500
committerChristian Cleberg <[email protected]>2026-04-18 23:23:25 -0500
commit5d356671364305b590447da577602b3cc55b7179 (patch)
tree32e0fda957b8d07a210ca2107fd109bbb9da0745
parent6f904cff030f5a5c714af4a38f72b9640a7b62e5 (diff)
downloadbrand-bench-5d356671364305b590447da577602b3cc55b7179.tar.gz
brand-bench-5d356671364305b590447da577602b3cc55b7179.tar.bz2
brand-bench-5d356671364305b590447da577602b3cc55b7179.zip
adjust for ai output structures
-rw-r--r--src/engine/aiGenerator.ts5
-rw-r--r--src/lib/sanitize.ts37
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) => {