File size: 15,490 Bytes
3a12290
 
 
 
 
 
 
 
 
 
 
 
8058fce
 
3a12290
 
 
 
 
04b4cff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
 
 
 
 
 
 
 
 
 
04b4cff
c1f2130
04b4cff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436ada0
8058fce
 
bca888a
 
 
 
 
8058fce
 
 
 
 
 
 
3a12290
 
 
 
 
 
 
 
04b4cff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
 
 
03e2430
 
 
3a12290
04b4cff
3a12290
 
 
 
 
 
 
 
 
 
bca888a
3a12290
 
bca888a
3a12290
 
 
436ada0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
 
 
 
 
 
 
 
 
 
04b4cff
 
 
 
 
 
 
 
3a12290
 
 
c1f2130
 
3a12290
 
 
 
 
 
 
bca888a
 
3a12290
 
 
04b4cff
 
 
 
3a12290
 
8058fce
 
 
 
 
 
 
 
 
3a12290
 
 
c1f2130
3a12290
 
 
 
 
04b4cff
c1f2130
04b4cff
3a12290
 
 
 
04b4cff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3a12290
 
 
 
 
 
 
04b4cff
 
bca888a
3a12290
bca888a
3a12290
 
 
 
8058fce
 
 
 
 
 
 
 
 
3a12290
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8058fce
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
"use client"

import type { ComponentType, CSSProperties } from "react"
import { useAudienceMode } from "@/components/audience-mode-provider"
import { Badge } from "@/components/ui/badge"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { useRouter } from "next/navigation"
import {
  AlertTriangle,
  BadgeCheck,
  BookOpenText,
  ChartNoAxesColumn,
  Database,
  ExternalLink,
  FlaskConical,
  Scale,
  Users,
} from "lucide-react"
import type { BenchmarkEvalListItem } from "@/lib/eval-processing"
import { getCategoryColor } from "@/lib/benchmark-schema"

const LICENSE_COLORS: Record<string, string> = {
  "mit": "bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-200",
  "apache": "bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-950/40 dark:text-sky-200",
  "cc by": "bg-violet-100 text-violet-800 border-violet-200 dark:bg-violet-950/40 dark:text-violet-200",
  "cc0": "bg-teal-100 text-teal-800 border-teal-200 dark:bg-teal-950/40 dark:text-teal-200",
  "cc-by-sa": "bg-indigo-100 text-indigo-800 border-indigo-200 dark:bg-indigo-950/40 dark:text-indigo-200",
}

function licenseBadgeClass(license: string): string {
  const l = license.toLowerCase()
  for (const [key, cls] of Object.entries(LICENSE_COLORS)) {
    if (l.includes(key)) return cls
  }
  return "bg-muted text-muted-foreground border-border"
}

function shortenLicense(license: string): string {
  if (!license || license === "Not specified") return ""
  // Shorten known verbose license names
  if (license.toLowerCase().includes("creative commons attribution 4")) return "CC BY 4.0"
  if (license.toLowerCase().includes("creative commons zero")) return "CC0"
  if (license.toLowerCase().includes("apache license 2") || license.toLowerCase().includes("apache 2")) return "Apache 2.0"
  if (license.toLowerCase().includes("mit license")) return "MIT"
  if (license.toLowerCase().includes("cc-by-sa")) return "CC BY-SA"
  if (license.length > 24) return license.slice(0, 22) + "…"
  return license
}

interface EvalCardProps {
  summary: BenchmarkEvalListItem
  delayMs?: number
}

export function EvalCard({ summary, delayMs = 0 }: EvalCardProps) {
  const router = useRouter()
  const { mode } = useAudienceMode()
  const isResearchView = mode === "research"
  const scorePercent = `${Math.round(summary.avg_score_norm * 100)}%`
  const card = summary.benchmark_card
  const domains: string[] = summary.tags?.domains ?? card?.benchmark_details?.domains ?? []
  const license = card?.ethical_and_legal_considerations?.data_licensing ?? ""
  const shortLicense = shortenLicense(license)
  // Use the benchmark overview as a richer description when available
  const overviewText = card?.benchmark_details?.overview
  // Policy: rich context from metadata card
  const policyGoal = card?.purpose_and_intended_users?.goal
  const policyLimitations = card?.purpose_and_intended_users?.limitations
  const policyAudience = card?.purpose_and_intended_users?.audience
  const audienceText = Array.isArray(policyAudience)
    ? policyAudience.slice(0, 2).join("; ")
    : typeof policyAudience === "string"
    ? policyAudience
    : null
  // Research: score interpretation + similar benchmarks
  const scoreInterpretation = card?.methodology?.interpretation
  const rawSimilar = card?.benchmark_details?.similar_benchmarks
  const similarBenchmarks: string[] = Array.isArray(rawSimilar) ? rawSimilar : rawSimilar ? [rawSimilar] : []
  const domainPreview = domains.slice(0, 2)
  // Source provenance pulled from the pipeline's source_data
  const sourceData = summary.source_data
  const reproducibilitySummary = summary.reproducibility_summary
  const reproducibilityGapCount =
    reproducibilitySummary?.has_reproducibility_gap_count ?? summary.missing_generation_config_count
  const reproducibilityResultsTotal =
    reproducibilitySummary?.results_total ?? summary.models_count
  const datasetName = sourceData?.dataset_name
  const datasetUrl =
    sourceData?.dataset_url ??
    (Array.isArray(sourceData?.url) ? sourceData?.url?.[0] : sourceData?.url) ??
    (sourceData?.hf_repo ? `https://huggingface.co/datasets/${sourceData.hf_repo}` : undefined)
  const datasetVersion = sourceData?.dataset_version
  const sourceTypeLabel = sourceData?.source_type

  return (
    <Card
      className="motion-academic-enter motion-academic-surface motion-academic-hover cursor-pointer overflow-hidden border-border/70 bg-card hover:shadow-lg"
      style={{ "--enter-delay": `${delayMs}ms` } as CSSProperties}
      onClick={() => router.push(`/evals/${summary.evaluation_id}`)}
    >
      <CardHeader className="space-y-3 border-b border-border/60 pb-4">
        <div className="flex items-center justify-between gap-2">
          <div className="flex items-center gap-2">
            <div className="text-[10px] font-semibold uppercase tracking-[0.24em] text-muted-foreground">
              Single Benchmark
            </div>
            {summary.category && (
              <span className={`rounded-full border px-2 py-0.5 text-[10px] font-semibold ${getCategoryColor(summary.category)}`}>
                {summary.category}
              </span>
            )}
          </div>
          {shortLicense && (
            <span className={`rounded-full border px-2 py-0.5 text-[10px] font-semibold ${licenseBadgeClass(license)}`}>
              {shortLicense}
            </span>
          )}
        </div>

        <div className="min-w-0">
          <div className="text-xl font-bold">{summary.evaluation_name}</div>
          <div className="mt-1 text-xs font-medium uppercase tracking-[0.18em] text-muted-foreground">
            Composite benchmark: {summary.composite_benchmark_name}
          </div>
          <div className="mt-1 text-sm text-muted-foreground line-clamp-2">
            {overviewText ?? summary.metric_config.evaluation_description}
          </div>
        </div>

        <div className="flex flex-wrap gap-2">
          {summary.third_party_ratio > 0 && (
            <Badge className="bg-emerald-600 text-white hover:bg-emerald-600">
              <BadgeCheck className="mr-1 h-3 w-3" />
              Independently evaluated
            </Badge>
          )}
          {reproducibilityGapCount > 0 && (
            <Badge className="bg-amber-500 text-amber-950 hover:bg-amber-500">
              <AlertTriangle className="mr-1 h-3 w-3" />
              Documentation gaps
            </Badge>
          )}
        </div>

        {domains.length > 0 && (
          <div className="flex flex-wrap items-center gap-2 text-[11px] text-muted-foreground">
            <span className="font-semibold uppercase tracking-[0.16em]">Domain coverage</span>
            {domainPreview.map((domain) => (
              <span
                key={domain}
                className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-medium capitalize text-muted-foreground"
              >
                {domain}
              </span>
            ))}
            {domains.length > domainPreview.length && <span>+{domains.length - domainPreview.length} more</span>}
          </div>
        )}
      </CardHeader>

      <CardContent className="space-y-4 pt-4">
        {isResearchView ? (
          <>
            <div className="grid gap-2 sm:grid-cols-3">
              <MetricPill icon={FlaskConical} label="Models" value={summary.models_count.toLocaleString()} tone="bg-sky-100/80 text-sky-900 dark:bg-sky-950/40 dark:text-sky-100" />
              <MetricPill icon={ChartNoAxesColumn} label="Avg Score" value={scorePercent} tone="bg-amber-100/80 text-amber-900 dark:bg-amber-950/40 dark:text-amber-100" />
              <MetricPill icon={Users} label="Evaluators" value={summary.evaluator_names.length.toLocaleString()} tone="bg-emerald-100/80 text-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-100" />
            </div>

            {/* Research: score interpretation from metadata */}
            {scoreInterpretation && (
              <div className="rounded-xl border border-sky-200/60 bg-sky-50/40 p-3 text-sm dark:border-sky-900/40 dark:bg-sky-950/10">
                <div className="mb-1 text-[10px] font-semibold uppercase tracking-[0.18em] text-sky-700 dark:text-sky-300">Score interpretation</div>
                <p className="text-muted-foreground line-clamp-2">{scoreInterpretation}</p>
              </div>
            )}

            <div className="rounded-xl border bg-muted/10 p-3">
              <div className="mb-2 text-xs font-semibold uppercase tracking-wider text-muted-foreground">Methodology</div>
              <div className="space-y-1.5 text-sm">
                {summary.tags?.tasks && summary.tags.tasks.length > 0 && (
                  <DataRow label="Tasks" value={summary.tags.tasks.join(", ")} />
                )}
                {summary.latest_source_name && (
                  <DataRow label="Source" value={summary.latest_source_name} />
                )}
                <DataRow
                  label="Config"
                  value={
                    reproducibilityGapCount > 0
                      ? `${reproducibilityGapCount} of ${reproducibilityResultsTotal} scores have setup gaps`
                      : "Fully documented"
                  }
                />
                <DataRow label="Third-party" value={`${Math.round(summary.third_party_ratio * 100)}%`} />
                {similarBenchmarks.length > 0 && (
                  <DataRow label="See also" value={similarBenchmarks.slice(0, 3).join(", ")} />
                )}
              </div>
            </div>

            {(datasetName || datasetUrl || sourceTypeLabel) && (
              <ProvenanceRow
                datasetName={datasetName}
                datasetUrl={datasetUrl}
                datasetVersion={datasetVersion}
                sourceType={sourceTypeLabel}
              />
            )}
          </>
        ) : (
          <>
            {/* Policy: goal from metadata card */}
            <div className="rounded-xl border border-amber-200/70 bg-amber-50/75 p-3 dark:border-amber-900/40 dark:bg-amber-950/15">
              <div className="flex items-start gap-2">
                <Scale className="mt-0.5 h-4 w-4 shrink-0 text-amber-600" />
                <div>
                  <div className="text-sm font-semibold">Purpose</div>
                  <div className="text-sm text-muted-foreground line-clamp-3">
                    {policyGoal ?? "General single-benchmark evaluation"}
                  </div>
                </div>
              </div>
            </div>

            {/* Policy: audience + limitations from metadata */}
            {(audienceText || policyLimitations) && (
              <div className="space-y-2">
                {audienceText && (
                  <div className="rounded-xl border border-sky-200/60 bg-sky-50/50 p-3 text-sm dark:border-sky-900/40 dark:bg-sky-950/15">
                    <span className="font-semibold text-sky-800 dark:text-sky-200">Intended for: </span>
                    <span className="text-muted-foreground">{audienceText}</span>
                  </div>
                )}
                {policyLimitations && (
                  <div className="rounded-xl border border-rose-200/60 bg-rose-50/50 p-3 text-sm dark:border-rose-900/40 dark:bg-rose-950/15">
                    <span className="font-semibold text-rose-800 dark:text-rose-200">Known limitation: </span>
                    <span className="text-muted-foreground line-clamp-2">{policyLimitations}</span>
                  </div>
                )}
              </div>
            )}

            <div className="grid gap-2 sm:grid-cols-2">
              <MetricPill icon={BadgeCheck} label="Independent" value={`${Math.round(summary.third_party_ratio * 100)}%`} tone="bg-emerald-100/80 text-emerald-900 dark:bg-emerald-950/40 dark:text-emerald-100" />
              <MetricPill icon={BookOpenText} label="Models" value={summary.models_count.toLocaleString()} tone="bg-sky-100/80 text-sky-900 dark:bg-sky-950/40 dark:text-sky-100" />
            </div>

            <div className="rounded-xl border bg-muted/10 p-3">
              <div className="space-y-1.5 text-sm">
                <DataRow label="Avg score" value={scorePercent} />
                <DataRow label="Reported by" value={summary.evaluator_names.join(", ") || "Unknown"} />
                {reproducibilityGapCount > 0 && (
                  <p className="pt-1 text-xs text-muted-foreground">
                    {reproducibilityGapCount} of {reproducibilityResultsTotal} reported scores are not fully documented.
                  </p>
                )}
              </div>
            </div>

            {(datasetName || datasetUrl || sourceTypeLabel) && (
              <ProvenanceRow
                datasetName={datasetName}
                datasetUrl={datasetUrl}
                datasetVersion={datasetVersion}
                sourceType={sourceTypeLabel}
              />
            )}
          </>
        )}
      </CardContent>
    </Card>
  )
}

function MetricPill({
  icon: Icon,
  label,
  value,
  tone,
}: {
  icon: ComponentType<{ className?: string }>
  label: string
  value: string
  tone: string
}) {
  return (
    <div className={`flex items-center justify-between rounded-xl px-3 py-2 ${tone}`}>
      <div className="flex items-center gap-2">
        <Icon className="h-4 w-4" />
        <span className="text-[11px] font-semibold uppercase tracking-[0.18em] opacity-80">{label}</span>
      </div>
      <span className="text-sm font-bold">{value}</span>
    </div>
  )
}

function DataRow({ label, value }: { label: string; value: string }) {
  return (
    <div className="flex items-start justify-between gap-3">
      <span className="shrink-0 text-muted-foreground">{label}</span>
      <span className="text-right font-medium text-foreground">{value}</span>
    </div>
  )
}

function ProvenanceRow({
  datasetName,
  datasetUrl,
  datasetVersion,
  sourceType,
}: {
  datasetName?: string
  datasetUrl?: string
  datasetVersion?: string
  sourceType?: string
}) {
  return (
    <div className="rounded-xl border border-border/60 bg-background/60 p-3">
      <div className="mb-2 flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.18em] text-muted-foreground">
        <Database className="h-3 w-3" />
        Source dataset
      </div>
      <div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-sm">
        {datasetName && (
          <span className="font-medium text-foreground">
            {datasetName}
            {datasetVersion ? <span className="text-muted-foreground"> · v{datasetVersion}</span> : null}
          </span>
        )}
        {sourceType && (
          <span className="rounded-full border border-border/60 bg-muted/30 px-2 py-0.5 text-[10px] font-medium uppercase tracking-[0.14em] text-muted-foreground">
            {sourceType.replace(/_/g, " ")}
          </span>
        )}
        {datasetUrl && (
          <a
            href={datasetUrl}
            target="_blank"
            rel="noopener noreferrer"
            onClick={(event) => event.stopPropagation()}
            className="ml-auto inline-flex items-center gap-1 text-xs font-medium text-primary hover:underline"
          >
            View source
            <ExternalLink className="h-3 w-3" />
          </a>
        )}
      </div>
    </div>
  )
}