import { config } from './config.mjs';
import { loadBenchmark } from './data.mjs';
// ===== Config =====
const MAX_COMPARE = 999; // Allow selecting all models
// ===== State =====
const state = {
datasets: [],
models: [],
selectedDatasets: new Set(),
providerFilter: "",
search: "",
compare: new Set(),
dark: window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
};
// ===== Utils =====
const $ = (s)=>document.querySelector(s);
const $$ = (s)=>Array.from(document.querySelectorAll(s));
const showToast=(m)=>{const t=$("#toast");$("#toastMsg").textContent=m;t.classList.remove("hidden");setTimeout(()=>t.classList.add("hidden"),2200);};
const fmt=(n,d=2)=>(n==null||Number.isNaN(n))?"—":Number(n).toFixed(d);
const escapeHTML = value => String(value).replace(/[&<>"']/g, char => ({
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
})[char]);
const slug=(s)=>s.toLowerCase().replace(/[^a-z0-9]+/g,'-');
const TEXT_ONLY_EXCLUDED_DATASETS = new Set([
"멀티 모달", "팩트 체크",
"K-Prism (Text, Intact)", "K-Prism (Text, Perturbed)",
"K-Prism (Image, Intact)", "K-Prism (Image, Perturbed)"
]);
function isUnsupportedForTextOnly(model, dataset){
return !model.is_multimodal && TEXT_ONLY_EXCLUDED_DATASETS.has(dataset);
}
let hasDatasetPreference = false;
let hasLoadedData = false;
let isLoading = false;
function savePrefs(){
try {
localStorage.setItem('llm_lb_prefs', JSON.stringify({
selectedDatasets: [...state.selectedDatasets],
providerFilter: state.providerFilter,
search: state.search, dark: state.dark,
compare: [...state.compare],
}));
} catch { /* Browsers may block storage inside embedded Spaces. */ }
}
function loadPrefs(){
try {
const p = JSON.parse(localStorage.getItem('llm_lb_prefs') || '{}');
if (Array.isArray(p.selectedDatasets)) {
state.selectedDatasets = new Set(p.selectedDatasets.filter(x => typeof x === 'string'));
hasDatasetPreference = true;
}
if (typeof p.providerFilter === 'string') state.providerFilter = p.providerFilter;
if (typeof p.search === 'string') state.search = p.search;
if (typeof p.dark === 'boolean') state.dark = p.dark;
if (Array.isArray(p.compare)) state.compare = new Set(p.compare.filter(x => typeof x === 'string'));
} catch { /* Preferences are optional. */ }
}
// ===== Rendering =====
function visibleDatasets(){return state.selectedDatasets.size ? state.datasets.filter(d=>state.selectedDatasets.has(d)) : []; }
function modelRowMean(model, dsList){
// 텍스트 전용 모델이면 지원하지 않는 데이터셋은 평균 계산에서 제외
const effDs = dsList.filter(d => !isUnsupportedForTextOnly(model, d));
const map=Object.fromEntries(model.scores.map(s=>[s.dataset_name,s]));
const vals=effDs.map(d=>map[d]?.score).filter(v=>typeof v==='number');
return (vals.length >= effDs.length && effDs.length>0) ? vals.reduce((a,b)=>a+b,0)/vals.length : null;
}
function renderDatasetFilters(){
const c=$("#datasetFilters"); c.innerHTML='';
state.datasets.forEach(d=>{
const checked=state.selectedDatasets.has(d);
c.insertAdjacentHTML('beforeend', `
`);
});
c.querySelectorAll('input[type="checkbox"]').forEach(cb=>{
cb.addEventListener('change', ()=>{
if(cb.checked) state.selectedDatasets.add(cb.dataset.dataset);
else state.selectedDatasets.delete(cb.dataset.dataset);
if([...c.querySelectorAll('input:checked')].length===0) state.selectedDatasets=new Set();
savePrefs(); renderTable(); updateChart();
});
});
$("#kpiDatasets").textContent = state.datasets.length;
}
function renderProviders(){
const provs=[...new Set(state.models.map(m=>m.provider))].sort();
const sel=$("#providerSelect");
sel.replaceChildren(new Option('All providers', ''));
provs.forEach(provider => sel.add(new Option(provider, provider)));
sel.value = state.providerFilter;
}
function renderHeader(){
const head=$("#tableHeaderRow");
head.querySelectorAll('th[data-ds]').forEach(el=>el.remove());
visibleDatasets().forEach(d=>{
const th=document.createElement('th');
th.dataset.ds=d; th.className='p-3 text-left ds-col ds-head';
th.innerHTML = `
${escapeHTML(d)}
`;
head.appendChild(th);
});
}
function renderTable(){
renderHeader();
const ds=visibleDatasets();
const tb=$("#tableBody"); tb.innerHTML='';
let models=state.models.filter(m=>{
const text=(m.provider+" "+m.name+" "+m.repo).toLowerCase();
const okProvider=!state.providerFilter || m.provider===state.providerFilter;
const okSearch=!state.search || text.includes(state.search.toLowerCase());
return okProvider && okSearch;
}).map(m=>({...m, mean:modelRowMean(m, ds)}))
.sort((a,b)=>(b.mean??-1)-(a.mean??-1));
models.forEach((m, i)=>{
const sMap=Object.fromEntries(m.scores.map(s=>[s.dataset_name, s]));
const id=slug(m.provider+"-"+m.name);
const favOn=state.compare.has(id);
const row=document.createElement('tr');
row.className='border-b'; row.style.borderColor=getComputedStyle(document.documentElement).getPropertyValue('--border');
row.innerHTML = `
${i+1} |
${escapeHTML(m.provider)}
|
${escapeHTML(m.name)}
${escapeHTML(m.repo)}
${m.is_multimodal ? 'Multimodal' : 'Text-only'}
|
${m.mean == null ? '—' : fmt(m.mean,2)}
|
`;
ds.forEach(d=>{
const s=sMap[d]; const mt=s?.metric_type;
// const badge= mt==='llm-as-judge' ? 'metric-badge-judge' : 'metric-badge';
const cell=document.createElement('td');
cell.className='p-3 ds-col'; cell.title='';
// 텍스트 전용 모델이면 지원하지 않는 데이터셋 컬럼은 항상 비움(—)
if (isUnsupportedForTextOnly(m, d)) {
cell.innerHTML = '—';
row.appendChild(cell);
return;
}
{
cell.innerHTML = s ? (
mt==='llm-as-judge' ? `
` :
`
`
) : '—';
}
row.appendChild(cell);
});
tb.appendChild(row);
});
if (!models.length) {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 4 + ds.length;
cell.className = 'p-6 text-center text-slate-500';
cell.textContent = !hasLoadedData ? 'Results are unavailable.'
: state.models.length ? 'No models match your filters.' : 'No results have been published yet.';
row.appendChild(cell);
tb.appendChild(row);
}
$("#kpiModels").textContent = hasLoadedData ? models.length : '—';
$$('button[data-fav]').forEach(b=>{
b.onclick=()=>{
const id=b.dataset.fav;
if(state.compare.has(id)) state.compare.delete(id);
else{
if(state.compare.size>=MAX_COMPARE){ showToast(`You can compare up to ${MAX_COMPARE} models.`); return; }
state.compare.add(id);
}
savePrefs(); renderTable(); updateChart();
};
});
}
// ===== Chart =====
let chart;
const baseColors = [
'#4f46e5', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#14b8a6', '#f43f5e'
];
function updateChart(){
const chartUnavailable = typeof window.Chart !== 'function';
$("#chartUnavailable").classList.toggle('hidden', !chartUnavailable);
$("#scoresChart").classList.toggle('hidden', chartUnavailable);
if (chartUnavailable) return;
const ds=visibleDatasets();
const labels=ds;
const datasets=[];
const chosen=state.models.filter(m=>state.compare.has(slug(m.provider+"-"+m.name)));
chosen.forEach((m, idx)=>{
const map=Object.fromEntries(m.scores.map(s=>[s.dataset_name, s.score]));
const color = baseColors[idx % baseColors.length];
const lineData = labels.map(d => {
if (isUnsupportedForTextOnly(m, d)) return null;
if (d.startsWith("K-Prism")) return null;
return map[d] ?? null;
});
const barData = labels.map(d => {
if (isUnsupportedForTextOnly(m, d)) return null;
if (!d.startsWith("K-Prism")) return null;
return map[d] ?? null;
});
// Add line dataset
datasets.push({
type: 'line',
label: `${m.provider} / ${m.name}`,
data: lineData,
borderColor: color,
backgroundColor: color,
tension: .25, spanGaps: true, borderWidth: 3, pointRadius: 4, pointHoverRadius: 6,
});
// Add bar dataset
datasets.push({
type: 'bar',
label: `${m.provider} / ${m.name} (Bar)`,
data: barData,
borderColor: color,
backgroundColor: color + '80', // Add transparency for bars
borderWidth: 2,
borderRadius: 4,
});
});
const ctx=document.getElementById('scoresChart').getContext('2d');
if(!chart){
chart=new Chart(ctx,{
data:{labels, datasets},
options:{
responsive:true,
maintainAspectRatio:false, // uses #chartWrap height (clamp -> responsive)
plugins:{
legend:{
display:true,
labels:{
boxWidth:18,
usePointStyle:true,
filter: function(item, chart) {
// Hide the bar datasets from the legend to avoid duplicates
return !item.text.includes('(Bar)');
}
}
},
tooltip:{ mode:'index', intersect:false }
},
interaction:{ mode:'index', intersect:false },
scales:{
y:{ beginAtZero:true, max:100, title:{display:true, text:'Score'}, grid:{ drawBorder:false }},
x:{
type: 'category',
offset:true,
grid:{ display:false },
ticks:{ autoSkip:false, maxRotation:45, minRotation:45 }
}
},
layout:{ padding:0 }
}
});
}else{
chart.data.labels=labels;
chart.data.datasets=datasets;
chart.update();
}
}
// ===== Tabs / Theme / Export =====
function bindTabs(){
$$(".tab").forEach(btn=>{
btn.addEventListener('click', ()=>{
const t=btn.dataset.tab;
$$(".tab").forEach(b=>b.classList.remove('tab-active')); btn.classList.add('tab-active');
["leaderboard","about"].forEach(x=>{
const el=document.getElementById(`tab-${x}`); (x===t)?el.classList.remove('hidden'):el.classList.add('hidden');
});
});
});
}
function applyTheme(){
document.documentElement.classList.toggle('dark', state.dark);
$$('[data-action="theme"]').forEach(button => {
button.textContent = state.dark ? '☀️' : '🌙';
});
}
function exportCSV(){
const ds=visibleDatasets();
const headers=['Rank','Provider','Model','Repo','Mean',...ds];
const rows=[];
let models=state.models.map(m=>({...m, mean:modelRowMean(m, ds)})).sort((a,b)=>(b.mean??-1)-(a.mean??-1));
models=models.filter(m=>{
const text=(m.provider+" "+m.name+" "+m.repo).toLowerCase();
const okProvider=!state.providerFilter || m.provider===state.providerFilter;
const okSearch=!state.search || text.includes(state.search.toLowerCase());
return okProvider && okSearch;
});
models.forEach((m,i)=>{
const map=Object.fromEntries(m.scores.map(s=>[s.dataset_name, s]));
const row=[i+1, m.provider, m.name, m.repo, fmt(m.mean,2)];
ds.forEach(d=>{
if (isUnsupportedForTextOnly(m, d)) {
row.push('');
return;
}
const s=map[d];
row.push(s ? `${fmt(s.score,2)} (${s.metric_type})` : '');
});
rows.push(row);
});
const csv=[headers, ...rows].map(r=> r.map(x=>'"'+String(x).replaceAll('"','""')+'"').join(',')).join('\n');
const blob=new Blob([csv],{type:'text/csv;charset=utf-8;'}); const url=URL.createObjectURL(blob);
const a=document.createElement('a'); a.href=url; a.download='keti-leaderboard.csv'; a.click(); URL.revokeObjectURL(url);
}
// ===== Controls =====
function bindControls(){
$("#selectAllBtn").onclick=()=>{ state.selectedDatasets=new Set(state.datasets); renderDatasetFilters(); renderTable(); updateChart(); savePrefs(); };
$("#clearAllBtn").onclick=()=>{ state.selectedDatasets=new Set(); renderDatasetFilters(); renderTable(); updateChart(); savePrefs(); };
$("#searchInput").addEventListener('input', (e)=>{ state.search=e.target.value; savePrefs(); renderTable(); });
$("#providerSelect").addEventListener('change', (e)=>{ state.providerFilter=e.target.value; savePrefs(); renderTable(); });
$$('[data-action="refresh"]').forEach(button => button.onclick = refreshResults);
$("#exportCsvBtn").onclick=exportCSV;
$$('[data-action="theme"]').forEach(button => {
button.onclick=()=>{ state.dark=!state.dark; applyTheme(); savePrefs(); };
});
}
async function refreshResults(){
if (isLoading) return;
isLoading = true;
const status = $("#dataStatus");
status.dataset.state = 'loading';
status.textContent = 'Loading published results…';
$$('[data-action="refresh"]').forEach(button => button.disabled = true);
try {
const result = await loadBenchmark(config.dataUrl, config.requestTimeoutMs);
state.datasets = result.datasets;
state.models = result.models;
state.selectedDatasets = hasDatasetPreference
? new Set(state.datasets.filter(dataset => state.selectedDatasets.has(dataset)))
: new Set(state.datasets);
hasDatasetPreference = true;
hasLoadedData = true;
const modelIds = new Set(state.models.map(model => slug(model.provider + '-' + model.name)));
state.compare = new Set([...state.compare].filter(id => modelIds.has(id)));
if (!state.models.some(model => model.provider === state.providerFilter)) state.providerFilter = '';
const timestamps = state.models.map(model => Date.parse(model.updated_at)).filter(Number.isFinite);
$("#lastUpdated").textContent = timestamps.length
? 'Last updated ' + new Date(Math.max(...timestamps)).toLocaleString() : 'Last updated —';
renderDatasetFilters(); renderProviders(); renderTable(); updateChart();
$("#exportCsvBtn").disabled = !state.models.length;
status.dataset.state = 'ready';
status.textContent = `${state.models.length} models · ${state.datasets.length} datasets · Published results`;
savePrefs();
} catch (error) {
console.error('Unable to load published results:', error);
status.dataset.state = 'error';
status.textContent = hasLoadedData
? 'Could not refresh results. Previously loaded results are still shown. Try Refresh again.'
: 'Could not load results. Please check your connection and try Refresh again.';
if (!hasLoadedData) {
renderTable();
$("#kpiDatasets").textContent = '—';
}
} finally {
isLoading = false;
$$('[data-action="refresh"]').forEach(button => button.disabled = false);
}
}
// Bind once: refreshing must not register duplicate listeners.
loadPrefs(); applyTheme(); bindTabs(); bindControls();
$("#searchInput").value = state.search;
refreshResults();