Skip to content
Elements

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.
fig. 01 · plays once, replay from the corner

Installation

Install manually
npx shadcn@latest add "@assistant-ui/elements-score-breakdown"
First time? Set up a runtime

Runtime components read their state from an assistant-ui runtime. Add one to an existing project:

npx assistant-ui@latest init

Then wrap your app in a runtime provider:

import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";

export default function App() {
  const runtime = useChatRuntime({
    transport: new AssistantChatTransport({ api: "/api/chat" }),
  });

  return (
    <AssistantRuntimeProvider runtime={runtime}>
      {/* your components */}
    </AssistantRuntimeProvider>
  );
}

The installation guide covers new projects, templates, and API routes.

A verdict backed by its own arithmetic: a total out of a max, a colored verdict pill, and the weighted criteria that produced it, each with its own bar and an optional note on why it scored the way it did. With a runtime the score comes from a tool result; standalone you pass the criteria and the total yourself.

Getting started

No assistant-ui primitive backs this element, so the runtime wiring is a tool renderer. A score usually comes from grading work, such as running tests or comparing against a rubric, so the criteria belong on the tool's result, not on its streamed args.

Register the render function

app/toolkit.tsx
"use client";

import { defineToolkit } from "@assistant-ui/react";
import { ScoreBreakdown } from "@/components/assistant-ui/elements/score-breakdown";

export const toolkit = defineToolkit({
  review_change: {
    type: "backend",
    render: ({ args, result }) => {
      if (!result) return <p>Reviewing {args.target}…</p>;
      return (
        <ScoreBreakdown
          verdict={result.verdict}
          total={result.total}
          outOf={result.outOf}
          criteria={result.criteria}
          visibleCount={result.criteria.length}
        />
      );
    },
  },
});

The whole breakdown arrives as one result, so visibleCount is result.criteria.length.

Let the message list render it

app/MyRuntimeProvider.tsx
import {
  AssistantRuntimeProvider,
  AuiConfig,
  Tools,
} from "@assistant-ui/react";
import { useChatRuntime } from "@assistant-ui/ai-sdk";
import { toolkit } from "./toolkit";

export function MyRuntimeProvider({
  children,
}: {
  children: React.ReactNode;
}) {
  const runtime = useChatRuntime();
  const config = AuiConfig({ tools: Tools({ toolkit }) });
  return (
    <AssistantRuntimeProvider runtime={runtime} config={config}>
      {children}
    </AssistantRuntimeProvider>
  );
}

Once the toolkit is registered, Thread and any message list built on MessagePrimitive.Parts or MessagePrimitive.GroupedParts render this UI wherever a review_change call appears in a message.

Anatomy

<div data-slot="score-breakdown">
  <div>
    <span>{/* total.toFixed(1) */}</span>
    <span>{/* "/ outOf" */}</span>
    <span>{/* verdict pill, colored by the total/outOf ratio */}</span>
  </div>

  <div>
    {/* one row per visible criterion */}
    <div>
      <span>{/* criterion.label */}</span>
      <span>{/* "×" + weight */}</span>
      <span>{/* criterion.score.toFixed(1) */}</span>
    </div>
    <span role="meter">{/* per-criterion bar, named from the criterion */}</span>
    <span>{/* optional criterion.note */}</span>
  </div>
</div>

The header (the total, the / outOf, and the verdict pill) renders whatever visibleCount is. Only the criteria list is sliced, with visibleCount floored and clamped into 0…criteria.length. The verdict pill's color is a plain threshold on total / outOf, treated as 0 when outOf is 0: emerald at 0.75 and above, amber from 0.5 up to just under 0.75, red below 0.5. Each criterion's bar is scaled against the overall outOf, so two criteria with the same score draw the same bar width whatever their weights. weight appears only as the ×N label next to the score. Each criterion bar's track is a named meter with a 0…100 value matching its score width and value text reading the same score of outOf the row prints; the value sits on the track rather than the fill, which collapses to nothing at zero.

Examples

An empty denominator

With outOf: 0 the component skips the division and treats the ratio as 0, so the verdict pill is red whatever total is.

<ScoreBreakdown
  verdict="not scored"
  total={0}
  outOf={0}
  criteria={[]}
  visibleCount={0}
/>

Where the score comes from

The backend entry declares only what is being reviewed. The model supplies target, your server runs the grading, and the client renderer receives the finished verdict as result.

app/api/chat/route.ts
review_change: tool({
  description: "Grade a proposed change against a rubric.",
  inputSchema: z.object({ target: z.string() }),
  execute: async ({ target }) => gradeChange(target),
}),

Restyle the breakdown

Both lanes take className on the root. The weight and score labels use the shared mono surface from surfaces.tsx, so restyling that token restyles every element that uses it.

<ScoreBreakdown className="max-w-none gap-4" /* ... */ />

API reference

Render props

PropTypeDescription
args{ target: string }The tool's arguments, naming what is being reviewed.
result{ verdict: string; total: number; outOf: number; criteria: ScoreCriterion[] } | undefinedThe finished verdict, undefined until the backend executor resolves.

review_change result

FieldTypeDescription
verdictstringShort label shown in the pill.
totalnumberThe overall score, shown to one decimal place.
outOfnumberThe maximum possible score, also the denominator every criterion's bar is scaled against.
criteria[].labelstringThe criterion's name.
criteria[].scorenumberThat criterion's own score, shown to one decimal place.
criteria[].weightnumberShown as ×weight, has no effect on the bar width.
criteria[].notestringOptional caption under the bar.