Elements

58 / 122 · Structured output

Score breakdown

A verdict with its arithmetic shown: criteria, weights, and what pulled it down.

4.1/ 5approve
Fixes the root cause×34.5
Clears the slot at the source rather than at the call site.

Installation

npx shadcn@latest add "@assistant-ui/elements-score-breakdown"

Usage

import { ScoreBreakdown } from "@/components/elements/score-breakdown";

<ScoreBreakdown
  verdict="approve"
  total={4.2}
  outOf={5}
  criteria={criteria}
  visibleCount={3}
/>

Props

ScoreBreakdown

verdict*stringThe decision in a word. Its tint follows total over outOf.
total*numberThe composite score.
outOf*numberMaximum possible, for the headline and for every criterion bar.
criteria*ScoreCriterion[]What produced the number, so the verdict can be argued with.
visibleCount*numberHow many criteria have been scored.
classNamestringExtra classes merged onto the root.

ScoreCriterion

label*stringWhat is being scored. Truncates rather than wraps.
score*numberScore for this criterion, on the same scale as outOf. Its bar is drawn against outOf, so the two cannot drift apart.
weight*numberHow much it counted toward the total.
notestringWhy it scored that way. Worth writing for anything below full marks.

Source

score-breakdown.tsx
"use client";

import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";

export interface ScoreCriterion {
  label: string;
  score: number;
  weight: number;
  note?: string;
}

export function ScoreBreakdown({
  verdict,
  total,
  outOf,
  criteria,
  visibleCount,
  className,
}: {
  verdict: string;
  total: number;
  outOf: number;
  criteria: readonly ScoreCriterion[];
  visibleCount: number;
  className?: string;
}) {
  const ratio = outOf === 0 ? 0 : total / outOf;

  return (
    <div
      className={cn(
        paper,
        "flex w-full max-w-sm flex-col gap-3 rounded-2xl p-4",
        className,
      )}
    >
      <div className="flex items-baseline gap-2">
        <span className="text-2xl font-medium tracking-tight tabular-nums">
          {total.toFixed(1)}
        </span>
        <span className={cn(mono, "text-foreground/30 tabular-nums")}>
          / {outOf}
        </span>
        <span
          className={cn(
            "ms-auto rounded-full px-2 py-0.5 text-xs font-medium",
            ratio >= 0.75
              ? "bg-emerald-500/12 text-emerald-700 dark:text-emerald-300"
              : ratio >= 0.5
                ? "bg-amber-500/12 text-amber-700 dark:text-amber-300"
                : "bg-red-500/12 text-red-700 dark:text-red-300",
          )}
        >
          {verdict}
        </span>
      </div>

      <div className="flex flex-col gap-2">
        {criteria.slice(0, visibleCount).map((criterion) => (
          <div
            key={criterion.label}
            className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex flex-col gap-1 duration-300"
          >
            <div className="flex items-baseline gap-2">
              <span className="text-foreground/75 min-w-0 flex-1 truncate text-[13px]">
                {criterion.label}
              </span>
              <span className={cn(mono, "text-foreground/25 shrink-0")}>
                ×{criterion.weight}
              </span>
              <span
                className={cn(mono, "text-foreground/55 shrink-0 tabular-nums")}
              >
                {criterion.score.toFixed(1)}
              </span>
            </div>
            <span className="bg-foreground/[0.06] h-[3px] w-full overflow-hidden rounded-full">
              <span
                className="block h-full rounded-full bg-blue-500 transition-[width] duration-500 motion-reduce:transition-none dark:bg-blue-400"
                style={{
                  width: `${Math.min(100, outOf === 0 ? 0 : (criterion.score / outOf) * 100)}%`,
                }}
              />
            </span>
            {criterion.note && (
              <span className="text-foreground/40 text-xs leading-relaxed">
                {criterion.note}
              </span>
            )}
          </div>
        ))}
      </div>
    </div>
  );
}