/* Brand Lens — calibration loop: the AI calls it FIRST, you react and say why. Your comment is the signal; one Agree/Disagree tap captures convergence. */ function Review({ recs, reload, goLibrary, goScore, layout, setLayout }) { const buildQueue = useCallback( () => recs.filter((r) => !r.user).sort((a, b) => a.shuffleKey - b.shuffleKey).map((r) => r.id), [recs] ); const [queue, setQueue] = useState(buildQueue); const [pos, setPos] = useState(0); const [agree, setAgree] = useState(null); // true = AI got it right, false = wrong const [comment, setComment] = useState(''); const [category, setCategory] = useState('auto'); const [aiState, setAiState] = useState('idle'); // idle|loading|ready|error const [submitting, setSubmitting] = useState(false); const [retryTick, setRetryTick] = useState(0); // bump to re-run analysis after an error const aiCache = useRef({}); // 'id|cat' -> verdict const recById = useMemo(() => Object.fromEntries(recs.map((r) => [r.id, r])), [recs]); const currentId = queue[pos]; const rec = currentId ? recById[currentId] : null; const done = pos >= queue.length; const catKey = (id, cat) => id + '|' + cat; useEffect(() => { if (rec) setCategory(rec.category || 'auto'); /* eslint-disable-next-line */ }, [currentId]); // analyze current image (shown immediately) for the chosen category; prefetch next useEffect(() => { let cancelled = false; async function ensure(id, cat, isCurrent) { if (!id) return; const r = recById[id]; if (!r) return; const key = catKey(id, cat); if (aiCache.current[key]) { if (isCurrent) setAiState('ready'); return; } if (r.ai && (r.ai.declared || 'auto') === cat) { aiCache.current[key] = r.ai; if (isCurrent) setAiState('ready'); return; } if (isCurrent) setAiState('loading'); try { const v = await window.Lens.analyzeImage(r, cat === 'auto' ? null : cat); if (cancelled) return; aiCache.current[key] = v; if (isCurrent) await window.LensDB.patch(id, { ai: v, category: cat }); if (isCurrent) setAiState('ready'); } catch (e) { console.warn('analyze failed', e); if (isCurrent && !cancelled) setAiState('error'); } } if (currentId) ensure(currentId, category, true); const nextId = queue[pos + 1]; if (nextId) ensure(nextId, (recById[nextId] && recById[nextId].category) || 'auto', false); return () => { cancelled = true; }; // eslint-disable-next-line }, [currentId, category, retryTick]); const ai = aiState === 'ready' ? aiCache.current[catKey(currentId, category)] : null; async function submit() { if (agree === null || !ai) return; setSubmitting(true); const aiKeep = ai.verdict === 'keep'; const like = agree ? aiKeep : !aiKeep; // derive the user's keep/reject from their reaction await window.LensDB.patch(currentId, { ai, category, user: { agree, like, comment: comment.trim(), reviewedAt: Date.now() }, }); await reload(); setSubmitting(false); advance(); } function advance() { setAgree(null); setComment(''); setAiState('idle'); setPos((p) => p + 1); } function retry() { setAiState('idle'); setRetryTick((t) => t + 1); } function restart() { reload(); const q = buildQueue(); setQueue(q); setPos(0); setAgree(null); setComment(''); setAiState('idle'); } if (done || queue.length === 0) { return (

{queue.length === 0 ? 'Nothing left to review' : 'Round complete'}

{queue.length === 0 ? 'Add more images in the repository, or revisit your scoreboard and style guide.' : 'Now refresh the style guide — it re-teaches the judge from this round, so the next guesses come in closer.'}

See the scoreboard Back to repository {recs.some((r) => !r.user) && Review more}
); } const total = queue.length; const panel = ( ); const imgCard = ; return (
{layout === 'split' ? (
{imgCard}
{panel}
) : (
{imgCard}
{panel}
)}
); } function ReviewHeader({ pos, total, layout, setLayout }) { const brief = window.Lens.getLearnedBrief(); return (
Calibrate
Image {pos + 1} of {total} · the AI calls it first — you react and say why
{brief && ( Calibrated · {brief.basedOn} )}
{[['focus', 'cards', 'Focus'], ['split', 'panel', 'Split']].map(([k, ic, lab]) => ( ))}
); } function ImageStage({ rec, layout }) { const cap = layout === 'split' ? 'calc(100vh - 200px)' : '60vh'; return (
{rec.name}
); } window.Review = Review;