rafmacalaba commited on
Commit
75ee81e
Β·
1 Parent(s): c9986d8

feat: add annotation leaderboard with medals and stats

Browse files

- /api/leaderboard tallies per-annotator: verified, added, docs, score
- Leaderboard modal with πŸ₯‡πŸ₯ˆπŸ₯‰ medals and stats table
- πŸ† button in top bar to open
- Score = verified + human-added mentions

app/api/leaderboard/route.js ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HF_DATASET_BASE_URL, MAX_DOCS_TO_SCAN } from '../../../utils/config.js';
2
+
3
+ /**
4
+ * GET /api/leaderboard
5
+ * Returns annotator rankings based on validation counts.
6
+ */
7
+ export async function GET() {
8
+ try {
9
+ const linksUrl = `${HF_DATASET_BASE_URL}/raw/main/annotation_data/wbg_data/wbg_pdf_links.json`;
10
+ const linksRes = await fetch(linksUrl, {
11
+ headers: { 'Authorization': `Bearer ${process.env.HF_TOKEN}` },
12
+ next: { revalidate: 300 }
13
+ });
14
+
15
+ if (!linksRes.ok) {
16
+ return new Response(JSON.stringify({ error: 'Failed to fetch links' }), { status: 500 });
17
+ }
18
+
19
+ const links = await linksRes.json();
20
+ const activeLinks = links
21
+ .filter(l => l.status === 'success' && l.has_revalidation === true)
22
+ .slice(0, MAX_DOCS_TO_SCAN);
23
+
24
+ // Tally per-annotator stats
25
+ const stats = {}; // annotator -> { verified, correct, incorrect, docs, humanAdded }
26
+
27
+ const results = await Promise.allSettled(
28
+ activeLinks.map(async (link) => {
29
+ const docUrl = `${HF_DATASET_BASE_URL}/raw/main/annotation_data/wbg_extractions/doc_${link.index}/raw/doc_${link.index}_direct_judged.jsonl`;
30
+ const docRes = await fetch(docUrl, {
31
+ headers: { 'Authorization': `Bearer ${process.env.HF_TOKEN}` }
32
+ });
33
+ if (!docRes.ok) return;
34
+
35
+ const pagesData = await docRes.json();
36
+ const docAnnotators = new Set();
37
+
38
+ for (const page of pagesData) {
39
+ for (const ds of (page.datasets || [])) {
40
+ // Count human-added annotations
41
+ if (ds.source === 'human' && ds.annotator) {
42
+ if (!stats[ds.annotator]) {
43
+ stats[ds.annotator] = { verified: 0, correct: 0, incorrect: 0, docs: new Set(), humanAdded: 0 };
44
+ }
45
+ stats[ds.annotator].humanAdded++;
46
+ stats[ds.annotator].docs.add(link.index);
47
+ }
48
+
49
+ // Count validations
50
+ for (const v of (ds.validations || [])) {
51
+ if (!v.annotator || !v.human_validated) continue;
52
+ if (!stats[v.annotator]) {
53
+ stats[v.annotator] = { verified: 0, correct: 0, incorrect: 0, docs: new Set(), humanAdded: 0 };
54
+ }
55
+ stats[v.annotator].verified++;
56
+ if (v.human_verdict === true) stats[v.annotator].correct++;
57
+ else stats[v.annotator].incorrect++;
58
+ stats[v.annotator].docs.add(link.index);
59
+ }
60
+ }
61
+ }
62
+ })
63
+ );
64
+
65
+ // Build ranked list
66
+ const leaderboard = Object.entries(stats)
67
+ .map(([annotator, s]) => ({
68
+ annotator,
69
+ verified: s.verified,
70
+ correct: s.correct,
71
+ incorrect: s.incorrect,
72
+ humanAdded: s.humanAdded,
73
+ docsWorked: s.docs.size,
74
+ score: s.verified + s.humanAdded, // total contributions
75
+ }))
76
+ .sort((a, b) => b.score - a.score);
77
+
78
+ return new Response(JSON.stringify({ leaderboard }), {
79
+ status: 200,
80
+ headers: {
81
+ 'Content-Type': 'application/json',
82
+ 'Cache-Control': 'public, s-maxage=120, stale-while-revalidate=59'
83
+ }
84
+ });
85
+ } catch (error) {
86
+ console.error('Leaderboard API error:', error);
87
+ return new Response(
88
+ JSON.stringify({ error: 'Failed to compute leaderboard' }),
89
+ { status: 500, headers: { 'Content-Type': 'application/json' } }
90
+ );
91
+ }
92
+ }
app/components/Leaderboard.js ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ const MEDALS = ['πŸ₯‡', 'πŸ₯ˆ', 'πŸ₯‰'];
6
+
7
+ export default function Leaderboard({ isOpen, onClose }) {
8
+ const [data, setData] = useState(null);
9
+ const [loading, setLoading] = useState(false);
10
+
11
+ useEffect(() => {
12
+ if (isOpen && !data) {
13
+ setLoading(true);
14
+ fetch('/api/leaderboard')
15
+ .then(res => res.json())
16
+ .then(d => { setData(d); setLoading(false); })
17
+ .catch(() => setLoading(false));
18
+ }
19
+ }, [isOpen, data]);
20
+
21
+ if (!isOpen) return null;
22
+
23
+ return (
24
+ <>
25
+ <div className="panel-backdrop" onClick={onClose} />
26
+ <div className="leaderboard-modal">
27
+ <div className="leaderboard-header">
28
+ <h3>πŸ† Annotation Leaderboard</h3>
29
+ <button className="panel-close" onClick={onClose}>&times;</button>
30
+ </div>
31
+
32
+ <div className="leaderboard-body">
33
+ {loading ? (
34
+ <div className="leaderboard-loading">
35
+ <div className="spinner" />
36
+ <p>Tallying scores...</p>
37
+ </div>
38
+ ) : !data?.leaderboard?.length ? (
39
+ <p className="leaderboard-empty">No annotations yet. Be the first! πŸš€</p>
40
+ ) : (
41
+ <table className="leaderboard-table">
42
+ <thead>
43
+ <tr>
44
+ <th>#</th>
45
+ <th>Annotator</th>
46
+ <th title="Total verifications">βœ… Verified</th>
47
+ <th title="Manually added mentions">✍️ Added</th>
48
+ <th title="Documents worked on">πŸ“„ Docs</th>
49
+ <th title="Total score">⭐ Score</th>
50
+ </tr>
51
+ </thead>
52
+ <tbody>
53
+ {data.leaderboard.map((entry, i) => (
54
+ <tr key={entry.annotator} className={i < 3 ? `rank-${i + 1}` : ''}>
55
+ <td className="rank-cell">
56
+ {i < 3 ? MEDALS[i] : i + 1}
57
+ </td>
58
+ <td className="annotator-cell">
59
+ {entry.annotator}
60
+ </td>
61
+ <td>
62
+ <span className="stat-verified">{entry.verified}</span>
63
+ {entry.incorrect > 0 && (
64
+ <span className="stat-detail"> ({entry.correct}βœ“ {entry.incorrect}βœ•)</span>
65
+ )}
66
+ </td>
67
+ <td>{entry.humanAdded}</td>
68
+ <td>{entry.docsWorked}</td>
69
+ <td className="score-cell">{entry.score}</td>
70
+ </tr>
71
+ ))}
72
+ </tbody>
73
+ </table>
74
+ )}
75
+ </div>
76
+
77
+ <div className="leaderboard-footer">
78
+ <p>Score = Verified + Added β€’ Updates every 2 min</p>
79
+ </div>
80
+ </div>
81
+ </>
82
+ );
83
+ }
app/globals.css CHANGED
@@ -218,6 +218,142 @@ h4 {
218
  background: var(--border-color);
219
  }
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  .pane {
222
  flex: 1;
223
  padding: 20px 24px;
 
218
  background: var(--border-color);
219
  }
220
 
221
+ /* ── Leaderboard ─────────────────────────────── */
222
+
223
+ .btn-leaderboard {
224
+ background: none;
225
+ border: 1px solid var(--border-color);
226
+ color: var(--text-color);
227
+ padding: 3px 10px;
228
+ border-radius: 6px;
229
+ font-size: 0.72rem;
230
+ cursor: pointer;
231
+ transition: all 0.2s;
232
+ }
233
+
234
+ .btn-leaderboard:hover {
235
+ background: var(--surface);
236
+ border-color: var(--accent);
237
+ }
238
+
239
+ .leaderboard-modal {
240
+ position: fixed;
241
+ top: 50%;
242
+ left: 50%;
243
+ transform: translate(-50%, -50%);
244
+ z-index: 1001;
245
+ background: var(--pane-bg);
246
+ border: 1px solid var(--border-color);
247
+ border-radius: 16px;
248
+ width: 480px;
249
+ max-height: 70vh;
250
+ display: flex;
251
+ flex-direction: column;
252
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
253
+ }
254
+
255
+ .leaderboard-header {
256
+ display: flex;
257
+ align-items: center;
258
+ justify-content: space-between;
259
+ padding: 16px 20px;
260
+ border-bottom: 1px solid var(--border-color);
261
+ }
262
+
263
+ .leaderboard-header h3 {
264
+ margin: 0;
265
+ font-size: 1rem;
266
+ }
267
+
268
+ .leaderboard-body {
269
+ padding: 12px 20px;
270
+ overflow-y: auto;
271
+ flex: 1;
272
+ }
273
+
274
+ .leaderboard-loading {
275
+ display: flex;
276
+ flex-direction: column;
277
+ align-items: center;
278
+ gap: 10px;
279
+ padding: 30px;
280
+ color: #94a3b8;
281
+ }
282
+
283
+ .leaderboard-empty {
284
+ text-align: center;
285
+ color: #94a3b8;
286
+ padding: 30px;
287
+ }
288
+
289
+ .leaderboard-table {
290
+ width: 100%;
291
+ border-collapse: collapse;
292
+ font-size: 0.8rem;
293
+ }
294
+
295
+ .leaderboard-table th {
296
+ text-align: left;
297
+ padding: 6px 8px;
298
+ color: #64748b;
299
+ font-weight: 600;
300
+ font-size: 0.72rem;
301
+ border-bottom: 1px solid var(--border-color);
302
+ }
303
+
304
+ .leaderboard-table td {
305
+ padding: 8px 8px;
306
+ border-bottom: 1px solid var(--border-color);
307
+ }
308
+
309
+ .leaderboard-table tr:last-child td {
310
+ border-bottom: none;
311
+ }
312
+
313
+ .rank-cell {
314
+ font-size: 1rem;
315
+ text-align: center;
316
+ width: 32px;
317
+ }
318
+
319
+ .annotator-cell {
320
+ font-weight: 600;
321
+ }
322
+
323
+ .score-cell {
324
+ font-weight: 700;
325
+ color: var(--accent);
326
+ }
327
+
328
+ .stat-verified {
329
+ font-weight: 600;
330
+ }
331
+
332
+ .stat-detail {
333
+ font-size: 0.65rem;
334
+ color: #64748b;
335
+ }
336
+
337
+ .rank-1 {
338
+ background: rgba(255, 215, 0, 0.06);
339
+ }
340
+
341
+ .rank-2 {
342
+ background: rgba(192, 192, 192, 0.06);
343
+ }
344
+
345
+ .rank-3 {
346
+ background: rgba(205, 127, 50, 0.06);
347
+ }
348
+
349
+ .leaderboard-footer {
350
+ padding: 8px 20px;
351
+ border-top: 1px solid var(--border-color);
352
+ font-size: 0.65rem;
353
+ color: #64748b;
354
+ text-align: center;
355
+ }
356
+
357
  .pane {
358
  flex: 1;
359
  padding: 20px 24px;
app/page.js CHANGED
@@ -8,6 +8,7 @@ import AnnotationPanel from './components/AnnotationPanel';
8
  import AnnotationModal from './components/AnnotationModal';
9
  import PageNavigator from './components/PageNavigator';
10
  import ProgressBar from './components/ProgressBar';
 
11
 
12
  export default function Home() {
13
  const [documents, setDocuments] = useState([]);
@@ -35,6 +36,9 @@ export default function Home() {
35
  // Toast state
36
  const [toast, setToast] = useState(null);
37
 
 
 
 
38
  const showToast = useCallback((message, type = 'success') => {
39
  setToast({ message, type });
40
  setTimeout(() => setToast(null), 3000);
@@ -441,6 +445,12 @@ export default function Home() {
441
  annotatorName={annotatorName}
442
  />
443
  <div className="top-bar-user">
 
 
 
 
 
 
444
  {annotatorName ? (
445
  <span className="user-badge">πŸ‘€ {annotatorName}</span>
446
  ) : (
@@ -523,6 +533,7 @@ export default function Home() {
523
  </div>
524
  )}
525
  </div>
 
526
  </div>
527
  );
528
  }
 
8
  import AnnotationModal from './components/AnnotationModal';
9
  import PageNavigator from './components/PageNavigator';
10
  import ProgressBar from './components/ProgressBar';
11
+ import Leaderboard from './components/Leaderboard';
12
 
13
  export default function Home() {
14
  const [documents, setDocuments] = useState([]);
 
36
  // Toast state
37
  const [toast, setToast] = useState(null);
38
 
39
+ // Leaderboard state
40
+ const [leaderboardOpen, setLeaderboardOpen] = useState(false);
41
+
42
  const showToast = useCallback((message, type = 'success') => {
43
  setToast({ message, type });
44
  setTimeout(() => setToast(null), 3000);
 
445
  annotatorName={annotatorName}
446
  />
447
  <div className="top-bar-user">
448
+ <button
449
+ className="btn-leaderboard"
450
+ onClick={() => setLeaderboardOpen(true)}
451
+ >
452
+ πŸ† Leaderboard
453
+ </button>
454
  {annotatorName ? (
455
  <span className="user-badge">πŸ‘€ {annotatorName}</span>
456
  ) : (
 
533
  </div>
534
  )}
535
  </div>
536
+ <Leaderboard isOpen={leaderboardOpen} onClose={() => setLeaderboardOpen(false)} />
537
  </div>
538
  );
539
  }