Spaces:
Running
Running
name lock, checkpoint toasts, modern selects, header export
Browse files- review.html +58 -6
review.html
CHANGED
|
@@ -34,8 +34,23 @@ button.drop { border-color: var(--drop); } button.drop.on { background: var(--dr
|
|
| 34 |
button.nav { padding: 10px 14px; }
|
| 35 |
#mine { color: var(--mut); font-size: 13px; margin-top: 8px; min-height: 20px; }
|
| 36 |
kbd { background: var(--chip); border-radius: 4px; padding: 1px 6px; font-size: 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
footer { position: fixed; bottom: 0; left: 0; right: 0; background: var(--card); border-top: 1px solid var(--line); padding: 8px 16px; display: flex; gap: 10px; align-items: center; font-size: 13px; color: var(--mut); }
|
| 38 |
-
footer .sp { flex: 1; }
|
| 39 |
</style>
|
| 40 |
</head>
|
| 41 |
<body>
|
|
@@ -51,10 +66,12 @@ footer .sp { flex: 1; }
|
|
| 51 |
<option value="rafael">rafael · 240</option>
|
| 52 |
</select>
|
| 53 |
<select id="src" onchange="reset()"></select>
|
| 54 |
-
<input id="who" placeholder="your name to annotate" autocomplete="off"
|
| 55 |
-
|
|
|
|
| 56 |
<button onclick="theme()" title="toggle theme">◐</button>
|
| 57 |
</header>
|
|
|
|
| 58 |
<div style="display:flex;align-items:center;gap:10px;padding:6px 16px;background:var(--card);border-bottom:1px solid var(--line);font-size:13px;color:var(--mut)">
|
| 59 |
<span id="ruledLab" style="white-space:nowrap">0 ruled</span>
|
| 60 |
<div style="flex:1;height:6px;background:var(--chip);border-radius:3px;overflow:hidden"><div id="ruledFill" style="height:100%;width:0;background:var(--keep)"></div></div>
|
|
@@ -129,8 +146,25 @@ function loadWho() {
|
|
| 129 |
});
|
| 130 |
const ht = $('hubtok');
|
| 131 |
if (ht) { ht.value = localStorage.getItem('review_hubtok') || ''; ht.addEventListener('input', () => localStorage.setItem('review_hubtok', ht.value.trim())); }
|
|
|
|
| 132 |
paintGate();
|
| 133 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
function paintGate() {
|
| 135 |
const ok = who().length > 0;
|
| 136 |
$('who').style.borderColor = ok ? '' : 'var(--drop)';
|
|
@@ -228,12 +262,10 @@ function renderRef(sp) {
|
|
| 228 |
}
|
| 229 |
$('ref').innerHTML = ref;
|
| 230 |
}
|
| 231 |
-
let lastSaved = '';
|
| 232 |
function lightMeta() {
|
| 233 |
const mine = R[span().key];
|
| 234 |
$('bKeep').className = 'keep' + (mine === 'DATA_MENTION' ? ' on' : '');
|
| 235 |
$('bDrop').className = 'drop' + (mine === 'NON_MENTION' ? ' on' : '');
|
| 236 |
-
$('mine').textContent = mine ? `your ruling: ${mine === 'DATA_MENTION' ? 'keep' : 'drop'}` : '';
|
| 237 |
$('count').textContent = Object.keys(R).length + ' ruled' + (lastSaved ? ' · last save: ' + lastSaved : '');
|
| 238 |
paintRuled();
|
| 239 |
}
|
|
@@ -242,8 +274,27 @@ function paintRuled() {
|
|
| 242 |
for (const o of ITEMS) for (const x of o.spans) { tot++; if (R[x.key]) done++; }
|
| 243 |
$('ruledFill').style.width = (tot ? 100 * done / tot : 0) + '%';
|
| 244 |
$('ruledLab').textContent = `${done}/${tot} mentions ruled`;
|
| 245 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
}
|
|
|
|
| 247 |
function bindMarks(o) {
|
| 248 |
document.querySelectorAll('#passage mark').forEach(m => m.onclick = () => {
|
| 249 |
show(i, o.spans.findIndex(x => x.key === m.dataset.k));
|
|
@@ -287,6 +338,7 @@ async function rule(v) {
|
|
| 287 |
RQ[k] = $('ds').value;
|
| 288 |
localStorage.setItem(LS, JSON.stringify(R));
|
| 289 |
localStorage.setItem('review_queues', JSON.stringify(RQ));
|
|
|
|
| 290 |
paintGate();
|
| 291 |
const entry = {key: k, ruling: v, queue: $('ds').value, annotator: who()};
|
| 292 |
let saved = 'this browser only';
|
|
|
|
| 34 |
button.nav { padding: 10px 14px; }
|
| 35 |
#mine { color: var(--mut); font-size: 13px; margin-top: 8px; min-height: 20px; }
|
| 36 |
kbd { background: var(--chip); border-radius: 4px; padding: 1px 6px; font-size: 12px; }
|
| 37 |
+
select { appearance: none; -webkit-appearance: none; background-color: var(--card);
|
| 38 |
+
border: 1px solid var(--line); border-radius: 8px; padding: 8px 30px 8px 12px; cursor: pointer;
|
| 39 |
+
background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%23888' stroke-width='1.6' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
|
| 40 |
+
background-repeat: no-repeat; background-position: right 10px center; max-width: 220px; }
|
| 41 |
+
select:focus-visible, button:focus-visible, input:focus-visible { outline: 2px solid var(--hl); outline-offset: 1px; }
|
| 42 |
+
button { transition: background .12s ease, transform .05s ease; }
|
| 43 |
+
button:active:not(:disabled) { transform: scale(.97); }
|
| 44 |
+
button:disabled { cursor: not-allowed; }
|
| 45 |
+
input[type="text"], input:not([type]) { font: inherit; color: var(--fg); background: var(--bg);
|
| 46 |
+
border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; }
|
| 47 |
+
#who { max-width: 170px; }
|
| 48 |
+
#who.locked { background: var(--chip); color: var(--mut); border-style: dashed; }
|
| 49 |
+
#lockBtn { padding: 8px 10px; }
|
| 50 |
+
#toast { position: fixed; top: 110px; left: 50%; transform: translateX(-50%); background: var(--card);
|
| 51 |
+
border: 1px solid var(--hl); border-radius: 12px; padding: 12px 16px; display: none; gap: 12px;
|
| 52 |
+
align-items: center; z-index: 50; box-shadow: 0 8px 30px rgba(0,0,0,.25); font-size: 14px; }
|
| 53 |
footer { position: fixed; bottom: 0; left: 0; right: 0; background: var(--card); border-top: 1px solid var(--line); padding: 8px 16px; display: flex; gap: 10px; align-items: center; font-size: 13px; color: var(--mut); }
|
|
|
|
| 54 |
</style>
|
| 55 |
</head>
|
| 56 |
<body>
|
|
|
|
| 66 |
<option value="rafael">rafael · 240</option>
|
| 67 |
</select>
|
| 68 |
<select id="src" onchange="reset()"></select>
|
| 69 |
+
<input id="who" placeholder="your name to annotate" autocomplete="off">
|
| 70 |
+
<button id="lockBtn" onclick="toggleLock()" title="lock / change name">unlock</button>
|
| 71 |
+
<button onclick="exp()" title="export rulings now">export</button>
|
| 72 |
<button onclick="theme()" title="toggle theme">◐</button>
|
| 73 |
</header>
|
| 74 |
+
<div id="toast" role="status"></div>
|
| 75 |
<div style="display:flex;align-items:center;gap:10px;padding:6px 16px;background:var(--card);border-bottom:1px solid var(--line);font-size:13px;color:var(--mut)">
|
| 76 |
<span id="ruledLab" style="white-space:nowrap">0 ruled</span>
|
| 77 |
<div style="flex:1;height:6px;background:var(--chip);border-radius:3px;overflow:hidden"><div id="ruledFill" style="height:100%;width:0;background:var(--keep)"></div></div>
|
|
|
|
| 146 |
});
|
| 147 |
const ht = $('hubtok');
|
| 148 |
if (ht) { ht.value = localStorage.getItem('review_hubtok') || ''; ht.addEventListener('input', () => localStorage.setItem('review_hubtok', ht.value.trim())); }
|
| 149 |
+
if (localStorage.getItem('review_locked') === '1' && who()) setLocked(true);
|
| 150 |
paintGate();
|
| 151 |
}
|
| 152 |
+
function setLocked(on) {
|
| 153 |
+
$('who').disabled = on;
|
| 154 |
+
$('who').classList.toggle('locked', on);
|
| 155 |
+
$('lockBtn').textContent = on ? 'locked' : 'unlock';
|
| 156 |
+
localStorage.setItem('review_locked', on ? '1' : '0');
|
| 157 |
+
}
|
| 158 |
+
function toggleLock() {
|
| 159 |
+
if ($('who').disabled) {
|
| 160 |
+
if (confirm('Change annotator name? Existing rulings stay under the old name.')) {
|
| 161 |
+
setLocked(false); $('who').focus();
|
| 162 |
+
}
|
| 163 |
+
return;
|
| 164 |
+
}
|
| 165 |
+
if (!who()) { needName(); return; }
|
| 166 |
+
setLocked(true);
|
| 167 |
+
}
|
| 168 |
function paintGate() {
|
| 169 |
const ok = who().length > 0;
|
| 170 |
$('who').style.borderColor = ok ? '' : 'var(--drop)';
|
|
|
|
| 262 |
}
|
| 263 |
$('ref').innerHTML = ref;
|
| 264 |
}
|
|
|
|
| 265 |
function lightMeta() {
|
| 266 |
const mine = R[span().key];
|
| 267 |
$('bKeep').className = 'keep' + (mine === 'DATA_MENTION' ? ' on' : '');
|
| 268 |
$('bDrop').className = 'drop' + (mine === 'NON_MENTION' ? ' on' : '');
|
|
|
|
| 269 |
$('count').textContent = Object.keys(R).length + ' ruled' + (lastSaved ? ' · last save: ' + lastSaved : '');
|
| 270 |
paintRuled();
|
| 271 |
}
|
|
|
|
| 274 |
for (const o of ITEMS) for (const x of o.spans) { tot++; if (R[x.key]) done++; }
|
| 275 |
$('ruledFill').style.width = (tot ? 100 * done / tot : 0) + '%';
|
| 276 |
$('ruledLab').textContent = `${done}/${tot} mentions ruled`;
|
| 277 |
+
const pct = tot ? Math.round(100 * done / tot) : 0;
|
| 278 |
+
$('ruledPct').textContent = tot ? pct + '%' : '';
|
| 279 |
+
// checkpoint nudge at each new 10% mark (per dataset)
|
| 280 |
+
const mark = Math.floor(pct / 10) * 10;
|
| 281 |
+
const mem = 'review_decile_' + $('ds').value;
|
| 282 |
+
if (tot && mark >= 10 && +(localStorage.getItem(mem) || 0) < mark) {
|
| 283 |
+
localStorage.setItem(mem, String(mark));
|
| 284 |
+
toast(`${mark}% annotated — export a checkpoint?`, true);
|
| 285 |
+
}
|
| 286 |
+
}
|
| 287 |
+
let toastTimer = null;
|
| 288 |
+
function toast(msg, withExport) {
|
| 289 |
+
const t = $('toast');
|
| 290 |
+
t.innerHTML = `<span>${esc(msg)}</span>` +
|
| 291 |
+
(withExport ? `<button onclick="exp();hideToast()">export now</button>` : '') +
|
| 292 |
+
`<button onclick="hideToast()">dismiss</button>`;
|
| 293 |
+
t.style.display = 'flex';
|
| 294 |
+
clearTimeout(toastTimer);
|
| 295 |
+
toastTimer = setTimeout(hideToast, 15000);
|
| 296 |
}
|
| 297 |
+
function hideToast() { $('toast').style.display = 'none'; }
|
| 298 |
function bindMarks(o) {
|
| 299 |
document.querySelectorAll('#passage mark').forEach(m => m.onclick = () => {
|
| 300 |
show(i, o.spans.findIndex(x => x.key === m.dataset.k));
|
|
|
|
| 338 |
RQ[k] = $('ds').value;
|
| 339 |
localStorage.setItem(LS, JSON.stringify(R));
|
| 340 |
localStorage.setItem('review_queues', JSON.stringify(RQ));
|
| 341 |
+
setLocked(true);
|
| 342 |
paintGate();
|
| 343 |
const entry = {key: k, ruling: v, queue: $('ds').value, annotator: who()};
|
| 344 |
let saved = 'this browser only';
|