evijit's picture
evijit HF Staff
Tighten eval cards UI and clean up stale local data
32864b0
Raw
History Blame
17.2 kB
"use client"
import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "react"
import { ArrowRightLeft, Search, X } from "lucide-react"
import { type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
import { DeveloperTable } from "@/components/developer-table"
import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
import { ModelCompareDialog } from "@/components/model-compare-dialog"
import { ModelTable } from "@/components/model-table"
import { Navigation } from "@/components/navigation"
import { ParamRangePicker } from "@/components/param-range-picker"
import { fetchDevelopers, fetchModelCards, fetchBenchmarkMetadata, type DeveloperListItem } from "@/lib/dashboard-data-client"
import type { BenchmarkCard } from "@/lib/benchmark-schema"
import { PARAM_RANGE_MAX_INDEX, paramStepToNumeric } from "@/lib/param-range"
const PAGE_SIZE = 40
const MAX_COMPARE_MODELS = 4
type ModelSort = "benchmarks" | "results" | "name" | "released" | "params"
type DevSort = "coverage" | "evaluated" | "models" | "name"
function safeTimestamp(value: string | null | undefined) {
if (!value) return 0
const numeric = Number(value)
if (!Number.isNaN(numeric) && !value.includes("-")) {
return numeric > 1_000_000_000_000 ? numeric : numeric * 1000
}
const parsed = new Date(value).getTime()
return Number.isNaN(parsed) ? 0 : parsed
}
export default function ModelsPage() {
const [evaluations, setEvaluations] = useState<BenchmarkEvaluationCardData[]>([])
const [developers, setDevelopers] = useState<DeveloperListItem[]>([])
const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
const [loadingModels, setLoadingModels] = useState(true)
const [loadingDevelopers, setLoadingDevelopers] = useState(false)
const [developersReady, setDevelopersReady] = useState(false)
const [groupByDeveloper, setGroupByDeveloper] = useState(false)
const [modelSortBy, setModelSortBy] = useState<ModelSort>("benchmarks")
const [developerSortBy, setDeveloperSortBy] = useState<DevSort>("coverage")
const [searchQuery, setSearchQuery] = useState("")
const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
const [compareOpen, setCompareOpen] = useState(false)
const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
const [minParamStep, setMinParamStep] = useState(0)
const [maxParamStep, setMaxParamStep] = useState(PARAM_RANGE_MAX_INDEX)
const [showUnknownSize, setShowUnknownSize] = useState(true)
const deferredSearchQuery = useDeferredValue(searchQuery)
const numericMinParams = useMemo(() => paramStepToNumeric(minParamStep, "min"), [minParamStep])
const numericMaxParams = useMemo(() => paramStepToNumeric(maxParamStep, "max"), [maxParamStep])
useEffect(() => {
Promise.all([fetchModelCards(), fetchBenchmarkMetadata()])
.then(([cards, metadata]) => {
setEvaluations(cards)
setBenchmarkCards(metadata)
})
.catch((error) => {
console.error("Failed to load evaluations:", error)
})
.finally(() => setLoadingModels(false))
}, [])
useEffect(() => {
if (!groupByDeveloper || developersReady || loadingDevelopers) return
setLoadingDevelopers(true)
fetchDevelopers()
.then(setDevelopers)
.catch((error) => {
console.error("Failed to load developers:", error)
})
.finally(() => {
setLoadingDevelopers(false)
setDevelopersReady(true)
})
}, [developersReady, groupByDeveloper, loadingDevelopers])
const totalBenchmarks = useMemo(() => Object.keys(benchmarkCards).length, [benchmarkCards])
// Models — filter + sort
const sortedEvaluations = useMemo(() => {
const query = deferredSearchQuery.trim().toLowerCase()
let filtered = evaluations
filtered = filtered.filter((row) => {
if (row.params_billions == null) return showUnknownSize
if (numericMinParams != null && row.params_billions < numericMinParams) return false
if (numericMaxParams != null && row.params_billions > numericMaxParams) return false
return true
})
if (query) {
filtered = filtered.filter((row) => {
return (
row.model_name.toLowerCase().includes(query) ||
row.canonical_model_name.toLowerCase().includes(query) ||
row.developer.toLowerCase().includes(query) ||
(row.benchmark_names ?? []).some((b) => b.toLowerCase().includes(query))
)
})
}
return filtered.slice().sort((a, b) => {
switch (modelSortBy) {
case "name":
return a.model_name.localeCompare(b.model_name)
case "released":
return safeTimestamp(b.release_date ?? b.latest_timestamp) - safeTimestamp(a.release_date ?? a.latest_timestamp)
case "params":
return (b.params_billions ?? 0) - (a.params_billions ?? 0)
case "results":
return b.evaluations_count - a.evaluations_count
case "benchmarks":
default:
return b.benchmarks_count - a.benchmarks_count
}
})
}, [evaluations, deferredSearchQuery, modelSortBy, numericMinParams, numericMaxParams, showUnknownSize])
// Developers — filter + sort
const sortedDevelopers = useMemo(() => {
const query = deferredSearchQuery.trim().toLowerCase()
let filtered = developers
if (query) {
filtered = filtered.filter(
(dev) =>
dev.developer.toLowerCase().includes(query) ||
dev.popular_evals.some((ev) => ev.benchmark.toLowerCase().includes(query)),
)
}
return filtered.slice().sort((a, b) => {
switch (developerSortBy) {
case "evaluated":
return b.evaluation_count - a.evaluation_count
case "models":
return b.model_count - a.model_count
case "name":
return a.developer.localeCompare(b.developer)
case "coverage":
default:
return b.benchmark_count - a.benchmark_count
}
})
}, [developers, deferredSearchQuery, developerSortBy])
// Reset visible window when filter/sort changes
useEffect(() => {
setVisibleCount(PAGE_SIZE)
}, [groupByDeveloper, modelSortBy, developerSortBy, deferredSearchQuery, minParamStep, maxParamStep, showUnknownSize])
const totalCount = groupByDeveloper ? sortedDevelopers.length : sortedEvaluations.length
const visibleEvaluations = useMemo(
() => sortedEvaluations.slice(0, visibleCount),
[sortedEvaluations, visibleCount],
)
const visibleDevelopers = useMemo(
() => sortedDevelopers.slice(0, visibleCount),
[sortedDevelopers, visibleCount],
)
const hasMore = visibleCount < totalCount
const handleLoadMore = useCallback(() => {
setVisibleCount((current) => Math.min(current + PAGE_SIZE, totalCount))
}, [totalCount])
const toggleModelSelection = useCallback((id: string) => {
setSelectedModelIds((current) => {
if (current.includes(id)) return current.filter((existing) => existing !== id)
if (current.length >= MAX_COMPARE_MODELS) return current
return [...current, id]
})
}, [])
const selectedModels = useMemo(
() =>
selectedModelIds
.map((id) => evaluations.find((evaluation) => evaluation.id === id))
.filter((evaluation): evaluation is BenchmarkEvaluationCardData => Boolean(evaluation)),
[evaluations, selectedModelIds],
)
const loading = loadingModels || (groupByDeveloper && !developersReady)
const totalResults = useMemo(
() => sortedEvaluations.reduce((sum, row) => sum + row.evaluations_count, 0),
[sortedEvaluations],
)
return (
<div className="min-h-screen bg-background">
<Navigation />
<main className="mx-auto w-full max-w-[96rem] px-4 pt-12 pb-24 sm:px-8">
{/* HEADER --------------------------------------------------- */}
<div className="kicker">Index</div>
<h1 className="ec-page-h1">{groupByDeveloper ? "Model developers" : "Models"}</h1>
<p className="ec-page-lede">
{groupByDeveloper
? "Every reporting organisation in the corpus and the breadth of evaluation it ships."
: (
<>
Every indexed model and the shape of its published evaluation record. Coverage shows the
share of the{" "}
<span className="font-mono text-[13px]">
{totalBenchmarks.toLocaleString()}-benchmark
</span>{" "}
registry that the developer (or a third party) has reported on.
</>
)}
</p>
{/* META + FILTER BAR — single row -------------------------- */}
<div className="mb-6 flex flex-wrap items-center gap-x-8 gap-y-3 border-y border-[color:var(--border-soft)] py-4">
{/* Inline meta — mono kicker stat strip */}
<div className="flex shrink-0 flex-wrap items-baseline gap-x-4 gap-y-1 font-mono text-[11px] tracking-[0.1em] uppercase text-[color:var(--fg-subtle)]">
<span>
<span className="text-[color:var(--fg)] tabular-nums font-semibold mr-1">
{totalCount.toLocaleString()}
</span>
{groupByDeveloper ? "developers" : "models"}
</span>
{!groupByDeveloper && (
<span>
· <span className="text-[color:var(--fg)] tabular-nums font-semibold mr-1">{totalResults.toLocaleString()}</span>
results
</span>
)}
{!groupByDeveloper && (
<span>
· <span className="text-[color:var(--fg)] tabular-nums font-semibold mr-1">{selectedModels.length}/{MAX_COMPARE_MODELS}</span>
tray
</span>
)}
</div>
<span className="hidden h-5 w-px bg-[color:var(--border-soft)] sm:block" />
<div className="ec-mode-toggle">
<button
type="button"
className={!groupByDeveloper ? "on" : ""}
onClick={() => setGroupByDeveloper(false)}
>
Models
</button>
<button
type="button"
className={groupByDeveloper ? "on" : ""}
onClick={() => setGroupByDeveloper(true)}
>
Developers
</button>
</div>
<div className="relative min-w-[180px] flex-1 sm:max-w-[260px]">
<Search className="pointer-events-none absolute left-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-[color:var(--fg-subtle)]" />
<input
className="ec-input pl-9"
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={
groupByDeveloper ? "Search developers…" : "Search model or developer…"
}
/>
</div>
<select
className="ec-select ml-auto shrink-0"
value={groupByDeveloper ? developerSortBy : modelSortBy}
onChange={(event) => {
const value = event.target.value
if (groupByDeveloper) setDeveloperSortBy(value as DevSort)
else setModelSortBy(value as ModelSort)
}}
>
{groupByDeveloper ? (
<>
<option value="coverage">Sort · Coverage</option>
<option value="evaluated">Sort · Reported results</option>
<option value="models">Sort · Most models</option>
<option value="name">Sort · Name</option>
</>
) : (
<>
<option value="benchmarks">Sort · Benchmarks</option>
<option value="results">Sort · Reported results</option>
<option value="released">Sort · Released</option>
<option value="params">Sort · Parameters</option>
<option value="name">Sort · Name</option>
</>
)}
</select>
</div>
{/* PARAM RANGE — its own row so the rail has room to breathe.
Sharing the toolbar with stats / search / sort squished it. */}
{!groupByDeveloper && (
<div className="mb-6 -mt-2">
<ParamRangePicker
variant="inline"
headline="Params"
minStep={minParamStep}
maxStep={maxParamStep}
onMinChange={setMinParamStep}
onMaxChange={setMaxParamStep}
onReset={() => {
setMinParamStep(0)
setMaxParamStep(PARAM_RANGE_MAX_INDEX)
}}
showUnknownSize={showUnknownSize}
onShowUnknownSizeChange={setShowUnknownSize}
/>
</div>
)}
{/* TABLE ---------------------------------------------------- */}
{loading ? (
<div className="py-24 text-center font-mono text-[11px] uppercase tracking-[0.2em] text-[color:var(--fg-subtle)]">
Loading…
</div>
) : totalCount === 0 ? (
<div className="py-16 text-center border border-dashed border-[color:var(--border-soft)] bg-[color:var(--bg-warm)]">
<p className="mb-4 text-base text-[color:var(--fg-muted)]">
{groupByDeveloper
? "No developers found matching your filters."
: "No models found matching your filters."}
</p>
<button
type="button"
className="btn-ec outline"
onClick={() => {
setSearchQuery("")
setModelSortBy("benchmarks")
setDeveloperSortBy("coverage")
}}
>
Reset filters
</button>
</div>
) : groupByDeveloper ? (
<DeveloperTable rows={visibleDevelopers} />
) : (
<ModelTable
rows={visibleEvaluations}
totalBenchmarks={totalBenchmarks}
selectedIds={selectedModelIds}
onToggleSelect={toggleModelSelection}
maxCompare={MAX_COMPARE_MODELS}
/>
)}
<InfiniteScrollSentinel
hasMore={hasMore}
onLoadMore={handleLoadMore}
loadingLabel="Loading more…"
endLabel={`Showing ${Math.min(visibleCount, totalCount).toLocaleString()} of ${totalCount.toLocaleString()} ${groupByDeveloper ? "developers" : "models"}`}
/>
{/* COMPARE TRAY (sticky bottom) ---------------------------- */}
{!groupByDeveloper && selectedModels.length > 0 && (
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-40 flex justify-center px-4">
<div className="pointer-events-auto w-full max-w-5xl border border-[color:var(--fg)] bg-[color:var(--bg)] p-4 shadow-[var(--shadow-card)]">
<div className="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between">
<div className="space-y-2">
<div className="kicker">Compare tray</div>
<div className="flex flex-wrap gap-2">
{selectedModels.map((model) => (
<span
key={model.id}
className="inline-flex items-center gap-2 border border-[color:var(--border-soft)] bg-[color:var(--bg-warm)] px-3 py-1.5 text-sm"
>
<span className="font-medium text-[color:var(--fg)]">
{model.model_name}
</span>
<button
type="button"
onClick={() => toggleModelSelection(model.id)}
className="text-[color:var(--fg-muted)] transition-colors hover:text-[color:var(--fg)]"
aria-label={`Remove ${model.model_name} from compare`}
>
<X className="h-3.5 w-3.5" />
</button>
</span>
))}
</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
className="btn-ec ghost"
onClick={() => setSelectedModelIds([])}
>
Clear
</button>
<button
type="button"
onClick={() => setCompareOpen(true)}
disabled={selectedModels.length < 2}
className="btn-ec disabled:opacity-50 disabled:cursor-not-allowed"
>
<ArrowRightLeft className="h-3.5 w-3.5" />
Compare {selectedModels.length}
</button>
</div>
</div>
</div>
</div>
)}
</main>
<ModelCompareDialog
models={selectedModels}
open={compareOpen}
onOpenChange={setCompareOpen}
/>
</div>
)
}