50 / Agent UI
Source Card
One retrieved passage: where it came from, what it said, and how well it matched.
Understanding Success Criterion 2.5.8: Target Size (Minimum)(opens in a new tab)
w3.org
The size of the target for pointer inputs is at least 24 by 24 CSS pixels, except where spacing, equivalents, or inline targets apply.
ARIA Authoring Practices: Combobox Pattern(opens in a new tab)
w3.org
A combobox is an input that controls another element, such as a listbox, that can dynamically pop up to help the user set its value.
"use client"
import { SourceCard } from "mischief-ui/source-card"
const results = [
{
id: "wcag",
title: "Understanding Success Criterion 2.5.8: Target Size (Minimum)",
url: "https://www.w3.org/WAI/WCAG22/Understanding/target-size-minimum.html",
snippet:
"The size of the target for pointer inputs is at least 24 by 24 CSS pixels, except where spacing, equivalents, or inline targets apply.",
score: 0.94,
},
{
id: "aria",
title: "ARIA Authoring Practices: Combobox Pattern",
url: "https://www.w3.org/WAI/ARIA/apg/patterns/combobox/",
snippet:
"A combobox is an input that controls another element, such as a listbox, that can dynamically pop up to help the user set its value.",
score: 0.71,
},
]
export function SourceCardDemo() {
return (
<div className="grid w-full max-w-xl gap-2">
{results.map((result, index) => (
<SourceCard
key={result.id}
index={index + 1}
title={result.title}
url={result.url}
snippet={result.snippet}
score={result.score}
/>
))}
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/source-cardimport { SourceCard } from "mischief-ui/source-card"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
import * as React from "react"import { ExternalLink } from "lucide-react" import { cn } from "@/lib/utils" export type SourceCardProps = Omit< React.HTMLAttributes<HTMLElement>, "title" | "children"> & { title: React.ReactNode /** Makes the title a link. Opens in a new tab. */ url?: string /** The retrieved passage. */ snippet?: React.ReactNodeUsage
export function Sources() {
return results.map((result, index) => (
<SourceCard
key={result.id}
index={index + 1}
title={result.title}
url={result.url}
snippet={result.text}
score={result.score}
/>
))
}About that percentage
score is a fraction from 0 to 1, clamped, and shown as a percentage. What it means is entirely your retriever's business: a cosine similarity, a reranker's output, and a BM25 score are three different quantities, and none of them is a probability that the answer is correct.
So show it only where the reader can act on it. A number that always reads between 80 and 90 percent teaches nobody anything, and a confident-looking percentage attached to a bad passage is worse than no number at all. Leave score out and the bar disappears.
Making a source checkable
The snippet should be the passage the claim actually rests on, not the opening of the document. The whole value of showing sources is that someone can check the claim in a second, and a first paragraph that happens to sit above the relevant text does not let them.
Where there is no url -- an internal document, a chunk from your own store -- the card still works and simply stops being a link. Give it a source in that case, since the host it would otherwise fall back to does not exist.
<SourceCard
index={1}
title="Refund policy, section 4"
source="Support handbook"
snippet="Refunds are issued to the original payment method within ten working days."
/>API
titleReactNodeThe heading. Becomes a link when a url is given.urlstringOpens in a new tab, with the host shown underneath.snippetReactNodeThe retrieved passage.sourceReactNodeWhere it came from. Falls back to the host of the url.indexnumberPosition in the result list, shown as a marker.scorenumberRelevance from 0 to 1, shown as a bar and a percentage.icon, footerReactNode, ReactNodeA leading mark, and a row beneath the passage.Accessibility
Each card is an article with a real heading, so a list of them can be navigated by heading. Links say they open in a new tab and carry rel=noreferrer noopener. The relevance bar is decoration with the percentage written beside it, so the score never depends on seeing the bar. A malformed url degrades to no host rather than throwing.