evijit HF Staff Claude Opus 4.7 (1M context) commited on
Commit
5a2d59c
·
1 Parent(s): eec1852

Make /models tables column-sortable; rebalance /evals + /models toolbars

Browse files

* /models, /developers/[id]: replace the 'Sort · X' select with
click-to-sort column headers. Each column toggles direction on
repeat click; switching columns picks a sensible default direction
(asc for name/developer, desc for numeric / recency). Defaults land
on Released ↓ for the model tables and Models ↓ for the developer
table so the newest models surface first.

* /developers (developer table): drop the 'Top evaluations' column
— its chip strip duplicated info that lives one click deeper on the
developer page and was crowding the row.

* /evals: fold the standalone search row into the existing meta row
(Families / Composites / Single benchmarks / Slices / Metrics) with
the input pinned right via ml-auto. Same compaction on /models —
search now sits at ml-auto where the sort dropdown used to be and
widens to a 360px cap so it fills the freed space cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

app/developers/[id]/page.tsx CHANGED
@@ -6,14 +6,34 @@ import { ArrowLeft, Search } from "lucide-react"
6
 
7
  import { type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
8
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
- import { ModelTable } from "@/components/model-table"
10
  import { Navigation } from "@/components/navigation"
11
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
12
  import { fetchDeveloperSummary, fetchBenchmarkMetadata } from "@/lib/dashboard-data-client"
13
 
14
  const PAGE_SIZE = 40
15
 
16
- type ModelSort = "date" | "name" | "benchmarks" | "results"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  export default function DeveloperDetailPage() {
19
  const params = useParams()
@@ -24,7 +44,19 @@ export default function DeveloperDetailPage() {
24
  const [loading, setLoading] = useState(true)
25
  const [error, setError] = useState<string | null>(null)
26
  const [searchQuery, setSearchQuery] = useState("")
27
- const [sortBy, setSortBy] = useState<ModelSort>("date")
 
 
 
 
 
 
 
 
 
 
 
 
28
  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
29
  const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
30
 
@@ -72,31 +104,40 @@ export default function DeveloperDetailPage() {
72
  })
73
  : [...models]
74
 
75
- switch (sortBy) {
76
- case "date":
77
- filtered.sort(
78
- (a, b) =>
79
- new Date(b.latest_timestamp).getTime() -
80
- new Date(a.latest_timestamp).getTime()
81
- )
82
- break
83
- case "name":
84
- filtered.sort((a, b) => a.model_name.localeCompare(b.model_name))
85
- break
86
- case "benchmarks":
87
- filtered.sort((a, b) => b.benchmarks_count - a.benchmarks_count)
88
- break
89
- case "results":
90
- filtered.sort((a, b) => b.evaluations_count - a.evaluations_count)
91
- break
92
- }
 
 
 
 
 
 
 
 
 
93
 
94
  return filtered
95
- }, [models, searchQuery, sortBy])
96
 
97
  useEffect(() => {
98
  setVisibleCount(PAGE_SIZE)
99
- }, [searchQuery, sortBy])
100
 
101
  const visibleModels = useMemo(
102
  () => filteredModels.slice(0, visibleCount),
@@ -202,16 +243,6 @@ export default function DeveloperDetailPage() {
202
  />
203
  </div>
204
 
205
- <select
206
- className="ec-select ml-auto shrink-0"
207
- value={sortBy}
208
- onChange={(event) => setSortBy(event.target.value as ModelSort)}
209
- >
210
- <option value="date">Sort · Latest first</option>
211
- <option value="benchmarks">Sort · Benchmarks</option>
212
- <option value="results">Sort · Reported results</option>
213
- <option value="name">Sort · Name (A–Z)</option>
214
- </select>
215
  </div>
216
 
217
  {/* TABLE ---------------------------------------------------- */}
@@ -226,6 +257,9 @@ export default function DeveloperDetailPage() {
226
  selectedIds={selectedModelIds}
227
  onToggleSelect={toggleModelSelection}
228
  maxCompare={4}
 
 
 
229
  />
230
  )}
231
 
 
6
 
7
  import { type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
8
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
+ import { ModelTable, type ModelTableSortCol } from "@/components/model-table"
10
  import { Navigation } from "@/components/navigation"
11
  import type { BenchmarkCard } from "@/lib/benchmark-schema"
12
  import { fetchDeveloperSummary, fetchBenchmarkMetadata } from "@/lib/dashboard-data-client"
13
 
14
  const PAGE_SIZE = 40
15
 
16
+ type SortDir = "asc" | "desc"
17
+
18
+ const MODEL_DEFAULT_DIR: Record<ModelTableSortCol, SortDir> = {
19
+ name: "asc",
20
+ developer: "asc",
21
+ released: "desc",
22
+ params: "desc",
23
+ benchmarks: "desc",
24
+ results: "desc",
25
+ coverage: "desc",
26
+ }
27
+
28
+ function safeTimestamp(value: string | null | undefined) {
29
+ if (!value) return 0
30
+ const numeric = Number(value)
31
+ if (!Number.isNaN(numeric) && !value.includes("-")) {
32
+ return numeric > 1_000_000_000_000 ? numeric : numeric * 1000
33
+ }
34
+ const parsed = new Date(value).getTime()
35
+ return Number.isNaN(parsed) ? 0 : parsed
36
+ }
37
 
38
  export default function DeveloperDetailPage() {
39
  const params = useParams()
 
44
  const [loading, setLoading] = useState(true)
45
  const [error, setError] = useState<string | null>(null)
46
  const [searchQuery, setSearchQuery] = useState("")
47
+ const [sortBy, setSortBy] = useState<ModelTableSortCol>("released")
48
+ const [sortDir, setSortDir] = useState<SortDir>("desc")
49
+
50
+ const handleSort = useCallback((col: ModelTableSortCol) => {
51
+ setSortBy((current) => {
52
+ if (current === col) {
53
+ setSortDir((dir) => (dir === "asc" ? "desc" : "asc"))
54
+ return current
55
+ }
56
+ setSortDir(MODEL_DEFAULT_DIR[col])
57
+ return col
58
+ })
59
+ }, [])
60
  const [visibleCount, setVisibleCount] = useState(PAGE_SIZE)
61
  const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
62
 
 
104
  })
105
  : [...models]
106
 
107
+ const dirMul = sortDir === "asc" ? 1 : -1
108
+ filtered.sort((a, b) => {
109
+ let cmp = 0
110
+ switch (sortBy) {
111
+ case "name":
112
+ cmp = a.model_name.localeCompare(b.model_name)
113
+ break
114
+ case "developer":
115
+ cmp = a.developer.localeCompare(b.developer)
116
+ break
117
+ case "released":
118
+ cmp = safeTimestamp(a.release_date) - safeTimestamp(b.release_date)
119
+ break
120
+ case "params":
121
+ cmp = (a.params_billions ?? -1) - (b.params_billions ?? -1)
122
+ break
123
+ case "results":
124
+ cmp = a.evaluations_count - b.evaluations_count
125
+ break
126
+ case "coverage":
127
+ case "benchmarks":
128
+ cmp = a.benchmarks_count - b.benchmarks_count
129
+ break
130
+ }
131
+ if (cmp === 0) return a.model_name.localeCompare(b.model_name)
132
+ return cmp * dirMul
133
+ })
134
 
135
  return filtered
136
+ }, [models, searchQuery, sortBy, sortDir])
137
 
138
  useEffect(() => {
139
  setVisibleCount(PAGE_SIZE)
140
+ }, [searchQuery, sortBy, sortDir])
141
 
142
  const visibleModels = useMemo(
143
  () => filteredModels.slice(0, visibleCount),
 
243
  />
244
  </div>
245
 
 
 
 
 
 
 
 
 
 
 
246
  </div>
247
 
248
  {/* TABLE ---------------------------------------------------- */}
 
257
  selectedIds={selectedModelIds}
258
  onToggleSelect={toggleModelSelection}
259
  maxCompare={4}
260
+ sortCol={sortBy}
261
+ sortDir={sortDir}
262
+ onSort={handleSort}
263
  />
264
  )}
265
 
app/evals/page.tsx CHANGED
@@ -204,45 +204,44 @@ function EvalsPageInner() {
204
  metrics.
205
  </p>
206
 
207
- {/* META ROW ------------------------------------------------- */}
208
- {stats && (
209
- <div className="ec-page-meta mb-8">
210
- <div className="ec-page-meta-item">
211
- <span className="ec-page-meta-item-l">Families</span>
212
- <span className="ec-page-meta-item-v">
213
- {stats.family_count.toLocaleString()}
214
- </span>
215
- </div>
216
- <div className="ec-page-meta-item">
217
- <span className="ec-page-meta-item-l">Composites</span>
218
- <span className="ec-page-meta-item-v">
219
- {stats.composite_count.toLocaleString()}
220
- </span>
221
- </div>
222
- <div className="ec-page-meta-item">
223
- <span className="ec-page-meta-item-l">Single benchmarks</span>
224
- <span className="ec-page-meta-item-v">
225
- {(stats.benchmark_count ?? 0).toLocaleString()}
226
- </span>
227
- </div>
228
- <div className="ec-page-meta-item">
229
- <span className="ec-page-meta-item-l">Slices</span>
230
- <span className="ec-page-meta-item-v">
231
- {stats.slice_count.toLocaleString()}
232
- </span>
233
- </div>
234
- <div className="ec-page-meta-item">
235
- <span className="ec-page-meta-item-l">Metrics</span>
236
- <span className="ec-page-meta-item-v">
237
- {stats.metric_count.toLocaleString()}
238
- </span>
239
- </div>
240
- </div>
241
- )}
 
242
 
243
- {/* FILTER BAR ----------------------------------------------- */}
244
- <div className="mb-6 flex flex-wrap items-center gap-3 border-b border-[color:var(--border-soft)] pb-5">
245
- <div className="relative w-full sm:max-w-sm">
246
  <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)]" />
247
  <input
248
  className="ec-input pl-9"
@@ -251,7 +250,6 @@ function EvalsPageInner() {
251
  placeholder="Search family, category…"
252
  />
253
  </div>
254
-
255
  </div>
256
 
257
  {/* CATEGORY PILLS — multi-select filter by curated benchmark
 
204
  metrics.
205
  </p>
206
 
207
+ {/* META + SEARCH ROW --------------------------------------- */}
208
+ <div className="ec-page-meta mb-6 items-center border-b border-[color:var(--border-soft)] pb-5">
209
+ {stats && (
210
+ <>
211
+ <div className="ec-page-meta-item">
212
+ <span className="ec-page-meta-item-l">Families</span>
213
+ <span className="ec-page-meta-item-v">
214
+ {stats.family_count.toLocaleString()}
215
+ </span>
216
+ </div>
217
+ <div className="ec-page-meta-item">
218
+ <span className="ec-page-meta-item-l">Composites</span>
219
+ <span className="ec-page-meta-item-v">
220
+ {stats.composite_count.toLocaleString()}
221
+ </span>
222
+ </div>
223
+ <div className="ec-page-meta-item">
224
+ <span className="ec-page-meta-item-l">Single benchmarks</span>
225
+ <span className="ec-page-meta-item-v">
226
+ {(stats.benchmark_count ?? 0).toLocaleString()}
227
+ </span>
228
+ </div>
229
+ <div className="ec-page-meta-item">
230
+ <span className="ec-page-meta-item-l">Slices</span>
231
+ <span className="ec-page-meta-item-v">
232
+ {stats.slice_count.toLocaleString()}
233
+ </span>
234
+ </div>
235
+ <div className="ec-page-meta-item">
236
+ <span className="ec-page-meta-item-l">Metrics</span>
237
+ <span className="ec-page-meta-item-v">
238
+ {stats.metric_count.toLocaleString()}
239
+ </span>
240
+ </div>
241
+ </>
242
+ )}
243
 
244
+ <div className="relative ml-auto min-w-[180px] flex-1 sm:max-w-[360px]">
 
 
245
  <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)]" />
246
  <input
247
  className="ec-input pl-9"
 
250
  placeholder="Search family, category…"
251
  />
252
  </div>
 
253
  </div>
254
 
255
  {/* CATEGORY PILLS — multi-select filter by curated benchmark
app/models/page.tsx CHANGED
@@ -4,10 +4,10 @@ import { useCallback, useDeferredValue, useEffect, useMemo, useState } from "rea
4
  import { ArrowRightLeft, Search, X } from "lucide-react"
5
 
6
  import { type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
7
- import { DeveloperTable } from "@/components/developer-table"
8
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
  import { ModelCompareDialog } from "@/components/model-compare-dialog"
10
- import { ModelTable } from "@/components/model-table"
11
  import { Navigation } from "@/components/navigation"
12
  import { ParamRangePicker } from "@/components/param-range-picker"
13
  import { fetchCorpusAggregates, fetchDevelopers, fetchModelCards, fetchBenchmarkMetadata, type DeveloperListItem } from "@/lib/dashboard-data-client"
@@ -17,8 +17,27 @@ import { PARAM_RANGE_MAX_INDEX, paramStepToNumeric } from "@/lib/param-range"
17
  const PAGE_SIZE = 40
18
  const MAX_COMPARE_MODELS = 4
19
 
20
- type ModelSort = "benchmarks" | "results" | "name" | "released" | "params"
21
- type DevSort = "coverage" | "evaluated" | "models" | "name"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
  function safeTimestamp(value: string | null | undefined) {
24
  if (!value) return 0
@@ -39,8 +58,32 @@ export default function ModelsPage() {
39
  const [loadingDevelopers, setLoadingDevelopers] = useState(false)
40
  const [developersReady, setDevelopersReady] = useState(false)
41
  const [groupByDeveloper, setGroupByDeveloper] = useState(false)
42
- const [modelSortBy, setModelSortBy] = useState<ModelSort>("benchmarks")
43
- const [developerSortBy, setDeveloperSortBy] = useState<DevSort>("coverage")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  const [searchQuery, setSearchQuery] = useState("")
45
  const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
46
  const [compareOpen, setCompareOpen] = useState(false)
@@ -116,22 +159,35 @@ export default function ModelsPage() {
116
  })
117
  }
118
 
 
119
  return filtered.slice().sort((a, b) => {
 
120
  switch (modelSortBy) {
121
  case "name":
122
- return a.model_name.localeCompare(b.model_name)
 
 
 
 
123
  case "released":
124
- return safeTimestamp(b.release_date) - safeTimestamp(a.release_date)
 
125
  case "params":
126
- return (b.params_billions ?? 0) - (a.params_billions ?? 0)
 
127
  case "results":
128
- return b.evaluations_count - a.evaluations_count
 
 
129
  case "benchmarks":
130
- default:
131
- return b.benchmarks_count - a.benchmarks_count
132
  }
 
 
 
133
  })
134
- }, [evaluations, deferredSearchQuery, modelSortBy, numericMinParams, numericMaxParams, showUnknownSize])
135
 
136
  // Developers �� filter + sort
137
  const sortedDevelopers = useMemo(() => {
@@ -146,25 +202,32 @@ export default function ModelsPage() {
146
  )
147
  }
148
 
 
149
  return filtered.slice().sort((a, b) => {
 
150
  switch (developerSortBy) {
151
- case "evaluated":
152
- return b.evaluation_count - a.evaluation_count
153
- case "models":
154
- return b.model_count - a.model_count
155
  case "name":
156
- return a.developer.localeCompare(b.developer)
157
- case "coverage":
158
- default:
159
- return b.benchmark_count - a.benchmark_count
 
 
 
 
 
 
 
160
  }
 
 
161
  })
162
- }, [developers, deferredSearchQuery, developerSortBy])
163
 
164
  // Reset visible window when filter/sort changes
165
  useEffect(() => {
166
  setVisibleCount(PAGE_SIZE)
167
- }, [groupByDeveloper, modelSortBy, developerSortBy, deferredSearchQuery, minParamStep, maxParamStep, showUnknownSize])
168
 
169
  const totalCount = groupByDeveloper ? sortedDevelopers.length : sortedEvaluations.length
170
  const visibleEvaluations = useMemo(
@@ -268,7 +331,7 @@ export default function ModelsPage() {
268
  </button>
269
  </div>
270
 
271
- <div className="relative min-w-[180px] flex-1 sm:max-w-[260px]">
272
  <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)]" />
273
  <input
274
  className="ec-input pl-9"
@@ -280,32 +343,6 @@ export default function ModelsPage() {
280
  />
281
  </div>
282
 
283
- <select
284
- className="ec-select ml-auto shrink-0"
285
- value={groupByDeveloper ? developerSortBy : modelSortBy}
286
- onChange={(event) => {
287
- const value = event.target.value
288
- if (groupByDeveloper) setDeveloperSortBy(value as DevSort)
289
- else setModelSortBy(value as ModelSort)
290
- }}
291
- >
292
- {groupByDeveloper ? (
293
- <>
294
- <option value="coverage">Sort · Coverage</option>
295
- <option value="evaluated">Sort · Reported results</option>
296
- <option value="models">Sort · Most models</option>
297
- <option value="name">Sort · Name</option>
298
- </>
299
- ) : (
300
- <>
301
- <option value="benchmarks">Sort · Benchmarks</option>
302
- <option value="results">Sort · Reported results</option>
303
- <option value="released">Sort · Released</option>
304
- <option value="params">Sort · Parameters</option>
305
- <option value="name">Sort · Name</option>
306
- </>
307
- )}
308
- </select>
309
  </div>
310
 
311
  {/* PARAM RANGE — its own row so the rail has room to breathe.
@@ -346,15 +383,22 @@ export default function ModelsPage() {
346
  className="btn-ec outline"
347
  onClick={() => {
348
  setSearchQuery("")
349
- setModelSortBy("benchmarks")
350
- setDeveloperSortBy("coverage")
 
 
351
  }}
352
  >
353
  Reset filters
354
  </button>
355
  </div>
356
  ) : groupByDeveloper ? (
357
- <DeveloperTable rows={visibleDevelopers} />
 
 
 
 
 
358
  ) : (
359
  <ModelTable
360
  rows={visibleEvaluations}
@@ -362,6 +406,9 @@ export default function ModelsPage() {
362
  selectedIds={selectedModelIds}
363
  onToggleSelect={toggleModelSelection}
364
  maxCompare={MAX_COMPARE_MODELS}
 
 
 
365
  />
366
  )}
367
 
 
4
  import { ArrowRightLeft, Search, X } from "lucide-react"
5
 
6
  import { type BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
7
+ import { DeveloperTable, type DeveloperTableSortCol } from "@/components/developer-table"
8
  import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
9
  import { ModelCompareDialog } from "@/components/model-compare-dialog"
10
+ import { ModelTable, type ModelTableSortCol } from "@/components/model-table"
11
  import { Navigation } from "@/components/navigation"
12
  import { ParamRangePicker } from "@/components/param-range-picker"
13
  import { fetchCorpusAggregates, fetchDevelopers, fetchModelCards, fetchBenchmarkMetadata, type DeveloperListItem } from "@/lib/dashboard-data-client"
 
17
  const PAGE_SIZE = 40
18
  const MAX_COMPARE_MODELS = 4
19
 
20
+ type ModelSort = ModelTableSortCol
21
+ type DevSort = DeveloperTableSortCol
22
+ type SortDir = "asc" | "desc"
23
+
24
+ // Default sort direction per column when the user first clicks it. Numeric /
25
+ // recency columns descend (newest, biggest first); name columns ascend.
26
+ const MODEL_DEFAULT_DIR: Record<ModelSort, SortDir> = {
27
+ name: "asc",
28
+ developer: "asc",
29
+ released: "desc",
30
+ params: "desc",
31
+ benchmarks: "desc",
32
+ results: "desc",
33
+ coverage: "desc",
34
+ }
35
+ const DEV_DEFAULT_DIR: Record<DevSort, SortDir> = {
36
+ name: "asc",
37
+ models: "desc",
38
+ benchmarks: "desc",
39
+ results: "desc",
40
+ }
41
 
42
  function safeTimestamp(value: string | null | undefined) {
43
  if (!value) return 0
 
58
  const [loadingDevelopers, setLoadingDevelopers] = useState(false)
59
  const [developersReady, setDevelopersReady] = useState(false)
60
  const [groupByDeveloper, setGroupByDeveloper] = useState(false)
61
+ const [modelSortBy, setModelSortBy] = useState<ModelSort>("released")
62
+ const [modelSortDir, setModelSortDir] = useState<SortDir>("desc")
63
+ const [developerSortBy, setDeveloperSortBy] = useState<DevSort>("models")
64
+ const [developerSortDir, setDeveloperSortDir] = useState<SortDir>("desc")
65
+
66
+ const handleModelSort = useCallback((col: ModelSort) => {
67
+ setModelSortBy((current) => {
68
+ if (current === col) {
69
+ setModelSortDir((dir) => (dir === "asc" ? "desc" : "asc"))
70
+ return current
71
+ }
72
+ setModelSortDir(MODEL_DEFAULT_DIR[col])
73
+ return col
74
+ })
75
+ }, [])
76
+
77
+ const handleDeveloperSort = useCallback((col: DevSort) => {
78
+ setDeveloperSortBy((current) => {
79
+ if (current === col) {
80
+ setDeveloperSortDir((dir) => (dir === "asc" ? "desc" : "asc"))
81
+ return current
82
+ }
83
+ setDeveloperSortDir(DEV_DEFAULT_DIR[col])
84
+ return col
85
+ })
86
+ }, [])
87
  const [searchQuery, setSearchQuery] = useState("")
88
  const [selectedModelIds, setSelectedModelIds] = useState<string[]>([])
89
  const [compareOpen, setCompareOpen] = useState(false)
 
159
  })
160
  }
161
 
162
+ const dirMul = modelSortDir === "asc" ? 1 : -1
163
  return filtered.slice().sort((a, b) => {
164
+ let cmp = 0
165
  switch (modelSortBy) {
166
  case "name":
167
+ cmp = a.model_name.localeCompare(b.model_name)
168
+ break
169
+ case "developer":
170
+ cmp = a.developer.localeCompare(b.developer)
171
+ break
172
  case "released":
173
+ cmp = safeTimestamp(a.release_date) - safeTimestamp(b.release_date)
174
+ break
175
  case "params":
176
+ cmp = (a.params_billions ?? -1) - (b.params_billions ?? -1)
177
+ break
178
  case "results":
179
+ cmp = a.evaluations_count - b.evaluations_count
180
+ break
181
+ case "coverage":
182
  case "benchmarks":
183
+ cmp = a.benchmarks_count - b.benchmarks_count
184
+ break
185
  }
186
+ // Stable tie-break by model name so equal rows don't shuffle.
187
+ if (cmp === 0) return a.model_name.localeCompare(b.model_name)
188
+ return cmp * dirMul
189
  })
190
+ }, [evaluations, deferredSearchQuery, modelSortBy, modelSortDir, numericMinParams, numericMaxParams, showUnknownSize])
191
 
192
  // Developers �� filter + sort
193
  const sortedDevelopers = useMemo(() => {
 
202
  )
203
  }
204
 
205
+ const dirMul = developerSortDir === "asc" ? 1 : -1
206
  return filtered.slice().sort((a, b) => {
207
+ let cmp = 0
208
  switch (developerSortBy) {
 
 
 
 
209
  case "name":
210
+ cmp = a.developer.localeCompare(b.developer)
211
+ break
212
+ case "models":
213
+ cmp = a.model_count - b.model_count
214
+ break
215
+ case "benchmarks":
216
+ cmp = a.benchmark_count - b.benchmark_count
217
+ break
218
+ case "results":
219
+ cmp = a.evaluation_count - b.evaluation_count
220
+ break
221
  }
222
+ if (cmp === 0) return a.developer.localeCompare(b.developer)
223
+ return cmp * dirMul
224
  })
225
+ }, [developers, deferredSearchQuery, developerSortBy, developerSortDir])
226
 
227
  // Reset visible window when filter/sort changes
228
  useEffect(() => {
229
  setVisibleCount(PAGE_SIZE)
230
+ }, [groupByDeveloper, modelSortBy, modelSortDir, developerSortBy, developerSortDir, deferredSearchQuery, minParamStep, maxParamStep, showUnknownSize])
231
 
232
  const totalCount = groupByDeveloper ? sortedDevelopers.length : sortedEvaluations.length
233
  const visibleEvaluations = useMemo(
 
331
  </button>
332
  </div>
333
 
334
+ <div className="relative ml-auto min-w-[180px] flex-1 sm:max-w-[360px]">
335
  <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)]" />
336
  <input
337
  className="ec-input pl-9"
 
343
  />
344
  </div>
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  </div>
347
 
348
  {/* PARAM RANGE — its own row so the rail has room to breathe.
 
383
  className="btn-ec outline"
384
  onClick={() => {
385
  setSearchQuery("")
386
+ setModelSortBy("released")
387
+ setModelSortDir("desc")
388
+ setDeveloperSortBy("models")
389
+ setDeveloperSortDir("desc")
390
  }}
391
  >
392
  Reset filters
393
  </button>
394
  </div>
395
  ) : groupByDeveloper ? (
396
+ <DeveloperTable
397
+ rows={visibleDevelopers}
398
+ sortCol={developerSortBy}
399
+ sortDir={developerSortDir}
400
+ onSort={handleDeveloperSort}
401
+ />
402
  ) : (
403
  <ModelTable
404
  rows={visibleEvaluations}
 
406
  selectedIds={selectedModelIds}
407
  onToggleSelect={toggleModelSelection}
408
  maxCompare={MAX_COMPARE_MODELS}
409
+ sortCol={modelSortBy}
410
+ sortDir={modelSortDir}
411
+ onSort={handleModelSort}
412
  />
413
  )}
414
 
components/developer-table.tsx CHANGED
@@ -1,27 +1,74 @@
1
  "use client"
2
 
3
  import Link from "next/link"
4
- import { ArrowUpRight } from "lucide-react"
5
 
6
  import type { DeveloperListItem } from "@/lib/dashboard-data-client"
 
 
 
 
 
 
 
7
 
8
  interface DeveloperTableProps {
9
  rows: DeveloperListItem[]
 
 
 
10
  }
11
 
12
- export function DeveloperTable({ rows }: DeveloperTableProps) {
13
  const maxModels = rows.reduce((m, r) => Math.max(m, r.model_count), 1)
14
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  return (
16
  <div className="overflow-x-auto">
17
  <table className="ec-htable">
18
  <thead>
19
  <tr>
20
- <th style={{ width: "32%" }}>Developer</th>
21
- <th className="num">Models</th>
22
- <th className="num">Benchmarks</th>
23
- <th className="num">Reported results</th>
24
- <th style={{ width: "32%" }}>Top evaluations</th>
25
  <th style={{ width: 90 }} />
26
  </tr>
27
  </thead>
@@ -55,24 +102,6 @@ export function DeveloperTable({ rows }: DeveloperTableProps) {
55
  <td className="num font-mono text-[13px]">
56
  {dev.evaluation_count.toLocaleString()}
57
  </td>
58
- <td>
59
- <div className="flex flex-wrap gap-1.5">
60
- {dev.popular_evals.slice(0, 3).map((ev) => (
61
- <span
62
- key={ev.benchmark}
63
- className="inline-flex items-center gap-1 border border-[color:var(--border-soft)] bg-[color:var(--bg)] px-2 py-0.5 font-mono text-[10px] tracking-[0.04em] text-[color:var(--fg-muted)]"
64
- title={`${ev.benchmark} · ${ev.model_count} models`}
65
- >
66
- <span className="truncate max-w-[120px]">
67
- {ev.benchmark}
68
- </span>
69
- <span className="text-[color:var(--fg-subtle)]">
70
- {ev.model_count}
71
- </span>
72
- </span>
73
- ))}
74
- </div>
75
- </td>
76
  <td>
77
  <Link
78
  href={`/developers/${dev.route_id}`}
 
1
  "use client"
2
 
3
  import Link from "next/link"
4
+ import { ArrowUpRight, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"
5
 
6
  import type { DeveloperListItem } from "@/lib/dashboard-data-client"
7
+ import { cn } from "@/lib/utils"
8
+
9
+ export type DeveloperTableSortCol =
10
+ | "name"
11
+ | "models"
12
+ | "benchmarks"
13
+ | "results"
14
 
15
  interface DeveloperTableProps {
16
  rows: DeveloperListItem[]
17
+ sortCol: DeveloperTableSortCol
18
+ sortDir: "asc" | "desc"
19
+ onSort: (col: DeveloperTableSortCol) => void
20
  }
21
 
22
+ export function DeveloperTable({ rows, sortCol, sortDir, onSort }: DeveloperTableProps) {
23
  const maxModels = rows.reduce((m, r) => Math.max(m, r.model_count), 1)
24
 
25
+ function SortIcon({ col }: { col: DeveloperTableSortCol }) {
26
+ if (sortCol !== col) return <ChevronsUpDown className="h-3 w-3 opacity-30" aria-hidden />
27
+ return sortDir === "asc"
28
+ ? <ChevronUp className="h-3 w-3" aria-hidden />
29
+ : <ChevronDown className="h-3 w-3" aria-hidden />
30
+ }
31
+
32
+ function SortTh({
33
+ col,
34
+ children,
35
+ className,
36
+ style,
37
+ }: {
38
+ col: DeveloperTableSortCol
39
+ children: React.ReactNode
40
+ className?: string
41
+ style?: React.CSSProperties
42
+ }) {
43
+ const active = sortCol === col
44
+ return (
45
+ <th
46
+ className={className}
47
+ style={{
48
+ ...style,
49
+ cursor: "pointer",
50
+ userSelect: "none",
51
+ color: active ? "var(--fg)" : undefined,
52
+ }}
53
+ onClick={() => onSort(col)}
54
+ >
55
+ <span className={cn("inline-flex items-center gap-1", className?.includes("num") && "justify-end")}>
56
+ {children}
57
+ <SortIcon col={col} />
58
+ </span>
59
+ </th>
60
+ )
61
+ }
62
+
63
  return (
64
  <div className="overflow-x-auto">
65
  <table className="ec-htable">
66
  <thead>
67
  <tr>
68
+ <SortTh col="name" style={{ width: "32%" }}>Developer</SortTh>
69
+ <SortTh col="models" className="num">Models</SortTh>
70
+ <SortTh col="benchmarks" className="num">Benchmarks</SortTh>
71
+ <SortTh col="results" className="num">Reported results</SortTh>
 
72
  <th style={{ width: 90 }} />
73
  </tr>
74
  </thead>
 
102
  <td className="num font-mono text-[13px]">
103
  {dev.evaluation_count.toLocaleString()}
104
  </td>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
105
  <td>
106
  <Link
107
  href={`/developers/${dev.route_id}`}
components/model-table.tsx CHANGED
@@ -1,17 +1,29 @@
1
  "use client"
2
 
3
  import Link from "next/link"
4
- import { ArrowUpRight } from "lucide-react"
5
 
6
  import type { BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
7
  import { cn } from "@/lib/utils"
8
 
 
 
 
 
 
 
 
 
 
9
  interface ModelTableProps {
10
  rows: BenchmarkEvaluationCardData[]
11
  totalBenchmarks: number
12
  selectedIds: string[]
13
  onToggleSelect: (id: string) => void
14
  maxCompare: number
 
 
 
15
  }
16
 
17
  function formatDateShort(value: string | undefined) {
@@ -49,19 +61,60 @@ export function ModelTable({
49
  selectedIds,
50
  onToggleSelect,
51
  maxCompare,
 
 
 
52
  }: ModelTableProps) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  return (
54
  <div className="overflow-x-auto">
55
  <table className="ec-htable">
56
  <thead>
57
  <tr>
58
- <th style={{ width: "30%" }}>Model</th>
59
- <th>Developer</th>
60
- <th>Released</th>
61
- <th>Params</th>
62
- <th className="num">Benchmarks</th>
63
- <th className="num">Results</th>
64
- <th style={{ width: "20%" }}>Coverage</th>
65
  <th style={{ width: 90 }} />
66
  </tr>
67
  </thead>
 
1
  "use client"
2
 
3
  import Link from "next/link"
4
+ import { ArrowUpRight, ChevronDown, ChevronUp, ChevronsUpDown } from "lucide-react"
5
 
6
  import type { BenchmarkEvaluationCardData } from "@/components/benchmark-evaluation-card"
7
  import { cn } from "@/lib/utils"
8
 
9
+ export type ModelTableSortCol =
10
+ | "name"
11
+ | "developer"
12
+ | "released"
13
+ | "params"
14
+ | "benchmarks"
15
+ | "results"
16
+ | "coverage"
17
+
18
  interface ModelTableProps {
19
  rows: BenchmarkEvaluationCardData[]
20
  totalBenchmarks: number
21
  selectedIds: string[]
22
  onToggleSelect: (id: string) => void
23
  maxCompare: number
24
+ sortCol: ModelTableSortCol
25
+ sortDir: "asc" | "desc"
26
+ onSort: (col: ModelTableSortCol) => void
27
  }
28
 
29
  function formatDateShort(value: string | undefined) {
 
61
  selectedIds,
62
  onToggleSelect,
63
  maxCompare,
64
+ sortCol,
65
+ sortDir,
66
+ onSort,
67
  }: ModelTableProps) {
68
+ function SortIcon({ col }: { col: ModelTableSortCol }) {
69
+ if (sortCol !== col) return <ChevronsUpDown className="h-3 w-3 opacity-30" aria-hidden />
70
+ return sortDir === "asc"
71
+ ? <ChevronUp className="h-3 w-3" aria-hidden />
72
+ : <ChevronDown className="h-3 w-3" aria-hidden />
73
+ }
74
+
75
+ function SortTh({
76
+ col,
77
+ children,
78
+ className,
79
+ style,
80
+ }: {
81
+ col: ModelTableSortCol
82
+ children: React.ReactNode
83
+ className?: string
84
+ style?: React.CSSProperties
85
+ }) {
86
+ const active = sortCol === col
87
+ return (
88
+ <th
89
+ className={className}
90
+ style={{
91
+ ...style,
92
+ cursor: "pointer",
93
+ userSelect: "none",
94
+ color: active ? "var(--fg)" : undefined,
95
+ }}
96
+ onClick={() => onSort(col)}
97
+ >
98
+ <span className={cn("inline-flex items-center gap-1", className?.includes("num") && "justify-end")}>
99
+ {children}
100
+ <SortIcon col={col} />
101
+ </span>
102
+ </th>
103
+ )
104
+ }
105
+
106
  return (
107
  <div className="overflow-x-auto">
108
  <table className="ec-htable">
109
  <thead>
110
  <tr>
111
+ <SortTh col="name" style={{ width: "30%" }}>Model</SortTh>
112
+ <SortTh col="developer">Developer</SortTh>
113
+ <SortTh col="released">Released</SortTh>
114
+ <SortTh col="params">Params</SortTh>
115
+ <SortTh col="benchmarks" className="num">Benchmarks</SortTh>
116
+ <SortTh col="results" className="num">Results</SortTh>
117
+ <SortTh col="coverage" style={{ width: "20%" }}>Coverage</SortTh>
118
  <th style={{ width: 90 }} />
119
  </tr>
120
  </thead>