Spaces:
Running
Running
Fix model comparison + add evaluator logo
Browse files- .gitattributes +2 -0
- app/api/org-metadata/route.ts +8 -0
- app/evals/page.tsx +4 -2
- app/evaluators/[...id]/page.tsx +19 -21
- app/globals.css +7 -0
- app/layout.tsx +14 -6
- components/eval-card.tsx +4 -2
- components/eval-detail.tsx +4 -2
- components/evaluator/evaluator-letterhead.tsx +127 -0
- components/evaluator/org-logo.tsx +153 -0
- components/model-compare-dialog.tsx +3 -2
- components/org-metadata-provider.tsx +41 -0
- lib/backend-artifacts.ts +27 -0
- lib/dashboard-data-client.ts +5 -0
- lib/data-backend.ts +4 -0
- lib/evaluator-logo.ts +66 -0
- lib/evaluators.ts +34 -8
- lib/sidecars.ts +27 -0
- public/org-logos/arcadia.webp +3 -0
- public/org-logos/artificial-analysis.svg +30 -0
- public/org-logos/exgentic.png +3 -0
- public/org-logos/huggingface.svg +37 -0
- public/org-logos/llm-stats.svg +12 -0
- public/org-logos/mercor.png +3 -0
- public/org-logos/si-anthropic.svg +1 -0
- public/org-logos/si-kaggle.svg +1 -0
- public/org-logos/stanford-crfm.png +3 -0
- public/org-logos/writer.svg +8 -0
.gitattributes
CHANGED
|
@@ -35,3 +35,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
public/tutorials/screenshots/*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
public/tutorials/screenshots/*.gif filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
public/tutorials/screenshots/*.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
public/tutorials/screenshots/*.gif filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
public/org-logos/*.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
public/org-logos/*.webp filter=lfs diff=lfs merge=lfs -text
|
app/api/org-metadata/route.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { NextResponse } from "next/server"
|
| 2 |
+
|
| 3 |
+
import { getOrganizationsData } from "@/lib/data-backend"
|
| 4 |
+
|
| 5 |
+
export async function GET() {
|
| 6 |
+
const orgs = await getOrganizationsData()
|
| 7 |
+
return NextResponse.json(orgs)
|
| 8 |
+
}
|
app/evals/page.tsx
CHANGED
|
@@ -5,6 +5,7 @@ import { useRouter, useSearchParams } from "next/navigation"
|
|
| 5 |
import { Search } from "lucide-react"
|
| 6 |
|
| 7 |
import { EvaluatorTable, type EvaluatorTableSortCol } from "@/components/evaluator-table"
|
|
|
|
| 8 |
import { FamilyTable, getFamilyNavId, type FamilySortCol } from "@/components/family-table"
|
| 9 |
import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
|
| 10 |
import { Navigation } from "@/components/navigation"
|
|
@@ -172,9 +173,10 @@ function EvalsPageInner() {
|
|
| 172 |
|
| 173 |
// Evaluator groups (group-by-Evaluator mode). Verified filter is
|
| 174 |
// evaluator-aware: counts only (eval, org) pairs where org is verified.
|
|
|
|
| 175 |
const evaluatorGroups = useMemo(
|
| 176 |
-
() => groupEvalsByEvaluator(allEvals, { verifiedOnly }),
|
| 177 |
-
[allEvals, verifiedOnly],
|
| 178 |
)
|
| 179 |
|
| 180 |
const filteredEvaluators = useMemo(() => {
|
|
|
|
| 5 |
import { Search } from "lucide-react"
|
| 6 |
|
| 7 |
import { EvaluatorTable, type EvaluatorTableSortCol } from "@/components/evaluator-table"
|
| 8 |
+
import { useOrgMetadata } from "@/components/org-metadata-provider"
|
| 9 |
import { FamilyTable, getFamilyNavId, type FamilySortCol } from "@/components/family-table"
|
| 10 |
import { InfiniteScrollSentinel } from "@/components/infinite-scroll"
|
| 11 |
import { Navigation } from "@/components/navigation"
|
|
|
|
| 173 |
|
| 174 |
// Evaluator groups (group-by-Evaluator mode). Verified filter is
|
| 175 |
// evaluator-aware: counts only (eval, org) pairs where org is verified.
|
| 176 |
+
const orgMeta = useOrgMetadata()
|
| 177 |
const evaluatorGroups = useMemo(
|
| 178 |
+
() => groupEvalsByEvaluator(allEvals, { verifiedOnly, orgMeta }),
|
| 179 |
+
[allEvals, verifiedOnly, orgMeta],
|
| 180 |
)
|
| 181 |
|
| 182 |
const filteredEvaluators = useMemo(() => {
|
app/evaluators/[...id]/page.tsx
CHANGED
|
@@ -4,13 +4,15 @@ import { Suspense, useCallback, useEffect, useMemo, useState } from "react"
|
|
| 4 |
import { useParams, useRouter } from "next/navigation"
|
| 5 |
import { ArrowLeft, Search } from "lucide-react"
|
| 6 |
|
|
|
|
|
|
|
| 7 |
import { FamilyTable, type FamilySortCol } from "@/components/family-table"
|
| 8 |
import { Navigation } from "@/components/navigation"
|
| 9 |
-
import { VerifiedBadge } from "@/components/signals/verified-badge"
|
| 10 |
import type { EvalHierarchy } from "@/lib/backend-artifacts"
|
| 11 |
import type { BenchmarkCard } from "@/lib/benchmark-schema"
|
| 12 |
import { fetchBenchmarkMetadata, fetchEvalHierarchy, fetchEvalList } from "@/lib/dashboard-data-client"
|
| 13 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
|
|
|
| 14 |
import { getEvalsForEvaluator, isRecognizedEvaluator } from "@/lib/evaluators"
|
| 15 |
|
| 16 |
function EvaluatorDetailInner() {
|
|
@@ -28,6 +30,9 @@ function EvaluatorDetailInner() {
|
|
| 28 |
const [allEvals, setAllEvals] = useState<BenchmarkEvalListItem[]>([])
|
| 29 |
const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
|
| 30 |
const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
|
|
|
|
|
|
|
|
|
|
| 31 |
const [loading, setLoading] = useState(true)
|
| 32 |
const [error, setError] = useState<string | null>(null)
|
| 33 |
const [searchQuery, setSearchQuery] = useState("")
|
|
@@ -66,8 +71,8 @@ function EvaluatorDetailInner() {
|
|
| 66 |
// this route (the header reports total + verified counts separately, so no
|
| 67 |
// information is hidden). See evaluator-table.tsx hrefFor.
|
| 68 |
const { name, isVerified, evals } = useMemo(
|
| 69 |
-
() => getEvalsForEvaluator(allEvals, slug),
|
| 70 |
-
[allEvals, slug],
|
| 71 |
)
|
| 72 |
|
| 73 |
// Eval-id universe owned by this evaluator — restricts the family tree to
|
|
@@ -160,24 +165,17 @@ function EvaluatorDetailInner() {
|
|
| 160 |
Evaluators
|
| 161 |
</button>
|
| 162 |
|
| 163 |
-
{/* HEADER --------------------
|
| 164 |
-
<
|
| 165 |
-
|
| 166 |
-
{name}
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 175 |
-
<span>
|
| 176 |
-
{familyCount} {familyCount === 1 ? "family" : "families"}
|
| 177 |
-
</span>
|
| 178 |
-
<span style={{ color: "var(--fg-subtle)" }}>·</span>
|
| 179 |
-
<span>{verifiedCount} verified</span>
|
| 180 |
-
</div>
|
| 181 |
<p className="ec-page-lede">
|
| 182 |
Reported <strong>{evals.length.toLocaleString()}</strong>{" "}
|
| 183 |
{evals.length === 1 ? "evaluation" : "evaluations"} across{" "}
|
|
|
|
| 4 |
import { useParams, useRouter } from "next/navigation"
|
| 5 |
import { ArrowLeft, Search } from "lucide-react"
|
| 6 |
|
| 7 |
+
import EvaluatorLetterhead from "@/components/evaluator/evaluator-letterhead"
|
| 8 |
+
import { useOrgMetadata } from "@/components/org-metadata-provider"
|
| 9 |
import { FamilyTable, type FamilySortCol } from "@/components/family-table"
|
| 10 |
import { Navigation } from "@/components/navigation"
|
|
|
|
| 11 |
import type { EvalHierarchy } from "@/lib/backend-artifacts"
|
| 12 |
import type { BenchmarkCard } from "@/lib/benchmark-schema"
|
| 13 |
import { fetchBenchmarkMetadata, fetchEvalHierarchy, fetchEvalList } from "@/lib/dashboard-data-client"
|
| 14 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
| 15 |
+
import { normalizeOrgKey } from "@/lib/evaluator-logo"
|
| 16 |
import { getEvalsForEvaluator, isRecognizedEvaluator } from "@/lib/evaluators"
|
| 17 |
|
| 18 |
function EvaluatorDetailInner() {
|
|
|
|
| 30 |
const [allEvals, setAllEvals] = useState<BenchmarkEvalListItem[]>([])
|
| 31 |
const [hierarchy, setHierarchy] = useState<EvalHierarchy | null>(null)
|
| 32 |
const [benchmarkCards, setBenchmarkCards] = useState<Record<string, BenchmarkCard>>({})
|
| 33 |
+
// Org metadata (homepage url, logo, stable id) comes from the app-wide
|
| 34 |
+
// provider (server-fetched in the root layout) — no per-page fetch, no flash.
|
| 35 |
+
const orgMeta = useOrgMetadata()
|
| 36 |
const [loading, setLoading] = useState(true)
|
| 37 |
const [error, setError] = useState<string | null>(null)
|
| 38 |
const [searchQuery, setSearchQuery] = useState("")
|
|
|
|
| 71 |
// this route (the header reports total + verified counts separately, so no
|
| 72 |
// information is hidden). See evaluator-table.tsx hrefFor.
|
| 73 |
const { name, isVerified, evals } = useMemo(
|
| 74 |
+
() => getEvalsForEvaluator(allEvals, slug, { orgMeta }),
|
| 75 |
+
[allEvals, slug, orgMeta],
|
| 76 |
)
|
| 77 |
|
| 78 |
// Eval-id universe owned by this evaluator — restricts the family tree to
|
|
|
|
| 165 |
Evaluators
|
| 166 |
</button>
|
| 167 |
|
| 168 |
+
{/* HEADER — editorial letterhead masthead -------------------- */}
|
| 169 |
+
<EvaluatorLetterhead
|
| 170 |
+
name={name}
|
| 171 |
+
logoSrc={orgMeta[normalizeOrgKey(name)]?.logo ?? null}
|
| 172 |
+
homepageUrl={orgMeta[normalizeOrgKey(name)]?.url}
|
| 173 |
+
isVerified={isVerified}
|
| 174 |
+
recognized={isRecognizedEvaluator(name)}
|
| 175 |
+
evalCount={evals.length}
|
| 176 |
+
familyCount={familyCount}
|
| 177 |
+
verifiedCount={verifiedCount}
|
| 178 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
<p className="ec-page-lede">
|
| 180 |
Reported <strong>{evals.length.toLocaleString()}</strong>{" "}
|
| 181 |
{evals.length === 1 ? "evaluation" : "evaluations"} across{" "}
|
app/globals.css
CHANGED
|
@@ -39,6 +39,13 @@
|
|
| 39 |
--nav-bg: rgba(255, 255, 254, 0.95);
|
| 40 |
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 0, 0, 0.07);
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
/* Tracking */
|
| 43 |
--tracking-wide: 0.025em;
|
| 44 |
--tracking-widest: 0.15em;
|
|
|
|
| 39 |
--nav-bg: rgba(255, 255, 254, 0.95);
|
| 40 |
--shadow-card: 0 1px 2px rgba(0, 0, 0, 0.04), 0 4px 16px rgba(0, 0, 0, 0.07);
|
| 41 |
|
| 42 |
+
/* Logo plate — a subtly warm near-white surface for framing org brand marks.
|
| 43 |
+
Deliberately defined ONLY here (not under .dark) so it never flips to a
|
| 44 |
+
dark surface: near-black or transparent marks (e.g. Anthropic) stay legible
|
| 45 |
+
on the evaluator page in both themes. Warmer than #fff so it sits in the
|
| 46 |
+
warm-neutral system rather than reading as a cold clinical white. */
|
| 47 |
+
--logo-plate: #fdfcfa;
|
| 48 |
+
|
| 49 |
/* Tracking */
|
| 50 |
--tracking-wide: 0.025em;
|
| 51 |
--tracking-widest: 0.15em;
|
app/layout.tsx
CHANGED
|
@@ -3,9 +3,11 @@ import type { Metadata } from "next"
|
|
| 3 |
import "./globals.css"
|
| 4 |
import { AudienceModeProvider } from "@/components/audience-mode-provider"
|
| 5 |
import { BackendRefreshListener } from "@/components/backend-refresh-listener"
|
|
|
|
| 6 |
import { QuickStartProvider } from "@/components/quick-start"
|
| 7 |
import { SiteFooter } from "@/components/site-footer"
|
| 8 |
import { ThemeProvider } from "@/components/theme-provider"
|
|
|
|
| 9 |
|
| 10 |
const SITE_URL = "https://evalcards.evalevalai.com"
|
| 11 |
const SITE_NAME = "Evaluation Cards"
|
|
@@ -59,21 +61,27 @@ export const metadata: Metadata = {
|
|
| 59 |
},
|
| 60 |
}
|
| 61 |
|
| 62 |
-
export default function RootLayout({
|
| 63 |
children,
|
| 64 |
}: Readonly<{
|
| 65 |
children: React.ReactNode
|
| 66 |
}>) {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
return (
|
| 68 |
<html lang="en" className="antialiased" suppressHydrationWarning>
|
| 69 |
<body className="font-sans">
|
| 70 |
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
| 71 |
<AudienceModeProvider>
|
| 72 |
-
<
|
| 73 |
-
<
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
| 77 |
</AudienceModeProvider>
|
| 78 |
</ThemeProvider>
|
| 79 |
</body>
|
|
|
|
| 3 |
import "./globals.css"
|
| 4 |
import { AudienceModeProvider } from "@/components/audience-mode-provider"
|
| 5 |
import { BackendRefreshListener } from "@/components/backend-refresh-listener"
|
| 6 |
+
import { OrgMetadataProvider } from "@/components/org-metadata-provider"
|
| 7 |
import { QuickStartProvider } from "@/components/quick-start"
|
| 8 |
import { SiteFooter } from "@/components/site-footer"
|
| 9 |
import { ThemeProvider } from "@/components/theme-provider"
|
| 10 |
+
import { getOrganizationsData } from "@/lib/data-backend"
|
| 11 |
|
| 12 |
const SITE_URL = "https://evalcards.evalevalai.com"
|
| 13 |
const SITE_NAME = "Evaluation Cards"
|
|
|
|
| 61 |
},
|
| 62 |
}
|
| 63 |
|
| 64 |
+
export default async function RootLayout({
|
| 65 |
children,
|
| 66 |
}: Readonly<{
|
| 67 |
children: React.ReactNode
|
| 68 |
}>) {
|
| 69 |
+
// Per-org registry metadata (homepage url, logo, stable canonical id) for the
|
| 70 |
+
// whole app. Small + cached; degrades to {} when the snapshot lacks the
|
| 71 |
+
// organizations sidecar. Drives rename-stable evaluator URLs (slug = id).
|
| 72 |
+
const orgMetadata = await getOrganizationsData().catch(() => ({}))
|
| 73 |
return (
|
| 74 |
<html lang="en" className="antialiased" suppressHydrationWarning>
|
| 75 |
<body className="font-sans">
|
| 76 |
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
| 77 |
<AudienceModeProvider>
|
| 78 |
+
<OrgMetadataProvider value={orgMetadata}>
|
| 79 |
+
<QuickStartProvider>
|
| 80 |
+
<BackendRefreshListener />
|
| 81 |
+
{children}
|
| 82 |
+
<SiteFooter />
|
| 83 |
+
</QuickStartProvider>
|
| 84 |
+
</OrgMetadataProvider>
|
| 85 |
</AudienceModeProvider>
|
| 86 |
</ThemeProvider>
|
| 87 |
</body>
|
components/eval-card.tsx
CHANGED
|
@@ -19,7 +19,8 @@ import {
|
|
| 19 |
} from "lucide-react"
|
| 20 |
import Link from "next/link"
|
| 21 |
import { routeIdToPath } from "@/lib/utils"
|
| 22 |
-
import {
|
|
|
|
| 23 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
| 24 |
import { getTagColor, tagLabel } from "@/lib/benchmark-schema"
|
| 25 |
|
|
@@ -58,6 +59,7 @@ interface EvalCardProps {
|
|
| 58 |
|
| 59 |
export function EvalCard({ summary, delayMs = 0 }: EvalCardProps) {
|
| 60 |
const router = useRouter()
|
|
|
|
| 61 |
const { mode } = useAudienceMode()
|
| 62 |
const isResearchView = mode === "research"
|
| 63 |
const scorePercent = `${Math.round(summary.avg_score_norm * 100)}%`
|
|
@@ -272,7 +274,7 @@ export function EvalCard({ summary, delayMs = 0 }: EvalCardProps) {
|
|
| 272 |
<span key={name} className="inline-flex items-center">
|
| 273 |
{i > 0 ? ", " : null}
|
| 274 |
<Link
|
| 275 |
-
href={`/evaluators/${
|
| 276 |
className="hover:text-[color:var(--accent)] hover:underline"
|
| 277 |
onClick={(e) => e.stopPropagation()}
|
| 278 |
>
|
|
|
|
| 19 |
} from "lucide-react"
|
| 20 |
import Link from "next/link"
|
| 21 |
import { routeIdToPath } from "@/lib/utils"
|
| 22 |
+
import { useEvaluatorSlug } from "@/components/org-metadata-provider"
|
| 23 |
+
import { isRecognizedEvaluator } from "@/lib/evaluators"
|
| 24 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
| 25 |
import { getTagColor, tagLabel } from "@/lib/benchmark-schema"
|
| 26 |
|
|
|
|
| 59 |
|
| 60 |
export function EvalCard({ summary, delayMs = 0 }: EvalCardProps) {
|
| 61 |
const router = useRouter()
|
| 62 |
+
const slugFor = useEvaluatorSlug()
|
| 63 |
const { mode } = useAudienceMode()
|
| 64 |
const isResearchView = mode === "research"
|
| 65 |
const scorePercent = `${Math.round(summary.avg_score_norm * 100)}%`
|
|
|
|
| 274 |
<span key={name} className="inline-flex items-center">
|
| 275 |
{i > 0 ? ", " : null}
|
| 276 |
<Link
|
| 277 |
+
href={`/evaluators/${slugFor(name)}`}
|
| 278 |
className="hover:text-[color:var(--accent)] hover:underline"
|
| 279 |
onClick={(e) => e.stopPropagation()}
|
| 280 |
>
|
components/eval-detail.tsx
CHANGED
|
@@ -54,7 +54,8 @@ import {
|
|
| 54 |
import type { BenchmarkCard, SourceData } from "@/lib/benchmark-schema"
|
| 55 |
import { tagLabel } from "@/lib/benchmark-schema"
|
| 56 |
import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
|
| 57 |
-
import {
|
|
|
|
| 58 |
import type { ComparisonIndex, EvalHierarchy } from "@/lib/backend-artifacts"
|
| 59 |
import type { HierarchyEvalLocation } from "@/lib/hierarchy-lookup"
|
| 60 |
import { PolicyOverview } from "@/components/policy-overview"
|
|
@@ -425,12 +426,13 @@ function EvaluatorName({
|
|
| 425 |
className?: string
|
| 426 |
style?: React.CSSProperties
|
| 427 |
}) {
|
|
|
|
| 428 |
if (!linkName) {
|
| 429 |
return <span className={className} style={style}>{display}</span>
|
| 430 |
}
|
| 431 |
return (
|
| 432 |
<Link
|
| 433 |
-
href={`/evaluators/${
|
| 434 |
className={cn("hover:text-[color:var(--accent)] hover:underline", className)}
|
| 435 |
style={style}
|
| 436 |
onClick={(e) => e.stopPropagation()}
|
|
|
|
| 54 |
import type { BenchmarkCard, SourceData } from "@/lib/benchmark-schema"
|
| 55 |
import { tagLabel } from "@/lib/benchmark-schema"
|
| 56 |
import type { BenchmarkEvalSummary, ModelResultForBenchmark } from "@/lib/eval-processing"
|
| 57 |
+
import { isRecognizedEvaluator } from "@/lib/evaluators"
|
| 58 |
+
import { useEvaluatorSlug } from "@/components/org-metadata-provider"
|
| 59 |
import type { ComparisonIndex, EvalHierarchy } from "@/lib/backend-artifacts"
|
| 60 |
import type { HierarchyEvalLocation } from "@/lib/hierarchy-lookup"
|
| 61 |
import { PolicyOverview } from "@/components/policy-overview"
|
|
|
|
| 426 |
className?: string
|
| 427 |
style?: React.CSSProperties
|
| 428 |
}) {
|
| 429 |
+
const slugFor = useEvaluatorSlug()
|
| 430 |
if (!linkName) {
|
| 431 |
return <span className={className} style={style}>{display}</span>
|
| 432 |
}
|
| 433 |
return (
|
| 434 |
<Link
|
| 435 |
+
href={`/evaluators/${slugFor(linkName)}`}
|
| 436 |
className={cn("hover:text-[color:var(--accent)] hover:underline", className)}
|
| 437 |
style={style}
|
| 438 |
onClick={(e) => e.stopPropagation()}
|
components/evaluator/evaluator-letterhead.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { ExternalLink } from "lucide-react"
|
| 4 |
+
|
| 5 |
+
import { OrgLogo } from "@/components/evaluator/org-logo"
|
| 6 |
+
import { VerifiedBadge } from "@/components/signals/verified-badge"
|
| 7 |
+
import { cn } from "@/lib/utils"
|
| 8 |
+
|
| 9 |
+
/**
|
| 10 |
+
* EvaluatorLetterhead — the masthead header for an evaluator (reporting org)
|
| 11 |
+
* page. A full-bleed warm band with a hairline base rule, set like an academic
|
| 12 |
+
* journal letterhead:
|
| 13 |
+
*
|
| 14 |
+
* eyebrow ("Evaluator")
|
| 15 |
+
* [ logo plate ] Org Name ✓ ← vertically-centred hero row
|
| 16 |
+
* reporting organisation · N evaluations · N families · N verified
|
| 17 |
+
*
|
| 18 |
+
* The logo is rendered through <OrgLogo/>, which auto-squares and auto-sizes
|
| 19 |
+
* any aspect ratio onto a consistent light plate and falls back to a monogram
|
| 20 |
+
* when no mark is known. The meta strip is indented to begin under the name
|
| 21 |
+
* (plate width + gap) so the lockup reads as one unit.
|
| 22 |
+
*
|
| 23 |
+
* Full bleed: the negative margin matches the evaluator page's responsive
|
| 24 |
+
* padding (px-4 → sm:px-8) so the band paints to the content-column edges
|
| 25 |
+
* without causing horizontal overflow at any width.
|
| 26 |
+
*/
|
| 27 |
+
|
| 28 |
+
export interface EvaluatorLetterheadProps {
|
| 29 |
+
name: string
|
| 30 |
+
/** Brand-mark URL/path or null → monogram. Sourced from the registry org
|
| 31 |
+
* metadata (organizations.json sidecar). */
|
| 32 |
+
logoSrc: string | null
|
| 33 |
+
/** Org homepage (registry `website`). When present, a small external-link
|
| 34 |
+
* affordance is shown next to the name; hidden entirely when absent. */
|
| 35 |
+
homepageUrl?: string
|
| 36 |
+
isVerified: boolean
|
| 37 |
+
recognized: boolean
|
| 38 |
+
evalCount: number
|
| 39 |
+
familyCount: number
|
| 40 |
+
verifiedCount: number
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
export default function EvaluatorLetterhead({
|
| 44 |
+
name,
|
| 45 |
+
logoSrc,
|
| 46 |
+
homepageUrl,
|
| 47 |
+
isVerified,
|
| 48 |
+
recognized,
|
| 49 |
+
evalCount,
|
| 50 |
+
familyCount,
|
| 51 |
+
verifiedCount,
|
| 52 |
+
}: EvaluatorLetterheadProps) {
|
| 53 |
+
return (
|
| 54 |
+
<header
|
| 55 |
+
className={cn(
|
| 56 |
+
"-mx-4 mb-8 border-b border-[color:var(--border-soft)] bg-[color:var(--bg-warm)] px-4",
|
| 57 |
+
"sm:-mx-8 sm:px-8",
|
| 58 |
+
)}
|
| 59 |
+
>
|
| 60 |
+
<div className="py-8">
|
| 61 |
+
{logoSrc === null && <span className="sr-only">{name} (no logo available)</span>}
|
| 62 |
+
|
| 63 |
+
<div className="kicker">Evaluator</div>
|
| 64 |
+
|
| 65 |
+
{/* Hero row — plate + name vertically centred together. */}
|
| 66 |
+
<div className="mt-2.5 flex items-center gap-5 sm:gap-6">
|
| 67 |
+
<OrgLogo
|
| 68 |
+
name={name}
|
| 69 |
+
src={logoSrc}
|
| 70 |
+
className="[--logo-size:64px] sm:[--logo-size:72px]"
|
| 71 |
+
/>
|
| 72 |
+
|
| 73 |
+
<h1 className="ec-page-h1 inline-flex flex-wrap items-center gap-x-2.5 gap-y-1">
|
| 74 |
+
<span className="min-w-0 break-words">{name}</span>
|
| 75 |
+
<VerifiedBadge verified={isVerified} recognized={recognized} size="md" />
|
| 76 |
+
{homepageUrl && (
|
| 77 |
+
<a
|
| 78 |
+
href={homepageUrl}
|
| 79 |
+
target="_blank"
|
| 80 |
+
rel="noopener noreferrer"
|
| 81 |
+
aria-label={`Visit ${name}'s homepage`}
|
| 82 |
+
title={`Visit ${name}'s homepage`}
|
| 83 |
+
className="inline-flex shrink-0 items-center text-[color:var(--fg-subtle)] transition-colors hover:text-[color:var(--accent)]"
|
| 84 |
+
>
|
| 85 |
+
<ExternalLink className="h-4 w-4" aria-hidden />
|
| 86 |
+
</a>
|
| 87 |
+
)}
|
| 88 |
+
</h1>
|
| 89 |
+
</div>
|
| 90 |
+
|
| 91 |
+
{/* Mono-caps colophon — indented to begin under the name. */}
|
| 92 |
+
<div className="mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 pl-[84px] font-mono text-[11px] uppercase tracking-[0.12em] text-[color:var(--fg-muted)] sm:pl-[96px]">
|
| 93 |
+
<span>Reporting organisation</span>
|
| 94 |
+
<Dot />
|
| 95 |
+
<span>
|
| 96 |
+
<span className="font-semibold tabular-nums text-[color:var(--fg)]">
|
| 97 |
+
{evalCount.toLocaleString()}
|
| 98 |
+
</span>{" "}
|
| 99 |
+
{evalCount === 1 ? "evaluation" : "evaluations"}
|
| 100 |
+
</span>
|
| 101 |
+
<Dot />
|
| 102 |
+
<span>
|
| 103 |
+
<span className="font-semibold tabular-nums text-[color:var(--fg)]">
|
| 104 |
+
{familyCount.toLocaleString()}
|
| 105 |
+
</span>{" "}
|
| 106 |
+
{familyCount === 1 ? "family" : "families"}
|
| 107 |
+
</span>
|
| 108 |
+
<Dot />
|
| 109 |
+
<span>
|
| 110 |
+
<span className="font-semibold tabular-nums text-[color:var(--fg)]">
|
| 111 |
+
{verifiedCount.toLocaleString()}
|
| 112 |
+
</span>{" "}
|
| 113 |
+
verified
|
| 114 |
+
</span>
|
| 115 |
+
</div>
|
| 116 |
+
</div>
|
| 117 |
+
</header>
|
| 118 |
+
)
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
function Dot() {
|
| 122 |
+
return (
|
| 123 |
+
<span aria-hidden="true" className="text-[color:var(--fg-subtle)]">
|
| 124 |
+
·
|
| 125 |
+
</span>
|
| 126 |
+
)
|
| 127 |
+
}
|
components/evaluator/org-logo.tsx
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { useCallback, useEffect, useRef, useState } from "react"
|
| 4 |
+
|
| 5 |
+
import { monogramFor, monogramHue } from "@/lib/evaluator-logo"
|
| 6 |
+
import { cn } from "@/lib/utils"
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* OrgLogo — renders an evaluating org's brand mark inside a consistent square
|
| 10 |
+
* plate, robust to the wildly inconsistent assets real orgs ship.
|
| 11 |
+
*
|
| 12 |
+
* The problem: some orgs have PR-ready square icons; most don't. We get 4:1
|
| 13 |
+
* horizontal wordmarks, tall lockups, tiny favicons, transparent PNGs and SVGs
|
| 14 |
+
* — every aspect ratio and resolution. We want a single, consistent on-page
|
| 15 |
+
* footprint regardless.
|
| 16 |
+
*
|
| 17 |
+
* The strategy ("auto-square + auto-size"):
|
| 18 |
+
* 1. Fixed SQUARE plate. Size is driven by the `--logo-size` CSS var (default
|
| 19 |
+
* 72px) so a caller can make it responsive with one arbitrary class, e.g.
|
| 20 |
+
* `className="[--logo-size:64px] sm:[--logo-size:72px]"`. The mark never
|
| 21 |
+
* changes the plate's shape.
|
| 22 |
+
* 2. `object-contain`, always — the mark is scaled to FIT, never cropped
|
| 23 |
+
* (object-cover) and never stretched (object-fill). No distortion, ever.
|
| 24 |
+
* 3. Aspect-aware bounds. With object-contain alone, a wide wordmark shrinks
|
| 25 |
+
* to a tiny sliver in a square box. So we measure the loaded image's
|
| 26 |
+
* natural aspect and cap width/height per shape class (wide / tall /
|
| 27 |
+
* square) so each reaches a comparable OPTICAL size: a wordmark is allowed
|
| 28 |
+
* to span most of the plate width, a square icon keeps breathing room.
|
| 29 |
+
* 4. Graceful fallbacks: no src, or a load error, or a degenerate 0×0 asset
|
| 30 |
+
* → a deterministic monogram tile on the same plate. The layout never
|
| 31 |
+
* collapses or shifts.
|
| 32 |
+
*
|
| 33 |
+
* Measuring handles cached images too (onLoad may not fire) via a ref check on
|
| 34 |
+
* mount. The mark fades in once measured to avoid a first-paint size jump.
|
| 35 |
+
*/
|
| 36 |
+
|
| 37 |
+
type Fit = "square" | "wide" | "tall"
|
| 38 |
+
|
| 39 |
+
// Max width/height the mark may occupy inside the plate, as a fraction, per
|
| 40 |
+
// shape. Tuned so a 3.7:1 wordmark and a 1:1 icon feel like the same "weight".
|
| 41 |
+
const BOUNDS: Record<Fit, { maxW: number; maxH: number }> = {
|
| 42 |
+
square: { maxW: 0.64, maxH: 0.64 },
|
| 43 |
+
wide: { maxW: 0.9, maxH: 0.62 },
|
| 44 |
+
tall: { maxW: 0.62, maxH: 0.86 },
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
function classifyAspect(aspect: number): Fit {
|
| 48 |
+
if (!Number.isFinite(aspect) || aspect <= 0) return "square"
|
| 49 |
+
if (aspect > 1.3) return "wide"
|
| 50 |
+
if (aspect < 0.77) return "tall"
|
| 51 |
+
return "square"
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
export function OrgLogo({
|
| 55 |
+
name,
|
| 56 |
+
src,
|
| 57 |
+
className,
|
| 58 |
+
rounded = "rounded-[4px]",
|
| 59 |
+
}: {
|
| 60 |
+
name: string
|
| 61 |
+
/** Brand mark URL, or null when none is known → monogram. */
|
| 62 |
+
src: string | null
|
| 63 |
+
/** Box sizing / `--logo-size` override, e.g. "[--logo-size:64px] sm:[--logo-size:72px]". */
|
| 64 |
+
className?: string
|
| 65 |
+
rounded?: string
|
| 66 |
+
}) {
|
| 67 |
+
// "pending" until the image loads & is measured; "ok" once we have an aspect;
|
| 68 |
+
// "error" if it fails / is degenerate → monogram.
|
| 69 |
+
const [status, setStatus] = useState<"pending" | "ok" | "error">(src ? "pending" : "error")
|
| 70 |
+
const [fit, setFit] = useState<Fit>("square")
|
| 71 |
+
const imgRef = useRef<HTMLImageElement | null>(null)
|
| 72 |
+
|
| 73 |
+
const measure = useCallback((img: HTMLImageElement) => {
|
| 74 |
+
const { naturalWidth: w, naturalHeight: h } = img
|
| 75 |
+
if (!w || !h) {
|
| 76 |
+
setStatus("error") // degenerate asset → monogram
|
| 77 |
+
return
|
| 78 |
+
}
|
| 79 |
+
setFit(classifyAspect(w / h))
|
| 80 |
+
setStatus("ok")
|
| 81 |
+
}, [])
|
| 82 |
+
|
| 83 |
+
// Cached images can be `complete` before React attaches onLoad — measure on
|
| 84 |
+
// mount if so. Re-runs when src changes.
|
| 85 |
+
useEffect(() => {
|
| 86 |
+
if (!src) {
|
| 87 |
+
setStatus("error")
|
| 88 |
+
return
|
| 89 |
+
}
|
| 90 |
+
setStatus("pending")
|
| 91 |
+
const img = imgRef.current
|
| 92 |
+
if (img && img.complete) {
|
| 93 |
+
if (img.naturalWidth) measure(img)
|
| 94 |
+
else setStatus("error")
|
| 95 |
+
}
|
| 96 |
+
}, [src, measure])
|
| 97 |
+
|
| 98 |
+
const plateClass = cn(
|
| 99 |
+
"relative flex shrink-0 items-center justify-center overflow-hidden border",
|
| 100 |
+
"h-[var(--logo-size,72px)] w-[var(--logo-size,72px)] bg-[color:var(--logo-plate)] shadow-[var(--shadow-card)]",
|
| 101 |
+
rounded,
|
| 102 |
+
className,
|
| 103 |
+
)
|
| 104 |
+
const plateBorder = { borderColor: "var(--border-soft)" } as const
|
| 105 |
+
|
| 106 |
+
if (status === "error") {
|
| 107 |
+
return (
|
| 108 |
+
<div className={plateClass} style={plateBorder}>
|
| 109 |
+
<Monogram name={name} />
|
| 110 |
+
</div>
|
| 111 |
+
)
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
const { maxW, maxH } = BOUNDS[fit]
|
| 115 |
+
return (
|
| 116 |
+
<div className={plateClass} style={plateBorder}>
|
| 117 |
+
<img
|
| 118 |
+
ref={imgRef}
|
| 119 |
+
src={src ?? undefined}
|
| 120 |
+
alt={`${name} logo`}
|
| 121 |
+
loading="eager"
|
| 122 |
+
decoding="async"
|
| 123 |
+
onLoad={(e) => measure(e.currentTarget)}
|
| 124 |
+
onError={() => setStatus("error")}
|
| 125 |
+
className={cn(
|
| 126 |
+
"object-contain transition-opacity duration-200",
|
| 127 |
+
status === "pending" ? "opacity-0" : "opacity-100",
|
| 128 |
+
)}
|
| 129 |
+
style={{ maxWidth: `${maxW * 100}%`, maxHeight: `${maxH * 100}%` }}
|
| 130 |
+
/>
|
| 131 |
+
</div>
|
| 132 |
+
)
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
/** Deterministic monogram tile — fills the plate; org name carried by an
|
| 136 |
+
* adjacent heading, so the glyph itself is aria-hidden. */
|
| 137 |
+
function Monogram({ name }: { name: string }) {
|
| 138 |
+
const hue = monogramHue(name)
|
| 139 |
+
return (
|
| 140 |
+
<span
|
| 141 |
+
aria-hidden="true"
|
| 142 |
+
className="flex h-full w-full select-none items-center justify-center rounded-[3px] font-sans font-semibold leading-none"
|
| 143 |
+
style={{
|
| 144 |
+
color: `hsl(${hue} 32% 38%)`,
|
| 145 |
+
backgroundColor: `hsl(${hue} 38% 92%)`,
|
| 146 |
+
fontSize: "calc(var(--logo-size, 72px) * 0.34)",
|
| 147 |
+
}}
|
| 148 |
+
>
|
| 149 |
+
{monogramFor(name)}
|
| 150 |
+
<span className="sr-only">{name}</span>
|
| 151 |
+
</span>
|
| 152 |
+
)
|
| 153 |
+
}
|
components/model-compare-dialog.tsx
CHANGED
|
@@ -24,7 +24,8 @@ import {
|
|
| 24 |
} from "@/components/ui/dialog"
|
| 25 |
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
| 26 |
|
| 27 |
-
function formatDate(isoString: string) {
|
|
|
|
| 28 |
const numeric = Number(isoString)
|
| 29 |
const parsedDate =
|
| 30 |
!Number.isNaN(numeric) && !isoString.includes("-")
|
|
@@ -430,7 +431,7 @@ export function ModelCompareDialog({
|
|
| 430 |
{row.key === "latest" ? (
|
| 431 |
<div className="flex items-center gap-2">
|
| 432 |
<span>{model.latest_source_name || `${model.benchmarks_count} benchmark composites summarized`}</span>
|
| 433 |
-
{model.source_urls[0] ? (
|
| 434 |
<a
|
| 435 |
href={model.source_urls[0]}
|
| 436 |
target="_blank"
|
|
|
|
| 24 |
} from "@/components/ui/dialog"
|
| 25 |
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
| 26 |
|
| 27 |
+
function formatDate(isoString: string | null | undefined) {
|
| 28 |
+
if (!isoString) return "—"
|
| 29 |
const numeric = Number(isoString)
|
| 30 |
const parsedDate =
|
| 31 |
!Number.isNaN(numeric) && !isoString.includes("-")
|
|
|
|
| 431 |
{row.key === "latest" ? (
|
| 432 |
<div className="flex items-center gap-2">
|
| 433 |
<span>{model.latest_source_name || `${model.benchmarks_count} benchmark composites summarized`}</span>
|
| 434 |
+
{model.source_urls?.[0] ? (
|
| 435 |
<a
|
| 436 |
href={model.source_urls[0]}
|
| 437 |
target="_blank"
|
components/org-metadata-provider.tsx
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
|
| 3 |
+
import { createContext, useContext, useMemo } from "react"
|
| 4 |
+
|
| 5 |
+
import { evaluatorSlugFor, type OrgMetaMap } from "@/lib/evaluators"
|
| 6 |
+
|
| 7 |
+
/**
|
| 8 |
+
* Makes the per-org registry metadata (from the organizations.json sidecar)
|
| 9 |
+
* available app-wide, keyed by normalizeOrgKey(displayName). It carries each
|
| 10 |
+
* org's stable canonical `id`, which is what evaluator URLs are slugged from —
|
| 11 |
+
* so renaming an org's display name never changes its /evaluators/<slug> URL.
|
| 12 |
+
*
|
| 13 |
+
* Fetched once server-side in the root layout and handed down as a plain
|
| 14 |
+
* (serializable) object, so the slug is correct on first render with no flash.
|
| 15 |
+
*/
|
| 16 |
+
const OrgMetadataContext = createContext<OrgMetaMap>({})
|
| 17 |
+
|
| 18 |
+
export function OrgMetadataProvider({
|
| 19 |
+
value,
|
| 20 |
+
children,
|
| 21 |
+
}: {
|
| 22 |
+
value: OrgMetaMap
|
| 23 |
+
children: React.ReactNode
|
| 24 |
+
}) {
|
| 25 |
+
return <OrgMetadataContext.Provider value={value ?? {}}>{children}</OrgMetadataContext.Provider>
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
/** Raw display→metadata map (homepage url, logo, canonical id). */
|
| 29 |
+
export function useOrgMetadata(): OrgMetaMap {
|
| 30 |
+
return useContext(OrgMetadataContext)
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/**
|
| 34 |
+
* Returns the canonical, rename-stable slug function bound to the current org
|
| 35 |
+
* metadata. Use this everywhere an /evaluators/<slug> link is built so the
|
| 36 |
+
* whole app agrees on one URL per org.
|
| 37 |
+
*/
|
| 38 |
+
export function useEvaluatorSlug(): (name: string) => string {
|
| 39 |
+
const orgMeta = useContext(OrgMetadataContext)
|
| 40 |
+
return useMemo(() => (name: string) => evaluatorSlugFor(name, orgMeta), [orgMeta])
|
| 41 |
+
}
|
lib/backend-artifacts.ts
CHANGED
|
@@ -525,3 +525,30 @@ export interface PeerRanksSidecar {
|
|
| 525 |
config_version: number
|
| 526 |
ranks: PeerRanksMap
|
| 527 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 525 |
config_version: number
|
| 526 |
ranks: PeerRanksMap
|
| 527 |
}
|
| 528 |
+
|
| 529 |
+
// ---------------------------------------------------------------------------
|
| 530 |
+
// organizations.json — per-evaluator-org metadata (homepage URL + logo)
|
| 531 |
+
// ---------------------------------------------------------------------------
|
| 532 |
+
|
| 533 |
+
/** Metadata for one reporting org, sourced from the registry (canonical_orgs).
|
| 534 |
+
* All fields beyond `name` are optional — an org only appears here when it
|
| 535 |
+
* carries a website or logo_url, so the evaluator page degrades gracefully
|
| 536 |
+
* (monogram, no link) for everyone absent. */
|
| 537 |
+
export interface OrgMetadata {
|
| 538 |
+
/** Canonical display name (matches an `evaluator_names` string). */
|
| 539 |
+
name: string
|
| 540 |
+
/** Canonical org id, when resolved. */
|
| 541 |
+
id?: string
|
| 542 |
+
/** Homepage URL (registry `website`). */
|
| 543 |
+
url?: string
|
| 544 |
+
/** Brand-mark pointer (registry `logo_url`): frontend-relative path or URL. */
|
| 545 |
+
logo?: string
|
| 546 |
+
}
|
| 547 |
+
|
| 548 |
+
/** organizations.json envelope. `orgs` is keyed by the NORMALISED display name
|
| 549 |
+
* (lower-cased, whitespace-collapsed — see normalizeOrgKey) so the evaluator
|
| 550 |
+
* page can look an org up directly from the name it already holds. */
|
| 551 |
+
export interface OrgMetadataIndex {
|
| 552 |
+
generated_at: string
|
| 553 |
+
orgs: Record<string, OrgMetadata>
|
| 554 |
+
}
|
lib/dashboard-data-client.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type {
|
|
| 3 |
ComparisonIndex,
|
| 4 |
CorpusAggregates,
|
| 5 |
EvalHierarchy,
|
|
|
|
| 6 |
PeerRanksMap,
|
| 7 |
} from "@/lib/backend-artifacts"
|
| 8 |
import { decorateHierarchyDerivedTags } from "@/lib/benchmark-tags"
|
|
@@ -114,3 +115,7 @@ export function fetchCorpusAggregates() {
|
|
| 114 |
export function fetchPeerRanks() {
|
| 115 |
return fetchJson<PeerRanksMap>("/api/peer-ranks")
|
| 116 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
ComparisonIndex,
|
| 4 |
CorpusAggregates,
|
| 5 |
EvalHierarchy,
|
| 6 |
+
OrgMetadata,
|
| 7 |
PeerRanksMap,
|
| 8 |
} from "@/lib/backend-artifacts"
|
| 9 |
import { decorateHierarchyDerivedTags } from "@/lib/benchmark-tags"
|
|
|
|
| 115 |
export function fetchPeerRanks() {
|
| 116 |
return fetchJson<PeerRanksMap>("/api/peer-ranks")
|
| 117 |
}
|
| 118 |
+
|
| 119 |
+
export function fetchOrganizations() {
|
| 120 |
+
return fetchJson<Record<string, OrgMetadata>>("/api/org-metadata")
|
| 121 |
+
}
|
lib/data-backend.ts
CHANGED
|
@@ -90,6 +90,10 @@ export async function getBackendManifestStatusData(): Promise<BackendManifestSta
|
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
export async function getEvalHierarchyData() {
|
| 94 |
// The v2 sidecar ships hierarchy.json in the composite/family/slice
|
| 95 |
// taxonomy shape (top-level `composites[]`, flat `families[]` lookup
|
|
|
|
| 90 |
}
|
| 91 |
}
|
| 92 |
|
| 93 |
+
export async function getOrganizationsData() {
|
| 94 |
+
return (await sidecars()).fetchOrganizations()
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
export async function getEvalHierarchyData() {
|
| 98 |
// The v2 sidecar ships hierarchy.json in the composite/family/slice
|
| 99 |
// taxonomy shape (top-level `composites[]`, flat `families[]` lookup
|
lib/evaluator-logo.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* Org-logo helpers for the evaluator page.
|
| 3 |
+
*
|
| 4 |
+
* Logos + homepage URLs are sourced from the registry (canonical_orgs) and
|
| 5 |
+
* delivered to the frontend via the organizations.json sidecar — see
|
| 6 |
+
* lib/dashboard-data-client.fetchOrganizations(). The evaluator page looks an
|
| 7 |
+
* org up by normalizeOrgKey(name); the mark is rendered through <OrgLogo/>,
|
| 8 |
+
* which auto-squares + auto-sizes any aspect ratio and falls back to the
|
| 9 |
+
* name-derived monogram when the registry has no logo for the org.
|
| 10 |
+
*/
|
| 11 |
+
|
| 12 |
+
/**
|
| 13 |
+
* Monogram initials for the logo-less fallback. Drops common org-suffix noise
|
| 14 |
+
* ("AI", "Institute", "Inc"…), takes the first letter of up to two significant
|
| 15 |
+
* words, and falls back to the first two characters for single-word names.
|
| 16 |
+
*/
|
| 17 |
+
const STOP_WORDS = new Set([
|
| 18 |
+
"ai",
|
| 19 |
+
"the",
|
| 20 |
+
"of",
|
| 21 |
+
"for",
|
| 22 |
+
"and",
|
| 23 |
+
"institute",
|
| 24 |
+
"inc",
|
| 25 |
+
"labs",
|
| 26 |
+
"lab",
|
| 27 |
+
"research",
|
| 28 |
+
])
|
| 29 |
+
|
| 30 |
+
export function monogramFor(name: string): string {
|
| 31 |
+
const words = name
|
| 32 |
+
.replace(/[^\p{L}\p{N}\s]/gu, " ")
|
| 33 |
+
.split(/\s+/)
|
| 34 |
+
.filter(Boolean)
|
| 35 |
+
const significant = words.filter((w) => !STOP_WORDS.has(w.toLowerCase()))
|
| 36 |
+
const pool = significant.length > 0 ? significant : words
|
| 37 |
+
if (pool.length === 0) return "?"
|
| 38 |
+
if (pool.length === 1) {
|
| 39 |
+
return pool[0].slice(0, 2).toUpperCase()
|
| 40 |
+
}
|
| 41 |
+
return (pool[0][0] + pool[1][0]).toUpperCase()
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
/**
|
| 45 |
+
* Deterministic warm hue for a monogram tile, derived from the name so the
|
| 46 |
+
* same org always gets the same colour. Tuned to sit quietly against the warm
|
| 47 |
+
* neutral palette (low saturation, mid lightness).
|
| 48 |
+
*/
|
| 49 |
+
export function monogramHue(name: string): number {
|
| 50 |
+
let h = 0
|
| 51 |
+
for (let i = 0; i < name.length; i++) {
|
| 52 |
+
h = (h * 31 + name.charCodeAt(i)) % 360
|
| 53 |
+
}
|
| 54 |
+
return h
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/**
|
| 58 |
+
* Normalise an org/evaluator name to the key used by the organizations.json
|
| 59 |
+
* sidecar. MUST stay in sync with the pipeline's `_normalize_org_key`
|
| 60 |
+
* (eval_cards_backend_pipeline/.../sidecars.py) so the frontend's lookup keys
|
| 61 |
+
* match the producer's. The evaluator page uses this to find an org's homepage
|
| 62 |
+
* URL + logo from the sidecar map.
|
| 63 |
+
*/
|
| 64 |
+
export function normalizeOrgKey(name: string): string {
|
| 65 |
+
return name.trim().toLowerCase().replace(/\s+/g, " ")
|
| 66 |
+
}
|
lib/evaluators.ts
CHANGED
|
@@ -17,7 +17,13 @@
|
|
| 17 |
* 2025)") still produce a valid, unique, non-empty slug.
|
| 18 |
*/
|
| 19 |
|
|
|
|
| 20 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
/**
|
| 23 |
* "Recognized" evaluator orgs — public leaderboards whose results we ingest
|
|
@@ -75,6 +81,23 @@ export function evaluatorSlug(name: string): string {
|
|
| 75 |
return slug || "evaluator"
|
| 76 |
}
|
| 77 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 78 |
/** @deprecated internal alias — use {@link evaluatorSlug}. */
|
| 79 |
const baseSlug = evaluatorSlug
|
| 80 |
|
|
@@ -84,7 +107,10 @@ const baseSlug = evaluatorSlug
|
|
| 84 |
* order (name-sorted) so the same corpus always yields the same slugs;
|
| 85 |
* collisions get a numeric suffix (`-2`, `-3`, …).
|
| 86 |
*/
|
| 87 |
-
export function buildEvaluatorSlugMap(
|
|
|
|
|
|
|
|
|
|
| 88 |
slugToName: Map<string, string>
|
| 89 |
nameToSlug: Map<string, string>
|
| 90 |
} {
|
|
@@ -101,7 +127,7 @@ export function buildEvaluatorSlugMap(evals: BenchmarkEvalListItem[]): {
|
|
| 101 |
const used = new Map<string, number>()
|
| 102 |
|
| 103 |
for (const name of Array.from(names).sort((a, b) => a.localeCompare(b))) {
|
| 104 |
-
const base =
|
| 105 |
const seen = used.get(base) ?? 0
|
| 106 |
const slug = seen === 0 ? base : `${base}-${seen + 1}`
|
| 107 |
used.set(base, seen + 1)
|
|
@@ -120,10 +146,10 @@ export function buildEvaluatorSlugMap(evals: BenchmarkEvalListItem[]): {
|
|
| 120 |
*/
|
| 121 |
export function groupEvalsByEvaluator(
|
| 122 |
evals: BenchmarkEvalListItem[],
|
| 123 |
-
opts?: { verifiedOnly?: boolean },
|
| 124 |
): EvaluatorGroup[] {
|
| 125 |
const verifiedOnly = opts?.verifiedOnly ?? false
|
| 126 |
-
const { nameToSlug } = buildEvaluatorSlugMap(evals)
|
| 127 |
|
| 128 |
const acc = new Map<string, { evalCount: number; verifiedCount: number; isVerified: boolean }>()
|
| 129 |
|
|
@@ -159,7 +185,7 @@ export function groupEvalsByEvaluator(
|
|
| 159 |
for (const [name, v] of acc) {
|
| 160 |
groups.push({
|
| 161 |
name,
|
| 162 |
-
slug: nameToSlug.get(name) ??
|
| 163 |
evalCount: v.evalCount,
|
| 164 |
verifiedCount: v.verifiedCount,
|
| 165 |
isVerified: v.isVerified,
|
|
@@ -179,10 +205,10 @@ export function groupEvalsByEvaluator(
|
|
| 179 |
export function getEvalsForEvaluator(
|
| 180 |
allEvals: BenchmarkEvalListItem[],
|
| 181 |
slug: string,
|
| 182 |
-
opts?: { verifiedOnly?: boolean },
|
| 183 |
): { name: string | null; isVerified: boolean; evals: BenchmarkEvalListItem[] } {
|
| 184 |
const verifiedOnly = opts?.verifiedOnly ?? false
|
| 185 |
-
const { slugToName } = buildEvaluatorSlugMap(allEvals)
|
| 186 |
// Direct hit on the (possibly collision-suffixed) full slug, else fall back
|
| 187 |
// to base-slug matching so links built from the shared `evaluatorSlug(name)`
|
| 188 |
// helper — which can't see the suffix step — still resolve. The fallback is
|
|
@@ -192,7 +218,7 @@ export function getEvalsForEvaluator(
|
|
| 192 |
for (const [, candidate] of Array.from(slugToName.entries()).sort((a, b) =>
|
| 193 |
a[1].localeCompare(b[1]),
|
| 194 |
)) {
|
| 195 |
-
if (
|
| 196 |
name = candidate
|
| 197 |
break
|
| 198 |
}
|
|
|
|
| 17 |
* 2025)") still produce a valid, unique, non-empty slug.
|
| 18 |
*/
|
| 19 |
|
| 20 |
+
import type { OrgMetadata } from "@/lib/backend-artifacts"
|
| 21 |
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
|
| 22 |
+
import { normalizeOrgKey } from "@/lib/evaluator-logo"
|
| 23 |
+
|
| 24 |
+
/** Display-name → registry org metadata (incl. the stable canonical `id`),
|
| 25 |
+
* as delivered by the organizations.json sidecar (keyed by normalizeOrgKey). */
|
| 26 |
+
export type OrgMetaMap = Record<string, OrgMetadata>
|
| 27 |
|
| 28 |
/**
|
| 29 |
* "Recognized" evaluator orgs — public leaderboards whose results we ingest
|
|
|
|
| 81 |
return slug || "evaluator"
|
| 82 |
}
|
| 83 |
|
| 84 |
+
/**
|
| 85 |
+
* Stable, URL-safe slug for an evaluator — the SINGLE source of truth for
|
| 86 |
+
* /evaluators/<slug> URLs (every link builder and the detail-route resolver
|
| 87 |
+
* go through this).
|
| 88 |
+
*
|
| 89 |
+
* When the org resolves to a registry entity (`orgMeta` carries its canonical
|
| 90 |
+
* `id`), the slug is derived from that STABLE id, so renaming the display name
|
| 91 |
+
* never changes the URL. Orgs with no registry entry (raw corpus strings,
|
| 92 |
+
* which can't be renamed) fall back to the display-name slug. `evaluatorSlug`
|
| 93 |
+
* is applied to the id too so the slug stays lowercase/url-safe regardless of
|
| 94 |
+
* id casing (e.g. id "TIGER-Lab" → "tiger-lab").
|
| 95 |
+
*/
|
| 96 |
+
export function evaluatorSlugFor(name: string, orgMeta?: OrgMetaMap): string {
|
| 97 |
+
const id = orgMeta?.[normalizeOrgKey(name)]?.id
|
| 98 |
+
return id && id.trim() ? evaluatorSlug(id) : evaluatorSlug(name)
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
/** @deprecated internal alias — use {@link evaluatorSlug}. */
|
| 102 |
const baseSlug = evaluatorSlug
|
| 103 |
|
|
|
|
| 107 |
* order (name-sorted) so the same corpus always yields the same slugs;
|
| 108 |
* collisions get a numeric suffix (`-2`, `-3`, …).
|
| 109 |
*/
|
| 110 |
+
export function buildEvaluatorSlugMap(
|
| 111 |
+
evals: BenchmarkEvalListItem[],
|
| 112 |
+
orgMeta?: OrgMetaMap,
|
| 113 |
+
): {
|
| 114 |
slugToName: Map<string, string>
|
| 115 |
nameToSlug: Map<string, string>
|
| 116 |
} {
|
|
|
|
| 127 |
const used = new Map<string, number>()
|
| 128 |
|
| 129 |
for (const name of Array.from(names).sort((a, b) => a.localeCompare(b))) {
|
| 130 |
+
const base = evaluatorSlugFor(name, orgMeta)
|
| 131 |
const seen = used.get(base) ?? 0
|
| 132 |
const slug = seen === 0 ? base : `${base}-${seen + 1}`
|
| 133 |
used.set(base, seen + 1)
|
|
|
|
| 146 |
*/
|
| 147 |
export function groupEvalsByEvaluator(
|
| 148 |
evals: BenchmarkEvalListItem[],
|
| 149 |
+
opts?: { verifiedOnly?: boolean; orgMeta?: OrgMetaMap },
|
| 150 |
): EvaluatorGroup[] {
|
| 151 |
const verifiedOnly = opts?.verifiedOnly ?? false
|
| 152 |
+
const { nameToSlug } = buildEvaluatorSlugMap(evals, opts?.orgMeta)
|
| 153 |
|
| 154 |
const acc = new Map<string, { evalCount: number; verifiedCount: number; isVerified: boolean }>()
|
| 155 |
|
|
|
|
| 185 |
for (const [name, v] of acc) {
|
| 186 |
groups.push({
|
| 187 |
name,
|
| 188 |
+
slug: nameToSlug.get(name) ?? evaluatorSlugFor(name, opts?.orgMeta),
|
| 189 |
evalCount: v.evalCount,
|
| 190 |
verifiedCount: v.verifiedCount,
|
| 191 |
isVerified: v.isVerified,
|
|
|
|
| 205 |
export function getEvalsForEvaluator(
|
| 206 |
allEvals: BenchmarkEvalListItem[],
|
| 207 |
slug: string,
|
| 208 |
+
opts?: { verifiedOnly?: boolean; orgMeta?: OrgMetaMap },
|
| 209 |
): { name: string | null; isVerified: boolean; evals: BenchmarkEvalListItem[] } {
|
| 210 |
const verifiedOnly = opts?.verifiedOnly ?? false
|
| 211 |
+
const { slugToName } = buildEvaluatorSlugMap(allEvals, opts?.orgMeta)
|
| 212 |
// Direct hit on the (possibly collision-suffixed) full slug, else fall back
|
| 213 |
// to base-slug matching so links built from the shared `evaluatorSlug(name)`
|
| 214 |
// helper — which can't see the suffix step — still resolve. The fallback is
|
|
|
|
| 218 |
for (const [, candidate] of Array.from(slugToName.entries()).sort((a, b) =>
|
| 219 |
a[1].localeCompare(b[1]),
|
| 220 |
)) {
|
| 221 |
+
if (evaluatorSlugFor(candidate, opts?.orgMeta) === slug) {
|
| 222 |
name = candidate
|
| 223 |
break
|
| 224 |
}
|
lib/sidecars.ts
CHANGED
|
@@ -18,6 +18,8 @@ import type {
|
|
| 18 |
ComparisonIndex,
|
| 19 |
CorpusAggregates,
|
| 20 |
EvalHierarchy,
|
|
|
|
|
|
|
| 21 |
PeerRanksMap,
|
| 22 |
PeerRanksSidecar,
|
| 23 |
} from "@/lib/backend-artifacts"
|
|
@@ -35,6 +37,7 @@ let cache: {
|
|
| 35 |
hierarchy?: CacheSlot<EvalHierarchy>
|
| 36 |
comparisonIndex?: CacheSlot<ComparisonIndex>
|
| 37 |
peerRanks?: CacheSlot<PeerRanksMap>
|
|
|
|
| 38 |
} = {}
|
| 39 |
|
| 40 |
function getSnapshotUrl() {
|
|
@@ -378,6 +381,30 @@ export function fetchPeerRanks(): Promise<PeerRanksMap> {
|
|
| 378 |
)
|
| 379 |
}
|
| 380 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 381 |
/**
|
| 382 |
* Fail fast on contract regressions. Comparison-index rows must carry
|
| 383 |
* `family_id`; the model-page graph view collapses without it.
|
|
|
|
| 18 |
ComparisonIndex,
|
| 19 |
CorpusAggregates,
|
| 20 |
EvalHierarchy,
|
| 21 |
+
OrgMetadata,
|
| 22 |
+
OrgMetadataIndex,
|
| 23 |
PeerRanksMap,
|
| 24 |
PeerRanksSidecar,
|
| 25 |
} from "@/lib/backend-artifacts"
|
|
|
|
| 37 |
hierarchy?: CacheSlot<EvalHierarchy>
|
| 38 |
comparisonIndex?: CacheSlot<ComparisonIndex>
|
| 39 |
peerRanks?: CacheSlot<PeerRanksMap>
|
| 40 |
+
organizations?: CacheSlot<Record<string, OrgMetadata>>
|
| 41 |
} = {}
|
| 42 |
|
| 43 |
function getSnapshotUrl() {
|
|
|
|
| 381 |
)
|
| 382 |
}
|
| 383 |
|
| 384 |
+
/**
|
| 385 |
+
* Per-evaluator-org metadata (homepage URL + logo pointer) from
|
| 386 |
+
* `warehouse/<snapshot>/organizations.json`, sourced from the registry.
|
| 387 |
+
* Resolves to the bare `normalizedName -> OrgMetadata` map the evaluator page
|
| 388 |
+
* looks up by name.
|
| 389 |
+
*
|
| 390 |
+
* Returns an empty map when the snapshot doesn't carry the file (older pinned
|
| 391 |
+
* snapshots predate the Stage J `organizations` sidecar), logging a warning
|
| 392 |
+
* rather than throwing so the page still renders with monograms + no links.
|
| 393 |
+
*/
|
| 394 |
+
export function fetchOrganizations(): Promise<Record<string, OrgMetadata>> {
|
| 395 |
+
return getCachedValue("organizations", "organizations", (preferStale) =>
|
| 396 |
+
fetchJson<OrgMetadataIndex>("organizations.json", preferStale)
|
| 397 |
+
.then((payload) => payload?.orgs ?? {})
|
| 398 |
+
.catch((err) => {
|
| 399 |
+
console.warn(
|
| 400 |
+
`[sidecars] organizations.json not available on snapshot; ` +
|
| 401 |
+
`falling back to empty map. ${err instanceof Error ? err.message : String(err)}`,
|
| 402 |
+
)
|
| 403 |
+
return {} as Record<string, OrgMetadata>
|
| 404 |
+
}),
|
| 405 |
+
)
|
| 406 |
+
}
|
| 407 |
+
|
| 408 |
/**
|
| 409 |
* Fail fast on contract regressions. Comparison-index rows must carry
|
| 410 |
* `family_id`; the model-page graph view collapses without it.
|
public/org-logos/arcadia.webp
ADDED
|
Git LFS Details
|
public/org-logos/artificial-analysis.svg
ADDED
|
|
public/org-logos/exgentic.png
ADDED
|
Git LFS Details
|
public/org-logos/huggingface.svg
ADDED
|
|
public/org-logos/llm-stats.svg
ADDED
|
|
public/org-logos/mercor.png
ADDED
|
Git LFS Details
|
public/org-logos/si-anthropic.svg
ADDED
|
|
public/org-logos/si-kaggle.svg
ADDED
|
|
public/org-logos/stanford-crfm.png
ADDED
|
Git LFS Details
|
public/org-logos/writer.svg
ADDED
|
|