/* Brand Lens — scoreboard: alignment, calibration, divergences */ function Scoreboard({ recs, goReview, goGuide }) { const reviewed = useMemo(() => recs.filter((r) => r.user && r.ai), [recs]); const m = useMemo(() => { let kk = 0, kr = 0, rk = 0, rr = 0; // you-keep/AI-keep ... you-reject/AI-reject let likedScores = [], dislikedScores = []; for (const r of reviewed) { const aiKeep = r.ai.verdict === 'keep'; const youKeep = !!r.user.like; if (youKeep && aiKeep) kk++; else if (youKeep && !aiKeep) kr++; else if (!youKeep && aiKeep) rk++; else rr++; (youKeep ? likedScores : dislikedScores).push(r.ai.score); } const aligned = kk + rr; const total = reviewed.length; const avg = (a) => (a.length ? Math.round(a.reduce((x, y) => x + y, 0) / a.length) : null); return { kk, kr, rk, rr, aligned, total, pct: total ? Math.round((aligned / total) * 100) : 0, avgLiked: avg(likedScores), avgDisliked: avg(dislikedScores) }; }, [reviewed]); const divergences = useMemo( () => reviewed.filter((r) => (r.ai.verdict === 'keep') !== !!r.user.like) .sort((a, b) => Math.abs(b.ai.score - 50) - Math.abs(a.ai.score - 50)), [reviewed] ); if (reviewed.length === 0) { return ; } const brief = window.Lens.getLearnedBrief(); return (

Scoreboard

How often you agreed with the AI's call across {m.total} reviewed {m.total === 1 ? 'image' : 'images'} — and where you didn't. Refresh the style guide between rounds to re-teach the judge; this number should climb.

{brief && (
The judge is currently calibrated from {brief.basedOn} of your reviews. Agreement above reflects that calibration.
)}
{m.pct >= 80 ? 'The AI reads your taste well.' : m.pct >= 55 ? 'The AI is roughly tracking your taste.' : 'The AI and your taste diverge a lot — refresh the guide to recalibrate it.'}
You agreed with {m.aligned} of {m.total} calls · {divergences.length} divergence{divergences.length === 1 ? '' : 's'} to learn from.
{/* confusion matrix */}

Where the calls landed

{/* calibration */}

Score calibration

Average brand-fit score the AI gave the images you actually kept vs. the ones you passed. A big gap means the score is a reliable proxy for your taste.

{m.avgLiked != null && m.avgDisliked != null && (
Separation of {Math.abs(m.avgLiked - m.avgDisliked)} points {Math.abs(m.avgLiked - m.avgDisliked) >= 25 ? ' — the score tracks your taste well.' : ' — modest; your taste has nuance the raw score misses.'}
)}
{/* divergences */} {divergences.length > 0 && ( <>

Learn from the divergences

The images where you and the AI disagreed. These carry the most signal about what makes your taste yours.

{divergences.map((r) => )}
)}
Build the style guide {recs.some((r) => !r.user) && Keep reviewing}
); } function Cell({ n, label, sub, aligned }) { return (
{n} {label}
{sub}
); } function CalBar({ label, score, good }) { return (
{label}
{score == null ? '—' : score}
); } function DivergeRow({ rec }) { return (
{rec.name}
You AI fit {rec.ai.score}
{rec.user.comment ? “{rec.user.comment}” : No note left.} {' '}· AI: {rec.ai.headline}
); } function Empty({ title, body, cta, onCta }) { return (

{title}

{body}

{cta}
); } window.Scoreboard = Scoreboard; window.LensEmpty = Empty;