HextorUI
block

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-panel

Source

"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.

Dependencies

lucide-reactseparator@hextor/complexity-plot@hextor/report-sections