|
|
const rawData = window.SERVER_DATA || []; |
|
|
|
|
|
|
|
|
let state = { |
|
|
championTrack: "Standard", |
|
|
sortKey: "overall_em", |
|
|
sortDir: "desc", |
|
|
filters: { |
|
|
track: "all", |
|
|
agent: "", |
|
|
backbone: "", |
|
|
retriever: "" |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const SCORE_KEYS = ["overall_em", "overall_f1", "intra_em", "intra_f1", "inter_em", "inter_f1"]; |
|
|
|
|
|
|
|
|
function switchMainTab(tabName) { |
|
|
document.querySelectorAll('.nav-btn').forEach(btn => btn.classList.remove('active')); |
|
|
const map = {'leaderboard': 0, 'full-metrics': 1, 'submit': 2}; |
|
|
document.querySelectorAll('.nav-btn')[map[tabName]].classList.add('active'); |
|
|
|
|
|
document.querySelectorAll('.view-section').forEach(el => el.style.display = 'none'); |
|
|
document.getElementById(`view-${tabName}`).style.display = 'block'; |
|
|
|
|
|
if (tabName === 'leaderboard') renderChampionTable(); |
|
|
if (tabName === 'full-metrics') renderFullTable(); |
|
|
} |
|
|
|
|
|
|
|
|
function updateSortHeaders() { |
|
|
document.querySelectorAll('.sortable').forEach(th => { |
|
|
const key = th.dataset.key; |
|
|
|
|
|
let text = th.textContent.replace(/ β/g, '').replace(/ β/g, '').trim(); |
|
|
|
|
|
if (key === state.sortKey) { |
|
|
th.classList.add('active-sort'); |
|
|
text += state.sortDir === 'desc' ? ' β' : ' β'; |
|
|
} else { |
|
|
th.classList.remove('active-sort'); |
|
|
} |
|
|
th.textContent = text; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function renderChampionTable() { |
|
|
const tbody = document.querySelector('#champion-table tbody'); |
|
|
let data = rawData.filter(d => d.track === state.championTrack); |
|
|
|
|
|
|
|
|
const methodBestMap = new Map(); |
|
|
data.forEach(row => { |
|
|
const method = row.method; |
|
|
if (!methodBestMap.has(method) || row.overall_em > methodBestMap.get(method).overall_em) { |
|
|
methodBestMap.set(method, row); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
data = Array.from(methodBestMap.values()); |
|
|
data = sortData(data); |
|
|
|
|
|
tbody.innerHTML = data.map((row, idx) => { |
|
|
return buildRowHTML(row, idx + 1, false); |
|
|
}).join(''); |
|
|
|
|
|
updateSortHeaders(); |
|
|
} |
|
|
|
|
|
|
|
|
function renderFullTable() { |
|
|
const tbody = document.querySelector('#full-table tbody'); |
|
|
|
|
|
let data = rawData.filter(d => { |
|
|
const f = state.filters; |
|
|
if (f.track !== 'all' && d.track !== f.track) return false; |
|
|
if (f.agent && !d.agent.toLowerCase().includes(f.agent.toLowerCase())) return false; |
|
|
if (f.backbone && !d.backbone.toLowerCase().includes(f.backbone.toLowerCase())) return false; |
|
|
if (f.retriever && !d.retriever.toLowerCase().includes(f.retriever.toLowerCase())) return false; |
|
|
return true; |
|
|
}); |
|
|
|
|
|
data = sortData(data); |
|
|
|
|
|
if (data.length === 0) { |
|
|
tbody.innerHTML = `<tr><td colspan="12" style="text-align:center; padding:20px; color:#888;">No matching results.</td></tr>`; |
|
|
return; |
|
|
} |
|
|
|
|
|
tbody.innerHTML = data.map((row, idx) => { |
|
|
return buildRowHTML(row, idx + 1, true); |
|
|
}).join(''); |
|
|
|
|
|
updateSortHeaders(); |
|
|
} |
|
|
|
|
|
|
|
|
function sortData(data) { |
|
|
return data.sort((a, b) => { |
|
|
let valA = a[state.sortKey]; |
|
|
let valB = b[state.sortKey]; |
|
|
if (typeof valA === 'number') { |
|
|
return state.sortDir === 'desc' ? valB - valA : valA - valB; |
|
|
} |
|
|
return 0; |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function buildRowHTML(row, rank, showTrack) { |
|
|
const medal = rank === 1 ? 'π₯' : rank === 2 ? 'π₯' : rank === 3 ? 'π₯' : rank; |
|
|
|
|
|
const trackTd = showTrack |
|
|
? `<td><span class="track-tag ${row.track}">${row.track}</span></td>` |
|
|
: ''; |
|
|
|
|
|
|
|
|
function scoreCell(key, value) { |
|
|
const isActive = (key === state.sortKey) ? ' active-col' : ''; |
|
|
return `<td class="score-cell${isActive}">${value.toFixed(1)}</td>`; |
|
|
} |
|
|
|
|
|
return ` |
|
|
<tr> |
|
|
<td class="rank-col">${medal}</td> |
|
|
<td class="method-col align-left"> |
|
|
<a href="${row.url}" target="_blank" class="method-name">${row.method}</a> |
|
|
<div class="org-name">${row.date}</div> |
|
|
</td> |
|
|
${trackTd} |
|
|
<td>${row.agent}</td> |
|
|
<td>${row.backbone}</td> |
|
|
<td>${row.retriever}</td> |
|
|
${scoreCell('overall_em', row.overall_em)} |
|
|
${scoreCell('overall_f1', row.overall_f1)} |
|
|
${scoreCell('intra_em', row.intra_em)} |
|
|
${scoreCell('intra_f1', row.intra_f1)} |
|
|
${scoreCell('inter_em', row.inter_em)} |
|
|
${scoreCell('inter_f1', row.inter_f1)} |
|
|
</tr> |
|
|
`; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.sub-tab-btn').forEach(btn => { |
|
|
btn.addEventListener('click', (e) => { |
|
|
document.querySelectorAll('.sub-tab-btn').forEach(b => b.classList.remove('active')); |
|
|
e.target.classList.add('active'); |
|
|
state.championTrack = e.target.dataset.track; |
|
|
renderChampionTable(); |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.querySelectorAll('.sortable').forEach(th => { |
|
|
th.addEventListener('click', (e) => { |
|
|
const key = e.currentTarget.dataset.key; |
|
|
if (state.sortKey === key) { |
|
|
state.sortDir = state.sortDir === 'desc' ? 'asc' : 'desc'; |
|
|
} else { |
|
|
state.sortKey = key; |
|
|
state.sortDir = 'desc'; |
|
|
} |
|
|
|
|
|
|
|
|
if (document.getElementById('view-leaderboard').style.display !== 'none') { |
|
|
renderChampionTable(); |
|
|
} else { |
|
|
renderFullTable(); |
|
|
} |
|
|
}); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('filter-track').addEventListener('change', (e) => { |
|
|
state.filters.track = e.target.value; |
|
|
renderFullTable(); |
|
|
}); |
|
|
document.getElementById('filter-agent').addEventListener('input', (e) => { |
|
|
state.filters.agent = e.target.value; |
|
|
renderFullTable(); |
|
|
}); |
|
|
document.getElementById('filter-backbone').addEventListener('input', (e) => { |
|
|
state.filters.backbone = e.target.value; |
|
|
renderFullTable(); |
|
|
}); |
|
|
document.getElementById('filter-retriever').addEventListener('input', (e) => { |
|
|
state.filters.retriever = e.target.value; |
|
|
renderFullTable(); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('submit-form').addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
|
|
|
const fileInput = document.getElementById('submit-file'); |
|
|
const submitBtn = document.getElementById('submit-btn'); |
|
|
const resultDiv = document.getElementById('submit-result'); |
|
|
|
|
|
if (!fileInput.files.length) return; |
|
|
|
|
|
|
|
|
submitBtn.disabled = true; |
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Submitting...'; |
|
|
resultDiv.style.display = 'none'; |
|
|
|
|
|
const formData = new FormData(); |
|
|
formData.append('file', fileInput.files[0]); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/upload', { |
|
|
method: 'POST', |
|
|
body: formData, |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
resultDiv.style.display = 'block'; |
|
|
|
|
|
if (data.success) { |
|
|
let html = `<strong>β
${data.message}</strong>`; |
|
|
if (data.pr_url) { |
|
|
html += `<br><br>π <a href="${data.pr_url}" target="_blank">View your Pull Request β</a>`; |
|
|
} |
|
|
html += `<br><br><small>After maintainers review and merge your PR, scores will be computed and published on the leaderboard.</small>`; |
|
|
resultDiv.className = 'success'; |
|
|
resultDiv.innerHTML = html; |
|
|
} else { |
|
|
let html = `<strong>β ${data.error || 'Submission failed.'}</strong>`; |
|
|
if (data.details) { |
|
|
html += '<ul>' + data.details.map(d => `<li>${d}</li>`).join('') + '</ul>'; |
|
|
} |
|
|
resultDiv.className = 'error'; |
|
|
resultDiv.innerHTML = html; |
|
|
} |
|
|
} catch (err) { |
|
|
resultDiv.style.display = 'block'; |
|
|
resultDiv.className = 'error'; |
|
|
resultDiv.innerHTML = `<strong>β Network error:</strong> ${err.message}`; |
|
|
} finally { |
|
|
submitBtn.disabled = false; |
|
|
submitBtn.innerHTML = '<i class="fas fa-paper-plane"></i> Submit & Create PR'; |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
renderChampionTable(); |