Spaces:
Sleeping
Sleeping
| '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> (≤ 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> | |
| ) | |
| } | |