From 25c87651d8347e63fa6ce7e768090c48a8be915c Mon Sep 17 00:00:00 2001 From: oleksiiovdiienko Date: Sun, 29 Mar 2026 18:15:46 +0300 Subject: [PATCH 1/2] feat: add model provider filtering and sidebar navigation** Implemented a comprehensive model provider filtering system and transitioned the leaderboard to a sidebar-based navigation layout to improve usability and feature discoverability. **Details:** - **New `FilterSidebar` Component**: - Automatically extracts and counts unique providers from leaderboard entries. - Added a provider search/filter within the sidebar. - Integrated "Official only" and "Open-weight only" toggles into the sidebar. - Included a benchmark version selector in the sidebar for better context. - Added a "Clear All" functionality for active filters with a counter badge. - **Layout Refactor**: - Integrated `SidebarProvider` and `SidebarInset` from shadcn/ui into `LeaderboardView`. - Moved global filters (Official, Weights, Version) from the header to the new sidebar. - Updated `LeaderboardHeader` to include a `SidebarTrigger` and simplified its responsive design. - **Filtering Logic**: - Enhanced URL state management in `LeaderboardView` to support `provider`, `weights`, and `official` parameters. - Ensured provider colors are consistent with `PROVIDER_COLORS` across the UI. - **UI/UX Improvements**: - Added sticky header with backdrop blur for better navigation on long lists. - Improved mobile responsiveness by moving complex filters into the off-canvas sidebar. - Standardized font sizes and spacing across header and filtering controls. **Files changed:** - filter-sidebar.tsx (New) - leaderboard-header.tsx - leaderboard-view.tsx Closes #39  Conflicts:  components/leaderboard-header.tsx  components/leaderboard-view.tsx --- components/filter-sidebar.tsx | 257 ++++++++++++++++++++++++++++++ components/leaderboard-header.tsx | 166 +++++-------------- components/leaderboard-view.tsx | 166 ++++++++++--------- components/task-heatmap.tsx | 3 +- lib/mock-data.ts | 1 + 5 files changed, 392 insertions(+), 201 deletions(-) create mode 100644 components/filter-sidebar.tsx diff --git a/components/filter-sidebar.tsx b/components/filter-sidebar.tsx new file mode 100644 index 0000000..9c82b7d --- /dev/null +++ b/components/filter-sidebar.tsx @@ -0,0 +1,257 @@ +'use client' + +import { useMemo, useState } from 'react' +import { Search, X, Filter, ChevronDown, ChevronRight } from 'lucide-react' +import type { LeaderboardEntry, BenchmarkVersion } from '@/lib/types' +import { PROVIDER_COLORS } from '@/lib/types' +import { VersionSelector } from '@/components/version-selector' +import { Checkbox } from '@/components/ui/checkbox' +import { + Sidebar, + SidebarContent, + SidebarHeader, + SidebarGroup, + SidebarGroupContent, + SidebarInput, + SidebarSeparator, + SidebarFooter, +} from '@/components/ui/sidebar' + +interface FilterSidebarProps { + entries: LeaderboardEntry[] + versions: BenchmarkVersion[] + currentVersion: string | null + officialOnly: boolean + openWeightsOnly: boolean + providerFilter: string | null + lastUpdated: string + onOfficialOnlyChange: (officialOnly: boolean) => void + onOpenWeightsOnlyChange: (openWeightsOnly: boolean) => void + onProviderFilterChange: (provider: string | null) => void +} + +function CollapsibleGroup({ + label, + defaultOpen = true, + children, +}: { + label: string + defaultOpen?: boolean + children: React.ReactNode +}) { + const [open, setOpen] = useState(defaultOpen) + + return ( + + + {open && {children}} + + ) +} + +export function FilterSidebar({ + entries, + versions, + currentVersion, + officialOnly, + openWeightsOnly, + providerFilter, + lastUpdated, + onOfficialOnlyChange, + onOpenWeightsOnlyChange, + onProviderFilterChange, +}: FilterSidebarProps) { + const [providerSearch, setProviderSearch] = useState('') + + // Extract unique providers from entries, sorted by count + const providers = useMemo(() => { + const counts = new Map() + for (const entry of entries) { + const p = entry.provider.toLowerCase() + counts.set(p, (counts.get(p) || 0) + 1) + } + return Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1]) + .map(([name, count]) => ({ name, displayName: name.charAt(0).toUpperCase() + name.slice(1), count })) + }, [entries]) + + const filteredProviders = useMemo(() => { + if (!providerSearch) return providers + const q = providerSearch.toLowerCase() + return providers.filter((p) => p.name.includes(q)) + }, [providers, providerSearch]) + + const activeFilterCount = useMemo(() => { + let count = 0 + if (!officialOnly) count++ + if (openWeightsOnly) count++ + if (providerFilter) count++ + return count + }, [officialOnly, openWeightsOnly, providerFilter]) + + const clearAllFilters = () => { + onOfficialOnlyChange(true) + onOpenWeightsOnlyChange(false) + onProviderFilterChange(null) + } + + return ( + + +
+
+ + Filters + {activeFilterCount > 0 && ( + + {activeFilterCount} + + )} +
+ {activeFilterCount > 0 && ( + + )} +
+
+ + + {/* Data Quality */} + +
+ + +
+
+ + + + {/* Version */} + +
+ +
+
+ + + + {/* Providers */} + +
+ {/* Search input */} +
+ + setProviderSearch(e.target.value)} + className="h-8 pl-7 pr-7 text-xs" + /> + {providerSearch && ( + + )} +
+ + {/* Active provider filter badge */} + {providerFilter && ( +
+ + + {providerFilter} + + +
+ )} + + {/* Provider checkbox list */} +
+ {filteredProviders.map((provider) => { + const isActive = providerFilter?.toLowerCase() === provider.name + const color = PROVIDER_COLORS[provider.name] || '#666' + return ( + + ) + })} + {filteredProviders.length === 0 && ( +

+ No providers match "{providerSearch}" +

+ )} +
+
+
+
+ + +

+ Updated {lastUpdated} +

+
+
+ ) +} diff --git a/components/leaderboard-header.tsx b/components/leaderboard-header.tsx index 8ab93e5..94ad62b 100644 --- a/components/leaderboard-header.tsx +++ b/components/leaderboard-header.tsx @@ -1,9 +1,9 @@ 'use client' import Link from 'next/link' -import type { BenchmarkVersion, LeaderboardEntry } from '@/lib/types' -import { VersionSelector } from '@/components/version-selector' +import type { LeaderboardEntry } from '@/lib/types' import { ModelSearch } from '@/components/model-search' +import { SidebarTrigger } from '@/components/ui/sidebar' type ViewMode = 'success' | 'speed' | 'cost' | 'value' | 'graphs' type ScoreMode = 'best' | 'average' @@ -12,21 +12,13 @@ interface LeaderboardHeaderProps { entries: LeaderboardEntry[] filteredEntryCount: number totalRuns: number - versions: BenchmarkVersion[] currentVersion: string | null - lastUpdated: string - providerFilter: string | null - providerColor?: string + officialOnly: boolean view: ViewMode scoreMode: ScoreMode - officialOnly: boolean - openWeightsOnly: boolean modelSearchValue: string onViewChange: (view: ViewMode) => void onScoreModeChange: (mode: ScoreMode) => void - onOfficialOnlyChange: (officialOnly: boolean) => void - onOpenWeightsOnlyChange: (openWeightsOnly: boolean) => void - onClearProviderFilter: () => void onModelSearchChange: (value: string) => void } @@ -34,37 +26,28 @@ export function LeaderboardHeader({ entries, filteredEntryCount, totalRuns, - versions, currentVersion, - lastUpdated, - providerFilter, - providerColor, + officialOnly, view, scoreMode, - officialOnly, - openWeightsOnly, modelSearchValue, onViewChange, onScoreModeChange, - onOfficialOnlyChange, - onOpenWeightsOnlyChange, - onClearProviderFilter, onModelSearchChange, }: LeaderboardHeaderProps) { return ( -
-
-
-
-
- PinchBench - OpenClaw Benchmark +
+
+
+
+ + + PinchBench
- -

PinchBench

- -

Find the best model for your OpenClaw

+

PinchBench

+

Find the best model for your OpenClaw

-
+
@@ -76,81 +59,37 @@ export function LeaderboardHeader({ />
-
-
- - About - - - - GitHub - -
-
- - - - Updated {lastUpdated} -
-
-
- - {!officialOnly && ( -
- Showing official + unofficial results -
- )} - - {providerFilter && ( - - )} +
- {/* Navigation buttons - 2x3 grid on mobile, inline on desktop */} -
+ {/* View mode buttons */} +
-
- - - Updated {lastUpdated} -
) diff --git a/components/leaderboard-view.tsx b/components/leaderboard-view.tsx index e8ccb9a..9a620f0 100644 --- a/components/leaderboard-view.tsx +++ b/components/leaderboard-view.tsx @@ -3,14 +3,17 @@ import { useCallback, useMemo, useState } from 'react' import { useSearchParams, useRouter, usePathname } from 'next/navigation' import type { LeaderboardEntry, BenchmarkVersion } from '@/lib/types' -import { PROVIDER_COLORS } from '@/lib/types' + import { SimpleLeaderboard } from '@/components/simple-leaderboard' import { ScatterGraphs } from '@/components/scatter-graphs' import { TaskHeatmap } from '@/components/task-heatmap' import { ScoreDistribution } from '@/components/score-distribution' import { ModelRadar } from '@/components/model-radar' import { LeaderboardHeader } from '@/components/leaderboard-header' +import { FilterSidebar } from '@/components/filter-sidebar' import { KiloClawAdCard } from '@/components/kiloclaw-ad-card' +import { SidebarProvider, SidebarInset } from '@/components/ui/sidebar' + type ViewMode = 'success' | 'speed' | 'cost' | 'value' | 'graphs' type ScoreMode = 'best' | 'average' @@ -132,91 +135,102 @@ export function LeaderboardView({ entries, lastUpdated, versions, currentVersion }) }, [entries, providerFilter, openWeightsOnly, modelSearch]) - const providerColor = providerFilter - ? PROVIDER_COLORS[providerFilter.toLowerCase()] || '#666' - : undefined - - const totalRuns = entries.reduce((acc, entry) => acc + (entry.submission_count || 1), 0) + const totalRuns = useMemo(() => { + return filteredEntries.reduce((sum, entry) => sum + (entry.submission_count || 1), 0) + }, [filteredEntries]) return ( -
- + setProviderFilter(null)} - onModelSearchChange={handleModelSearchChange} - modelSearchValue={modelSearch} + onProviderFilterChange={setProviderFilter} /> - -
- {view === 'graphs' ? ( -
- {/* Graph sub-tabs */} -
- {([ - ['scatter', 'Scatter Plots'], - ['heatmap', 'Task Heatmap'], - ['distribution', 'Score Distribution'], - ['radar', 'Model Comparison'], - ] as const).map(([tab, label]) => ( - - ))} + + + +
+ {!officialOnlyState && ( +
+ Showing official + unofficial results
- - {graphSubTab === 'scatter' && ( - - )} - {graphSubTab === 'heatmap' && ( - - )} - {graphSubTab === 'distribution' && ( - - )} - {graphSubTab === 'radar' && ( - - )} - - -
- ) : ( - - )} -
-
+ )} + + {view === 'graphs' ? ( +
+ {/* Graph sub-tabs */} +
+ {([ + ['scatter', 'Scatter Plots'], + ['heatmap', 'Task Heatmap'], + ['distribution', 'Score Distribution'], + ['radar', 'Model Comparison'], + ] as const).map(([tab, label]) => ( + + ))} +
+ + {graphSubTab === 'scatter' && ( + + )} + {graphSubTab === 'heatmap' && ( + + )} + {graphSubTab === 'distribution' && ( + + )} + {graphSubTab === 'radar' && ( + + )} + + +
+ ) : ( + + )} + + + ) } diff --git a/components/task-heatmap.tsx b/components/task-heatmap.tsx index 99258f6..144cea6 100644 --- a/components/task-heatmap.tsx +++ b/components/task-heatmap.tsx @@ -10,6 +10,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp interface TaskHeatmapProps { entries: LeaderboardEntry[] + scoreMode: 'best' | 'average' selectedCategories: string[] onCategoriesChange: (categories: string[]) => void } @@ -38,7 +39,7 @@ function getScoreTextColor(ratio: number): string { return 'hsl(0, 70%, 75%)' } -export function TaskHeatmap({ entries, selectedCategories, onCategoriesChange }: TaskHeatmapProps) { +export function TaskHeatmap({ entries, scoreMode, selectedCategories, onCategoriesChange }: TaskHeatmapProps) { const [modelData, setModelData] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) diff --git a/lib/mock-data.ts b/lib/mock-data.ts index 9e50464..3ce0698 100644 --- a/lib/mock-data.ts +++ b/lib/mock-data.ts @@ -50,6 +50,7 @@ export const mockSubmissions: Record = { provider: "anthropic", timestamp: "2026-02-10T15:30:00Z", openclaw_version: "2.1.0", + benchmark_version: "v1.0.0", total_score: 9.2, max_score: 10.0, metadata: { From 98a3c61b8049ee2eef3f801c07ecd98ae4684bfa Mon Sep 17 00:00:00 2001 From: oleksiiovdiienko Date: Tue, 31 Mar 2026 21:35:42 +0300 Subject: [PATCH 2/2] feat: implement provider filtering and update sidebar components --- app/about/page.tsx | 5 +- app/claim/error/page.tsx | 2 + app/claim/page.tsx | 2 + app/claim/success/page.tsx | 2 + app/layout.tsx | 1 - app/runs/page.tsx | 2 + app/submission/[id]/page.tsx | 3 ++ app/user/[github_username]/page.tsx | 2 + components/filter-sidebar.tsx | 72 +++++++++++++++-------------- components/leaderboard-view.tsx | 46 ++++++++++++++---- 10 files changed, 90 insertions(+), 47 deletions(-) diff --git a/app/about/page.tsx b/app/about/page.tsx index dc295c7..468e699 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -1,6 +1,7 @@ import type { Metadata } from 'next' import { Github, ExternalLink, FileCode, Database, BarChart3, Cog, GitCommit } from 'lucide-react' import Link from 'next/link' +import { TopBanner } from '@/components/top-banner' export const metadata: Metadata = { title: 'About PinchBench - Best Models for OpenClaw FAQ', @@ -48,12 +49,12 @@ const faqJsonLd = { export default function AboutPage() { return ( - <> +