rafmacalaba commited on
Commit
cb0a82b
·
verified ·
1 Parent(s): 05c6007

legacy-index.html: landing + deep links

Browse files
Files changed (1) hide show
  1. legacy-index.html +326 -0
legacy-index.html ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en"><head><meta charset="utf-8">
3
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4
+ <title>datause-ner labeling · passage queue</title>
5
+ <style>
6
+ :root{
7
+ --bg:#F6F4EF; --card:#FFFFFF; --line:#E7E3DD; --ink:#211D1A; --mut:#6F6A63;
8
+ --primary:#4F46E5; --primary-ink:#FFFFFF;
9
+ --keep:#0F7A4D; --keep-bg:#E2F5E8; --keep-line:#7BC7A2;
10
+ --drop:#C2412B; --drop-bg:#FCE8E3; --drop-line:#E8A394;
11
+ --conf:#B45309; --conf-bg:#FBF0DC; --conf-line:#E0B37A;
12
+ --unsc:#6B7280; --unsc-bg:#F1F3F5;
13
+ --mark-u:#FDF0DA; --mark-u-line:#D97706; --mark-k:#DCFCE7; --mark-k-line:#22A55B;
14
+ --mark-d:#FEE2E2; --mark-d-line:#DC2626;
15
+ }
16
+ *{box-sizing:border-box}
17
+ body{margin:0;background:var(--bg);color:var(--ink);font-family:"Fira Sans","Segoe UI",system-ui,sans-serif;font-size:15px;line-height:1.6}
18
+ header{position:sticky;top:0;z-index:30;background:rgba(255,255,255,.94);backdrop-filter:blur(6px);border-bottom:1px solid var(--line)}
19
+ .hrow{display:flex;gap:10px;align-items:center;flex-wrap:wrap;padding:10px 20px}
20
+ header h1{font-size:14px;margin:0;letter-spacing:.2px;font-weight:800}
21
+ .sub{font-size:12.5px;color:var(--mut)}
22
+ #bar{height:5px;background:var(--line);border-radius:3px;margin:2px 0}
23
+ #bar>i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--primary),#22A55B);border-radius:3px;transition:width .25s}
24
+ main{max-width:1120px;margin:0 auto;padding:18px 20px 110px;display:grid;grid-template-columns:1fr 356px;gap:22px;align-items:start}
25
+ @media(max-width:920px){main{grid-template-columns:1fr}}
26
+ .card{background:var(--card);border:1px solid var(--line);border-radius:16px;box-shadow:0 1px 2px rgba(33,29,26,.05),0 10px 28px rgba(33,29,26,.05)}
27
+ .passage-card{padding:16px 20px 14px}
28
+ .grid-2{display:grid;grid-template-columns:auto 1fr;gap:18px}
29
+ .badges{display:flex;gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:10px}
30
+ .badge{font-size:11px;font-weight:700;padding:3px 10px;border-radius:999px;background:var(--unsc-bg);color:var(--unsc)}
31
+ .b-keep{background:var(--keep-bg);color:var(--keep)}.b-drop{background:var(--drop-bg);color:var(--drop)}
32
+ .b-confusion{background:var(--conf-bg);color:var(--conf)}.b-mixed{background:#E9E6FF;color:#4338CA}.b-unscored{background:var(--unsc-bg);color:var(--unsc)}
33
+ .passage{max-height:min(54vh,560px);overflow-y:auto;font-size:14.5px;line-height:1.8;padding:14px 6px;border:1px solid var(--line);border-radius:12px;background:#FDFCFA}
34
+ .passage mark{cursor:pointer;border-radius:3px;padding:0 2px;font-weight:600}
35
+ mark.m-u{background:var(--mark-u);border-bottom:2px solid var(--mark-u-line)}
36
+ mark.m-k{background:var(--mark-k);border-bottom:2px solid var(--mark-k-line)}
37
+ mark.m-d{background:var(--mark-d);border-bottom:2px solid var(--mark-d-line)}
38
+ mark.on{outline:2px solid var(--primary);outline-offset:1px}
39
+ mark sup{font-size:9.5px;font-weight:800;color:var(--primary);margin-left:1px}
40
+ .legend{display:flex;gap:8px;align-items:center;margin-top:8px;font-size:11.5px;color:var(--mut);flex-wrap:wrap}
41
+ .legend i{display:inline-block;width:20px;height:4px;border-radius:2px}
42
+ .legend .u i{background:var(--mark-u);border-bottom:2px solid var(--mark-u-line)}
43
+ .legend .k i{background:var(--mark-k);border-bottom:2px solid var(--mark-k-line)}
44
+ .legend .d i{background:var(--mark-d);border-bottom:2px solid var(--mark-d-line)}
45
+ .legend .a i{outline:2px solid var(--primary);outline-offset:1px}
46
+ .chips{display:flex;gap:7px;align-items:center;overflow-x:auto;margin-top:10px;padding:7px 4px;scrollbar-width:thin}
47
+ .chips::-webkit-scrollbar{height:5px}.chips::-webkit-scrollbar-thumb{background:var(--line);border-radius:3px}
48
+ .chip{font:inherit;font-size:11.5px;cursor:pointer;border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:999px;padding:5px 11px;white-space:nowrap;display:inline-flex;gap:5px;align-items:center;transition:all .15s}
49
+ .chip b{width:18px;height:18px;border-radius:50%;font-size:10px;display:inline-grid;place-items:center;color:#fff}
50
+ .chip .dot{width:7px;height:7px;border-radius:50%;background:var(--conf-line)}
51
+ .chip.r-k{background:var(--keep-bg);border-color:var(--keep-line);color:var(--keep)}
52
+ .chip.r-d{background:var(--drop-bg);border-color:var(--drop-line);color:var(--drop)}
53
+ .chip.on{outline:2px solid var(--primary);outline-offset:1px}
54
+ .chip .sc{font-variant-numeric:tabular-nums}
55
+ .panel-card{padding:16px 18px;position:sticky;top:96px;display:flex;flex-direction:column;gap:12px}
56
+ .panel-label{font-size:10.5px;text-transform:uppercase;letter-spacing:.8px;color:var(--mut);font-weight:800}
57
+ .surface{font-size:20px;font-weight:800;line-height:1.35;padding:10px 12px;background:#FBF8F1;border:1px solid #EADFC4;border-radius:12px}
58
+ .pstate{font-size:13px;font-weight:700}
59
+ .pstate.un{color:var(--conf)}.pstate.k{color:var(--keep)}.pstate.d{color:var(--drop)}
60
+ .scoreline{display:flex;gap:7px;align-items:center;flex-wrap:wrap;font-size:12.5px;color:var(--mut)}
61
+ .tag{font-size:11px;font-weight:800;padding:2px 9px;border-radius:999px;letter-spacing:.3px}
62
+ .tag.k{background:var(--keep-bg);color:var(--keep)}.tag.d{background:var(--drop-bg);color:var(--drop)}
63
+ .tag.c{background:var(--conf-bg);color:var(--conf)}.tag.u{background:var(--unsc-bg);color:var(--unsc)}
64
+ .note{font:inherit;font-size:13.5px;padding:9px 12px;border:1px solid var(--line);border-radius:10px;background:#FDFCFA;width:100%}
65
+ .note:focus{outline:2px solid var(--primary);outline-offset:0;border-color:var(--primary)}
66
+ .hint{font-size:11.5px;color:var(--mut);line-height:1.55}
67
+ kbd{font-family:inherit;font-size:10.5px;background:var(--unsc-bg);border:1px solid var(--line);border-radius:5px;padding:1px 5px}
68
+ #dock{position:fixed;left:0;right:0;bottom:0;z-index:30;background:rgba(255,255,255,.97);backdrop-filter:blur(6px);border-top:1px solid var(--line);padding:11px 20px;display:flex;gap:8px;align-items:center;justify-content:center;flex-wrap:wrap}
69
+ button{font:inherit;cursor:pointer;border:1px solid var(--line);background:var(--card);color:var(--ink);border-radius:12px;padding:10px 18px;font-size:13.5px;transition:all .15s}
70
+ button.nav:hover{border-color:var(--primary);color:var(--primary)}
71
+ button:disabled{opacity:.4;cursor:default}
72
+ button.keep{background:var(--keep);border-color:var(--keep);color:#fff;font-weight:800;min-width:132px;box-shadow:0 1px 3px rgba(15,122,77,.35)}
73
+ button.keep:hover{filter:brightness(1.05)}
74
+ button.drop{background:var(--drop);border-color:var(--drop);color:#fff;font-weight:800;min-width:132px;box-shadow:0 1px 3px rgba(194,65,43,.35)}
75
+ button.drop:hover{filter:brightness(1.05)}
76
+ button.primary{background:var(--primary);border-color:var(--primary);color:#fff}
77
+ select,input[type=text]{font:inherit;font-size:12.5px;padding:6px 10px;border:1px solid var(--line);border-radius:9px;background:var(--card);color:var(--ink)}
78
+ .state{font-size:12.5px;color:var(--mut);min-width:180px;text-align:center;font-variant-numeric:tabular-nums}
79
+ #who{width:76px}
80
+ .btnrow{display:flex;gap:8px;align-items:center}
81
+ .foot{text-align:center;color:var(--mut);font-size:12px;padding-top:14px}
82
+ .empty{text-align:center;color:var(--mut);padding:60px 0;font-size:16px}
83
+ .warn{background:#FBF0DC;border:1px solid var(--conf-line);border-radius:10px;padding:8px 12px;font-size:12.5px}
84
+ </style></head><body>
85
+ <header>
86
+ <div class="hrow">
87
+ <h1>datause-ner labeling</h1>
88
+ <span class="sub" id="qname">queue</span>
89
+ <span class="sub" id="prog" style="font-weight:800;color:var(--ink)"></span><span style="flex:1"></span>
90
+ <label class="sub">annotator <input type="text" id="who" placeholder="initials" style="width:76px"></label>
91
+ <label class="sub"><input type="checkbox" id="rev"> scores</label>
92
+ <select id="fQueue" title="queue"></select>
93
+ <select id="fOrigin" title="origin filter"></select>
94
+ <select id="fBand" title="band filter"></select>
95
+ <select id="fLuna" title="Luna verdict vs model">
96
+ <option value="">luna×model: all</option><option value="agree">agree</option>
97
+ <option value="conflict">conflict</option><option value="unsure">model unsure</option></select>
98
+ <button class="primary" onclick="exportRulings()">⬇ Export</button>
99
+ <span class="sub" id="sink"></span>
100
+ </div>
101
+ <div id="bar"><i></i></div>
102
+ </header>
103
+ <main>
104
+ <div class="card passage-card">
105
+ <div class="badges"><span class="badge" id="bOrigin"></span><span class="badge" id="bQueue"></span><span class="badge" id="bBand"></span><span class="badge" id="bKey"></span></div>
106
+ <div id="noctx" class="warn" style="display:none">surface-only item — no passage available.</div>
107
+ <div class="passage" id="passage"></div>
108
+ <div class="legend">
109
+ <span class="u"><i></i> unruled</span><span class="k"><i></i> kept</span><span class="d"><i></i> dropped</span><span class="a"><i></i> active</span>
110
+ </div>
111
+ <div class="chips" id="chips"></div>
112
+ <div style="margin-top:4px"></div>
113
+ </div>
114
+ <div class="card panel-card">
115
+ <div class="panel-label">Ruling target</div>
116
+ <div class="surface" id="surface"></div>
117
+ <div class="scoreline"><span id="pos"></span><span class="sc" id="sc"></span></div>
118
+ <div class="pstate" id="pstate">unruled</div>
119
+ <div class="btnrow">
120
+ <button class="keep" id="bKeep" onclick="rule('DATA_MENTION')">✓ Keep</button>
121
+ <button class="drop" id="bDrop" onclick="rule('NON_MENTION')">✕ Drop</button>
122
+ </div>
123
+ <input class="note" id="note" placeholder="note (optional) — saved with the ruling">
124
+ <div class="hint"><kbd>k</kbd> keep · <kbd>d</kbd> drop · <kbd>←</kbd><kbd>→</kbd> mention · <kbd>↑</kbd><kbd>↓</kbd> passage · <kbd>s</kbd> skip · <kbd>z</kbd> undo</div>
125
+ </div>
126
+ <div class="empty" id="empty" style="display:none">Nothing matches the active filters.</div>
127
+ </main>
128
+ <div id="dock">
129
+ <button class="nav" id="bPPrev" onclick="moveP(-1)">⏮ P</button>
130
+ <button class="nav" id="bMPrev" onclick="moveM(-1)">‹ M <span id="mPos"></span></button>
131
+ <button class="keep" onclick="rule('DATA_MENTION')">✓ Keep</button>
132
+ <button class="drop" onclick="rule('NON_MENTION')">✕ Drop</button>
133
+ <button class="nav" id="bMNext" onclick="moveM(1)"><span id="mPos2"></span> M ›</button>
134
+ <button class="nav" id="bPNext" onclick="moveP(1)">P ⏭</button>
135
+ <button class="nav" onclick="moveP(1)">Skip <small>(s)</small></button>
136
+ <button class="nav" id="bUndo" onclick="undo()">⟲ Undo</button>
137
+ <span class="state" id="state"></span>
138
+ </div>
139
+ <script>
140
+ let Q=[], R={}, pidx=0, mi=0, lastRuled=null;
141
+ const $=id=>document.getElementById(id);
142
+ const LS='datause_ner_rulings_v1';
143
+ try{R=JSON.parse(localStorage.getItem(LS)||'{}')}catch(e){R={}}
144
+ $('who').value=localStorage.getItem('datause_ner_who')||'';
145
+ $('who').oninput=()=>localStorage.setItem('datause_ner_who',$('who').value);
146
+ const save=()=>localStorage.setItem(LS,JSON.stringify(R));
147
+ const esc=s=>String(s??'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
148
+ const mentionsOf=q=>Array.isArray(q.mentions)&&q.mentions.length?q.mentions:[{
149
+ key:q.key,surface:q.surface,start:q.start,end:q.end,
150
+ head_score:q.head_score,band:q.band}];
151
+ const isFlat=q=>!Array.isArray(q.mentions);
152
+ async function post(ruling){
153
+ try{const r=await fetch('/api/rulings',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(ruling)});
154
+ if(r.ok){$('sink').textContent='● server ('+(await r.json()).n+')';return}}catch(e){}
155
+ $('sink').textContent='● local export';
156
+ }
157
+ async function load(){
158
+ try{const r=await fetch('/api/queue');if(r.ok)Q=(await r.json()).items||[]}catch(e){}
159
+ if(!Q.length){
160
+ for(const f of ['./queue_gliner.json']){
161
+ try{const r=await fetch(f);if(r.ok)Q.push(...(await r.text()).split('\n').filter(Boolean).map(JSON.parse))}catch(e){}
162
+ }
163
+ }
164
+ fillFilters();render();
165
+ }
166
+ const lunaState=m=>{
167
+ if(m.luna==null||m.head_score==null)return null;
168
+ if(m.luna_split)return 'conflict'; // Luna itself split on the same span
169
+ if(m.band==='keep')return m.luna===1?'agree':'conflict';
170
+ if(m.band==='drop')return m.luna===0?'agree':'conflict';
171
+ return 'unsure';
172
+ };
173
+ const filtered=()=>{const qu=$('fQueue').value,o=$('fOrigin').value,b=$('fBand').value,lu=$('fLuna').value;
174
+ return Q.map((q,i)=>({q,i})).filter(({q})=>(!qu||q.queue===qu)&&(!o||q.origin===o)&&(!b||q.band===b)
175
+ &&(!lu||mentionsOf(q).some(m=>lunaState(m)===lu)))};
176
+ const totalMentions=()=>Q.reduce((n,q)=>n+(q.mentions?.length||1),0);
177
+ function fillFilters(){
178
+ const qs=[...new Set(Q.map(q=>q.queue))].sort();
179
+ $('fQueue').innerHTML=qs.map(q=>`<option value="${esc(q)}">${esc(q)} · ${Q.filter(x=>x.queue===q).length}</option>`).join('');
180
+ if(qs.includes('gliner'))$('fQueue').value='gliner';
181
+ else if(qs.includes('probe_candidates/pool'))$('fQueue').value='probe_candidates/pool';
182
+ else if(qs.includes('human473'))$('fQueue').value='human473';
183
+ const os=[...new Set(Q.map(q=>q.origin))].sort(),bs=[...new Set(Q.map(q=>q.band))].sort();
184
+ $('fOrigin').innerHTML='<option value="">all origins</option>'+os.map(o=>`<option>${esc(o)}</option>`).join('');
185
+ $('fBand').innerHTML='<option value="">all bands</option>'+bs.map(b=>`<option>${esc(b)}</option>`).join('');
186
+ for(const id of ['fQueue','fOrigin','fBand','fLuna']) $(id).onchange=()=>{pidx=0;mi=0;render()};
187
+ $('rev').onchange=()=>render();
188
+ }
189
+ function bandsHTML(m){
190
+ const t=m.band==='keep'?'k':m.band==='drop'?'d':m.band==='unscored'?'u':'c';
191
+ return `<span class="tag ${t}">${esc(m.band||'?')}</span>`;
192
+ }
193
+ function contextHTML(q){
194
+ const p=q.ctx||'';const ms=mentionsOf(q);
195
+ const marks=[];
196
+ for(let i=0;i<ms.length;i++){
197
+ const m=ms[i];let s=m.start,e=m.end;
198
+ if(!(Number.isInteger(s)&&Number.isInteger(e)&&e>s&&p.slice(s,e)===m.surface)){
199
+ s=p.indexOf(m.surface||'');e=s<0?0:s+(m.surface||'').length;
200
+ }
201
+ if(s<0)continue;
202
+ marks.push({s,e,i});
203
+ }
204
+ // char-once walk: overlapping spans never duplicate passage text; a mark
205
+ // starting inside the previous one is suppressed in-text (chip remains)
206
+ marks.sort((a,b)=>a.s-b.s||b.e-a.e);
207
+ let out='',cur=0,suppressed=0;
208
+ for(const mk of marks){
209
+ if(mk.s<cur){suppressed++;continue}
210
+ out+=esc(p.slice(cur,mk.s));
211
+ const r=R[ms[mk.i].key];
212
+ const cls=r? (r.ruling==='DATA_MENTION'?'m-k':'m-d') : 'm-u';
213
+ out+=`<mark class="${cls}" data-mi="${mk.i}" onclick="clickM(${mk.i})">${esc(p.slice(mk.s,mk.e))}${ms.length>1?`<sup>${mk.i+1}</sup>`:''}</mark>`;
214
+ cur=mk.e;
215
+ }
216
+ out+=esc(p.slice(cur));
217
+ return out;
218
+ }
219
+ function render(){
220
+ const F=filtered();
221
+ const ruled=Object.keys(R).length, tm=totalMentions();
222
+ $('prog').textContent=`${ruled}/${tm} ruled`;
223
+ $('qname').textContent=`${$('fQueue').value||'queue'} · ${F.length}`;
224
+ $('empty').style.display=F.length?'none':'block';
225
+ const bar=document.querySelector('#bar>i');bar.style.width=(tm?100*ruled/tm:0)+'%';
226
+ if(!F.length){$('passage').innerHTML='';$('chips').innerHTML='';$('state').textContent='adjust filters';return}
227
+ pidx=Math.max(0,Math.min(pidx,F.length-1));
228
+ const q=F[pidx].q, ms=mentionsOf(q);
229
+ mi=Math.max(0,Math.min(mi,ms.length-1));
230
+ const m=ms[mi],r=R[m.key];
231
+ $('bOrigin').textContent=[q.origin,q.subset].filter(Boolean).join(' · ');
232
+ const nk=ms.filter(x=>x.luna===1).length,nd=ms.filter(x=>x.luna===0).length;
233
+ $('bQueue').textContent=(isFlat(q)?'single mention':`${q.mentions.length} mentions`)
234
+ +(nk+nd?` · luna: ${nk}✓ / ${nd}✕`:'');
235
+ const bb=$('bBand');bb.textContent=`tag: ${q.band}`;bb.className='badge b-'+(q.band||'unscored');
236
+ $('bKey').textContent=q.mentions?m.key:q.key;
237
+ $('noctx').style.display=q.ctx?'none':'block';
238
+ $('passage').innerHTML=q.ctx?contextHTML(q):`<div class="empty">${esc(m.surface||'(no context)')}</div>`;
239
+ for(const el of $('passage').querySelectorAll('mark[data-mi]')){
240
+ if(el.dataset.mi===String(mi))el.classList.add('on');
241
+ else el.classList.remove('on');
242
+ }
243
+ $('passage').style.display='';
244
+ const act=$('passage').querySelector('mark.on');
245
+ if(act)act.scrollIntoView({block:'center',behavior:'smooth'});
246
+ $('chips').innerHTML=ms.map((mm,i)=>{
247
+ const rr=R[mm.key];
248
+ const cls=rr?(rr.ruling==='DATA_MENTION'?'r-k':'r-d'):'';
249
+ const on=i===mi?'on':'';
250
+ const dot=mm.luna_split?'var(--conf)':mm.luna===1?'var(--keep)':mm.luna===0?'var(--drop)':'var(--conf-line)';
251
+ return `<button class="chip ${cls} ${on}" data-ma="${i}" onclick="clickM(${i})"><b>${i+1}</b><span class="dot" style="background:${dot}"></span>${esc(mm.surface)}</button>`;
252
+ }).join('');
253
+ const actChip=$('chips').querySelector('button.on');
254
+ if(actChip)actChip.scrollIntoView({inline:'center',behavior:'smooth',block:'nearest'});
255
+ const sc=$('rev').checked?(m.head_score==null?'n/a':(+m.head_score).toFixed(3)):'•••';
256
+ $('surface').textContent=m.surface||'(no surface)';
257
+ $('pos').textContent=`M ${mi+1}/${ms.length}`;
258
+ $('sc').innerHTML=bandsHTML(m)
259
+ +(m.luna_split?`<span class="tag c">Luna split ✓/✕</span>`
260
+ :m.luna!=null?`<span class="tag ${m.luna===1?'k':'d'}">Luna ${m.luna===1?'✓ kept':'✕ dropped'}</span>`:'')
261
+ +`<span class="sc">probe ${sc}</span>`
262
+ +(m.extractor_score!=null?`<span class="sc">ext ${(+m.extractor_score).toFixed(2)}</span>`:'');
263
+ const st=$('pstate');
264
+ if(r){st.textContent=(r.ruling==='DATA_MENTION'?'✓ kept':'✕ dropped')+' — your ruling';st.className='pstate '+(r.ruling==='DATA_MENTION'?'k':'d')}
265
+ else{st.textContent='unruled — model tag above is a hint, you decide';st.className='pstate un'}
266
+ $('note').value=r?.note||'';
267
+ $('state').textContent=`P ${pidx+1}/${F.length} · M ${mi+1}/${ms.length}${r?' · ruled':''}`;
268
+ $('mPos').textContent=`${mi+1}/${ms.length}`;$('mPos2').textContent=`${mi+1}/${ms.length}`;
269
+ $('bPPrev').disabled=pidx===0;$('bPNext').disabled=pidx>=F.length-1;
270
+ $('bMPrev').disabled=mi===0&&pidx===0;$('bMNext').disabled=mi>=ms.length-1&&pidx>=F.length-1;
271
+ $('bUndo').disabled=!lastRuled;
272
+ }
273
+ function clickM(i){mi=i;render()}
274
+ function moveM(d){
275
+ const F=filtered();if(!F.length)return;
276
+ const ms=mentionsOf(F[pidx].q);
277
+ if(d>0){if(mi<ms.length-1)mi++;else if(pidx<F.length-1){pidx++;mi=0} }
278
+ else{if(mi>0)mi--;else if(pidx>0){pidx--;mi=mentionsOf(F[pidx].q).length-1} }
279
+ render();
280
+ }
281
+ function moveP(d){
282
+ const F=filtered();if(!F.length)return;
283
+ pidx=Math.max(0,Math.min(pidx+d,F.length-1));mi=0;render();
284
+ }
285
+ async function rule(v){
286
+ const F=filtered();if(!F.length)return;
287
+ const q=F[pidx].q,ms=mentionsOf(q),m=ms[mi];
288
+ R[m.key]={key:m.key,ruling:v,annotator:$('who').value||'anon',ts:new Date().toISOString(),
289
+ note:$('note').value||'',head_score:m.head_score,origin:q.origin,surface:m.surface,queue:q.queue};
290
+ lastRuled=m.key;save();post(R[m.key]);
291
+ if(mi<ms.length-1)mi++;
292
+ else if(pidx<F.length-1){pidx++;mi=0}
293
+ render();
294
+ }
295
+ function undo(){
296
+ if(!lastRuled)return;
297
+ const F=filtered();
298
+ let found=false;
299
+ for(let fi=0;fi<F.length;fi++){
300
+ const ms=mentionsOf(F[fi].q);
301
+ for(let j=0;j<ms.length;j++){
302
+ if(ms[j].key===lastRuled){pidx=fi;mi=j;found=true;break}
303
+ }
304
+ if(found)break;
305
+ }
306
+ delete R[lastRuled];lastRuled=null;save();
307
+ if(!found){$('state').textContent='ruled item outside current filters';return}
308
+ render();
309
+ }
310
+ function exportRulings(){
311
+ const blob=new Blob([JSON.stringify({items:Object.values(R)},null,1)],{type:'application/json'});
312
+ const a=document.createElement('a');a.href=URL.createObjectURL(blob);
313
+ a.download=`rulings-${$('who').value||'anon'}-export.json`;a.click();
314
+ }
315
+ document.addEventListener('keydown',e=>{
316
+ if(/INPUT|SELECT|TEXTAREA/.test(e.target.tagName))return;
317
+ if(e.key==='k')rule('DATA_MENTION');
318
+ else if(e.key==='d')rule('NON_MENTION');
319
+ else if(e.key==='ArrowRight')moveM(1);
320
+ else if(e.key==='ArrowLeft')moveM(-1);
321
+ else if(e.key==='ArrowDown'||e.key==='s')moveP(1);
322
+ else if(e.key==='ArrowUp')moveP(-1);
323
+ else if(e.key==='z')undo();
324
+ });
325
+ load();
326
+ </script></body></html>