Result Panel
The live submission shell: stage list, score block, performance, test quality and measured complexity, plus the states before any of them exist.
npx shadcn@latest add @hextor/result-panelSource
"use client";
import Link from "next/link";
import { ArrowUpRight, Gauge, Loader2, ShieldAlert } from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { ComplexityPlot } from "@/registry/hextor/components/hextor/complexity-plot/complexity-plot";
import { PerfPanel } from "@/registry/hextor/components/hextor/report-sections/perf-panel";
import { ScoreBlock } from "@/registry/hextor/components/hextor/report-sections/score-block";
import { SectionTitle } from "@/registry/hextor/components/hextor/report-sections/section-title";
import { StageList } from "@/registry/hextor/components/hextor/report-sections/stage-list";
import { TestQualityPanel } from "@/registry/hextor/components/hextor/report-sections/test-quality-panel";
import {
resolveLabels,
type ResultPanelLabels,
} from "@/registry/hextor/components/hextor/result-panel/labels";
import type { SubmissionState } from "@/registry/hextor/components/hextor/result-panel/types";
/**
* The arena's live result pane.
*
* Every block below is shared with `/report/[submissionId]` via the
* `report-sections` blocks. This file is now only the *shell*: the streaming
* header, the empty state, and the order the blocks appear in as they arrive.
* The conventions that matter — a withheld axis rendering as an em-dash
* rather than a zero, `error` warning instead of failing red, missed mutants
* named as concepts — live in one place so the permanent report cannot drift
* from what the user watched happen live.
*/
export function ResultPanel({
state,
labels,
}: {
state: SubmissionState | null;
/** Overrides `defaultLabels` (Thai) per-section; pass nothing to keep them. */
labels?: Partial<ResultPanelLabels>;
}) {
const copy = resolveLabels(labels);
if (!state) return <EmptyState copy={copy} />;
return (
<div className="flex h-full flex-col">
<div className="flex items-center gap-2 border-b px-4 py-2.5">
<h2 className="text-sm font-medium">{copy.header.title}</h2>
{!state.done && (
<Loader2 className="size-3.5 animate-spin text-muted-foreground" />
)}
<code className="ml-auto text-[10px] text-muted-foreground">
{state.id}
</code>
</div>
<div className="min-h-0 flex-1 overflow-y-auto">
<div className="space-y-4 p-4">
{/* Our failure, said out loud and said first. `error` means the harness
broke, not that the code is wrong, and the one thing that must not
happen is a user reading a red result and spending an hour
debugging a solution that was never the problem. Warn colours, and
the first sentence is whose fault it is. */}
{state.outcome === "error" && <InfraFailure copy={copy} />}
{/* A wait we cannot justify with a spinner. See StreamNotice. */}
{state.notice && (
<NoticeCard
title={state.notice.messageTh}
detail={state.notice.detailTh}
spinning={!state.done}
/>
)}
<StageList stages={state.stages} />
{state.testQuality && (
<>
<Separator />
<TestQualityPanel result={state.testQuality} />
</>
)}
{state.perf && (
<>
<Separator />
<PerfPanel result={state.perf} />
</>
)}
{state.bigO && (
<>
<Separator />
<section className="space-y-2">
<SectionTitle>{copy.bigO.title}</SectionTitle>
<ComplexityPlot result={state.bigO} />
</section>
</>
)}
{state.scores && (
<>
<Separator />
<ScoreBlock scores={state.scores} />
</>
)}
{/* The permanent version. This pane dies the moment the user navigates
away or submits again; the report at this URL does not, and it is
what carries the share card. Only offered once the run has settled —
a half-finished report is not worth linking to. */}
{state.done && (
<>
<Separator />
<Link
href={`/report/${state.id}`}
className="flex items-center gap-1.5 text-xs font-medium underline decoration-dotted underline-offset-4 hover:decoration-solid"
>
{copy.fullReportLink}
<ArrowUpRight className="size-3.5" />
</Link>
</>
)}
</div>
</div>
</div>
);
}
/**
* The banner for `outcome: "error"`.
*
* Deliberately NOT the fail colour. `error` is the pipeline's own failure — a
* harness crash, a drained bench node, a sandbox that could not start — and the
* user has no action to take on their code. It offers the one action that can
* help (submit again) and says nothing about correctness, because nothing about
* correctness was established.
*/
function InfraFailure({ copy }: { copy: ResultPanelLabels }) {
return (
<div
className="flex gap-2 rounded-md border p-2.5 text-xs"
style={{
borderColor: "color-mix(in oklab, var(--verdict-warn) 45%, transparent)",
background: "color-mix(in oklab, var(--verdict-warn) 9%, transparent)",
}}
>
<ShieldAlert
className="mt-px size-4 shrink-0"
style={{ color: "var(--verdict-warn)" }}
/>
<div className="space-y-1">
<p className="font-medium" style={{ color: "var(--verdict-warn)" }}>
{copy.infraFailure.heading}
</p>
<p className="leading-relaxed text-muted-foreground">
{copy.infraFailure.body}
</p>
</div>
</div>
);
}
/** A wait, explained. Same warn convention: none of these are the user's doing. */
function NoticeCard({
title,
detail,
spinning,
}: {
title: string;
detail?: string;
spinning: boolean;
}) {
return (
<div
className="flex gap-2 rounded-md border p-2.5 text-xs"
style={{
borderColor: "color-mix(in oklab, var(--verdict-warn) 35%, transparent)",
background: "color-mix(in oklab, var(--verdict-warn) 6%, transparent)",
}}
>
{spinning ? (
<Loader2
className="mt-px size-3.5 shrink-0 animate-spin"
style={{ color: "var(--verdict-warn)" }}
/>
) : (
<ShieldAlert
className="mt-px size-3.5 shrink-0"
style={{ color: "var(--verdict-warn)" }}
/>
)}
<div className="space-y-0.5">
<p className="font-medium">{title}</p>
{detail && <p className="leading-relaxed text-muted-foreground">{detail}</p>}
</div>
</div>
);
}
function EmptyState({ copy }: { copy: ResultPanelLabels }) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 p-8 text-center">
<div className="rounded-full border border-dashed p-3">
<Gauge className="size-5 text-muted-foreground" />
</div>
<div className="space-y-1">
<p className="text-sm font-medium">{copy.emptyState.heading}</p>
<p className="max-w-[34ch] text-xs leading-relaxed text-muted-foreground">
{copy.emptyState.body.before}
<span className="font-medium text-foreground">
{copy.emptyState.body.emphasis}
</span>
{copy.emptyState.body.after}
</p>
</div>
</div>
);
}
Docs
The shell a submission streams into. It composes the already-shipped panels rather than reimplementing them, so a report page and a live run show the identical thing — two renderings of one submission that disagree are worse than one that is late.
Most of the file is the states people actually hit: nothing submitted yet, a stream that has degraded, and infrastructure that failed. That last one is deliberately distinct from a failed submission — an errored stage means OUR side broke, and telling someone their code failed when our runner did is the one message this panel must never send.
Installing this pulls @hextor/report-sections and @hextor/complexity-plot. TestQualityPanel inside it renders a <Tooltip>, so a <TooltipProvider> must be mounted somewhere in your app shell.