general-eval-card / components /flag-score-button.tsx
evijit's picture
evijit HF Staff
Cross-suite signals, sortable leaderboard, theme cleanup
0314721
Raw
History Blame
11.1 kB
"use client"
import { useMemo, useState, type ReactNode } from "react"
import {
Copy,
ExternalLink,
FileSearch,
Flag,
GitPullRequestArrow,
MessageSquare,
} from "lucide-react"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
interface FlagScoreButtonProps {
modelName: string
modelId: string
benchmarkName: string
benchmarkId?: string
score: number | string
/** URL to the published source the score was extracted from (paper, blog, leaderboard). */
sourceUrl?: string
/** URL to the processed record JSON in the card_backend HF dataset. */
sourceRecordUrl?: string
/** Optional explicit upstream record URL in evaleval/EEE_datastore (raw source of truth). */
eeeRecordUrl?: string
}
interface DatasetLinks {
repoSlug: string
recordViewUrl: string | null
recordRawUrl: string | null
discussionsUrl: string
newDiscussionUrl: string
}
/**
* Given a /resolve/main/... HF dataset URL, derive the helpful sibling URLs
* for the dataset (file viewer, discussions list, new-discussion form). Returns
* null when the input doesn't look like a HF dataset URL.
*/
function deriveDatasetLinks(recordUrl: string | undefined, prefilledTitle: string): DatasetLinks | null {
if (!recordUrl) return null
const m = recordUrl.match(
/^https:\/\/huggingface\.co\/datasets\/([^/]+\/[^/]+)\/(?:resolve|raw|blob)\/[^/]+\/(.*)$/,
)
if (!m) return null
const repoSlug = m[1]
const path = m[2]
const datasetBase = `https://huggingface.co/datasets/${repoSlug}`
const recordViewUrl = `${datasetBase}/blob/main/${path}`
const recordRawUrl = `${datasetBase}/resolve/main/${path}`
const discussionsUrl = `${datasetBase}/discussions`
const newDiscussionUrl = `${datasetBase}/discussions/new?title=${encodeURIComponent(prefilledTitle)}`
return { repoSlug, recordViewUrl, recordRawUrl, discussionsUrl, newDiscussionUrl }
}
/**
* "Flag this score" — researcher affordance that, instead of capturing the
* report ourselves, sends the user directly to the upstream HF dataset where
* the data lives. They can then file a discussion or submit a correction PR
* against the actual record. We also offer a copyable context snippet so the
* issue body has all the relevant identifiers without needing to retype them.
*/
export function FlagScoreButton({
modelName,
modelId,
benchmarkName,
benchmarkId,
score,
sourceUrl,
sourceRecordUrl,
eeeRecordUrl,
}: FlagScoreButtonProps) {
const [open, setOpen] = useState(false)
const [copied, setCopied] = useState<"context" | null>(null)
const prefilledTitle = `Possible issue: ${modelName} on ${benchmarkName} (score ${score})`
const cardBackendLinks = useMemo(
() => deriveDatasetLinks(sourceRecordUrl, prefilledTitle),
[sourceRecordUrl, prefilledTitle],
)
const eeeLinks = useMemo(
() => deriveDatasetLinks(eeeRecordUrl, prefilledTitle),
[eeeRecordUrl, prefilledTitle],
)
// Prefer the EEE upstream as the "correction venue" when known — that's
// where raw evaluation records live. Fall back to card_backend (the
// pipeline output) when EEE isn't directly addressable for this row.
const correctionLinks = eeeLinks ?? cardBackendLinks
const contextSnippet = [
`Model: ${modelName} (${modelId})`,
`Benchmark: ${benchmarkName}${benchmarkId ? ` (${benchmarkId})` : ""}`,
`Reported score: ${score}`,
sourceUrl ? `Original source URL: ${sourceUrl}` : null,
sourceRecordUrl ? `Pipeline record: ${sourceRecordUrl}` : null,
eeeRecordUrl ? `EEE upstream record: ${eeeRecordUrl}` : null,
]
.filter(Boolean)
.join("\n")
const handleCopyContext = async () => {
try {
await navigator.clipboard.writeText(contextSnippet)
setCopied("context")
setTimeout(() => setCopied(null), 1800)
} catch {
// Clipboard might be blocked; ignore — the user can still select text manually.
}
}
return (
<>
<button
type="button"
className="btn-ec outline inline-flex items-center gap-1.5"
style={{ fontSize: 11, padding: "4px 10px" }}
onClick={() => setOpen(true)}
>
<Flag className="h-3 w-3" />
Flag this score
</button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Flag this score</DialogTitle>
<DialogDescription>
Take this report directly to the dataset where the record lives. You can file a
discussion or open a correction PR against the actual file.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div
style={{
padding: "10px 12px",
border: "1px solid var(--border-soft)",
background: "var(--bg-warm)",
fontSize: 12,
}}
>
<div
className="font-mono uppercase"
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
>
Flagging
</div>
<div className="mt-1" style={{ color: "var(--fg)" }}>
<span className="font-semibold">{modelName}</span>{" "}
<span style={{ color: "var(--fg-muted)" }}>on</span>{" "}
<span className="font-semibold">{benchmarkName}</span>
</div>
<div
className="font-mono tabular-nums"
style={{ color: "var(--fg-muted)", fontSize: 12 }}
>
Score: {score}
</div>
</div>
<div className="space-y-2">
{correctionLinks?.recordViewUrl && (
<FlagAction
href={correctionLinks.recordViewUrl}
icon={<FileSearch className="h-4 w-4" style={{ color: "var(--fg-muted)" }} />}
title="View the underlying record"
detail={
<>
Opens the JSON file on{" "}
<span className="font-mono">{correctionLinks.repoSlug}</span>
</>
}
/>
)}
{correctionLinks && (
<FlagAction
href={correctionLinks.newDiscussionUrl}
icon={<MessageSquare className="h-4 w-4" style={{ color: "var(--fg-muted)" }} />}
title="Open a discussion"
detail="Pre-filled title; paste the context snippet into the body."
/>
)}
{correctionLinks && (
<FlagAction
href={correctionLinks.discussionsUrl}
icon={<GitPullRequestArrow className="h-4 w-4" style={{ color: "var(--fg-muted)" }} />}
title="Browse existing discussions"
detail="Check whether someone already filed a similar correction."
/>
)}
{!correctionLinks && (
<div
className="text-[12px]"
style={{
padding: "10px 12px",
border: "1px dashed var(--accent)",
background: "var(--bg-warm)",
color: "var(--accent)",
lineHeight: 1.6,
}}
>
No upstream record URL is recorded for this row, so we can't link directly to
the dataset. Copy the context below and file an issue at{" "}
<a
className="underline-offset-4 hover:underline"
href="https://huggingface.co/datasets/evaleval/EEE_datastore/discussions"
target="_blank"
rel="noreferrer"
style={{ color: "var(--accent)" }}
>
evaleval/EEE_datastore/discussions
</a>
.
</div>
)}
</div>
<div className="space-y-1.5">
<div className="flex items-center justify-between">
<label
className="font-mono uppercase"
style={{ fontSize: 10, letterSpacing: "0.14em", color: "var(--fg-subtle)" }}
>
Context to paste into the issue
</label>
<button
type="button"
onClick={handleCopyContext}
className="inline-flex items-center gap-1.5 font-mono uppercase underline-offset-4 hover:underline"
style={{ fontSize: 10, letterSpacing: "0.12em", color: "var(--accent)" }}
>
<Copy className="h-3 w-3" />
{copied === "context" ? "Copied" : "Copy"}
</button>
</div>
<pre
className="max-h-40 overflow-auto whitespace-pre-wrap break-all"
style={{
padding: 12,
fontFamily: "var(--font-mono)",
fontSize: 11,
lineHeight: 1.5,
border: "1px solid var(--border-soft)",
background: "var(--bg)",
color: "var(--fg-muted)",
}}
>
{contextSnippet}
</pre>
</div>
<div className="flex justify-end pt-1">
<button
type="button"
onClick={() => setOpen(false)}
className="font-mono uppercase underline-offset-4 hover:underline"
style={{ fontSize: 11, letterSpacing: "0.12em", color: "var(--fg-muted)" }}
>
Close
</button>
</div>
</div>
</DialogContent>
</Dialog>
</>
)
}
function FlagAction({
href,
icon,
title,
detail,
}: {
href: string
icon: ReactNode
title: string
detail: ReactNode
}) {
return (
<a
href={href}
target="_blank"
rel="noreferrer"
className="flex items-center justify-between transition-colors hover:bg-[color:var(--bg-warm)]"
style={{
padding: "10px 12px",
border: "1px solid var(--border-soft)",
background: "var(--bg)",
textDecoration: "none",
color: "var(--fg)",
}}
>
<div className="flex items-center gap-2 min-w-0">
<span className="shrink-0">{icon}</span>
<div className="min-w-0">
<div className="font-semibold text-[13px]" style={{ color: "var(--fg)" }}>{title}</div>
<div className="text-[11px]" style={{ color: "var(--fg-muted)", lineHeight: 1.5 }}>
{detail}
</div>
</div>
</div>
<ExternalLink className="h-3.5 w-3.5 shrink-0" style={{ color: "var(--fg-muted)" }} />
</a>
)
}