rai-bench / src /components /InsightsSection.tsx
rohanjaggi
fix: mobile responsiveness
fa3f2ca
Raw
History Blame Contribute Delete
15.4 kB
'use client'
import { useEffect, useRef, useState, useMemo } from 'react'
import type { ModelData, GuardrailData } from '@/lib/types'
import { creatorColor, inferModelSize } from '@/lib/utils'
import { LabLogo } from '@/components/LabLogo'
import GuardrailBarChart from '@/components/GuardrailBarChart'
const SIZE_COLORS = { Small: '#2BBFB3', Medium: '#E8892B', Large: '#9B72CF' } as const
const SIZE_ORDER = { Small: 0, Medium: 1, Large: 2 } as const
type ModelSize = keyof typeof SIZE_COLORS
function getAvg(m: ModelData, key: 'luc' | 'rag' | 'fairness'): number | null {
if (key === 'luc') return m.luc.avg
if (key === 'rag') return m.rag.avg
return m.fairness.avg
}
function LabSizeBreakdown({ models, maxFairness }: { models: ModelData[]; maxFairness: number }) {
const active = useMemo(() => models.filter(m => !m.archived), [models])
const allCreators = useMemo(() => [...new Set(active.map(m => m.creator))].sort(), [active])
const [creator, setCreator] = useState<string>(() => allCreators[0] ?? '')
const [metric, setMetric] = useState<'luc' | 'rag' | 'fairness'>('luc')
const [hoveredModel, setHoveredModel] = useState<string | null>(null)
const sel = allCreators.includes(creator) ? creator : (allCreators[0] ?? '')
const rows = useMemo(() => {
return active
.filter(m => m.creator === sel && getAvg(m, metric) !== null)
.map(m => ({ m, size: inferModelSize(m.model) as ModelSize }))
.sort((a, b) => {
const sd = SIZE_ORDER[a.size] - SIZE_ORDER[b.size]
if (sd !== 0) return sd
const va = getAvg(a.m, metric)!, vb = getAvg(b.m, metric)!
return metric === 'fairness' ? va - vb : vb - va
})
}, [active, sel, metric])
const groups = useMemo(() =>
(['Small', 'Medium', 'Large'] as ModelSize[])
.map(sz => ({ sz, items: rows.filter(r => r.size === sz) }))
.filter(g => g.items.length > 0),
[rows],
)
const maxVal = metric === 'fairness' ? maxFairness : 1
const fieldAvg = (() => {
const vals = active.filter(m => getAvg(m, metric) !== null).map(m => getAvg(m, metric)!)
return vals.length > 0 ? vals.reduce((s, v) => s + v, 0) / vals.length : null
})()
const METRIC_LABELS: Record<'luc'|'rag'|'fairness', string> = {
luc: 'Refusal Rate', rag: 'RAG Score', fairness: 'Fairness',
}
const fmtVal = (val: number) =>
metric === 'fairness' ? val.toFixed(3) : `${(val * 100).toFixed(0)}%`
const fieldAvgPct = fieldAvg !== null ? Math.min(1, fieldAvg / maxVal) : null
const n = rows.length
const ROW_H = 32
const GROUP_HEADER_H = 28
const GROUP_GAP = 12
return (
<div>
{/* Controls */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '0.75rem', marginBottom: '1rem', flexWrap: 'wrap' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
{allCreators.map(c => {
const on = c === sel
return (
<button key={c} onClick={() => setCreator(c)} style={{
height: 26, padding: '0 10px',
border: `1.5px solid ${on ? creatorColor(c) : 'var(--border-1)'}`,
borderRadius: 5, fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
color: on ? creatorColor(c) : 'var(--text-2)',
background: on ? `${creatorColor(c)}18` : 'var(--bg-0)',
cursor: 'pointer',
transition: 'all 0.15s',
display: 'inline-flex', alignItems: 'center', gap: 5,
}}>
<span style={{ display: 'inline-flex', alignItems: 'center', lineHeight: 0 }}>
<LabLogo creator={c} size={14} />
</span>
{c}
</button>
)
})}
</div>
<div style={{ display: 'inline-flex', border: '1.5px solid var(--border-1)', borderRadius: 7, overflow: 'hidden', flexShrink: 0 }}>
{(['luc', 'rag', 'fairness'] as const).map((m, i) => {
const on = m === metric
return (
<button key={m} onClick={() => setMetric(m)} style={{
height: 28, padding: '0 13px',
borderLeft: i > 0 ? '1px solid var(--border-1)' : 'none',
outline: 'none', border: i > 0 ? '1px solid var(--border-1)' : 'none',
borderTop: 'none', borderRight: 'none', borderBottom: 'none',
fontSize: 10, fontFamily: 'inherit', fontWeight: 700,
letterSpacing: '0.05em', textTransform: 'uppercase' as const,
color: on ? 'white' : 'var(--text-2)',
background: on ? 'var(--accent)' : 'transparent',
cursor: 'pointer', whiteSpace: 'nowrap' as const,
transition: 'background 0.12s, color 0.12s',
}}>
{METRIC_LABELS[m]}
</button>
)
})}
</div>
</div>
{n === 0 ? (
<div style={{
height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'var(--text-3)', fontSize: 13,
border: '1px dashed var(--border-1)', borderRadius: 8,
}}>
No data for {sel}
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{/* Axis scale */}
<div style={{ display: 'flex', alignItems: 'center', marginBottom: 6, paddingLeft: 0 }}>
<div className="insights-chart-name" style={{ width: 170, flexShrink: 0 }} />
<div style={{ flex: 1, position: 'relative', height: 16 }}>
{[0, 0.25, 0.5, 0.75, 1].map(t => (
<span key={t} style={{
position: 'absolute',
left: `${t * 100}%`,
transform: 'translateX(-50%)',
fontSize: 9,
fontFamily: 'inherit',
fontVariantNumeric: 'tabular-nums',
color: 'var(--text-3)',
whiteSpace: 'nowrap',
}}>
{metric === 'fairness' ? (t * maxVal).toFixed(2) : `${Math.round(t * 100)}%`}
</span>
))}
</div>
<div style={{ width: 56, flexShrink: 0 }} />
</div>
{/* Groups */}
{groups.map((g, gi) => (
<div key={g.sz} style={{ marginTop: gi > 0 ? GROUP_GAP : 0 }}>
{/* Group header */}
<div style={{
height: GROUP_HEADER_H,
display: 'flex',
alignItems: 'center',
gap: 8,
marginBottom: 2,
}}>
<div style={{
width: 6, height: 6, borderRadius: '50%',
background: SIZE_COLORS[g.sz],
flexShrink: 0,
}} />
<span style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: '0.08em',
textTransform: 'uppercase' as const,
color: SIZE_COLORS[g.sz],
}}>
{g.sz}
</span>
<span style={{
fontSize: 10,
color: 'var(--text-3)',
fontFamily: 'inherit',
fontVariantNumeric: 'tabular-nums',
}}>
{g.items.length} model{g.items.length !== 1 ? 's' : ''}
</span>
<div style={{ flex: 1, height: 1, background: 'var(--border-0)', marginLeft: 4 }} />
</div>
{/* Rows */}
{g.items.map(r => {
const val = getAvg(r.m, metric)!
const pct = Math.min(1, val / maxVal)
const isHovered = hoveredModel === r.m.model
const cc = SIZE_COLORS[r.size]
return (
<div
key={r.m.model}
onMouseEnter={() => setHoveredModel(r.m.model)}
onMouseLeave={() => setHoveredModel(null)}
style={{
display: 'flex',
alignItems: 'center',
height: ROW_H,
gap: 0,
borderRadius: 4,
background: isHovered ? `${cc}0A` : 'transparent',
transition: 'background 0.12s',
cursor: 'default',
}}
>
{/* Model name */}
<div className="insights-chart-name" style={{
width: 170,
flexShrink: 0,
paddingLeft: 20,
paddingRight: 12,
overflow: 'hidden',
}}>
<span style={{
fontSize: 11.5,
fontWeight: isHovered ? 700 : 500,
color: isHovered ? 'var(--text-0)' : 'var(--text-1)',
whiteSpace: 'nowrap',
transition: 'color 0.12s, font-weight 0.12s',
display: 'block',
overflow: 'hidden',
textOverflow: 'ellipsis',
}}
title={r.m.model}
>
{r.m.model}
</span>
</div>
{/* Bar track */}
<div style={{
flex: 1,
height: 10,
background: 'var(--border-0)',
borderRadius: 3,
overflow: 'visible',
position: 'relative',
}}>
{/* Field average marker */}
{fieldAvgPct !== null && (
<div style={{
position: 'absolute',
left: `${fieldAvgPct * 100}%`,
top: -4,
bottom: -4,
width: 1,
background: 'var(--text-3)',
opacity: 0.5,
zIndex: 1,
}} />
)}
{/* Bar fill */}
<div style={{
width: `${pct * 100}%`,
height: '100%',
background: cc,
borderRadius: 3,
opacity: isHovered ? 1 : 0.78,
transition: 'opacity 0.12s',
position: 'relative',
}} />
</div>
{/* Value */}
<div style={{
width: 56,
flexShrink: 0,
textAlign: 'right',
paddingRight: 4,
paddingLeft: 8,
}}>
<span style={{
fontSize: 11,
fontFamily: 'inherit',
fontVariantNumeric: 'tabular-nums',
fontWeight: 600,
color: isHovered ? cc : 'var(--text-2)',
transition: 'color 0.12s',
}}>
{fmtVal(val)}
</span>
</div>
</div>
)
})}
</div>
))}
{/* Field average legend */}
{fieldAvg !== null && (
<div className="insights-chart-legend" style={{
display: 'flex', alignItems: 'center', gap: 6,
marginTop: 10, paddingLeft: 170,
}}>
<div style={{
width: 16, height: 1,
background: 'var(--text-3)', opacity: 0.5,
}} />
<span style={{
fontSize: 9,
fontFamily: 'inherit',
fontVariantNumeric: 'tabular-nums',
color: 'var(--text-3)',
}}>
field avg {fmtVal(fieldAvg)}
</span>
</div>
)}
</div>
)}
</div>
)
}
interface Props {
models: ModelData[]
guardrails: GuardrailData[]
maxFairness: number
mode: 'models' | 'guardrails'
}
export default function InsightsSection({ models, guardrails, maxFairness, mode }: Props) {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const obs = new IntersectionObserver(
entries => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.classList.add('visible'); obs.unobserve(e.target) }
})
},
{ threshold: 0.05, rootMargin: '0px 0px -60px 0px' },
)
ref.current?.querySelectorAll('.reveal').forEach(el => obs.observe(el))
return () => obs.disconnect()
}, [mode])
return (
<section
id="insights"
ref={ref}
style={{ borderTop: '1px solid var(--border-0)' }}
>
<div className="section-wrap" style={{ paddingTop: '1.5rem', paddingBottom: '1.5rem' }}>
<div className="reveal">
<h2 className="section-title">Data in context</h2>
</div>
{mode === 'guardrails' && (
<div className="insight-card reveal reveal-delay-1" style={{ marginTop: '2rem' }}>
<div className="insight-card-title">Metric Comparison</div>
<div className="insight-card-heading">How guardrails rank across metrics</div>
<GuardrailBarChart guardrails={guardrails} />
</div>
)}
{mode === 'models' && <div className="insight-card reveal reveal-delay-1" style={{ marginTop: '2rem' }}>
<div className="insight-card-title">Lab × Size Breakdown</div>
<div className="insight-card-heading" style={{ marginBottom: '0.75rem' }}>How size affects performance within each lab</div>
<p style={{ fontSize: 15, color: 'var(--text-2)', lineHeight: 1.65, marginBottom: '0.5rem' }}>
Models are grouped by approximate size: <strong style={{ color: SIZE_COLORS.Small }}>Small</strong> (≤&thinsp;10 B params),{' '}
<strong style={{ color: SIZE_COLORS.Medium }}>Medium</strong> (11–40 B), and{' '}
<strong style={{ color: SIZE_COLORS.Large }}>Large</strong> (40 B+).
Commercial closed-weight models are classified by model-family tier
(e.g. Haiku → Small, Sonnet/Flash → Medium, Opus/Pro → Large).
</p>
<p style={{ fontSize: 13, color: 'var(--text-3)', lineHeight: 1.55, marginBottom: '1.25rem', fontStyle: 'italic' }}>
Size classifications are approximate and inherently subjective — exact
parameter counts are not published for most commercial models.
Categorisations are based on publicly available information and
model-family naming conventions at the time of evaluation; they may not
reflect the true underlying model size.
</p>
<LabSizeBreakdown models={models} maxFairness={maxFairness} />
</div>}
</div>
</section>
)
}