krz/brand-bench

A dynamic brand documentation generator with Ollama integration. branding documentation generator ollama

Commit b4f395d373

b4f395d373dcd214f3084756f50617fa4453f16e

parent: 5143028cb8

Unsigned

cmc <hello@cleberg.net> · 2026-04-19 04:23 UTC

adjust for ai output structures

Layout: unified · split

src/engine/aiGenerator.ts +4 −1
@@ -75,7 +75,10 @@ function buildUserPrompt(inputs: BrandInputs): string {
75 75
76function validate(raw: unknown): BrandOutputs { 76function validate(raw: unknown): BrandOutputs {
77 const result = sanitizeOutputs(raw); 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 return result; 82 return result;
80} 83}
81 84
src/lib/sanitize.ts +27 −10
@@ -48,30 +48,47 @@ function normalizeHex(hex: string): string {
48 * Recursively coerce every field of a brand output object to its expected type. 48 * Recursively coerce every field of a brand output object to its expected type.
49 * Returns `null` if the input is clearly not a valid brand output at all. 49 * Returns `null` if the input is clearly not a valid brand output at all.
50 */ 50 */
51const 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
51export function sanitizeOutputs(raw: unknown): BrandOutputs | null { 59export function sanitizeOutputs(raw: unknown): BrandOutputs | null {
52 if (!raw || typeof raw !== 'object') return null; 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 69 // Must have at least one recognisable content field
56 if (!r.overview && !r.positioning && !r.tone) return null; 70 if (!r.overview && !r.positioning && !r.tone && !r.titles && !r.palette) return null;
57 71
58 const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>; 72 const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>;
59 const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>; 73 const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>;
60 const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>; 74 const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>;
61 75
62 const swatches = Array.isArray(palette.swatches) 76 // Accept swatches under palette.swatches or palette.colors
63 ? palette.swatches.map((s: unknown, i: number) => { 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 const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>; 83 const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>;
65 return { 84 return {
66 id: str(sw.id, `s${i}`), 85 id: str(sw.id, `s${i}`),
67 name: str(sw.name, 'Color'), 86 name: str(sw.name, 'Color'),
68 hex: normalizeHex(str(sw.hex, '#888888')), 87 hex: normalizeHex(str(sw.hex ?? sw.color ?? sw.value, '#888888')),
69 role: str(sw.role, 'accent'), 88 role: str(sw.role ?? sw.type, 'accent'),
70 }; 89 };
71 }) 90 })
72 : []; 91 : DEFAULT_SWATCHES;
73
74 if (!swatches.length) return null;
75 92
76 const visualDirections = Array.isArray(r.visualDirections) 93 const visualDirections = Array.isArray(r.visualDirections)
77 ? r.visualDirections.map((v: unknown, i: number) => { 94 ? r.visualDirections.map((v: unknown, i: number) => {