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 {
7575
7676function validate(raw: unknown): BrandOutputs {
7777 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 }
7982 return result;
8083}
8184
src/lib/sanitize.ts +27 −10
@@ -48,30 +48,47 @@ function normalizeHex(hex: string): string {
4848 * Recursively coerce every field of a brand output object to its expected type.
4949 * Returns `null` if the input is clearly not a valid brand output at all.
5050 */
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
5159export function sanitizeOutputs(raw: unknown): BrandOutputs | null {
5260 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 }
5468
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;
5771
5872 const tone = (r.tone && typeof r.tone === 'object' ? r.tone : {}) as Record<string, unknown>;
5973 const typo = (r.typography && typeof r.typography === 'object' ? r.typography : {}) as Record<string, unknown>;
6074 const palette = (r.palette && typeof r.palette === 'object' ? r.palette : {}) as Record<string, unknown>;
6175
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) => {
6483 const sw = (s && typeof s === 'object' ? s : {}) as Record<string, unknown>;
6584 return {
6685 id: str(sw.id, `s${i}`),
6786 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'),
7089 };
7190 })
72 : [];
73
74 if (!swatches.length) return null;
91 : DEFAULT_SWATCHES;
7592
7693 const visualDirections = Array.isArray(r.visualDirections)
7794 ? r.visualDirections.map((v: unknown, i: number) => {