rai-bench / src /components /LeaderboardTable.tsx
rohanjaggi
fix: ui tweaks
ca83a13
Raw
History Blame Contribute Delete
12.6 kB
'use client'
import { useState, useMemo, useCallback } from 'react'
import type { ModelData } from '@/lib/types'
import { fmtPct, fmtNum, lucColor, ragColor, fairnessColor, creatorColor, CREATOR_COLORS } from '@/lib/utils'
import { LabLogo } from '@/components/LabLogo'
type SortKey = 'rank' | 'luc' | 'rag' | 'fairness'
interface Props {
models: ModelData[]
maxFairness: number
}
function ScoreCell({
value,
color,
max = 1,
format,
invert = false,
}: {
value: number | null
color: string
max?: number
format: (v: number | null) => string
invert?: boolean
}) {
const barWidth = value === null ? 0 : invert ? Math.max(0, 1 - value / max) : value / max
return (
<td className="score-cell">
{value === null ? (
<span className="score-null"></span>
) : (
<>
<div className="score-val" style={{ color }}>{format(value)}</div>
<div className="score-bar-track">
<div
className="score-bar-fill"
style={{ width: `${barWidth * 100}%`, background: color }}
/>
</div>
</>
)}
</td>
)
}
function ExpandedRow({ model, maxFairness }: { model: ModelData; maxFairness: number }) {
const lucMetrics = [
{ label: 'General', val: model.luc.general, color: lucColor(model.luc.general) },
{ label: 'Physics misuse', val: model.luc.physics, color: lucColor(model.luc.physics) },
{ label: 'Career scams', val: model.luc.career, color: lucColor(model.luc.career) },
{ label: 'Job discrimination', val: model.luc.jd, color: lucColor(model.luc.jd) },
]
const ragMetrics = [
{ label: 'Long In-Context Abstractive', val: model.rag.lcAbs, color: ragColor(model.rag.lcAbs) },
{ label: 'Long In-Context Factual', val: model.rag.lcFact, color: ragColor(model.rag.lcFact) },
{ label: 'HyDE RAG Abstractive', val: model.rag.hyAbs, color: ragColor(model.rag.hyAbs) },
{ label: 'HyDE RAG Factual', val: model.rag.hyFact, color: ragColor(model.rag.hyFact) },
]
const fairMetrics = [
{ label: 'Style disparity', val: model.fairness.style, invert: true },
{ label: 'Content disparity', val: model.fairness.con, invert: true },
]
return (
<tr className="lb-expand-row">
<td colSpan={6}>
<div className="lb-expand-inner">
{/* LUC */}
<div>
<div className="expand-group-title">Localised Undesired Content</div>
{lucMetrics.map(m => (
<div className="expand-metric" key={m.label}>
<div className="expand-metric-label">{m.label}</div>
<div className="expand-metric-row">
<span className="expand-metric-val" style={{ color: m.color }}>
{fmtPct(m.val)}
</span>
<div className="mini-bar-track">
<div
className="mini-bar-fill"
style={{
width: m.val !== null ? `${m.val * 100}%` : '0%',
background: m.color,
}}
/>
</div>
</div>
</div>
))}
</div>
{/* RAG */}
<div>
<div className="expand-group-title">RAG Robustness</div>
{ragMetrics.map(m => (
<div className="expand-metric" key={m.label}>
<div className="expand-metric-label">{m.label}</div>
<div className="expand-metric-row">
<span className="expand-metric-val" style={{ color: m.color }}>
{fmtPct(m.val)}
</span>
<div className="mini-bar-track">
<div
className="mini-bar-fill"
style={{
width: m.val !== null ? `${m.val * 100}%` : '0%',
background: m.color,
}}
/>
</div>
</div>
</div>
))}
</div>
{/* Fairness */}
<div>
<div className="expand-group-title">Fairness Disparity</div>
{fairMetrics.map(m => {
const fc = fairnessColor(m.val)
const pct = m.val !== null ? Math.min(100, (m.val / maxFairness) * 100) : 0
return (
<div className="expand-metric" key={m.label}>
<div className="expand-metric-label">{m.label}</div>
<div className="expand-metric-row">
<span className="expand-metric-val" style={{ color: fc }}>
{fmtNum(m.val, 4)}
</span>
<div className="mini-bar-track">
<div
className="mini-bar-fill"
style={{
width: `${100 - pct}%`,
background: fc,
}}
/>
</div>
</div>
</div>
)
})}
</div>
</div>
</td>
</tr>
)
}
export default function LeaderboardTable({ models, maxFairness }: Props) {
const [sortKey, setSortKey] = useState<SortKey>('luc')
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc')
const [query, setQuery] = useState('')
const [expandedRank, setExpandedRank] = useState<number | null>(null)
const [activeCreator, setActiveCreator] = useState<string | null>(null)
const creators = useMemo(() => [...new Set(models.map(m => m.creator))].sort(), [models])
const handleSort = useCallback((key: SortKey) => {
if (sortKey === key) {
setSortDir(d => (d === 'asc' ? 'desc' : 'asc'))
} else {
setSortKey(key)
setSortDir(key === 'fairness' ? 'asc' : 'desc')
}
}, [sortKey])
const sortedFiltered = useMemo(() => {
let list = models.filter(m => !m.archived)
if (activeCreator) list = list.filter(m => m.creator === activeCreator)
if (query.trim()) {
const q = query.toLowerCase()
list = list.filter(
m => m.model.toLowerCase().includes(q) || m.creator.toLowerCase().includes(q),
)
}
list.sort((a, b) => {
let va: number | null, vb: number | null
if (sortKey === 'rank') { va = a.rank; vb = b.rank }
else if (sortKey === 'luc') { va = a.luc.avg; vb = b.luc.avg }
else if (sortKey === 'rag') { va = a.rag.avg; vb = b.rag.avg }
else { va = a.fairness.avg; vb = b.fairness.avg }
if (va === null && vb === null) return 0
if (va === null) return 1
if (vb === null) return -1
return sortDir === 'asc' ? va - vb : vb - va
})
return list
}, [models, activeCreator, query, sortKey, sortDir])
function SortArrow({ k }: { k: SortKey }) {
if (sortKey !== k) return <span style={{ color: 'var(--border-2)', marginLeft: 4 }}></span>
return <span style={{ color: 'var(--accent)', marginLeft: 4 }}>{sortDir === 'asc' ? '↑' : '↓'}</span>
}
return (
<div>
{/* Controls */}
<div className="lb-controls">
<div className="lb-search-wrap">
<svg className="lb-search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<circle cx="11" cy="11" r="8" /><path d="m21 21-4.35-4.35" />
</svg>
<input
className="lb-search"
placeholder="Search models..."
value={query}
onChange={e => setQuery(e.target.value)}
/>
</div>
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
{creators.map(c => (
<button
key={c}
className={`filter-chip ${activeCreator === c ? 'active' : ''}`}
style={activeCreator === c ? {
borderColor: creatorColor(c),
color: creatorColor(c),
background: `${creatorColor(c)}12`,
} : {}}
onClick={() => setActiveCreator(prev => prev === c ? null : c)}
>
<span className="filter-chip-logo"><LabLogo creator={c} /></span>
{c}
</button>
))}
</div>
</div>
{/* Table */}
<div className="lb-table-wrap">
<table className="lb-table">
<thead>
<tr>
<th onClick={() => handleSort('rank')} className={sortKey === 'rank' ? 'sort-active' : ''} style={{ width: 48 }}>
# <SortArrow k="rank" />
</th>
<th style={{ width: 130 }}>Creator</th>
<th>Model</th>
<th onClick={() => handleSort('luc')} className={sortKey === 'luc' ? 'sort-active' : ''}>
Refusal Rate <SortArrow k="luc" />
</th>
<th onClick={() => handleSort('rag')} className={sortKey === 'rag' ? 'sort-active' : ''}>
RAG Score <SortArrow k="rag" />
</th>
<th onClick={() => handleSort('fairness')} className={sortKey === 'fairness' ? 'sort-active' : ''}>
Fairness ↓ <SortArrow k="fairness" />
</th>
</tr>
</thead>
<tbody>
{sortedFiltered.map((model, i) => {
const isExpanded = expandedRank === model.rank
const cc = creatorColor(model.creator)
return [
<tr
key={`row-${model.rank}`}
className={isExpanded ? 'row-expanded' : ''}
style={{ background: `${cc}08` }}
>
<td>
<span className={`rank-cell ${i < 3 && !activeCreator && !query ? 'rank-top3' : ''}`}>
{i + 1}
</span>
</td>
<td>
<button
className="creator-badge"
style={{ background: `${cc}18`, color: cc }}
onClick={() => setActiveCreator(prev => prev === model.creator ? null : model.creator)}
>
<span className="creator-badge-logo">
<LabLogo creator={model.creator} />
</span>
{model.creator}
</button>
</td>
<td>
<button
className="model-name"
onClick={() => setExpandedRank(isExpanded ? null : model.rank)}
>
<svg
className={`expand-chevron ${isExpanded ? 'open' : ''}`}
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
>
<polyline points="9 18 15 12 9 6" />
</svg>
{model.model}
</button>
</td>
<ScoreCell
value={model.luc.avg}
color={lucColor(model.luc.avg)}
format={v => fmtPct(v)}
/>
<ScoreCell
value={model.rag.avg}
color={ragColor(model.rag.avg)}
format={v => fmtPct(v)}
/>
<ScoreCell
value={model.fairness.avg}
color={fairnessColor(model.fairness.avg)}
max={maxFairness}
format={v => fmtNum(v, 4)}
invert
/>
</tr>,
isExpanded && (
<ExpandedRow key={`expand-${model.rank}`} model={model} maxFairness={maxFairness} />
),
]
})}
</tbody>
</table>
</div>
<div style={{
marginTop: 12,
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
}}>
<span style={{
fontSize: 12,
color: 'var(--text-3)',
}}>
{sortedFiltered.length} models · ↓ lower fairness score = more equitable
</span>
</div>
</div>
)
}