/* Brand Lens — Library / repository view */ function Library({ recs, reload, goReview }) { const [filter, setFilter] = useState('all'); const [busy, setBusy] = useState(null); // {done,total} const [drag, setDrag] = useState(false); const [note, setNote] = useState(null); const fileRef = useRef(null); const importRef = useRef(null); const ordered = useMemo( () => [...recs].sort((a, b) => a.shuffleKey - b.shuffleKey), [recs] ); const counts = useMemo(() => { let reviewed = 0, aligned = 0, unrev = 0; for (const r of recs) { if (r.user) { reviewed++; if (r.ai && (r.ai.verdict === 'keep') === !!r.user.like) aligned++; } else unrev++; } return { total: recs.length, reviewed, aligned, unrev }; }, [recs]); const shown = ordered.filter((r) => { if (filter === 'all') return true; if (filter === 'unrev') return !r.user; if (filter === 'liked') return r.user && r.user.like; if (filter === 'disliked') return r.user && !r.user.like; return true; }); async function handleFiles(list) { if (!list || !list.length) return; setBusy({ done: 0, total: Array.from(list).filter((f) => /^image\//.test(f.type)).length }); await window.LensDB.addFiles(list, (done, total) => setBusy({ done, total })); setBusy(null); reload(); } async function removeOne(id) { await window.LensDB.del(id); reload(); } async function resetOne(id) { await window.LensDB.patch(id, { ai: null, user: null }); reload(); } async function clearAll() { if (!confirm('Remove ALL images and their feedback? This cannot be undone.')) return; await window.LensDB.clearAll(); reload(); } async function shuffle() { await window.LensDB.reshuffle(); reload(); } async function exportData() { const bundle = await window.LensDB.exportBundle(); const blob = new Blob([JSON.stringify(bundle)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const d = new Date(); a.href = url; a.download = `brand-lens-backup-${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}.json`; document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(url), 2000); setNote('Backup downloaded — keep it safe or move it to another machine.'); } async function importData(file) { if (!file) return; try { const bundle = JSON.parse(await file.text()); const mode = recs.length > 0 ? (confirm(`Import ${bundle.count || (bundle.images || []).length} images.\n\nOK = MERGE into your current ${recs.length}.\nCancel = REPLACE everything with the backup.`) ? 'merge' : 'replace') : 'replace'; const added = await window.LensDB.importBundle(bundle, mode); reload(); setNote(`Imported ${added} images (${mode}).`); } catch (e) { setNote('Could not read that file — is it a Brand Lens backup?'); } } const filters = [ ['all', 'All', counts.total], ['unrev', 'Not reviewed', counts.unrev], ['liked', 'Kept', recs.filter((r) => r.user && r.user.like).length], ['disliked', 'Rejected', recs.filter((r) => r.user && !r.user.like).length], ]; return (
{/* header row */}

The repository

Your taste library. Drop in photos, stock, and clip-art — they're shuffled, not served in upload order. Review them blind, then watch a style guide build itself from your calls.

fileRef.current && fileRef.current.click()}>Add images {counts.unrev > 0 && Review {counts.unrev} blind}
{ handleFiles(e.target.files); e.target.value = ''; }} /> { importData(e.target.files[0]); e.target.value = ''; }} /> {note && (
{note}
)} {/* stat strip */} {counts.total > 0 && (
Back up importRef.current && importRef.current.click()}>Restore Reshuffle Clear all
)} {/* dropzone / empty */} {counts.total === 0 ? ( fileRef.current && fileRef.current.click()} onRestore={() => importRef.current && importRef.current.click()} /> ) : (
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); handleFiles(e.dataTransfer.files); }} style={{ outline: drag ? '2px dashed var(--ps-primary-blue)' : 'none', outlineOffset: 8, borderRadius: 14 }} > {/* filters */}
{filters.map(([k, lab, n]) => ( ))}
{busy && (
Importing {busy.done}/{busy.total}…
)}
{shown.map((r) => removeOne(r.id)} onReset={() => resetOne(r.id)} />)}
{shown.length === 0 && (

Nothing in this filter yet.

)}
)}
); } function Stat({ n, l }) { return (
{n}
{l}
); } function LibCard({ rec, onRemove, onReset }) { const [hover, setHover] = useState(false); return (
setHover(true)} onMouseLeave={() => setHover(false)} style={{ margin: 0, background: 'var(--ps-white)', border: '1px solid var(--ps-line)', borderRadius: 14, overflow: 'hidden', boxShadow: hover ? 'var(--sh-2)' : 'var(--sh-1)', transform: hover ? 'translateY(-2px)' : 'none', transition: 'all .18s var(--ease-standard)' }}>
{rec.name} {(rec.ai || (rec.category && rec.category !== 'auto')) && (
)} {hover && (
{rec.user && ( )}
)}
{rec.user && }
{rec.name}
); } function IconBtn({ name, onClick, title, danger }) { return ( ); } function Dropzone({ drag, setDrag, onFiles, onPick, busy, onRestore }) { return (
{ e.preventDefault(); setDrag(true); }} onDragLeave={() => setDrag(false)} onDrop={(e) => { e.preventDefault(); setDrag(false); onFiles(e.dataTransfer.files); }} onClick={onPick} style={{ cursor: 'pointer', textAlign: 'center', padding: '72px 30px', border: `2px dashed ${drag ? 'var(--ps-primary-blue)' : 'var(--ps-line-strong)'}`, borderRadius: 20, background: drag ? 'var(--ps-dark-blue-100)' : 'var(--ps-cloud-gray)', transition: 'all .18s var(--ease-standard)' }}>
{busy ? : }
{busy ? `Importing ${busy.done}/${busy.total}…` : 'Drop images here'}
or click to browse — photos, stock, clip-art. They're resized and stored right in your browser.
{!busy && onRestore && (
Returning?{' '}
)}
); } window.Library = Library;