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:
parent
3f0760c98e
commit
12568b82cc
3 changed files with 104 additions and 2 deletions
|
|
@ -38,7 +38,7 @@ cd scripts/barrier_picker_app
|
||||||
docker compose up --build
|
docker compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
Then browse to http://localhost:8000/.
|
Then browse to http://localhost:8085/.
|
||||||
|
|
||||||
The container mounts:
|
The container mounts:
|
||||||
- `/mnt/data/projects/cupido` (data volume, read-only)
|
- `/mnt/data/projects/cupido` (data volume, read-only)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ services:
|
||||||
image: cupido-barrier-picker
|
image: cupido-barrier-picker
|
||||||
container_name: cupido-barrier-picker
|
container_name: cupido-barrier-picker
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8085:8000"
|
||||||
volumes:
|
volumes:
|
||||||
# Project data volume (videos + tracking DBs + merged TSV) — read-only.
|
# Project data volume (videos + tracking DBs + merged TSV) — read-only.
|
||||||
- /mnt/data/projects/cupido:/mnt/data/projects/cupido:ro
|
- /mnt/data/projects/cupido:/mnt/data/projects/cupido:ro
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,32 @@
|
||||||
#flash.show { opacity: 1; }
|
#flash.show { opacity: 1; }
|
||||||
#flash.ok { background: #2d5; color: #042; }
|
#flash.ok { background: #2d5; color: #042; }
|
||||||
#flash.err { background: #d44; color: white; }
|
#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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -57,8 +83,56 @@
|
||||||
<h1>Cupido — barrier picker</h1>
|
<h1>Cupido — barrier picker</h1>
|
||||||
<span id="info">loading…</span>
|
<span id="info">loading…</span>
|
||||||
<span id="progress"></span>
|
<span id="progress"></span>
|
||||||
|
<button id="help-btn" title="show the help modal">?</button>
|
||||||
</header>
|
</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 ·
|
||||||
|
<kbd>1</kbd>/<kbd>2</kbd>/<kbd>3</kbd> the three buttons ·
|
||||||
|
<kbd>s</kbd> skip · <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>
|
<main>
|
||||||
<div id="meta"></div>
|
<div id="meta"></div>
|
||||||
<video id="player" controls preload="auto"></video>
|
<video id="player" controls preload="auto"></video>
|
||||||
|
|
@ -213,6 +287,8 @@
|
||||||
// Keyboard shortcuts
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
|
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).
|
// Prevent the browser default (e.g. video focus side effects on space).
|
||||||
const stop = () => { e.preventDefault(); e.stopPropagation(); };
|
const stop = () => { e.preventDefault(); e.stopPropagation(); };
|
||||||
switch (e.key) {
|
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();
|
fetchQueue();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue