Picker: identify the analyst (initials) per pick

Each annotation row now carries an `analyst` column. On first visit the
web picker shows a small login modal asking for initials, persists them
in localStorage, and shows the badge in the top-right. Click the badge
to change identities. Submissions without initials are rejected by the
backend (HTTP 400). Skip remains analyst-free.

Backfill: every existing barrier_opening.csv row marked as `GG` since
all current picks were done by Giorgio.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-01 14:23:57 +01:00
parent 12568b82cc
commit 2623df4172
3 changed files with 145 additions and 14 deletions

View file

@ -76,6 +76,37 @@
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
font-size: 0.95rem; }
#modal-close:hover { background: #3e6; }
/* User badge in header */
#user-badge { background: #2a3; color: #042; font-weight: 700;
padding: 0.2rem 0.6rem; border-radius: 12px;
font-family: ui-monospace, monospace; font-size: 0.85rem;
cursor: pointer; user-select: none; }
#user-badge:hover { background: #3b4; }
#user-badge.empty { background: #d84; color: #311; }
/* Login modal — narrower than the welcome modal */
#login-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.85);
display: none; align-items: center; justify-content: center;
z-index: 200; }
#login-backdrop.show { display: flex; }
#login-box { background: #222; border: 1px solid #444; border-radius: 6px;
padding: 1.6rem 2rem; max-width: 380px; width: 90%;
color: #ddd; }
#login-box h2 { margin: 0 0 0.6rem; color: #fff; font-size: 1.1rem; }
#login-box p { margin: 0.4rem 0 1rem; color: #aab; font-size: 0.9rem; }
#login-input { width: 100%; padding: 0.6rem 0.8rem; font-size: 1.1rem;
background: #111; color: #fff; border: 1px solid #444;
border-radius: 4px; text-align: center;
font-family: ui-monospace, monospace; letter-spacing: 0.2em;
text-transform: uppercase; }
#login-input:focus { outline: none; border-color: #6c5; }
#login-submit { width: 100%; margin-top: 1rem; padding: 0.7rem;
background: #2d5; color: #053; font-weight: 600;
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
font-size: 0.95rem; }
#login-submit:hover { background: #3e6; }
#login-submit:disabled { background: #444; color: #888; cursor: not-allowed; }
</style>
</head>
<body>
@ -83,9 +114,20 @@
<h1>Cupido — barrier picker</h1>
<span id="info">loading…</span>
<span id="progress"></span>
<span id="user-badge" title="click to change initials"></span>
<button id="help-btn" title="show the help modal">?</button>
</header>
<div id="login-backdrop">
<div id="login-box">
<h2>Who are you?</h2>
<p>Enter your initials so we can record who annotated each video.
(Just letters, e.g. <code>GG</code>.)</p>
<input id="login-input" maxlength="4" autocomplete="off" autofocus />
<button id="login-submit" disabled>Continue</button>
</div>
</div>
<div id="modal-backdrop">
<div id="modal">
<h2>Welcome to the Cupido barrier picker</h2>
@ -162,6 +204,55 @@
const meta = document.getElementById('meta');
const progress = document.getElementById('progress');
const flash = document.getElementById('flash');
const userBadge = document.getElementById('user-badge');
const loginBackdrop = document.getElementById('login-backdrop');
const loginInput = document.getElementById('login-input');
const loginSubmit = document.getElementById('login-submit');
// ─── Analyst identity (persisted in localStorage) ───────────────────
function getAnalyst() {
try { return localStorage.getItem('cupido.analyst') || ''; }
catch (e) { return ''; }
}
function setAnalyst(s) {
try { localStorage.setItem('cupido.analyst', s); } catch (e) {}
renderUserBadge();
}
function renderUserBadge() {
const a = getAnalyst();
userBadge.textContent = a || 'sign in';
userBadge.classList.toggle('empty', !a);
}
function showLogin() {
loginBackdrop.classList.add('show');
loginInput.value = getAnalyst();
loginSubmit.disabled = !loginInput.value.trim();
setTimeout(() => loginInput.focus(), 50);
}
function hideLogin() { loginBackdrop.classList.remove('show'); }
function submitLogin() {
const v = loginInput.value.trim().toUpperCase();
if (!v) return;
setAnalyst(v);
hideLogin();
// After first login, show the welcome modal if not yet seen.
try {
if (localStorage.getItem('cupido.welcomed') !== '1') {
showModal(); // welcome modal — defined below
}
} catch (e) {}
}
loginInput.addEventListener('input', () => {
loginSubmit.disabled = !loginInput.value.trim();
});
loginInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && loginInput.value.trim()) submitLogin();
});
loginSubmit.addEventListener('click', submitLogin);
userBadge.addEventListener('click', showLogin);
let queue = [];
let cursor = 0;
@ -232,11 +323,17 @@
async function submit(mode) {
if (queue.length === 0) return;
// Require initials before any picking action (skip is OK without).
if (mode !== 'skip' && !getAnalyst()) {
showLogin();
return;
}
const item = queue[cursor];
const payload = {
idx: item.idx,
time_s: (mode === 'skip' || mode === 'unusable') ? null : player.currentTime,
mode: mode,
analyst: getAnalyst(),
notes: '',
};
try {
@ -287,8 +384,9 @@
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
// Don't react to picker shortcuts while the modal is open.
// Don't react to picker shortcuts while either modal is open.
if (modalBackdrop.classList.contains('show')) return;
if (loginBackdrop.classList.contains('show')) return;
// Prevent the browser default (e.g. video focus side effects on space).
const stop = () => { e.preventDefault(); e.stopPropagation(); };
switch (e.key) {
@ -324,19 +422,24 @@
}
modalClose.addEventListener('click', hideModal);
helpBtn.addEventListener('click', showModal);
// Allow click-on-backdrop to dismiss (but not click-inside-modal)
modalBackdrop.addEventListener('click', (e) => {
if (e.target === modalBackdrop) hideModal();
});
// Escape closes the modal too.
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && modalBackdrop.classList.contains('show')) {
e.stopPropagation(); hideModal();
}
});
let welcomed = false;
try { welcomed = localStorage.getItem('cupido.welcomed') === '1'; } catch (e) {}
if (!welcomed) showModal();
// On first visit, login first (mandatory) then welcome modal.
renderUserBadge();
if (!getAnalyst()) {
showLogin();
} else {
let welcomed = false;
try { welcomed = localStorage.getItem('cupido.welcomed') === '1'; } catch (e) {}
if (!welcomed) showModal();
}
fetchQueue();
</script>