Spaces:
Running on CPU Upgrade
Running on CPU Upgrade
Add Dots/Bars toggle to 'Where models cluster' card
#2
by leannetanyt - opened
src/components/BarChartSection.tsx
DELETED
|
@@ -1,252 +0,0 @@
|
|
| 1 |
-
'use client'
|
| 2 |
-
|
| 3 |
-
import { useState, useMemo } from 'react'
|
| 4 |
-
import type { ModelData } from '@/lib/types'
|
| 5 |
-
import { fmtPct, fmtNum, creatorColor } from '@/lib/utils'
|
| 6 |
-
import { LabLogo } from '@/components/LabLogo'
|
| 7 |
-
|
| 8 |
-
type Metric = 'luc' | 'rag' | 'fairness'
|
| 9 |
-
|
| 10 |
-
const TABS: { key: Metric; label: string; yLabel: string }[] = [
|
| 11 |
-
{ key: 'luc', label: 'Refusal Rate', yLabel: 'Score (Higher is Better)' },
|
| 12 |
-
{ key: 'rag', label: 'RAG Score', yLabel: 'Score (Higher is Better)' },
|
| 13 |
-
{ key: 'fairness', label: 'Fairness', yLabel: 'Wasserstein Distance (Lower is Better)' },
|
| 14 |
-
]
|
| 15 |
-
|
| 16 |
-
function getValue(m: ModelData, metric: Metric): number | null {
|
| 17 |
-
if (metric === 'luc') return m.luc.avg
|
| 18 |
-
if (metric === 'rag') return m.rag.avg
|
| 19 |
-
return m.fairness.avg
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
-
// SVG layout (coordinate units)
|
| 23 |
-
const ML = 80 // left margin
|
| 24 |
-
const MR = 80 // right margin
|
| 25 |
-
const MT = 14 // top margin
|
| 26 |
-
const BAR_H = 300 // bar area height
|
| 27 |
-
const LBL_H = 150 // rotated label area
|
| 28 |
-
const VW = 1100
|
| 29 |
-
const VH = MT + BAR_H + LBL_H
|
| 30 |
-
const CHART_W = VW - ML - MR
|
| 31 |
-
const GAP = 4
|
| 32 |
-
|
| 33 |
-
const GRID = [0, 0.25, 0.5, 0.75, 1.0]
|
| 34 |
-
|
| 35 |
-
export default function BarChartSection({
|
| 36 |
-
models,
|
| 37 |
-
maxFairness,
|
| 38 |
-
}: {
|
| 39 |
-
models: ModelData[]
|
| 40 |
-
maxFairness: number
|
| 41 |
-
}) {
|
| 42 |
-
const [metric, setMetric] = useState<Metric>('luc')
|
| 43 |
-
const [activeCreators, setCreators] = useState<Set<string>>(new Set())
|
| 44 |
-
|
| 45 |
-
const allCreators = useMemo(
|
| 46 |
-
() => [...new Set(models.filter(m => !m.archived).map(m => m.creator))].sort(),
|
| 47 |
-
[models],
|
| 48 |
-
)
|
| 49 |
-
|
| 50 |
-
const toggleCreator = (c: string) =>
|
| 51 |
-
setCreators(prev => { const s = new Set(prev); s.has(c) ? s.delete(c) : s.add(c); return s })
|
| 52 |
-
|
| 53 |
-
const sorted = useMemo(() => {
|
| 54 |
-
let list = models.filter(m => !m.archived)
|
| 55 |
-
if (activeCreators.size) list = list.filter(m => activeCreators.has(m.creator))
|
| 56 |
-
list = list.filter(m => getValue(m, metric) !== null)
|
| 57 |
-
return [...list].sort((a, b) => {
|
| 58 |
-
const va = getValue(a, metric)!
|
| 59 |
-
const vb = getValue(b, metric)!
|
| 60 |
-
return metric === 'fairness' ? va - vb : vb - va
|
| 61 |
-
})
|
| 62 |
-
}, [models, metric, activeCreators])
|
| 63 |
-
|
| 64 |
-
const maxVal = metric === 'fairness' ? maxFairness : 1
|
| 65 |
-
const n = sorted.length
|
| 66 |
-
const bw = (CHART_W - (n - 1) * GAP) / n
|
| 67 |
-
|
| 68 |
-
const legendCreators = useMemo(
|
| 69 |
-
() => [...new Set(sorted.map(m => m.creator))].sort(),
|
| 70 |
-
[sorted],
|
| 71 |
-
)
|
| 72 |
-
|
| 73 |
-
const tab = TABS.find(t => t.key === metric)!
|
| 74 |
-
|
| 75 |
-
return (
|
| 76 |
-
<div>
|
| 77 |
-
{/* Filters row */}
|
| 78 |
-
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.75rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
|
| 79 |
-
{/* Creator chips */}
|
| 80 |
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
|
| 81 |
-
{allCreators.map(c => {
|
| 82 |
-
const cc = creatorColor(c)
|
| 83 |
-
const on = activeCreators.has(c)
|
| 84 |
-
return (
|
| 85 |
-
<button key={c} onClick={() => toggleCreator(c)} style={{
|
| 86 |
-
height: 26, padding: '0 10px',
|
| 87 |
-
border: `1.5px solid ${on ? cc : 'var(--border-1)'}`,
|
| 88 |
-
borderRadius: 5, fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
|
| 89 |
-
color: on ? cc : 'var(--text-2)',
|
| 90 |
-
background: on ? `${cc}18` : 'var(--bg-0)',
|
| 91 |
-
cursor: 'pointer',
|
| 92 |
-
transition: 'all 0.15s',
|
| 93 |
-
display: 'inline-flex', alignItems: 'center', gap: 5,
|
| 94 |
-
}}>
|
| 95 |
-
<span style={{ display: 'inline-flex', alignItems: 'center', lineHeight: 0 }}>
|
| 96 |
-
<LabLogo creator={c} size={14} />
|
| 97 |
-
</span>
|
| 98 |
-
{c}
|
| 99 |
-
</button>
|
| 100 |
-
)
|
| 101 |
-
})}
|
| 102 |
-
</div>
|
| 103 |
-
|
| 104 |
-
{/* Metric tabs */}
|
| 105 |
-
<div style={{ display: 'inline-flex', border: '1.5px solid var(--border-1)', borderRadius: 7, overflow: 'hidden', flexShrink: 0 }}>
|
| 106 |
-
{TABS.map((t, i) => (
|
| 107 |
-
<button key={t.key} onClick={() => setMetric(t.key)} style={{
|
| 108 |
-
height: 28, padding: '0 13px',
|
| 109 |
-
borderLeft: i > 0 ? '1px solid var(--border-1)' : 'none',
|
| 110 |
-
outline: 'none', border: i > 0 ? '1px solid var(--border-1)' : 'none',
|
| 111 |
-
borderTop: 'none', borderRight: 'none', borderBottom: 'none',
|
| 112 |
-
fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
|
| 113 |
-
letterSpacing: '0.05em', textTransform: 'uppercase' as const,
|
| 114 |
-
color: metric === t.key ? 'white' : 'var(--text-2)',
|
| 115 |
-
background: metric === t.key ? 'var(--accent)' : 'transparent',
|
| 116 |
-
cursor: 'pointer', whiteSpace: 'nowrap' as const,
|
| 117 |
-
transition: 'background 0.12s, color 0.12s',
|
| 118 |
-
}}>
|
| 119 |
-
{t.label}
|
| 120 |
-
</button>
|
| 121 |
-
))}
|
| 122 |
-
</div>
|
| 123 |
-
</div>
|
| 124 |
-
|
| 125 |
-
{/* Chart container */}
|
| 126 |
-
<div style={{
|
| 127 |
-
background: 'var(--bg-1)',
|
| 128 |
-
border: '1.5px solid var(--border-1)',
|
| 129 |
-
borderRadius: 10,
|
| 130 |
-
padding: '1rem 1rem 0',
|
| 131 |
-
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
| 132 |
-
}}>
|
| 133 |
-
<svg
|
| 134 |
-
viewBox={`0 0 ${VW} ${VH}`}
|
| 135 |
-
style={{ width: '100%', height: 'auto', display: 'block' }}
|
| 136 |
-
aria-label={`Bar chart: ${tab.label}`}
|
| 137 |
-
>
|
| 138 |
-
{/* Grid lines + y-axis labels */}
|
| 139 |
-
{GRID.map(lvl => {
|
| 140 |
-
const y = MT + BAR_H - lvl * BAR_H
|
| 141 |
-
const display = metric === 'fairness'
|
| 142 |
-
? (lvl * maxVal).toFixed(2)
|
| 143 |
-
: `${Math.round(lvl * 100)}`
|
| 144 |
-
return (
|
| 145 |
-
<g key={lvl}>
|
| 146 |
-
<line
|
| 147 |
-
x1={ML} y1={y} x2={VW - MR} y2={y}
|
| 148 |
-
stroke={lvl === 0 ? 'var(--border-2)' : 'var(--border-0)'}
|
| 149 |
-
strokeWidth={lvl === 0 ? 1 : 0.8}
|
| 150 |
-
strokeDasharray={lvl === 0 || lvl === 1 ? undefined : '4 4'}
|
| 151 |
-
/>
|
| 152 |
-
<text
|
| 153 |
-
x={ML - 5} y={y + 3.5}
|
| 154 |
-
textAnchor="end"
|
| 155 |
-
fontSize={9}
|
| 156 |
-
fill="var(--text-3)"
|
| 157 |
-
fontFamily="inherit"
|
| 158 |
-
>
|
| 159 |
-
{display}
|
| 160 |
-
</text>
|
| 161 |
-
</g>
|
| 162 |
-
)
|
| 163 |
-
})}
|
| 164 |
-
|
| 165 |
-
{/* Y-axis title */}
|
| 166 |
-
<text
|
| 167 |
-
x={11}
|
| 168 |
-
y={MT + BAR_H / 2}
|
| 169 |
-
textAnchor="middle"
|
| 170 |
-
fontSize={8.5}
|
| 171 |
-
fill="var(--text-3)"
|
| 172 |
-
fontFamily="inherit"
|
| 173 |
-
transform={`rotate(-90, 11, ${MT + BAR_H / 2})`}
|
| 174 |
-
>
|
| 175 |
-
{tab.yLabel}
|
| 176 |
-
</text>
|
| 177 |
-
|
| 178 |
-
{/* Bars + labels */}
|
| 179 |
-
{sorted.map((m, i) => {
|
| 180 |
-
const val = getValue(m, metric)!
|
| 181 |
-
const pct = Math.min(1, val / maxVal)
|
| 182 |
-
const barHeight = pct * BAR_H
|
| 183 |
-
const x = ML + i * (bw + GAP)
|
| 184 |
-
const y = MT + BAR_H - barHeight
|
| 185 |
-
const cc = creatorColor(m.creator)
|
| 186 |
-
const valLabel = metric === 'fairness'
|
| 187 |
-
? val.toFixed(3)
|
| 188 |
-
: `${(val * 100).toFixed(0)}%`
|
| 189 |
-
|
| 190 |
-
return (
|
| 191 |
-
<g key={`${m.rank}-${metric}`}>
|
| 192 |
-
{/* Bar */}
|
| 193 |
-
<rect
|
| 194 |
-
x={x} y={y}
|
| 195 |
-
width={bw} height={barHeight}
|
| 196 |
-
fill={cc} fillOpacity={0.88}
|
| 197 |
-
rx={2}
|
| 198 |
-
/>
|
| 199 |
-
|
| 200 |
-
{/* Value above bar */}
|
| 201 |
-
{barHeight > 18 && (
|
| 202 |
-
<text
|
| 203 |
-
x={x + bw / 2} y={y - 4}
|
| 204 |
-
textAnchor="middle"
|
| 205 |
-
fontSize={8}
|
| 206 |
-
fill={cc}
|
| 207 |
-
fontFamily="inherit"
|
| 208 |
-
fontWeight="bold"
|
| 209 |
-
>
|
| 210 |
-
{valLabel}
|
| 211 |
-
</text>
|
| 212 |
-
)}
|
| 213 |
-
|
| 214 |
-
{/* Model name */}
|
| 215 |
-
<text
|
| 216 |
-
x={x + bw / 2}
|
| 217 |
-
y={MT + BAR_H + 5}
|
| 218 |
-
transform={`rotate(-45, ${x + bw / 2}, ${MT + BAR_H + 5})`}
|
| 219 |
-
textAnchor="end"
|
| 220 |
-
fontSize={10}
|
| 221 |
-
fill="var(--text-1)"
|
| 222 |
-
fontFamily="inherit"
|
| 223 |
-
fontWeight="600"
|
| 224 |
-
>
|
| 225 |
-
{m.model}
|
| 226 |
-
</text>
|
| 227 |
-
</g>
|
| 228 |
-
)
|
| 229 |
-
})}
|
| 230 |
-
</svg>
|
| 231 |
-
|
| 232 |
-
{/* Creator legend */}
|
| 233 |
-
<div style={{
|
| 234 |
-
display: 'flex',
|
| 235 |
-
flexWrap: 'wrap',
|
| 236 |
-
gap: '8px 20px',
|
| 237 |
-
justifyContent: 'center',
|
| 238 |
-
padding: '12px 8px',
|
| 239 |
-
borderTop: '1px solid var(--border-0)',
|
| 240 |
-
marginTop: 4,
|
| 241 |
-
}}>
|
| 242 |
-
{legendCreators.map(c => (
|
| 243 |
-
<div key={c} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-2)', fontWeight: 600 }}>
|
| 244 |
-
<div style={{ width: 11, height: 11, borderRadius: 2, background: creatorColor(c), flexShrink: 0 }} />
|
| 245 |
-
{c}
|
| 246 |
-
</div>
|
| 247 |
-
))}
|
| 248 |
-
</div>
|
| 249 |
-
</div>
|
| 250 |
-
</div>
|
| 251 |
-
)
|
| 252 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/components/ClusterView.tsx
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react'
|
| 4 |
+
import type { ModelData } from '@/lib/types'
|
| 5 |
+
import ScoreDistribution from '@/components/ScoreDistribution'
|
| 6 |
+
import MetricBarAccordion from '@/components/MetricBarAccordion'
|
| 7 |
+
|
| 8 |
+
type View = 'dots' | 'bars'
|
| 9 |
+
|
| 10 |
+
const VIEWS: { key: View; label: string }[] = [
|
| 11 |
+
{ key: 'dots', label: 'Dots' },
|
| 12 |
+
{ key: 'bars', label: 'Bars' },
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
export default function ClusterView({
|
| 16 |
+
models,
|
| 17 |
+
maxFairness,
|
| 18 |
+
}: {
|
| 19 |
+
models: ModelData[]
|
| 20 |
+
maxFairness: number
|
| 21 |
+
}) {
|
| 22 |
+
const [view, setView] = useState<View>('dots')
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div>
|
| 26 |
+
{/* Dots ⇄ Bars toggle */}
|
| 27 |
+
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '1rem' }}>
|
| 28 |
+
<div style={{ display: 'inline-flex', border: '1.5px solid var(--border-1)', borderRadius: 7, overflow: 'hidden' }}>
|
| 29 |
+
{VIEWS.map((v, i) => (
|
| 30 |
+
<button key={v.key} onClick={() => setView(v.key)} style={{
|
| 31 |
+
height: 28, padding: '0 16px',
|
| 32 |
+
border: 'none', borderLeft: i > 0 ? '1px solid var(--border-1)' : 'none',
|
| 33 |
+
outline: 'none',
|
| 34 |
+
fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
|
| 35 |
+
letterSpacing: '0.05em', textTransform: 'uppercase',
|
| 36 |
+
color: view === v.key ? 'white' : 'var(--text-2)',
|
| 37 |
+
background: view === v.key ? 'var(--accent)' : 'transparent',
|
| 38 |
+
cursor: 'pointer', transition: 'background 0.12s, color 0.12s',
|
| 39 |
+
}}>
|
| 40 |
+
{v.label}
|
| 41 |
+
</button>
|
| 42 |
+
))}
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{view === 'dots'
|
| 47 |
+
? <ScoreDistribution models={models} maxFairness={maxFairness} />
|
| 48 |
+
: <MetricBarAccordion models={models} maxFairness={maxFairness} />
|
| 49 |
+
}
|
| 50 |
+
</div>
|
| 51 |
+
)
|
| 52 |
+
}
|
src/components/EvaluationShell.tsx
CHANGED
|
@@ -6,7 +6,7 @@ import LeaderboardTable from '@/components/LeaderboardTable'
|
|
| 6 |
import GuardrailsTable from '@/components/GuardrailsTable'
|
| 7 |
import GuardrailDistribution from '@/components/GuardrailDistribution'
|
| 8 |
import AboutSection from '@/components/AboutSection'
|
| 9 |
-
import
|
| 10 |
import InsightsSection from '@/components/InsightsSection'
|
| 11 |
import ScrollReveal from '@/components/ScrollReveal'
|
| 12 |
import type { ModelData, GuardrailData, MetricThresholds, GuardrailThresholds } from '@/lib/types'
|
|
@@ -63,7 +63,7 @@ export default function EvaluationShell({ models, guardrails, maxFairness, thres
|
|
| 63 |
Each dot is one active model, coloured by lab. The dashed line marks the field average.
|
| 64 |
Fairness is inverted: dots further right are more equitable.
|
| 65 |
</p>
|
| 66 |
-
<
|
| 67 |
</div>
|
| 68 |
</ScrollReveal>
|
| 69 |
</div>
|
|
|
|
| 6 |
import GuardrailsTable from '@/components/GuardrailsTable'
|
| 7 |
import GuardrailDistribution from '@/components/GuardrailDistribution'
|
| 8 |
import AboutSection from '@/components/AboutSection'
|
| 9 |
+
import ClusterView from '@/components/ClusterView'
|
| 10 |
import InsightsSection from '@/components/InsightsSection'
|
| 11 |
import ScrollReveal from '@/components/ScrollReveal'
|
| 12 |
import type { ModelData, GuardrailData, MetricThresholds, GuardrailThresholds } from '@/lib/types'
|
|
|
|
| 63 |
Each dot is one active model, coloured by lab. The dashed line marks the field average.
|
| 64 |
Fairness is inverted: dots further right are more equitable.
|
| 65 |
</p>
|
| 66 |
+
<ClusterView models={models} maxFairness={maxFairness} />
|
| 67 |
</div>
|
| 68 |
</ScrollReveal>
|
| 69 |
</div>
|
src/components/MetricBarAccordion.tsx
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useState } from 'react'
|
| 4 |
+
import type { ModelData } from '@/lib/types'
|
| 5 |
+
import MetricBars from '@/components/MetricBars'
|
| 6 |
+
|
| 7 |
+
type Metric = 'luc' | 'rag' | 'fairness'
|
| 8 |
+
|
| 9 |
+
const ROWS: { key: Metric; label: string }[] = [
|
| 10 |
+
{ key: 'luc', label: 'Refusal Rate' },
|
| 11 |
+
{ key: 'rag', label: 'RAG Score' },
|
| 12 |
+
{ key: 'fairness', label: 'Fairness' },
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
export default function MetricBarAccordion({
|
| 16 |
+
models,
|
| 17 |
+
maxFairness,
|
| 18 |
+
}: {
|
| 19 |
+
models: ModelData[]
|
| 20 |
+
maxFairness: number
|
| 21 |
+
}) {
|
| 22 |
+
const [open, setOpen] = useState<Metric>('luc')
|
| 23 |
+
|
| 24 |
+
return (
|
| 25 |
+
<div style={{ borderTop: '1px solid var(--border-0)' }}>
|
| 26 |
+
{ROWS.map(row => {
|
| 27 |
+
const isOpen = open === row.key
|
| 28 |
+
return (
|
| 29 |
+
<div key={row.key} style={{ borderBottom: '1px solid var(--border-0)' }}>
|
| 30 |
+
<button
|
| 31 |
+
onClick={() => setOpen(row.key)}
|
| 32 |
+
aria-expanded={isOpen}
|
| 33 |
+
style={{
|
| 34 |
+
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
| 35 |
+
padding: '14px 4px', background: 'transparent',
|
| 36 |
+
border: 'none',
|
| 37 |
+
cursor: 'pointer', fontFamily: 'inherit',
|
| 38 |
+
fontSize: 13, fontWeight: 700, color: 'var(--text-1)',
|
| 39 |
+
letterSpacing: '0.03em', textTransform: 'uppercase',
|
| 40 |
+
}}
|
| 41 |
+
>
|
| 42 |
+
{row.label}
|
| 43 |
+
<span style={{
|
| 44 |
+
display: 'inline-block', fontSize: 11, color: 'var(--text-3)',
|
| 45 |
+
transform: isOpen ? 'rotate(90deg)' : 'rotate(0deg)',
|
| 46 |
+
transition: 'transform 0.15s',
|
| 47 |
+
}}>
|
| 48 |
+
▸
|
| 49 |
+
</span>
|
| 50 |
+
</button>
|
| 51 |
+
{isOpen && (
|
| 52 |
+
<div style={{ padding: '4px 0 20px' }}>
|
| 53 |
+
<MetricBars models={models} maxFairness={maxFairness} metric={row.key} />
|
| 54 |
+
</div>
|
| 55 |
+
)}
|
| 56 |
+
</div>
|
| 57 |
+
)
|
| 58 |
+
})}
|
| 59 |
+
</div>
|
| 60 |
+
)
|
| 61 |
+
}
|
src/components/MetricBars.tsx
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use client'
|
| 2 |
+
|
| 3 |
+
import { useMemo, useState } from 'react'
|
| 4 |
+
import type { ModelData } from '@/lib/types'
|
| 5 |
+
import { creatorColor } from '@/lib/utils'
|
| 6 |
+
import { LabLogo } from '@/components/LabLogo'
|
| 7 |
+
|
| 8 |
+
type Metric = 'luc' | 'rag' | 'fairness'
|
| 9 |
+
|
| 10 |
+
const Y_LABEL: Record<Metric, string> = {
|
| 11 |
+
luc: 'Score (Higher is Better)',
|
| 12 |
+
rag: 'Score (Higher is Better)',
|
| 13 |
+
fairness: 'Wasserstein Distance (Lower is Better)',
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
function getValue(m: ModelData, metric: Metric): number | null {
|
| 17 |
+
if (metric === 'luc') return m.luc.avg
|
| 18 |
+
if (metric === 'rag') return m.rag.avg
|
| 19 |
+
return m.fairness.avg
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
// SVG layout (coordinate units)
|
| 23 |
+
const ML = 80, MR = 80, MT = 14, BAR_H = 300, LBL_H = 150
|
| 24 |
+
const VW = 1100
|
| 25 |
+
const VH = MT + BAR_H + LBL_H
|
| 26 |
+
const CHART_W = VW - ML - MR
|
| 27 |
+
const GAP = 4
|
| 28 |
+
const GRID = [0, 0.25, 0.5, 0.75, 1.0]
|
| 29 |
+
|
| 30 |
+
export default function MetricBars({
|
| 31 |
+
models,
|
| 32 |
+
maxFairness,
|
| 33 |
+
metric,
|
| 34 |
+
}: {
|
| 35 |
+
models: ModelData[]
|
| 36 |
+
maxFairness: number
|
| 37 |
+
metric: Metric
|
| 38 |
+
}) {
|
| 39 |
+
const [activeCreators, setCreators] = useState<Set<string>>(new Set())
|
| 40 |
+
|
| 41 |
+
const allCreators = useMemo(
|
| 42 |
+
() => [...new Set(models.filter(m => !m.archived).map(m => m.creator))].sort(),
|
| 43 |
+
[models],
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
const toggleCreator = (c: string) =>
|
| 47 |
+
setCreators(prev => { const s = new Set(prev); s.has(c) ? s.delete(c) : s.add(c); return s })
|
| 48 |
+
|
| 49 |
+
const sorted = useMemo(() => {
|
| 50 |
+
let list = models.filter(m => !m.archived)
|
| 51 |
+
if (activeCreators.size) list = list.filter(m => activeCreators.has(m.creator))
|
| 52 |
+
list = list.filter(m => getValue(m, metric) !== null)
|
| 53 |
+
return [...list].sort((a, b) => {
|
| 54 |
+
const va = getValue(a, metric)!
|
| 55 |
+
const vb = getValue(b, metric)!
|
| 56 |
+
return metric === 'fairness' ? va - vb : vb - va
|
| 57 |
+
})
|
| 58 |
+
}, [models, metric, activeCreators])
|
| 59 |
+
|
| 60 |
+
const maxVal = metric === 'fairness' ? maxFairness : 1
|
| 61 |
+
const n = sorted.length
|
| 62 |
+
const bw = n > 0 ? (CHART_W - (n - 1) * GAP) / n : CHART_W
|
| 63 |
+
|
| 64 |
+
const legendCreators = useMemo(
|
| 65 |
+
() => [...new Set(sorted.map(m => m.creator))].sort(),
|
| 66 |
+
[sorted],
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
return (
|
| 70 |
+
<div>
|
| 71 |
+
{/* Lab filter chips */}
|
| 72 |
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5, marginBottom: '1rem' }}>
|
| 73 |
+
{allCreators.map(c => {
|
| 74 |
+
const cc = creatorColor(c)
|
| 75 |
+
const on = activeCreators.has(c)
|
| 76 |
+
return (
|
| 77 |
+
<button key={c} onClick={() => toggleCreator(c)} style={{
|
| 78 |
+
height: 26, padding: '0 10px',
|
| 79 |
+
border: `1.5px solid ${on ? cc : 'var(--border-1)'}`,
|
| 80 |
+
borderRadius: 5, fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
|
| 81 |
+
color: on ? cc : 'var(--text-2)',
|
| 82 |
+
background: on ? `${cc}18` : 'var(--bg-0)',
|
| 83 |
+
cursor: 'pointer', transition: 'all 0.15s',
|
| 84 |
+
display: 'inline-flex', alignItems: 'center', gap: 5,
|
| 85 |
+
}}>
|
| 86 |
+
<span style={{ display: 'inline-flex', alignItems: 'center', lineHeight: 0 }}>
|
| 87 |
+
<LabLogo creator={c} size={14} />
|
| 88 |
+
</span>
|
| 89 |
+
{c}
|
| 90 |
+
</button>
|
| 91 |
+
)
|
| 92 |
+
})}
|
| 93 |
+
</div>
|
| 94 |
+
|
| 95 |
+
{/* Chart container */}
|
| 96 |
+
<div style={{
|
| 97 |
+
background: 'var(--bg-1)',
|
| 98 |
+
border: '1.5px solid var(--border-1)',
|
| 99 |
+
borderRadius: 10,
|
| 100 |
+
padding: '1rem 1rem 0',
|
| 101 |
+
boxShadow: '0 1px 4px rgba(0,0,0,0.05)',
|
| 102 |
+
}}>
|
| 103 |
+
<svg
|
| 104 |
+
viewBox={`0 0 ${VW} ${VH}`}
|
| 105 |
+
style={{ width: '100%', height: 'auto', display: 'block' }}
|
| 106 |
+
aria-label={`Bar chart: ${metric}`}
|
| 107 |
+
>
|
| 108 |
+
{/* Grid lines + y-axis labels */}
|
| 109 |
+
{GRID.map(lvl => {
|
| 110 |
+
const y = MT + BAR_H - lvl * BAR_H
|
| 111 |
+
const display = metric === 'fairness'
|
| 112 |
+
? (lvl * maxVal).toFixed(2)
|
| 113 |
+
: `${Math.round(lvl * 100)}`
|
| 114 |
+
return (
|
| 115 |
+
<g key={lvl}>
|
| 116 |
+
<line
|
| 117 |
+
x1={ML} y1={y} x2={VW - MR} y2={y}
|
| 118 |
+
stroke={lvl === 0 ? 'var(--border-2)' : 'var(--border-0)'}
|
| 119 |
+
strokeWidth={lvl === 0 ? 1 : 0.8}
|
| 120 |
+
strokeDasharray={lvl === 0 || lvl === 1 ? undefined : '4 4'}
|
| 121 |
+
/>
|
| 122 |
+
<text x={ML - 5} y={y + 3.5} textAnchor="end" fontSize={9}
|
| 123 |
+
fill="var(--text-3)" fontFamily="inherit">
|
| 124 |
+
{display}
|
| 125 |
+
</text>
|
| 126 |
+
</g>
|
| 127 |
+
)
|
| 128 |
+
})}
|
| 129 |
+
|
| 130 |
+
{/* Y-axis title */}
|
| 131 |
+
<text
|
| 132 |
+
x={11} y={MT + BAR_H / 2} textAnchor="middle"
|
| 133 |
+
fontSize={8.5} fill="var(--text-3)" fontFamily="inherit"
|
| 134 |
+
transform={`rotate(-90, 11, ${MT + BAR_H / 2})`}
|
| 135 |
+
>
|
| 136 |
+
{Y_LABEL[metric]}
|
| 137 |
+
</text>
|
| 138 |
+
|
| 139 |
+
{/* Bars + labels */}
|
| 140 |
+
{sorted.map((m, i) => {
|
| 141 |
+
const val = getValue(m, metric)!
|
| 142 |
+
const pct = Math.min(1, val / maxVal)
|
| 143 |
+
const barHeight = pct * BAR_H
|
| 144 |
+
const x = ML + i * (bw + GAP)
|
| 145 |
+
const y = MT + BAR_H - barHeight
|
| 146 |
+
const cc = creatorColor(m.creator)
|
| 147 |
+
const valLabel = metric === 'fairness' ? val.toFixed(3) : `${(val * 100).toFixed(0)}%`
|
| 148 |
+
return (
|
| 149 |
+
<g key={`${m.rank}-${metric}`}>
|
| 150 |
+
<rect x={x} y={y} width={bw} height={barHeight} fill={cc} fillOpacity={0.88} rx={2} />
|
| 151 |
+
{barHeight > 18 && (
|
| 152 |
+
<text x={x + bw / 2} y={y - 4} textAnchor="middle" fontSize={8}
|
| 153 |
+
fill={cc} fontFamily="inherit" fontWeight="bold">
|
| 154 |
+
{valLabel}
|
| 155 |
+
</text>
|
| 156 |
+
)}
|
| 157 |
+
<text
|
| 158 |
+
x={x + bw / 2} y={MT + BAR_H + 5}
|
| 159 |
+
transform={`rotate(-45, ${x + bw / 2}, ${MT + BAR_H + 5})`}
|
| 160 |
+
textAnchor="end" fontSize={10} fill="var(--text-1)"
|
| 161 |
+
fontFamily="inherit" fontWeight="600"
|
| 162 |
+
>
|
| 163 |
+
{m.model}
|
| 164 |
+
</text>
|
| 165 |
+
</g>
|
| 166 |
+
)
|
| 167 |
+
})}
|
| 168 |
+
</svg>
|
| 169 |
+
|
| 170 |
+
{/* Creator legend */}
|
| 171 |
+
<div style={{
|
| 172 |
+
display: 'flex', flexWrap: 'wrap', gap: '8px 20px', justifyContent: 'center',
|
| 173 |
+
padding: '12px 8px', borderTop: '1px solid var(--border-0)', marginTop: 4,
|
| 174 |
+
}}>
|
| 175 |
+
{legendCreators.map(c => (
|
| 176 |
+
<div key={c} style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'var(--text-2)', fontWeight: 600 }}>
|
| 177 |
+
<div style={{ width: 11, height: 11, borderRadius: 2, background: creatorColor(c), flexShrink: 0 }} />
|
| 178 |
+
{c}
|
| 179 |
+
</div>
|
| 180 |
+
))}
|
| 181 |
+
</div>
|
| 182 |
+
</div>
|
| 183 |
+
</div>
|
| 184 |
+
)
|
| 185 |
+
}
|