"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, type DeveloperTableSortCol } from "@/components/developer-table" import { InfiniteScrollSentinel } from "@/components/infinite-scroll" import { ModelCompareDialog } from "@/components/model-compare-dialog" import { ModelTable, type ModelTableSortCol } from "@/components/model-table" import { Navigation } from "@/components/navigation" import { PageLoadingState, type PageLoadingStage } from "@/components/page-loading-state" import { ParamRangePicker } from "@/components/param-range-picker" import { fetchCorpusAggregates, fetchDevelopers, fetchModelCards, fetchBenchmarkMetadata, type DeveloperListItem } from "@/lib/dashboard-data-client" import type { BenchmarkCard } from "@/lib/benchmark-schema" import { isOfficialDeveloper } from "@/lib/known-developers" import { PARAM_RANGE_MAX_INDEX, paramStepToNumeric } from "@/lib/param-range" const PAGE_SIZE = 40 const MAX_COMPARE_MODELS = 4 type ModelSort = ModelTableSortCol type DevSort = DeveloperTableSortCol type SortDir = "asc" | "desc" type DevScope = "official" | "community" | "all" // Default sort direction per column when the user first clicks it. Numeric / // recency columns descend (newest, biggest first); name columns ascend. const MODEL_DEFAULT_DIR: Record = { name: "asc", developer: "asc", released: "desc", params: "desc", results: "desc", } const DEV_DEFAULT_DIR: Record = { name: "asc", models: "desc", benchmarks: "desc", results: "desc", } 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([]) const [developers, setDevelopers] = useState([]) const [benchmarkCards, setBenchmarkCards] = useState>({}) const [totalBenchmarksFromHeadline, setTotalBenchmarksFromHeadline] = useState(null) const [modelsStageDone, setModelsStageDone] = useState(false) const [metadataStageDone, setMetadataStageDone] = useState(false) const [headlineStageDone, setHeadlineStageDone] = useState(false) const [loadingModels, setLoadingModels] = useState(true) const [loadingDevelopers, setLoadingDevelopers] = useState(false) const [developersReady, setDevelopersReady] = useState(false) const [groupByDeveloper, setGroupByDeveloper] = useState(false) const [modelSortBy, setModelSortBy] = useState("released") const [modelSortDir, setModelSortDir] = useState("desc") const [developerSortBy, setDeveloperSortBy] = useState("models") const [developerSortDir, setDeveloperSortDir] = useState("desc") const [developerScope, setDeveloperScope] = useState("official") const handleModelSort = useCallback((col: ModelSort) => { setModelSortBy((current) => { if (current === col) { setModelSortDir((dir) => (dir === "asc" ? "desc" : "asc")) return current } setModelSortDir(MODEL_DEFAULT_DIR[col]) return col }) }, []) const handleDeveloperSort = useCallback((col: DevSort) => { setDeveloperSortBy((current) => { if (current === col) { setDeveloperSortDir((dir) => (dir === "asc" ? "desc" : "asc")) return current } setDeveloperSortDir(DEV_DEFAULT_DIR[col]) return col }) }, []) const [searchQuery, setSearchQuery] = useState("") const [selectedModelIds, setSelectedModelIds] = useState([]) 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(() => { const modelCardsRequest = fetchModelCards() .then((cards) => { setEvaluations(cards) setModelsStageDone(true) }) .catch((error) => { console.error("Failed to load evaluations:", error) }) const benchmarkMetadataRequest = fetchBenchmarkMetadata() .then((metadata) => { setBenchmarkCards(metadata) setMetadataStageDone(true) }) .catch((error) => { console.error("Failed to load benchmark metadata:", error) }) const corpusAggregatesRequest = fetchCorpusAggregates() .then((aggregates) => { if (typeof aggregates?.total_benchmarks === "number") { setTotalBenchmarksFromHeadline(aggregates.total_benchmarks) } setHeadlineStageDone(true) }) .catch((error) => { console.error("Failed to load corpus aggregates:", error) setHeadlineStageDone(true) }) Promise.allSettled([modelCardsRequest, benchmarkMetadataRequest, corpusAggregatesRequest]) .finally(() => setLoadingModels(false)) }, []) const totalBenchmarks = useMemo( () => totalBenchmarksFromHeadline ?? Object.keys(benchmarkCards).length, [totalBenchmarksFromHeadline, benchmarkCards], ) 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 loadingStages = useMemo(() => { if (groupByDeveloper && !developersReady) { return [ { label: "Model index", done: modelsStageDone }, { label: "Benchmark metadata", done: metadataStageDone }, { label: "Corpus totals", done: headlineStageDone }, { label: "Developer rollups", done: developersReady }, ] } return [ { label: "Model index", done: modelsStageDone }, { label: "Benchmark metadata", done: metadataStageDone }, { label: "Corpus totals", done: headlineStageDone }, ] }, [developersReady, groupByDeveloper, headlineStageDone, metadataStageDone, modelsStageDone]) // 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)) ) }) } const dirMul = modelSortDir === "asc" ? 1 : -1 const nameOf = (row: BenchmarkEvaluationCardData) => row.model_name ?? "" const developerOf = (row: BenchmarkEvaluationCardData) => row.developer ?? "" return filtered.slice().sort((a, b) => { let cmp = 0 switch (modelSortBy) { case "name": cmp = nameOf(a).localeCompare(nameOf(b)) break case "developer": cmp = developerOf(a).localeCompare(developerOf(b)) break case "released": cmp = safeTimestamp(a.release_date) - safeTimestamp(b.release_date) break case "params": cmp = (a.params_billions ?? -1) - (b.params_billions ?? -1) break case "results": cmp = a.evaluations_count - b.evaluations_count break } // Stable tie-break by model name so equal rows don't shuffle. if (cmp === 0) return nameOf(a).localeCompare(nameOf(b)) return cmp * dirMul }) }, [evaluations, deferredSearchQuery, modelSortBy, modelSortDir, numericMinParams, numericMaxParams, showUnknownSize]) // Developers — filter + sort const sortedDevelopers = useMemo(() => { const query = deferredSearchQuery.trim().toLowerCase() let filtered = developers if (developerScope !== "all") { filtered = filtered.filter((dev) => { const official = isOfficialDeveloper(dev.developer) return developerScope === "official" ? official : !official }) } if (query) { filtered = filtered.filter( (dev) => (dev.developer ?? "").toLowerCase().includes(query) || (dev.popular_evals ?? []).some((ev) => (ev?.benchmark ?? "").toLowerCase().includes(query)), ) } const dirMul = developerSortDir === "asc" ? 1 : -1 return filtered.slice().sort((a, b) => { const devA = a.developer ?? "" const devB = b.developer ?? "" let cmp = 0 switch (developerSortBy) { case "name": cmp = devA.localeCompare(devB) break case "models": cmp = a.model_count - b.model_count break case "benchmarks": cmp = a.benchmark_count - b.benchmark_count break case "results": cmp = a.evaluation_count - b.evaluation_count break } if (cmp === 0) return devA.localeCompare(devB) return cmp * dirMul }) }, [developers, deferredSearchQuery, developerSortBy, developerSortDir, developerScope]) // Reset visible window when filter/sort changes useEffect(() => { setVisibleCount(PAGE_SIZE) }, [groupByDeveloper, modelSortBy, modelSortDir, developerSortBy, developerSortDir, developerScope, 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.evidence_count, 0), [sortedEvaluations], ) return (
{/* HEADER --------------------------------------------------- */}
Index

{groupByDeveloper ? "Model developers" : "Models"}

{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 across{" "} {totalBenchmarks.toLocaleString()} {" "} benchmarks reported on by the developer (or a third party). )}

{/* META + FILTER BAR — single row -------------------------- */}
{/* Inline meta — mono kicker stat strip */}
{totalCount.toLocaleString()} {groupByDeveloper ? "developers" : "models"} {!groupByDeveloper && ( · {totalResults.toLocaleString()} results )} {!groupByDeveloper && ( · {selectedModels.length}/{MAX_COMPARE_MODELS} selected to compare )}
{groupByDeveloper && (
)}
setSearchQuery(event.target.value)} placeholder={ groupByDeveloper ? "Search developers…" : "Search model or developer…" } />
{/* PARAM RANGE — its own row so the rail has room to breathe. Sharing the toolbar with stats / search / sort squished it. */} {!groupByDeveloper && (
{ setMinParamStep(0) setMaxParamStep(PARAM_RANGE_MAX_INDEX) }} showUnknownSize={showUnknownSize} onShowUnknownSizeChange={setShowUnknownSize} />
)} {/* TABLE ---------------------------------------------------- */} {loading ? ( ) : totalCount === 0 ? (

{groupByDeveloper ? "No developers found matching your filters." : "No models found matching your filters."}

) : groupByDeveloper ? ( ) : ( )} {/* COMPARE TRAY (sticky bottom) ---------------------------- */} {!groupByDeveloper && selectedModels.length > 0 && (
Models selected to compare
{selectedModels.map((model) => ( {model.model_name} ))}
)}
) }