Welcome modal + port 8085

Add a dismissable welcome modal that walks first-time users through the
proper annotation sequence (slider to end → check open ROIs → slider to
start → arrow-key fine-tune → click). Stays hidden after the first
"Got it" via localStorage; the ? button in the header reopens it any
time. Picker keyboard shortcuts are inert while the modal is showing.

Container exposes 8085 instead of 8000 (8000 was free, but Giorgio's
preferred 8082 is already in use on this host; 8085 is the closest
free port). Internal port stays 8000 so the FastAPI app is unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Giorgio Gilestro 2026-05-01 14:15:42 +01:00
parent 3f0760c98e
commit 12568b82cc
3 changed files with 104 additions and 2 deletions

View file

@ -38,7 +38,7 @@ cd scripts/barrier_picker_app
docker compose up --build
```
Then browse to http://localhost:8000/.
Then browse to http://localhost:8085/.
The container mounts:
- `/mnt/data/projects/cupido` (data volume, read-only)

View file

@ -4,7 +4,7 @@ services:
image: cupido-barrier-picker
container_name: cupido-barrier-picker
ports:
- "8000:8000"
- "8085:8000"
volumes:
# Project data volume (videos + tracking DBs + merged TSV) — read-only.
- /mnt/data/projects/cupido:/mnt/data/projects/cupido:ro

View file

@ -50,6 +50,32 @@
#flash.show { opacity: 1; }
#flash.ok { background: #2d5; color: #042; }
#flash.err { background: #d44; color: white; }
/* Welcome modal */
#help-btn { background: transparent; border: 1px solid #444; color: #aab;
font-size: 0.85rem; padding: 0.3rem 0.7rem; }
#help-btn:hover { background: #2a2a2a; }
#modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,0.85);
display: none; align-items: center; justify-content: center;
z-index: 100; padding: 1rem; }
#modal-backdrop.show { display: flex; }
#modal { background: #222; border: 1px solid #444; border-radius: 6px;
padding: 1.6rem 2rem; max-width: 640px; width: 100%;
max-height: 90vh; overflow-y: auto; color: #ddd; line-height: 1.55; }
#modal h2 { margin: 0 0 0.6rem; color: #fff; font-size: 1.2rem; }
#modal h3 { margin: 1.2rem 0 0.4rem; color: #cce; font-size: 0.95rem;
font-weight: 600; }
#modal p { margin: 0.5rem 0; }
#modal ol { padding-left: 1.4rem; margin: 0.5rem 0; }
#modal li { margin: 0.6rem 0; }
#modal .button-tag { display: inline-block; padding: 0.05rem 0.4rem;
background: #2d5; color: #053; border-radius: 3px;
font-weight: 600; font-size: 0.85rem; }
#modal-close { margin-top: 1.4rem; width: 100%; padding: 0.7rem;
background: #2d5; color: #053; font-weight: 600;
border-radius: 4px; cursor: pointer; border: 1px solid #1a4;
font-size: 0.95rem; }
#modal-close:hover { background: #3e6; }
</style>
</head>
<body>
@ -57,8 +83,56 @@
<h1>Cupido — barrier picker</h1>
<span id="info">loading…</span>
<span id="progress"></span>
<button id="help-btn" title="show the help modal">?</button>
</header>
<div id="modal-backdrop">
<div id="modal">
<h2>Welcome to the Cupido barrier picker</h2>
<p>For each tracked video you'll mark the moment the barrier between
the two halves of the arena is removed and the flies start interacting.
That timestamp is what every downstream "post-opening" analysis needs.</p>
<h3>How to annotate one video</h3>
<ol>
<li><strong>Drag the seekbar to the end of the video</strong> and check
whether <em>all</em> ROIs are open or only the upper or lower half.
This determines which button you'll click later.</li>
<li><strong>Drag back toward the beginning</strong> to roughly find
the moment the barrier(s) lift. The opening can be anywhere from a
few seconds to several minutes in.</li>
<li><strong>Use the arrow keys</strong> to pin down the exact frame:
<kbd></kbd>/<kbd></kbd> jump ±5 s, <kbd>Shift</kbd>+arrow jump ±30 s.
Pause the player on the opening frame.</li>
<li><strong>Click the button</strong> matching what opened — the
playhead time is recorded, ROI inclusion is set accordingly, and
the next video loads automatically.</li>
</ol>
<h3>The three buttons</h3>
<p>
<span class="button-tag">All barriers open</span> — every ROI usable<br>
<span class="button-tag">Upper barrier opens</span> — only ROIs 1, 3, 5 (top row); lower row will be excluded from analysis<br>
<span class="button-tag">Lower barrier opens</span> — only ROIs 2, 4, 6 (bottom row); upper row excluded
</p>
<h3>Other controls</h3>
<p>
<kbd>Space</kbd> play/pause &middot;
<kbd>1</kbd>/<kbd>2</kbd>/<kbd>3</kbd> the three buttons &middot;
<kbd>s</kbd> skip &middot; <kbd>u</kbd> mark unusable
</p>
<p style="font-size: 0.85rem; color: #889;">Each click saves to
<code>barrier_opening.csv</code> immediately, so closing the tab
and coming back resumes from where you left off.</p>
<button id="modal-close">Got it — let's start</button>
</div>
</div>
<main>
<div id="meta"></div>
<video id="player" controls preload="auto"></video>
@ -213,6 +287,8 @@
// 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.
if (modalBackdrop.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) {
@ -236,6 +312,32 @@
}
});
// Welcome modal — show first time, dismissable; ? button reopens it.
const modalBackdrop = document.getElementById('modal-backdrop');
const modalClose = document.getElementById('modal-close');
const helpBtn = document.getElementById('help-btn');
function showModal() { modalBackdrop.classList.add('show'); }
function hideModal() {
modalBackdrop.classList.remove('show');
try { localStorage.setItem('cupido.welcomed', '1'); } catch (e) {}
}
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();
fetchQueue();
</script>
</body>