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:
parent
12568b82cc
commit
2623df4172
3 changed files with 145 additions and 14 deletions
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue