Elements

23 / 122 · Messages

Confidence

Which claims came from a source, which were inferred, and which are guesses.

The composer owns its draft from 0.14 onward. Most apps will not need the hydrate effect anymore. Removing it should cut a render on every thread switch.

Installation

npx shadcn@latest add "@assistant-ui/elements-confidence-marker"

Usage

import { ConfidenceMarker } from "@/components/elements/confidence-marker";

<ConfidenceMarker claims={claims} hoveredId={hoveredId} onHover={setHoveredId} />

Props

ConfidenceMarker

claims*ConfidenceClaim[]The answer split into claims. Each is underlined according to how well it is supported.
hoveredId*stringWhich claim is hovered. Pass an empty string for none.
onHover(id: string) => voidCalled with the hovered claim id, or an empty string on exit.
classNamestringExtra classes merged onto the root.

ConfidenceClaim

id*stringStable identity, compared against hoveredId.
text*stringThe claim, rendered inline with its confidence underline.
confidence*"grounded" | "inferred" | "uncertain"Drives the underline: solid green, solid amber, dotted red.
basis*stringWhat backs the claim, shown in the hover pill.

Source

confidence-marker.tsx
"use client";

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

export type Confidence = "grounded" | "inferred" | "uncertain";

export interface ConfidenceClaim {
  id: string;
  text: string;
  confidence: Confidence;
  basis: string;
}

const UNDERLINE: Record<Confidence, string> = {
  grounded: "decoration-emerald-500/50",
  inferred: "decoration-amber-500/60",
  uncertain: "decoration-red-500/50 decoration-dotted",
};

const LABEL: Record<Confidence, string> = {
  grounded: "from a source",
  inferred: "inferred",
  uncertain: "unverified",
};

export function ConfidenceMarker({
  claims,
  hoveredId,
  onHover,
  className,
}: {
  claims: readonly ConfidenceClaim[];
  hoveredId: string;
  onHover?: (id: string) => void;
  className?: string;
}) {
  const hovered = claims.find((claim) => claim.id === hoveredId);

  return (
    <div className={cn("flex w-full max-w-sm flex-col gap-2.5", className)}>
      <p className="text-[13.5px] leading-relaxed">
        {claims.map((claim) => (
          <span
            key={claim.id}
            onMouseEnter={() => onHover?.(claim.id)}
            onMouseLeave={() => onHover?.("")}
            className={cn(
              "underline decoration-2 underline-offset-[3px] transition-colors",
              UNDERLINE[claim.confidence],
              hoveredId === claim.id
                ? "text-foreground/95"
                : "text-foreground/70",
            )}
          >
            {claim.text}{" "}
          </span>
        ))}
      </p>

      <div className="flex h-9 items-start">
        {hovered && (
          <span
            className={cn(
              floating,
              mono,
              "fade-in zoom-in-95 animate-in text-foreground/55 flex items-center gap-1.5 rounded-full px-2.5 py-1.5 duration-150",
            )}
          >
            <span
              aria-hidden
              className={cn(
                "size-1.5 rounded-full",
                hovered.confidence === "grounded" && "bg-emerald-500",
                hovered.confidence === "inferred" && "bg-amber-500",
                hovered.confidence === "uncertain" && "bg-red-500",
              )}
            />
            {LABEL[hovered.confidence]} · {hovered.basis}
          </span>
        )}
      </div>
    </div>
  );
}