58 / 122 · Structured output
Score breakdown
A verdict with its arithmetic shown: criteria, weights, and what pulled it down.
4.1/ 5approve
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>
);
}