// ========================================================================== // Generative-UI vocabulary — the card library // Every card follows the same chrome: mono header row + content + optional footer // ========================================================================== const genStyles = { card: { background: 'var(--card-solid)', border: '1px solid var(--card-border)', borderRadius: 'var(--radius)', boxShadow: 'var(--shadow-md)', overflow: 'hidden', position: 'relative', }, header: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 18px', borderBottom: '1px solid var(--line-soft)', fontFamily: 'var(--font-mono)', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase', color: 'var(--ink-mute)', }, body: { padding: '18px' }, }; function CardShell({ kind, title, children, actions, flush, style }) { return (
{kind} {title && — {title}}
{actions}
{children}
); } // ============================================================ // 1. FILE UPLOAD CARD — shows parsed context from a file // ============================================================ function FileCard({ file }) { return (
{file.parsed.title}
{file.type === 'pdf' ? `${file.pages}p` : `${file.rows} rows`}
{file.parsed.summary}
{file.parsed.tags.map(t => ( {t} ))}
); } // ============================================================ // 2. CONTEXT CARD — "here's what I understood" // ============================================================ function ContextCard({ files, onOpenFile }) { return (

I've read both files. You're launching a Dry January push for BRAME — the botanical aperitif, 3 variants. Last quarter your Craft Heritage series drove most of your growth (CTR 3.2%), and two creatives are flagged for fatigue.

{files.map((f,i) => (
onOpenFile && onOpenFile(f)} style={{ padding: '10px 12px', background: 'var(--bg)', border: '1px solid var(--line-soft)', borderRadius: 8, display: 'flex', gap: 10, alignItems: 'center', cursor: 'pointer', }}>
{f.name}
))}
); } // ============================================================ // 3. AESTHETIC TILE PICKER // ============================================================ function AestheticTiles({ tiles, value, onChange }) { return (
{tiles.map(t => { const active = value === t.id; return ( ); })}
); } // ============================================================ // 4. AUDIENCE SLIDER (Belgian ↔ international) // ============================================================ function AudienceSlider({ value, onChange, labels = ['Hyper-local', 'International'] }) { return (
{labels[0]}
onChange(+e.target.value)} style={{ width: '100%', accentColor: 'var(--moss-deep)' }} />
{value}
{labels[1]}
); } // ============================================================ // 5. TONE SLIDER (named poles) // ============================================================ function ToneSlider({ poles, value, onChange, kind = 'enrichment' }) { return (
{poles[0]}
onChange(+e.target.value)} style={{ width: '100%', accentColor: 'var(--moss-deep)' }} />
{poles[1]}
); } // ============================================================ // 6. CHANNEL MATRIX // ============================================================ function ChannelMatrix({ channels, value, onChange, kind = 'enrichment · 3 of 4', title = 'where should this live' }) { return (
{channels.map(c => { const active = value.includes(c.id); return ( ); })}
); } // ============================================================ // 7. PERSONA CHIP RACK // ============================================================ function PersonaRack({ personas, value, onChange, kind = 'enrichment · 2 of 3' }) { return (
{personas.map(p => { const active = value.includes(p.id); return ( ); })}
); } // ============================================================ // 8. BUDGET DIAL — radial control // ============================================================ function BudgetDial({ value, onChange, kind = 'enrichment · 3 of 3' }) { const tiers = [ { id: 'small', label: '$', note: '< €25K', angle: -120 }, { id: 'mid', label: '$$', note: '€25–100K', angle: -40 }, { id: 'large', label: '$$$', note: '€100–500K', angle: 40 }, { id: 'xl', label: '$$$$', note: '€500K+', angle: 120 }, ]; const active = tiers.find(t => t.id === value) || tiers[1]; return (
{tiers.map(t => { const x = 70 * Math.cos((t.angle - 90) * Math.PI / 180); const y = 70 * Math.sin((t.angle - 90) * Math.PI / 180); return ; })} {active.label} {active.note}
{tiers.map(t => ( ))}
); } // ============================================================ // 9. CONSTRAINT CHIPS // ============================================================ function ConstraintChips({ value, onChange, kind = 'constraints' }) { const chips = [ "Dutch-language primary", "Must include product in hero frame", "Must not feature alcohol", "18+ targeting", "No human faces", "Founder not on-camera", "Vegan-certified callout", ]; return (
{chips.map(c => { const on = value.includes(c); return ( ); })}
); } // ============================================================ // 10. BRIEF CARD — structured brief // ============================================================ // Small card showing a past ad variant: thumbnail + ROAS chip + caption. // Used inside BriefCard to surface real evidence next to the new brief. function ReferenceVariantCard({ ref_ }) { const [imgErr, setImgErr] = React.useState(false); const [expanded, setExpanded] = React.useState(false); const url = ref_ && ref_.visual_url; const roas = ref_ && typeof ref_.roas === 'number' ? ref_.roas : null; const isVideo = url && /\.(mp4|mov|webm)(\?|$)/i.test(url); const signals = Array.isArray(ref_ && ref_.match_signals) ? ref_.match_signals : []; const rank = ref_ && ref_.rank; const winPattern = ref_ && ref_.win_pattern; // adaptation_note is the new field; why_referenced is the legacy field — render whichever is present. const adaptationNote = (ref_ && (ref_.adaptation_note || ref_.why_referenced)) || ''; const selectionReasoning = (ref_ && ref_.selection_reasoning) || ''; const hasMore = Boolean(winPattern || adaptationNote || signals.length > 0); return (
{url && !imgErr && !isVideo && ( {ref_.ad_id setImgErr(true)} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> )} {(imgErr || !url || isVideo) && (
{isVideo ? '▶ video reference' : 'no preview'}
)} {rank && typeof rank.position === 'number' && typeof rank.of === 'number' && (
#{rank.position} of {rank.of}
)} {roas !== null && (
ROAS {roas.toFixed(2)}
)}
{selectionReasoning && (
Why this card
{selectionReasoning}
)} {expanded && ( <> {signals.length > 0 && (
{signals.map((s, i) => ( {s} ))}
)} {winPattern && (
What worked
{winPattern}
)} {adaptationNote && (
How to adapt
{adaptationNote}
)} )}
{ref_.ad_id ? (
{ref_.ad_id}
) :
} {hasMore && ( )}
); } function BriefCard({ brief, critique, onOpen, generating }) { const warnings = (critique && Array.isArray(critique.warnings)) ? critique.warnings : []; const sevColor = (sev) => { if (sev === 'error') return { bg: '#fdecec', border: '#e08a8a', text: '#a72020' }; if (sev === 'warning') return { bg: '#fdf4e3', border: '#d6a85a', text: '#8a5a10' }; return { bg: '#eef3fb', border: '#9bb6dc', text: '#345583' }; }; return ( }> {generating ? ( ) : (
{warnings.length > 0 && (
Grounding check · {warnings.length} flag{warnings.length === 1 ? '' : 's'}
{warnings.map((w, i) => { const c = sevColor(w.severity); return (
{w.severity || 'warning'} · {w.field || 'unknown'}
"{w.claim}"
{w.gap}
); })}
)} {brief.why_now && (
Why now
{typeof brief.why_now === 'string' ? (
{brief.why_now}
) : ( <> {brief.why_now.hook && (
{brief.why_now.hook}
)} {Array.isArray(brief.why_now.signals) && brief.why_now.signals.length > 0 && (
    {brief.why_now.signals.map((s, i) =>
  • {s}
  • )}
)} )}
)} {brief.objective && (
Objective
{brief.objective}
)} {brief.audience && (
Audience
{typeof brief.audience === 'string' ? (
{brief.audience}
) : ( <> {brief.audience.who && (
{brief.audience.who}
)}
{Array.isArray(brief.audience.pains) && brief.audience.pains.length > 0 && (
Pains
    {brief.audience.pains.map((p, i) =>
  • {p}
  • )}
)} {Array.isArray(brief.audience.desires) && brief.audience.desires.length > 0 && (
Desires
    {brief.audience.desires.map((d, i) =>
  • {d}
  • )}
)}
)}
)} {[ { k: 'Core insight', v: brief.insight, serif: true }, { k: 'Message', v: brief.message, big: true }, ].map(row => (
{row.k}
{row.v}
))} {brief.reference_variants && brief.reference_variants.length > 0 && (
Grounded in · {brief.reference_variants.length} past performer{brief.reference_variants.length === 1 ? '' : 's'}
{brief.selection_logic && (
Selection logic
{brief.selection_logic}
)}
{brief.reference_variants.map((ref, i) => ( ))}
)}
Channels
{brief.channels.map(c => ( {c} ))}
KPIs
    {brief.kpis.map(k =>
  • {k}
  • )}
)}
); } // ============================================================ // 11. MESSAGING ARCHITECTURE — hierarchy diagram // ============================================================ function MessagingArchitecture({ pillars, onOpen }) { return ( }>
{pillars.map((p,i) => (
Pillar 0{i+1}
{p.pillar}
Proof
{p.proof}
Activation
{p.activation}
))}
); } // ============================================================ // 12. AD CONCEPT CARD — single ad mockup // ============================================================ function AdConcept({ concept, onClick, mini = false }) { const h = mini ? 180 : 280; const isVert = concept.channel.includes('9:16') || concept.channel.includes('Story'); const aspect = isVert ? 9/16 : (concept.channel.includes('OOH') ? 16/9 : 1); const w = h * aspect; return ( ); } // ============================================================ // 12. AD CONCEPT GRID // ============================================================ // // TODO[image-pipeline, Claude Code]: // Each concept currently renders with `c.bg` as a CSS gradient/solid color — // a placeholder for the generated creative. When the image pipeline is live: // 1. Extend the `concept` shape from api.generateConcepts() to include: // imageUrl : string (signed URL to the generated image asset) // imagePrompt: string (the prompt used — kept for regen + transparency) // imageModel : string ('imagen-4' / 'flux-pro' / 'internal-brand-lora') // 2. Replace the `background: c.bg` inline style below with a layered // `background: url(${c.imageUrl}) center/cover, ${c.bg}` (bg as fallback // while the image loads or if gen fails). // 3. The "Open in Inku" handoff on HandoffCard should carry imagePrompt + // imageModel so the creative team can regenerate variants downstream. // 4. If the brand-LoRA is trained (per-org), route gen through that // endpoint and tag each artifact with the LoRA version for audit. // Seam lives in api.js → generateConcepts() and generateVariants(). function AdConceptGrid({ concepts, onOpen }) { return ( }>
{concepts.map((c, i) => (
onOpen && onOpen(c)} />
))}
); } // ============================================================ // 13. VARIANT MATRIX — 12 variants laid out in a grid // ============================================================ // TODO[image-pipeline, Claude Code]: same seam as AdConceptGrid above — // each variant's `bg` is a placeholder for a real generated image. Extend // the variant shape with imageUrl / imagePrompt / imageModel and swap the // background in the grid cell below. Per-org brand-LoRA routing applies. function VariantMatrix({ variants, onOpen }) { return ( }>
{variants.map((v, i) => (
onOpen && onOpen(v)}>
BRAME
{v.copy}
{v.persona}
{v.channel} · {v.hook}
))}
); } // ============================================================ // 14. CONTENT CALENDAR // ============================================================ function ContentCalendar({ onOpen }) { // 4 weeks × 7 days = 28 cells. Mix of filled/empty const posts = [ { day: 1, ch: 'Meta', label: 'Teaser' }, { day: 3, ch: 'TikTok', label: 'Hero' }, { day: 4, ch: 'Email', label: 'Drop' }, { day: 6, ch: 'Pinterest', label: 'Pin' }, { day: 8, ch: 'Meta', label: 'Reel' }, { day: 10, ch: 'TikTok', label: 'Creator' }, { day: 12, ch: 'OOH', label: 'Wild' }, { day: 14, ch: 'Email', label: 'Week2' }, { day: 15, ch: 'Meta', label: 'UGC' }, { day: 17, ch: 'TikTok', label: 'Ritual' }, { day: 19, ch: 'Pinterest', label: 'Pin' }, { day: 22, ch: 'Meta', label: 'Proof' }, { day: 23, ch: 'Email', label: 'Recap' }, { day: 25, ch: 'TikTok', label: 'Story' }, { day: 28, ch: 'OOH', label: 'Refresh' }, { day: 30, ch: 'Email', label: 'Finale' }, ]; const channelColor = { Meta: '#1877F2', TikTok: '#1F1A13', Pinterest: '#E60023', OOH: '#C67B4A', Email: '#4E5A2F' }; const dayNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S']; return ( }>
{dayNames.map((d, i) => (
{d}
))}
{Array.from({ length: 31 }).map((_, i) => { const day = i + 1; const dayPosts = posts.filter(p => p.day === day); return (
{day}
{dayPosts.slice(0,2).map((p, j) => (
{p.label}
))}
); })}
{Object.entries(channelColor).map(([ch, color]) => (
{ch}
))}
); } // ============================================================ // 15. HANDOFF CARD — Open in Inku // ============================================================ function HandoffCard({ brief, onOpen }) { return (
Ready to produce
Send this package to Inku production
1 brief · 3 pillars · 6 concepts · 31-day calendar · 12 variants queued
); } // ============================================================ // Open-in-Inku mini button (used in many card actions) // ============================================================ function OpenInInku({ onClick, label = 'Open in Inku' }) { return ( ); } // ============================================================ // Performance hypothesis card — predicted uplift // ============================================================ function PerfHypothesis({ concept, lift = 23, because }) { return (
+{lift}%
"{because}"
Based on 412 rows of last-quarter performance + 3 matched cohorts
); } // ============================================================ // Helper: a streaming "drafting..." loader // ============================================================ function GeneratingStream({ lines }) { const [idx, setIdx] = React.useState(0); React.useEffect(() => { if (idx >= lines.length) return; const t = setTimeout(() => setIdx(idx + 1), 600); return () => clearTimeout(t); }, [idx, lines.length]); return (
{lines.map((l, i) => (
{i < idx ? : (i === idx ? : )} {l}
))}
); } Object.assign(window, { CardShell, FileCard, ContextCard, AestheticTiles, AudienceSlider, ToneSlider, ChannelMatrix, PersonaRack, BudgetDial, ConstraintChips, BriefCard, MessagingArchitecture, AdConcept, AdConceptGrid, VariantMatrix, ContentCalendar, HandoffCard, OpenInInku, PerfHypothesis, GeneratingStream, });