Spaces:
Running
Running
| '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> | |
| ) | |
| } | |