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' ? `
${fmt(s.score,2)}
${mt}
` : `
${fmt(s.score,2)}
` ) : '—'; } 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();